Skip to content

ci(release): the Version PR lane reads the v3 .changeset/pre/ layout (changesets/action v2.1.2, pinned by commit) and rehearses that read before the cut - #23313

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-23282-version-pr-pre-layout
Oct 14, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/issue-23282-version-pr-pre-layout

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23282

Clause-②: no (release CI)

One file, one job: .github/workflows/release.yml, the version-pr job (Version PR maintenance). No publish-lane line is touched.

What broke, measured

  • The scheduled Release run 38395962884 failed in Version PR maintenance at the PR step with ENOENT ... .changeset/pre/changes.md. The job log shows changesets/action@v1 resolved to a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d, which is the v1.9.0 tag.
  • The repository's own CLI is fine. pnpm changeset status exits 0 on main. In the same failing run, the "Render the post-version tree" step ran pnpm run version (changeset v3.0.3) to completion.
  • The failing reader is the action's own copy. v1.9.0 bundles @changesets/read 0.6.7. Its legacy v1 reader treats every directory under .changeset/ as an old folder changeset and opens DIR/changes.md and DIR/changes.json. Since the 18.0.0-next.0 cut, .changeset/pre/ is such a directory.
  • Reproduced with the same code: the v1.9.0 bundle, run on a scratch copy of main at 9c7cfbf0a9 (local mock API, no remote), fails at the same point (setting GitHub credentials) with the same error. The workflow-shaped replay of the base workflow fails the same way.

What this changes

  1. PR step: changesets/action v2.1.2, pinned by commit (ae32849d5ba541f9ae29e40e22a623bc13562f51). v2 is the major upstream pairs with @changesets/cli v3. It bundles @changesets/read 1.0.0, which reads .changeset/pre/. In pre mode, readChangesetState leaves pre/ ids out of hasChangesets; in exit mode it keeps them. The inputs move to v2's names: github-token, version-script, commit-message, pr-title. v2 throws on each v1 name instead of ignoring it. push-with-git-cli: true keeps v1's git-CLI push. create-github-releases: false stays, and push-git-tags: false is added; both only matter if a publish script were ever added. The GITHUB_TOKEN env is dropped, because v2 takes the token only through github-token.
    • It is a commit and not @v2 because upstream v2 is a branch. A moving ref would change the bundled reader without any diff here.
  2. The card's pin: a read rehearsal on the post-version tree. Three new steps go between "Validate the post-version tree" and "Restore the pre-version tree". They copy the rendered .changeset/ (plus the root package.json, pnpm-workspace.yaml and a node_modules link) to a directory outside every git work tree. They run the same pinned action there with no credential, then require has-changesets == false.
    • The rendered tree is the tree the next cut lands on main. A reader that cannot read what the CLI writes now goes red while the Version Packages PR is still open, not on the first refresh after it merges.
    • It cannot write. The only path out of a "pending" read is runVersion, and its first act is git checkout. In a directory that is not a repository, that fails before any push or API call.
  3. Bounded in-place fix, same defect class: the lane's pending count treated .changeset/pre/*.md as pending in every mode. In pre mode those changesets are consumed (the CLI's getRelevantChangesets), so they are now counted only on pre exit.
    • Measured on the post-version tree, which is the tree right after next.1 merges with nothing new: the old count reads 539 pending, the version pass moves 0 paths, and "Validate the post-version tree" goes red with changesets were pending but the version step moved no path. That would have been the first scheduled tick after next.1.
    • The same count fix is made in the Restore step. There the mode is read from the restored tree, because a pre exit version deletes pre.json.
    • All four in-place conditions hold: same class (a lane reader that does not know the v3 pre/ layout), a mechanical fix whose shape the CLI pins, no other claim on the file, and no new verification surface.

#6170: this lane still cannot publish

The pinned v2.1.2 src/index.ts branches on hasPublishScript = !!core.getInput("publish-script"):

  • With no changesets and no publish script, it returns.
  • With changesets, it runs runVersion, the PR path only.
  • runPublish needs a publish-script, and neither invocation passes one.

The rehearsal invocation also runs in a non-repository directory with no token. The publish job, the environment gate and the tag push are untouched. Measured: every replay below reports published=false.

Evidence

All runs were local, on scratch copies of main (an independent git init repo, its origin a scratch bare repo, the API a local mock). The real action bundles ran with the runner's INPUT_* and GITHUB_* contract. Nothing was dispatched and nothing reached GitHub.

run workflow result
S1 this PR, main (32 new changesets + 507 consumed) job green. Render pending_before=32; rehearsal has-changesets=false; restore 32 = 32; PR step: git push --force of chore: version packages (next) by github-actions[bot], then the GraphQL update of the existing PR (title chore: version packages (next), body 4319 chars, the same length as #21988's). Rehearsal API requests: 0.
S1 content (the pushed commit) 32 R100 renames .changeset/X.md to .changeset/pre/X.md (exactly the 32 new ones), 168 M (79 package.json, 79 CHANGELOG.md, 10 SKILL.md), @objectstack/spec 18.0.0-next.0 to 18.0.0-next.1. The 507 consumed changesets: 0 blob mismatches, 0 missing.
S2 base (main at 9c7cfbf0a9) reverse control. PR step fails with ENOENT on .changeset/pre/changes.json. Which of the two parallel reads rejects first varies; CI showed changes.md.
S4 this PR, rehearsal ref swapped to @v1 pin's reverse control. The rehearsal fails with ENOENT on .changeset/pre/changes.md, the PR step is skipped, and the standing PR keeps its last validated content. This is with setupGitUser: false. With v1's default it fails one call earlier, on git config in the non-repository directory. Red either way, before the cut.
S3 this PR, zero-pending tree (post-version commit as main) job green. pending_before=0, the rehearsal is skipped, and the PR step logs No changesets present ... publish-script is not set.
S3 base base, same tree the latent red. pending_before=539, then Validate fails with changesets were pending but the version step moved no path.
probe v2.1.2 rehearsal on the PRE-version copy has-changesets=true, then exit 1 at git checkout (not a git repository), 0 API requests.
exit v2.1.2 bundled reader vs the lane count, pre exit scratch tree both count 541 (34 + 507). pre mode, post-version: both 0. pre mode, pre-version: both 32.

Options weighed (the four axes)

  • (a) chosen: action v2.1.2 by commit, plus the read rehearsal.
    • Real need: it opens next.1, and the rehearsal pins the exact failure.
    • Long-term: this is the upstream-sanctioned pairing for CLI v3, not a workaround. The second reader stays, but it is pinned and rehearsed on every refresh.
    • AI-proofing: a CLI or action bump that desyncs the readers goes red before the cut, and the pin makes the reader a reviewed diff.
    • Scope: one job, no new workflow, job or check:* script.
  • (b) a repo-owned version-PR script on the repo's CLI (pnpm run version, commit, force-push, gh pr create|edit, PR body).
    • It has one reader by construction and no third-party code holding a write token.
    • It also replaces a maintained upstream component with new in-house PR machinery that only the live tick can fully verify. That is more surface on a p1 release lane.
    • Worth revisiting if the action and the CLI drift again.
  • (c) stay on v1 and keep .changeset/pre/ away from it. Ruled out by the card: no moving or deleting the consumed changesets, no pre.json edit, no skipped steps.

The 2026-08-19 hold on v2 (#9208)

The maintainer kept v1 then (「同意 9208」). The recommendation was that a safety pin must not be upgraded before the property it encodes is read in v2's source. Its stated reopen path was: read v2's source for the publish property and for the bundled reader. Both are read above at the pinned commit, and the bundle was run end to end on the next.0-shaped tree. The other premise, "no measured pull", no longer holds: v1 cannot read the layout the repo's CLI writes. This PR reverses that hold, so it needs the maintainer's word as well as the Tier H approval.

Verification

  • Derived gates (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack): 40 families, all run, all exit 0, at 838bc026b5 (the final commit). --ran reconciliation: 40 derived, 40 run, 0 NOT-MEASURED, 0 UNRUN. The four artifact-roster gates whose roster lives under .github/ were also run: all exit 0.
  • NOT MEASURED: the Validate step's content gates (create-objectstack suite, release-notes gates and the rest). Reason: unchanged by this diff, so the emulation stubbed that step. The live tick runs them.
  • NOT MEASURED: GitHub-side behaviour of v2's git-CLI push auth header and the PR create/update. Reason: only a live run measures it, and this lane is not dispatched while the PD15 question on tooling(pm): the fleet relay's workflow_dispatch can start release.yml with refresh_version_pr: true only, so a seat refreshes the Version PR through the relay (#23177 ruling B, item 5) #23193 is open. The local replay exercised the same code against a bare remote and a mock API.
  • NOT MEASURED: actionlint. Reason: not installed here. The YAML parses, and check:workflow-status-functions and check:workflow-step-name-quoting scanned it green.
  • Live check after merge: the next 6-hourly scheduled Release tick. Expected log lines: Rehearse the version-PR changeset read ... gives No changesets present ...; the assertion step gives has-changesets=false; and a chore: version packages (next) PR opens.

Acceptance notes (not filed)

  • Version-bump-only regressions surface only on the Version Packages PR — rehearse pnpm run version on main on a schedule and test the bumped tree #20562 landed nothing: it was closed not_planned, and the maintainer kept it closed on 2026-09-30. The scheduled rehearsal this pin extends is the version-pr job's own post-version render and validation.
  • objectui's changeset-release.yml runs changesets/action@v1 with @changesets/cli ^3.0.0. It would meet the same reader the first time a pre-mode version pass writes .changeset/pre/ there. objectui has no pre.json today, so this is dormant. Carrier: none.
  • The lane's README exclusion mirrors @changesets/read 0.x. Read 1.x also ignores AGENTS.md, CLAUDE.md and GEMINI.md under .changeset/. Dormant: no such file exists.

Changeset

None. This needs skip-changeset, and the label is left to the seat. The only changed path, .github/workflows/release.yml, belongs to no workspace package, so it is in no package's files[]. Positive control: packages/spec/src/index.ts resolves to @objectstack/spec (public), one of the 82 workspace packages (71 public).

维护者速读(草稿)

改了什么

只改了发版工作流里"维护 Version Packages PR"这一个定时任务:

  • 把它用的第三方 Action(changesets/action)从 v1 换成 v2.1.2,并按提交固定版本;
  • 新增一次只读预演:每次刷新时,先用同一个 Action 去读"版本提交之后"的目录,读不懂就当场变红;
  • 修正了"待发布 changeset"的计数:prerelease 模式下,.changeset/pre/ 里已被消费的 changeset 不再算作待发布。

为什么改

18.0.0-next.0 切版后,已消费的 507 个 changeset 被移进 .changeset/pre/。v1 自带的旧读取器把这个目录当成 v1 时代的旧格式去读,每 6 小时一次的刷新都报 ENOENT。结果是 next.1 的 Version Packages PR 开不出来,next.1 无法发版。已在本地用同一份 Action 代码复现,并验证 v2.1.2 能正确开出 PR:只消费 next.0 之后的 32 个新 changeset,507 个旧的一字未动。计数修正针对的是 next.1 合入后、尚无新 changeset 时必然出现的一次误报变红。

风险与代价(含回滚)

席位意见

你要做的

  1. 审阅并批准本 PR:受管面 Tier H,且推翻了 chore(ci)(deps): Bump changesets/action from 1 to 2 #9208 当时的保留决定,需要你的明确同意。
  2. 合入后无需任何操作:下一次 6 小时定时运行即是实测,应看到预演步骤输出 has-changesets=false,并开出 chore: version packages (next) PR。之后照常由你决定何时合并。

Generated by Claude Code

…with changesets/action v2.1.2, rehearses that read on the post-version tree, and stops counting consumed prerelease changesets as pending

changesets/action@v1 resolved to v1.9.0, whose bundled @changesets/read 0.6.7
takes .changeset/pre/ for a legacy v1 folder changeset and opens
.changeset/pre/changes.md: ENOENT on every refresh since the 18.0.0-next.0
cut moved 507 consumed changesets there.

- Pin the PR step to changesets/action v2.1.2 by commit (the major upstream
  pairs with @changesets/cli v3), with v2's input names; no publish-script,
  push-with-git-cli, create-github-releases and push-git-tags off.
- Run the same pinned reader on a copy of the post-version tree outside any
  git work tree, holding no credential, and require has-changesets false,
  so a reader that cannot read what the CLI writes fails before the cut.
- Count .changeset/pre/*.md as pending only on pre exit, the CLI's own rule;
  in pre mode a tree with no new changeset read as 539 pending.

Claude-Session: https://claude.ai/code/session_012zwsLMxQBwHUYJgPyrpEvx
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读 · PR #23313(#23282:Version PR 刷新 ENOENT,18.0.0-next.1 被卡住) · domain:devx seat 1 · 2026-10-13T18:22Z

改了什么(只有 .github/workflows/release.yml 的 version-pr job,受管 Tier H)

  1. 「Create or update the version packages PR」这一步:changesets/action@v1 换成 v2.1.2,按 commit 固定(ae32849d…;上游 tag v2.1.2^{} 已核对)。输入名按 v2 改写。
  2. 加一组「读取预演」:在 version 跑完后的树的一份副本上(不在任何 git 工作树里、不给凭证),用同一个固定版本的 action 读一遍 changeset,要求答「没有待发版的」。读取器和 CLI 再出现不一致时,在 Version PR 还开着的时候就红,不会等切版后才坏。
  3. 顺手修了同一类的潜在红:pre 模式下,.changeset/pre/*.md 不再算作待发版。否则 next.1 合入后第一次刷新会把 539 个已消费的算成待发版,然后 Validate 红。

为什么

  • next.0 的版本提交把 507 个已消费的 changeset 移进了 .changeset/pre/。v1 自带的旧读取器(@changesets/read 0.6.7)把这个目录当成旧格式的 changeset,去打开 pre/changes.md,于是每次刷新都 ENOENT。仓库自己的 CLI 是好的。
  • dev 拿真实的 action bundle 在本地端到端跑过:
    • v1 复现同一个 ENOENT;
    • v2.1.2 刷出 chore: version packages (next),只消费 32 个新 changeset,507 个原样不动,spec 升到 18.0.0-next.1。

为什么安全

需要你拍板(本 PR 撤销 2026-08-19 让 action 停在 v1 的决定,#9208「同意 9208」)

当时定下的重开条件是:读 v2 源码,确认发布属性和自带的读取器。这两项现已读完。选项:

  • A(席位推荐):接受 v2.1.2 commit 固定加读取预演,即本 PR 原样合入。v2 是上游与 CLI v3 配套的版本,不是绕过;读取器再不一致会在切版前红。
  • B:不用 changesets/action,改为仓库自己的脚本(pnpm run version、提交、强推、gh pr create/edit)。好处是只有一个读取器,第三方代码不碰写权限 token。代价是在 p1 发版路径上新写一套自有机制,大约一天,而且合入前只能 mock 验证。
  • C:保留 v1。不可行:v1 读不了 CLI 写出的布局,卡上又禁止移动或删除已消费的 changeset。

一个动作

  • 选 A:用 os-zhuang 或 hotlong 提交 Approve review。之后由席位翻 ready 并入队,或者你直接合并。合入后,下一个定时 tick(或你手动触发 refresh_version_pr)会开出 next.1 的 Version PR。
  • 选 B 或有别的意见:在本 PR 留言即可。

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

Labels

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

Projects

None yet

3 participants