#!/usr/bin/env bash # Mint a Gitea API token and store it as the repo Actions secret GITEA_SETTINGS_TOKEN, # so ci/gitea-settings.sh can read the live branch-protection rule during the gate. # # 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 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 # "invalid secret name". The first version of this script used GITEA_SETTINGS_TOKEN, sent the # PUT to /dev/null, and printed "stored" regardless; the secret never existed and nobody knew. SECRET=SETTINGS_API_TOKEN NAME="ci-gitea-settings-$(date +%Y%m%d%H%M%S)" creds="$(printf 'protocol=https\nhost=%s\n\n' "$HOST" | git credential fill)" U="$(sed -n 's/^username=//p' <<<"$creds")"; P="$(sed -n 's/^password=//p' <<<"$creds")" 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\",\"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)" echo "==> storing it as repo Actions secret ${SECRET}" code="$(basic -o /tmp/set-ci-token.out -w '%{http_code}' -X PUT -H 'Content-Type: application/json' \ -d "$(jq -n --arg d "$TOKEN" '{data:$d}')" \ "https://${HOST}/api/v1/repos/${OWNER}/${REPO}/actions/secrets/${SECRET}")" unset TOKEN if [ "$code" != 201 ] && [ "$code" != 204 ]; then echo " PUT failed: HTTP ${code} -- $(head -c 200 /tmp/set-ci-token.out)" >&2 rm -f /tmp/set-ci-token.out; exit 1 fi rm -f /tmp/set-ci-token.out # VERIFY BY READING IT BACK. curl -sS exits 0 on an HTTP error, so without this the script # reports success for a store that never happened -- which is exactly what it did. names="$(basic "https://${HOST}/api/v1/repos/${OWNER}/${REPO}/actions/secrets" \ | jq -rc '[.. | objects | select(has("name")) | .name] | unique')" grep -q "$SECRET" <<<"$names" \ || { echo " stored HTTP ${code} but ${SECRET} is NOT in ${names} -- refusing to claim success" >&2; exit 1; } echo "==> stored and VERIFIED present: ${names}" cat <<'NOTE' Next: 1. .gitea/workflows/gate.yml already passes this secret to the gate step. 2. Open any PR and confirm the gate prints "gitea-settings: green" -- it SKIPS silently without the token, so a green run is the only proof the wiring works. 3. THEN set REQUIRE_GITEA_SETTINGS=true in gate.yml, so a future missing token BLOCKS instead of skipping. Do that only after you have seen it green, or you lock the repo. NOTE