gitea_resolve_api_for_login silently fell back to the host-default identity whenever the named login could not be resolved for ANY reason -- including when the name came from an EXPLICIT --login override. A caller passing a dedicated per-role credential could thus have its write attributed to the shared default identity while being told it succeeded as requested. Thread an "override was explicit" signal into gitea_resolve_api_for_login (second param, "explicit" when LOGIN_OVERRIDE is non-empty). When the override is explicit and that login's token cannot be resolved, FAIL CLOSED (return 1, clear error naming the login, no host-default fallback). The best-effort host-default fallback now applies ONLY on the no-override default path. Applied symmetrically to issue-comment.sh and all three pr-review.sh dispatch sites (approve / request-changes / comment). Tests: both scripts' write flows now assert credential attribution via a token->identity seam in the curl stub -- (a) resolvable --login override drives the entire write/read-back chain under THAT login's token, nothing under the default; (b) unresolvable --login override fails closed (nonzero, no success line, no write, no default-identity request); (c) no-override default path still succeeds under the host-default best-effort credential. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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, approvals, and change requests count as durable provenance only after the wrapper reads the created provider record back and verifies that it was created by this write.
The write is a direct Gitea REST POST that returns the created record's id. Neither wrapper writes through tea — tea 0.11.1 can silently no-op while exiting 0 and cannot emit the id of a record it creates, so its exit code is worthless as proof of a durable write (#865). Instead:
- Comments (
issue-comment.sh, and thecommentaction ofpr-review.sh)POST /api/v1/repos/{owner}/{repo}/issues/{index}/comments, requiring a201and parsing the created comment'sidfrom the response body. - Reviews (
approve/request-changes)POST /api/v1/repos/{owner}/{repo}/pulls/{index}/reviewswith theevent(APPROVED/REQUEST_CHANGES), the reviewbody, andcommit_idpinned to the PR's current head, then parse the created review'sid. The review body travels in the review submit itself — there is no separate detached comment to reconcile (a GiteaREQUEST_CHANGESreview requires a non-empty body, which the submit carries).
Verification keys on that exact provider-returned id. The wrapper then GETs that one record directly — GET /issues/comments/{id} or GET /pulls/{n}/reviews/{id} — and requires that its id equals the created id, its author login equals the acting identity (resolved via GET /api/v1/user for the token in use), and, for comments, its body exactly matches what was submitted, or, for reviews, its state matches the requested action and its reviewed commit_id equals the PR head. The write, the /user identity lookup, and the read-back all use the same credential — the effective login's token, or the host credential when no login is named — so the write is verified against the identity that actually performed it.
This closes the concurrency window rather than documenting it. Because verification keys on the id the create returned, a no-op create yields no id and fails closed with no list-scan fallback, and a concurrent record — even one written by the same identity with an identical body/state — has a different id and cannot be mistaken for this write. There is no residual same-identity window: the earlier boundary-and-author heuristic (accept any id > pre-write-max with a matching author) is replaced entirely by exact-id attribution.
Full pagination. After the exact-id read-back, each wrapper also confirms the created id is enumerable in the record list, walking every page (?limit=&page=1,2,… until a short/empty page) so a record that lands beyond the first page is still found regardless of how many comments or reviews already exist.
tea invocation notes (Gitea)
- tea v0.11.1 has no
commentsubcommand undertea prortea issue— thetea pr comment/tea issue commentforms don't error, they silently fall through to a no-op and still exit 0, producing a false-success write (#865). tea's write subcommands (tea comment,tea pr approve/reject) also cannot report the id of the record they create, so their exit code cannot prove a durable write. These wrappers therefore do not write reviews or comments throughteaat all; they use direct Gitea RESTPOSTs that return the created record's id (see "Durable review provenance" above).teais consulted only to enumerate the login list for host→login resolution. - Because the review body is carried in the
POST …/reviewssubmit itself, there is no separate detached review comment, and the historicaltea pr approve/rejecttrailing-positional-argument vs. nonexistent--comment/-commentflag hazard (#835) no longer applies to these wrappers — no review comment is ever passed totea.
--login override
Both pr-review.sh and issue-comment.sh accept an optional --login <name> flag that overrides the automatically detected Gitea login for that single invocation. The override selects which credential the REST write, the /user identity lookup, and the read-back all use — its token is resolved from the tea config for that login name (get_gitea_token_for_login), falling back to the repo host's credential when no login is named. Resolving the acting identity and the read-back from the same login that performs the write is essential: a write performed under an overridden login must be verified against that login's identity, not the host default's. Callers who need a different login than the host default should pass --login <reviewer-login>.
As a durable successor to this mechanism, consider giving each reviewer/approver slot its own dedicated Gitea login credential, so that author≠reviewer holds at the credential level rather than relying on wrapper-level --login bookkeeping. This is a recommendation for future hardening, not something implemented by this flag.