Repository navigation
Commit e4d604e
fix(release): re-derive release-verify-npm read-back budget from the 18.0.0 reading (35 min, flat) (#24989)
Fixes #24888
Clause-②: no
`scripts/release-verify-npm.mjs` waited 15 minutes for npm's read path
to settle. On 18.0.0 the last package became readable 16m22s after the
first read, so a release that was complete read red. This PR re-derives
the budget from the 18.0.0 reading and sets it to **35 minutes, one flat
number for the group**. It still fails closed. The self-test now replays
the 18.0.0 window on npm's own clock and pins `main()`'s exit code.
## The measurements, re-read
| Reading | Source | Value |
|---|---|---|
| Publish job | Actions API, job 118624531794 (run 39421537775, head
`3fbef41b4a`) | 12:24:14Z to 12:57:40Z, failure |
| Step 21, "Publish to npm + push version tags" | same | 12:36:08Z to
12:57:38Z, failure; steps 22 and 23 skipped |
| The verdict | check-run annotations of job 118624531794 | `publish ran
but 1 of 73 package(s) are still not readable on npm after 19 round(s)
over 900s: @objectstack/console@18.0.0` |
| `console` readable | `npm view @objectstack/console time --json` |
`"18.0.0": "2026-10-20T12:57:59.327Z"` |
| Verification start | the card's reading of the job log | 12:41:37Z |
**The start time is not re-read here.** The job-log download redirects
to a blob host that this container's egress proxy refuses (403, a policy
denial, not retried). The readings above corroborate it:
- 900 s of waits that end at 12:57:38Z put the start at or before
12:42:38Z.
- Replaying the 19 rounds against npm's `time` for all 73 packages gives
408 probes. The card's start implies 61 s of probing, about 150 ms per
probe.
**Spread: 12:41:37Z to 12:57:59.327Z = 982.3 s (16m22s).**
**Sizes** come from npm's `dist` fields and the tarball bytes,
downloaded:
| Package | Packed | Unpacked | Files | Readable at |
|---|---|---|---|---|
| `@objectstack/console` | 9,399,458 B | 37,040,621 B | 2282 | +16m22s
(last) |
| `@objectstack/spec` | 42,118,133 B | 197,756,260 B | 2100 | +10m04s |
The card's "42424 KB" for `console` lives in the job log, so it could
not be re-read. It is not `console`'s tarball, which is 9.4 MB. The
release's largest tarball is `spec`'s.
## Shape: flat, 35 minutes. The deciding axis: actual business need
(实际业务需求)
**Derivation.** 17.3.0's margin was 900 s / 427 s = 2.11x. The same
margin over the new spread is 982.3 s x 2.11 = 2070 s (34m30s). Rounded
up, that is 35 minutes (2100 s, 2.14x the 18.0.0 spread). The header
records this next to the 17.3.0 derivation.
| Axis | Flat budget | Size-scaled allowance per package |
|---|---|---|
| **Actual business need (实际业务需求), measured: decides** | Covers the
group's spread, which is what the 18.0.0 red was. | Its premise is that
the slow package was slow because it is large. The measurement says
otherwise: `spec` (42.1 MB packed, the largest) was readable six minutes
**before** `console` (9.4 MB). On 17.5.0 the order was reversed
(`console` 08:05:24Z, `spec` last at 08:09:33Z), and on 17.3.0 the last
package was `runtime`. Any monotonic size scale gives the slowest 18.0.0
package a smaller allowance than one that settled six minutes earlier. |
| Long-term soundness (项目长远合理性) | One constant, re-derived from readings
recorded in the header. | Adds a second model (size to latency) that has
no reading behind it and would need its own re-derivation. |
| Guarding against AI mistakes (防 AI 写代码犯错) | One number that a reader
can check against the header. | Needs the size of a package the registry
cannot serve yet, so the size has to come from a new local input. Each
missing or wrong size becomes a per-package fallback, which is the
lenient direction. |
| No scope spread at startup stage (创业阶段不扩散需求) | Changes one constant. |
Adds a new mechanism and a new input. |
**The budget is a ceiling, not a wait.** The loop returns on the first
round that finds the last target, so a good publish pays only its own
settle time. Replayed on this schedule, 18.0.0 goes green at +17m15s
(battery 14 pins that figure). Only a publish that is really broken
spends all 35 minutes.
## Fit against the caller
`release.yml` was read, not edited. Neither the `publish` job (`:1565`
to `:2132`) nor its step "Publish to npm + push version tags" declares
`timeout-minutes`. The only `timeout-minutes` in the file is
`full-battery`'s 10, at `:1536`. GitHub's default therefore bounds both:
360 minutes per job. The `release-integrity` audit calls the script with
`--probe` (`:1251`). That path passes a budget of 0 and is unchanged;
battery 10 pins its single round.
Measured on 18.0.0:
| Segment | Time |
|---|---|
| Before step 21 (build 4m44s, vendored Console SPA 5m44s) | 11m54s |
| Step 21 before verification (`pnpm run release`: publish plus tag
push) | 5m29s |
| Verification at the old budget | 16m01s (900 s of waits plus about 61
s of probing) |
| Steps 22 and 23 on a green run | 1m55s: 1m48s plus 7s, measured on the
same two scripts in the backfill, run 39426677839, job 118640898073 |
Worst case under the new budget, using budget plus poll interval plus
the rest of the step:
| Term | Value |
|---|---|
| Waits | exactly 2100 s (38 waits, 39 rounds); battery 1 pins that the
schedule sums to the budget |
| Poll interval | 60 s, the cap |
| Probing when **nothing** reached npm | 39 rounds x 73 probes x 150 ms
= 427 s (7m07s) |
| Rest of the step | 5m29s |
| **Step** | 5m29s + 35m00s + 1m00s + 7m07s = **48m36s** |
| **Job** | 11m54s + 48m36s + 1m55s = **62m25s, against 360 minutes: 297
minutes of margin (5.8x)** |
The same terms at the old budget gave 38m46s.
One case lies outside that formula and existed before this change: a
registry where every request hangs to the 15 s per-request ceiling. Each
round then costs 73 x 15 s = 18m15s. That totals 347 minutes of probing
at the old 19 rounds and 712 minutes at the new 39. In both cases
GitHub's 360-minute default cancels the job. The job is red, so the run
still fails closed, but the cancellation does not name a package. The
outcome is the same before and after this change.
## Self-test and ablation
The self-test goes from 115 cases across 13 batteries to **131 cases
across 14 batteries**, with every battery floored.
**Battery 1** gains two cases: the budget outlasts the 18.0.0 spread,
and it keeps the 2.11x margin over it.
**Battery 14** ("The 18.0.0 publish window: console readable at +16m22s,
past the old 900 s") has 14 cases. It replays npm's own `time` for
`sdui-parser`, `cli`, `spec` and `console` on a clock that the stub
`sleep` drives. Probing is taken as instant, so a green in the replay is
one the real step would also have seen. No case sleeps.
- The old budget (900 s, passed as a parameter) reds and names only
`console`, after 19 rounds over 900 s, which matches the run's
annotation. Its last read of `console` came before `console` landed.
- The production budget is never passed in, so it is the default. It
goes green on the first round after `console` landed, at +17m15s, past
the round where 900 s ran out.
- **A package readable after 900 s and before the new budget passes:**
`main()` over a fixture workspace exits 0 and prints the `::notice::`
line.
- **An absent package exits non-zero and is named:** with `console`
never landing, `main()` exits 1. Its `::error::` line names
`@objectstack/console@18.0.0` and "1 of 4", and names none of the three
packages that landed. It concludes only after the waits sum to exactly
`RETRY_BUDGET_MS`.
This needed one seam change. `main()` now accepts the verifier's own
`probe` and `sleep` seams; the defaults are the production ones. Its
exit code and naming can therefore be pinned without the network or a
real wait.
The case of a large and a small package, each held to its own allowance,
does not apply, because there is no per-package allowance.
**Ablation.** I committed the fix first (`a9e61579a3`). I then ran
`scripts/ablation-replace.mjs` in wrap mode, replacing `export const
RETRY_BUDGET_MS = 35 * 60 * 1000;` with `15 * 60 * 1000;`.
- Proof that the mutation landed: anchor count went from 1 to 0,
replacement count from 0 to 1, blob `7f259d67ff62` became
`c36e8cf51a07`.
- `node scripts/release-verify-npm.mjs --self-test` then exited 1 with
**8 of 131 cases failed**, exactly the cases that need more than 900 s
(one JSON detail is shortened):
```text
x the budget outlasts the 16m22s spread measured on the 18.0.0 burst -- 900000 > 982327
x and keeps the 17.3.0 margin (2.11x) over it -- 900000 >= 2070478 (0.92x)
x THIS BUDGET sits through the 16m22s spread and goes GREEN -- {"ok":false,"missing":[{"name":"@objectstack/console",...}],"rounds":19,"waitedMs":900000,"checked":4}
x ...on a round the old budget never reached -- waited=900000
x ...the FIRST round after console landed, not by spending the budget -- 2026-10-20T12:55:52.000Z .. 2026-10-20T12:56:37.000Z
x ...at +17m15s, the figure the header quotes -- waited=900000
x main() exits 0 on the 18.0.0 replay, under the production budget -- code=1 ::error::publish ran but 1 of 4 package(s) are still not readable on npm after 19 round(s) over 900s: @objectstack/console@18.0.0
x ...and its notice says the whole group settled -- 1 of 4 package(s) not readable yet after round 18; waiting 45s (900s of 900s budget spent).
x release-verify-npm self-test: 8 of 131 case(s) failed.
```
The red went in the expected direction. The old-budget repro and the
absent-package cases stayed green, as they should.
The restore was proven on disk: the blob after the restore equals the
HEAD blob (`7f259d67ff62`), `git diff HEAD` was empty and `git status
--porcelain` printed nothing.
## Changeset: `skip-changeset`, measured
`scripts/release-verify-npm.mjs` belongs to the root package
`@objectstack/spec-monorepo`, which is `private: true` and has no
`files`. No non-private workspace package lists a `files[]` entry under
`scripts` or with `..`. No tracked symlink points into the root
`scripts/`.
`git grep -l RETRY_BUDGET_MS` over `packages/**`, `apps/**` and
`skills/**` returns 0 hits (exit 1). The positive control over
`scripts/**` returns `scripts/release-verify-npm.mjs` (exit 0).
## Gates (local, at `a9e61579a3`)
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 33 commands for this one-file diff.
All 33 ran and each exited 0, `check:pm-dispatch-gates` included (fast
tier, 1913 cases).
- `--ran` reconciliation: `33 derived, 33 run, 0 NOT-MEASURED, 0 UNRUN`.
- `npx eslint --no-inline-config scripts/release-verify-npm.mjs` exited
0.
- `git merge-tree --write-tree origin/main HEAD` at `origin/main`
`78ab6474ca` exited 0. The commits since the base touch none of this
script, `release-github-releases.mjs`, `workspace-enumerator.mjs` or
`release.yml`.
CI owns the rest of the farm. `lint.yml`'s "Post-publish npm
verification self-test" step runs this self-test.
## Acceptance notes
- **Stale comments in a governed file, not edited here.** Two comments
in `.github/workflows/release.yml` still describe the old budget:
- `:2077` to `:2080` says the verifier "retries with bounded backoff for
15 minutes, derived from ... 7m07s";
- `:1236` says "instead of waited on for 15 minutes".
Both now read stale. `.github/workflows/**` is governed, so this needs a
separate comment-only edit if wanted. Carrier: none.
- **Pre-existing.** The hung-registry worst case above exceeds GitHub's
360-minute default at either budget, and is cancelled red without naming
a package. This is an inference from the constants, not a reproduction.
Carrier: none.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 9532e27 commit e4d604e
1 file changed
Lines changed: 198 additions & 19 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
45 | | - | |
46 | | - | |
47 | | - | |
48 | | - | |
49 | | - | |
50 | | - | |
51 | | - | |
52 | | - | |
53 | | - | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
54 | 83 | | |
55 | 84 | | |
56 | 85 | | |
| |||
128 | 157 | | |
129 | 158 | | |
130 | 159 | | |
131 | | - | |
| 160 | + | |
132 | 161 | | |
133 | 162 | | |
134 | 163 | | |
| |||
195 | 224 | | |
196 | 225 | | |
197 | 226 | | |
198 | | - | |
| 227 | + | |
199 | 228 | | |
200 | 229 | | |
201 | 230 | | |
| |||
208 | 237 | | |
209 | 238 | | |
210 | 239 | | |
| 240 | + | |
211 | 241 | | |
212 | 242 | | |
213 | 243 | | |
214 | 244 | | |
215 | | - | |
| 245 | + | |
216 | 246 | | |
217 | 247 | | |
218 | 248 | | |
| |||
282 | 312 | | |
283 | 313 | | |
284 | 314 | | |
285 | | - | |
286 | | - | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
287 | 320 | | |
288 | 321 | | |
289 | 322 | | |
| |||
697 | 730 | | |
698 | 731 | | |
699 | 732 | | |
700 | | - | |
| 733 | + | |
| 734 | + | |
| 735 | + | |
| 736 | + | |
| 737 | + | |
| 738 | + | |
| 739 | + | |
701 | 740 | | |
702 | 741 | | |
703 | | - | |
| 742 | + | |
| 743 | + | |
| 744 | + | |
| 745 | + | |
| 746 | + | |
| 747 | + | |
704 | 748 | | |
705 | 749 | | |
706 | 750 | | |
| |||
733 | 777 | | |
734 | 778 | | |
735 | 779 | | |
736 | | - | |
| 780 | + | |
737 | 781 | | |
738 | 782 | | |
739 | 783 | | |
| |||
753 | 797 | | |
754 | 798 | | |
755 | 799 | | |
756 | | - | |
| 800 | + | |
| 801 | + | |
757 | 802 | | |
758 | 803 | | |
759 | 804 | | |
| |||
1381 | 1426 | | |
1382 | 1427 | | |
1383 | 1428 | | |
| 1429 | + | |
| 1430 | + | |
| 1431 | + | |
| 1432 | + | |
| 1433 | + | |
| 1434 | + | |
| 1435 | + | |
| 1436 | + | |
| 1437 | + | |
| 1438 | + | |
| 1439 | + | |
| 1440 | + | |
1384 | 1441 | | |
1385 | 1442 | | |
1386 | 1443 | | |
| |||
2214 | 2271 | | |
2215 | 2272 | | |
2216 | 2273 | | |
| 2274 | + | |
| 2275 | + | |
| 2276 | + | |
| 2277 | + | |
| 2278 | + | |
| 2279 | + | |
| 2280 | + | |
| 2281 | + | |
| 2282 | + | |
| 2283 | + | |
| 2284 | + | |
| 2285 | + | |
| 2286 | + | |
| 2287 | + | |
| 2288 | + | |
| 2289 | + | |
| 2290 | + | |
| 2291 | + | |
| 2292 | + | |
| 2293 | + | |
| 2294 | + | |
| 2295 | + | |
| 2296 | + | |
| 2297 | + | |
| 2298 | + | |
| 2299 | + | |
| 2300 | + | |
| 2301 | + | |
| 2302 | + | |
| 2303 | + | |
| 2304 | + | |
| 2305 | + | |
| 2306 | + | |
| 2307 | + | |
| 2308 | + | |
| 2309 | + | |
| 2310 | + | |
| 2311 | + | |
| 2312 | + | |
| 2313 | + | |
| 2314 | + | |
| 2315 | + | |
| 2316 | + | |
| 2317 | + | |
| 2318 | + | |
| 2319 | + | |
| 2320 | + | |
| 2321 | + | |
| 2322 | + | |
| 2323 | + | |
| 2324 | + | |
| 2325 | + | |
| 2326 | + | |
| 2327 | + | |
| 2328 | + | |
| 2329 | + | |
| 2330 | + | |
| 2331 | + | |
| 2332 | + | |
| 2333 | + | |
| 2334 | + | |
| 2335 | + | |
| 2336 | + | |
| 2337 | + | |
| 2338 | + | |
| 2339 | + | |
| 2340 | + | |
| 2341 | + | |
| 2342 | + | |
| 2343 | + | |
| 2344 | + | |
| 2345 | + | |
| 2346 | + | |
| 2347 | + | |
| 2348 | + | |
| 2349 | + | |
| 2350 | + | |
| 2351 | + | |
| 2352 | + | |
| 2353 | + | |
| 2354 | + | |
| 2355 | + | |
| 2356 | + | |
| 2357 | + | |
| 2358 | + | |
| 2359 | + | |
| 2360 | + | |
| 2361 | + | |
| 2362 | + | |
| 2363 | + | |
| 2364 | + | |
| 2365 | + | |
| 2366 | + | |
| 2367 | + | |
| 2368 | + | |
| 2369 | + | |
| 2370 | + | |
| 2371 | + | |
| 2372 | + | |
| 2373 | + | |
| 2374 | + | |
| 2375 | + | |
| 2376 | + | |
| 2377 | + | |
| 2378 | + | |
| 2379 | + | |
| 2380 | + | |
| 2381 | + | |
| 2382 | + | |
| 2383 | + | |
| 2384 | + | |
| 2385 | + | |
| 2386 | + | |
| 2387 | + | |
| 2388 | + | |
| 2389 | + | |
| 2390 | + | |
| 2391 | + | |
| 2392 | + | |
| 2393 | + | |
| 2394 | + | |
2217 | 2395 | | |
2218 | 2396 | | |
2219 | 2397 | | |
| |||
2226 | 2404 | | |
2227 | 2405 | | |
2228 | 2406 | | |
2229 | | - | |
| 2407 | + | |
| 2408 | + | |
2230 | 2409 | | |
2231 | 2410 | | |
2232 | 2411 | | |
| |||
0 commit comments