fix: close ungated CLI merge bypasses in review_pr.py and merge_pr.py (#16)

Reviewer found the MCP merge surface is gated but two local CLI scripts remain
ungated merge paths that LLM automations in this project have been using.
Close them (Option B — minimal safe fix; full gated CLI merge left to a
follow-up):

- review_pr.py: `--merge` now fails closed BEFORE any API call with a clear
  message directing callers to the gated `gitea_merge_pr` MCP workflow. The
  review-only path is unchanged. The merge execution block was removed.
- merge_pr.py: main() is now a fail-closed no-op — reads no credentials and
  makes no merge API call; prints that merge is only available via the gated
  workflow.
- README: the `review_pr.py` row and Quick Examples no longer advertise a CLI
  `--merge` path; added an audit-logging clarification that #16 returns
  structured gate/merge results but does not add durable audit logging, which
  is tracked by #18.

Tests updated/added:
- test_review_pr.py: `--merge` fails closed with no API call; message points to
  the gated workflow.
- test_merge_pr.py: merge fails closed with no API call, even with
  --force/--do/--title/--message; message points to the gated workflow.
- test_mcp_server.py: README no longer advertises the ungated CLI merge example.

The gated MCP `gitea_merge_pr` is unchanged and still gated; `gitea_review_pr`
still fails closed on merge=True; `gitea_submit_pr_review` still cannot merge.
No secrets, auth headers, raw env, or credential paths are exposed. No
Jenkins/Ops/GlitchTip/Release/deploy/CI behavior added. #17/#18 not started.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 15:40:34 -04:00
parent f04cf44975
commit 4dee03b2aa
6 changed files with 105 additions and 88 deletions
+18 -19
View File
@@ -37,6 +37,22 @@ def main(argv=None):
help="Merge method/style to use if merging (default: merge).")
args = parser.parse_args(argv)
# Fail closed: direct CLI merge is disabled (#16). LLM automations were
# using this flag as an ungated merge bypass. Merge is only available via
# the gated `gitea_merge_pr` MCP workflow, which enforces
# identity/profile/eligibility, explicit confirmation, expected head SHA,
# and self-merge protection. No API call is made here.
if args.merge:
print(
"Direct CLI merge is disabled. Merge is only available through the "
"gated #16 workflow (MCP tool 'gitea_merge_pr'), which enforces "
"identity/profile/eligibility, explicit confirmation, expected head "
"SHA checking, and self-merge protection. Re-run without --merge to "
"submit a review only.",
file=sys.stderr,
)
return 2
host, org, repo = resolve_remote(args)
body = args.body
@@ -80,25 +96,8 @@ def main(argv=None):
print(f"Error submitting review: {e}", file=sys.stderr)
return 1
# 3. Merge PR if --merge is requested and event is APPROVE
if args.merge:
if args.event != "APPROVE":
print("Warning: Skipping merge because review event is not 'APPROVE'.", file=sys.stderr)
return 0
merge_url = f"{repo_api_url(host, org, repo)}/pulls/{args.pr_number}/merge"
merge_payload = {
"Do": args.merge_method,
"force_merge": False
}
try:
api_request("POST", merge_url, auth, merge_payload)
print(f"Successfully merged PR #{args.pr_number} using '{args.merge_method}' method.")
except Exception as e:
print(f"Error merging PR: {e}", file=sys.stderr)
return 1
# Merge is intentionally not performed here — see the fail-closed guard
# above. Use the gated `gitea_merge_pr` MCP workflow (#16) to merge.
return 0