Skip to content

fix(release): unknown npm codes retry and say so - #21

Merged
JumpLink merged 2 commits into
mainfrom
fix/retry-unknown-codes
Sep 15, 2026
Merged

JumpLink merged 2 commits into
mainfrom
fix/retry-unknown-codes

Conversation

@JumpLink

@JumpLink JumpLink commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

@girs/clutter-7 and @girs/meta-8 are the only two packages that did not reach 5.2.0. Both
died on

npm error code IDENTITY_TOKEN_READ_ERROR
npm error error retrieving identity token

1h26m and 2h50m into a 3h10m publish sweep — a transient OIDC token read — and both were
retried zero times.

Why zero

isRetryablePublishError consulted two sets. IDENTITY_TOKEN_READ_ERROR parses fine (the code
regex 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_CODES already records
this exact bug class from the 4.2.0 release, where E409 was 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

classifyPublishError answers three ways — terminal, retry, unknown — so "I do not
recognise this" is a value the caller has to handle rather than the absence of one.

  • Unknown defaults to retry, and announces itself: a line per package naming the code,
    and one ::warning:: per distinct code saying which list it belongs in.
  • The unknown budget is 2 retries, not 10. Measured against this repo'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
    ~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_ERROR is named explicitly as well — recognised beats defaulted.
  • The classifier stays pure; the announcing happens at the call site, so the startup
    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 to terminal
fails the self-test on exactly those vectors:

❌ Fatal error: retry-classifier self-test FAILED:
  an unrecognised code is UNKNOWN, and unknown retries: expected retryable=true
  an unrecognised code is UNKNOWN, and unknown retries: expected verdict=unknown, got terminal
  no code line at all is UNKNOWN, not terminal: expected retryable=true

Retry arithmetic checked against the real loop shape: unknown → 3 attempts, known-retryable →
11, terminal → 1. tsc --noEmit clean, 28 unit tests pass.

Two operational warnings recorded

release.yml had 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.

  1. A merge to main publishes. The trigger is push: main, not only release. Merging
    PR fix(sdk-types): wait until the generator installs #20 — a change to sdk-types.yml and nothing else — started run 35017963560, which
    reported 714 already published, 2 to publish and published @girs/clutter-7@5.2.0 and
    @girs/meta-8@5.2.0. The right outcome, but a side effect rather than a decision.

  2. Reading npm tells you what npm cached. Packuments are served
    cache-control: public, max-age=300 (abbreviated form included) and npm ships
    prefer-online=false, so a 30-second retry loop spends its first ten attempts on one
    five-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 where
    prose survives — and a README moved there would be the first thing a regeneration removes.

🤖 Generated with Claude Code

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.
@JumpLink
JumpLink requested a review from ewlsh as a code owner September 15, 2026 20:34
@JumpLink
JumpLink merged commit 19c3c05 into main Sep 15, 2026
4 checks passed
@JumpLink
JumpLink deleted the fix/retry-unknown-codes branch September 15, 2026 20:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant