Skip to content

fix(publish-smoke): derive the tarball pin set from the publishable population, not a by-name exclusion - #11257

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-11253-smoke-unscoped-pin
Aug 23, 2026
Merged

fix(publish-smoke): derive the tarball pin set from the publishable population, not a by-name exclusion#11257
os-zhuang merged 2 commits into
mainfrom
claude/issue-11253-smoke-unscoped-pin

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#11253

The packed-tarball smoke never packed or pinned create-objectstack, so the 17.2.0 release candidate's own smoke died on a version that by definition does not exist yet. Failed run: 32622283383, job Packed-tarball smoke (release candidate).

[ERR_PNPM_NO_MATCHING_VERSION] No matching version found for create-objectstack@17.2.0
This error happened while installing the dependencies of @objectstack/cli@17.2.0
The latest release of create-objectstack is "17.1.0".

The mechanism — one correction to the card's reading

The card attributes the hole to an @objectstack/*scope filter in the pin-set construction. The pin set was not built from a scope glob: publish-smoke-pack.mjs already derived from private !== true. The hole was a hand-written by-name exclusion sitting on top of that derivation:

// Not consumed as npm dependencies by a scaffolded project:// create-objectstack — the scaffolder itself; the smoke runs it straight// from the repo's built bin, and no @objectstack/* manifest depends on it.constEXCLUDE=newSet(['create-objectstack']);

The rationale was true when written and expired when @objectstack/cli took a dependency on the scaffolder. Its general shape is the real bill: whether a workspace package is reachable from some other manifest is a fact about the dependency graph at one moment, and it is not a question this script gets to ask. Publishable is the question.

A scope filter did exist, in the second place the card told me to look — the registry-leak assertion in publish-smoke.sh, which drove off a '@objectstack/[^'@]+@[0-9] grep. That made the guard blind in exactly the case it existed to catch, so the run got ok — 0 leaks and then died 200 lines later inside pnpm. A guard whose alphabet is narrower than the set it guards is not a guard.

Source of truth: the workspace, not the fixed group

Both candidates enumerate the same 69 names, and they are provably equivalent right now — measured on this branch:

publishable (private !== true): 69
.changeset/config.json fixed group: 69
set identical to fixed group: true

They are equivalent because scripts/check-changeset-fixed.mjs already asserts it in both directions (a public package missing from the group reddens; a group name that no longer exists in the workspace reddens). That is exactly why the workspace is the right source and the group is not: the group is a derived declaration that a gate keeps honest. Deriving the pin set from it would mean reading a copy — correct today, and correct only for as long as a second gate keeps it so. The workspace is the thing itself.

Before / after

Same command, same tree, before and after the change:

pin set sizecreate-objectstack in set
before68false
after69true

create-objectstack is the only unscoped publishable package in the workspace — which is why no fixture whose names all begin with @ could have caught this.

What changed

  • scripts/publish-smoke-pack.mjs — the set is private !== true, full stop: no scope filter, no exclusion, no hand list. assertPinSetTotal re-checks pin set == publishable set in both directions at the derivation site and reports offenders by name (both halves matter: a missing pin silently tests published code, a surplus pin tests a resolution no real user can reproduce).
  • scripts/publish-smoke.sh — the registry-leak assertion now drives off the override map's own names, scoped and unscoped alike, instead of a scope grep. De-duplicated across the lockfile's packages:/snapshots: sections.
  • --self-test wired as check:publish-smoke-pin in lint.yml. The assertion it pins lives on the release path, which runs only during a release — without this step a regression in it would again be discovered by a release candidate rather than by the PR that caused it. Pure functions, synthetic fixtures, no pnpm/workspace/network, ~0.05s.
  • Prose in publish-smoke.yml / publish-smoke.sh that described the pin set as @objectstack/* — the self-description the card quoted — corrected to match what the code does.

Verification

The full smoke ran locally, green, in pack mode — CI does not have to take over any leg of it:

Packing 69 publishable package(s) → /tmp/objectstack-publish-smoke.sblLoc/tarballs
wrote .../smoke-app/pnpm-workspace.yaml (69 overrides, template settings preserved)
'create-objectstack': 'file:.../tarballs/create-objectstack-17.1.0.tgz'
== Asserting no pinned package leaked to the registry
ok — 58/69 pinned packages resolved from tarballs, 0 registry leaks
ok — POST /auth/sign-up/email → 200
ok — POST /data/smoke_app_note (create) → 201
ok — DELETE /data/smoke_app_note/IUZR2guTmyOFvXne (delete) → 200
== Publish smoke passed (pack mode)

Isolated install leg — the defect and the fix, with the real tarballs. Two throwaway projects outside the workspace, same tarball set, differing only in whether create-objectstack is pinned:

LEG OLD (68 overrides, create-objectstack pinned: false)
create-objectstack@17.1.0: ← npm REGISTRY
LEG NEW (69 overrides, create-objectstack pinned: true)
create-objectstack@file:../tarballs/create-objectstack-17.1.0.tgz ← candidate tarball

At 17.1.0 that registry resolution succeeds and silently smokes published code; at 17.2.0 it is the hard ERR_PNPM_NO_MATCHING_VERSION above. Same hole, two faces. The packed @objectstack/cli manifest confirms the trigger — pnpm pack rewrites workspace:* to a concrete version, so the published manifest really does demand the unpublished one:

name/version: @objectstack/cli 17.1.0
create-objectstack dep: "17.1.0"

The guard half, both assertions run against that same leaking lockfile:

OLD guard (scope grep): ok — no @objectstack/* leaked ← BLIND; the install had leaked
NEW guard: EXIT=1
::error::these PINNED packages resolved from the npm registry:
create-objectstack@17.1.0

Self-test, ablation-checked. Restoring the by-name exclusion turns case 1 red (unscoped public package dropped from the set: ["@objectstack/cli"]); neutering the equality branch turns cases 2 and 3 red. Each mutation was confirmed on disk by grepping for the injected and the removed text before running — and the restore leg was re-verified green (SELF-TEST PASSED (4 cases)) with the mutation proven absent. No build/dist is involved: the script is executed directly, not resolved through a package exports.

Gates.node scripts/pm/dispatch-gates.mjs derived 17 families from the real change set; all 17 run green at c2fc2d9e (the final commit — the union was run after it):

check:cross-package-test-inputs · check:entry-guard · check:node-version · check:parse-guard
check:pnpm-filter-targets · check:publish-smoke-pin · check:required-contexts
check:shard-attestation · check:type-check-coverage · check:type-check-debt
check:workflow-status-functions · check-aggregator-roster · check-ci-filter-parity
check-cross-package-test-inputs · check-required-contexts · check-shard-attestation
check-step-collectors

check:type-check-debt needs the built closure to --re-measure, so the closure was built first exactly as lint.yml does (turbo run build --filter='./packages/*' --filter='./packages/*/*', 70/70 successful) and it then reported OK — 33 ledger entr(ies) re-measured, 1897 raw tsc error(s) total, none above its recorded number. Its lowering hints are pre-existing and untouched by this diff. check:nul-bytes green. The new gate registers itself in the derivation (check:publish-smoke-pin … matched via scripts/publish-smoke-pack.mjs ⇢ gate script).

node scripts/pm/check-governed-merges.mjs --test on the final file list:

governed-surface predicate: 0 of 5 path(s) hit the register (5 surfaces, repo-agnostic).
✅ NOT governed — ordinary queue landing applies to a PR with exactly this file list.

Changeset: none — skip-changeset. Nothing here ships to npm: two scripts, two workflows, and one script entry in the private monorepo root manifest.

For the release operator

After this merges, re-run the smoke from the Actions UI: open publish-smoke.ymlRun workflow → pick branch main → Run. publish-smoke.yml has no push/pull_request trigger (only schedule for the weekly registry canary, plus workflow_dispatch), so merging alone does not start a run — the dispatch is a manual step. workflow_dispatch runs the pack-smoke job (if: github.event_name != 'schedule'), which is the leg that was failing.

Then: green smoke → merge #10183 (17.2.0) → approve the release environment. #10183 needs no refresh; this PR is scripts-only and does not enter the publish set.


Generated by Claude Code

…opulation, not a scope/exclusion
The packed-tarball smoke built its pin set as "publishable MINUS a hand-written
exclusion", and the exclusion held `create-objectstack` on the rationale that no
@objectstack/* manifest depends on it. That rationale expired when
@objectstack/cli took a dependency on the scaffolder: the unscoped name was
neither packed nor pinned, pnpm fell back to the registry, and the smoke died on
ERR_PNPM_NO_MATCHING_VERSION for a version that by definition is not published
yet — a deterministic false red on every release candidate.
- publish-smoke-pack.mjs: the set is `private !== true`, full stop. No scope
filter, no exclusion, no hand list. `assertPinSetTotal` re-checks pin set ==
publishable set in BOTH directions at the derivation site and names the
offending packages, so a future exclusion cannot reopen the hole silently.
- publish-smoke.sh: the registry-leak assertion drove off a `@objectstack/*`
grep, which made it blind in exactly the case it existed to catch. It now
drives off the override map's own names, scoped and unscoped alike.
- A --self-test (unscoped package survives derivation; both directions of the
equality assertion) wired as `check:publish-smoke-pin` in lint.yml, so a
regression reddens on the PR that causes it rather than on a release run.
Co-Authored-By: Claude <noreply@anthropic.com>
…lockfile sections
A name is keyed in both `packages:` and `snapshots:`, so counting lines listed
every offender twice and inflated the tarball-resolved total.
Co-Authored-By: Claude <noreply@anthropic.com>
@os-zhuangos-zhuang added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 23, 2026 — with Claude
@github-actionsgithub-actionsBot added ci/cd dependencies Pull requests that update a dependency file labels Aug 23, 2026
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

ACCEPT — session 757f0dc0-5dae-51d7-9053-c15c57ebfe3b(派发席,卡 #11253,发版阻塞件)。

全量 diff 复核(5 文件 +230/−25,head c2fc2d9e):

  1. 机制修正采信且比卡上更深:洞不是作用域 glob,是派生之上的手写按名排除(EXCLUDE = new Set(['create-objectstack'])),其理由(「无 @objectstack/* 清单依赖它」)在 cli 引入脚手架依赖之日过期——「某包是否被依赖」是依赖图某一时刻的事实,不是这个脚本该问的问题;private !== true 才是。并且在 17.1.0 时同一个洞是静默的(registry 解析成功,烟测测的是已发布代码而非候选)——过往部分绿是假绿,本修复同时终结两副面孔。
  2. 第二盲区同修:泄漏守卫原 grep @objectstack/* 作用域,在它存在意义的那个场景恰好失明(失败 run 先打「ok — 0 leaks」再死在 pnpm);现改为以 override 表自身名字为字母表,file: 与 registry 解析逐名区分,泄漏逐名点名。「守卫的字母表窄于它守的集合就不是守卫」——这句该进语料。
  3. 双向断言 + PR 时自测:assertPinSetTotal 缺员/多员双向逐名红;4 例自测(夹具特意含无作用域包——全 @ 夹具对这类缺陷不可见)消融验证过两个方向;check:publish-smoke-pin 接进 lint.yml,发布路径断言从此在开洞的 PR 上红,不在发版操作者的 run 上红。
  4. 本地全烟测跑通:pack 69 包(含 create-objectstack tarball 钉入)→ 脚手架安装 → auth + REST CRUD 探针 200/201 → Publish smoke passed (pack mode),无一腿留给 CI。真源头论证(workspace 本体 > 被门守着的 fixed 组副本,69≡69 实测)成立。
  5. NOT governed(0/5)、skip-changeset 已读回、17 族门禁全绿引用在案。

落地:flip ready + auto-merge。合并后按 PR 正文「For the release operator」节操作。


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddependenciesPull requests that update a dependency filesize/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-zhuang@claude