MOSAIC_GIT_IDENTITY=fargo produced objects attributed to mos-dt-0: every write wrapper resolved its acting principal from tea's login list, which enumerates whatever logins the host happens to hold and knows nothing about which seat is calling. The identity-aware code was present and correct but unreachable on the happy path — it sat on arms that only ran when tea failed. One shared resolver, not twenty patches: resolve_gitea_principal() in detect-platform.sh implements the precedence (explicit --login beats MOSAIC_GIT_IDENTITY / worktree git config mosaic.gitIdentity; the tea login list is the LAST resort), fails loud (nonzero, naming the identity or login and the expected slot path) when the requested principal has no credential, and never prints a token value. gitea_identity_token_slot() is the single source of truth for the slot layout, shared with get_gitea_token, so resolver and token resolution cannot disagree. Call-site conversions (the proving five): pr-review.sh (principal resolved once for every action; the comment action now honors --login), issue-comment.sh, pr-create.sh (identity mode reaches the REST API on the HAPPY path — tea is never consulted, so the login list cannot shadow the identity; --login wins even on the tea-failure fallback arm), issue-create.sh (same), pr-merge.sh (gains --login; --dry-run reports the principal the merge WOULD act as, resolved exactly as the merge resolves it; no cross-principal fallback — an identity-bound 401 is a hard stop). Remaining wrappers are call-site conversions onto the same resolver, measured: write-path issue-assign, issue-close, issue-edit, issue-reopen, milestone-close, milestone-create, pr-close; read-path issue-list, milestone-list, pr-list, pr-view (issue-view mixed). pr-diff, pr-metadata, pr-ci-wait and ci-queue-wait already inherit identity-first resolution via get_gitea_token. Known interaction: on a host with a workstation-GLOBAL mosaic.gitIdentity, this fix activates identity mode for every seat that has not set a local one — correct behavior driven by a wrong configuration (measured: #1282-#1287, six accidental live issues, closed with provenance by fred). Set mosaic.gitIdentity per-worktree, never --global. Tests: test-gitea-principal-resolution.sh (resolver matrix — identity present/absent, --login precedence, env vs git-config, unrecognized-host containment, slot-path-by-path-never-by-value); test-pr-create-identity- first.sh (the load-bearing ordering test: identity arm REACHED on the happy path with tea never invoked, fail-loud BEFORE any write on a missing slot, --login wins, default preserved); test-pr-merge-principal- resolution.sh (dry-run truthfulness, merge credential binding, unknown --login never reaches the provider). All wired into test:framework-shell. Sabotage control: precedence inverted to tea-list-first inside the resolver -> exactly the three new suites redden with the #1280 signatures (identity resolves to the tea-list account; missing slot returns rc=0 with silent fallthrough) while all 11 pre-existing git suites stay green; restored byte-identical (sha256 verified); all 14 green again.
222 lines
7.6 KiB
Bash
Executable File
222 lines
7.6 KiB
Bash
Executable File
#!/bin/bash
|
|
# issue-create.sh - Create issues on Gitea or GitHub
|
|
# Usage: issue-create.sh -t "Title" [-b "Body"] [-l "label1,label2"] [-m "milestone"] [--login <name>]
|
|
#
|
|
# Acting principal is resolved identity-first (#1280): an explicit --login
|
|
# wins; otherwise MOSAIC_GIT_IDENTITY / per-worktree git config
|
|
# mosaic.gitIdentity selects the principal when a per-slot token exists (and
|
|
# the wrapper then creates the issue through the REST API with that identity's
|
|
# token — tea is never invoked, so the tea login list cannot shadow the
|
|
# requested principal); the tea login list is the LAST resort. A requested
|
|
# identity with no per-slot token fails LOUD rather than writing under
|
|
# whichever account tea happens to hold.
|
|
|
|
set -e
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
source "$SCRIPT_DIR/detect-platform.sh"
|
|
|
|
# Default values
|
|
TITLE=""
|
|
BODY=""
|
|
LABELS=""
|
|
MILESTONE=""
|
|
INTERACTIVE=false
|
|
|
|
# get_remote_host and get_gitea_token are provided by detect-platform.sh
|
|
|
|
# Acting-principal mode set in the Gitea branch below (from
|
|
# resolve_gitea_principal): "login" when --login was given, "identity" when a
|
|
# git identity bound, "default" otherwise. PRINCIPAL_MODE=login makes the API
|
|
# arm resolve the --login principal's token too, so an explicit --login keeps
|
|
# winning even on the tea-FAILURE fallback arm.
|
|
PRINCIPAL_MODE=""
|
|
PRINCIPAL_NAME=""
|
|
|
|
gitea_issue_create_api() {
|
|
local host repo token url payload
|
|
host=$(get_remote_host) || {
|
|
echo "Error: could not determine remote host for API fallback" >&2
|
|
return 1
|
|
}
|
|
repo=$(get_repo_info) || {
|
|
echo "Error: could not determine repo owner/name for API fallback" >&2
|
|
return 1
|
|
}
|
|
if [[ "$PRINCIPAL_MODE" == "login" ]]; then
|
|
token=$(get_gitea_token_for_login "$PRINCIPAL_NAME" "$host") || {
|
|
echo "Error: could not resolve a host-matched Gitea token for --login '$PRINCIPAL_NAME' on host '$host' (API path)" >&2
|
|
return 1
|
|
}
|
|
else
|
|
# Identity-first when MOSAIC_GIT_IDENTITY / git config mosaic.gitIdentity
|
|
# is set (per-slot token, fail-loud on absence); shared default otherwise.
|
|
token=$(get_gitea_token "$host") || {
|
|
echo "Error: Gitea token not found for API fallback (set GITEA_TOKEN or configure ~/.git-credentials)" >&2
|
|
return 1
|
|
}
|
|
fi
|
|
|
|
if [[ -n "$LABELS" || -n "$MILESTONE" ]]; then
|
|
echo "Warning: API fallback currently applies title/body only; labels/milestone require authenticated tea setup." >&2
|
|
fi
|
|
|
|
payload=$(TITLE="$TITLE" BODY="$BODY" python3 - <<'PY'
|
|
import json
|
|
import os
|
|
|
|
payload = {"title": os.environ["TITLE"]}
|
|
body = os.environ.get("BODY", "")
|
|
if body:
|
|
payload["body"] = body
|
|
print(json.dumps(payload))
|
|
PY
|
|
)
|
|
|
|
url="https://${host}/api/v1/repos/${repo}/issues"
|
|
curl -fsS -X POST \
|
|
-H "User-Agent: curl/8" \
|
|
-H "Authorization: token ${token}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$payload" \
|
|
"$url"
|
|
}
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: $(basename "$0") [OPTIONS]
|
|
|
|
Create an issue on the current repository (Gitea or GitHub).
|
|
|
|
Options:
|
|
-t, --title TITLE Issue title (required)
|
|
-b, --body BODY Issue body/description
|
|
-l, --labels LABELS Comma-separated labels (e.g., "bug,feature")
|
|
-m, --milestone NAME Milestone name to assign
|
|
--login NAME Act as this Gitea tea login (wins over MOSAIC_GIT_IDENTITY)
|
|
-i, --interactive Prompt for missing issue fields
|
|
-h, --help Show this help message
|
|
|
|
Examples:
|
|
$(basename "$0") -t "Fix login bug" -l "bug,priority-high"
|
|
$(basename "$0") -t "Add dark mode" -b "Implement theme switching" -m "0.2.0"
|
|
$(basename "$0") -i
|
|
EOF
|
|
exit "${1:-1}"
|
|
}
|
|
|
|
# Parse arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
-t|--title)
|
|
TITLE="$2"
|
|
shift 2
|
|
;;
|
|
-b|--body)
|
|
BODY="$2"
|
|
shift 2
|
|
;;
|
|
-l|--labels)
|
|
LABELS="$2"
|
|
shift 2
|
|
;;
|
|
-m|--milestone)
|
|
MILESTONE="$2"
|
|
shift 2
|
|
;;
|
|
--login)
|
|
LOGIN_OVERRIDE="$2"
|
|
shift 2
|
|
;;
|
|
-i|--interactive)
|
|
INTERACTIVE=true
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage 0
|
|
;;
|
|
*)
|
|
echo "Unknown option: $1" >&2
|
|
usage
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ "$INTERACTIVE" == true ]]; then
|
|
[[ -n "$TITLE" ]] || read -r -p "Issue title: " TITLE
|
|
[[ -n "$BODY" ]] || read -r -p "Issue body (optional): " BODY || true
|
|
[[ -n "$LABELS" ]] || read -r -p "Labels, comma-separated (optional): " LABELS || true
|
|
[[ -n "$MILESTONE" ]] || read -r -p "Milestone (optional): " MILESTONE || true
|
|
fi
|
|
|
|
if [[ -z "$TITLE" ]]; then
|
|
echo "Error: Title is required (-t)" >&2
|
|
usage
|
|
fi
|
|
|
|
PLATFORM=$(detect_platform)
|
|
|
|
case "$PLATFORM" in
|
|
github)
|
|
CMD=(gh issue create --title "$TITLE")
|
|
[[ -n "$BODY" ]] && CMD+=(--body "$BODY")
|
|
[[ -n "$LABELS" ]] && CMD+=(--label "$LABELS")
|
|
[[ -n "$MILESTONE" ]] && CMD+=(--milestone "$MILESTONE")
|
|
"${CMD[@]}"
|
|
;;
|
|
gitea)
|
|
# Resolve the acting principal identity-first (#1280). The tea login
|
|
# list is the LAST resort: it knows nothing about which seat is calling,
|
|
# and a login resolved from it first is what attributed issues to the
|
|
# wrong account even when MOSAIC_GIT_IDENTITY was set.
|
|
principal_host=$(get_remote_host 2>/dev/null || true)
|
|
if ! principal_resolved="$(resolve_gitea_principal "${LOGIN_OVERRIDE:-}" "$principal_host")"; then
|
|
# resolve_gitea_principal already printed the fail-loud diagnostic.
|
|
exit 1
|
|
fi
|
|
PRINCIPAL_MODE="$(printf '%s' "$principal_resolved" | cut -f1)"
|
|
PRINCIPAL_NAME="$(printf '%s' "$principal_resolved" | cut -f2)"
|
|
|
|
if [[ "$PRINCIPAL_MODE" == "identity" ]]; then
|
|
# HAPPY PATH for a requested identity: create through the REST API
|
|
# with the per-slot token and never invoke tea — the identity arm
|
|
# must be REACHED, not sit behind a tea failure (#1280).
|
|
gitea_issue_create_api
|
|
exit $?
|
|
fi
|
|
|
|
if command -v tea >/dev/null 2>&1; then
|
|
REPO_SLUG=$(get_repo_slug)
|
|
if [[ "$PRINCIPAL_MODE" == "login" ]]; then
|
|
GITEA_LOGIN_NAME="$PRINCIPAL_NAME"
|
|
else
|
|
GITEA_LOGIN_NAME=$(get_gitea_login) || {
|
|
echo "Warning: could not resolve Gitea login for tea; trying Gitea API fallback..." >&2
|
|
gitea_issue_create_api
|
|
exit $?
|
|
}
|
|
fi
|
|
if ! get_gitea_authenticated_user "$GITEA_LOGIN_NAME" >/dev/null; then
|
|
echo "Warning: Tea authenticated-user validation failed (possible stale user/login); trying Gitea API fallback..." >&2
|
|
gitea_issue_create_api
|
|
exit $?
|
|
fi
|
|
REPO_ARGS=(--repo "$REPO_SLUG" --login "$GITEA_LOGIN_NAME")
|
|
CMD=(tea issue create "${REPO_ARGS[@]}" --title "$TITLE")
|
|
[[ -n "$BODY" ]] && CMD+=(--description "$BODY")
|
|
[[ -n "$LABELS" ]] && CMD+=(--labels "$LABELS")
|
|
# tea accepts milestone by name directly (verified 2026-02-05)
|
|
[[ -n "$MILESTONE" ]] && CMD+=(--milestone "$MILESTONE")
|
|
if "${CMD[@]}"; then
|
|
exit 0
|
|
fi
|
|
echo "Warning: tea issue create failed, trying Gitea API fallback..." >&2
|
|
fi
|
|
gitea_issue_create_api
|
|
;;
|
|
*)
|
|
echo "Error: Could not detect git platform" >&2
|
|
exit 1
|
|
;;
|
|
esac
|