ci: survive a revoked GITEA_BUILD_TOKEN on public commit reads
Lint / actionlint (push) Successful in 14s
Lint / hadolint (push) Successful in 13s

Follow-up to a2f0a4a, which documented the hazard; this removes it.

resolve-versions read three PUBLIC Gitea repos with `curl -sf -H "$AUTH_HEADER"`.
Gitea rejects an invalid token rather than ignoring it, so the token turned a
read that works anonymously into a hard failure:

  no Authorization header    200
  empty token (secret unset) 200   <- absent secret was always safe
  garbage/revoked token      401   <- stale secret broke the release

A revoked GITEA_BUILD_TOKEN therefore failed resolve-versions via require_sha,
presenting as connectivity or an API fault, on data any anonymous client could
fetch. Hit exactly that failure mode today with an expired PAT.

New gitea_sha() helper tries authed, and on 401/403 retries anonymously with a
loud stderr warning naming the token as the cause. Deliberate choices:

- non-200 after the retry emits nothing and returns 0, so require_sha still
  raises the explicit abort — the helper never invents a fallback ref, which is
  the property the surrounding code exists to guarantee
- warnings go to stderr, NOT as ::warning:: annotations: the function's stdout
  IS the SHA, so an annotation there would be captured into the ref
- the header is still sent first, so a private repo keeps working

Verified by extracting the function from the YAML step body (so the test ran the
committed text, not a copy) and calling it against live Gitea:

  valid token        -> 0e1369e6b496 (pi-toolkit)
  REVOKED token      -> warns, retries anon, f60cf9c73205 (mempalace-toolkit)
  unset secret       -> 98eb07bce60a (pi-extensions)
  nonexistent repo   -> empty + HTTP 404 warning, so require_sha aborts

Behaviour unchanged on the happy path: all three SHAs are byte-identical to the
ones the v1.8.1 release run resolved with the old curl code. `bash -n` clean on
the extracted step body; YAML re-parsed.
This commit is contained in:
Joakim Persson
2026-08-15 14:27:18 +02:00
parent a2f0a4a441
commit ae13c2264e
+37 -9
View File
@@ -177,6 +177,40 @@ jobs:
fi fi
} }
# Read a commit SHA from Gitea, surviving a bad build token.
#
# These repos are public (see the note at the call sites), so auth is
# a convenience, not a requirement — but Gitea REJECTS an invalid
# token (401) rather than ignoring it, so a revoked or malformed
# GITEA_BUILD_TOKEN could fail an entire release on reads that work
# fine anonymously. An ABSENT secret was always safe (Gitea ignores an
# empty `token ` value and serves the request, 200); a STALE one was
# not. So: try authed, and on 401/403 retry anonymously.
#
# A non-200 after that emits nothing and returns 0 deliberately, so
# require_sha raises the loud explicit abort rather than this helper
# inventing a fallback ref.
#
# Messages go to STDERR, not as ::warning:: annotations: this
# function's stdout IS the SHA, so anything written there would be
# captured into the ref by the command substitution.
gitea_sha() { # $1=repo
local repo="$1" url resp code
url="https://gitea.jordbo.se/api/v1/repos/joakimp/${repo}/commits?limit=1&sha=main"
resp=$(curl -s -w '\n%{http_code}' -H "$AUTH_HEADER" "$url" || printf '\n000')
code=${resp##*$'\n'}
if [ "$code" = "401" ] || [ "$code" = "403" ]; then
printf 'WARNING: Gitea rejected the build token for %s (HTTP %s); retrying anonymously. The read should succeed (public repo), but GITEA_BUILD_TOKEN is stale or malformed and should be rotated.\n' "$repo" "$code" >&2
resp=$(curl -s -w '\n%{http_code}' "$url" || printf '\n000')
code=${resp##*$'\n'}
fi
if [ "$code" != "200" ]; then
printf 'WARNING: Gitea commit lookup for %s returned HTTP %s\n' "$repo" "$code" >&2
return 0
fi
printf '%s' "${resp%$'\n'*}" | jq -r '.[0].sha // empty' 2>/dev/null || true
}
# ── pi version: from the PIN, not from npm `latest` ─────────── # ── pi version: from the PIN, not from npm `latest` ───────────
# Until v1.7.0 this followed npm `latest`, which meant every release # Until v1.7.0 this followed npm `latest`, which meant every release
# silently adopted whatever pi had shipped that morning — unaudited — # silently adopted whatever pi had shipped that morning — unaudited —
@@ -256,13 +290,9 @@ jobs:
# require_sha failure that reads like an API or network fault. If this # require_sha failure that reads like an API or network fault. If this
# step ever fails on a repo you can browse anonymously, suspect the # step ever fails on a repo you can browse anonymously, suspect the
# token before you suspect Gitea. # token before you suspect Gitea.
TOOLKIT_REF=$(curl -sf -H "$AUTH_HEADER" \ TOOLKIT_REF=$(gitea_sha pi-toolkit)
"https://gitea.jordbo.se/api/v1/repos/joakimp/pi-toolkit/commits?limit=1&sha=main" \
| jq -r '.[0].sha // empty' 2>/dev/null || true)
require_sha PI_TOOLKIT_REF "$TOOLKIT_REF" require_sha PI_TOOLKIT_REF "$TOOLKIT_REF"
EXTENSIONS_REF=$(curl -sf -H "$AUTH_HEADER" \ EXTENSIONS_REF=$(gitea_sha pi-extensions)
"https://gitea.jordbo.se/api/v1/repos/joakimp/pi-extensions/commits?limit=1&sha=main" \
| jq -r '.[0].sha // empty' 2>/dev/null || true)
require_sha PI_EXTENSIONS_REF "$EXTENSIONS_REF" require_sha PI_EXTENSIONS_REF "$EXTENSIONS_REF"
echo "toolkit_ref=${TOOLKIT_REF}" >> "$GITHUB_OUTPUT" echo "toolkit_ref=${TOOLKIT_REF}" >> "$GITHUB_OUTPUT"
echo "extensions_ref=${EXTENSIONS_REF}" >> "$GITHUB_OUTPUT" echo "extensions_ref=${EXTENSIONS_REF}" >> "$GITHUB_OUTPUT"
@@ -272,9 +302,7 @@ jobs:
# into the base-decide hash (see that job) to force a base rebuild # into the base-decide hash (see that job) to force a base rebuild
# when the toolkit moves — otherwise a toolkit-only fix silently # when the toolkit moves — otherwise a toolkit-only fix silently
# fails to land unless Dockerfile.base itself changes. # fails to land unless Dockerfile.base itself changes.
MEMPALACE_TOOLKIT_REF=$(curl -sf -H "$AUTH_HEADER" \ MEMPALACE_TOOLKIT_REF=$(gitea_sha mempalace-toolkit)
"https://gitea.jordbo.se/api/v1/repos/joakimp/mempalace-toolkit/commits?limit=1&sha=main" \
| jq -r '.[0].sha // empty' 2>/dev/null || true)
require_sha MEMPALACE_TOOLKIT_REF "$MEMPALACE_TOOLKIT_REF" require_sha MEMPALACE_TOOLKIT_REF "$MEMPALACE_TOOLKIT_REF"
echo "mempalace_toolkit_ref=${MEMPALACE_TOOLKIT_REF}" >> "$GITHUB_OUTPUT" echo "mempalace_toolkit_ref=${MEMPALACE_TOOLKIT_REF}" >> "$GITHUB_OUTPUT"