Skip to content

tooling: publish computes each @objectstack/* floor from .objectstack-sha (objectui#12958) - #13024

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-12958-objectstack-floor-from-pin
Oct 19, 2026
Merged

os-zhuang merged 4 commits into
mainfrom
claude/issue-12958-objectstack-floor-from-pin

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #12958

Executes ruling B on objectui#12955 (comment 6248859734): 「下限也可以在那时按「哪个已发布版本包含这个提交」机械地算出来」. pnpm changeset:publish now starts with scripts/objectstack-floor.mjs, which computes the @objectstack/* floor from .objectstack-sha, writes it into every published manifest, and stops the publish when no published release contains the pin.

node scripts/objectstack-floor.mjs && node scripts/check-published-dist-tooling.mjs && node scripts/check-spec-range-floors.mjs && changeset publish

⛔ Stays draft: the diff touches .github/workflows/spec-range-floors.yml, a governed path (node scripts/check-governed-queue-guard.mjs --test answers GOVERNED, exit 3). It also came from a dispatch. It needs an APPROVED review from os-zhuang or hotlong before the claiming seat lands it.

The six "Done when" items

  1. Compute the floor. Done. The step reads the pin with objectstack-pin.mjs's readPin. It reads npm view @objectstack/spec versions --json once, then fetches objectstack's @objectstack/spec@* tags together with their history. For each npm version that has a tag, in ascending semver precedence, it runs git merge-base --is-ancestor PIN refs/tags/@objectstack/spec@VERSION. The first version that contains the pin is the floor.
    • Only versions on npm count. A tag that npm does not carry is skipped; the test's c5 pin covers this.
    • Prereleases count. 18.x exists on npm only as 18.0.0-next.N. Versions are ordered by semver precedence: numeric identifiers compare as numbers, and a release outranks its own prereleases. So a pin inside next.3 floors at next.3, even after 18.0.0 ships.
    • One version floors every package. objectstack versions its packages as one fixed group (.changeset/config.json there lists all seven that objectui reaches). The spec's version is therefore the floor for every @objectstack/* range. Before writing anything, the step checks that npm carries each moved name at that version.
  2. Write the floor. Done. The step reads every @objectstack/* range in all four dependency fields of every published manifest, the same population discoverPublishedPackages gives the other publish gates.
    • A range whose minimum is below the floor becomes ^FLOOR.
    • A range at or above the floor is left as written. It already admits no release older than the pin, and lowering it would widen what a consumer may install.
    • Every range is printed as moved or kept.
    • A range whose minimum the step cannot read (anything other than ^, ~, >=, = or a bare version) is refused with exit 1, and nothing is written.
  3. Stop when no release contains the pin. Done. The step exits 2. Its message names the pin and the newest published release. Because the script chains its steps with &&, changeset publish does not run. This is the floor computation having no answer, not a new gate, and no CI job is added.
  4. check-spec-range-floors.mjs: kept, and it now runs after the floor step.
    • The computed floor answers one question: which release contains the pin. It does not answer whether that release still exports every symbol the artifact uses. A release cut after the pin can have removed one.
    • Only the floor gate reads the floor release's exports, so retiring it would leave that case silent. Its header now says this.
    • The nightly Spec Range Floors follows the same decision. It runs the floor step between the build and the scan, so the scan judges the floors a publish would ship. Without the step, the nightly would judge main's hand-written floors, which nothing maintains any more.
    • On the step's exit 2 (no published release contains the pin yet), the nightly logs a notice and skips the scan. Under ruling B that is the normal state between a pin bump and objectstack's next release, and a nightly that stayed red through it would raise false alarms.
  5. Release notes. Done, by a documented equivalent, not a generated changeset.
    • A generated changeset cannot work on the publish lane. By the time it runs, changeset version has already rendered the CHANGELOG in the version PR, the lane deletes pending changesets before publishing, and changeset publish reads none.
    • Instead, for each package with a moved range, the step writes a ### Dependency floors entry under that package's ## VERSION section of CHANGELOG.md. The entry lists each moved range and names the pin.
    • Every package's files includes CHANGELOG.md, so the entry ships in the tarball. scripts/release-github-releases.mjs cuts the GitHub Release body from the same file, so the entry appears there too.
    • The boundaries are in Acceptance notes below.
  6. Test. Done. scripts/__tests__/objectstack-floor.test.ts runs real git over a throwaway objectstack-shaped history with annotated tags (c1 - c2 [next.0] - c3 - c4 [next.1] - c5 [next.2, tagged, not on npm] - c6) and a recorded npm list.
    • Floor cases: pin inside a release (c3 → next.1); pin exactly a release commit (c4 → next.1, not next.0); pin newer than every published release (c5 and c6 → no floor).
    • End to end: writes the manifest and the CHANGELOG entry and keeps a range above the floor; exit 2 writes nothing; an unreadable range exits 1 and writes nothing; --dry-run writes nothing.
    • Wiring pins: the step is first in changeset:publish, and the nightly runs it between the build and the scan.

Where the tags come from at publish (dispatch hypothesis 3)

The release job's objectui checkout has no objectstack history, so the step brings its own.

  • It runs an anonymous treeless fetch (git fetch --filter=tree:0 --no-tags) of +refs/tags/@objectstack/spec@* from objectstack's public repository into a temporary bare repository, then writes a commit-graph.
  • The release job already reaches that repository anonymously: its Build @objectstack at the pin step fetches the pin with objectstack-pin.mjs. It already reaches npm as well. So the route needs no new secret and no new step in changeset-release.yml.
  • OBJECTSTACK_REPO_URL overrides the URL, as it does for objectstack-pin.mjs.
  • The fetch has no depth limit, so the repository is not shallow and an --is-ancestor exit 1 is a real non-ancestor.

Measured (dispatch hypotheses 1 and 2)

  • Hypothesis 1 holds at 4016370af3: changeset:publish had the two-gate form shown in the card, .objectstack-sha = 2c9bf9430a1198bfaa5c0de9ff4a811838c6869c, and spec-range-floors.yml exists.
  • Tag spelling: @objectstack/spec@VERSION, annotated. git ls-remote --tags lists a peeled ^{} entry for each tag; 171 spec tags. The peeled commit of @objectstack/spec@18.0.0-next.3 is exactly the current pin.
  • npm: 173 versions, prereleases included; the next dist-tag is 18.0.0-next.3.
  • The fetch, measured once in this container: about 2 s, 37 MB, --is-shallow-repository false. Control leg: merge-base --is-ancestor @objectstack/spec@17.0.0 @objectstack/spec@18.0.0-next.2 exits 0.
  • Without a commit-graph, one --is-ancestor per tag walked the whole history: the real dry run took 89 s. With the graph it took 7.4 s.
  • Dry run against this branch's tree: floor 18.0.0-next.3; 35 ranges moved, 2 kept (the @objectstack/spec ranges of app-shell and plugin-detail, already at next.3); exit 0.
  • Write mode was run once on a scratch copy of every manifest and CHANGELOG. The only manifest bytes that changed were the moved ranges, and the CHANGELOG entry landed under ## 17.7.0. This was not a publish.

Gates (local; final run cited with its HEAD below)

  • pnpm exec vitest run scripts/__tests__ (repo root, HEAD 27b5fc4, clean tree): Test Files 189 passed | 2 skipped (191), Tests 5661 passed | 2 skipped (5663), exit 0. On bd57569 the same run had 1 failure: markdown-test-inputs.test.ts flagged the new test's fixture CHANGELOG.md as unadjudicated. 27b5fc4 adds the ledger entry.
  • pnpm type-check:scripts (tsc -p tsconfig.scripts.json, the project that compiles scripts/__tests__): exit 0, same run.
  • pnpm type-check (turbo over the workspace packages) was not run locally. It does not reach scripts/ (see the header of tsconfig.scripts.json), and this diff changes no workspace package. CI runs it.
  • pnpm exec eslint --no-inline-config on the two new files: exit 0.
  • Exit 0 from pnpm check:new-line-citations (0 new), pnpm check:control-bytes, pnpm check:test-path-roots, pnpm check:pre-install-import-graph, and node scripts/check-changeset-presence.mjs (no changeset owed: no released package's source or published contract changed).
  • The doc-parity pins in ci-cd-pipeline-doc.test.ts gained one declaration: the new publish leg, reached through the action's publish: input the same way as the other legs.

Acceptance notes

  • The tagged commit and main keep the hand-written ranges. The publish lane commits nothing, so main's CHANGELOG.md and the tagged tree never carry the moved ranges or the Dependency floors entry. The tarball and the GitHub Release do carry them.
  • A later release does not repeat the entry. Its CHANGELOG is rendered from main's, so its tarball's copy will not include the earlier release's entry.
  • main's manifest ranges will keep drifting below the computed floor, by design under ruling B (today 35 of 37). Nothing reads them as a claim any more except the next publish, which rewrites them.
  • The nightly still builds every published package on nights the scan skips. Moving the floor step before the build would save that build. The step was placed after the build to mirror the publish order, and the approver may prefer otherwise.

Line count

git diff --numstat against 4016370af3: 353 added, 9 deleted. scripts/objectstack-floor.mjs 181 and its test 119 come to 300, over the dispatch's ~250 for the step and its test. The rest: the nightly step 18, the doc section 12+3, its parity declarations 11+5, the gate-header note 5, the ledger entry 6, and changeset:publish 1+1.

维护者速读(草稿)

  • 改了什么: objectui 发版(pnpm changeset:publish)的第一步现在会做三件事。第一,按 .objectstack-sha 找出第一个包含这个提交、并且已经发布到 npm 的 objectstack 版本。第二,把所有发布包里低于这个版本的 @objectstack/* 版本下限提到它,并在该包这一版的 CHANGELOG 里写明提升了什么。第三,如果还没有这样的版本,就停止发版。夜检 Spec Range Floors 也会先跑这一步再检查;遇到「还没有版本包含这个提交」时跳过检查,不变红。
  • 为什么改: 这是执行 12955 的裁决 B。下限从此不再靠人手写,不会再出现 12943、12946 那样漏提的情况。
  • 风险与代价(含回滚):
    • 每次发版多读一次 objectstack 公开仓库和 npm,实测约 7 秒,不需要新的密钥。
    • pin 领先于所有已发布版本时,发版会停下。这是有意的设计:要等 objectstack 发出包含该提交的版本。
    • 提升记录只出现在 npm 包内的 CHANGELOG 和 GitHub Release 里,main 上的 CHANGELOG 和 tag 指向的提交不记录。
    • 回滚:删掉 changeset:publish 的第一段,并撤回 workflow 里新增的那一步。
  • 席位意见:
  • 你要做的: 本 PR 改了 .github/workflows/spec-range-floors.yml,属于受管面。需要 os-zhuang 或 hotlong 批准,之后由认领席落地。

The implementing session is https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp.


Generated by Claude Code

…tstack-sha (objectui#12958)

`scripts/objectstack-floor.mjs` runs first in `pnpm changeset:publish`. It
finds the earliest @objectstack/spec version npm carries whose objectstack
release tag contains the pinned commit, raises every published manifest's
@objectstack/* range below it to `^<floor>` (a range at or above it stays),
records each move under the package's CHANGELOG.md version section, and
exits 2 -- so `changeset publish` does not run -- when no published release
contains the pin. Tags and history come from an anonymous treeless fetch of
objectstack's public repository: no new secret.

check-spec-range-floors stays after it: a release that contains the pin can
still have removed a symbol the artifact uses. The nightly Spec Range Floors
workflow runs the floor step before its scan and skips the scan on exit 2.

Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp
Co-authored-by: Claude <noreply@anthropic.com>
…ry checks (objectui#12958)

One `merge-base --is-ancestor` per spec release tag walked the whole
objectstack history each time without it. Tighten the step's header and
its test.

Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp
Co-authored-by: Claude <noreply@anthropic.com>
… ledger (objectui#12958)

Its CHANGELOG.md is a fixture written into a temp directory.

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

Copy link
Copy Markdown
Contributor Author

维护者速读

改了什么:objectui 发版(pnpm changeset:publish)的第一步新增 scripts/objectstack-floor.mjs。它按 .objectstack-sha 找出第一个包含该提交、并已发布到 npm 的 objectstack 版本,把所有发布包里低于它的 @objectstack/* 下限提到这个版本,并在该包这一版的 CHANGELOG 里写明;如果还没有这样的版本,就停止发版。夜检 Spec Range Floors 也先跑这一步。

为什么改:执行 12955 的裁决 B(「下限也可以在那时按「哪个已发布版本包含这个提交」机械地算出来」)。下限不再手写,不会再出现 12943、12946 那样漏提的情况;pin 指向尚未发布的 objectstack 提交时,也不会发出依赖着不存在版本的包。

风险与代价(含回滚):每次发版多读一次 objectstack 公开仓库和 npm,实测约 7 秒,不需要新密钥。pin 领先于所有已发布版本时发版会停下——这是裁决要的行为,要等 objectstack 发出包含该提交的版本。提升记录只写进 npm 包里的 CHANGELOG 和 GitHub Release,不提交回 main。回滚:删掉 changeset:publish 的第一段,并撤回 workflow 里新增的那一步。

席位意见:建议批准。席位逐段读过脚本:版本比较按 semver 先后(正式版排在其预发布之后),下限取"npm 上有、tag 包含 pin"的最早版本,只往上提不往下改,读不懂的范围直接拒绝且不写任何文件。用真实数据试跑:下限 18.0.0-next.3,35 个范围上提、2 个保持。

你要做的:给本 PR 一次批准(APPROVED review)。

席位核对

ACCEPT · objectui domain:devx seat 1 · os-warren · session_01FM29ZqLTrnPNXxijioZwMp · 2026-10-19T06:08Z · head 27b5fc4eb · card objectui#12958 (claim 6259982847, ruling-ref 6248859734)

  • Form. Draft, base main, first line Fixes #12958 (the only closing keyword). Assignee os-warren. No changeset: check-changeset-presence answers none is owed (root scripts, docs, a workflow, the private root manifest).
  • Scope. 8 files, +353 / −9: the new step and its test, the changeset:publish wiring, check-spec-range-floors.mjs (kept, with the reason stated), the nightly spec-range-floors.yml, the doc-parity declaration and the markdown-inputs ledger entry the existing gates forced. Over the dispatch's ~250-line budget by the forced doc and ledger rows; accepted.
  • The six "Done when" items, read in the diff. ① floor = earliest npm @objectstack/spec whose @objectstack/spec@V tag contains the pin, by merge-base --is-ancestor over a full-depth treeless fetch (a non-ancestor is real); ② every @objectstack/* range below the floor → ^floor in all dependency fields, at-or-above kept, each move printed; ③ exit 2 and nothing written when no release contains the pin; ④ the floor gate kept after the step; ⑤ a ### Dependency floors entry under the package's version section of the shipped CHANGELOG; ⑥ the three pin cases tested over annotated fixture tags with a recorded npm list.
  • Gates, pinned to head 27b5fc4eb. 33 check-runs, all completed: 30 success, 3 skipped (Test (coverage), its unexpanded shard matrix, dependabot). Lint, Type Check, Build & E2E, Repo Gates, Changeset Declaration and Governed Surface Queue Guard success. The dev's local reading: pnpm exec vitest run scripts/__tests__ 189 files / 5661 tests passed, pnpm type-check:scripts exit 0.
  • Path fork. .github/workflows/spec-range-floors.yml is governed in objectui: Tier H. This record, needs-user-decision on the PR, review requested from os-zhuang and hotlong. ⛔ Not readied or queued until an authorised approval.

Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review October 19, 2026 06:13
@os-zhuang
os-zhuang enabled auto-merge October 19, 2026 06:13
@os-zhuang
os-zhuang added this pull request to the merge queue Oct 19, 2026
Merged via the queue into main with commit 4484a11 Oct 19, 2026
33 of 34 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-12958-objectstack-floor-from-pin branch October 19, 2026 06:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd configuration dependencies documentation Improvements or additions to documentation tests

Projects

None yet

3 participants