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
38 changes: 38 additions & 0 deletions .github/workflows/pr-automation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,44 @@ jobs:
node scripts/check-empty-changeset.mjs --self-test
node scripts/check-empty-changeset.mjs --base "$MERGE_BASE"

# #6148: a declared-breaking changeset must say IN WRITING what it did about
# the ADR-0087 migration ledger. The two existing ledger gates
# (`check:spec-changes`, `check:upgrade-guide`) both pin ledger <-> ARTIFACT
# SYNCHRONY; neither pins that a retirement which actually happened has an
# entry at all. The artifacts are a pure projection of the registry, so an
# entry that was NEVER written leaves the two perfectly consistent and every
# gate green, repo-wide. PR #6048 removed `ctx.user.roles` exactly that way;
# a human triage seat caught it by eye (#6011), and a separate dispatch round
# backfilled it (PR #6138).
#
# This step lives HERE, beside check-empty-changeset, rather than in ci.yml's
# lint family, for one reason: its verdict is a function of the PR's DIFF, so
# it needs the same `$MERGE_BASE` the steps above derive. A lint-job copy
# would have no branch point to judge against and would fall back to reading
# stock -- which is precisely the "judge the author for what main gained while
# their PR was open" defect (#6129) the merge base exists to prevent.
#
# It does NOT detect retirements. Cross-package retirement detection is
# undecidable in the case that actually happened (the removal was in
# `packages/runtime`, the ledger is in `packages/spec`), and the maintainer's
# ruling on #6148 routes around it: the AUTHOR has already declared "breaking"
# in the changeset, so the gate only has to notice that a declared-breaking
# change said nothing. Measured, ~1 declared-breaking change in 7 needs a
# ledger entry, so `not-required (...)` is the ordinary answer and costs one
# line -- but it is a line someone wrote and a reviewer can see.
#
# `--self-test` runs AHEAD of the real check, the repo convention for
# `check-*.mjs` gates: its fixtures are the only place the red directions are
# pinned, and each of them was verified to flip green when the corresponding
# check is ablated. Real temp git repositories, well under a second.
- name: Require an ADR-0087 disposition on a declared-breaking changeset
if: steps.labels.outputs.skip != 'true'
env:
MERGE_BASE: ${{ steps.diffbase.outputs.merge_base }}
run: |
node scripts/check-adr-0087-registration.mjs --self-test
node scripts/check-adr-0087-registration.mjs --base "$MERGE_BASE"

- name: Guard against accidental major bumps (launch window)
# Every publishable package is in one Changesets "fixed" (lockstep) group,
# so a single `major` bump promotes the ENTIRE monorepo to a new major
Expand Down
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,14 @@ it to `OPEN_CAPABILITY_REGISTRIES` in the same PR that fixes it.
isn't green yet). A finished task = a merged PR, not a dirty working tree.
3. **Add a changeset for feature work.** When the change is a feature or functional improvement, run `pnpm changeset` (or add a `.changeset/*.md` entry) describing it before committing. Pure bug fixes do **not** require a changeset.
**Breaking changesets must carry their migration.** If the change removes or renames anything an author can write (a spec key, an export, a config field), the changeset body must state the FROM → TO mapping and the one-line fix — this text ships to consumers as `CHANGELOG.md` inside the npm package and is what an upgrading agent greps after the tombstone error. Removing an authorable spec key also requires a tombstone so the rejection itself carries the prescription — `retiredKey()` (`packages/spec/src/shared/retired-key.ts`) on a non-strict schema, or an entry in the relevant `UNKNOWN_KEY_GUIDANCE` / `*_RETIRED_KEY_GUIDANCE` map (see `object.zod.ts`, `ai/tool.zod.ts`) when the schema is `.strict()`. The changeset is one of fourteen surfaces a retirement touches — follow the `spec-property-retirement` skill (`.claude/skills/`) rather than reconstructing the kit, and note the two routes imply **opposite** liveness-ledger dispositions.
**A breaking changeset must also state its ADR-0087 disposition, in writing.** Add exactly one marker to the changeset body — `pnpm check:adr-0087-registration` enforces it, and the CI step is *Require an ADR-0087 disposition on a declared-breaking changeset*:
```
<!-- adr-0087: registered SOME-MIGRATION-ID -->
<!-- adr-0087: not-required (unpublished) why -->
<!-- adr-0087: not-required (already-registered SOME-MIGRATION-ID) why -->
<!-- adr-0087: not-required (no-migration-prescription) why -->
```
Why it is asked of you at all: the two ADR-0087 gates (`check:spec-changes`, `check:upgrade-guide`) pin ledger ↔ **artifact synchrony**, and the artifacts are a pure projection of the registry — so a retirement whose entry was **never written** leaves the two perfectly consistent and every gate in the repo green. PR #6048 removed `ctx.user.roles` that way and only a human comparing by eye caught it (#6011, backfilled by PR #6138). Ledger entries are the sole data source for `objectstack migrate meta`, `spec-changes.json` and the generated upgrade guide, and for a surface with **no spec schema** (`ctx.user` is only a runtime TS interface) there is no tombstone and no schema rejection either — the ledger entry is the *only* channel that reaches an upgrader. Measured: roughly **1 declared-breaking change in 7** needs an entry, so `not-required` is the ordinary answer and costs one line. Three of the four dispositions are re-verified mechanically on every run, and the fourth is refused when the changeset's own body carries a FROM → TO prescription — a changeset that ships migration instructions cannot also claim nobody must migrate (#6148).
4. **Added or removed a `packages/spec` export? Run `pnpm --filter @objectstack/spec gen:api-surface` and commit the result.** The `TypeScript Type Check` job diffs spec's built export surface against `api-surface/` (one shard per entry point since #5837); a new export makes the snapshot stale and turns the job red. It reads the **built `dist` declarations**, so `OS_SKIP_DTS=1` — the flag you reach for to make local builds fast — skips exactly the artifact the gate inspects, and the check passes locally while failing in CI. Same shape for the other generated-artifact gates in that job (`check:docs`, `check:skill-refs`, `check:react-blocks`), which read `src/` and so do reproduce locally.
5. Update `CHANGELOG.md` / `ROADMAP.md` if user-facing or architectural.
6. **Delete temporary artifacts** — screenshots, traces, scratch logs, `.playwright-mcp/`, throwaway `tmp*.ts`, ad-hoc scripts. Repo must look identical to before, minus intended changes.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"check:objectui-pin-fresh": "node scripts/check-objectui-pin-fresh.mjs --self-test && node scripts/check-objectui-pin-fresh.mjs",
"check:prerelease-pins": "node scripts/check-prerelease-pin-watch.mjs --self-test && node scripts/check-prerelease-pin-watch.mjs",
"check:empty-changeset": "node scripts/check-empty-changeset.mjs --self-test && node scripts/check-empty-changeset.mjs",
"check:adr-0087-registration": "node scripts/check-adr-0087-registration.mjs --self-test && node scripts/check-adr-0087-registration.mjs",
"check:override-consistency": "node scripts/check-override-consistency.mjs --self-test && node scripts/check-override-consistency.mjs",
"check:release-notes": "node scripts/check-release-notes.mjs",
"check:release-body": "node scripts/release-github-releases.mjs --self-test",
Expand Down
Loading
Loading