diff --git a/docs/scratchpads/536-wrapper-login-pin.md b/docs/scratchpads/536-wrapper-login-pin.md new file mode 100644 index 0000000..f260b2b --- /dev/null +++ b/docs/scratchpads/536-wrapper-login-pin.md @@ -0,0 +1,45 @@ +# Issue 536 Wrapper Login Pin Scratchpad + +## Metadata + +- Date: 2026-06-12 +- Worktree: `/home/hermes/agent-work/536-wrapper-audit` +- Branch: `fix/536-wrapper-login-pin` +- Coordinator: `mos-claude` +- Issue: `mosaicstack/stack#536` +- Scope: Audit and fix Gitea git wrappers that hardcode or incorrectly inherit tea login/instance selection. + +## Objective + +Fix the framework git wrappers so Gitea issue/PR operations resolve the tea login from the target repository host instead of pinning `mosaicstack`. The fix must cover the class of bug across `packages/mosaic/framework/tools/git/`, not only `issue-close.sh`. + +## Acceptance Criteria + +1. `issue-close.sh` no longer uses `--login mosaicstack` for non-mosaic hosts. +2. All wrappers in `packages/mosaic/framework/tools/git/` avoid hardcoded Gitea login fallback where host-specific resolution is available. +3. Host-specific resolution works for `git.mosaicstack.dev` and `git.uscllc.com` using configured credentials / tea login data. +4. Read-only verification runs against both Gitea instances where possible. +5. Queue guard passes before push, PR is opened referencing #536, and merge is left to the coordinator. + +## Progress Log + +- Read required Mosaic hard-gate docs and coordinator briefing. +- Read issue #536 via Gitea API with mosaicstack credentials. +- Initial audit found hardcoded `${GITEA_LOGIN:-mosaicstack}` in issue and PR wrappers, plus shared `get_gitea_repo_args`. +- Added host-aware Gitea login resolution in `detect-platform.sh`, including exact host matching for `tea login list` entries and HTTPS remotes with embedded credentials. +- Updated Gitea issue, PR, milestone, and CI wrappers to use resolved host-specific tea login arguments instead of defaulting to `mosaicstack`. +- Added authenticated API fallbacks for close/reopen paths so wrappers can still operate when a matching `tea` login is absent but token credentials are available. +- Added regression coverage for stale `GITEA_LOGIN`, exact host matching, `--repo` override flows, USC issue close routing, mosaicstack API fallback, and PR metadata/merge fallbacks. + +## Verification + +- `bash -n packages/mosaic/framework/tools/git/*.sh` +- `packages/mosaic/framework/tools/git/test-gitea-login-resolution.sh` +- `packages/mosaic/framework/tools/git/test-pr-metadata-gitea.sh` +- `packages/mosaic/framework/tools/git/test-pr-merge-gitea-empty-uid.sh` +- `pnpm typecheck` +- `pnpm lint` +- `pnpm format:check` +- Live read-only: direct Gitea API read of `mosaicstack/stack#536` with `User-Agent: curl/8`. +- Live read-only: USC temporary repo remote to `https://git.uscllc.com/USC/uconnect.git`; `issue-list.sh -n 1` resolved the USC login and returned USC issues. +- Independent Codex review final verdict: approve, no findings. diff --git a/packages/mosaic/framework/tools/git/ci-queue-wait.sh b/packages/mosaic/framework/tools/git/ci-queue-wait.sh index 266577b..7fb42d3 100755 --- a/packages/mosaic/framework/tools/git/ci-queue-wait.sh +++ b/packages/mosaic/framework/tools/git/ci-queue-wait.sh @@ -137,7 +137,7 @@ gitea_get_branch_head_sha() { local branch="$3" local token="$4" local url="https://${host}/api/v1/repos/${repo}/branches/${branch}" - curl -fsSL -H "Authorization: token ${token}" "$url" | python3 -c ' + curl -fsSL -H "User-Agent: curl/8" -H "Authorization: token ${token}" "$url" | python3 -c ' import json, sys data = json.load(sys.stdin) commit = data.get("commit") or {} @@ -151,7 +151,7 @@ gitea_get_commit_status_json() { local sha="$3" local token="$4" local url="https://${host}/api/v1/repos/${repo}/commits/${sha}/status" - curl -fsSL -H "Authorization: token ${token}" "$url" + curl -fsSL -H "User-Agent: curl/8" -H "Authorization: token ${token}" "$url" } while [[ $# -gt 0 ]]; do diff --git a/packages/mosaic/framework/tools/git/detect-platform.sh b/packages/mosaic/framework/tools/git/detect-platform.sh index df3dff5..6835d56 100755 --- a/packages/mosaic/framework/tools/git/detect-platform.sh +++ b/packages/mosaic/framework/tools/git/detect-platform.sh @@ -78,10 +78,186 @@ get_repo_slug() { get_repo_info } +gitea_url_matches_host() { + local url="${1:-}" host="${2:-}" + [[ -n "$url" && -n "$host" ]] || return 1 + [[ "${url%/}" == "https://$host" || "${url%/}" == "http://$host" || "${url%/}" == *"//$host" ]] +} + +get_gitea_service_for_host() { + local host="$1" + local cred_file="${MOSAIC_CREDENTIALS_FILE:-$HOME/src/jarvis-brain/credentials.json}" + + case "$host" in + git.mosaicstack.dev) + echo "mosaicstack" + return 0 + ;; + git.uscllc.com) + echo "usc" + return 0 + ;; + esac + + [[ -f "$cred_file" ]] || return 1 + command -v jq >/dev/null 2>&1 || return 1 + + jq -r --arg host "$host" ' + .gitea // {} + | to_entries[] + | select((.value.url // "" | sub("/+$"; "")) | test("https?://" + $host + "$")) + | .key + ' "$cred_file" | head -n 1 +} + +find_tea_login_for_host() { + local host="$1" + local logins_json + + command -v tea >/dev/null 2>&1 || return 1 + logins_json=$(tea login list --output json 2>/dev/null) || return 1 + TEA_LOGINS_JSON="$logins_json" python3 - "$host" <<'PY' +import json +import os +import sys +from urllib.parse import urlparse + +host = sys.argv[1] +try: + logins = json.loads(os.environ.get("TEA_LOGINS_JSON", "[]")) +except Exception: + raise SystemExit(1) + +for login in logins if isinstance(logins, list) else []: + url = str(login.get("url") or login.get("URL") or "") + name = str(login.get("name") or login.get("Name") or "") + parsed = urlparse(url) + if parsed.hostname == host and name: + print(name) + raise SystemExit(0) + +raise SystemExit(1) +PY +} + +tea_login_matches_host() { + local login_name="$1" host="$2" + local logins_json + + command -v tea >/dev/null 2>&1 || return 1 + logins_json=$(tea login list --output json 2>/dev/null) || return 1 + TEA_LOGINS_JSON="$logins_json" python3 - "$login_name" "$host" <<'PY' +import json +import os +import sys +from urllib.parse import urlparse + +login_name, host = sys.argv[1], sys.argv[2] +try: + logins = json.loads(os.environ.get("TEA_LOGINS_JSON", "[]")) +except Exception: + raise SystemExit(1) + +for login in logins if isinstance(logins, list) else []: + url = str(login.get("url") or login.get("URL") or "") + name = str(login.get("name") or login.get("Name") or "") + parsed = urlparse(url) + if name == login_name and parsed.hostname == host: + raise SystemExit(0) + +raise SystemExit(1) +PY +} + +get_gitea_login_for_host() { + local host="${1:-}" + local login + + if [[ -z "$host" ]]; then + host=$(get_remote_host) || return 1 + fi + + if [[ -n "${GITEA_LOGIN:-}" ]]; then + if gitea_url_matches_host "${GITEA_URL:-}" "$host" || tea_login_matches_host "$GITEA_LOGIN" "$host"; then + echo "$GITEA_LOGIN" + return 0 + fi + fi + + login=$(find_tea_login_for_host "$host" || true) + if [[ -n "$login" ]]; then + echo "$login" + return 0 + fi + + return 1 +} + +get_default_tea_login() { + local logins_json + + command -v tea >/dev/null 2>&1 || return 1 + logins_json=$(tea login list --output json 2>/dev/null) || return 1 + TEA_LOGINS_JSON="$logins_json" python3 - <<'PY' +import json +import os + +try: + logins = json.loads(os.environ.get("TEA_LOGINS_JSON", "[]")) +except Exception: + raise SystemExit(1) + +if not isinstance(logins, list) or not logins: + raise SystemExit(1) + +for login in logins: + if not isinstance(login, dict): + continue + is_default = str(login.get("default") or login.get("Default") or "").lower() + name = str(login.get("name") or login.get("Name") or "") + if name and is_default == "true": + print(name) + raise SystemExit(0) + +for login in logins: + if not isinstance(login, dict): + continue + name = str(login.get("name") or login.get("Name") or "") + if name: + print(name) + raise SystemExit(0) + +raise SystemExit(1) +PY +} + +get_gitea_login_for_repo_override() { + local login + + if [[ -n "${GITEA_LOGIN:-}" ]]; then + echo "$GITEA_LOGIN" + return 0 + fi + + login=$(get_default_tea_login || true) + if [[ -n "$login" ]]; then + echo "$login" + return 0 + fi + + return 1 +} + get_gitea_repo_args() { - local repo + local repo host login repo=$(get_repo_slug) || return 1 - printf -- '--repo %q --login %q' "$repo" "${GITEA_LOGIN:-mosaicstack}" + host=$(get_remote_host) || return 1 + login=$(get_gitea_login_for_host "$host") || return 1 + printf -- '--repo %q --login %q' "$repo" "$login" +} + +get_gitea_login() { + get_gitea_login_for_host "$(get_remote_host)" } get_remote_host() { @@ -91,7 +267,8 @@ get_remote_host() { return 1 fi if [[ "$remote_url" =~ ^https?://([^/]+)/ ]]; then - echo "${BASH_REMATCH[1]}" + local host="${BASH_REMATCH[1]}" + echo "${host##*@}" return 0 fi if [[ "$remote_url" =~ ^git@([^:]+): ]]; then diff --git a/packages/mosaic/framework/tools/git/issue-assign.sh b/packages/mosaic/framework/tools/git/issue-assign.sh index 941f22a..f3a15cb 100755 --- a/packages/mosaic/framework/tools/git/issue-assign.sh +++ b/packages/mosaic/framework/tools/git/issue-assign.sh @@ -98,7 +98,11 @@ case "$PLATFORM" in ;; gitea) # tea issue edit syntax - CMD="tea issue edit $ISSUE" + REPO_ARGS=$(get_gitea_repo_args) || { + echo "Error: Could not resolve Gitea repo/login args for remote host" >&2 + exit 1 + } + CMD="tea issue edit $ISSUE $REPO_ARGS" NEEDS_EDIT=false if [[ -n "$ASSIGNEE" ]]; then @@ -112,7 +116,7 @@ case "$PLATFORM" in NEEDS_EDIT=true fi if [[ -n "$MILESTONE" ]]; then - MILESTONE_ID=$(tea milestones list 2>/dev/null | grep -E "^\s*[0-9]+" | grep "$MILESTONE" | awk '{print $1}' | head -1) + MILESTONE_ID=$(tea milestones list $REPO_ARGS 2>/dev/null | grep -E "^\s*[0-9]+" | grep "$MILESTONE" | awk '{print $1}' | head -1) if [[ -n "$MILESTONE_ID" ]]; then CMD="$CMD --milestone $MILESTONE_ID" NEEDS_EDIT=true diff --git a/packages/mosaic/framework/tools/git/issue-close.sh b/packages/mosaic/framework/tools/git/issue-close.sh index b773357..646c8b0 100755 --- a/packages/mosaic/framework/tools/git/issue-close.sh +++ b/packages/mosaic/framework/tools/git/issue-close.sh @@ -44,10 +44,43 @@ if [[ -z "$ISSUE_NUMBER" ]]; then fi # Detect platform and close issue -detect_platform +detect_platform >/dev/null OWNER=$(get_repo_owner) REPO=$(get_repo_name) +gitea_issue_comment_api() { + local host token url payload + host=$(get_remote_host) || return 1 + token=$(get_gitea_token "$host") || return 1 + url="https://${host}/api/v1/repos/${OWNER}/${REPO}/issues/${ISSUE_NUMBER}/comments" + payload=$(COMMENT="$COMMENT" python3 - <<'PY' +import json +import os + +print(json.dumps({"body": os.environ["COMMENT"]})) +PY +) + curl -fsS -X POST \ + -H "User-Agent: curl/8" \ + -H "Authorization: token ${token}" \ + -H "Content-Type: application/json" \ + -d "$payload" \ + "$url" >/dev/null +} + +gitea_issue_close_api() { + local host token url + host=$(get_remote_host) || return 1 + token=$(get_gitea_token "$host") || return 1 + url="https://${host}/api/v1/repos/${OWNER}/${REPO}/issues/${ISSUE_NUMBER}" + curl -fsS -X PATCH \ + -H "User-Agent: curl/8" \ + -H "Authorization: token ${token}" \ + -H "Content-Type: application/json" \ + -d '{"state":"closed"}' \ + "$url" >/dev/null +} + if [[ "$PLATFORM" == "github" ]]; then if [[ -n "$COMMENT" ]]; then gh issue comment "$ISSUE_NUMBER" --body "$COMMENT" @@ -55,10 +88,19 @@ if [[ "$PLATFORM" == "github" ]]; then gh issue close "$ISSUE_NUMBER" echo "Closed GitHub issue #$ISSUE_NUMBER" elif [[ "$PLATFORM" == "gitea" ]]; then - if [[ -n "$COMMENT" ]]; then - tea issue comment "$ISSUE_NUMBER" "$COMMENT" --repo "$OWNER/$REPO" --login "${GITEA_LOGIN:-mosaicstack}" + GITEA_LOGIN_NAME=$(get_gitea_login || true) + if [[ -n "$GITEA_LOGIN_NAME" ]]; then + if [[ -n "$COMMENT" ]]; then + tea issue comment "$ISSUE_NUMBER" "$COMMENT" --repo "$OWNER/$REPO" --login "$GITEA_LOGIN_NAME" + fi + tea issue close "$ISSUE_NUMBER" --repo "$OWNER/$REPO" --login "$GITEA_LOGIN_NAME" + else + echo "No tea login configured for $(get_remote_host); using authenticated Gitea API fallback." >&2 + if [[ -n "$COMMENT" ]]; then + gitea_issue_comment_api + fi + gitea_issue_close_api fi - tea issue close "$ISSUE_NUMBER" --repo "$OWNER/$REPO" --login "${GITEA_LOGIN:-mosaicstack}" echo "Closed Gitea issue #$ISSUE_NUMBER" else echo "Error: Unknown platform" diff --git a/packages/mosaic/framework/tools/git/issue-comment.sh b/packages/mosaic/framework/tools/git/issue-comment.sh index b1cc4ad..70f4a42 100755 --- a/packages/mosaic/framework/tools/git/issue-comment.sh +++ b/packages/mosaic/framework/tools/git/issue-comment.sh @@ -47,7 +47,7 @@ if [[ -z "$COMMENT" ]]; then exit 1 fi -detect_platform +detect_platform >/dev/null if [[ "$PLATFORM" == "github" ]]; then gh issue comment "$ISSUE_NUMBER" --body "$COMMENT" diff --git a/packages/mosaic/framework/tools/git/issue-create.sh b/packages/mosaic/framework/tools/git/issue-create.sh index 6fd1799..d516ed0 100755 --- a/packages/mosaic/framework/tools/git/issue-create.sh +++ b/packages/mosaic/framework/tools/git/issue-create.sh @@ -48,6 +48,7 @@ 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" \ @@ -121,7 +122,12 @@ case "$PLATFORM" in gitea) if command -v tea >/dev/null 2>&1; then REPO_SLUG=$(get_repo_slug) - REPO_ARGS=(--repo "$REPO_SLUG" --login "${GITEA_LOGIN:-mosaicstack}") + 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 $? + } + 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") diff --git a/packages/mosaic/framework/tools/git/issue-edit.sh b/packages/mosaic/framework/tools/git/issue-edit.sh index 70d57c9..0ffcc0b 100755 --- a/packages/mosaic/framework/tools/git/issue-edit.sh +++ b/packages/mosaic/framework/tools/git/issue-edit.sh @@ -60,7 +60,7 @@ if [[ -z "$ISSUE_NUMBER" ]]; then exit 1 fi -detect_platform +detect_platform >/dev/null if [[ "$PLATFORM" == "github" ]]; then CMD="gh issue edit $ISSUE_NUMBER" @@ -71,7 +71,11 @@ if [[ "$PLATFORM" == "github" ]]; then eval $CMD echo "Updated GitHub issue #$ISSUE_NUMBER" elif [[ "$PLATFORM" == "gitea" ]]; then - CMD="tea issue edit $ISSUE_NUMBER" + REPO_ARGS=$(get_gitea_repo_args) || { + echo "Error: Could not resolve Gitea repo/login args for remote host" >&2 + exit 1 + } + CMD="tea issue edit $ISSUE_NUMBER $REPO_ARGS" [[ -n "$TITLE" ]] && CMD="$CMD --title \"$TITLE\"" [[ -n "$BODY" ]] && CMD="$CMD --description \"$BODY\"" [[ -n "$LABELS" ]] && CMD="$CMD --add-labels \"$LABELS\"" diff --git a/packages/mosaic/framework/tools/git/issue-list.sh b/packages/mosaic/framework/tools/git/issue-list.sh index b162592..dd7b73d 100755 --- a/packages/mosaic/framework/tools/git/issue-list.sh +++ b/packages/mosaic/framework/tools/git/issue-list.sh @@ -98,7 +98,18 @@ case "$PLATFORM" in "${CMD[@]}" ;; gitea) - CMD=(tea issues list --repo "$REPO_INFO" --login "${GITEA_LOGIN:-mosaicstack}" --state "$STATE" --limit "$LIMIT") + if [[ -n "$REPO_OVERRIDE" ]]; then + GITEA_LOGIN_NAME=$(get_gitea_login_for_repo_override) || { + echo "Error: Could not resolve Gitea login for --repo override. Set GITEA_LOGIN or configure a default tea login." >&2 + exit 1 + } + else + GITEA_LOGIN_NAME=$(get_gitea_login) || { + echo "Error: Could not resolve Gitea login for remote host" >&2 + exit 1 + } + fi + CMD=(tea issues list --repo "$REPO_INFO" --login "$GITEA_LOGIN_NAME" --state "$STATE" --limit "$LIMIT") [[ -n "$LABEL" ]] && CMD+=(--labels "$LABEL") [[ -n "$MILESTONE" ]] && CMD+=(--milestones "$MILESTONE") # Note: tea may not support assignee filter directly in all versions. diff --git a/packages/mosaic/framework/tools/git/issue-reopen.sh b/packages/mosaic/framework/tools/git/issue-reopen.sh index 734fe34..cb92acd 100755 --- a/packages/mosaic/framework/tools/git/issue-reopen.sh +++ b/packages/mosaic/framework/tools/git/issue-reopen.sh @@ -42,7 +42,42 @@ if [[ -z "$ISSUE_NUMBER" ]]; then exit 1 fi -detect_platform +detect_platform >/dev/null +OWNER=$(get_repo_owner) +REPO=$(get_repo_name) + +gitea_issue_comment_api() { + local host token url payload + host=$(get_remote_host) || return 1 + token=$(get_gitea_token "$host") || return 1 + url="https://${host}/api/v1/repos/${OWNER}/${REPO}/issues/${ISSUE_NUMBER}/comments" + payload=$(COMMENT="$COMMENT" python3 - <<'PY' +import json +import os + +print(json.dumps({"body": os.environ["COMMENT"]})) +PY +) + curl -fsS -X POST \ + -H "User-Agent: curl/8" \ + -H "Authorization: token ${token}" \ + -H "Content-Type: application/json" \ + -d "$payload" \ + "$url" >/dev/null +} + +gitea_issue_reopen_api() { + local host token url + host=$(get_remote_host) || return 1 + token=$(get_gitea_token "$host") || return 1 + url="https://${host}/api/v1/repos/${OWNER}/${REPO}/issues/${ISSUE_NUMBER}" + curl -fsS -X PATCH \ + -H "User-Agent: curl/8" \ + -H "Authorization: token ${token}" \ + -H "Content-Type: application/json" \ + -d '{"state":"open"}' \ + "$url" >/dev/null +} if [[ "$PLATFORM" == "github" ]]; then if [[ -n "$COMMENT" ]]; then @@ -51,10 +86,19 @@ if [[ "$PLATFORM" == "github" ]]; then gh issue reopen "$ISSUE_NUMBER" echo "Reopened GitHub issue #$ISSUE_NUMBER" elif [[ "$PLATFORM" == "gitea" ]]; then - if [[ -n "$COMMENT" ]]; then - tea issue comment "$ISSUE_NUMBER" "$COMMENT" $(get_gitea_repo_args) + REPO_ARGS=$(get_gitea_repo_args || true) + if [[ -n "$REPO_ARGS" ]]; then + if [[ -n "$COMMENT" ]]; then + tea issue comment "$ISSUE_NUMBER" "$COMMENT" $REPO_ARGS + fi + tea issue reopen "$ISSUE_NUMBER" $REPO_ARGS + else + echo "No tea login configured for $(get_remote_host); using authenticated Gitea API fallback." >&2 + if [[ -n "$COMMENT" ]]; then + gitea_issue_comment_api + fi + gitea_issue_reopen_api fi - tea issue reopen "$ISSUE_NUMBER" $(get_gitea_repo_args) echo "Reopened Gitea issue #$ISSUE_NUMBER" else echo "Error: Unknown platform" diff --git a/packages/mosaic/framework/tools/git/issue-view.sh b/packages/mosaic/framework/tools/git/issue-view.sh index 18460e5..eccab98 100755 --- a/packages/mosaic/framework/tools/git/issue-view.sh +++ b/packages/mosaic/framework/tools/git/issue-view.sh @@ -29,9 +29,9 @@ gitea_issue_view_api() { url="https://${host}/api/v1/repos/${repo}/issues/${ISSUE_NUMBER}" if command -v python3 >/dev/null 2>&1; then - curl -fsS -H "Authorization: token ${token}" "$url" | python3 -m json.tool + curl -fsS -H "User-Agent: curl/8" -H "Authorization: token ${token}" "$url" | python3 -m json.tool else - curl -fsS -H "Authorization: token ${token}" "$url" + curl -fsS -H "User-Agent: curl/8" -H "Authorization: token ${token}" "$url" fi } @@ -61,7 +61,7 @@ if [[ -z "$ISSUE_NUMBER" ]]; then exit 1 fi -detect_platform +detect_platform >/dev/null if [[ "$PLATFORM" == "github" ]]; then gh issue view "$ISSUE_NUMBER" diff --git a/packages/mosaic/framework/tools/git/milestone-close.sh b/packages/mosaic/framework/tools/git/milestone-close.sh index ac7ad89..9d5a9f7 100755 --- a/packages/mosaic/framework/tools/git/milestone-close.sh +++ b/packages/mosaic/framework/tools/git/milestone-close.sh @@ -36,7 +36,7 @@ if [[ -z "$TITLE" ]]; then exit 1 fi -detect_platform +detect_platform >/dev/null if [[ "$PLATFORM" == "github" ]]; then gh api -X PATCH "/repos/{owner}/{repo}/milestones/$(gh api "/repos/{owner}/{repo}/milestones" --jq ".[] | select(.title==\"$TITLE\") | .number")" -f state=closed diff --git a/packages/mosaic/framework/tools/git/milestone-list.sh b/packages/mosaic/framework/tools/git/milestone-list.sh index e9b8656..c28b0eb 100755 --- a/packages/mosaic/framework/tools/git/milestone-list.sh +++ b/packages/mosaic/framework/tools/git/milestone-list.sh @@ -31,7 +31,7 @@ while [[ $# -gt 0 ]]; do esac done -detect_platform +detect_platform >/dev/null if [[ "$PLATFORM" == "github" ]]; then gh api "/repos/{owner}/{repo}/milestones?state=$STATE" --jq '.[] | "\(.title) (\(.state)) - \(.open_issues) open, \(.closed_issues) closed"' diff --git a/packages/mosaic/framework/tools/git/pr-ci-wait.sh b/packages/mosaic/framework/tools/git/pr-ci-wait.sh index 50c8b21..d9f0a6c 100755 --- a/packages/mosaic/framework/tools/git/pr-ci-wait.sh +++ b/packages/mosaic/framework/tools/git/pr-ci-wait.sh @@ -124,7 +124,7 @@ gitea_get_pr_head_sha() { local repo="$2" local token="$3" local url="https://${host}/api/v1/repos/${repo}/pulls/${PR_NUMBER}" - curl -fsSL -H "Authorization: token ${token}" "$url" | python3 -c ' + curl -fsSL -H "User-Agent: curl/8" -H "Authorization: token ${token}" "$url" | python3 -c ' import json, sys data = json.load(sys.stdin) print((data.get("head") or {}).get("sha", "")) @@ -137,7 +137,7 @@ gitea_get_commit_status_json() { local token="$3" local sha="$4" local url="https://${host}/api/v1/repos/${repo}/commits/${sha}/status" - curl -fsSL -H "Authorization: token ${token}" "$url" + curl -fsSL -H "User-Agent: curl/8" -H "Authorization: token ${token}" "$url" } while [[ $# -gt 0 ]]; do diff --git a/packages/mosaic/framework/tools/git/pr-close.sh b/packages/mosaic/framework/tools/git/pr-close.sh index afdfd3e..9fcb00f 100755 --- a/packages/mosaic/framework/tools/git/pr-close.sh +++ b/packages/mosaic/framework/tools/git/pr-close.sh @@ -42,7 +42,7 @@ if [[ -z "$PR_NUMBER" ]]; then exit 1 fi -detect_platform +detect_platform >/dev/null if [[ "$PLATFORM" == "github" ]]; then if [[ -n "$COMMENT" ]]; then diff --git a/packages/mosaic/framework/tools/git/pr-create.sh b/packages/mosaic/framework/tools/git/pr-create.sh index 30747d8..60997e6 100755 --- a/packages/mosaic/framework/tools/git/pr-create.sh +++ b/packages/mosaic/framework/tools/git/pr-create.sh @@ -56,6 +56,7 @@ PY url="https://${host}/api/v1/repos/${repo}/pulls" curl -fsS -X POST \ + -H "User-Agent: curl/8" \ -H "Authorization: token ${token}" \ -H "Content-Type: application/json" \ -d "$payload" \ @@ -177,7 +178,12 @@ case "$PLATFORM" in # is unreliable in Mosaic worktrees/profile shells. Use arrays instead # of eval so markdown backticks/body content are not shell-executed. REPO_SLUG=$(get_repo_slug) - REPO_ARGS=(--repo "$REPO_SLUG" --login "${GITEA_LOGIN:-mosaicstack}") + GITEA_LOGIN_NAME=$(get_gitea_login) || { + echo "Warning: could not resolve Gitea login for tea; trying Gitea API fallback..." >&2 + gitea_pr_create_api + exit $? + } + REPO_ARGS=(--repo "$REPO_SLUG" --login "$GITEA_LOGIN_NAME") CMD=(tea pr create "${REPO_ARGS[@]}" --title "$TITLE") [[ -n "$BODY" ]] && CMD+=(--description "$BODY") [[ -n "$BASE_BRANCH" ]] && CMD+=(--base "$BASE_BRANCH") diff --git a/packages/mosaic/framework/tools/git/pr-diff.sh b/packages/mosaic/framework/tools/git/pr-diff.sh index 2b2cfc7..34792d9 100755 --- a/packages/mosaic/framework/tools/git/pr-diff.sh +++ b/packages/mosaic/framework/tools/git/pr-diff.sh @@ -76,9 +76,9 @@ elif [[ "$PLATFORM" == "gitea" ]]; then GITEA_API_TOKEN=$(get_gitea_token "$HOST" || true) if [[ -n "$GITEA_API_TOKEN" ]]; then - DIFF_CONTENT=$(curl -sS -H "Authorization: token $GITEA_API_TOKEN" "$DIFF_URL") + DIFF_CONTENT=$(curl -sS -H "User-Agent: curl/8" -H "Authorization: token $GITEA_API_TOKEN" "$DIFF_URL") else - DIFF_CONTENT=$(curl -sS "$DIFF_URL") + DIFF_CONTENT=$(curl -sS -H "User-Agent: curl/8" "$DIFF_URL") fi if [[ -n "$OUTPUT_FILE" ]]; then diff --git a/packages/mosaic/framework/tools/git/pr-list.sh b/packages/mosaic/framework/tools/git/pr-list.sh index 7f0719b..0b923d2 100755 --- a/packages/mosaic/framework/tools/git/pr-list.sh +++ b/packages/mosaic/framework/tools/git/pr-list.sh @@ -93,7 +93,18 @@ case "$PLATFORM" in "${CMD[@]}" ;; gitea) - CMD=(tea pr list --repo "$REPO_INFO" --login "${GITEA_LOGIN:-mosaicstack}" --state "$STATE" --limit "$LIMIT") + if [[ -n "$REPO_OVERRIDE" ]]; then + GITEA_LOGIN_NAME=$(get_gitea_login_for_repo_override) || { + echo "Error: Could not resolve Gitea login for --repo override. Set GITEA_LOGIN or configure a default tea login." >&2 + exit 1 + } + else + GITEA_LOGIN_NAME=$(get_gitea_login) || { + echo "Error: Could not resolve Gitea login for remote host" >&2 + exit 1 + } + fi + CMD=(tea pr list --repo "$REPO_INFO" --login "$GITEA_LOGIN_NAME" --state "$STATE" --limit "$LIMIT") # tea filtering may be limited if [[ -n "$LABEL" ]]; then diff --git a/packages/mosaic/framework/tools/git/pr-merge.sh b/packages/mosaic/framework/tools/git/pr-merge.sh index 3c48b08..be658a8 100755 --- a/packages/mosaic/framework/tools/git/pr-merge.sh +++ b/packages/mosaic/framework/tools/git/pr-merge.sh @@ -106,34 +106,6 @@ PLATFORM=$(detect_platform) OWNER=$(get_repo_owner) REPO=$(get_repo_name) -find_tea_login_for_host() { - local host="$1" - local logins_json - - command -v tea >/dev/null 2>&1 || return 1 - logins_json=$(tea login list --output json 2>/dev/null) || return 1 - TEA_LOGINS_JSON="$logins_json" python3 - "$host" <<'PY' -import json -import os -import sys - -host = sys.argv[1] -try: - logins = json.loads(os.environ.get("TEA_LOGINS_JSON", "[]")) -except Exception: - raise SystemExit(1) - -for login in logins if isinstance(logins, list) else []: - url = str(login.get("url") or login.get("URL") or "") - name = str(login.get("name") or login.get("Name") or "") - if url.rstrip("/").endswith(host) and name: - print(name) - raise SystemExit(0) - -raise SystemExit(1) -PY -} - is_known_tea_empty_identity_failure() { local error_file="$1" @@ -164,6 +136,7 @@ merge_gitea_with_api() { if [[ -n "$token" ]]; then raw_code=$(curl -sS -w '%{http_code}' -o "$body_file" \ -X POST \ + -H "User-Agent: curl/8" \ -H "Authorization: token $token" \ -H 'Content-Type: application/json' \ -d "$payload" \ @@ -179,6 +152,7 @@ merge_gitea_with_api() { raw_code=$(curl -sS -w '%{http_code}' -o "$body_file" \ -X POST \ -u "$basic_auth" \ + -H "User-Agent: curl/8" \ -H 'Content-Type: application/json' \ -d "$payload" \ "$api_url" || true) @@ -214,7 +188,7 @@ if [[ "$DRY_RUN" == true ]]; then echo "Error: Cannot determine host from origin remote URL" >&2 exit 1 } - TEA_LOGIN="${GITEA_LOGIN:-$(find_tea_login_for_host "$HOST" || true)}" + TEA_LOGIN="$(get_gitea_login_for_host "$HOST" || true)" if [[ -n "$TEA_LOGIN" ]]; then echo "Dry run: would merge PR #$PR_NUMBER on $HOST with tea login '$TEA_LOGIN' (base=$BASE_BRANCH, method=squash)." else @@ -237,7 +211,7 @@ case "$PLATFORM" in echo "Error: Cannot determine host from origin remote URL" >&2 exit 1 } - TEA_LOGIN="${GITEA_LOGIN:-$(find_tea_login_for_host "$HOST" || true)}" + TEA_LOGIN="$(get_gitea_login_for_host "$HOST" || true)" if [[ -n "$TEA_LOGIN" ]]; then mkdir -p "${AGENT_WORK_ROOT:-/home/hermes/agent-work}" diff --git a/packages/mosaic/framework/tools/git/pr-metadata.sh b/packages/mosaic/framework/tools/git/pr-metadata.sh index 40fc0d2..b6373bf 100755 --- a/packages/mosaic/framework/tools/git/pr-metadata.sh +++ b/packages/mosaic/framework/tools/git/pr-metadata.sh @@ -59,7 +59,7 @@ curl_gitea_pull() { token=$(get_gitea_token "$HOST" || true) if [[ -n "$token" ]]; then - raw_code=$(curl -sS -w '%{http_code}' -o "$body_file" -H "Authorization: token $token" "$api_url" || true) + raw_code=$(curl -sS -w '%{http_code}' -o "$body_file" -H "User-Agent: curl/8" -H "Authorization: token $token" "$api_url" || true) if [[ "$raw_code" =~ ^2 ]]; then cat "$body_file" rm -f "$body_file" @@ -70,7 +70,7 @@ curl_gitea_pull() { basic_auth=$(get_gitea_basic_auth "$HOST" || true) if [[ -n "$basic_auth" ]]; then - raw_code=$(curl -sS -w '%{http_code}' -o "$body_file" -u "$basic_auth" "$api_url" || true) + raw_code=$(curl -sS -w '%{http_code}' -o "$body_file" -u "$basic_auth" -H "User-Agent: curl/8" "$api_url" || true) if [[ "$raw_code" =~ ^2 ]]; then cat "$body_file" rm -f "$body_file" @@ -80,7 +80,7 @@ curl_gitea_pull() { fi if [[ -z "${http_code:-}" ]]; then - raw_code=$(curl -sS -w '%{http_code}' -o "$body_file" "$api_url" || true) + raw_code=$(curl -sS -w '%{http_code}' -o "$body_file" -H "User-Agent: curl/8" "$api_url" || true) http_code="$raw_code" fi diff --git a/packages/mosaic/framework/tools/git/pr-review.sh b/packages/mosaic/framework/tools/git/pr-review.sh index 51c2bb4..35acf9a 100755 --- a/packages/mosaic/framework/tools/git/pr-review.sh +++ b/packages/mosaic/framework/tools/git/pr-review.sh @@ -53,7 +53,7 @@ if [[ -z "$ACTION" ]]; then exit 1 fi -detect_platform +detect_platform >/dev/null if [[ "$PLATFORM" == "github" ]]; then case $ACTION in diff --git a/packages/mosaic/framework/tools/git/pr-view.sh b/packages/mosaic/framework/tools/git/pr-view.sh index 25b0d01..8a1283a 100755 --- a/packages/mosaic/framework/tools/git/pr-view.sh +++ b/packages/mosaic/framework/tools/git/pr-view.sh @@ -58,7 +58,18 @@ fi if [[ "$PLATFORM" == "github" ]]; then gh pr view "$PR_NUMBER" --repo "$REPO_INFO" elif [[ "$PLATFORM" == "gitea" ]]; then - tea pr "$PR_NUMBER" --repo "$REPO_INFO" --login "${GITEA_LOGIN:-mosaicstack}" + if [[ -n "$REPO_OVERRIDE" ]]; then + GITEA_LOGIN_NAME=$(get_gitea_login_for_repo_override) || { + echo "Error: Could not resolve Gitea login for --repo override. Set GITEA_LOGIN or configure a default tea login." >&2 + exit 1 + } + else + GITEA_LOGIN_NAME=$(get_gitea_login) || { + echo "Error: Could not resolve Gitea login for remote host" >&2 + exit 1 + } + fi + tea pr "$PR_NUMBER" --repo "$REPO_INFO" --login "$GITEA_LOGIN_NAME" else echo "Error: Unknown platform" exit 1 diff --git a/packages/mosaic/framework/tools/git/test-gitea-login-resolution.sh b/packages/mosaic/framework/tools/git/test-gitea-login-resolution.sh new file mode 100755 index 0000000..de80dcb --- /dev/null +++ b/packages/mosaic/framework/tools/git/test-gitea-login-resolution.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +# Regression harness for host-specific Gitea tea login resolution. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/gitea-login-resolution}" +REPO_DIR="$WORK_DIR/repo" +BIN_DIR="$WORK_DIR/bin" +LOG_FILE="$WORK_DIR/calls.log" +CREDENTIALS_FILE="$WORK_DIR/credentials.json" + +rm -rf "$WORK_DIR" +mkdir -p "$REPO_DIR" "$BIN_DIR" + +git -C "$REPO_DIR" init -q +git -C "$REPO_DIR" remote add origin https://git.uscllc.com/USC/uconnect.git + +cat > "$CREDENTIALS_FILE" <<'JSON' +{ + "gitea": { + "mosaicstack": { + "url": "https://git.mosaicstack.dev", + "token": "mosaic-token" + }, + "usc": { + "url": "https://git.uscllc.com", + "token": "usc-token" + } + } +} +JSON + +cat > "$BIN_DIR/tea" <<'SH' +#!/usr/bin/env bash +set -euo pipefail + +if [[ "$*" == "login list --output json" ]]; then + cat <<'JSON' +[ + {"name":"evil-usc","url":"https://evilgit.uscllc.com","user":"bad.actor"}, + {"name":"usc","url":"https://git.uscllc.com","user":"jason.woltje"} +] +JSON + exit 0 +fi + +printf 'tea %s\n' "$*" >> "$MOSAIC_TEST_LOG" +exit 0 +SH + +cat > "$BIN_DIR/curl" <<'SH' +#!/usr/bin/env bash +set -euo pipefail + +printf 'curl %s\n' "$*" >> "$MOSAIC_TEST_LOG" +printf '{}' +SH + +chmod +x "$BIN_DIR/tea" "$BIN_DIR/curl" + +run_in_repo() { + ( + cd "$REPO_DIR" + PATH="$BIN_DIR:$PATH" \ + MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \ + MOSAIC_TEST_LOG="$LOG_FILE" \ + "$@" + ) +} + +usc_login=$(run_in_repo bash -c ' + export GITEA_LOGIN=mosaicstack + export GITEA_URL=https://git.mosaicstack.dev + source "'"$SCRIPT_DIR"'/detect-platform.sh" + get_gitea_login +') +if [[ "$usc_login" != "usc" ]]; then + echo "Expected USC host to resolve tea login 'usc' despite stale mosaicstack env; got '$usc_login'" >&2 + exit 1 +fi + +usc_login_without_url=$(run_in_repo bash -c ' + export GITEA_LOGIN=mosaicstack + unset GITEA_URL + source "'"$SCRIPT_DIR"'/detect-platform.sh" + get_gitea_login +') +if [[ "$usc_login_without_url" != "usc" ]]; then + echo "Expected USC host to ignore unmatched GITEA_LOGIN without URL; got '$usc_login_without_url'" >&2 + exit 1 +fi + +git -C "$REPO_DIR" remote set-url origin https://hermes:token@git.uscllc.com/USC/uconnect.git +embedded_host=$(run_in_repo bash -c ' + source "'"$SCRIPT_DIR"'/detect-platform.sh" + get_remote_host +') +if [[ "$embedded_host" != "git.uscllc.com" ]]; then + echo "Expected credential-bearing remote host to strip userinfo; got '$embedded_host'" >&2 + exit 1 +fi +git -C "$REPO_DIR" remote set-url origin https://git.uscllc.com/USC/uconnect.git + +override_login=$(run_in_repo bash -c ' + export GITEA_LOGIN=usc + source "'"$SCRIPT_DIR"'/detect-platform.sh" + get_gitea_login_for_repo_override +') +if [[ "$override_login" != "usc" ]]; then + echo "Expected --repo override path to honor explicit GITEA_LOGIN; got '$override_login'" >&2 + exit 1 +fi + +git -C "$REPO_DIR" remote set-url origin https://git.mosaicstack.dev/mosaicstack/stack.git +: > "$LOG_FILE" +run_in_repo env GITEA_LOGIN=usc "$SCRIPT_DIR/issue-list.sh" --repo USC/uconnect -n 1 +grep -q -- 'tea issues list --repo USC/uconnect --login usc' "$LOG_FILE" +git -C "$REPO_DIR" remote set-url origin https://git.uscllc.com/USC/uconnect.git + +: > "$LOG_FILE" +run_in_repo "$SCRIPT_DIR/issue-close.sh" -i 42 +grep -q -- 'tea issue close 42 --repo USC/uconnect --login usc' "$LOG_FILE" +if grep -q -- '--login mosaicstack' "$LOG_FILE"; then + echo "issue-close.sh used hardcoded mosaicstack login on USC host" >&2 + exit 1 +fi + +git -C "$REPO_DIR" remote set-url origin https://git.mosaicstack.dev/mosaicstack/stack.git +: > "$LOG_FILE" +run_in_repo env GITEA_TOKEN=mosaic-token GITEA_URL=https://git.mosaicstack.dev "$SCRIPT_DIR/issue-close.sh" -i 536 +grep -q -- 'curl .*https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/536' "$LOG_FILE" +if grep -q -- 'tea issue close 536 .*--login mosaicstack' "$LOG_FILE"; then + echo "issue-close.sh invented a mosaicstack tea login instead of using API fallback" >&2 + exit 1 +fi + +echo "Gitea login resolution regression harness passed" diff --git a/packages/mosaic/framework/tools/git/test-pr-merge-gitea-empty-uid.sh b/packages/mosaic/framework/tools/git/test-pr-merge-gitea-empty-uid.sh index 7be5cb8..9dbf2e2 100755 --- a/packages/mosaic/framework/tools/git/test-pr-merge-gitea-empty-uid.sh +++ b/packages/mosaic/framework/tools/git/test-pr-merge-gitea-empty-uid.sh @@ -99,6 +99,7 @@ git remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git export PATH="$MOCK_BIN:$PATH" export PR_MERGE_TEST_LOG="$LOG_FILE" export GITEA_LOGIN="git.mosaicstack.dev" +export GITEA_URL="https://git.mosaicstack.dev" export GITEA_TOKEN="redacted-test-token" OUTPUT="$SANDBOX/output.log"