Skip to content

Unpublish Snapshots workflow broken - npm blocks unpublish from bypass-2FA tokens #4646

Description

@amcclain

The Unpublish Snapshots workflow (.github/workflows/unpublishSnapshots.yml) no longer works. The most recent run failed all 101 unpublish attempts: https://github.com/xh/hoist-react/actions/runs/33552025089

Symptom

Every call returns:

npm error code E403
403 Forbidden - PUT https://registry.npmjs.org/@xh%2fhoist/-rev/8665-...
Granular access tokens that bypass two-factor authentication may not perform this action.

Auth itself is fine - npm whoami succeeds as exhi, the version list is fetched, and all 101 targets are correctly identified. Final tally was Total 101 / Success 0 / Failed 101. Nothing was removed.

Cause

As of 2026-07-31, npm blocks granular access tokens configured to bypass 2FA from sensitive package-management actions, unpublish included. Our NPM_TOKEN is such a token. See https://github.blog/changelog/2026-07-31-restricting-npm-bypass-2fa-granular-access-tokens/

There is no unattended-CI fix

  • npm removed classic/automation tokens in Nov 2025; granular access tokens are the only type left.
  • A GAT either bypasses 2FA (now blocked from unpublish) or is subject to 2FA (needs an interactive OTP per write, which an unattended runner cannot produce).
  • Trusted publishing (OIDC) documents npm publish and npm stage publish only - no unpublish support.

Note the same changelog targets Jan 2027 for removing direct publish from bypass-2FA tokens, so deploySnapshot.yml and deployRelease.yml are on the same clock and will need an OIDC migration regardless of what happens here.

Possible paths, none yet tested

The workflow is always manually dispatched, so an otp input is plausible. Timing from the failed run: setup is only ~6s (job start 19:52:32, first unpublish 19:52:38), but the unpublish loop runs 4m36s for 101 versions - far longer than a 30s TOTP window. That implies chunking to ~10 versions per dispatch, ~11 dispatches to clear the current backlog.

Two unknowns to test locally before rewriting anything:

  1. Does a GAT with bypass-2FA off, plus --otp, actually succeed at unpublish? The error reads as the token being disqualified by type rather than missing an OTP, so a newly minted token is needed either way.
  2. Does npm accept the same OTP across several sequential requests, or is there replay protection? If it is one code per request, chunking is dead.

Both are answerable in a couple of minutes locally: mint the token, unpublish one snapshot with --otp, then immediately try a second with the same code.

Running it locally with generated TOTP codes is the other option. Putting the TOTP seed into GitHub secrets would make CI work unattended, but stores both factors in one place on a shared org account - not recommended.

Also worth deciding

Whether these old public SNAPSHOT builds need removing at all. If the goal is registry tidiness rather than pulling something that should not be public, leaving them costs nothing.

Interim

The workflow is guarded to fail immediately with an explanatory message rather than grinding through ~5 minutes of E403s.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions