fix(release): unknown npm codes retry and say so - #21
Merged
Merged
Conversation
Twice a publish defect has been fixed by adding a code to one of two lists, and twice the partition kept its shape: two named halves and an unnamed remainder that silently meant "give up". `E409` in 4.2.0, then `IDENTITY_TOKEN_READ_ERROR` in 5.2.0, where `@girs/clutter-7` and `@girs/meta-8` died on a transient OIDC token read 1h26m and 2h50m into a 3h10m sweep, were retried ZERO times, and sat at 5.1.0 while the other 714 went to 5.2.0. A third one-line fix would have been the third time this was written down and the third time it recurred. So the classifier now answers three ways, and "unknown" is a value the caller must handle. An unrecognised code DEFAULTS TO RETRY and announces itself by name — a log line per package and one `::warning::` per distinct code, saying which list it belongs in — so the fourth occurrence is a grep away instead of a diff of 716 versions against the registry. The default is only safe because the budget is small. Measured against this repository's own settings (`NPM_MAX_RETRIES=10`, base 5 s, cap 300 s) the full budget is 11 attempts and 25 minutes of sleep per package: a systematic unrecognised code would exhaust the 360-minute job after about 12 of 716 packages, trading two missing packages for a stopped release. Two retries costs ~35 s and still catches the blip this exists for. `IDENTITY_TOKEN_READ_ERROR` is also named outright — being recognised beats being caught by the default. Self-test vectors cover the unknown case, including a code that deliberately does not exist; reverting the default to "terminal" fails the self-test on exactly those vectors. release.yml gains the header it never had: a merge to main publishes, measured on PR #20, plus what npm's own `max-age=300` does to a retry loop that reads the registry.
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.
@girs/clutter-7and@girs/meta-8are the only two packages that did not reach 5.2.0. Bothdied on
1h26m and 2h50m into a 3h10m publish sweep — a transient OIDC token read — and both were
retried zero times.
Why zero
isRetryablePublishErrorconsulted two sets.IDENTITY_TOKEN_READ_ERRORparses fine (the coderegex already allows underscores) but is in neither, so the composed predicate returned false
and the code meant "settled, give up". The comment above
RETRYABLE_NPM_CODESalready recordsthis exact bug class from the 4.2.0 release, where
E409was the code in neither list.That is the second occurrence, and adding a third code would be the third time the lesson is
written down and the third time the shape survives: two named halves and an unnamed remainder
that silently means terminal.
What changed
classifyPublishErroranswers three ways —terminal,retry,unknown— so "I do notrecognise this" is a value the caller has to handle rather than the absence of one.
and one
::warning::per distinct code saying which list it belongs in.(
NPM_MAX_RETRIES=10, base 5 s, cap 300 s), the full budget is 11 attempts and 25 minutes ofsleep per package — a systematic unrecognised code would exhaust the 360-minute job after
~12 of 716 packages, trading "two packages missing" for "the release stopped". Two retries
costs ~35 s and still catches the blip this exists for, which keeps the premise true.
IDENTITY_TOKEN_READ_ERRORis named explicitly as well — recognised beats defaulted.self-test runs the real decision rather than a copy.
Proof the guard is not decorative
Self-test is 14 vectors, up from 10, including a code that deliberately does not exist
(
EWATERMELON) and a message with no code line at all. Reverting the default toterminalfails the self-test on exactly those vectors:
Retry arithmetic checked against the real loop shape: unknown → 3 attempts, known-retryable →
11, terminal → 1.
tsc --noEmitclean, 28 unit tests pass.Two operational warnings recorded
release.ymlhad no header. It has one now, because this repository has no README,CONTRIBUTING or AGENTS.md — the workflow headers are where its operational prose lives.
A merge to
mainpublishes. The trigger ispush: main, not onlyrelease. MergingPR fix(sdk-types): wait until the generator installs #20 — a change to
sdk-types.ymland nothing else — started run 35017963560, whichreported
714 already published, 2 to publishand published@girs/clutter-7@5.2.0and@girs/meta-8@5.2.0. The right outcome, but a side effect rather than a decision.Reading npm tells you what npm cached. Packuments are served
cache-control: public, max-age=300(abbreviated form included) and npm shipsprefer-online=false, so a 30-second retry loop spends its first ten attempts on onefive-minute-old snapshot: twenty attempts, two real lookups. Verifying a publish needs a
real cache buster, not
npm view.The header also says WHY it is a header: the root is ~700 generated directories the type
generator rewrites wholesale,
.github/is the part it does not touch, so that is whereprose survives — and a README moved there would be the first thing a regeneration removes.
🤖 Generated with Claude Code