From ff1b2bec3b83ebd18427ea4c82b914e013a4ff61 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 11 Jun 2026 20:17:28 -0500 Subject: [PATCH 1/4] Fix Gitea wrapper login resolution --- docs/scratchpads/536-wrapper-login-pin.md | 45 +++++ .../framework/tools/git/ci-queue-wait.sh | 4 +- .../framework/tools/git/detect-platform.sh | 183 +++++++++++++++++- .../framework/tools/git/issue-assign.sh | 8 +- .../mosaic/framework/tools/git/issue-close.sh | 50 ++++- .../framework/tools/git/issue-comment.sh | 2 +- .../framework/tools/git/issue-create.sh | 8 +- .../mosaic/framework/tools/git/issue-edit.sh | 8 +- .../mosaic/framework/tools/git/issue-list.sh | 13 +- .../framework/tools/git/issue-reopen.sh | 52 ++++- .../mosaic/framework/tools/git/issue-view.sh | 6 +- .../framework/tools/git/milestone-close.sh | 2 +- .../framework/tools/git/milestone-list.sh | 2 +- .../mosaic/framework/tools/git/pr-ci-wait.sh | 4 +- .../mosaic/framework/tools/git/pr-close.sh | 2 +- .../mosaic/framework/tools/git/pr-create.sh | 8 +- .../mosaic/framework/tools/git/pr-diff.sh | 4 +- .../mosaic/framework/tools/git/pr-list.sh | 13 +- .../mosaic/framework/tools/git/pr-merge.sh | 34 +--- .../mosaic/framework/tools/git/pr-metadata.sh | 6 +- .../mosaic/framework/tools/git/pr-review.sh | 2 +- .../mosaic/framework/tools/git/pr-view.sh | 13 +- .../tools/git/test-gitea-login-resolution.sh | 138 +++++++++++++ .../git/test-pr-merge-gitea-empty-uid.sh | 1 + 24 files changed, 541 insertions(+), 67 deletions(-) create mode 100644 docs/scratchpads/536-wrapper-login-pin.md create mode 100755 packages/mosaic/framework/tools/git/test-gitea-login-resolution.sh 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" -- 2.49.1 From 9bbf8e3e5d0ddf016e5d2cf8a313a2d3e6c120a0 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 11 Jun 2026 20:32:54 -0500 Subject: [PATCH 2/4] Update wrapper redirect assertion --- docs/scratchpads/536-wrapper-login-pin.md | 2 ++ packages/mosaic/src/commands/git-wrapper-redirects.spec.ts | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/scratchpads/536-wrapper-login-pin.md b/docs/scratchpads/536-wrapper-login-pin.md index f260b2b..1584b2e 100644 --- a/docs/scratchpads/536-wrapper-login-pin.md +++ b/docs/scratchpads/536-wrapper-login-pin.md @@ -40,6 +40,8 @@ Fix the framework git wrappers so Gitea issue/PR operations resolve the tea logi - `pnpm typecheck` - `pnpm lint` - `pnpm format:check` +- `pnpm --filter @mosaicstack/mosaic test -- src/commands/git-wrapper-redirects.spec.ts` +- `pnpm test` progressed past wrapper redirect assertions; local run then stopped on `apps/gateway` Postgres connection refused at `localhost:5433`, which CI provides as a service. - 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/src/commands/git-wrapper-redirects.spec.ts b/packages/mosaic/src/commands/git-wrapper-redirects.spec.ts index 84f60ff..f0e60ce 100644 --- a/packages/mosaic/src/commands/git-wrapper-redirects.spec.ts +++ b/packages/mosaic/src/commands/git-wrapper-redirects.spec.ts @@ -15,8 +15,8 @@ describe('Gitea git wrapper API calls', () => { (scriptName) => { const script = readGitTool(scriptName); - expect(script).not.toContain('curl -fsS -H "Authorization: token'); - expect(script).toContain('curl -fsSL -H "Authorization: token'); + expect(script).not.toMatch(/curl -fsS\s+(?:-H "[^"]+"\s+)*-H "Authorization: token/); + expect(script).toMatch(/curl -fsSL\s+(?:-H "[^"]+"\s+)*-H "Authorization: token/); }, ); }); -- 2.49.1 From c4c8a255ee2e71bee339d1930ffe7c5590422a8e Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 11 Jun 2026 21:00:06 -0500 Subject: [PATCH 3/4] Complete Gitea wrapper host resolution --- docs/scratchpads/536-wrapper-login-pin.md | 2 + .../framework/tools/git/detect-platform.ps1 | 148 ++++++++++++++++++ .../framework/tools/git/detect-platform.sh | 25 +++ .../framework/tools/git/issue-assign.ps1 | 8 +- .../framework/tools/git/issue-create.ps1 | 8 +- .../mosaic/framework/tools/git/issue-list.ps1 | 6 + .../framework/tools/git/milestone-close.sh | 6 +- .../framework/tools/git/milestone-create.ps1 | 13 +- .../framework/tools/git/milestone-create.sh | 18 ++- .../framework/tools/git/milestone-list.sh | 6 +- .../mosaic/framework/tools/git/pr-ci-wait.sh | 20 ++- .../mosaic/framework/tools/git/pr-create.ps1 | 9 +- .../mosaic/framework/tools/git/pr-diff.sh | 22 ++- .../mosaic/framework/tools/git/pr-list.ps1 | 6 + .../mosaic/framework/tools/git/pr-merge.ps1 | 6 + .../tools/git/test-gitea-login-resolution.sh | 73 ++++++++- 16 files changed, 360 insertions(+), 16 deletions(-) diff --git a/docs/scratchpads/536-wrapper-login-pin.md b/docs/scratchpads/536-wrapper-login-pin.md index 1584b2e..fabe4a3 100644 --- a/docs/scratchpads/536-wrapper-login-pin.md +++ b/docs/scratchpads/536-wrapper-login-pin.md @@ -30,6 +30,7 @@ Fix the framework git wrappers so Gitea issue/PR operations resolve the tea logi - 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. +- Delta after PR #538 review: extended host-aware login/repo resolution to PowerShell wrappers, Bash milestone wrappers, and API-only `--repo` fallback paths. ## Verification @@ -37,6 +38,7 @@ Fix the framework git wrappers so Gitea issue/PR operations resolve the tea logi - `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` +- `pwsh -NoProfile` parse check for all `packages/mosaic/framework/tools/git/*.ps1` - `pnpm typecheck` - `pnpm lint` - `pnpm format:check` diff --git a/packages/mosaic/framework/tools/git/detect-platform.ps1 b/packages/mosaic/framework/tools/git/detect-platform.ps1 index fab4ba9..fb5dbd0 100644 --- a/packages/mosaic/framework/tools/git/detect-platform.ps1 +++ b/packages/mosaic/framework/tools/git/detect-platform.ps1 @@ -55,6 +55,154 @@ function Get-GitRepoInfo { return $repoPath } +function Get-GitRemoteHost { + [CmdletBinding()] + param() + + $remoteUrl = git remote get-url origin 2>$null + + if ([string]::IsNullOrEmpty($remoteUrl)) { + Write-Error "Not a git repository or no origin remote" + return $null + } + + if ($remoteUrl -match "^https?://([^/]+)/") { + $remoteHost = $Matches[1] + return ($remoteHost -replace "^.*@", "") + } + + if ($remoteUrl -match "^git@([^:]+):") { + return $Matches[1] + } + + return $null +} + +function Get-TeaLoginList { + [CmdletBinding()] + param() + + $json = tea login list --output json 2>$null + if (-not $json) { + return @() + } + + try { + $items = $json | ConvertFrom-Json + } catch { + return @() + } + + if ($null -eq $items) { + return @() + } + + return @($items) +} + +function Test-GiteaUrlMatchesHost { + [CmdletBinding()] + param( + [string]$Url, + [string]$GiteaHost + ) + + if ([string]::IsNullOrEmpty($Url) -or [string]::IsNullOrEmpty($GiteaHost)) { + return $false + } + + try { + $uri = [Uri]$Url + return $uri.Host -eq $GiteaHost + } catch { + return $false + } +} + +function Find-TeaLoginForHost { + [CmdletBinding()] + param([Parameter(Mandatory=$true)][string]$GiteaHost) + + foreach ($login in Get-TeaLoginList) { + $name = if ($login.name) { [string]$login.name } elseif ($login.Name) { [string]$login.Name } else { "" } + $url = if ($login.url) { [string]$login.url } elseif ($login.URL) { [string]$login.URL } else { "" } + if ([string]::IsNullOrEmpty($name) -or [string]::IsNullOrEmpty($url)) { + continue + } + + try { + $uri = [Uri]$url + if ($uri.Host -eq $GiteaHost) { + return $name + } + } catch { + continue + } + } + + return $null +} + +function Test-TeaLoginMatchesHost { + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)][string]$LoginName, + [Parameter(Mandatory=$true)][string]$GiteaHost + ) + + foreach ($login in Get-TeaLoginList) { + $name = if ($login.name) { [string]$login.name } elseif ($login.Name) { [string]$login.Name } else { "" } + $url = if ($login.url) { [string]$login.url } elseif ($login.URL) { [string]$login.URL } else { "" } + if ($name -ne $LoginName -or [string]::IsNullOrEmpty($url)) { + continue + } + + try { + $uri = [Uri]$url + return $uri.Host -eq $GiteaHost + } catch { + return $false + } + } + + return $false +} + +function Get-GiteaLoginForHost { + [CmdletBinding()] + param([string]$GiteaHost) + + if ([string]::IsNullOrEmpty($GiteaHost)) { + $GiteaHost = Get-GitRemoteHost + } + if ([string]::IsNullOrEmpty($GiteaHost)) { + return $null + } + + if ($env:GITEA_LOGIN) { + if ((Test-GiteaUrlMatchesHost -Url $env:GITEA_URL -GiteaHost $GiteaHost) -or (Test-TeaLoginMatchesHost -LoginName $env:GITEA_LOGIN -GiteaHost $GiteaHost)) { + return $env:GITEA_LOGIN + } + } + + return Find-TeaLoginForHost -GiteaHost $GiteaHost +} + +function Get-GiteaRepoArgs { + [CmdletBinding()] + param() + + $repo = Get-GitRepoInfo + $hostName = Get-GitRemoteHost + $login = Get-GiteaLoginForHost -GiteaHost $hostName + + if ([string]::IsNullOrEmpty($repo) -or [string]::IsNullOrEmpty($login)) { + return @() + } + + return @("--repo", $repo, "--login", $login) +} + function Get-GitRepoOwner { [CmdletBinding()] param() diff --git a/packages/mosaic/framework/tools/git/detect-platform.sh b/packages/mosaic/framework/tools/git/detect-platform.sh index 6835d56..b88aa19 100755 --- a/packages/mosaic/framework/tools/git/detect-platform.sh +++ b/packages/mosaic/framework/tools/git/detect-platform.sh @@ -248,6 +248,31 @@ get_gitea_login_for_repo_override() { return 1 } +get_host_from_url() { + local url="${1:-}" + [[ -n "$url" ]] || return 1 + + python3 - "$url" <<'PY' +import sys +from urllib.parse import urlparse + +parsed = urlparse(sys.argv[1]) +if parsed.hostname: + print(parsed.hostname) + raise SystemExit(0) +raise SystemExit(1) +PY +} + +get_gitea_api_host_for_repo_override() { + if [[ -n "${GITEA_HOST:-}" ]]; then + echo "$GITEA_HOST" + return 0 + fi + + get_host_from_url "${GITEA_URL:-}" +} + get_gitea_repo_args() { local repo host login repo=$(get_repo_slug) || return 1 diff --git a/packages/mosaic/framework/tools/git/issue-assign.ps1 b/packages/mosaic/framework/tools/git/issue-assign.ps1 index d73ea25..55bbd1d 100644 --- a/packages/mosaic/framework/tools/git/issue-assign.ps1 +++ b/packages/mosaic/framework/tools/git/issue-assign.ps1 @@ -75,6 +75,11 @@ switch ($platform) { Write-Host "Issue #$Issue updated successfully" } "gitea" { + $repoArgs = @(Get-GiteaRepoArgs) + if ($repoArgs.Length -eq 0) { + Write-Error "Could not resolve Gitea repo/login for remote host" + exit 1 + } $needsEdit = $false $cmd = @("tea", "issue", "edit", $Issue) @@ -87,7 +92,7 @@ switch ($platform) { $needsEdit = $true } if ($Milestone) { - $milestoneList = tea milestones list 2>$null + $milestoneList = tea milestones list @repoArgs 2>$null $milestoneId = ($milestoneList | Select-String "^\s*(\d+).*$Milestone" | ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1) if ($milestoneId) { $cmd += @("--milestone", $milestoneId) @@ -98,6 +103,7 @@ switch ($platform) { } if ($needsEdit) { + $cmd += $repoArgs & $cmd[0] $cmd[1..($cmd.Length-1)] Write-Host "Issue #$Issue updated successfully" } else { diff --git a/packages/mosaic/framework/tools/git/issue-create.ps1 b/packages/mosaic/framework/tools/git/issue-create.ps1 index 23c188d..7d5ba2a 100644 --- a/packages/mosaic/framework/tools/git/issue-create.ps1 +++ b/packages/mosaic/framework/tools/git/issue-create.ps1 @@ -58,12 +58,17 @@ switch ($platform) { & $cmd[0] $cmd[1..($cmd.Length-1)] } "gitea" { + $repoArgs = @(Get-GiteaRepoArgs) + if ($repoArgs.Length -eq 0) { + Write-Error "Could not resolve Gitea repo/login for remote host" + exit 1 + } $cmd = @("tea", "issue", "create", "--title", $Title) if ($Body) { $cmd += @("--description", $Body) } if ($Labels) { $cmd += @("--labels", $Labels) } if ($Milestone) { # Try to get milestone ID by name - $milestoneList = tea milestones list 2>$null + $milestoneList = tea milestones list @repoArgs 2>$null $milestoneId = ($milestoneList | Select-String "^\s*(\d+).*$Milestone" | ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1) if ($milestoneId) { $cmd += @("--milestone", $milestoneId) @@ -71,6 +76,7 @@ switch ($platform) { Write-Warning "Could not find milestone '$Milestone', creating without milestone" } } + $cmd += $repoArgs & $cmd[0] $cmd[1..($cmd.Length-1)] } default { diff --git a/packages/mosaic/framework/tools/git/issue-list.ps1 b/packages/mosaic/framework/tools/git/issue-list.ps1 index 335f18d..cfb55f7 100644 --- a/packages/mosaic/framework/tools/git/issue-list.ps1 +++ b/packages/mosaic/framework/tools/git/issue-list.ps1 @@ -63,9 +63,15 @@ switch ($platform) { & $cmd[0] $cmd[1..($cmd.Length-1)] } "gitea" { + $repoArgs = @(Get-GiteaRepoArgs) + if ($repoArgs.Length -eq 0) { + Write-Error "Could not resolve Gitea repo/login for remote host" + exit 1 + } $cmd = @("tea", "issues", "list", "--state", $State, "--limit", $Limit) if ($Label) { $cmd += @("--labels", $Label) } if ($Milestone) { $cmd += @("--milestones", $Milestone) } + $cmd += $repoArgs & $cmd[0] $cmd[1..($cmd.Length-1)] if ($Assignee) { Write-Warning "Assignee filtering may require manual review for Gitea" diff --git a/packages/mosaic/framework/tools/git/milestone-close.sh b/packages/mosaic/framework/tools/git/milestone-close.sh index 9d5a9f7..f57e6cc 100755 --- a/packages/mosaic/framework/tools/git/milestone-close.sh +++ b/packages/mosaic/framework/tools/git/milestone-close.sh @@ -42,7 +42,11 @@ 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 echo "Closed GitHub milestone: $TITLE" elif [[ "$PLATFORM" == "gitea" ]]; then - tea milestone close "$TITLE" + REPO_ARGS=$(get_gitea_repo_args) || { + echo "Error: Could not resolve Gitea repo/login for remote host" >&2 + exit 1 + } + tea milestone close "$TITLE" $REPO_ARGS echo "Closed Gitea milestone: $TITLE" else echo "Error: Unknown platform" diff --git a/packages/mosaic/framework/tools/git/milestone-create.ps1 b/packages/mosaic/framework/tools/git/milestone-create.ps1 index d96c284..775723e 100644 --- a/packages/mosaic/framework/tools/git/milestone-create.ps1 +++ b/packages/mosaic/framework/tools/git/milestone-create.ps1 @@ -59,7 +59,12 @@ if ($List) { gh api repos/:owner/:repo/milestones --jq '.[] | "\(.number)`t\(.title)`t\(.state)`t\(.open_issues)/\(.closed_issues) issues"' } "gitea" { - tea milestones list + $repoArgs = @(Get-GiteaRepoArgs) + if ($repoArgs.Length -eq 0) { + Write-Error "Could not resolve Gitea repo/login for remote host" + exit 1 + } + tea milestones list @repoArgs } default { Write-Error "Could not detect git platform" @@ -85,9 +90,15 @@ switch ($platform) { Write-Host "Milestone '$Title' created successfully" } "gitea" { + $repoArgs = @(Get-GiteaRepoArgs) + if ($repoArgs.Length -eq 0) { + Write-Error "Could not resolve Gitea repo/login for remote host" + exit 1 + } $cmd = @("tea", "milestones", "create", "--title", $Title) if ($Description) { $cmd += @("--description", $Description) } if ($Due) { $cmd += @("--deadline", $Due) } + $cmd += $repoArgs & $cmd[0] $cmd[1..($cmd.Length-1)] Write-Host "Milestone '$Title' created successfully" } diff --git a/packages/mosaic/framework/tools/git/milestone-create.sh b/packages/mosaic/framework/tools/git/milestone-create.sh index 92f49c0..c5ffb52 100755 --- a/packages/mosaic/framework/tools/git/milestone-create.sh +++ b/packages/mosaic/framework/tools/git/milestone-create.sh @@ -77,7 +77,11 @@ if [[ "$LIST_ONLY" == true ]]; then gh api repos/:owner/:repo/milestones --jq '.[] | "\(.number)\t\(.title)\t\(.state)\t\(.open_issues)/\(.closed_issues) issues"' ;; gitea) - tea milestones list + REPO_ARGS=$(get_gitea_repo_args) || { + echo "Error: Could not resolve Gitea repo/login for remote host" >&2 + exit 1 + } + tea milestones list $REPO_ARGS ;; *) echo "Error: Could not detect git platform" >&2 @@ -104,10 +108,14 @@ case "$PLATFORM" in echo "Milestone '$TITLE' created successfully" ;; gitea) - CMD="tea milestones create --title \"$TITLE\"" - [[ -n "$DESCRIPTION" ]] && CMD="$CMD --description \"$DESCRIPTION\"" - [[ -n "$DUE_DATE" ]] && CMD="$CMD --deadline \"$DUE_DATE\"" - eval "$CMD" + REPO_ARGS=$(get_gitea_repo_args) || { + echo "Error: Could not resolve Gitea repo/login for remote host" >&2 + exit 1 + } + CMD=(tea milestones create --title "$TITLE") + [[ -n "$DESCRIPTION" ]] && CMD+=(--description "$DESCRIPTION") + [[ -n "$DUE_DATE" ]] && CMD+=(--deadline "$DUE_DATE") + "${CMD[@]}" $REPO_ARGS echo "Milestone '$TITLE' created successfully" ;; *) diff --git a/packages/mosaic/framework/tools/git/milestone-list.sh b/packages/mosaic/framework/tools/git/milestone-list.sh index c28b0eb..3b46c3d 100755 --- a/packages/mosaic/framework/tools/git/milestone-list.sh +++ b/packages/mosaic/framework/tools/git/milestone-list.sh @@ -36,7 +36,11 @@ 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"' elif [[ "$PLATFORM" == "gitea" ]]; then - tea milestone list + REPO_ARGS=$(get_gitea_repo_args) || { + echo "Error: Could not resolve Gitea repo/login for remote host" >&2 + exit 1 + } + tea milestone list $REPO_ARGS else echo "Error: Unknown platform" exit 1 diff --git a/packages/mosaic/framework/tools/git/pr-ci-wait.sh b/packages/mosaic/framework/tools/git/pr-ci-wait.sh index d9f0a6c..7b9e8b6 100755 --- a/packages/mosaic/framework/tools/git/pr-ci-wait.sh +++ b/packages/mosaic/framework/tools/git/pr-ci-wait.sh @@ -11,6 +11,7 @@ PR_NUMBER="" TIMEOUT_SEC=1800 INTERVAL_SEC=15 REPO_OVERRIDE="" +HOST_OVERRIDE="" usage() { cat < [-t timeout_sec] [-i interval_sec] Options: -n, --number NUMBER PR number (required) -r, --repo OWNER/REPO Repository slug (default: infer from git origin) + --host HOST Gitea host for --repo API calls (or set GITEA_HOST/GITEA_URL) -t, --timeout SECONDS Max wait time in seconds (default: 1800) -i, --interval SECONDS Poll interval in seconds (default: 15) -h, --help Show this help @@ -150,6 +152,10 @@ while [[ $# -gt 0 ]]; do REPO_OVERRIDE="$2" shift 2 ;; + --host) + HOST_OVERRIDE="$2" + shift 2 + ;; -t|--timeout) TIMEOUT_SEC="$2" shift 2 @@ -211,7 +217,19 @@ if [[ "$PLATFORM" == "github" ]]; then fi echo "[pr-ci-wait] Platform=github PR=#${PR_NUMBER} head_sha=${HEAD_SHA}" elif [[ "$PLATFORM" == "gitea" ]]; then - HOST=$(get_remote_host 2>/dev/null || echo "git.mosaicstack.dev") + if [[ -n "$HOST_OVERRIDE" ]]; then + HOST="$HOST_OVERRIDE" + elif [[ -n "$REPO_OVERRIDE" ]]; then + HOST=$(get_gitea_api_host_for_repo_override) || { + echo "Error: Gitea host is required with --repo. Pass --host or set GITEA_HOST/GITEA_URL." >&2 + exit 1 + } + else + HOST=$(get_remote_host) || { + echo "Error: Could not determine Gitea host from git origin." >&2 + exit 1 + } + fi TOKEN=$(get_gitea_token "$HOST") || { echo "Error: Gitea token not found. Set GITEA_TOKEN or configure ~/.git-credentials." >&2 exit 1 diff --git a/packages/mosaic/framework/tools/git/pr-create.ps1 b/packages/mosaic/framework/tools/git/pr-create.ps1 index faa86f7..3ce9964 100644 --- a/packages/mosaic/framework/tools/git/pr-create.ps1 +++ b/packages/mosaic/framework/tools/git/pr-create.ps1 @@ -9,7 +9,6 @@ param( [Alias("b")] [string]$Body, - [Alias("B")] [string]$Base, [Alias("H")] @@ -101,6 +100,11 @@ switch ($platform) { & $cmd[0] $cmd[1..($cmd.Length-1)] } "gitea" { + $repoArgs = @(Get-GiteaRepoArgs) + if ($repoArgs.Length -eq 0) { + Write-Error "Could not resolve Gitea repo/login for remote host" + exit 1 + } $cmd = @("tea", "pr", "create", "--title", $Title) if ($Body) { $cmd += @("--description", $Body) } if ($Base) { $cmd += @("--base", $Base) } @@ -108,7 +112,7 @@ switch ($platform) { if ($Labels) { $cmd += @("--labels", $Labels) } if ($Milestone) { - $milestoneList = tea milestones list 2>$null + $milestoneList = tea milestones list @repoArgs 2>$null $milestoneId = ($milestoneList | Select-String "^\s*(\d+).*$Milestone" | ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1) if ($milestoneId) { $cmd += @("--milestone", $milestoneId) @@ -121,6 +125,7 @@ switch ($platform) { Write-Warning "Draft PR may not be supported by your tea version" } + $cmd += $repoArgs & $cmd[0] $cmd[1..($cmd.Length-1)] } default { diff --git a/packages/mosaic/framework/tools/git/pr-diff.sh b/packages/mosaic/framework/tools/git/pr-diff.sh index 34792d9..4657b5d 100755 --- a/packages/mosaic/framework/tools/git/pr-diff.sh +++ b/packages/mosaic/framework/tools/git/pr-diff.sh @@ -11,6 +11,7 @@ source "$SCRIPT_DIR/detect-platform.sh" PR_NUMBER="" OUTPUT_FILE="" REPO_OVERRIDE="" +HOST_OVERRIDE="" while [[ $# -gt 0 ]]; do case $1 in @@ -26,12 +27,17 @@ while [[ $# -gt 0 ]]; do REPO_OVERRIDE="$2" shift 2 ;; + --host) + HOST_OVERRIDE="$2" + shift 2 + ;; -h|--help) - echo "Usage: pr-diff.sh -n [-r owner/repo] [-o ]" + echo "Usage: pr-diff.sh -n [-r owner/repo] [--host host] [-o ]" echo "" echo "Options:" echo " -n, --number PR number (required)" echo " -r, --repo Repository slug (default: infer from git origin)" + echo " --host Gitea host for --repo API calls (or set GITEA_HOST/GITEA_URL)" echo " -o, --output Output file (optional, prints to stdout if omitted)" echo " -h, --help Show this help" exit 0 @@ -69,7 +75,19 @@ if [[ "$PLATFORM" == "github" ]]; then fi elif [[ "$PLATFORM" == "gitea" ]]; then # tea doesn't have a direct diff command — use the API - HOST=$(get_remote_host 2>/dev/null || echo "git.mosaicstack.dev") + if [[ -n "$HOST_OVERRIDE" ]]; then + HOST="$HOST_OVERRIDE" + elif [[ -n "$REPO_OVERRIDE" ]]; then + HOST=$(get_gitea_api_host_for_repo_override) || { + echo "Error: Gitea host is required with --repo. Pass --host or set GITEA_HOST/GITEA_URL." >&2 + exit 1 + } + else + HOST=$(get_remote_host) || { + echo "Error: Could not determine Gitea host from git origin." >&2 + exit 1 + } + fi DIFF_URL="https://${HOST}/api/v1/repos/${REPO_INFO}/pulls/${PR_NUMBER}.diff" diff --git a/packages/mosaic/framework/tools/git/pr-list.ps1 b/packages/mosaic/framework/tools/git/pr-list.ps1 index aaa6ad4..0789d33 100644 --- a/packages/mosaic/framework/tools/git/pr-list.ps1 +++ b/packages/mosaic/framework/tools/git/pr-list.ps1 @@ -58,6 +58,11 @@ switch ($platform) { & $cmd[0] $cmd[1..($cmd.Length-1)] } "gitea" { + $repoArgs = @(Get-GiteaRepoArgs) + if ($repoArgs.Length -eq 0) { + Write-Error "Could not resolve Gitea repo/login for remote host" + exit 1 + } $cmd = @("tea", "pr", "list", "--state", $State, "--limit", $Limit) if ($Label) { @@ -67,6 +72,7 @@ switch ($platform) { Write-Warning "Author filtering may require manual review for Gitea" } + $cmd += $repoArgs & $cmd[0] $cmd[1..($cmd.Length-1)] } default { diff --git a/packages/mosaic/framework/tools/git/pr-merge.ps1 b/packages/mosaic/framework/tools/git/pr-merge.ps1 index 51d6fd9..49bb819 100755 --- a/packages/mosaic/framework/tools/git/pr-merge.ps1 +++ b/packages/mosaic/framework/tools/git/pr-merge.ps1 @@ -74,6 +74,11 @@ switch ($platform) { & $cmd[0] $cmd[1..($cmd.Length-1)] } "gitea" { + $repoArgs = @(Get-GiteaRepoArgs) + if ($repoArgs.Length -eq 0) { + Write-Error "Could not resolve Gitea repo/login for remote host" + exit 1 + } if (-not $SkipQueueGuard) { $timeout = if ($env:MOSAIC_CI_QUEUE_TIMEOUT_SEC) { [int]$env:MOSAIC_CI_QUEUE_TIMEOUT_SEC } else { 900 } $interval = if ($env:MOSAIC_CI_QUEUE_POLL_SEC) { [int]$env:MOSAIC_CI_QUEUE_POLL_SEC } else { 15 } @@ -87,6 +92,7 @@ switch ($platform) { Write-Warning "Branch deletion after merge may need to be done separately with tea" } + $cmd += $repoArgs & $cmd[0] $cmd[1..($cmd.Length-1)] } default { diff --git a/packages/mosaic/framework/tools/git/test-gitea-login-resolution.sh b/packages/mosaic/framework/tools/git/test-gitea-login-resolution.sh index de80dcb..ec80441 100755 --- a/packages/mosaic/framework/tools/git/test-gitea-login-resolution.sh +++ b/packages/mosaic/framework/tools/git/test-gitea-login-resolution.sh @@ -54,7 +54,21 @@ cat > "$BIN_DIR/curl" <<'SH' set -euo pipefail printf 'curl %s\n' "$*" >> "$MOSAIC_TEST_LOG" -printf '{}' +url="${*: -1}" +case "$url" in + */pulls/*.diff) + printf 'diff --git a/file b/file\n' + ;; + */pulls/*) + printf '{"head":{"sha":"abc123"}}' + ;; + */commits/*/status) + printf '{"state":"success","statuses":[{"context":"ci/mock","status":"success"}]}' + ;; + *) + printf '{}' + ;; +esac SH chmod +x "$BIN_DIR/tea" "$BIN_DIR/curl" @@ -126,6 +140,63 @@ if grep -q -- '--login mosaicstack' "$LOG_FILE"; then exit 1 fi +: > "$LOG_FILE" +run_in_repo "$SCRIPT_DIR/milestone-list.sh" +grep -q -- 'tea milestone list --repo USC/uconnect --login usc' "$LOG_FILE" + +: > "$LOG_FILE" +run_in_repo "$SCRIPT_DIR/milestone-create.sh" -t "0.2.0" -d "USC milestone" +grep -q -- 'tea milestones create --title 0.2.0 --description USC milestone --repo USC/uconnect --login usc' "$LOG_FILE" + +: > "$LOG_FILE" +run_in_repo "$SCRIPT_DIR/milestone-close.sh" -t "0.2.0" +grep -q -- 'tea milestone close 0.2.0 --repo USC/uconnect --login usc' "$LOG_FILE" + +if command -v pwsh >/dev/null 2>&1; then + : > "$LOG_FILE" + run_in_repo pwsh -NoProfile -File "$SCRIPT_DIR/issue-list.ps1" -Limit 1 + grep -q -- 'tea issues list --state open --limit 1 --repo USC/uconnect --login usc' "$LOG_FILE" + + : > "$LOG_FILE" + run_in_repo pwsh -NoProfile -File "$SCRIPT_DIR/issue-create.ps1" -Title "PowerShell issue" + grep -q -- 'tea issue create --title PowerShell issue --repo USC/uconnect --login usc' "$LOG_FILE" + + : > "$LOG_FILE" + run_in_repo pwsh -NoProfile -File "$SCRIPT_DIR/pr-list.ps1" -Limit 1 + grep -q -- 'tea pr list --state open --limit 1 --repo USC/uconnect --login usc' "$LOG_FILE" + + : > "$LOG_FILE" + run_in_repo pwsh -NoProfile -File "$SCRIPT_DIR/pr-create.ps1" -Title "PowerShell PR" + grep -q -- 'tea pr create --title PowerShell PR --head master --repo USC/uconnect --login usc' "$LOG_FILE" + + : > "$LOG_FILE" + run_in_repo pwsh -NoProfile -File "$SCRIPT_DIR/pr-merge.ps1" -Number 42 -SkipQueueGuard + grep -q -- 'tea pr merge 42 --style squash --repo USC/uconnect --login usc' "$LOG_FILE" + + : > "$LOG_FILE" + run_in_repo pwsh -NoProfile -File "$SCRIPT_DIR/milestone-create.ps1" -List + grep -q -- 'tea milestones list --repo USC/uconnect --login usc' "$LOG_FILE" +fi + +: > "$LOG_FILE" +if run_in_repo "$SCRIPT_DIR/pr-diff.sh" --repo USC/uconnect -n 7 >/dev/null 2>&1; then + echo "Expected pr-diff.sh --repo without host to fail loud" >&2 + exit 1 +fi +if grep -q -- 'git.mosaicstack.dev/api/v1/repos/USC/uconnect' "$LOG_FILE"; then + echo "pr-diff.sh --repo defaulted API host to git.mosaicstack.dev" >&2 + exit 1 +fi + +: > "$LOG_FILE" +run_in_repo env GITEA_URL=https://git.uscllc.com "$SCRIPT_DIR/pr-diff.sh" --repo USC/uconnect -n 7 >/dev/null +grep -q -- 'curl .*https://git.uscllc.com/api/v1/repos/USC/uconnect/pulls/7.diff' "$LOG_FILE" + +: > "$LOG_FILE" +run_in_repo "$SCRIPT_DIR/pr-ci-wait.sh" --repo USC/uconnect --host git.uscllc.com -n 9 -t 2 -i 1 +grep -q -- 'curl .*https://git.uscllc.com/api/v1/repos/USC/uconnect/pulls/9' "$LOG_FILE" +grep -q -- 'curl .*https://git.uscllc.com/api/v1/repos/USC/uconnect/commits/abc123/status' "$LOG_FILE" + 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 -- 2.49.1 From 90e565976d239c51088a6b8109ebb282a372d860 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 11 Jun 2026 21:15:56 -0500 Subject: [PATCH 4/4] Harden Gitea login fallback selection --- docs/scratchpads/536-wrapper-login-pin.md | 1 + .../framework/tools/git/detect-platform.ps1 | 2 +- .../framework/tools/git/detect-platform.sh | 2 +- .../tools/git/test-gitea-login-resolution.sh | 24 +++++++++++++++++++ .../git/test-pr-merge-gitea-empty-uid.sh | 8 +++++++ 5 files changed, 35 insertions(+), 2 deletions(-) diff --git a/docs/scratchpads/536-wrapper-login-pin.md b/docs/scratchpads/536-wrapper-login-pin.md index fabe4a3..1807b46 100644 --- a/docs/scratchpads/536-wrapper-login-pin.md +++ b/docs/scratchpads/536-wrapper-login-pin.md @@ -31,6 +31,7 @@ Fix the framework git wrappers so Gitea issue/PR operations resolve the tea logi - 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. - Delta after PR #538 review: extended host-aware login/repo resolution to PowerShell wrappers, Bash milestone wrappers, and API-only `--repo` fallback paths. +- Delta after live USC `pr-create.sh` repro: tightened `GITEA_LOGIN` trust so stale login names are ignored unless the tea login itself matches the target host, and added USC API fallback coverage for `pr-create.sh`. ## Verification diff --git a/packages/mosaic/framework/tools/git/detect-platform.ps1 b/packages/mosaic/framework/tools/git/detect-platform.ps1 index fb5dbd0..49cb4d7 100644 --- a/packages/mosaic/framework/tools/git/detect-platform.ps1 +++ b/packages/mosaic/framework/tools/git/detect-platform.ps1 @@ -180,7 +180,7 @@ function Get-GiteaLoginForHost { } if ($env:GITEA_LOGIN) { - if ((Test-GiteaUrlMatchesHost -Url $env:GITEA_URL -GiteaHost $GiteaHost) -or (Test-TeaLoginMatchesHost -LoginName $env:GITEA_LOGIN -GiteaHost $GiteaHost)) { + if (Test-TeaLoginMatchesHost -LoginName $env:GITEA_LOGIN -GiteaHost $GiteaHost) { return $env:GITEA_LOGIN } } diff --git a/packages/mosaic/framework/tools/git/detect-platform.sh b/packages/mosaic/framework/tools/git/detect-platform.sh index b88aa19..58ac2fe 100755 --- a/packages/mosaic/framework/tools/git/detect-platform.sh +++ b/packages/mosaic/framework/tools/git/detect-platform.sh @@ -178,7 +178,7 @@ get_gitea_login_for_host() { fi if [[ -n "${GITEA_LOGIN:-}" ]]; then - if gitea_url_matches_host "${GITEA_URL:-}" "$host" || tea_login_matches_host "$GITEA_LOGIN" "$host"; then + if tea_login_matches_host "$GITEA_LOGIN" "$host"; then echo "$GITEA_LOGIN" return 0 fi diff --git a/packages/mosaic/framework/tools/git/test-gitea-login-resolution.sh b/packages/mosaic/framework/tools/git/test-gitea-login-resolution.sh index ec80441..411848f 100755 --- a/packages/mosaic/framework/tools/git/test-gitea-login-resolution.sh +++ b/packages/mosaic/framework/tools/git/test-gitea-login-resolution.sh @@ -46,6 +46,10 @@ JSON fi printf 'tea %s\n' "$*" >> "$MOSAIC_TEST_LOG" +if [[ "${MOSAIC_TEA_FAIL_PR_CREATE:-}" == "1" && "$*" == pr\ create* ]]; then + echo 'GetUserByName: simulated stale login failure' >&2 + exit 1 +fi exit 0 SH @@ -94,6 +98,17 @@ if [[ "$usc_login" != "usc" ]]; then exit 1 fi +usc_login_with_usc_url=$(run_in_repo bash -c ' + export GITEA_LOGIN=mosaicstack + export GITEA_URL=https://git.uscllc.com + source "'"$SCRIPT_DIR"'/detect-platform.sh" + get_gitea_login +') +if [[ "$usc_login_with_usc_url" != "usc" ]]; then + echo "Expected USC host to reject stale GITEA_LOGIN even when GITEA_URL matches USC; got '$usc_login_with_usc_url'" >&2 + exit 1 +fi + usc_login_without_url=$(run_in_repo bash -c ' export GITEA_LOGIN=mosaicstack unset GITEA_URL @@ -197,6 +212,15 @@ run_in_repo "$SCRIPT_DIR/pr-ci-wait.sh" --repo USC/uconnect --host git.uscllc.co grep -q -- 'curl .*https://git.uscllc.com/api/v1/repos/USC/uconnect/pulls/9' "$LOG_FILE" grep -q -- 'curl .*https://git.uscllc.com/api/v1/repos/USC/uconnect/commits/abc123/status' "$LOG_FILE" +: > "$LOG_FILE" +run_in_repo env MOSAIC_TEA_FAIL_PR_CREATE=1 GITEA_TOKEN=usc-token GITEA_URL=https://git.uscllc.com "$SCRIPT_DIR/pr-create.sh" -t "USC API fallback" -H feature/pr-create +grep -q -- 'tea pr create --repo USC/uconnect --login usc --title USC API fallback --head feature/pr-create' "$LOG_FILE" +grep -q -- 'curl .*Authorization: token usc-token .*https://git.uscllc.com/api/v1/repos/USC/uconnect/pulls' "$LOG_FILE" +if grep -q -- 'git.mosaicstack.dev/api/v1/repos/USC/uconnect/pulls' "$LOG_FILE"; then + echo "pr-create.sh API fallback defaulted USC repo to git.mosaicstack.dev" >&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 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 9dbf2e2..c1aeb27 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 @@ -23,6 +23,10 @@ cat > "$MOCK_BIN/tea" <<'EOF' set -euo pipefail printf 'tea %q ' "$@" >> "$PR_MERGE_TEST_LOG" printf '\n' >> "$PR_MERGE_TEST_LOG" +if [[ "$*" == *"login list"* ]]; then + echo '[{"name":"git.mosaicstack.dev","url":"https://git.mosaicstack.dev"}]' + exit 0 +fi if [[ "$*" == *"pr merge"* ]]; then echo 'user does not exist [uid: 0, name: ]' >&2 exit 1 @@ -128,6 +132,10 @@ cat > "$MOCK_BIN/tea" <<'EOF' set -euo pipefail printf 'tea %q ' "$@" >> "$PR_MERGE_TEST_LOG" printf '\n' >> "$PR_MERGE_TEST_LOG" +if [[ "$*" == *"login list"* ]]; then + echo '[{"name":"git.mosaicstack.dev","url":"https://git.mosaicstack.dev"}]' + exit 0 +fi if [[ "$*" == *"pr merge"* ]]; then echo 'tea network timeout' >&2 exit 2 -- 2.49.1