Token health #1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Token health | |
| # NPM_TOKEN expires. Without this, the thing that discovers that fact is a | |
| # release failing months from now, at which point someone has to work out why — | |
| # an npm auth error does not say "your token expired", it says 403 or ENEEDAUTH, | |
| # which reads like a permissions problem. | |
| # | |
| # So the token is checked on a schedule and the failure is turned into an issue | |
| # with the fix written in it, instead of a surprise during a release. | |
| on: | |
| schedule: | |
| # Weekly, Monday 06:00 UTC. | |
| - cron: '0 6 * * 1' | |
| workflow_dispatch: | |
| jobs: | |
| check: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| issues: write | |
| steps: | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: '24' | |
| registry-url: 'https://registry.npmjs.org' | |
| - name: Can the token still authenticate? | |
| id: probe | |
| continue-on-error: true | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| run: | | |
| # `npm whoami` is the cheapest call that proves the credential is live. | |
| # It does not publish, and it does not need a package to exist. | |
| if who=$(npm whoami 2>&1); then | |
| echo "→ token is valid (authenticated as $who)" | |
| echo "ok=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "→ token did NOT authenticate: $who" | |
| echo "ok=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Open an issue if the token is dead | |
| if: steps.probe.outputs.ok != 'true' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const title = 'NPM_TOKEN cannot authenticate — releases are blocked'; | |
| // One open issue, not one per week. | |
| const existing = await github.rest.issues.listForRepo({ | |
| owner: context.repo.owner, repo: context.repo.repo, | |
| state: 'open', labels: 'release-blocked', | |
| }); | |
| if (existing.data.some(i => i.title === title)) { | |
| core.info('issue already open'); | |
| return; | |
| } | |
| await github.rest.issues.create({ | |
| owner: context.repo.owner, repo: context.repo.repo, | |
| title, | |
| labels: ['release-blocked'], | |
| body: [ | |
| '`npm whoami` failed with the `NPM_TOKEN` secret, so **publishing is broken**.', | |
| 'Nothing is wrong with the package — the credential is.', | |
| '', | |
| 'Most likely the token expired. npm tokens are created with an expiry,', | |
| 'and an expired one fails with `403`/`ENEEDAUTH`, which reads like a', | |
| 'permissions problem rather than an expiry.', | |
| '', | |
| '**Two ways to fix it, cheapest first:**', | |
| '', | |
| '1. **Configure trusted publishing and delete the token entirely.**', | |
| ' npmjs.com → this package → Settings → Trusted Publisher →', | |
| ' GitHub Actions → this org/repo → `publish.yml` → allow `npm publish`.', | |
| ' Then remove the `NODE_AUTH_TOKEN` line from `publish.yml`.', | |
| ' Tokens stop existing, so they stop expiring.', | |
| ' https://docs.npmjs.com/trusted-publishers', | |
| '', | |
| '2. Create a new granular token and re-set the secret:', | |
| ' `gh secret set NPM_TOKEN --repo ' + context.repo.owner + '/' + context.repo.repo + '`', | |
| ' (the secret NAME is `NPM_TOKEN`; the token itself is pasted at the prompt)', | |
| ].join('\n'), | |
| }); |