Skip to content

ci(release): the release-integrity audit names the changesets a version commit did not consume - #21373

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21361-unconsumed-changeset-audit
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21361-unconsumed-changeset-audit

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21361
Clause-②: no

What this does

Triage's third direction, and only that one. When the release-integrity audit queues a publish, it now reports every pending .changeset/*.md in the version commit's tree. These are the changesets the version commit did not consume. The report names each one with the commit that added it, so the gap shows up when the release is cut, not one release later.

  • Logic: a new unconsumed mode in scripts/release-pending-publish.mjs. That script already finds the version commit (select) and already refuses a shallow clone. It gets seven new --self-test batteries on throwaway repositories.
  • Wiring: one call in the existing id: audit step of .github/workflows/release.yml. The call sits in the branch that queues the publish (pending == true, on a push or the repair dispatch), right after the "waiting for your approval" summary. There is no new job and no new gate.
  • Pin of the wiring: battery 12 of scripts/release-verify-npm.mjs already runs the audit step's real text, taken from release.yml, in a throwaway repository. Its fixture now carries one consumed changeset, one that landed behind the Version Packages PR, and a README.md. The version push must warn about exactly the one that landed behind, and must still queue the publish.

The 17.6.0 instance (748b240, #21270, .changeset/21110-scheduled-work-host-reason.md) is carried by the maintainer's #21362. That PR has since merged as 9360df4138 and put the dated correction into content/docs/releases/v17/17-6.mdx. This PR edits no content/docs/releases/** and no CHANGELOG.md.

How the version commit is found, and what "unconsumed" means

The version commit is the one select already names for publish to check out. It is the newest first-parent commit at which packages/cli/package.json's version differs from its first parent's (findVersionCommit). The step passes that sha to the new mode.

"Pending" means whatever changeset version itself would read:

  • a top-level .changeset/*.md that @changesets/read 1.0.1 (the version pnpm-lock pins) does not skip;
  • so not a dotfile, and not README.md (any case), AGENTS.md, CLAUDE.md or GEMINI.md.

Pre mode is read in both of the shapes it has been stored in:

  • the current cli moves a consumed changeset into .changeset/pre/, which is not top-level;
  • the 2.x shape that the 17.0.0 release candidates carry kept it in place and listed its id in .changeset/pre.json. An id listed there counts as consumed.

The add commit comes from @changesets/git's own lookup, git log --diff-filter=A --max-count=1, with --no-renames added and the walk started at the version commit. So the named commit is always the version commit or one of its ancestors. A changeset that lands after the version commit is in a different tree.

A shallow clone is refused, never answered

--diff-filter=A cannot be answered in a shallow clone. Below the graft boundary, every older changeset reads as added by the boundary commit, which is a confident wrong commit. The mode refuses a shallow clone in the same way select does. Measured in this container's own shallow checkout:

$ node scripts/release-pending-publish.mjs unconsumed --version-commit HEAD --json
::error::release-pending-publish: refusing to name the commits that added changesets in a shallow clone: at the graft boundary every older changeset reads as added by the boundary commit. Check out with fetch-depth: 0.
exit 1

The job's checkout already uses fetch-depth: 0, and select would refuse a shallow clone before this point anyway. If the report still cannot be measured, the script prints its own error annotation with the reason. The step then writes NOT MEASURED to the log and the job summary, and leaves the publish queued. The report never comes back empty without saying so.

Level: a warning, never a refusal

The finding is a ::warning:: plus a job-summary section. That is the level this job already uses for every finding that leaves a release incomplete without stopping it:

  • GitHub Releases or the D4 asset are incomplete;
  • the image is missing;
  • the push range is unreadable;
  • npm could not be read for part of the fixed group.

This step uses ::error:: only together with exit 1, for refusals. The report must not fail the step: publish needs this job, so a failure here would hold the npm publish, the tag and the image. That is why it reports and never refuses.

It runs only on the event that queues the publish, because that is the run the approver opens. A later landing during the approval window, and every audit of an already published version, print nothing. Otherwise the same warning would appear on every push to main for a whole release cycle. Battery 12 pins this as well.

Real history (a full-history clone, --is-shallow-repository false; the script at head 27979b22f4)

release version commit pending in its tree unconsumed from commits
17.6.0 617f25f8a 1 1 1: 748b24072 (#21270)
17.5.0 8c87d26a5 8 8 7: f11b5f20a e73ee2ddc×2 c876a7426 7a1faf1a5 c9d234c40 24d521e53 2123fcca3
17.4.0 7e6337007 13 13 13
17.3.0 8a1bad8b8 4 4 3
17.2.0 e7d2cc67f 0 0 (the control)
17.1.0 47d1ae89e 0 0
17.0.0 24c1b91e4 0 0
17.0.0-rc.6 e7e0a6dd9 1704 0 (all 1704 recorded in pre.json)
17.0.0-rc.4 bd191338e 1277 8 (1269 recorded) 8
17.0.0-rc.2 3cfd9f0b1 862 2 (860 recorded) 2
  • Triage's pins hold exactly: 8 changesets from 7 commits for 17.5.0, and 1 (748b240) for 17.6.0.
  • The control the dispatch suggested, 17.4.0, does not come back clean. It left 13 changesets unconsumed, and 17.3.0 left 4. git show --name-status shows each set deleted by the NEXT version commit: 17.4.0's by 8c87d26a5, 17.3.0's by 7e6337007. So those entries were published one release late, under 17.5.0 and 17.4.0. The clean controls are 17.2.0, 17.1.0 and 17.0.0. See the acceptance notes.

Tests

  • node scripts/release-pending-publish.mjs --self-test: ✓ release-pending-publish self-test: 68 cases across 20 batteries pass., up from 13 batteries. The seven new batteries and their floors:
    • a Version Packages PR landing behind main (4);
    • the control (2);
    • only what changeset version reads (2);
    • the 2.x pre.json (2);
    • the add commit: newest add, rename, a landing after the version commit (3);
    • shallow clone and unresolvable sha refused (2);
    • the report text (4).
      The battery-count floor goes from 13 to 20.
  • node scripts/release-verify-npm.mjs --self-test: OK release-verify-npm self-test: 96 cases pass across 13 batteries. Battery 12's floor goes from 14 to 17, with the three new cases.
  • Ablation (reverse verification), run once and not kept. Through scripts/ablation-replace.mjs, the call in release.yml was replaced with if ! true; then:
    • the anchor went from 1 hit to 0, and the blob from c975b6f27bbf to dd45fa2c88c1;
    • release-verify-npm --self-test then exited 1, with the two positive battery-12 pins red ("the version push warns of the changeset its version commit did not consume…" and "…the job summary carries the section");
    • the restore was proven by the blob equalling HEAD (c975b6f27bbf) and git diff HEAD being empty.
  • Gates: at head 27979b22f4, node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths, change set from the merge base) derives 51 commands. All 51 were run and exited 0, and --ran reconciles: 51 derived, 51 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero, since every row recorded its exit code). That set includes check:self-test-wired (every one of the 232 script(s) CI runs that ship a --self-test has that self-test run by CI), check-self-test-workflow-commands (no self-test prints a line the runner would parse as a workflow command), check:nul-bytes and check:bash32-floor. Lint: CI owns the repo-wide run. The narrowed run eslint --no-inline-config --format json on the two changed scripts reported 2 files, 0 errors and 0 warnings. The population comes from eslint.config.mjs: its **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} object covers scripts/*.mjs. That config never enables type-aware linting (no parserOptions.project), so this diff cannot move a verdict on any untouched file. No TypeScript package is touched, so no typecheck is owed.

Acceptance notes

  • 17.3.0 and 17.4.0 have the same gap, and nothing says so yet. 17.4.0 shipped 13 changesets' code with no 17.4.0 CHANGELOG entry, and 17.3.0 shipped 4. Their entries appear in 17.5.0's and 17.4.0's CHANGELOGs respectively. The 17.3.0 and 17.4.0 notes carry no dated correction of the kind 17.5.0 and 17.6.0 now have. That is release-owned text, out of scope for a code PR, and left to the maintainer.
  • NOT MEASURED: the NOT MEASURED fallback branch in the step (the script exits non-zero inside the real step text) has no running pin. Battery 12's fixture has no way to make the mode refuse after select succeeded. The mode's own refusals are pinned in its self-test.
  • NOT MEASURED: no ablation moved the call out of the pending branch. The negative pin ("a landing that does not queue the publish reports no changesets") is therefore unablated.
  • The mode would also report a changeset that changeset version skips on purpose, meaning one whose every package is in ignore or is private with privatePackages.version: false. .changeset/config.json configures neither today. This is noted in the script's docblock.
  • No changeset (skip-changeset): the diff touches .github/workflows/ and two repository-root scripts/*.mjs, and neither is in any package's files[].

Generated by Claude Code

claude added 4 commits October 2, 2026 08:16
… commit did not consume

The unconsumed mode lists every pending .changeset/*.md in the version
commit's tree, each with the commit that added it, and refuses a shallow
clone. Seven self-test batteries pin it on throwaway repositories.

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <noreply@anthropic.com>
…ersion commit did not consume

In the run that queues the publish, the audit step runs
release-pending-publish.mjs unconsumed and prints a warning plus a job
summary section naming each changeset and the commit that added it. It
never fails the step or holds the publish job. Battery 12 of
release-verify-npm.mjs pins it on the step's real text.

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <noreply@anthropic.com>
… and spell no command token in a comment

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <noreply@anthropic.com>
…consumed-changeset-audit

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 2, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 2, 2026 09:12
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 2, 2026 09:12
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit 7b21af8 Oct 2, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21361-unconsumed-changeset-audit branch October 2, 2026 09:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants