Skip to content

Commit d53335e

Browse files
hotlonggithub-actions[bot]claude
authored
fix(release): retry the post-publish npm check, and verify all 69 packages instead of 1 (#15331)
* fix(release): retry the post-publish npm check, and check all 69 packages The last step of the publish job verified the release with one `npm view` of @objectstack/cli. On 17.3.0 it exited 1 on a release that had succeeded completely. It raced the registry: npm committed cli@17.3.0 at 10:53:25.108 and the check read at 10:53:32, seven seconds later, and got an absence. The write path and the CDN-fronted read path are eventually consistent. And it looked at 1 package of 69, so a package that genuinely failed to publish is the one case it could not see — and the false red on cli would have masked it. @objectstack/runtime did not commit until 11:00:32. scripts/release-verify-npm.mjs replaces it: bounded backoff over the whole derived publishable set, with the absent packages named on failure. It still fails closed — the retry absorbs latency, never absence — and an empty target set is refused rather than passed vacuously. Publish gating is untouched: everything up to the step body and everything from "Create GitHub Releases" to EOF is byte-identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m * fix(release): write the job summary synchronously, and pin that it lands The append was `import('node:fs').then(...)` — a floating promise, discarded by the caller's `process.exit(await main())`. It wrote the summary on neither path while reading, in review, exactly like one that did. Battery 9 pins it the only way this class can be pinned: append, then read the file back with no await in between. A deferred write fails that case. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent ab50c8f commit d53335e

3 files changed

Lines changed: 1037 additions & 3 deletions

File tree

.github/workflows/lint.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2872,6 +2872,30 @@ jobs:
28722872
- name: Release-body limit guard
28732873
run: pnpm check:release-body
28742874

2875+
# Post-publish npm verification (#15321). The last thing the `publish` job
2876+
# does is prove every published package is readable on npm. That check ran
2877+
# as one `npm view` of @objectstack/cli and failed the 17.3.0 release,
2878+
# which had succeeded completely: npm committed cli@17.3.0 at 10:53:25.108
2879+
# and the read at 10:53:32 got an absence, because the write path and the
2880+
# CDN-fronted read path are eventually consistent. Worse, it looked at 1
2881+
# package of 69, so the one failure it exists to catch — a package that
2882+
# genuinely did not publish — is the one it could not see, and the false
2883+
# red on `cli` would have masked it.
2884+
#
2885+
# scripts/release-verify-npm.mjs replaces it with bounded backoff over the
2886+
# whole derived publishable set. Its --self-test is the ONLY instrument on
2887+
# that logic: the production path runs once per release, on a runner, with
2888+
# npm's real timing, so nothing else can tell a working retry loop from a
2889+
# broken one. It drives the real code with a stub registry — the #15321
2890+
# cold read reproduced through the OLD parameters (one target, no retry,
2891+
# which is a red on a healthy package) and then absorbed by the new ones,
2892+
# plus the partial publish the old shape reported green.
2893+
#
2894+
# Invoked as `node` rather than through a `pnpm check:*` alias: see the
2895+
# GATE INVOCATION IDIOM note at the top of this file.
2896+
- name: Post-publish npm verification self-test
2897+
run: node scripts/release-verify-npm.mjs --self-test
2898+
28752899
# #3825 Node-version drift guard: a runtime pin is 18 separate string
28762900
# literals across .github/workflows, so a split is invisible until someone
28772901
# greps for it. One did open — every PR gate sat on Node 20 (EOL

.github/workflows/release.yml

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1224,10 +1224,47 @@ jobs:
12241224
12251225
# `changeset publish` skips versions already on the registry, so a
12261226
# re-dispatch over a partially-published release is a repair, not a
1227-
# duplicate. What is NOT optional is that the version is on npm when
1227+
# duplicate. What is NOT optional is that EVERY package is on npm when
12281228
# this step ends.
1229-
if ! npm view "@objectstack/cli@$VERSION" version >/dev/null 2>&1; then
1230-
echo "::error::publish ran but @objectstack/cli@$VERSION is still not on npm"
1229+
#
1230+
# ⚠️ This checks what happens AFTER a publish, never WHETHER one runs.
1231+
# The gating above it — `environment: release`, the `publish-pending`
1232+
# predicate and its parenthesisation, `force`'s dispatch-only
1233+
# semantics — is the "ONLY A HUMAN PUBLISHES" invariant and is not
1234+
# this check's business.
1235+
#
1236+
# It used to be one `npm view` of @objectstack/cli, and #15321 is the
1237+
# two ways that failed on the 17.3.0 release, which had succeeded
1238+
# completely:
1239+
#
1240+
# ① It raced the registry. npm committed cli@17.3.0 at 10:53:25.108
1241+
# and this read at 10:53:32 — seven seconds later — and got an
1242+
# absence. The write path and the CDN-fronted read path are
1243+
# eventually consistent; a single shot immediately after a
1244+
# 69-package burst bets the release on how fast reads settle.
1245+
# scripts/release-verify-npm.mjs retries with bounded backoff for
1246+
# 15 minutes, derived from that release's own npm `time` field:
1247+
# the last package (@objectstack/runtime) committed 7m07s after
1248+
# the first read.
1249+
#
1250+
# ② It looked at 1 package of 69. Had a package genuinely failed to
1251+
# publish, this step could not have seen it — and the false red
1252+
# on `cli` would have MASKED it. The verifier now derives the
1253+
# whole publishable set from the workspace (never a transcribed
1254+
# list) and names, on failure, exactly which packages are absent,
1255+
# so the `force` repair dispatch has something to act on.
1256+
#
1257+
# ⛔ It still fails CLOSED. The retry absorbs LATENCY, never absence:
1258+
# a package still missing when the budget is spent exits non-zero, and
1259+
# so does a registry that could not be read at all. No `|| true`, no
1260+
# downgrade to a warning — the value of this step is that it reds.
1261+
# `published=true` below is written only on its success.
1262+
#
1263+
# `$VERSION` is handed over as RELEASE_VERSION — the spelling the two
1264+
# steps below already use for the same value — so the verifier refuses
1265+
# outright if the workspace it derives targets from disagrees with the
1266+
# version this run was approved for.
1267+
if ! RELEASE_VERSION="$VERSION" node scripts/release-verify-npm.mjs; then
12311268
exit 1
12321269
fi
12331270
echo "published=true" >> "$GITHUB_OUTPUT"

0 commit comments

Comments
 (0)