The merge request previously posted an empty object, which this Gitea
instance's published MergePullRequestOption schema rejects. Post the
supported merge mode {"Do": "merge"} plus the head_commit_id read from
the PR, so a branch that moved after review cannot be merged
accidentally. Fail closed (GiteaClientError) when the PR record carries
no head commit id. Regression tests assert the exact request shape.
Implement the review-policy protocol on the production GiteaClient:
- list_open_prs(repo): all open PRs, Gitea pagination, normalized shape
- add_label / remove_label: resolve repo label id, mutate, verify state
- merge_pr(repo, pr): merge endpoint + merged confirmation (bool)
- close_issue(repo, n): state update + closed confirmation
- get_pr() now returns the normalized candidate shape (number, body,
head_sha, base_ref, labels as names, repo, changed_files)
Fail-closed: non-2xx responses raise GiteaApiError (token/authorization
header never leak, even when servers echo the request back); unknown
labels and failed post-condition verifications raise GiteaClientError.
Pagination has a defensive page cap.
Hermetic tests (tests/test_gitea_client.py) script a local threaded HTTP
stub and assert request methods, paths, bodies, pagination, normalized
results, failure handling, and a source-derived contract test that the
production client exposes every operation ReviewPolicy/reviewer call.