Revert the unpublish workflow — npm forbids it for CI tokens - #46
Merged
Conversation
…try says no The workflow was correct and cannot work. Every one of the nine versions came back the same way: npm error code E403 Granular access tokens that bypass two-factor authentication may not perform this action. That is registry policy, not configuration. RELEASE.md specifies a granular token scoped to this package precisely so a leak cannot publish anything else, and npm will not let such a token unpublish at all. A classic automation token would be the only way to satisfy it, which trades a bounded blast radius for an unbounded one to automate an operation that should be rare and deliberate. So the guards guarded a button that never fires. Reverted rather than left in place: a dispatch entry named "Unpublish" that 403s on every input is worse than its absence, because the next person reads it as the supported path and burns part of a 72-hour window finding out. The finding is worth more than the workflow, and it goes in RELEASE.md instead: CI publishes, humans withdraw, and withdrawing needs an interactive login with 2FA from a maintainer's own machine inside the window.
akf66
added a commit
that referenced
this pull request
Aug 26, 2026
…#47) The 1.x line has to stop being installed, and unpublishing it from CI turned out to be impossible: npm refuses a granular token that bypasses 2FA (#46). This is the half of that job the registry does allow. `npm deprecate` is also the better instrument for the goal. It leaves the tarball in place, so anyone pinned keeps working, and prints a warning on every install — which is the outcome an unpublish only approximates, by breaking them instead. RELEASE.md already named it the default; there was just no way to run it, because the only credential this project has is a repository secret. Reversible, which is why this one is safe to keep where the unpublish workflow was not: dispatching with an empty `message` clears the flag, per npm's own convention. A mistake here is one more dispatch, not a burned version number. One guard, for the one-click mistake that matters: the version in package.json is refused. Deprecating whatever dist-tags.latest points at puts a warning on every install of the package and reads as an outage. Verification reads the ANONYMOUS packument and asserts each version landed in the requested state, plus that the live release is not flagged. `npm deprecate` exiting 0 is not evidence, for the same reason publish.yml has a gate 6. A version that refuses warns and the loop continues, then the job fails at the end — one refusal must not leave the rest of the line unmarked.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#45 was correct and cannot work. Dispatched against all nine 1.x versions (run), every one came back:
That is registry policy, not configuration.
RELEASE.mdspecifies a granular token scoped to this one package precisely so a leak cannot publish anything else — and npm will not let such a token unpublish at all. A classic automation token is the only thing that would satisfy it, which trades a bounded blast radius for an unbounded one in order to automate an operation that should be rare and deliberate.So the guards guarded a button that never fires. Reverting rather than leaving it: a dispatch entry named Unpublish that 403s on every input is worse than its absence, because the next person reads it as the supported path and burns part of a 72-hour window finding out.
The finding is worth more than the workflow, so it goes in
RELEASE.md: CI publishes, humans withdraw, and withdrawing needs an interactivenpm loginwith 2FA from a maintainer's own machine inside the window.2.0.0 (#44) is unaffected — live, public,
latest, zero dependencies.