fix(ci): authenticate to Gitea's registry with a package-scoped token
`docker login` was refused with a bare `unauthorized`. The root cause is not a typo in the credentials but a difference in what a Gitea package is: it belongs to a user or org **owner**, not to a repository, so the ephemeral repo-scoped Actions token carries no authority over it and the registry rejects it outright. GHCR accepted GITHUB_TOKEN and GitLab issues working per-job credentials in $CI_REGISTRY_PASSWORD; Gitea deliberately does neither, so this needs a real access token with the package scope, supplied as REGISTRY_TOKEN. The username moves from github.actor to github.repository_owner for the same reason. The image is pushed into the owner's namespace and the token belongs to that owner, while actor is merely whoever triggered the run — a different person on a dispatch, the wrong account on a fork. Added a preflight check on the secret, because the failure it replaces said nothing about what to do. It also settles a question the evidence left open: the reports of this problem describe login succeeding and the *push* failing, while ours failed at login, which is consistent with the token being rejected but equally with its never having been set. The check distinguishes the two the next time it happens. No test pins this. The three workflow assertions in test/refresh.test.ts exist because those defects are silent; this one turns CI red on the spot, which is the condition that made them worth writing. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f32eb93289
commit
76bd7d0b70
@@ -141,6 +141,9 @@ jobs:
|
||||
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
permissions:
|
||||
contents: read
|
||||
# Kept as a statement of intent, but it does not grant what this job
|
||||
# needs: on Gitea the registry does not accept the Actions token at all,
|
||||
# so `packages: write` on it authorises nothing. See the login step.
|
||||
packages: write
|
||||
steps:
|
||||
- uses: https://github.com/actions/checkout@v4
|
||||
@@ -166,11 +169,40 @@ jobs:
|
||||
echo "host=${host}" >> "$GITHUB_OUTPUT"
|
||||
echo "name=${host}/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Why not `secrets.GITHUB_TOKEN`, which is what this was and what the
|
||||
# GHCR version used: a Gitea package belongs to a **user or org owner,
|
||||
# not to a repository**, so the ephemeral repo-scoped Actions token
|
||||
# carries no authority over it and the registry refuses it. GitLab hands
|
||||
# out working per-job registry credentials ($CI_REGISTRY_PASSWORD, which
|
||||
# .gitlab-ci.yml uses) and Gitea deliberately does not, so this needs a
|
||||
# real access token with package write scope, stored as REGISTRY_TOKEN.
|
||||
#
|
||||
# The username must be the token's owner, hence repository_owner rather
|
||||
# than github.actor: the image is pushed into the owner's namespace, and
|
||||
# actor is whoever happened to trigger the run — a different person on a
|
||||
# dispatch, and the wrong account entirely on a fork.
|
||||
- name: Check the registry credential exists
|
||||
# Without this the failure is a bare `unauthorized` from the daemon,
|
||||
# which says nothing about what to do. It also tells us something we do
|
||||
# not currently know: whether the old token was rejected or was simply
|
||||
# never set.
|
||||
env:
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: |
|
||||
if [ -z "${REGISTRY_TOKEN:-}" ]; then
|
||||
echo "::error::REGISTRY_TOKEN is not set. Create an access token at" \
|
||||
"${{ github.server_url }}/user/settings/applications with the" \
|
||||
"package read+write scope, then add it to this repository under" \
|
||||
"Settings → Actions → Secrets as REGISTRY_TOKEN."
|
||||
exit 1
|
||||
fi
|
||||
echo "REGISTRY_TOKEN is set (${#REGISTRY_TOKEN} chars)"
|
||||
|
||||
- uses: https://github.com/docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ steps.img.outputs.host }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
- uses: https://github.com/docker/build-push-action@v6
|
||||
with:
|
||||
|
||||
@@ -358,6 +358,22 @@ The image lands at `gitea.lucaswinther.info/lucasw89/gacha-event-tracker`, tagge
|
||||
the commit SHA. The registry host is derived from `github.server_url` rather than hardcoded, so a
|
||||
fork on another instance pushes to its own registry.
|
||||
|
||||
**Pushing the image needs one one-time step CI cannot do for itself.** A Gitea package belongs to a
|
||||
*user or org owner*, not to a repository, so the ephemeral Actions token has no authority over it and
|
||||
the registry refuses it — `secrets.GITHUB_TOKEN` does not work here, unlike on GHCR and unlike
|
||||
GitLab's `$CI_REGISTRY_PASSWORD`. So:
|
||||
|
||||
1. Create an access token at **Settings → Applications** with the **package** read + write scope.
|
||||
2. Add it to the repository under **Settings → Actions → Secrets** as **`REGISTRY_TOKEN`**.
|
||||
|
||||
The `image` job checks for that secret before it tries to log in, and fails with those instructions
|
||||
rather than a bare `unauthorized` from the Docker daemon. The username it logs in with is
|
||||
`github.repository_owner`, not `github.actor` — the image goes to the owner's namespace, and the
|
||||
actor is merely whoever triggered the run.
|
||||
|
||||
One consequence of packages being owner-scoped: the pushed image is **not** automatically associated
|
||||
with this repository. Linking it is a manual step on the package's own settings page.
|
||||
|
||||
The feed job fails if the event count collapses, or if any single source parses to nothing — nine
|
||||
healthy sources hide a tenth that has gone quiet, and the total stays comfortably over the floor
|
||||
while one game shows an empty calendar. That is the failure mode a parser-only pipeline is most
|
||||
|
||||
Reference in New Issue
Block a user