From 770e3f57edf9ae461a6c8a80ecbc0c08659f9692 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Fri, 17 Jul 2026 11:18:29 -0500 Subject: [PATCH 1/6] test(#812): reproduce false-positive gitea review comment --- .../tools/git/test-pr-review-gitea-comment.sh | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh diff --git a/packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh b/packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh new file mode 100644 index 00000000..5dd1ccca --- /dev/null +++ b/packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# Regression harness for durable Gitea PR review comments (#812). + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/pr-review-gitea-comment}" +REPO_DIR="$WORK_DIR/repo" +BIN_DIR="$WORK_DIR/bin" +LOG_FILE="$WORK_DIR/tea.log" +OUTPUT_FILE="$WORK_DIR/output.log" + +cleanup() { + rm -rf "$WORK_DIR" +} +trap cleanup EXIT + +mkdir -p "$REPO_DIR" "$BIN_DIR" +git -C "$REPO_DIR" init -q +git -C "$REPO_DIR" remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git + +cat > "$BIN_DIR/tea" <<'SH' +#!/usr/bin/env bash +set -euo pipefail + +printf '%s\n' "$*" >> "$PR_REVIEW_TEST_LOG" + +if [[ "$*" == "login list --output json" ]]; then + printf '%s\n' '[{"name":"mosaicstack","url":"https://git.mosaicstack.dev"}]' + exit 0 +fi + +case "${PR_REVIEW_TEST_MODE:-}" in + approve) + [[ "$*" == "pr approve 123 --repo mosaicstack/stack --login mosaicstack" ]] || exit 90 + ;; + request-changes) + [[ "$*" == "pr reject 123 --repo mosaicstack/stack --login mosaicstack --comment changes-required" ]] || exit 91 + ;; + legacy-fallback) + if [[ "$*" == pr\ comment* ]]; then + # tea v0.11.1 treats the nonexistent subcommand as `tea pr list` and exits 0. + printf '%s\n' 'INDEX TITLE STATE' + exit 0 + fi + if [[ "$*" == comment* ]]; then + printf '%s\n' 'supported comment write deliberately unavailable' >&2 + exit 23 + fi + exit 92 + ;; + comment-success|readback-failure) + if [[ "$*" == "comment 123 durable-body --login mosaicstack --repo mosaicstack/stack --output json" ]]; then + printf '%s\n' '{"id":456,"body":"durable-body"}' + exit 0 + fi + if [[ "$*" == "api --login mosaicstack /repos/mosaicstack/stack/issues/comments/456" ]]; then + if [[ "$PR_REVIEW_TEST_MODE" == "readback-failure" ]]; then + printf '%s\n' '{"id":456,"body":"different-body","issue_url":"https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/123"}' + else + printf '%s\n' '{"id":456,"body":"durable-body","issue_url":"https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/123"}' + fi + exit 0 + fi + if [[ "$*" == pr\ comment* ]]; then + # Reproduce the old false-positive fallback if the wrapper regresses. + printf '%s\n' 'INDEX TITLE STATE' + exit 0 + fi + exit 93 + ;; + write-failure) + if [[ "$*" == "comment 123 durable-body --login mosaicstack --repo mosaicstack/stack --output json" ]]; then + printf '%s\n' 'provider rejected comment' >&2 + exit 24 + fi + exit 94 + ;; + *) + exit 95 + ;; +esac +SH +chmod +x "$BIN_DIR/tea" + +run_review() { + local mode="$1" action="$2" comment="${3:-}" + : > "$LOG_FILE" + : > "$OUTPUT_FILE" + ( + cd "$REPO_DIR" + PATH="$BIN_DIR:$PATH" \ + PR_REVIEW_TEST_LOG="$LOG_FILE" \ + PR_REVIEW_TEST_MODE="$mode" \ + "$SCRIPT_DIR/pr-review.sh" -n 123 -a "$action" ${comment:+-c "$comment"} + ) > "$OUTPUT_FILE" 2>&1 +} + +run_review approve approve +grep -q '^pr approve 123 --repo mosaicstack/stack --login mosaicstack$' "$LOG_FILE" +grep -q 'Approved Gitea PR #123' "$OUTPUT_FILE" + +run_review request-changes request-changes changes-required +grep -q '^pr reject 123 --repo mosaicstack/stack --login mosaicstack --comment changes-required$' "$LOG_FILE" +grep -q 'Requested changes on Gitea PR #123' "$OUTPUT_FILE" + +if run_review legacy-fallback comment durable-body; then + echo "The old nonexistent tea pr comment fallback returned success" >&2 + cat "$OUTPUT_FILE" >&2 + exit 1 +fi +if grep -q '^pr comment ' "$LOG_FILE"; then + echo "Wrapper invoked unsupported tea pr comment" >&2 + exit 1 +fi +if grep -q 'Added comment to Gitea PR' "$OUTPUT_FILE"; then + echo "Wrapper reported success without durable persistence" >&2 + exit 1 +fi + +run_review comment-success comment durable-body +grep -q '^comment 123 durable-body --login mosaicstack --repo mosaicstack/stack --output json$' "$LOG_FILE" +grep -q '^api --login mosaicstack /repos/mosaicstack/stack/issues/comments/456$' "$LOG_FILE" +grep -q 'Added and verified comment on Gitea PR #123' "$OUTPUT_FILE" + +if run_review write-failure comment durable-body; then + echo "Expected provider write failure to return nonzero" >&2 + exit 1 +fi +if grep -q 'Added and verified comment' "$OUTPUT_FILE"; then + echo "Write failure reported durable success" >&2 + exit 1 +fi + +if run_review readback-failure comment durable-body; then + echo "Expected mismatched provider read-back to return nonzero" >&2 + exit 1 +fi +if grep -q 'Added and verified comment' "$OUTPUT_FILE"; then + echo "Read-back mismatch reported durable success" >&2 + exit 1 +fi + +echo "pr-review.sh durable Gitea comment regression passed" -- 2.49.1 From ea7f8c57582115e47b503066631ed9cf4a4bbd45 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Fri, 17 Jul 2026 11:19:31 -0500 Subject: [PATCH 2/6] fix(#812): verify gitea review comment persistence --- packages/mosaic/framework/tools/git/README.md | 9 ++++ .../mosaic/framework/tools/git/pr-review.sh | 54 ++++++++++++++++++- 2 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 packages/mosaic/framework/tools/git/README.md diff --git a/packages/mosaic/framework/tools/git/README.md b/packages/mosaic/framework/tools/git/README.md new file mode 100644 index 00000000..c463de14 --- /dev/null +++ b/packages/mosaic/framework/tools/git/README.md @@ -0,0 +1,9 @@ +# Git provider wrappers + +These scripts provide host-aware GitHub and Gitea issue, pull-request, milestone, and CI operations. + +## Durable review provenance + +A successful provider write command—or a wrapper message based only on that command's exit code—is **not** durable review provenance. Review comments count as durable provenance only after the wrapper reads the created provider record back and verifies that it belongs to the intended repository and pull request and contains the exact submitted body (or verifies the provider-returned record ID). + +`pr-review.sh` therefore fails closed when a Gitea comment cannot be written, its created comment ID cannot be identified, or provider read-back does not match. It reports comment success only after that read-back verification passes. diff --git a/packages/mosaic/framework/tools/git/pr-review.sh b/packages/mosaic/framework/tools/git/pr-review.sh index 35acf9af..f0bf6b89 100755 --- a/packages/mosaic/framework/tools/git/pr-review.sh +++ b/packages/mosaic/framework/tools/git/pr-review.sh @@ -101,8 +101,58 @@ elif [[ "$PLATFORM" == "gitea" ]]; then echo "Error: Comment required" exit 1 fi - tea pr comment "$PR_NUMBER" "$COMMENT" $(get_gitea_repo_args) - echo "Added comment to Gitea PR #$PR_NUMBER" + + repo=$(get_repo_slug) + host=$(get_remote_host) + login=$(get_gitea_login_for_host "$host") + + comment_json=$(tea comment "$PR_NUMBER" "$COMMENT" --login "$login" --repo "$repo" --output json) + comment_id=$(printf '%s' "$comment_json" | python3 -c ' +import json +import sys + +try: + comment = json.load(sys.stdin) + if isinstance(comment, list) and len(comment) == 1: + comment = comment[0] + comment_id = comment.get("id") if isinstance(comment, dict) else None + if not isinstance(comment_id, int) or comment_id <= 0: + raise ValueError("missing positive comment id") +except (json.JSONDecodeError, ValueError) as error: + print(f"Error: could not identify created Gitea comment: {error}", file=sys.stderr) + raise SystemExit(1) +print(comment_id) +') + + readback_json=$(tea api --login "$login" "/repos/$repo/issues/comments/$comment_id") + EXPECTED_COMMENT_ID="$comment_id" EXPECTED_COMMENT_BODY="$COMMENT" EXPECTED_REPO="$repo" EXPECTED_PR_NUMBER="$PR_NUMBER" \ + python3 -c ' +import json +import os +import sys +from urllib.parse import urlparse + +try: + comment = json.load(sys.stdin) + expected_id = int(os.environ["EXPECTED_COMMENT_ID"]) + expected_body = os.environ["EXPECTED_COMMENT_BODY"] + expected_repo = os.environ["EXPECTED_REPO"] + expected_pr = os.environ["EXPECTED_PR_NUMBER"] + issue_url = comment.get("issue_url", "") if isinstance(comment, dict) else "" + issue_path = urlparse(issue_url).path.rstrip("/") + expected_suffix = f"/repos/{expected_repo}/issues/{expected_pr}" + if comment.get("id") != expected_id: + raise ValueError("comment id mismatch") + if comment.get("body") != expected_body: + raise ValueError("comment body mismatch") + if not issue_path.endswith(expected_suffix): + raise ValueError("repository or PR mismatch") +except (json.JSONDecodeError, KeyError, TypeError, ValueError) as error: + print(f"Error: Gitea comment persistence verification failed: {error}", file=sys.stderr) + raise SystemExit(1) +' <<< "$readback_json" + + echo "Added and verified comment on Gitea PR #$PR_NUMBER (comment ID $comment_id)" ;; *) echo "Error: Unknown action: $ACTION" -- 2.49.1 From 79988a4e13c38961799669cc80d214232ea5f800 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Fri, 17 Jul 2026 11:24:46 -0500 Subject: [PATCH 3/6] =?UTF-8?q?chore(#812):=20WIP=20checkpoint=20=E2=80=94?= =?UTF-8?q?=20parked=20pending=20#789=20terminal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/scratchpads/812-pr-review-comment.md | 49 +++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 docs/scratchpads/812-pr-review-comment.md diff --git a/docs/scratchpads/812-pr-review-comment.md b/docs/scratchpads/812-pr-review-comment.md new file mode 100644 index 00000000..3a4437f7 --- /dev/null +++ b/docs/scratchpads/812-pr-review-comment.md @@ -0,0 +1,49 @@ +# Issue #812 — durable Gitea PR review comments + +- **Lane:** ms-812 +- **Branch:** `fix/812-pr-review-comment` +- **Issue:** mosaicstack/stack#812 +- **Budget:** 15K working estimate; single focused shell-wrapper/test/docs change. + +## Objective + +Make the Gitea `comment` action in `packages/mosaic/framework/tools/git/pr-review.sh` use tea v0.11.1's supported `tea comment` command and report success only after provider read-back verifies the created comment against the intended repository, PR, and exact body. + +## Plan + +1. Add and commit a failing shell regression harness before production changes. +2. Verify RED against the nonexistent `tea pr comment` fallback false-positive. +3. Implement the minimal supported write plus ID-based provider read-back. +4. Document that wrapper write output is not durable provenance until read-back succeeds. +5. Run focused regression tests, touched-package tests, and repository quality gates. +6. Run independent review, remediate findings, queue-guard, push, and open a PR. Stop at PR-open for coordinator review/merge. + +## Progress checkpoints + +- [x] RED regression committed and reported to mosaic-100 (`3585cdfa`) +- [x] Initial minimal fix implemented (`c3a83833`) +- [x] Focused and package tests green after dependency build +- [ ] Independent review passed +- [ ] PR opened and reported with exact head SHA + +## Tests run + +- RED: `bash packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh` failed against the old path because `tea pr comment` fell back and the wrapper returned success. +- GREEN: all `packages/mosaic/framework/tools/git/test-*.sh` harnesses passed. +- `pnpm build` passed (23/23 tasks). +- `pnpm --filter @mosaicstack/mosaic test` passed (67 files, 1278 tests). +- Package and root typecheck/lint/format gates passed. +- Codex security review found no issues. +- Codex code review requested changes: tea v0.11.1 has no supported `tea api`; replace read-back with the existing host-scoped authenticated curl/API path when work resumes. + +## Risks / blockers + +- **Coordination hold:** Mos directed #812 to remain parked until in-flight #789 reaches terminal state because #789 actively uses `pr-review.sh`. Branch must be pushed but no PR opened. +- Initial implementation's `tea api` read-back is not compatible with tea v0.11.1 and must be remediated before PR. +- Tea output shape must provide a created comment ID in JSON; failure to parse must fail closed. +- Read-back must not accept a pre-existing comment with the same body. +- Existing approve/request-changes behavior must remain unchanged and covered. + +## Final verification evidence + +Parked pending #789 terminal. No PR opened. -- 2.49.1 From 0115d92fdab480077723bcc27513947a14d03984 Mon Sep 17 00:00:00 2001 From: ms-lead-reviewer Date: Mon, 20 Jul 2026 00:36:48 -0500 Subject: [PATCH 4/6] fix(#812): use supported gitea comment REST read-back --- docs/scratchpads/812-pr-review-comment.md | 42 ++-- .../mosaic/framework/tools/git/pr-review.sh | 79 ++++++-- .../tools/git/test-pr-review-gitea-comment.sh | 191 +++++++++++++----- 3 files changed, 226 insertions(+), 86 deletions(-) diff --git a/docs/scratchpads/812-pr-review-comment.md b/docs/scratchpads/812-pr-review-comment.md index 3a4437f7..28ebec14 100644 --- a/docs/scratchpads/812-pr-review-comment.md +++ b/docs/scratchpads/812-pr-review-comment.md @@ -7,7 +7,7 @@ ## Objective -Make the Gitea `comment` action in `packages/mosaic/framework/tools/git/pr-review.sh` use tea v0.11.1's supported `tea comment` command and report success only after provider read-back verifies the created comment against the intended repository, PR, and exact body. +Make the Gitea `comment` action in `packages/mosaic/framework/tools/git/pr-review.sh` use the supported Gitea comments REST API and report success only after provider read-back verifies the created comment against the intended repository, PR, and exact body. ## Plan @@ -16,34 +16,36 @@ Make the Gitea `comment` action in `packages/mosaic/framework/tools/git/pr-revie 3. Implement the minimal supported write plus ID-based provider read-back. 4. Document that wrapper write output is not durable provenance until read-back succeeds. 5. Run focused regression tests, touched-package tests, and repository quality gates. -6. Run independent review, remediate findings, queue-guard, push, and open a PR. Stop at PR-open for coordinator review/merge. +6. Remediate review findings, queue-guard, and push for coordinator-owned independent review. Do not open or merge a PR. ## Progress checkpoints -- [x] RED regression committed and reported to mosaic-100 (`3585cdfa`) -- [x] Initial minimal fix implemented (`c3a83833`) -- [x] Focused and package tests green after dependency build -- [ ] Independent review passed -- [ ] PR opened and reported with exact head SHA +- [x] RED regression committed and reported to mosaic-100 (rebased commit `770e3f57`) +- [x] Initial minimal fix implemented (rebased commit `ea7f8c57`) +- [x] Rebased cleanly onto main `627cf2bb387f7c84a532d88819903a7679ce0d72` +- [x] Codex blocker remediated by replacing unsupported `tea api` with authenticated REST write/read-back +- [x] Focused, package, and repository gates green +- [ ] Coordinator-owned independent review pending after push +- [x] No PR opened; no self-review or self-merge ## Tests run -- RED: `bash packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh` failed against the old path because `tea pr comment` fell back and the wrapper returned success. -- GREEN: all `packages/mosaic/framework/tools/git/test-*.sh` harnesses passed. -- `pnpm build` passed (23/23 tasks). -- `pnpm --filter @mosaicstack/mosaic test` passed (67 files, 1278 tests). -- Package and root typecheck/lint/format gates passed. -- Codex security review found no issues. -- Codex code review requested changes: tea v0.11.1 has no supported `tea api`; replace read-back with the existing host-scoped authenticated curl/API path when work resumes. +- RED after rebase: the regression harness failed against `origin/main` with status 1 after reproducing the old `tea pr comment` zero-exit fallback and false success echo. +- GREEN at resumed head: the same harness passed with REST POST 201 plus GET 200 read-back. +- All `packages/mosaic/framework/tools/git/test-*.sh` harnesses passed. +- `shellcheck -x` passed for the changed scripts; `bash -n` passed. +- Manifest resolver returned `framework` for `tools/git/test-pr-review-gitea-comment.sh`. +- `pnpm test` passed (43/43 Turbo tasks; Mosaic 75 files/1434 tests; Gateway 56 files/628 tests plus documented skips). +- `pnpm typecheck` passed (42/42 tasks), `pnpm lint` passed (23/23), and `pnpm format:check` passed. +- Firewall checks found no user-home paths or operator identities in changed shipped files; no token value is logged or echoed. ## Risks / blockers -- **Coordination hold:** Mos directed #812 to remain parked until in-flight #789 reaches terminal state because #789 actively uses `pr-review.sh`. Branch must be pushed but no PR opened. -- Initial implementation's `tea api` read-back is not compatible with tea v0.11.1 and must be remediated before PR. -- Tea output shape must provide a created comment ID in JSON; failure to parse must fail closed. -- Read-back must not accept a pre-existing comment with the same body. -- Existing approve/request-changes behavior must remain unchanged and covered. +- No active implementation blocker. #789 reached terminal merged state and the coordination hold was lifted. +- REST transport failures, non-201 writes, malformed/missing created IDs, non-200 read-backs, and read-back mismatches all fail closed. +- Existing approve/request-changes behavior remains covered. +- Independent exact-head review remains coordinator-owned. ## Final verification evidence -Parked pending #789 terminal. No PR opened. +Acceptance criteria re-verified locally after clean rebase. Branch will be force-pushed with lease for coordinator verification; no PR opened. diff --git a/packages/mosaic/framework/tools/git/pr-review.sh b/packages/mosaic/framework/tools/git/pr-review.sh index f0bf6b89..20a37fbd 100755 --- a/packages/mosaic/framework/tools/git/pr-review.sh +++ b/packages/mosaic/framework/tools/git/pr-review.sh @@ -5,6 +5,7 @@ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=packages/mosaic/framework/tools/git/detect-platform.sh source "$SCRIPT_DIR/detect-platform.sh" # Parse arguments @@ -85,7 +86,10 @@ if [[ "$PLATFORM" == "github" ]]; then elif [[ "$PLATFORM" == "gitea" ]]; then case $ACTION in approve) - tea pr approve "$PR_NUMBER" $(get_gitea_repo_args) ${COMMENT:+--comment "$COMMENT"} + repo=$(get_repo_slug) + host=$(get_remote_host) + login=$(get_gitea_login_for_host "$host") + tea pr approve "$PR_NUMBER" --repo "$repo" --login "$login" ${COMMENT:+--comment "$COMMENT"} echo "Approved Gitea PR #$PR_NUMBER" ;; request-changes) @@ -93,7 +97,10 @@ elif [[ "$PLATFORM" == "gitea" ]]; then echo "Error: Comment required for request-changes" exit 1 fi - tea pr reject "$PR_NUMBER" $(get_gitea_repo_args) --comment "$COMMENT" + repo=$(get_repo_slug) + host=$(get_remote_host) + login=$(get_gitea_login_for_host "$host") + tea pr reject "$PR_NUMBER" --repo "$repo" --login "$login" --comment "$COMMENT" echo "Requested changes on Gitea PR #$PR_NUMBER" ;; comment) @@ -104,42 +111,80 @@ elif [[ "$PLATFORM" == "gitea" ]]; then repo=$(get_repo_slug) host=$(get_remote_host) - login=$(get_gitea_login_for_host "$host") + token=$(get_gitea_token "$host") || { + echo "Error: Gitea token not found for comment persistence" >&2 + exit 1 + } + api_base="https://$host/api/v1/repos/$repo" + payload=$(COMMENT_BODY="$COMMENT" python3 -c ' +import json +import os - comment_json=$(tea comment "$PR_NUMBER" "$COMMENT" --login "$login" --repo "$repo" --output json) - comment_id=$(printf '%s' "$comment_json" | python3 -c ' +print(json.dumps({"body": os.environ["COMMENT_BODY"]})) +') + write_response_file=$(mktemp "${TMPDIR:-/tmp}/mosaic-pr-review-write.XXXXXX") + readback_response_file=$(mktemp "${TMPDIR:-/tmp}/mosaic-pr-review-readback.XXXXXX") + trap 'rm -f "$write_response_file" "$readback_response_file"' EXIT + + if ! write_status=$(curl -sS -o "$write_response_file" -w '%{http_code}' \ + -X POST \ + -H "Authorization: token $token" \ + -H 'Content-Type: application/json' \ + -d "$payload" \ + "$api_base/issues/$PR_NUMBER/comments"); then + echo "Error: Gitea comment write transport failed" >&2 + exit 1 + fi + if [[ "$write_status" != "201" ]]; then + echo "Error: Gitea comment write failed with HTTP $write_status" >&2 + exit 1 + fi + + comment_id=$(python3 - "$write_response_file" <<'PY' import json import sys try: - comment = json.load(sys.stdin) - if isinstance(comment, list) and len(comment) == 1: - comment = comment[0] + with open(sys.argv[1], encoding="utf-8") as response: + comment = json.load(response) comment_id = comment.get("id") if isinstance(comment, dict) else None if not isinstance(comment_id, int) or comment_id <= 0: raise ValueError("missing positive comment id") -except (json.JSONDecodeError, ValueError) as error: +except (OSError, json.JSONDecodeError, ValueError) as error: print(f"Error: could not identify created Gitea comment: {error}", file=sys.stderr) raise SystemExit(1) print(comment_id) -') +PY +) + + if ! readback_status=$(curl -sS -o "$readback_response_file" -w '%{http_code}' \ + -H "Authorization: token $token" \ + "$api_base/issues/comments/$comment_id"); then + echo "Error: Gitea comment read-back transport failed" >&2 + exit 1 + fi + if [[ "$readback_status" != "200" ]]; then + echo "Error: Gitea comment read-back failed with HTTP $readback_status" >&2 + exit 1 + fi - readback_json=$(tea api --login "$login" "/repos/$repo/issues/comments/$comment_id") EXPECTED_COMMENT_ID="$comment_id" EXPECTED_COMMENT_BODY="$COMMENT" EXPECTED_REPO="$repo" EXPECTED_PR_NUMBER="$PR_NUMBER" \ - python3 -c ' + python3 - "$readback_response_file" <<'PY' import json import os import sys from urllib.parse import urlparse try: - comment = json.load(sys.stdin) + with open(sys.argv[1], encoding="utf-8") as response: + comment = json.load(response) + if not isinstance(comment, dict): + raise ValueError("response is not a comment object") expected_id = int(os.environ["EXPECTED_COMMENT_ID"]) expected_body = os.environ["EXPECTED_COMMENT_BODY"] expected_repo = os.environ["EXPECTED_REPO"] expected_pr = os.environ["EXPECTED_PR_NUMBER"] - issue_url = comment.get("issue_url", "") if isinstance(comment, dict) else "" - issue_path = urlparse(issue_url).path.rstrip("/") + issue_path = urlparse(comment.get("issue_url", "")).path.rstrip("/") expected_suffix = f"/repos/{expected_repo}/issues/{expected_pr}" if comment.get("id") != expected_id: raise ValueError("comment id mismatch") @@ -147,10 +192,10 @@ try: raise ValueError("comment body mismatch") if not issue_path.endswith(expected_suffix): raise ValueError("repository or PR mismatch") -except (json.JSONDecodeError, KeyError, TypeError, ValueError) as error: +except (OSError, json.JSONDecodeError, KeyError, TypeError, ValueError) as error: print(f"Error: Gitea comment persistence verification failed: {error}", file=sys.stderr) raise SystemExit(1) -' <<< "$readback_json" +PY echo "Added and verified comment on Gitea PR #$PR_NUMBER (comment ID $comment_id)" ;; diff --git a/packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh b/packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh index 5dd1ccca..8887a065 100644 --- a/packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh +++ b/packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh @@ -7,8 +7,10 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/pr-review-gitea-comment}" REPO_DIR="$WORK_DIR/repo" BIN_DIR="$WORK_DIR/bin" -LOG_FILE="$WORK_DIR/tea.log" +TEA_LOG="$WORK_DIR/tea.log" +CURL_LOG="$WORK_DIR/curl.log" OUTPUT_FILE="$WORK_DIR/output.log" +CREDENTIALS_FILE="$WORK_DIR/credentials.json" cleanup() { rm -rf "$WORK_DIR" @@ -19,11 +21,22 @@ mkdir -p "$REPO_DIR" "$BIN_DIR" git -C "$REPO_DIR" init -q git -C "$REPO_DIR" remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git +cat > "$CREDENTIALS_FILE" <<'JSON' +{ + "gitea": { + "mosaicstack": { + "url": "https://git.mosaicstack.dev", + "token": "test-only-placeholder" + } + } +} +JSON + cat > "$BIN_DIR/tea" <<'SH' #!/usr/bin/env bash set -euo pipefail -printf '%s\n' "$*" >> "$PR_REVIEW_TEST_LOG" +printf '%s\n' "$*" >> "$PR_REVIEW_TEA_LOG" if [[ "$*" == "login list --output json" ]]; then printf '%s\n' '[{"name":"mosaicstack","url":"https://git.mosaicstack.dev"}]' @@ -37,45 +50,15 @@ case "${PR_REVIEW_TEST_MODE:-}" in request-changes) [[ "$*" == "pr reject 123 --repo mosaicstack/stack --login mosaicstack --comment changes-required" ]] || exit 91 ;; - legacy-fallback) + legacy-fallback|comment-success|write-transport-failure|write-http-failure|readback-failure) if [[ "$*" == pr\ comment* ]]; then # tea v0.11.1 treats the nonexistent subcommand as `tea pr list` and exits 0. printf '%s\n' 'INDEX TITLE STATE' exit 0 fi - if [[ "$*" == comment* ]]; then - printf '%s\n' 'supported comment write deliberately unavailable' >&2 - exit 23 - fi + echo "Unexpected tea command: $*" >&2 exit 92 ;; - comment-success|readback-failure) - if [[ "$*" == "comment 123 durable-body --login mosaicstack --repo mosaicstack/stack --output json" ]]; then - printf '%s\n' '{"id":456,"body":"durable-body"}' - exit 0 - fi - if [[ "$*" == "api --login mosaicstack /repos/mosaicstack/stack/issues/comments/456" ]]; then - if [[ "$PR_REVIEW_TEST_MODE" == "readback-failure" ]]; then - printf '%s\n' '{"id":456,"body":"different-body","issue_url":"https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/123"}' - else - printf '%s\n' '{"id":456,"body":"durable-body","issue_url":"https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/123"}' - fi - exit 0 - fi - if [[ "$*" == pr\ comment* ]]; then - # Reproduce the old false-positive fallback if the wrapper regresses. - printf '%s\n' 'INDEX TITLE STATE' - exit 0 - fi - exit 93 - ;; - write-failure) - if [[ "$*" == "comment 123 durable-body --login mosaicstack --repo mosaicstack/stack --output json" ]]; then - printf '%s\n' 'provider rejected comment' >&2 - exit 24 - fi - exit 94 - ;; *) exit 95 ;; @@ -83,25 +66,130 @@ esac SH chmod +x "$BIN_DIR/tea" +cat > "$BIN_DIR/curl" <<'SH' +#!/usr/bin/env bash +set -euo pipefail + +output_file="" +method="GET" +payload="" +url="" +while [[ $# -gt 0 ]]; do + case "$1" in + -o) + output_file="$2" + shift 2 + ;; + -w|-H) + shift 2 + ;; + -X) + method="$2" + shift 2 + ;; + -d|--data) + payload="$2" + shift 2 + ;; + -s|-S|-sS) + shift + ;; + http://*|https://*) + url="$1" + shift + ;; + *) + shift + ;; + esac +done + +printf '%s %s\n' "$method" "$url" >> "$PR_REVIEW_CURL_LOG" + +write_response() { + local status="$1" body="$2" + [[ -n "$output_file" ]] || exit 96 + printf '%s' "$body" > "$output_file" + printf '%s' "$status" +} + +case "${PR_REVIEW_TEST_MODE:-}" in + legacy-fallback|write-transport-failure) + echo "simulated transport failure" >&2 + exit 7 + ;; + write-http-failure) + write_response 500 '{"message":"simulated rejection"}' + ;; + comment-success|readback-failure) + if [[ "$method" == "POST" && "$url" == "https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/123/comments" ]]; then + PR_REVIEW_PAYLOAD="$payload" python3 - <<'PY' +import json +import os + +assert json.loads(os.environ["PR_REVIEW_PAYLOAD"]) == {"body": os.environ["PR_REVIEW_EXPECTED_BODY"]} +PY + response=$(python3 - <<'PY' +import json +import os + +print(json.dumps({"id": 456, "body": os.environ["PR_REVIEW_EXPECTED_BODY"]})) +PY +) + write_response 201 "$response" + elif [[ "$method" == "GET" && "$url" == "https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/comments/456" ]]; then + if [[ "$PR_REVIEW_TEST_MODE" == "readback-failure" ]]; then + body="different-body" + else + body="$PR_REVIEW_EXPECTED_BODY" + fi + response=$(PR_REVIEW_BODY="$body" python3 - <<'PY' +import json +import os + +print(json.dumps({ + "id": 456, + "body": os.environ["PR_REVIEW_BODY"], + "issue_url": "https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/123", +})) +PY +) + write_response 200 "$response" + else + echo "Unexpected curl request: $method $url" >&2 + exit 97 + fi + ;; + *) + exit 98 + ;; +esac +SH +chmod +x "$BIN_DIR/curl" + run_review() { local mode="$1" action="$2" comment="${3:-}" - : > "$LOG_FILE" + : > "$TEA_LOG" + : > "$CURL_LOG" : > "$OUTPUT_FILE" ( cd "$REPO_DIR" PATH="$BIN_DIR:$PATH" \ - PR_REVIEW_TEST_LOG="$LOG_FILE" \ + MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \ + PR_REVIEW_TEA_LOG="$TEA_LOG" \ + PR_REVIEW_CURL_LOG="$CURL_LOG" \ PR_REVIEW_TEST_MODE="$mode" \ + PR_REVIEW_EXPECTED_BODY="$comment" \ "$SCRIPT_DIR/pr-review.sh" -n 123 -a "$action" ${comment:+-c "$comment"} ) > "$OUTPUT_FILE" 2>&1 } run_review approve approve -grep -q '^pr approve 123 --repo mosaicstack/stack --login mosaicstack$' "$LOG_FILE" +grep -q '^pr approve 123 --repo mosaicstack/stack --login mosaicstack$' "$TEA_LOG" grep -q 'Approved Gitea PR #123' "$OUTPUT_FILE" run_review request-changes request-changes changes-required -grep -q '^pr reject 123 --repo mosaicstack/stack --login mosaicstack --comment changes-required$' "$LOG_FILE" +grep -q '^pr reject 123 --repo mosaicstack/stack --login mosaicstack --comment changes-required$' "$TEA_LOG" grep -q 'Requested changes on Gitea PR #123' "$OUTPUT_FILE" if run_review legacy-fallback comment durable-body; then @@ -109,7 +197,7 @@ if run_review legacy-fallback comment durable-body; then cat "$OUTPUT_FILE" >&2 exit 1 fi -if grep -q '^pr comment ' "$LOG_FILE"; then +if grep -q '^pr comment ' "$TEA_LOG"; then echo "Wrapper invoked unsupported tea pr comment" >&2 exit 1 fi @@ -118,20 +206,25 @@ if grep -q 'Added comment to Gitea PR' "$OUTPUT_FILE"; then exit 1 fi -run_review comment-success comment durable-body -grep -q '^comment 123 durable-body --login mosaicstack --repo mosaicstack/stack --output json$' "$LOG_FILE" -grep -q '^api --login mosaicstack /repos/mosaicstack/stack/issues/comments/456$' "$LOG_FILE" +complex_body=$'durable "body"\n-- marker' +run_review comment-success comment "$complex_body" +grep -q '^POST https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/123/comments$' "$CURL_LOG" +grep -q '^GET https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/comments/456$' "$CURL_LOG" grep -q 'Added and verified comment on Gitea PR #123' "$OUTPUT_FILE" - -if run_review write-failure comment durable-body; then - echo "Expected provider write failure to return nonzero" >&2 - exit 1 -fi -if grep -q 'Added and verified comment' "$OUTPUT_FILE"; then - echo "Write failure reported durable success" >&2 +if [[ -s "$TEA_LOG" ]]; then + echo "REST comment path unexpectedly invoked tea" >&2 + cat "$TEA_LOG" >&2 exit 1 fi +if run_review write-transport-failure comment durable-body; then + echo "Expected provider transport failure to return nonzero" >&2 + exit 1 +fi +if run_review write-http-failure comment durable-body; then + echo "Expected non-201 provider write to return nonzero" >&2 + exit 1 +fi if run_review readback-failure comment durable-body; then echo "Expected mismatched provider read-back to return nonzero" >&2 exit 1 -- 2.49.1 From 1b1902010a0b95ab8026bee42636afce38d0119f Mon Sep 17 00:00:00 2001 From: ms-lead-reviewer Date: Mon, 20 Jul 2026 00:54:18 -0500 Subject: [PATCH 5/6] fix(#812): preserve configured gitea API base URL --- docs/scratchpads/812-pr-review-comment.md | 9 ++- .../framework/tools/git/detect-platform.sh | 58 ++++++++++++++++++- .../mosaic/framework/tools/git/pr-review.sh | 6 +- .../tools/git/test-pr-review-gitea-comment.sh | 48 ++++++++++----- 4 files changed, 101 insertions(+), 20 deletions(-) diff --git a/docs/scratchpads/812-pr-review-comment.md b/docs/scratchpads/812-pr-review-comment.md index 28ebec14..c98e9bc3 100644 --- a/docs/scratchpads/812-pr-review-comment.md +++ b/docs/scratchpads/812-pr-review-comment.md @@ -42,10 +42,15 @@ Make the Gitea `comment` action in `packages/mosaic/framework/tools/git/pr-revie ## Risks / blockers - No active implementation blocker. #789 reached terminal merged state and the coordination hold was lifted. +- Review round 1 found one portability blocker: the API base reconstructed `https://$host` and discarded configured schemes/path prefixes. +- Remediation adds host-matched configured Gitea URL resolution from the same credential source as token resolution; both POST and GET preserve the configured scheme and normalized path prefix. - REST transport failures, non-201 writes, malformed/missing created IDs, non-200 read-backs, and read-back mismatches all fail closed. - Existing approve/request-changes behavior remains covered. -- Independent exact-head review remains coordinator-owned. +- Independent exact-head re-review remains coordinator-owned. ## Final verification evidence -Acceptance criteria re-verified locally after clean rebase. Branch will be force-pushed with lease for coordinator verification; no PR opened. +- URL-portability regression was RED before remediation at the new `http://git.mosaicstack.dev` case and GREEN afterward. +- Regression coverage verifies POST and read-back GET for both `http://git.mosaicstack.dev` and `https://git.mosaicstack.dev/gitea/` configuration. +- Focused shell checks, all git-wrapper harnesses, and full repository test/typecheck/lint/format gates passed after remediation. +- Branch will be force-pushed with lease for coordinator re-verification; no PR opened. diff --git a/packages/mosaic/framework/tools/git/detect-platform.sh b/packages/mosaic/framework/tools/git/detect-platform.sh index 7519f11a..23f05e7a 100755 --- a/packages/mosaic/framework/tools/git/detect-platform.sh +++ b/packages/mosaic/framework/tools/git/detect-platform.sh @@ -81,7 +81,14 @@ get_repo_slug() { gitea_url_matches_host() { local url="${1:-}" host="${2:-}" [[ -n "$url" && -n "$host" ]] || return 1 - [[ "${url%/}" == "https://$host" || "${url%/}" == "http://$host" || "${url%/}" == *"//$host" ]] + python3 - "$url" "$host" <<'PY' +import sys +from urllib.parse import urlparse + +url, host = sys.argv[1:] +parsed = urlparse(url) +raise SystemExit(0 if parsed.scheme in {"http", "https"} and parsed.hostname == host else 1) +PY } get_gitea_service_for_host() { @@ -377,6 +384,51 @@ get_remote_host() { return 1 } +# Resolve the configured Gitea base URL for a host from the same credential +# source used by get_gitea_token. The scheme and any deployment path prefix are +# provider configuration and must not be reconstructed from the git remote. +get_gitea_url_for_host() { + local host="$1" script_dir cred_loader url + script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + cred_loader="$script_dir/../_lib/credentials.sh" + + if [[ -f "$cred_loader" ]]; then + url=$( + # shellcheck source=/dev/null + source "$cred_loader" + unset GITEA_TOKEN GITEA_URL + case "$host" in + git.mosaicstack.dev) load_credentials gitea-mosaicstack 2>/dev/null ;; + git.uscllc.com) load_credentials gitea-usc 2>/dev/null ;; + *) + for svc in gitea-mosaicstack gitea-usc; do + unset GITEA_TOKEN GITEA_URL + load_credentials "$svc" 2>/dev/null || continue + if gitea_url_matches_host "${GITEA_URL:-}" "$host"; then + break + fi + unset GITEA_TOKEN GITEA_URL + done + ;; + esac + if gitea_url_matches_host "${GITEA_URL:-}" "$host"; then + printf '%s' "${GITEA_URL%/}" + fi + ) + if [[ -n "$url" ]]; then + printf '%s\n' "$url" + return 0 + fi + fi + + if gitea_url_matches_host "${GITEA_URL:-}" "$host"; then + printf '%s\n' "${GITEA_URL%/}" + return 0 + fi + + return 1 +} + # Resolve a Gitea API token for the given host. # Priority: Mosaic credential loader → GITEA_TOKEN env → ~/.git-credentials get_gitea_token() { @@ -403,7 +455,7 @@ get_gitea_token() { for svc in gitea-mosaicstack gitea-usc; do unset GITEA_TOKEN GITEA_URL load_credentials "$svc" 2>/dev/null || continue - if [[ "${GITEA_URL:-}" == "https://$host" || "${GITEA_URL:-}" == "http://$host" || "${GITEA_URL:-}" == *"//$host" ]]; then + if gitea_url_matches_host "${GITEA_URL:-}" "$host"; then matched=true break fi @@ -423,7 +475,7 @@ get_gitea_token() { # 2. GITEA_TOKEN env var (only when GITEA_URL, if present, matches the remote host) if [[ -n "${GITEA_TOKEN:-}" ]]; then - if [[ -z "${GITEA_URL:-}" || "${GITEA_URL:-}" == "https://$host" || "${GITEA_URL:-}" == "http://$host" || "${GITEA_URL:-}" == *"//$host" ]]; then + if [[ -z "${GITEA_URL:-}" ]] || gitea_url_matches_host "$GITEA_URL" "$host"; then echo "$GITEA_TOKEN" return 0 fi diff --git a/packages/mosaic/framework/tools/git/pr-review.sh b/packages/mosaic/framework/tools/git/pr-review.sh index 20a37fbd..2e0f138a 100755 --- a/packages/mosaic/framework/tools/git/pr-review.sh +++ b/packages/mosaic/framework/tools/git/pr-review.sh @@ -115,7 +115,11 @@ elif [[ "$PLATFORM" == "gitea" ]]; then echo "Error: Gitea token not found for comment persistence" >&2 exit 1 } - api_base="https://$host/api/v1/repos/$repo" + configured_url=$(get_gitea_url_for_host "$host") || { + echo "Error: Configured Gitea URL not found for comment persistence" >&2 + exit 1 + } + api_base="${configured_url%/}/api/v1/repos/$repo" payload=$(COMMENT_BODY="$COMMENT" python3 -c ' import json import os diff --git a/packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh b/packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh index 8887a065..6515a756 100644 --- a/packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh +++ b/packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh @@ -21,16 +21,24 @@ mkdir -p "$REPO_DIR" "$BIN_DIR" git -C "$REPO_DIR" init -q git -C "$REPO_DIR" remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git -cat > "$CREDENTIALS_FILE" <<'JSON' -{ - "gitea": { - "mosaicstack": { - "url": "https://git.mosaicstack.dev", - "token": "test-only-placeholder" - } - } +write_credentials() { + local configured_url="$1" + CONFIGURED_GITEA_URL="$configured_url" python3 - "$CREDENTIALS_FILE" <<'PY' +import json +import os +import sys + +with open(sys.argv[1], "w", encoding="utf-8") as credentials: + json.dump({ + "gitea": { + "mosaicstack": { + "url": os.environ["CONFIGURED_GITEA_URL"], + "token": "test-only-placeholder", + } + } + }, credentials) +PY } -JSON cat > "$BIN_DIR/tea" <<'SH' #!/usr/bin/env bash @@ -50,7 +58,7 @@ case "${PR_REVIEW_TEST_MODE:-}" in request-changes) [[ "$*" == "pr reject 123 --repo mosaicstack/stack --login mosaicstack --comment changes-required" ]] || exit 91 ;; - legacy-fallback|comment-success|write-transport-failure|write-http-failure|readback-failure) + legacy-fallback|comment-success|http-success|prefix-success|write-transport-failure|write-http-failure|readback-failure) if [[ "$*" == pr\ comment* ]]; then # tea v0.11.1 treats the nonexistent subcommand as `tea pr list` and exits 0. printf '%s\n' 'INDEX TITLE STATE' @@ -121,8 +129,8 @@ case "${PR_REVIEW_TEST_MODE:-}" in write-http-failure) write_response 500 '{"message":"simulated rejection"}' ;; - comment-success|readback-failure) - if [[ "$method" == "POST" && "$url" == "https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/123/comments" ]]; then + comment-success|http-success|prefix-success|readback-failure) + if [[ "$method" == "POST" && "$url" == "$PR_REVIEW_EXPECTED_API_BASE/issues/123/comments" ]]; then PR_REVIEW_PAYLOAD="$payload" python3 - <<'PY' import json import os @@ -137,7 +145,7 @@ print(json.dumps({"id": 456, "body": os.environ["PR_REVIEW_EXPECTED_BODY"]})) PY ) write_response 201 "$response" - elif [[ "$method" == "GET" && "$url" == "https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/comments/456" ]]; then + elif [[ "$method" == "GET" && "$url" == "$PR_REVIEW_EXPECTED_API_BASE/issues/comments/456" ]]; then if [[ "$PR_REVIEW_TEST_MODE" == "readback-failure" ]]; then body="different-body" else @@ -150,7 +158,7 @@ import os print(json.dumps({ "id": 456, "body": os.environ["PR_REVIEW_BODY"], - "issue_url": "https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/123", + "issue_url": os.environ["PR_REVIEW_EXPECTED_API_BASE"] + "/issues/123", })) PY ) @@ -169,6 +177,9 @@ chmod +x "$BIN_DIR/curl" run_review() { local mode="$1" action="$2" comment="${3:-}" + local configured_url="${4:-https://git.mosaicstack.dev}" + local expected_api_base="${configured_url%/}/api/v1/repos/mosaicstack/stack" + write_credentials "$configured_url" : > "$TEA_LOG" : > "$CURL_LOG" : > "$OUTPUT_FILE" @@ -180,6 +191,7 @@ run_review() { PR_REVIEW_CURL_LOG="$CURL_LOG" \ PR_REVIEW_TEST_MODE="$mode" \ PR_REVIEW_EXPECTED_BODY="$comment" \ + PR_REVIEW_EXPECTED_API_BASE="$expected_api_base" \ "$SCRIPT_DIR/pr-review.sh" -n 123 -a "$action" ${comment:+-c "$comment"} ) > "$OUTPUT_FILE" 2>&1 } @@ -217,6 +229,14 @@ if [[ -s "$TEA_LOG" ]]; then exit 1 fi +run_review http-success comment durable-body http://git.mosaicstack.dev +grep -q '^POST http://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/123/comments$' "$CURL_LOG" +grep -q '^GET http://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/comments/456$' "$CURL_LOG" + +run_review prefix-success comment durable-body https://git.mosaicstack.dev/gitea/ +grep -q '^POST https://git.mosaicstack.dev/gitea/api/v1/repos/mosaicstack/stack/issues/123/comments$' "$CURL_LOG" +grep -q '^GET https://git.mosaicstack.dev/gitea/api/v1/repos/mosaicstack/stack/issues/comments/456$' "$CURL_LOG" + if run_review write-transport-failure comment durable-body; then echo "Expected provider transport failure to return nonzero" >&2 exit 1 -- 2.49.1 From 571b65d336af7236692e1ed3883d5570767d696e Mon Sep 17 00:00:00 2001 From: ms-lead-reviewer Date: Mon, 20 Jul 2026 01:11:42 -0500 Subject: [PATCH 6/6] fix(#812): resolve subpath-mounted gitea repo slug --- docs/scratchpads/812-pr-review-comment.md | 6 +- .../framework/tools/git/detect-platform.sh | 64 ++++++++++++++++++- .../mosaic/framework/tools/git/pr-review.sh | 5 +- .../tools/git/test-pr-review-gitea-comment.sh | 27 +++++++- 4 files changed, 93 insertions(+), 9 deletions(-) diff --git a/docs/scratchpads/812-pr-review-comment.md b/docs/scratchpads/812-pr-review-comment.md index c98e9bc3..e50e27a3 100644 --- a/docs/scratchpads/812-pr-review-comment.md +++ b/docs/scratchpads/812-pr-review-comment.md @@ -43,7 +43,8 @@ Make the Gitea `comment` action in `packages/mosaic/framework/tools/git/pr-revie - No active implementation blocker. #789 reached terminal merged state and the coordination hold was lifted. - Review round 1 found one portability blocker: the API base reconstructed `https://$host` and discarded configured schemes/path prefixes. -- Remediation adds host-matched configured Gitea URL resolution from the same credential source as token resolution; both POST and GET preserve the configured scheme and normalized path prefix. +- Review round 2 found a second subpath portability blocker: clone-derived `get_repo_slug` retained the deployment prefix, duplicating it under `/api/v1/repos/`. +- Round 3 resolves owner/repo relative to the configured Gitea base path for HTTP(S) clones while preserving root-mounted and SSH clone forms. Host matching now compares non-default ports consistently. - REST transport failures, non-201 writes, malformed/missing created IDs, non-200 read-backs, and read-back mismatches all fail closed. - Existing approve/request-changes behavior remains covered. - Independent exact-head re-review remains coordinator-owned. @@ -51,6 +52,7 @@ Make the Gitea `comment` action in `packages/mosaic/framework/tools/git/pr-revie ## Final verification evidence - URL-portability regression was RED before remediation at the new `http://git.mosaicstack.dev` case and GREEN afterward. -- Regression coverage verifies POST and read-back GET for both `http://git.mosaicstack.dev` and `https://git.mosaicstack.dev/gitea/` configuration. +- Round-3 genuine subpath regression was RED against round-2 head `1b190201` and GREEN after the fix: `https://git.example/gitea/owner/repo.git` maps to API repository `owner/repo` under configured base `/gitea`. +- Regression coverage verifies POST and read-back GET for root-mounted HTTP(S), path-prefixed HTTP(S), non-default HTTP port, scp-style SSH, and `ssh://` clone forms. - Focused shell checks, all git-wrapper harnesses, and full repository test/typecheck/lint/format gates passed after remediation. - Branch will be force-pushed with lease for coordinator re-verification; no PR opened. diff --git a/packages/mosaic/framework/tools/git/detect-platform.sh b/packages/mosaic/framework/tools/git/detect-platform.sh index 23f05e7a..38a6f230 100755 --- a/packages/mosaic/framework/tools/git/detect-platform.sh +++ b/packages/mosaic/framework/tools/git/detect-platform.sh @@ -85,9 +85,21 @@ gitea_url_matches_host() { import sys from urllib.parse import urlparse -url, host = sys.argv[1:] -parsed = urlparse(url) -raise SystemExit(0 if parsed.scheme in {"http", "https"} and parsed.hostname == host else 1) +url, remote_host = sys.argv[1:] +configured = urlparse(url) +remote = urlparse(f"//{remote_host}") +if configured.scheme not in {"http", "https"} or configured.hostname != remote.hostname: + raise SystemExit(1) + +configured_port = configured.port +remote_port = remote.port +if remote_port is None: + default_port = 80 if configured.scheme == "http" else 443 + if configured_port not in {None, default_port}: + raise SystemExit(1) +elif configured_port != remote_port: + raise SystemExit(1) +raise SystemExit(0) PY } @@ -354,6 +366,47 @@ get_gitea_api_host_for_repo_override() { get_host_from_url "${GITEA_URL:-}" } +# Resolve owner/repo relative to a configured Gitea base URL. HTTP(S) clone +# URLs can include the deployment prefix (for example /gitea/owner/repo.git), +# but Gitea's /repos API expects only owner/repo. Root-mounted and SSH clone +# forms retain their existing owner/repo behavior. +get_gitea_repo_slug_for_url() { + local configured_url="$1" remote_url + remote_url=$(git remote get-url origin 2>/dev/null) || return 1 + + if [[ "$remote_url" =~ ^https?:// ]]; then + python3 - "$remote_url" "$configured_url" <<'PY' +import sys +from urllib.parse import urlparse + +remote = urlparse(sys.argv[1]) +base = urlparse(sys.argv[2]) +remote_path = remote.path.strip("/") +if remote_path.endswith(".git"): + remote_path = remote_path[:-4] +base_path = base.path.strip("/") +remote_parts = [part for part in remote_path.split("/") if part] +base_parts = [part for part in base_path.split("/") if part] + +if base_parts and remote_parts[:len(base_parts)] == base_parts: + repo_parts = remote_parts[len(base_parts):] +elif len(remote_parts) == 2: + # Preserve a root-shaped clone URL when provider API configuration carries + # a reverse-proxy prefix separately. + repo_parts = remote_parts +else: + raise SystemExit(1) + +if len(repo_parts) != 2: + raise SystemExit(1) +print("/".join(repo_parts)) +PY + return + fi + + get_repo_slug +} + get_gitea_repo_args() { local repo host login repo=$(get_repo_slug) || return 1 @@ -377,6 +430,11 @@ get_remote_host() { echo "${host##*@}" return 0 fi + if [[ "$remote_url" =~ ^ssh://([^/]+)/ ]]; then + local host="${BASH_REMATCH[1]}" + echo "${host##*@}" + return 0 + fi if [[ "$remote_url" =~ ^git@([^:]+): ]]; then echo "${BASH_REMATCH[1]}" return 0 diff --git a/packages/mosaic/framework/tools/git/pr-review.sh b/packages/mosaic/framework/tools/git/pr-review.sh index 2e0f138a..9afd1687 100755 --- a/packages/mosaic/framework/tools/git/pr-review.sh +++ b/packages/mosaic/framework/tools/git/pr-review.sh @@ -109,7 +109,6 @@ elif [[ "$PLATFORM" == "gitea" ]]; then exit 1 fi - repo=$(get_repo_slug) host=$(get_remote_host) token=$(get_gitea_token "$host") || { echo "Error: Gitea token not found for comment persistence" >&2 @@ -119,6 +118,10 @@ elif [[ "$PLATFORM" == "gitea" ]]; then echo "Error: Configured Gitea URL not found for comment persistence" >&2 exit 1 } + repo=$(get_gitea_repo_slug_for_url "$configured_url") || { + echo "Error: Could not resolve Gitea owner/repository relative to configured URL" >&2 + exit 1 + } api_base="${configured_url%/}/api/v1/repos/$repo" payload=$(COMMENT_BODY="$COMMENT" python3 -c ' import json diff --git a/packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh b/packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh index 6515a756..16641f56 100644 --- a/packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh +++ b/packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh @@ -58,7 +58,7 @@ case "${PR_REVIEW_TEST_MODE:-}" in request-changes) [[ "$*" == "pr reject 123 --repo mosaicstack/stack --login mosaicstack --comment changes-required" ]] || exit 91 ;; - legacy-fallback|comment-success|http-success|prefix-success|write-transport-failure|write-http-failure|readback-failure) + legacy-fallback|comment-success|http-success|prefix-success|subpath-success|port-success|scp-ssh-success|url-ssh-success|write-transport-failure|write-http-failure|readback-failure) if [[ "$*" == pr\ comment* ]]; then # tea v0.11.1 treats the nonexistent subcommand as `tea pr list` and exits 0. printf '%s\n' 'INDEX TITLE STATE' @@ -129,7 +129,7 @@ case "${PR_REVIEW_TEST_MODE:-}" in write-http-failure) write_response 500 '{"message":"simulated rejection"}' ;; - comment-success|http-success|prefix-success|readback-failure) + comment-success|http-success|prefix-success|subpath-success|port-success|scp-ssh-success|url-ssh-success|readback-failure) if [[ "$method" == "POST" && "$url" == "$PR_REVIEW_EXPECTED_API_BASE/issues/123/comments" ]]; then PR_REVIEW_PAYLOAD="$payload" python3 - <<'PY' import json @@ -178,7 +178,10 @@ chmod +x "$BIN_DIR/curl" run_review() { local mode="$1" action="$2" comment="${3:-}" local configured_url="${4:-https://git.mosaicstack.dev}" - local expected_api_base="${configured_url%/}/api/v1/repos/mosaicstack/stack" + local remote_url="${5:-https://git.mosaicstack.dev/mosaicstack/stack.git}" + local expected_repo="${6:-mosaicstack/stack}" + local expected_api_base="${configured_url%/}/api/v1/repos/$expected_repo" + git -C "$REPO_DIR" remote set-url origin "$remote_url" write_credentials "$configured_url" : > "$TEA_LOG" : > "$CURL_LOG" @@ -237,6 +240,24 @@ run_review prefix-success comment durable-body https://git.mosaicstack.dev/gitea grep -q '^POST https://git.mosaicstack.dev/gitea/api/v1/repos/mosaicstack/stack/issues/123/comments$' "$CURL_LOG" grep -q '^GET https://git.mosaicstack.dev/gitea/api/v1/repos/mosaicstack/stack/issues/comments/456$' "$CURL_LOG" +run_review subpath-success comment durable-body https://git.example/gitea https://git.example/gitea/owner/repo.git owner/repo +grep -q '^POST https://git.example/gitea/api/v1/repos/owner/repo/issues/123/comments$' "$CURL_LOG" +grep -q '^GET https://git.example/gitea/api/v1/repos/owner/repo/issues/comments/456$' "$CURL_LOG" +if grep -q '/repos/gitea/owner/repo/' "$CURL_LOG"; then + echo "Configured Gitea path prefix leaked into the repository slug" >&2 + exit 1 +fi + +run_review port-success comment durable-body http://git.example:3000 http://git.example:3000/owner/repo.git owner/repo +grep -q '^POST http://git.example:3000/api/v1/repos/owner/repo/issues/123/comments$' "$CURL_LOG" +grep -q '^GET http://git.example:3000/api/v1/repos/owner/repo/issues/comments/456$' "$CURL_LOG" + +run_review scp-ssh-success comment durable-body https://git.example git@git.example:owner/repo.git owner/repo +grep -q '^POST https://git.example/api/v1/repos/owner/repo/issues/123/comments$' "$CURL_LOG" + +run_review url-ssh-success comment durable-body https://git.example ssh://git@git.example/owner/repo.git owner/repo +grep -q '^POST https://git.example/api/v1/repos/owner/repo/issues/123/comments$' "$CURL_LOG" + if run_review write-transport-failure comment durable-body; then echo "Expected provider transport failure to return nonzero" >&2 exit 1 -- 2.49.1