Skip to content

Commit c319420

Browse files
os-zhuangclaude
andauthored
fix(release): the pending release is 17.2.0, not 18.0.0 (#3237)
Four changesets scored `major` — 17 package entries across objectui#3159, objectui#3160, objectui#3161 and objectui#3225. Every publishable package sits in one `fixed` group (39 packages), and changesets applies the group's highest bump to all of it, so those entries staged the whole family as `18.0.0` on the open release PR (#3133) while `@objectstack/spec` is still `^17.0.0-rc.1`. objectui's major is pinned to the `@objectstack` major so that "same major ⇒ compatible" holds across the two repos; our own breaking changes ship as `minor` with the break spelled out in the changeset body (AGENTS.md §版本号策略). All four are rescored `minor` and each now carries the note saying why — nothing about the described breaks changes, only the number they would publish under. The rule was already written and nothing executed it, and no workflow even looked at a changeset: `ci.yml` and `lint.yml` both list `'**/*.md'` and `.changeset/**` under `paths-ignore`, so a PR adding only a changeset starts nothing. So it is mechanical now, in two places: - `scripts/check-changeset-no-major.mjs` (dependency-free — the CI job is a checkout plus one `node` call) exits non-zero on any `major` in changeset frontmatter, and prints the file:line and the fix. The release that follows objectstack across ITS major sets `OBJECTUI_ALLOW_MAJOR=1`. - `.github/workflows/changeset-guard.yml` runs it on the inverse trigger of every other workflow: only when `.changeset/**` changes. - `pnpm test` asserts the same repository state, so the rule survives that workflow being skipped or removed. Claude-Session: https://claude.ai/code/session_01R8TU2mGotfDRNhg7WP5esH Co-authored-by: Claude <noreply@anthropic.com>
1 parent 785b8a5 commit c319420

10 files changed

Lines changed: 378 additions & 18 deletions

.changeset/auth-components-react-spec-symbol-burn-down.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
2-
"@object-ui/auth": major
3-
"@object-ui/components": major
4-
"@object-ui/react": major
2+
"@object-ui/auth": minor
3+
"@object-ui/components": minor
4+
"@object-ui/react": minor
55
---
66

77
Stop declaring 18 `@object-ui/auth` / `@object-ui/components` / `@object-ui/react`
@@ -55,3 +55,8 @@ output type — every `.default()`ed key required — would reject outright.
5555

5656
`@objectstack/spec` moves from `devDependencies` to `dependencies` in
5757
`@object-ui/components`: its public type surface now references the spec.
58+
59+
Scored `minor`, not `major`, per this repo's fixed-group rule — objectui's major
60+
tracks `@objectstack`, so breaking changes of our own ship as minor with the
61+
semantics spelled out above (see AGENTS.md §版本号策略). A `major` here would carry
62+
all 39 packages of the fixed group to `18.0.0` and off objectstack's 17.x line.

.changeset/data-objectstack-chatbot-list-spec-symbol-burn-down.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
2-
"@object-ui/data-objectstack": major
3-
"@object-ui/plugin-chatbot": major
4-
"@object-ui/plugin-list": major
2+
"@object-ui/data-objectstack": minor
3+
"@object-ui/plugin-chatbot": minor
4+
"@object-ui/plugin-list": minor
55
---
66

77
Stop declaring 12 `@object-ui/data-objectstack` / `@object-ui/plugin-chatbot` /
@@ -58,3 +58,8 @@ vendored Vercel AI Elements / Shadcn primitives — upstream's component API, no
5858
objectui's authored surface — so the guard now skips that directory the same way
5959
it already skips `components/src/ui/`, with a test that fails if any file there
6060
stops carrying its vendor banner.
61+
62+
Scored `minor`, not `major`, per this repo's fixed-group rule — objectui's major
63+
tracks `@objectstack`, so breaking changes of our own ship as minor with the
64+
semantics spelled out above (see AGENTS.md §版本号策略). A `major` here would carry
65+
all 39 packages of the fixed group to `18.0.0` and off objectstack's 17.x line.

.changeset/long-tail-spec-symbol-burn-down.md

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
---
2-
"@object-ui/collaboration": major
3-
"@object-ui/fields": major
4-
"@object-ui/layout": major
5-
"@object-ui/plugin-charts": major
6-
"@object-ui/plugin-detail": major
7-
"@object-ui/plugin-form": major
8-
"@object-ui/plugin-grid": major
9-
"@object-ui/providers": major
10-
"@object-ui/runner": major
11-
"@object-ui/sdui-parser": major
2+
"@object-ui/collaboration": minor
3+
"@object-ui/fields": minor
4+
"@object-ui/layout": minor
5+
"@object-ui/plugin-charts": minor
6+
"@object-ui/plugin-detail": minor
7+
"@object-ui/plugin-form": minor
8+
"@object-ui/plugin-grid": minor
9+
"@object-ui/providers": minor
10+
"@object-ui/runner": minor
11+
"@object-ui/sdui-parser": minor
1212
---
1313

1414
Stop declaring 14 symbols across ten packages under names `@objectstack/spec`
@@ -67,3 +67,8 @@ batch 3, so one concept does not acquire two dialect names one package apart.
6767
`@objectstack/spec` moves from `devDependencies` to `dependencies` in
6868
`@object-ui/fields` (it re-exports a runtime function) and `@object-ui/providers`
6969
(its public `.d.ts` now references the spec).
70+
71+
Scored `minor`, not `major`, per this repo's fixed-group rule — objectui's major
72+
tracks `@objectstack`, so breaking changes of our own ship as minor with the
73+
semantics spelled out above (see AGENTS.md §版本号策略). A `major` here would carry
74+
all 39 packages of the fixed group to `18.0.0` and off objectstack's 17.x line.

.changeset/remove-dead-layout-page-node-renderer.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
"@object-ui/layout": major
2+
"@object-ui/layout": minor
33
---
44

55
Remove `PageNodeRenderer`, the dead page-node renderer (objectui#3223, ADR-0049
@@ -33,3 +33,8 @@ Also note: this supersedes the `Page` → `PageNodeRenderer` rename shipped for
3333
this package in the batch 7 symbol burn-down — the renamed symbol is gone rather
3434
than renamed again. `PageHeaderProps``PageHeaderComponentProps` from that same
3535
batch is unaffected.
36+
37+
Scored `minor`, not `major`, per this repo's fixed-group rule — objectui's major
38+
tracks `@objectstack`, so breaking changes of our own ship as minor with the
39+
semantics spelled out above (see AGENTS.md §版本号策略). A `major` here would carry
40+
all 39 packages of the fixed group to `18.0.0` and off objectstack's 17.x line.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Changeset Guard
2+
3+
# Why this is its own workflow instead of a job in `ci.yml`: a changeset is a
4+
# markdown file under `.changeset/`, and both `ci.yml` and `lint.yml` list
5+
# `'**/*.md'` AND `.changeset/**` under `paths-ignore`. A PR that adds only a
6+
# changeset therefore starts no workflow at all — which is exactly the PR this
7+
# check needs to see. GitHub has no per-job path filter, so the gate lives here,
8+
# with the inverse trigger: it runs *only* when `.changeset/**` changes.
9+
#
10+
# It needs no install and no build — a checkout plus one `node` call, a few
11+
# seconds — so keep it that way if you add checks to it.
12+
13+
on:
14+
pull_request:
15+
branches: [main, develop]
16+
paths:
17+
- '.changeset/**'
18+
push:
19+
branches: [main]
20+
paths:
21+
- '.changeset/**'
22+
23+
concurrency:
24+
group: changeset-guard-${{ github.event.pull_request.number || github.ref }}
25+
cancel-in-progress: true
26+
27+
permissions:
28+
contents: read
29+
30+
jobs:
31+
no-major:
32+
name: Changeset Bump Policy
33+
runs-on: ubuntu-latest
34+
timeout-minutes: 5
35+
36+
steps:
37+
- name: Checkout code
38+
uses: actions/checkout@v7
39+
40+
# One `major` in the 39-package `fixed` group publishes all 39 as the next
41+
# major, taking objectui off the `@objectstack` major it is pinned to.
42+
# See AGENTS.md §版本号策略, and the script's header for the full rationale.
43+
- name: Verify no changeset declares a major bump
44+
run: node scripts/check-changeset-no-major.mjs

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ export const SchemaRenderer = ({ schema }: { schema: UIComponent }) => {
168168
- minor/patch **独立演进**——objectstack 没动时不必跟发;objectui 自己的改动照常用 changeset 推进(从当前 major 起步,如 `11.0.0 → 11.1.0`)。
169169
- objectstack 跨 major(→12)时,下一次 objectui 发版一并把 major 提到 `12`
170170
- 推论:**changeset 里不要声明 `major`** —— fixed 组任一 `major` 都会把全组推上去、脱离 objectstack 的节奏(如 17.x 期间被推到 18)。objectui 自身的破坏性变更也标 `minor`(在正文里写清 breaking 语义即可);唯一例外是跟随 objectstack 跨 major 的那一次同步升级。
171+
- **这一条现在由 CI 机械强制** —— `scripts/check-changeset-no-major.mjs` 在任一 changeset 声明 `major` 时退出非零,由 `.github/workflows/changeset-guard.yml` 跑(它是唯一以 `.changeset/**`**触发**路径的 workflow:`ci.yml`/`lint.yml` 都把 `**/*.md``.changeset/**` 列进 `paths-ignore`,只加 changeset 的 PR 不会启动任何 workflow),`pnpm test` 里另有一条仓库状态断言兜底。跟随 objectstack 跨 major 的那一次发版设 `OBJECTUI_ALLOW_MAJOR=1` 放行。前情:objectui#3161/#3159/#3160/#3225 四个 changeset 在 17.x 期间标了 `major`(17 个包条目),足以把 39 个包发成 `18.0.0`
171172
- 这是约定优先于 semver 纯粹性的取舍(为可维护/好记),因此 objectui 的 major 不代表「它自身 API 的破坏性变更次数」。`@object-ui/site``@object-ui/example-*``ignore` 列表,不随组联动。
172173

173174
### 多 agent 协作纪律(并行修改本仓库,务必遵守)

content/docs/guide/ci-cd-pipeline.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,22 @@ Uses [Changesets](https://github.com/changesets/changesets) for automated versio
152152
3. Publishes to npm.
153153
4. Configures a pnpm-lock.yaml merge driver to prevent lock file conflicts.
154154

155+
### Changeset Guard (`changeset-guard.yml`)
156+
157+
**Trigger:** PR to `main`/`develop`, and push to `main`, **when `.changeset/**` changes** — the
158+
inverse of every other workflow's filter. `ci.yml` and `lint.yml` both list `'**/*.md'` and
159+
`.changeset/**` under `paths-ignore`, so a PR that adds only a changeset starts nothing else.
160+
161+
Runs `scripts/check-changeset-no-major.mjs`, which fails if any pending changeset declares a
162+
`major` bump. Every publishable package is in one `fixed` group (39 packages), so a single
163+
`major` publishes all of them as the next major — and objectui's major is pinned to the
164+
`@objectstack` major it is compatible with, not to its own count of breaking changes. Score
165+
breaking changes of our own as `minor` and describe the break in the changeset body.
166+
167+
The one release that legitimately bumps the major is the one following `@objectstack` across
168+
its major; it sets `OBJECTUI_ALLOW_MAJOR=1`. `pnpm test` asserts the same repository state, so
169+
the rule survives this workflow being skipped.
170+
155171
### Changelog Generation (`changelog.yml`)
156172

157173
**Trigger:** `release` event (when a GitHub Release is published), or manual dispatch.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
"build:console": "pnpm --filter @object-ui/console build",
5252
"shadcn:diff": "node scripts/shadcn-sync.js --diff",
5353
"shadcn:list": "node scripts/shadcn-sync.js --list",
54-
"changeset:check": "node scripts/check-changeset-fixed.mjs",
54+
"changeset:check": "node scripts/check-changeset-fixed.mjs && node scripts/check-changeset-no-major.mjs",
5555
"changeset": "changeset",
5656
"changeset:version": "changeset version",
5757
"changeset:publish": "changeset publish",
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { describe, expect, it } from 'vitest';
2+
import fs from 'node:fs';
3+
import path from 'node:path';
4+
import { fileURLToPath } from 'node:url';
5+
6+
// @ts-expect-error — plain-JS CI helper, intentionally untyped
7+
import { findMajorBumps, parseFrontmatterBumps } from '../check-changeset-no-major.mjs';
8+
9+
/**
10+
* objectui's major is not a count of its own breaking changes — it is pinned to
11+
* the `@objectstack` major so that "same major ⇒ compatible" holds across the
12+
* two repos (AGENTS.md §版本号策略). Since every publishable package sits in one
13+
* `fixed` group, ONE `major` in ONE changeset publishes all 39 of them as the
14+
* next major and breaks that pin.
15+
*
16+
* Four pending changesets had scored `major` (17 package entries) during the
17+
* 17.x line, which would have shipped 39 packages as 18.0.0 against an
18+
* `@objectstack` still on 17. The rule was written down and nothing ran it:
19+
* `ci.yml` and `lint.yml` both `paths-ignore` `.changeset/**`, so a
20+
* changeset-only PR started no workflow at all.
21+
*
22+
* These tests are the second lock. `.github/workflows/changeset-guard.yml`
23+
* catches it on the PR that adds the changeset; the repo-state test below
24+
* catches it in `pnpm test` even if that workflow is ever removed or its
25+
* trigger stops matching.
26+
*/
27+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
28+
const changesetDir = path.join(repoRoot, '.changeset');
29+
const workflowDir = path.join(repoRoot, '.github/workflows');
30+
31+
describe('parseFrontmatterBumps', () => {
32+
it('reads the package/bump pairs out of the frontmatter block', () => {
33+
const source = [
34+
'---',
35+
'"@object-ui/layout": minor',
36+
"'@object-ui/fields': patch",
37+
'@object-ui/core: minor',
38+
'---',
39+
'',
40+
'Body text.',
41+
].join('\n');
42+
43+
expect(parseFrontmatterBumps(source)).toEqual([
44+
{ pkg: '@object-ui/layout', bump: 'minor', line: 2 },
45+
{ pkg: '@object-ui/fields', bump: 'patch', line: 3 },
46+
{ pkg: '@object-ui/core', bump: 'minor', line: 4 },
47+
]);
48+
});
49+
50+
it('stops at the closing `---`, so prose about a bump is not a bump', () => {
51+
// Changeset bodies in this repo discuss the major/minor call explicitly —
52+
// and a body may hold a fenced YAML example. Neither may register as a
53+
// declaration, or the guard would fail on a changeset that is obeying it.
54+
const source = [
55+
'---',
56+
'"@object-ui/layout": minor',
57+
'---',
58+
'',
59+
'Scored `minor`, not `major`, per the fixed-group rule.',
60+
'',
61+
'```yaml',
62+
'"@object-ui/layout": major',
63+
'```',
64+
].join('\n');
65+
66+
expect(parseFrontmatterBumps(source)).toEqual([
67+
{ pkg: '@object-ui/layout', bump: 'minor', line: 2 },
68+
]);
69+
expect(findMajorBumps([{ file: 'x.md', source }])).toEqual([]);
70+
});
71+
72+
it('returns nothing for a file with no frontmatter', () => {
73+
expect(parseFrontmatterBumps('# Just a readme\n')).toEqual([]);
74+
});
75+
});
76+
77+
describe('findMajorBumps', () => {
78+
it('reports every major entry with a file:line to jump to', () => {
79+
const source = ['---', '"@object-ui/auth": major', '"@object-ui/react": minor', '---'].join(
80+
'\n'
81+
);
82+
83+
expect(findMajorBumps([{ file: '.changeset/a.md', source }])).toEqual([
84+
{ file: '.changeset/a.md', pkg: '@object-ui/auth', line: 2 },
85+
]);
86+
});
87+
});
88+
89+
describe('the repository itself', () => {
90+
it('has no pending changeset declaring a `major` bump', () => {
91+
const changesets = fs
92+
.readdirSync(changesetDir)
93+
.filter((name) => name.endsWith('.md') && name !== 'README.md')
94+
.map((name) => ({
95+
file: path.join('.changeset', name),
96+
source: fs.readFileSync(path.join(changesetDir, name), 'utf8'),
97+
}));
98+
99+
const offenders = findMajorBumps(changesets) as { file: string; pkg: string; line: number }[];
100+
101+
expect(
102+
offenders.map(({ file, pkg, line }) => `${pkg} at ${file}:${line}`),
103+
'A `major` publishes all 39 fixed-group packages as the next major, off the @objectstack ' +
104+
'major objectui is pinned to. Score it `minor` and describe the break in the body — ' +
105+
'see AGENTS.md §版本号策略.'
106+
).toEqual([]);
107+
});
108+
});
109+
110+
describe('changeset-guard.yml', () => {
111+
const guard = fs.readFileSync(path.join(workflowDir, 'changeset-guard.yml'), 'utf8');
112+
113+
it('runs the guard script', () => {
114+
expect(guard).toContain('node scripts/check-changeset-no-major.mjs');
115+
});
116+
117+
it('triggers on `.changeset/**` — the paths every other workflow ignores', () => {
118+
// The inverse pin. `ci.yml` cannot host this check: a changeset-only PR
119+
// matches its `paths-ignore` twice over (`**/*.md` and `.changeset/**`), so
120+
// no job in it would ever run. If that ever stops being true, this test
121+
// fails and the separate workflow can be folded back in.
122+
expect(guard).toMatch(/paths:\s*\n\s*- '\.changeset\/\*\*'/);
123+
124+
const ci = fs.readFileSync(path.join(workflowDir, 'ci.yml'), 'utf8');
125+
expect(ci).toContain("paths-ignore:");
126+
expect(ci).toContain("- '**/*.md'");
127+
});
128+
});

0 commit comments

Comments
 (0)