fix(tools): write Gitea reviews/comments via REST POST and verify by exact created id (#865)
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful

Replace the tea-based write + boundary/author read-back with a direct Gitea
REST POST that returns the created record's id, and verify that exact record.

BLOCKER 2 (credential ordering): resolve the acting identity, the write token,
and the read-back token from the SAME effective login. A --login override now
selects the credential used for the POST, GET /user, and the GET-by-id
read-back, so an overridden write is verified against the identity that
performed it -- not the host default. Login-name resolution is best-effort and
non-fatal (the override always wins; otherwise fall back to the host
credential), so exotic/ported hosts still resolve a token.

BLOCKER 1+3 (attribution + tautological tests): the write is now
POST /issues/{n}/comments or POST /pulls/{n}/reviews (event + body + commit_id
== PR head), parsing the provider-returned created id. Verification GETs that
exact id and checks author == acting identity and body (comments) or state +
commit_id (reviews). Keying on the created id closes the concurrency window:
a no-op create yields no id and fails closed with no list-scan fallback, and a
concurrent same-identity record has a different id. The review body travels in
the review submit, removing the separate detached comment.

Tests: the curl stub now models a real server with persistent on-disk
review/comment state -- a POST actually creates+persists a record and returns
its id, and the read-back reads that same state (no fabricated record for the
wrapper to find). Adds same-identity no-op-concurrent and author-mismatch
fail-closed cases for both comments and reviews, and >page-1 pagination
coverage for both. README "Durable review provenance" refreshed for the REST
mechanism.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hermes Agent
2026-07-21 19:46:32 -05:00
parent 16481ece3d
commit 9384f0bc0a
6 changed files with 1024 additions and 687 deletions

View File

@@ -563,6 +563,54 @@ get_gitea_token() {
return 1
}
# Resolve the API token for a SPECIFIC tea login name from tea's own config
# (the same store tea itself writes/reads for `--login <name>`). This is what
# lets a REST write be performed AS the selected --login identity: tea keys its
# per-login tokens by `name` in $XDG_CONFIG_HOME/tea/config.yml (default
# ~/.config/tea/config.yml), exactly as the `tea` CLI resolves them, so a
# --login override and its REST read-back bind to the SAME credential/identity.
# Prints the token on success; returns non-zero (no output) if the config or a
# matching login token cannot be found. Callers must not log the result.
get_gitea_token_for_login() {
local login_name="$1" config_file
[[ -n "$login_name" ]] || return 1
config_file="${XDG_CONFIG_HOME:-$HOME/.config}/tea/config.yml"
[[ -f "$config_file" ]] || return 1
LOGIN_NAME="$login_name" python3 - "$config_file" <<'PY'
import os
import sys
try:
import yaml
except ImportError:
raise SystemExit(1)
try:
with open(sys.argv[1], encoding="utf-8") as handle:
config = yaml.safe_load(handle)
except (OSError, yaml.YAMLError):
raise SystemExit(1)
wanted = os.environ["LOGIN_NAME"]
logins = config.get("logins") if isinstance(config, dict) else None
if not isinstance(logins, list):
raise SystemExit(1)
for login in logins:
if not isinstance(login, dict):
continue
if str(login.get("name") or "") == wanted:
token = login.get("token")
if isinstance(token, str) and token:
print(token)
raise SystemExit(0)
break
raise SystemExit(1)
PY
}
# Resolve HTTPS basic auth credentials for a Gitea host from ~/.git-credentials.
# Prints "username:password" for direct curl -u consumption. Callers must not log it.
get_gitea_basic_auth() {