Skip to content

Commit 4b7dbc4

Browse files
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

File tree

‎scripts/release-publish.sh‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,36 @@ set -euo pipefail
6060
# recomputes it and overrides whatever we export.
6161
unset npm_config_globalconfig NPM_CONFIG_GLOBALCONFIG
6262

63+
# Keep npm's own answer for every package.
64+
#
65+
# `changeset publish` (@changesets/cli 3.0.3, its pnpm branch) counts a
66+
# package as published when `pnpm publish` exits 0 and prints nothing pnpm or
67+
# npm said. pnpm 10 hands the upload to `npm publish`, and npm writes a debug
68+
# log of every run, its `http fetch PUT <status>` line included. Pointing
69+
# npm's logs dir at a record directory and lifting its 10-file cap keeps one
70+
# log per npm run, so "npm accepted it and the registry is slow" can be told
71+
# from "npm refused it" or "it was never uploaded". `release-verify-npm.mjs`
72+
# names the directory (one spelling, under $RUNNER_TEMP) and reads it back;
73+
# its header has the parse and what the record decides.
74+
#
75+
# Only npm's log settings change here: what is published, by whom, and the
76+
# `release` environment gating are untouched.
77+
NPM_RECORD_DIR="$(node scripts/release-verify-npm.mjs --start-npm-record)"
78+
export npm_config_logs_dir="$NPM_RECORD_DIR"
79+
export npm_config_logs_max=100000
80+
6381
# Publish to npm and create the local version tags.
64-
changeset publish
82+
publish_status=0
83+
changeset publish || publish_status=$?
84+
85+
# npm's answer per package, printed before anything else can fail. A report,
86+
# never the verdict: the verifier reads the record again and fails closed.
87+
node scripts/release-verify-npm.mjs --npm-answers \
88+
|| echo "::warning::could not print npm's per-package answers; scripts/release-verify-npm.mjs reads the record again."
89+
90+
if [ "$publish_status" -ne 0 ]; then
91+
exit "$publish_status"
92+
fi
6593

6694
# Push every new tag in one atomic transaction (see header). --tags pushes ALL
6795
# local tags regardless of reachability; --follow-tags would push nothing here

0 commit comments

Comments
 (0)