Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions .changeset/auth-components-react-spec-symbol-burn-down.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
"@object-ui/auth": major
"@object-ui/components": major
"@object-ui/react": major
"@object-ui/auth": minor
"@object-ui/components": minor
"@object-ui/react": minor
---

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

`@objectstack/spec` moves from `devDependencies` to `dependencies` in
`@object-ui/components`: its public type surface now references the spec.

Scored `minor`, not `major`, per this repo's fixed-group rule — objectui's major
tracks `@objectstack`, so breaking changes of our own ship as minor with the
semantics spelled out above (see AGENTS.md §版本号策略). A `major` here would carry
all 39 packages of the fixed group to `18.0.0` and off objectstack's 17.x line.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
"@object-ui/data-objectstack": major
"@object-ui/plugin-chatbot": major
"@object-ui/plugin-list": major
"@object-ui/data-objectstack": minor
"@object-ui/plugin-chatbot": minor
"@object-ui/plugin-list": minor
---

Stop declaring 12 `@object-ui/data-objectstack` / `@object-ui/plugin-chatbot` /
Expand Down Expand Up @@ -58,3 +58,8 @@ vendored Vercel AI Elements / Shadcn primitives — upstream's component API, no
objectui's authored surface — so the guard now skips that directory the same way
it already skips `components/src/ui/`, with a test that fails if any file there
stops carrying its vendor banner.

Scored `minor`, not `major`, per this repo's fixed-group rule — objectui's major
tracks `@objectstack`, so breaking changes of our own ship as minor with the
semantics spelled out above (see AGENTS.md §版本号策略). A `major` here would carry
all 39 packages of the fixed group to `18.0.0` and off objectstack's 17.x line.
25 changes: 15 additions & 10 deletions .changeset/long-tail-spec-symbol-burn-down.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
---
"@object-ui/collaboration": major
"@object-ui/fields": major
"@object-ui/layout": major
"@object-ui/plugin-charts": major
"@object-ui/plugin-detail": major
"@object-ui/plugin-form": major
"@object-ui/plugin-grid": major
"@object-ui/providers": major
"@object-ui/runner": major
"@object-ui/sdui-parser": major
"@object-ui/collaboration": minor
"@object-ui/fields": minor
"@object-ui/layout": minor
"@object-ui/plugin-charts": minor
"@object-ui/plugin-detail": minor
"@object-ui/plugin-form": minor
"@object-ui/plugin-grid": minor
"@object-ui/providers": minor
"@object-ui/runner": minor
"@object-ui/sdui-parser": minor
---

Stop declaring 14 symbols across ten packages under names `@objectstack/spec`
Expand Down Expand Up @@ -67,3 +67,8 @@ batch 3, so one concept does not acquire two dialect names one package apart.
`@objectstack/spec` moves from `devDependencies` to `dependencies` in
`@object-ui/fields` (it re-exports a runtime function) and `@object-ui/providers`
(its public `.d.ts` now references the spec).

Scored `minor`, not `major`, per this repo's fixed-group rule — objectui's major
tracks `@objectstack`, so breaking changes of our own ship as minor with the
semantics spelled out above (see AGENTS.md §版本号策略). A `major` here would carry
all 39 packages of the fixed group to `18.0.0` and off objectstack's 17.x line.
7 changes: 6 additions & 1 deletion .changeset/remove-dead-layout-page-node-renderer.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
"@object-ui/layout": major
"@object-ui/layout": minor
---

Remove `PageNodeRenderer`, the dead page-node renderer (objectui#3223, ADR-0049
Expand Down Expand Up @@ -33,3 +33,8 @@ Also note: this supersedes the `Page` → `PageNodeRenderer` rename shipped for
this package in the batch 7 symbol burn-down — the renamed symbol is gone rather
than renamed again. `PageHeaderProps` → `PageHeaderComponentProps` from that same
batch is unaffected.

Scored `minor`, not `major`, per this repo's fixed-group rule — objectui's major
tracks `@objectstack`, so breaking changes of our own ship as minor with the
semantics spelled out above (see AGENTS.md §版本号策略). A `major` here would carry
all 39 packages of the fixed group to `18.0.0` and off objectstack's 17.x line.
44 changes: 44 additions & 0 deletions .github/workflows/changeset-guard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Changeset Guard

# Why this is its own workflow instead of a job in `ci.yml`: a changeset is a
# markdown file under `.changeset/`, and both `ci.yml` and `lint.yml` list
# `'**/*.md'` AND `.changeset/**` under `paths-ignore`. A PR that adds only a
# changeset therefore starts no workflow at all — which is exactly the PR this
# check needs to see. GitHub has no per-job path filter, so the gate lives here,
# with the inverse trigger: it runs *only* when `.changeset/**` changes.
#
# It needs no install and no build — a checkout plus one `node` call, a few
# seconds — so keep it that way if you add checks to it.

on:
pull_request:
branches: [main, develop]
paths:
- '.changeset/**'
push:
branches: [main]
paths:
- '.changeset/**'

concurrency:
group: changeset-guard-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
no-major:
name: Changeset Bump Policy
runs-on: ubuntu-latest
timeout-minutes: 5

steps:
- name: Checkout code
uses: actions/checkout@v7

# One `major` in the 39-package `fixed` group publishes all 39 as the next
# major, taking objectui off the `@objectstack` major it is pinned to.
# See AGENTS.md §版本号策略, and the script's header for the full rationale.
- name: Verify no changeset declares a major bump
run: node scripts/check-changeset-no-major.mjs
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ export const SchemaRenderer = ({ schema }: { schema: UIComponent }) => {
- minor/patch **独立演进**——objectstack 没动时不必跟发;objectui 自己的改动照常用 changeset 推进(从当前 major 起步,如 `11.0.0 → 11.1.0`)。
- objectstack 跨 major(→12)时,下一次 objectui 发版一并把 major 提到 `12`。
- 推论:**changeset 里不要声明 `major`** —— fixed 组任一 `major` 都会把全组推上去、脱离 objectstack 的节奏(如 17.x 期间被推到 18)。objectui 自身的破坏性变更也标 `minor`(在正文里写清 breaking 语义即可);唯一例外是跟随 objectstack 跨 major 的那一次同步升级。
- **这一条现在由 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`。
- 这是约定优先于 semver 纯粹性的取舍(为可维护/好记),因此 objectui 的 major 不代表「它自身 API 的破坏性变更次数」。`@object-ui/site` 与 `@object-ui/example-*` 在 `ignore` 列表,不随组联动。

### 多 agent 协作纪律(并行修改本仓库,务必遵守)
Expand Down
16 changes: 16 additions & 0 deletions content/docs/guide/ci-cd-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,22 @@ Uses [Changesets](https://github.com/changesets/changesets) for automated versio
3. Publishes to npm.
4. Configures a pnpm-lock.yaml merge driver to prevent lock file conflicts.

### Changeset Guard (`changeset-guard.yml`)

**Trigger:** PR to `main`/`develop`, and push to `main`, **when `.changeset/**` changes** — the
inverse of every other workflow's filter. `ci.yml` and `lint.yml` both list `'**/*.md'` and
`.changeset/**` under `paths-ignore`, so a PR that adds only a changeset starts nothing else.

Runs `scripts/check-changeset-no-major.mjs`, which fails if any pending changeset declares a
`major` bump. Every publishable package is in one `fixed` group (39 packages), so a single
`major` publishes all of them as the next major — and objectui's major is pinned to the
`@objectstack` major it is compatible with, not to its own count of breaking changes. Score
breaking changes of our own as `minor` and describe the break in the changeset body.

The one release that legitimately bumps the major is the one following `@objectstack` across
its major; it sets `OBJECTUI_ALLOW_MAJOR=1`. `pnpm test` asserts the same repository state, so
the rule survives this workflow being skipped.

### Changelog Generation (`changelog.yml`)

**Trigger:** `release` event (when a GitHub Release is published), or manual dispatch.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"build:console": "pnpm --filter @object-ui/console build",
"shadcn:diff": "node scripts/shadcn-sync.js --diff",
"shadcn:list": "node scripts/shadcn-sync.js --list",
"changeset:check": "node scripts/check-changeset-fixed.mjs",
"changeset:check": "node scripts/check-changeset-fixed.mjs && node scripts/check-changeset-no-major.mjs",
"changeset": "changeset",
"changeset:version": "changeset version",
"changeset:publish": "changeset publish",
Expand Down
128 changes: 128 additions & 0 deletions scripts/__tests__/check-changeset-no-major.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { describe, expect, it } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

// @ts-expect-error — plain-JS CI helper, intentionally untyped
import { findMajorBumps, parseFrontmatterBumps } from '../check-changeset-no-major.mjs';

/**
* objectui's major is not a count of its own breaking changes — it is pinned to
* the `@objectstack` major so that "same major ⇒ compatible" holds across the
* two repos (AGENTS.md §版本号策略). Since every publishable package sits in one
* `fixed` group, ONE `major` in ONE changeset publishes all 39 of them as the
* next major and breaks that pin.
*
* Four pending changesets had scored `major` (17 package entries) during the
* 17.x line, which would have shipped 39 packages as 18.0.0 against an
* `@objectstack` still on 17. The rule was written down and nothing ran it:
* `ci.yml` and `lint.yml` both `paths-ignore` `.changeset/**`, so a
* changeset-only PR started no workflow at all.
*
* These tests are the second lock. `.github/workflows/changeset-guard.yml`
* catches it on the PR that adds the changeset; the repo-state test below
* catches it in `pnpm test` even if that workflow is ever removed or its
* trigger stops matching.
*/
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const changesetDir = path.join(repoRoot, '.changeset');
const workflowDir = path.join(repoRoot, '.github/workflows');

describe('parseFrontmatterBumps', () => {
it('reads the package/bump pairs out of the frontmatter block', () => {
const source = [
'---',
'"@object-ui/layout": minor',
"'@object-ui/fields': patch",
'@object-ui/core: minor',
'---',
'',
'Body text.',
].join('\n');

expect(parseFrontmatterBumps(source)).toEqual([
{ pkg: '@object-ui/layout', bump: 'minor', line: 2 },
{ pkg: '@object-ui/fields', bump: 'patch', line: 3 },
{ pkg: '@object-ui/core', bump: 'minor', line: 4 },
]);
});

it('stops at the closing `---`, so prose about a bump is not a bump', () => {
// Changeset bodies in this repo discuss the major/minor call explicitly —
// and a body may hold a fenced YAML example. Neither may register as a
// declaration, or the guard would fail on a changeset that is obeying it.
const source = [
'---',
'"@object-ui/layout": minor',
'---',
'',
'Scored `minor`, not `major`, per the fixed-group rule.',
'',
'```yaml',
'"@object-ui/layout": major',
'```',
].join('\n');

expect(parseFrontmatterBumps(source)).toEqual([
{ pkg: '@object-ui/layout', bump: 'minor', line: 2 },
]);
expect(findMajorBumps([{ file: 'x.md', source }])).toEqual([]);
});

it('returns nothing for a file with no frontmatter', () => {
expect(parseFrontmatterBumps('# Just a readme\n')).toEqual([]);
});
});

describe('findMajorBumps', () => {
it('reports every major entry with a file:line to jump to', () => {
const source = ['---', '"@object-ui/auth": major', '"@object-ui/react": minor', '---'].join(
'\n'
);

expect(findMajorBumps([{ file: '.changeset/a.md', source }])).toEqual([
{ file: '.changeset/a.md', pkg: '@object-ui/auth', line: 2 },
]);
});
});

describe('the repository itself', () => {
it('has no pending changeset declaring a `major` bump', () => {
const changesets = fs
.readdirSync(changesetDir)
.filter((name) => name.endsWith('.md') && name !== 'README.md')
.map((name) => ({
file: path.join('.changeset', name),
source: fs.readFileSync(path.join(changesetDir, name), 'utf8'),
}));

const offenders = findMajorBumps(changesets) as { file: string; pkg: string; line: number }[];

expect(
offenders.map(({ file, pkg, line }) => `${pkg} at ${file}:${line}`),
'A `major` publishes all 39 fixed-group packages as the next major, off the @objectstack ' +
'major objectui is pinned to. Score it `minor` and describe the break in the body — ' +
'see AGENTS.md §版本号策略.'
).toEqual([]);
});
});

describe('changeset-guard.yml', () => {
const guard = fs.readFileSync(path.join(workflowDir, 'changeset-guard.yml'), 'utf8');

it('runs the guard script', () => {
expect(guard).toContain('node scripts/check-changeset-no-major.mjs');
});

it('triggers on `.changeset/**` — the paths every other workflow ignores', () => {
// The inverse pin. `ci.yml` cannot host this check: a changeset-only PR
// matches its `paths-ignore` twice over (`**/*.md` and `.changeset/**`), so
// no job in it would ever run. If that ever stops being true, this test
// fails and the separate workflow can be folded back in.
expect(guard).toMatch(/paths:\s*\n\s*- '\.changeset\/\*\*'/);

const ci = fs.readFileSync(path.join(workflowDir, 'ci.yml'), 'utf8');
expect(ci).toContain("paths-ignore:");
expect(ci).toContain("- '**/*.md'");
});
});
Loading
Loading