diff --git a/.gitea/workflows/gate.yml b/.gitea/workflows/gate.yml index 2265f51..955ff90 100644 --- a/.gitea/workflows/gate.yml +++ b/.gitea/workflows/gate.yml @@ -124,8 +124,13 @@ jobs: - name: Merge gate — ci/gate.sh (what `make -C ci gate` runs) env: GATE_STRICT_SKIPS: "true" - GITEA_SETTINGS_TOKEN: ${{ secrets.GITEA_SETTINGS_TOKEN }} - # REQUIRE_GITEA_SETTINGS: "true" # <- uncomment AFTER seeing gitea-settings green + SETTINGS_API_TOKEN: ${{ secrets.SETTINGS_API_TOKEN }} + # ENABLED 2026-09-29. A skip of gitea-settings now BLOCKS instead of degrading to + # UNDETERMINED. Safe because THIS PR's own CI run is the proof: if the stage were + # skipping, this line turns that into a failure and the PR goes red instead of + # merging. Green here means the token resolved and the live Gitea matched the + # declaration in infra/gitea/expected-branch-protection.json. + REQUIRE_GITEA_SETTINGS: "true" run: ./ci/gate.sh || [ "$?" -eq 3 ] # NOTE: no actions/upload-artifact step. Gitea Actions rejects upload-artifact@v4 diff --git a/ci/gitea-settings.sh b/ci/gitea-settings.sh index 5293329..483e13c 100755 --- a/ci/gitea-settings.sh +++ b/ci/gitea-settings.sh @@ -56,14 +56,20 @@ log "Stage: Gitea platform settings match the declaration" DECL="${REPO_ROOT}/infra/gitea/expected-branch-protection.json" GITEA_URL="${GITEA_URL:-https://gitea.home.keithjackson.dev}" GITEA_REPO="${GITEA_REPO:-Home-Network/home-network-k3s-impl}" -TOKEN="${GITEA_SETTINGS_TOKEN:-}" +# SETTINGS_API_TOKEN, not GITEA_SETTINGS_TOKEN. Gitea RESERVES the `GITEA_` prefix for +# secret names and rejects a PUT of one with HTTP 400 "invalid secret name" — the same way +# GitHub reserves `GITHUB_`. The original name could therefore never be stored, and because +# infra/gitea/set-ci-token.sh sent that PUT to /dev/null and printed "stored" unconditionally, +# it looked like it had worked. A token was minted, no secret existed, and this stage would +# have skipped forever. GITEA_SETTINGS_TOKEN is still honoured for a local run. +TOKEN="${SETTINGS_API_TOKEN:-${GITEA_SETTINGS_TOKEN:-}}" [ -f "$DECL" ] || die "declaration missing: ${DECL}" require_tool jq require_tool curl if [ -z "$TOKEN" ]; then - skip "GITEA_SETTINGS_TOKEN is not set — cannot read the live Gitea rule." + skip "SETTINGS_API_TOKEN is not set — cannot read the live Gitea rule." skip " Branch protection and runner registration are therefore UNVERIFIED. They were" skip " wrong, unnoticed, for a month (2026-09-29). Wire it: infra/gitea/set-ci-token.sh" summary "- gitea-settings: **SKIPPED — no token** (protection + runners UNVERIFIED; not a pass)" diff --git a/infra/gitea/set-ci-token.sh b/infra/gitea/set-ci-token.sh index ca00b30..047a833 100755 --- a/infra/gitea/set-ci-token.sh +++ b/infra/gitea/set-ci-token.sh @@ -10,6 +10,10 @@ # confirm the check goes green rather than assuming the scope name. 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")" @@ -22,12 +26,24 @@ TOKEN="$(basic -X POST -H 'Content-Type: application/json' \ [ -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 GITEA_SETTINGS_TOKEN" -basic -X PUT -H 'Content-Type: application/json' \ +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/GITEA_SETTINGS_TOKEN" >/dev/null + "https://${HOST}/api/v1/repos/${OWNER}/${REPO}/actions/secrets/${SECRET}")" unset TOKEN -echo "==> stored." +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'