fix(release): key the publish lane on npm presence, not on pending changesets (#5442) - #5774
fix(release): key the publish lane on npm presence, not on pending changesets (#5442)#5774claude[bot] wants to merge 4 commits into
Conversation
…angesets (#5442) The publish half of `changeset-release.yml` asked "are changesets pending?" while being read as if it asked "did this version ship?". The two come apart on every release: the version PR is cut from `main` at T and merged at T+n, `main` takes ~18 merges a working day, and the merge does not remove the changesets that landed in between. Those belong to the next version — but keyed on them, the version this commit just bumped to is skipped and the next version PR bumps past it. Measured on bc21c70: 16 of the 90 versions `packages/core/CHANGELOG.md` declares never reached npm, and the repo says 17.6.0 where `dist-tags.latest` says 17.5.0. The `lane` job now also answers "is the declared version already on npm?" from the same sparse checkout, and the release job's push half turns on that instead. It is cheaper as well as more correct: an ordinary landing does not move the manifest version, so it skips the expensive job, where the old predicate ran it in full on every landing that happened to find `.changeset/` empty. `changesets/action@v1` picks its branch from repository state, so the publish lane clears pending changesets from the runner's working tree before invoking it — never committed, never pushed. And it ends with a loud check: the defect was a green run that published nothing, so the lane now reads the registry back and fails if the version it exists to ship is still absent. The refresh lane is untouched and still denied publishing twice over: no `publish:` input, no npm credentials. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0124Qg8rLvpXnQDwCmpKUmaJ
PM review — the safety-critical claim verified independently, and one consequence of merging that needs the maintainer's explicit go-aheadPM, ✅ The claim I most needed to check myselfThe new
Also confirmed from the shipped YAML rather than the prose: both refresh-lane denials survive (no
|
|
|
| claim | where it comes from |
|---|---|
clearing .changeset/ forces the publish branch |
v1's dispatch: !hasChangesets && hasPublishScript → runPublish |
the lane job's detector must mirror readChangesetState |
v1's src/index.ts |
the 161 runner-local deletions cannot reach main |
v1's src/run.ts — runPublish does only git.pushTag(); prepareBranch/pushChanges exist only in runVersion |
That third row is the one I verified by hand precisely because getting it wrong destroys every pending changeset in the repository. It is a v1 fact. If v2 changed the dispatch table, or gave runPublish any commit/push-branch behaviour, this PR's clear step stops being safe — and nothing in either PR would say so. The two PRs are individually sound and jointly hazardous, which is the shape that gets through review.
Not urgent, and here is the measurement rather than the reassurance
.github/workflows/dependabot-auto-merge.yml restricts auto-merge to version-update:semver-patch and version-update:semver-minor; a semver-major gets only a "
What follows
- ⛔ Do not merge chore(deps): bump changesets/action from 1 to 2 #4945 and this PR in either order without re-deriving. If chore(deps): bump changesets/action from 1 to 2 #4945 lands first, this PR must be re-derived against v2's source before it is merged — not rebased and shipped. If this PR lands first, chore(deps): bump changesets/action from 1 to 2 #4945 stops being a routine bump and becomes a change to the publish lane's semantics, needing the same scrutiny this PR got.
- The two belong in the same serial queue as [finding] The changeset release PR runs no CI at all — every ci.yml run on
changeset-release/mainisaction_required, so the release commit reachesmainunvalidated #5397 — same file, same region. The R2 fold-or-serial ruling on 16 of 90 versioned releases never reached npm — a release-PR merge that lands with changesets pending versions again instead of publishing (npm is currently a release behind: repo 17.6.0, npm 17.5.0) #5442 listed the lane's own cards; it did not account for a dependabot PR arriving in the middle, which is a gap in how that queue is built and worth remembering.
Noted on #4945 as well, so whoever reaches it first sees this. ⛔ Nothing pushed, no label changed, and nothing touched on #4945 — it is not this seat's PR.
Generated by Claude Code
…-presence-publish-predicate
…e the pin (#5442) ⛔ Comments only. This commit is a NO-OP to the workflow's execution: the pinned version, every input, every `if:` and every step body are byte-identical, and `yaml.safe_load` of the file before and after parses to structurally identical objects. Verified mechanically, not by eye. Dependabot proposed changesets/action v1 -> v2 in both repositories and it was declined in both — objectstack#9208 (closed unmerged 2026-08-19) and objectui#4945 (closed unmerged 2026-08-23). objectstack recorded the reasoning in its own repo (objectstack#9916); objectui had no equivalent, and #5442 makes that urgent: before it the pin rested on one v1 property, after it on two. The block states why v1 is pinned (safety properties asserted against v1's source, not a version preference), the two properties an upgrade must re-verify — the `!hasChangesets && hasPublishScript -> runPublish` dispatch table, and that `runPublish` never commits and never pushes a branch — and the five v2 changes readable from its CHANGELOG that would break this file. The second property is the sharp one: it is what makes the clear step's runner-local deletion safe, and v2's #692 moves pushes to the GitHub API by default, which is a different code path than the one that verification covers. It also records the plausible wrong answer explicitly: the Changesets CLI generation is NOT what pins us. v2's #699 directs CLI v2 users to @v1, and this repository is on CLI v3 (`^3.0.0`, lockfile 3.0.1). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0124Qg8rLvpXnQDwCmpKUmaJ
PM review of the pin-rationale commit — ACCEPTED. No-op proven twice, by two independent methods
The no-op claim, checked independently rather than acceptedThe commit is comments-only, and that is the whole safety argument for adding it to a PR whose merge triggers a release. The dev proved it by Two independent methods, same answer. The pinned version, all four inputs, both Also confirmed: the base was brought in with a merge ( Line references re-verifiedThe dev re-derived the Two things the dev added that the dispatch order did not have
The docs decision is right, and the reasoning is the valuable part
Litter, and one new findingThe stray The dev also reported, correctly asking rather than filing: a PR-body PATCH appends a fresh bare attribution footer on every edit instead of rewriting it (measured 1 → 2 → 3). The PM independently corroborated it against PR #5755 — a different card, a different dev, the same doubled shape — and filed it as objectstack#11273. Status unchanged⛔ Still a draft, ⛔ not enqueued, ⛔ no auto-merge, ⛔ no release executed. The maintainer has accepted the merge in principle and will choose the moment. Generated by Claude Code |
State change recorded: ready → enqueued → dequeued (08:16Z), and why the queue attempt could not have succeededPM note, Observed, all by The queue attempt would have failed regardless of anything in this PR, and the reason is structural rather than incidental. Per the mechanism recorded on objectui#5668 (comment The precondition just became available
The order this now has to go in
Generated by Claude Code |
…-presence-publish-predicate
✅ Unblocked and fully green — ready for the maintainer's chosen momentPM, The blocker cleared. objectui#5668's lockfile refresh landed as New head
Re-confirmed at this moment, as promised — ⛔ do not use this morning's figures
The divergence still holds, so Two siblings are now in the queue ahead of it#5769 (#5436) and #5785 (#5409) were flipped ready and enqueued by this seat — both ruled, reviewed, green, non-governed, and with no release consequence. Neither can trigger a publish: until this PR lands, the old predicate still skips the release job whenever changesets are pending, and 161 are. If they merge first, this PR's base moves. The merge queue builds the merged result, and their docs-page hunks are disjoint from this one's (lines 38 / 933–960 and the coverage rows, versus 718–771), so no conflict is expected — but @os-zhuang — nothing further is waiting on this seat. Enqueue when you want the release to happen. Generated by Claude Code |
Fixes #5442
Keys the publish lane of
.github/workflows/changeset-release.ymlon "is the version this commit declares already on npm?" instead of on "are changesets pending?", and adds a loud check so a repo/npm divergence fails visibly. Maintainer ruling 2026-08-22, Option B — not re-litigated here.Derived from the workflow as it stands at
bc21c704b, not from the card's mechanism table. Post-#5762 the card's mechanism paragraph is stale: a push carrying pending changesets no longer takesrunVersion, the wholereleasejob is skipped. Same outcome, different branch.The defect, re-measured read-only at
bc21c704bpackages/core/CHANGELOG.md0.2.1 0.2.2 3.2.0 4.0.0 4.1.0 4.2.0 4.3.1 5.1.0 5.2.0 11.1.0 12.0.0 12.1.0 13.0.0 17.0.0 17.4.0 17.6.0packages/core/package.json17.6.0dist-tags.latest17.5.0Identical to the set the card reported, so the evidence is unchanged and live. Evidence gathered read-only against
registry.npmjs.org. ⛔ No release was executed — nothing published, tagged, or dispatched..changeset/onmaincurrently holds 161 pending changesets (as@changesets/readcounts them), so the current predicate has the release job skipped on essentially every push.Why the predicate had to change
The version PR is cut from
mainat T and merged at T+n.maintakes ~18 merges a working day and the merge does not remove the changesets that landed in between. Those belong to the next version — but keyed on them, the version this commit just bumped to is skipped, and the next version PR bumps straight past it.The npm predicate is cheaper as well as more correct, not a trade against #5762's saving: an ordinary landing does not move the manifest version, so it answers "already published" and the expensive job is skipped — where "no changesets pending" ran the job in full on every landing that happened to find
.changeset/empty, only to publish nothing.What changed
lanejob gainsIs this commit's version already on npm?. One registry read; sparse checkout grows from.changesetto.changeset+packages/core. It asserts that@object-ui/coreis still in thefixedgroup rather than assuming it, and refuses to guess a lane when the registry is unreadable (200 published / 404 not / anything else fails).releasejobif:push half now readsversion_on_npm == 'false'instead ofpending_changesets == 'false'.Clear pending changesets for the publish branch—changesets/action@v1picks publish-vs-version from repository state, not from an input, so the predicate cannot reach it on its own. The lane hands it a tree with nothing pending. Nothing is committed and nothing is pushed: verified against the action'ssrc/run.ts, whererunPublishruns the publish script and then pushes tags and creates releases — it never commits and never pushes a branch.Verify the release reached npm— the loud check.The changeset detector is untouched, and the clear step is not a second copy of it
The
lanejob's declared mirror ofreadChangesetStateis unchanged, and its stated reasons carry forward — it still predicts the action's branch, which is what requires exact agreement.pending_changesetsremains load-bearing, but now for the clear step rather than for deciding whether a release exists.The clear step keeps only
README.mdand is deliberately a superset of what the action counts. Its obligation is one-directional — leave the count at zero — so it does not need to be the mirror, and it self-checks instead of reasoning: it re-scans afterwards and fails if anything survived..changeset/config.jsonis not touched (changeset publishandcheck-published-dist-tooling.mjsboth read thefixedgroup from it).⛔ The refresh lane still cannot publish — both denials preserved
Confirmed by parsing the shipped YAML, not by reading it:
Neither denial was weakened, and no new path reaches npm from
scheduleorworkflow_dispatch. The new publish-lane steps are both gatedgithub.event_name == 'push'.Acceptance, stated as measurements
Both new step bodies were extracted verbatim from the shipped YAML and executed.
1. A release-merge commit that lands while changesets are pending still publishes the version it bumped to.
Against a copy of the real
.changeset/(161 changesets, plus apre/probe.mdandAGENTS.md/CLAUDE.mdto exercise the ignore list), running the shipped clear step:0is exactlyhasChangesets == false, which is exactly the action's!hasChangesets && hasPublishScript -> runPublishbranch. And the predicate at the current head:Reverse direction, against a version that is published:
manifest_version=17.5.0 / version_on_npm=true-> skipped. The predicate discriminates; it is not stuck on one answer.2. A repo/npm version divergence produces a loud, visible failure.
The shipped verify step, run against the live divergence:
Against a published version:
@object-ui/core@17.5.0 is on npm./exit=0.Three guards also fired loudly when provoked: anchor no longer in the
fixedgroup (exit=1), registry unreadable withcurlshimmed to fail (exit=1after retries, refusing to guess), and lane-output-vs-checkout version mismatch (exit=1).This is the shape the card asked for: run 3370 on
cfeb378b5completedsuccesshaving published nothing. That run would now be red.Verification at
f537de3e6Union re-run on the final commit:
pnpm vitest run --maxWorkers=2 scripts/__tests__/->Test Files 61 passed (61)/Tests 1631 passed (1631)node scripts/check-changeset-presence.mjs->✅ No source of a released package changed in this range, so no changeset is owed.(exit 0) — the script answered; the empty-frontmatter changeset follows the repo's declared convention for CI-only PRs, matchingrelease-lane-tests-and-concurrency-5404.mdon this same file.node scripts/check-control-bytes.mjs->✅ check-control-bytes: OK (scanned 4808 tracked text file(s); skipped 85 binary).node scripts/check-doc-links.mjs->Links are valid across 13 scan roots.node scripts/check-changeset-fixed.mjs+check-changeset-no-major.mjs-> both✅run:block passesbash -n.Reverse verification (fix committed first, mutations confirmed on disk by grep count, restored by
git checkoutand the tree proven byte-identical toHEAD):changeset-release.ymlfrom the docs heading ->ci-cd-pipeline-doc.test.tsred (1 failed | 31 passed). The inventory pin is watching this section.version:value ->sync-quick-reference-release.test.tsred (1 failed | 11 passed). This one matters: that test takes the first^\s*version:line in the file, and this PR adds steps above it. The pin still resolves to the refresh step.Repo-wide
pnpm lintnarrowed, and the narrowing is measured rather than asserted.eslint --no-inline-config --format jsonon all three changed paths reports each asFile ignored because no matching configuration was supplied— they are outside eslint's population entirely, read from eslint's own config, not from my guess. None is a module input to any linted file, so this diff cannot move the verdict on an untouched file. CI runs the full farm regardless.Docs
content/docs/guide/ci-cd-pipeline.mdis pinned bidirectionally byscripts/__tests__/ci-cd-pipeline-doc.test.ts. Only the### Changeset Release (changeset-release.yml)section is touched — diff hunks are confined to lines 718–771. The inventory row at line 40 is left alone (triggers did not change), as are the lockfile-merge-driver and secrets tables. #5409 and #5436 are editing other regions of this page concurrently; ⛔ no other card's rows were hand-edited. Expect to rebase if this lands second.Second commit: the
@v1pin's rationale, written beside the pin⛔ Comments only — a no-op to the workflow's execution. Proven mechanically, not by eye:
yaml.safe_loadof the file before and after parses to structurally identical objects (a == b->True), and every changed line in the diff is a comment or blank. The pinned version, every input, everyif:and every step body are untouched.Dependabot's v1 -> v2 bump was declined in both repositories — objectstack#9208 (closed unmerged 2026-08-19) and objectui#4945 (closed unmerged 2026-08-23). objectstack recorded its reasoning in-repo (objectstack#9916); objectui had no equivalent, and this PR makes that urgent: before it the pin rested on one v1 property, after it on two.
The block sits directly under the shared "one changesets step per lane" banner, so a reader reaching either
uses: changesets/action@v1line passes through it, with a back-pointer at the far-away refresh step. It states:The two properties an upgrade must re-verify.
!hasChangesets && hasPublishScript -> runPublish— both thelanepredicate and the clear step exist only because the action picks its branch from repository state. If a future version takes an explicit instruction, the clear step becomes dead weight to delete rather than a workaround to port.runPublishnever commits and never pushes a branch. Re-verified myself in v1'ssrc/run.ts, by line:runPublishspans 83–198 (the next function,getVersionPrBody, starts at 199); the only git calls inside aregit.pushTag()at 124 and 146;git.prepareBranch()(290) andgit.pushChanges()(362) occur solely insiderunVersion. This is precisely what makes the clear step's runner-local deletion of ~161 files safe — if it stops holding, that step becomes a commit that deletes every pending changeset in the repository.Five v2 breakages, each re-checked against the v2.0.0 CHANGELOG entry rather than copied: #681/#668 inputs renamed and kebab-cased (
version→version-script,publish→publish-script,commit→commit-message,title→pr-title; alsobranch→pr-base-branch, unused here) — this file passes all four old names and both resulting failures are silent; #692 release commits and tags pushed via the GitHub API by default (commit-mode→ booleanpush-with-git-cli), which is the one that undercuts property 2, since that verification covers the git-CLI path; #674GITHUB_TOKENenv var no longer accepted, must use thegithub-tokeninput (both lanes pass it viaenv:); #695.npmrchandling removed whenNPM_TOKENis set; #678 published-package detection moved to aCHANGESETS_OUTPUTfile that custom scripts must forward — andpnpm changeset:publishis a custom script, with #697 downgrading the miss to a warning, i.e. failing quietly, which is this card's whole subject.The plausible wrong answer, recorded as wrong. The Changesets CLI generation is not what pins us. v2's #699 validates CLI v3 and directs CLI v2 users to
@v1; this repo declares@changesets/cli: ^3.0.0andpnpm-lock.yamlresolves 3.0.1 (objectstack declares^3.0.0too). Measured before writing it.Also noted as an opportunity rather than an action: v2's #656 adds
/select-mode,/versionand/publishsub-actions, and a dedicated publish sub-action is plausibly the shape that makes the clear step unnecessary. And #5775 is cross-referenced as the same class of drift, live under any bump — the mirror tracks a bundled copy this file does not pin.Why
ci-cd-pipeline.mddid NOT need a matching lineChecked rather than assumed. The pin test requires a heading and inventory row per workflow file; no workflow was added or removed, and it says nothing about action version pins. The page names
@v1once (line 755) only incidentally, while describing the publish-vs-version mechanism — that sentence is still accurate and nothing on the page became stale. More to the point, the pin's rationale is maintenance guidance for whoever edits the workflow, and its reader stands at theuses:line; copying it onto unpinned prose would recreate exactly the second-copy drift generator that objectui#3724 deleted.github/WORKFLOWS.mdto remove. So: no docs edit, deliberately.Not in scope
#5397is held strictly behind this card by the fold-or-serial ruling and is not addressed here..github/workflows/changelog.yml,cliff.tomland.github/workflows/ci.ymlwere not touched (#5409 / #5436 hold those).content/docs/releases/**untouched.One out-of-scope finding was filed rather than fixed — see the report on #5442.
Generated by Claude Code
Generated by Claude Code