Repository navigation
Commit 4b7dbc4
tooling(release): keep npm's own answer per package, so registry lag reads apart from loss (#26650)
<!-- claude-projects-attribution: {"github_login":"os-warren"} -->
_Requested by **Warren** · [project
thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8FiD8VYiNjD9zPG48Gh7MbM)_
Fixes #26558
## 维护者速读
- 改了什么:发版时把 npm 对每个包上传的原始回应(是否接受、HTTP 状态、耗时)留下来,publish
一结束就逐包打印进发布日志;核验超时时,日志会分清“npm 已接受、只是还读不到(registry 延迟)”和“npm 拒收 /
根本没上传”,并给出各自的下一步。
- 为什么改:18.2.0 的 `@object-ui/console` 被 npm 接受后约 55 分钟才可读,日志只写“not on the
registry”,和真丢包看起来一样,结果多了一次 `force` 重跑和一次人工审批。
- 风险与回滚:只改 `scripts/release-publish.sh` 和
`scripts/release-verify-npm.mjs` 两个脚本;发布内容、谁能发布、`release`
环境审批都没动,`release.yml` 未改。核验仍然“读不到就红”,npm 记录读不到时只是少了诊断,判定不变。回滚即 revert 本
PR。
- 先看:① `scripts/release-publish.sh` 新增的 npm 日志目录与 publish 后打印段(含失败时不推
tag 的分支);② `scripts/release-verify-npm.mjs` 的 `main()` 失败分支与
`formatFailure()`;③ 同文件头部 18.0.0 / 18.1.0 / 18.2.0 的逐包实测表与“预算不加”的理由。
- 两分钟验证:`node scripts/release-verify-npm.mjs --self-test`,预期末行为 `OK
release-verify-npm self-test: 164 cases pass across 16 batteries`。
- 需要你做的:无,席位落地。
## What changes
`changeset publish` (@changesets/cli 3.0.3, `dist/getPublishPlan.mjs`,
pnpm branch) counts a package as published when `pnpm publish` exits 0
and keeps none of its output. pnpm 10.31 hands the upload itself to `npm
publish` (read in the installed pnpm: `runNpm(["publish",
"--ignore-scripts", TARBALL, ...])`), and npm 10 writes a debug log of
every run: the argv naming the tarball, `http fetch PUT STATUS URL Nms`,
`error code ...` and `verbose exit N`.
- **`scripts/release-publish.sh`**: before `changeset publish`, it asks
the verifier for an empty record directory under `$RUNNER_TEMP`
(`--start-npm-record`, one spelling of the path), points npm's
`logs-dir` at it and lifts `logs-max` off its default of 10. After the
publish it prints `--npm-answers`, one line per package inside a
collapsed group with a count line under it, and only then exits on a
failed publish or pushes the tags. That keeps the old order: tags are
pushed only after a clean publish.
- **`scripts/release-verify-npm.mjs`**: when a package is still
unreadable after the budget, it reads the record and says what npm
answered. **accepted** (PUT 2xx) is registry lag, not loss: the
`::error::` line says `(npm accepted it: registry lag)`, and the text
says nothing needs republishing and gives the next step
(`RELEASE_VERSION=V node scripts/release-verify-npm.mjs --probe`, then
the `force: true` backfill once it answers `published`). **refused**
(non-2xx, with npm's code and summary), **not-sent** (npm exited before
uploading) and **no-upload** keep the `force: true` repair text. A
missing or unreadable record says the log cannot tell lag from loss. The
job summary carries the same answer.
- ⛔ It still fails closed. The record is read only after the loop has
failed, and only to say which kind of red it is. The exit code is 1 in
every case, and the target set still comes from the workspace, never
from the record.
- **Budget: unchanged at 35 minutes, now with the measurement on
record** (header table). Spread = npm `time` minus T0, where T0 is the
tagger date of the version's annotated tags, which `changeset publish`
creates after the last upload returns:
| version | pkgs | T0 | p50 | p90 | slowest |
|---|---|---|---|---|---|
| 18.0.0 | 73 | 12:41:28Z | +1m22s | +3m52s | `@objectstack/console`
+16m31s |
| 18.1.0 | 73 | 09:02:22Z | +0m48s | +2m53s | `@objectstack/spec` +9m34s
|
| 18.2.0 | 110 | 06:16:43Z | +1m13s | +3m18s | `@object-ui/console`
+54m22s (next: `@objectstack/console` +8m36s) |
The 18.2.0 tail is a single package, 3.3 times the next-worst spread.
Covering it with the 2.11x margin the budget has always used would need
a step of about 115 minutes, inside a 120-minute job that also holds the
build. It would also make every lost package take two hours to be named.
So that tail now reads as registry lag, still red, instead of buying a
longer wait.
- `release.yml` is not touched, so this PR is not Tier H. The publish
step already runs `pnpm run release` (which runs `release-publish.sh`)
and the verifier in the same shell, so the record dir under
`$RUNNER_TEMP` reaches both without a workflow edit.
## Tests
Two new self-test batteries in `node scripts/release-verify-npm.mjs
--self-test` (164 cases, 16 batteries; roster floor 14 to 16):
- **15** (25 cases): parsing accepted, refused, not-sent, non-publish
and two-runs-in-one-file (ambiguous) logs; credential-shaped text
redacted; acceptance wins over a later refusal of the same tarball; an
absent record is reported with where it looked; the per-package report.
It runs `main()` on the 18.2.0 shape (everything readable except
`@object-ui/console`): accepted means exit 1 with the registry-lag line
and no `Repair:` line; refused means exit 1, `(npm refused it: E403)`
and the force repair; no record means exit 1 and "cannot tell lag from
loss". It also runs the **real `pnpm publish`** (through to npm 10, as
changesets calls it) against a stub registry on 127.0.0.1 that answers
200 to one upload and 403 to the other, in a sealed environment, and the
production parser reads PUT 200 and PUT 403 / E403 back out of npm's own
logs. A change in npm's log format reds the self-test.
- **16** (8 cases): `release-publish.sh` itself, with `changeset` and
`git` stubbed. npm is handed the record dir and the lifted cap, a stale
record is emptied, `npm_config_globalconfig` is still dropped, the
publish log carries npm's answer and the count line, and tags are pushed
after the publish in one push. On a failed publish, the answers are
printed, the script exits 1 and no tag is pushed.
## Acceptance notes
- The record lives in `$RUNNER_TEMP` and is not uploaded as an artifact
(that would need a `release.yml` edit). Only parsed fields reach the
log: status, elapsed ms, npm's code and its first error line, scrubbed
of credential-shaped text.
- npm names a log after its start millisecond, so two npm runs started
in the same millisecond share one file. The parser reports both packages
as "cannot be read" instead of guessing, which is still red.
- The job-log blob host is denied by this container's egress policy, so
the 18.1.0 / 18.2.0 verification start instants could not be read from
the job logs. T0 is the annotated tags' tagger date instead. On 18.0.0
that reads nine seconds before the verification's first read.
## Verification
Head `3981356b` (branch merged with `origin/main` 03e898c; the diff
against it is the two scripts, +697/-15).
<details><summary>dispatch-gates: 31 derived, 31 run, all exit
0</summary>
`node scripts/pm/dispatch-gates.mjs --ran FILE --repo
objectstack-ai/objectstack`: `✓ dispatch-gates --ran: 31 derived
famil(ies) accounted for — 31 run, 0 NOT-MEASURED`. Among them: `node
scripts/release-verify-npm.mjs --self-test` (exit 0, 164 cases / 16
batteries), `pnpm check:nul-bytes`, `pnpm check:bash32-floor`, `pnpm
check:entry-guard`, `pnpm check:parse-guard`, `pnpm
check:cross-package-test-inputs`, `pnpm check:scripts-symbol-anchors`,
`node scripts/check-self-test-wired.mjs`, `node
scripts/check-self-test-workflow-commands.mjs`. `pnpm
check:pm-dispatch-gates` ran detached; its verdict line reads `✓
dispatch-gates self-test (fast tier): 1935 cases pass`.
</details>
<details><summary>Also run</summary>
- `npx eslint --no-inline-config scripts/release-verify-npm.mjs`: exit
0. `bash -n scripts/release-publish.sh`: exit 0.
- `node scripts/release-dist-tags.mjs --self-test` (its last battery
reads the publish job's wiring): exit 0.
- No changeset: root `scripts/` ships in no package (`skip-changeset`).
</details>
Clause-②: no
Metadata-protocol: none
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(not touched)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes. (eslint on the changed
file; the repo-wide run is CI's)
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset. This one publishes
nothing (root `scripts/`), so it carries `skip-changeset`.
- [x] 6. `packages/spec` touched: not touched.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English (the 维护者速读 block is the
maintainer digest).
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01JQwxbaFpnMwgpCgneKe2qS
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 378c940 commit 4b7dbc4
2 files changed
Lines changed: 733 additions & 16 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
60 | 60 | | |
61 | 61 | | |
62 | 62 | | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
63 | 81 | | |
64 | | - | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
65 | 93 | | |
66 | 94 | | |
67 | 95 | | |
| |||
0 commit comments