fix(ci): accept the override from a PR review, and stop misdiagnosing an unreadable comments API
gate / gate (pull_request) Failing after 1m47s
gate / conformance (pull_request) Has been skipped

The override did not work on its first real test, and the reason was invisible
from the outside. Run 203 on PR #38: a well-formed `/approve-override` posted by
jakman85 in the PR comment box, 29 minutes newer than the head commit, and the
stage still blocked with:

    approval-override: COMMENTS_JSON is not a list

SETTINGS_API_TOKEN is minted [read:repository, read:admin], and Gitea puts
/issues/{n}/comments behind the *issue* scope — so Gitea answered with an error
OBJECT, `jq -e .` accepted it as valid JSON, and the stage fell through to 'no
approving review'. A permission failure wearing the costume of a missing
override. /pulls/{n}/files and /pulls/{n}/reviews are under read:repository,
which is why every other part of the same stage worked.

1. The diagnosis no longer lies. COMMENTS_JSON leaves the must-parse-or-block
   loop and is checked for being an ARRAY; when it is not, the stage prints
   Gitea's own .message, names the missing scope and says where to post instead.
   An unreadable source can only cause a false BLOCK, never a false pass, so a
   loud warning is the honest behaviour and blocking was not.
2. An override is accepted from a PR REVIEW body as well as a comment, so the
   hatch is not hostage to one API scope — a review of type 'Comment' works on
   the token CI has today. The gate names which source it used; PENDING
   (unsubmitted, author-only) reviews are dropped.
3. set-ci-token.sh requests read:issue, making the ordinary comment box work —
   on a RE-RUN only, since an existing token's scopes cannot be widened.

ci/tests/approval-override.test.sh: 23 -> 38 assertions, covering the reviews
source, PENDING reviews, an error object from the comments API, 'an unreadable
comments source must not suppress a valid review override', latest-wins across
both sources, and that the diagnostic quotes Gitea's message.

Also measured and recorded: run 203 shows gitea-settings PASS after the operator
ran the live PATCH, with block_admin_merge_override still true — the narrow
change held, the approval count moved and the gate's teeth did not.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
jakman85andClaude Opus 5 committed 2026-10-02 00:44:31 -05:00
1 parent 8ccfdab180
commit b946c475c6
8 files changed
+350 -84

No files matched your search

+12 -1
View File
@@ -161,7 +161,18 @@ a *green* one no longer needs a signature that cannot exist.
**Security-relevant changes are still reviewed** — by `ci/approval-policy.sh` against
`approval-policy.json` `sensitive_paths`, not by this field. An administrator waives that review
only by posting `/approve-override <reason>` on the PR: reason mandatory, permission re-checked
live, invalidated by a new commit, and recorded by account in the gate summary. Note that the
live, invalidated by a new commit, and recorded by account in the gate summary.
> **Post the override as a PR *review* (type "Comment"), not in the plain comment box** — until
> the CI token is re-minted. `SETTINGS_API_TOKEN` is `[read:repository, read:admin]`, and
> `/issues/{n}/comments` needs Gitea's *issue* scope, so Gitea answers with an error **object**
> instead of an array and the override is invisible. Measured on PR #38 (2026-10-02): a correct,
> fresh, well-formed override in the comment box produced `approval-policy: FAIL` reading as "no
> approving review" — a permission failure disguised as a missing one. `/pulls/{n}/reviews` is
> under `read:repository` and worked in the same run. The stage now reads **both** sources, names
> which one it used, and says plainly when the comments API is unreadable. `set-ci-token.sh`
> requests `read:issue` as of 2026-10-02, so **re-run it** and the comment box works too — token
> scopes cannot be widened in place. Note that the
**agent account `gitea-admin` also reports permission `owner`**, so it can clear its own PRs that
way; that is an accepted consequence of authorising by permission alone, and the route to
narrowing it (an `override_admins` allowlist, already honoured by the code) is written down in
+2 -1
View File
@@ -32,7 +32,8 @@
"requires_reason": true,
"authorized_permissions": ["admin", "owner"],
"invalidated_by_new_commits": true,
"_how": "Comment on the PR with the directive at the START of a line, followed by a reason: '/approve-override <why>'. ci/approval-policy.sh resolves the commenter's LIVE repo permission via /collaborators/{login}/permission and accepts the override only if it is in authorized_permissions. The overrider, the timestamp and the reason are printed in the gate log and the job summary.",
"_how": "Post the directive at the START of a line, followed by a reason: '/approve-override <why>'. ci/approval-policy.sh resolves the poster's LIVE repo permission via /collaborators/{login}/permission and accepts the override only if it is in authorized_permissions. The overrider, the source, the timestamp and the reason are printed in the gate log and the job summary.",
"_where_to_post_it": "EITHER a PR REVIEW body (type 'Comment') OR the ordinary PR comment box. Prefer the REVIEW: measured 2026-10-02 on PR #38, a well-formed override in the comment box was INVISIBLE to CI, because /issues/{n}/comments sits behind Gitea's *issue* token scope and SETTINGS_API_TOKEN is minted [read:repository, read:admin]. Gitea returned an error object rather than an array, and the stage reported 'no approving review' -- a permission problem wearing the costume of a missing override. /pulls/{n}/reviews is under read:repository and was readable in the same run, so a review body is the route that works on today's token. infra/gitea/set-ci-token.sh now also requests read:issue, which makes the comment box work -- but only after that script is RE-RUN, since an existing token's scopes cannot be widened in place. The stage accepts both sources and names which one it used, so neither route is load-bearing alone.",
"_self_override_is_allowed": "The author may override their own PR if they hold an authorized permission. That is the entire point -- with one human there is no one else -- but it means the 'a change to the gate cannot be gated by the gate' protection is, for an administrator, advisory rather than structural. It is traded for an auditable record: the waiver is an explicit act, attributed, with a stated reason, in the PR's comment history and the gate output.",
"_known_consequence_agent_accounts": "ACCEPTED, EYES OPEN (operator decision, 2026-10-01). Authorisation is by live permission ALONE, with no allowlist of human accounts. The agent account `gitea-admin` reports permission `owner` on this repo (measured 2026-10-01), so an AGENT can clear the sensitive-path review on its own PR by posting the directive. Agents author most PRs here, so this is the broad case, not the edge case. To narrow it later without changing any logic, add an `override_admins` allowlist here and require membership in addition to the permission check -- ci/approval-policy.sh reads both from this file.",
"_staleness": "An override is dropped unless its comment is NEWER than the PR head commit, mirroring Gitea's own dismiss_stale_approvals. An override earned at commit A therefore does not silently cover commit B; push again and you must override again."
+18 -4
View File
@@ -5,9 +5,23 @@
# The token value is NEVER printed. It goes from the token API into the secrets API and
# nowhere else -- not to stdout, not to a file, not into an agent transcript.
#
# Scope: `read:repository` + `read:admin` are what the check needs (the rule object, and the
# runner list). Gitea token scopes are coarse; grant the least that works on your version and
# confirm the check goes green rather than assuming the scope name.
# Scope: `read:repository` + `read:admin` are what ci/gitea-settings.sh needs (the rule object,
# and the runner list). `read:issue` is for ci/approval-policy.sh, and it was MISSING — see below.
# Gitea token scopes are coarse; grant the least that works on your version and confirm the check
# goes green rather than assuming the scope name.
#
# WHY read:issue IS HERE (added 2026-10-02, measured on PR #38). ci/approval-policy.sh reads the
# PR's comments to find an admin `/approve-override`. /issues/{n}/comments sits behind the *issue*
# scope, which this script did not request — so Gitea answered with an error OBJECT instead of an
# array, and a perfectly well-formed override posted in the PR comment box was invisible to CI.
# The failure did not look like a permission problem from the outside; it looked like the override
# had not been posted. `/pulls/{n}/files` and `/pulls/{n}/reviews` are under read:repository, which
# is why every other part of the same stage worked and this one did not.
#
# The stage now accepts an override from a REVIEW body as well, so it still functions on a token
# WITHOUT read:issue. This scope is what makes the ordinary comment box work too — the thing
# anybody would reach for first. **Re-run this script to pick it up; an existing token's scopes
# cannot be widened in place.**
set -euo pipefail
HOST=gitea.home.keithjackson.dev; OWNER=Home-Network; REPO=home-network-k3s-impl
# NOT GITEA_* — Gitea reserves that prefix for secret names and returns HTTP 400
@@ -21,7 +35,7 @@ basic() { curl -sS --config <(printf 'user = "%s:%s"\n' "$U" "$P") "$@"; }
echo "==> minting token '${NAME}' as ${U}"
TOKEN="$(basic -X POST -H 'Content-Type: application/json' \
-d "{\"name\":\"${NAME}\",\"scopes\":[\"read:repository\",\"read:admin\"]}" \
-d "{\"name\":\"${NAME}\",\"scopes\":[\"read:repository\",\"read:admin\",\"read:issue\"]}" \
"https://${HOST}/api/v1/users/${U}/tokens" | jq -r '.sha1 // empty')"
[ -n "$TOKEN" ] || { echo "token creation failed (check scopes for your Gitea version)" >&2; exit 1; }
echo "==> got a ${#TOKEN}-character token (value not shown)"