Files
stack/packages/mosaic/framework/tools/git/pr-review.sh
ms-835-build 2e8e8aa83b
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
fix(framework): drop unsupported --comment from tea pr approve/reject; route review body via durable comment (#835)
tea v0.11.1 defines no --comment/-comment flag on `pr approve` or `pr
reject`, so pr-review.sh's Gitea approve/reject paths errored with
"flag provided but not defined: -comment" whenever a review body was
supplied, breaking the durable formal-review path fleet-wide.

Remove --comment from both tea invocations and, when a review body is
present, post it via the same verified Gitea comments REST API path
the `comment` action already uses (#812) — extracted into a shared
gitea_post_verified_comment() helper so all three actions share one
write+read-back-verified implementation instead of duplicating it.

Extends the existing #812 regression harness
(test-pr-review-gitea-comment.sh) with a `tea` stub that rejects
unknown flags exactly like real tea v0.11.1, so it fails RED against
the pre-fix wrapper and passes GREEN after.

Closes #835.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 03:58:51 -05:00

253 lines
8.9 KiB
Bash
Executable File

#!/bin/bash
# pr-review.sh - Review a pull request on GitHub or Gitea
# Usage: pr-review.sh -n <pr_number> -a <action> [-c <comment>]
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
PR_NUMBER=""
ACTION=""
COMMENT=""
while [[ $# -gt 0 ]]; do
case $1 in
-n|--number)
PR_NUMBER="$2"
shift 2
;;
-a|--action)
ACTION="$2"
shift 2
;;
-c|--comment)
COMMENT="$2"
shift 2
;;
-h|--help)
echo "Usage: pr-review.sh -n <pr_number> -a <action> [-c <comment>]"
echo ""
echo "Options:"
echo " -n, --number PR number (required)"
echo " -a, --action Review action: approve, request-changes, comment (required)"
echo " -c, --comment Review comment (required for request-changes)"
echo " -h, --help Show this help"
exit 0
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
done
if [[ -z "$PR_NUMBER" ]]; then
echo "Error: PR number is required (-n)"
exit 1
fi
if [[ -z "$ACTION" ]]; then
echo "Error: Action is required (-a): approve, request-changes, comment"
exit 1
fi
detect_platform >/dev/null
# Post a review comment body to a Gitea PR via the supported comments REST API
# and verify it durably via provider read-back (see docs on durable review
# provenance in README.md). Used by the `comment` action and, since `tea`
# v0.11.1 defines no `--comment`/`-comment` flag on `pr approve`/`pr reject`,
# also by the `approve` and `request-changes` actions to carry an optional
# review body that `tea` itself cannot attach.
#
# Args: $1 = PR number, $2 = comment body
# On success: prints only the created comment ID to stdout, returns 0.
# On failure: prints an error to stderr, returns 1.
gitea_post_verified_comment() {
local pr_number="$1" comment_body="$2"
local host token configured_url repo api_base payload
local write_response_file readback_response_file comment_id
host=$(get_remote_host)
token=$(get_gitea_token "$host") || {
echo "Error: Gitea token not found for comment persistence" >&2
return 1
}
configured_url=$(get_gitea_url_for_host "$host") || {
echo "Error: Configured Gitea URL not found for comment persistence" >&2
return 1
}
repo=$(get_gitea_repo_slug_for_url "$configured_url") || {
echo "Error: Could not resolve Gitea owner/repository relative to configured URL" >&2
return 1
}
api_base="${configured_url%/}/api/v1/repos/$repo"
payload=$(COMMENT_BODY="$comment_body" python3 -c '
import json
import os
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"' RETURN
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
return 1
fi
if [[ "$write_status" != "201" ]]; then
echo "Error: Gitea comment write failed with HTTP $write_status" >&2
return 1
fi
comment_id=$(python3 - "$write_response_file" <<'PY'
import json
import sys
try:
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 (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
) || return 1
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
return 1
fi
if [[ "$readback_status" != "200" ]]; then
echo "Error: Gitea comment read-back failed with HTTP $readback_status" >&2
return 1
fi
if EXPECTED_COMMENT_ID="$comment_id" EXPECTED_COMMENT_BODY="$comment_body" EXPECTED_REPO="$repo" EXPECTED_PR_NUMBER="$pr_number" \
python3 - "$readback_response_file" <<'PY'
import json
import os
import sys
from urllib.parse import urlparse
try:
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_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")
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 (OSError, json.JSONDecodeError, KeyError, TypeError, ValueError) as error:
print(f"Error: Gitea comment persistence verification failed: {error}", file=sys.stderr)
raise SystemExit(1)
PY
then
true
else
return 1
fi
echo "$comment_id"
return 0
}
if [[ "$PLATFORM" == "github" ]]; then
case $ACTION in
approve)
gh pr review "$PR_NUMBER" --approve ${COMMENT:+--body "$COMMENT"}
echo "Approved GitHub PR #$PR_NUMBER"
;;
request-changes)
if [[ -z "$COMMENT" ]]; then
echo "Error: Comment required for request-changes"
exit 1
fi
gh pr review "$PR_NUMBER" --request-changes --body "$COMMENT"
echo "Requested changes on GitHub PR #$PR_NUMBER"
;;
comment)
if [[ -z "$COMMENT" ]]; then
echo "Error: Comment required"
exit 1
fi
gh pr review "$PR_NUMBER" --comment --body "$COMMENT"
echo "Added review comment to GitHub PR #$PR_NUMBER"
;;
*)
echo "Error: Unknown action: $ACTION"
exit 1
;;
esac
elif [[ "$PLATFORM" == "gitea" ]]; then
case $ACTION in
approve)
repo=$(get_repo_slug)
host=$(get_remote_host)
login=$(get_gitea_login_for_host "$host")
# tea v0.11.1 defines no --comment/-comment flag on `pr approve`;
# route any review body via the durable comment API instead (#835).
tea pr approve "$PR_NUMBER" --repo "$repo" --login "$login"
echo "Approved Gitea PR #$PR_NUMBER"
if [[ -n "$COMMENT" ]]; then
comment_id=$(gitea_post_verified_comment "$PR_NUMBER" "$COMMENT") || exit 1
echo "Added and verified review comment on Gitea PR #$PR_NUMBER (comment ID $comment_id)"
fi
;;
request-changes)
if [[ -z "$COMMENT" ]]; then
echo "Error: Comment required for request-changes"
exit 1
fi
repo=$(get_repo_slug)
host=$(get_remote_host)
login=$(get_gitea_login_for_host "$host")
# tea v0.11.1 defines no --comment/-comment flag on `pr reject`;
# route the review body via the durable comment API instead (#835).
tea pr reject "$PR_NUMBER" --repo "$repo" --login "$login"
echo "Requested changes on Gitea PR #$PR_NUMBER"
comment_id=$(gitea_post_verified_comment "$PR_NUMBER" "$COMMENT") || exit 1
echo "Added and verified review comment on Gitea PR #$PR_NUMBER (comment ID $comment_id)"
;;
comment)
if [[ -z "$COMMENT" ]]; then
echo "Error: Comment required"
exit 1
fi
comment_id=$(gitea_post_verified_comment "$PR_NUMBER" "$COMMENT") || exit 1
echo "Added and verified comment on Gitea PR #$PR_NUMBER (comment ID $comment_id)"
;;
*)
echo "Error: Unknown action: $ACTION"
exit 1
;;
esac
else
echo "Error: Unknown platform"
exit 1
fi