Compare commits

..

2 Commits

Author SHA1 Message Date
Joakim Persson ae13c2264e 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.
2026-08-15 14:27:18 +02:00
Joakim Persson a2f0a4a441 ci: correct the false "Gitea requires auth for public reads" comment
Lint / hadolint (push) Successful in 13s
Lint / actionlint (push) Successful in 15s
resolve-versions claimed "Gitea API requires auth even for public-repo commit
listing" above the pi-toolkit / pi-extensions curls. Measurably false for the
repos it guards. Verified 2026-08-15, unauthenticated vs authenticated GET of
/api/v1/repos/joakimp/<repo>/commits?limit=1&sha=main:

  pi-toolkit         private=false  unauth=200 auth=200  sha 0e1369e6b496 identical
  pi-extensions      private=false  unauth=200 auth=200  sha 98eb07bce60a identical
  mempalace-toolkit  private=false  unauth=200 auth=200  sha f60cf9c73205 identical

Only /api/v1/repos/*/actions/* refuses anonymous reads with 401 — almost
certainly what the claim was over-generalised from. (Same over-generalisation I
nearly committed to opencode-devbox's AGENTS.md today; 69fc80a there narrowed it
to the actions endpoints for the same reason.)

Checked the three repos actually queried rather than reusing the pi-devbox
result — if any had been private the comment would have been TRUE, and the
correction wrong.

Behaviour deliberately unchanged: the header still gets passed. It survives a
repo being flipped private, and an unset secret degrades cleanly because Gitea
ignores an empty `token ` value and serves anonymously:

  no header                 200
  empty token (secret unset) 200
  garbage token             401

That last row is the fragility now documented: a REVOKED or malformed token
returns 401 where anonymous returns 200, so a stale GITEA_BUILD_TOKEN converts a
healthy public read into a require_sha failure that presents as an API or
network fault. Encountered exactly that today with an expired PAT on the actions
endpoints, so the note tells the next reader to suspect the token first.

Comment-only: no non-comment line changed, YAML re-parsed.
2026-08-15 14:21:25 +02:00
+55 -11
View File
@@ -177,6 +177,40 @@ jobs:
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` ───────────
# Until v1.7.0 this followed npm `latest`, which meant every release
# silently adopted whatever pi had shipped that morning — unaudited —
@@ -238,15 +272,27 @@ jobs:
echo "atelier_ref=${ATELIER_REF}" >> "$GITHUB_OUTPUT"
echo "atelier_tag=${ATELIER_TAG}" >> "$GITHUB_OUTPUT"
# pi-toolkit / pi-extensions (Gitea) → commit SHAs. Gitea API
# requires auth even for public-repo commit listing.
TOOLKIT_REF=$(curl -sf -H "$AUTH_HEADER" \
"https://gitea.jordbo.se/api/v1/repos/joakimp/pi-toolkit/commits?limit=1&sha=main" \
| jq -r '.[0].sha // empty' 2>/dev/null || true)
# pi-toolkit / pi-extensions (Gitea) → commit SHAs. All three Gitea
# repos read in this step are PUBLIC: an unauthenticated GET of these
# commit endpoints returns 200 with the IDENTICAL sha (verified
# 2026-08-15 for pi-toolkit, pi-extensions and mempalace-toolkit).
# The comment that used to sit here claimed the Gitea API "requires
# auth even for public-repo commit listing" — it does not. Only
# /api/v1/repos/*/actions/* refuses anonymous reads (401), which is
# what that claim was almost certainly generalised from.
#
# The header is still passed on purpose: it keeps working if a repo is
# ever flipped private, and an ABSENT secret degrades cleanly, because
# Gitea ignores an empty `token ` value and serves the request
# anonymously (200). The real hazard is the opposite one — a REVOKED or
# malformed token returns 401 where anonymous would have returned 200,
# so a stale GITEA_BUILD_TOKEN turns a healthy public read into a
# 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
# token before you suspect Gitea.
TOOLKIT_REF=$(gitea_sha pi-toolkit)
require_sha PI_TOOLKIT_REF "$TOOLKIT_REF"
EXTENSIONS_REF=$(curl -sf -H "$AUTH_HEADER" \
"https://gitea.jordbo.se/api/v1/repos/joakimp/pi-extensions/commits?limit=1&sha=main" \
| jq -r '.[0].sha // empty' 2>/dev/null || true)
EXTENSIONS_REF=$(gitea_sha pi-extensions)
require_sha PI_EXTENSIONS_REF "$EXTENSIONS_REF"
echo "toolkit_ref=${TOOLKIT_REF}" >> "$GITHUB_OUTPUT"
echo "extensions_ref=${EXTENSIONS_REF}" >> "$GITHUB_OUTPUT"
@@ -256,9 +302,7 @@ jobs:
# into the base-decide hash (see that job) to force a base rebuild
# when the toolkit moves — otherwise a toolkit-only fix silently
# fails to land unless Dockerfile.base itself changes.
MEMPALACE_TOOLKIT_REF=$(curl -sf -H "$AUTH_HEADER" \
"https://gitea.jordbo.se/api/v1/repos/joakimp/mempalace-toolkit/commits?limit=1&sha=main" \
| jq -r '.[0].sha // empty' 2>/dev/null || true)
MEMPALACE_TOOLKIT_REF=$(gitea_sha mempalace-toolkit)
require_sha MEMPALACE_TOOLKIT_REF "$MEMPALACE_TOOLKIT_REF"
echo "mempalace_toolkit_ref=${MEMPALACE_TOOLKIT_REF}" >> "$GITHUB_OUTPUT"