Skip to content

Token health

Token health #1

Workflow file for this run

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'),
});