Skip to content

v0.50.2 — CI reads GitHub Packages with GITHUB_TOKEN, not a PAT

The frontend-module install step 403'd against npm.pkg.github.com:

403 Permission permission_denied: The token provided does not match expected scopes

The PKG_TOKEN PAT had been replaced with a fine-grained token, and GitHub Packages does not accept fine-grained tokens at all — the npm registry documents classic PATs with read:packages as the only supported personal-token type, so no permission selection on a fine-grained token could have worked. Contents: read was the wrong axis regardless: package reads are not repository-contents reads.

That is the immediate cause, but the PAT was the recurring one. The same secret took CI down in v0.40.2 ("the PKG_TOKEN secret losing registry access"), and each failure needed a manual rotation to clear.

GITHUB_TOKEN replaces it. It is minted per job and expires when the job ends, so it cannot be left un-rotated, cannot be SSO-deauthorized, and — at the Docker build arg, where a token can persist in image history — is worthless by the time anyone could read it out of a layer.

  • permissions: { contents: read, packages: read } at the workflow level, covering all four jobs. Declaring permissions replaces the defaults wholesale, which is why contents: read is listed; nothing else is, since AWS auth uses static access keys rather than OIDC.
  • All five registry reads now use secrets.GITHUB_TOKEN: pnpm install in lint-and-test and migration-guard, both install-hosted-modules-frontend.sh steps, and docker build --build-arg NPM_TOKEN.
  • scripts/install-hosted-modules-frontend.sh's failure hint was rewritten. It named PKG_TOKEN and read:packages, which is now the wrong advice for CI and would send the next person to rotate a secret that no longer exists. It now separates the two cases: in CI, a 403 means a package has not granted this repo Actions access, or the permission block is missing; locally, it means a fine-grained token that the registry will never accept.
  • ciPackagesAuth.test.ts guards all of it — packages: read present, contents: read present, no write in the block, every auth line on GITHUB_TOKEN, no secrets.PKG_TOKEN left. It lives in server-core rather than beside apps/web's ciProdBackup.test.ts because apps/web's test script is a stub, so CI never runs that suite; a guard against a CI regression has to sit where CI executes. Each assertion was checked against a deliberately broken copy of the workflow.

The manual half, already done: each of the five packages in modules.hosted.json (vtt-assets, system8, dnd5e, genai, scenes) must grant purple-cat-games/platform read access under its Manage Actions access settings. GITHUB_TOKEN is scoped to this repository, so a package published from another repo is invisible to it until listed there. A 403 after this release means a package was missed, not that the token is wrong.

The PKG_TOKEN secret can now be deleted. Note that infra/gcp/cloudbuild.yaml still sources an NPM_TOKEN from Secret Manager; that path is outside GitHub Actions and still needs a classic PAT if it is ever used.