Skip to content

fix: tell a lagging read from a broken release - #17

Merged
JumpLink merged 1 commit into
mainfrom
fix/publisher-lag-and-counters
Sep 11, 2026
Merged

fix: tell a lagging read from a broken release#17
JumpLink merged 1 commit into
mainfrom
fix/publisher-lag-and-counters

Conversation

@JumpLink

Copy link
Copy Markdown
Contributor

The v5.0.0 sweep was the first real run of the topological publisher, and it worked: measured
from the run's own log, 0 ordering defects over 7593 edges, with the only out-of-order edges
being the 11 inside the six-member cycle, open for 27 seconds. Down from 508 packages over 128
minutes.

It still ended red, over 76 closure gaps. Not one of them was a defect. Every single one named a
dependency this same run had already published — the registry simply had not caught up. Measured
on that run: @girs/matekbd-1.0 was readable 4m12s after its own publish returned success.

What was wrong

One errors counter stood for two different facts: a publish that failed, and a closure that is
not readable yet. Everything else followed from that.

✅ 110.6% - Processed: 716, Errors: 76      <- 716 + 76, the same packages twice
   📋 Total: 792                            <- there are 716
❌ 76 of 792 package(s) failed to publish   <- they published fine

A reader of that log looks for 76 broken publishes that do not exist.

What this changes

A surviving gap is classified against the plan, not the registry. Whether a gap is a defect
was never a function of how long npm is asked:

the dependency is verdict fatal
published earlier, or in the same group lag no
published later in this same run ordering-defect yes
not published by this release at all not-in-release yes

The middle row already threw before this PR; it never fired in v5.0.0, which is the independent
confirmation that the plan was right. The last row is now its own case rather than an unnamed one.

The lag budget drops from 60s to 15s. It buys quiet, not correctness — a gap that resolves
inside the window never reaches the log, and nothing else. At 60s it cost the v5.0.0 sweep roughly
an hour of its two hours: 19.2 s per package against the 10.7 s of the release before it.

Phase 3 gets the last word, so it has to run and has to be patient. It now runs whenever
nothing failed to publish (before: only when the gap count was also zero, so lag skipped it), and
it retries a resolution miss up to four times at a minute apart. That is also the fix for the three
SDK channels that published cleanly and were then reported uninstallable by this very probe,
seconds later.

Validation

  • npm run check, npm test — 23 tests, 4 of them new for the classification, one asserting the
    same gap yields three different verdicts under three plans
  • npm run test:e2e green, including both negative arms — the fatal paths still exit 1
  • and the arm that matters for a change that makes something non-fatal: with classifyGap forced
    to return lag for everything, the e2e negative arm flips red. Without that mutation test this
    PR would be indistinguishable from one that quietly switched the gate off.

🤖 Generated with Claude Code

https://claude.ai/code/session_012KM53BXFvEdP4PLgcojKCS

v5.0.0 published all 716 packages in the planned order — 0 ordering defects over 7593
edges, measured from the log — and still ended red over 76 closure gaps, every one of
them naming a dependency this same run had already published. `@girs/matekbd-1.0` was
readable 4m12s after its own publish returned success.

One `errors` counter stood for two different facts, a failed publish and a closure not
yet readable. That is where `110.6%`, `Total: 792` (716 + 76 counted twice) and
`76 of 792 package(s) failed to publish` came from.

Classify a surviving gap against the PLAN instead: a dependency this run publishes
earlier or in the same group is lag, one it publishes later is an ordering defect, one
it never publishes is permanent. Only the last two are fatal. Whether a gap is a defect
was never a function of how long the registry is asked, so the lag budget drops from 60s
to 15s — it buys quiet, not correctness, and it cost the v5.0.0 sweep about an hour
(19.2 s/package against the 10.7 of the release before).

Phase 3 therefore becomes the check with the last word, so it runs whenever nothing
failed to publish, and retries a resolution miss up to four times before believing it.
That is also what left three SDK channels red after publishing cleanly.

Claude-Session: https://claude.ai/code/session_012KM53BXFvEdP4PLgcojKCS
@JumpLink
JumpLink requested a review from ewlsh as a code owner September 11, 2026 19:31
@JumpLink
JumpLink merged commit 010d440 into main Sep 11, 2026
4 checks passed
@JumpLink
JumpLink deleted the fix/publisher-lag-and-counters branch September 11, 2026 19:39
JumpLink added a commit that referenced this pull request Sep 11, 2026
Ported from #15, now closed: everything else it carried arrived via #16 and #17.

A sweep killed by `timeout-minutes` reports that the RUNNER exceeded its maximum execution time,
naming neither the package it was on nor how far it got. `NPM_DEADLINE_MIN` (default 300, under
release.yml's 360) makes the publisher fail by name first.

Elapsed and ETA go on every progress line for the same reason: the run-level `updatedAt` GitHub
exposes does not advance while a job streams logs, so from the API a sweep that is working looks
exactly like one that is wedged. That reading is what got the 4.8.0 sweep cancelled at 38% while
it was publishing normally.

Both decisions are pure functions in publish-plan.ts and held to cases that go both ways — zero
disables the deadline rather than expiring instantly, which a plain `now > start + budget` gets
wrong.

Claude-Session: https://claude.ai/code/session_012KM53BXFvEdP4PLgcojKCS
JumpLink added a commit that referenced this pull request Sep 11, 2026
`@girs/sdk-gnome-51@5.0.0` published cleanly at 20:12:33 and was reported uninstallable at
20:15:35. The retry from #17 did run, three times, and then ran out: four attempts at 60 s is
three minutes, and readability lag on that registry the same day reached 4m12s
(`@girs/matekbd-1.0`) — a number already measured when the budget was chosen.

Attempts are the wrong unit. One probe costs 5.5 minutes on the 716-package set and seconds on a
single SDK channel, so the same count means wildly different patience. Budget the wall clock
instead: 10 minutes, re-asking every 30 s.

The packument's own `time` entry is no guide either: it said 20:12:32.972 for a version no install
could resolve three minutes later. Written and readable are different events.

`shouldRetryResolution` keeps the arm that matters more than the budget — a 403, an EINTEGRITY, a
corrupt tarball are never retried, or one clear red becomes a slow one.

Claude-Session: https://claude.ai/code/session_012KM53BXFvEdP4PLgcojKCS
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