Skip to content

Commit c65e529

Browse files
os-zhuangclaude
andauthored
ci(spec): retire the generated paths filter — every artifact gate moves to the unfiltered required job (#4291) (#4292)
The filter was a hand-maintained duplicate of each gate's input set, and nothing reconciled the two. Its own comment asked for the pair to be kept "in lockstep"; nothing enforced that, and it drifted three times on record — each found by accident and written up in a comment rather than gated: - #2584 moved a generated page and the filter kept watching the old path, so hand-edits to the generated block went unchecked for months. - #3855 listed specific spec paths but no schema dirs, so check:authorable-surface went dormant on exactly the PRs that remove an authorable key. - packages/spec/json-schema.manifest.json was never watched at all. It is the #2978 ratchet and the only durable record of every schema ever emitted, since json-schema/ is gitignored, and build-schemas.ts requires a retirement to delete the key from it — so a manifest-only PR skipped its own verifier. Six of the ten gates had already escaped to lint.yml's typecheck job one at a time, each with a comment explaining that the filter had failed them. This moves the last four — check:skill-docs, check:spec-changes, check:upgrade-guide, check:authorable-surface — and deletes the check-generated job, the `generated` filter, and its output. There is no second ledger left to keep in sync: the failure mode #4255 gated for the check:generated ledger is removed at the source here instead. Affordable because that job was already doing the work: check:docs runs gen:schema — the same scripts/build-schemas.ts that backs check:authorable-surface — and that whole step measures 4s in CI, against a 5-minute job dominated by the workspace build (check:skill-refs 0s, check:react-blocks 1s). All four read source via tsx and need no build, so they run before the workspace build like the gates already there. Safe to delete: needs.filter.outputs.generated had exactly one reader, the job's own `if:`, and lint.yml recorded that the job "is not required, either" — so no branch-protection check disappears. Comments in lint.yml, check-generated.ts and AGENTS.md that described the two-job split are updated to match. Closes #4291 Refs #4255, #2584, #3855, #2978 Claude-Session: https://claude.ai/code/session_01QCrKazC5o5a77yXd7ykaZg Co-authored-by: Claude <noreply@anthropic.com>
1 parent e919087 commit c65e529

5 files changed

Lines changed: 96 additions & 116 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
Retire ci.yml's `generated` paths filter and its `Check Generated Artifacts` job; every
6+
spec artifact gate now runs in lint.yml's unfiltered, required TypeScript Type Check job.
7+
8+
The filter was a hand-maintained duplicate of each gate's input set, and nothing
9+
reconciled the two. It drifted three times on record, each found by accident and written
10+
up in a comment rather than gated:
11+
12+
- #2584 moved a generated page and the filter kept watching the old path, so hand-edits
13+
to the generated block went unchecked for months.
14+
- #3855 listed specific spec paths but no schema dirs, so `check:authorable-surface` went
15+
dormant on exactly the PRs that remove an authorable key.
16+
- `packages/spec/json-schema.manifest.json` — the #2978 ratchet, and the only durable
17+
record of every schema ever emitted since `json-schema/` is gitignored — was never
18+
watched at all, so a PR retiring a key from it skipped its own verifier.
19+
20+
Six of the ten gates had already escaped to the typecheck job one at a time, each with a
21+
comment explaining that the filter had failed them. This moves the last four
22+
(`check:skill-docs`, `check:spec-changes`, `check:upgrade-guide`,
23+
`check:authorable-surface`) and deletes the filter, so there is no second ledger to keep
24+
in sync — the failure mode #4255 fixed for the `check:generated` ledger, removed at the
25+
source here rather than gated.
26+
27+
Affordable because the work was already being done: `check:docs` in that job runs
28+
`gen:schema` — the same `scripts/build-schemas.ts` that backs `check:authorable-surface`
29+
and that entire step measures 4s in CI, against a 5-minute job dominated by the workspace
30+
build. All four read source via tsx and need no build.

.github/workflows/ci.yml

Lines changed: 0 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ jobs:
2525
outputs:
2626
docs: ${{ steps.changes.outputs.docs }}
2727
core: ${{ steps.changes.outputs.core }}
28-
generated: ${{ steps.changes.outputs.generated }}
2928
steps:
3029
- name: Checkout repository
3130
uses: actions/checkout@v7
@@ -39,37 +38,6 @@ jobs:
3938
- 'content/**'
4039
- 'pnpm-lock.yaml'
4140
- '.github/workflows/ci.yml'
42-
# Inputs AND outputs of every generated artifact checked by the
43-
# `check-generated` job. A path missing here makes that gate dormant on
44-
# exactly the PRs that can break it, so keep the two in lockstep.
45-
generated:
46-
# check:skill-docs — catalog source is skills/*/SKILL.md; the derived
47-
# listings are skills/README.md + the reference page below. (That page
48-
# was content/docs/guides/skills.mdx until #2584 moved it; this filter
49-
# kept watching the old path, so hand-edits to the generated block went
50-
# unchecked from then until now.)
51-
- 'skills/**'
52-
- 'content/docs/ai/skills-reference.mdx'
53-
- 'packages/spec/scripts/build-skill-docs.ts'
54-
# check:spec-changes / check:upgrade-guide — derived from the ADR-0087
55-
# registries and the protocol version, none of which the skills paths
56-
# above cover. Both gates were dormant on spec-only PRs: a protocol bump
57-
# or a new migration could not trigger the job that verifies them.
58-
- 'packages/spec/scripts/build-spec-changes.ts'
59-
- 'packages/spec/scripts/build-upgrade-guide.ts'
60-
- 'packages/spec/src/migrations/**'
61-
- 'packages/spec/src/conversions/**'
62-
- 'packages/spec/src/kernel/protocol-version.ts'
63-
- 'packages/spec/api-surface.json'
64-
- 'packages/spec/spec-changes.json'
65-
- 'docs/protocol-upgrade-guide.md'
66-
# check:authorable-surface — the ratchet's input is every Zod schema
67-
# in the spec, so the filter has to be that broad or the gate goes
68-
# dormant on exactly the PRs that remove an authorable key (#3855).
69-
- 'packages/spec/src/**'
70-
- 'packages/spec/scripts/build-schemas.ts'
71-
- 'packages/spec/authorable-surface.json'
72-
- '.github/workflows/ci.yml'
7341
core:
7442
- 'packages/**'
7543
- 'examples/**'
@@ -728,67 +696,3 @@ jobs:
728696

729697
- name: Build Docs
730698
run: pnpm --filter @objectstack/docs build
731-
732-
# Generated artifacts that are committed to the tree: regenerate, fail on drift.
733-
# Named for the class, not for skill docs alone — it has checked the ADR-0087
734-
# protocol artifacts for a while, and a name that hid them is part of why their
735-
# paths went missing from the filter above.
736-
#
737-
# The generated reference docs (content/docs/references/**) are deliberately NOT
738-
# here: they need to gate spec-only PRs, and this whole job is filter-gated, so
739-
# they live in lint.yml's required, unfiltered "TypeScript Type Check" job.
740-
check-generated:
741-
name: Check Generated Artifacts
742-
needs: filter
743-
if: needs.filter.outputs.generated == 'true'
744-
runs-on: ubuntu-latest
745-
timeout-minutes: 20
746-
permissions:
747-
contents: read
748-
749-
steps:
750-
- name: Checkout repository
751-
uses: actions/checkout@v7
752-
753-
- name: Setup Node.js
754-
uses: actions/setup-node@v7
755-
with:
756-
node-version: '22'
757-
758-
- name: Enable Corepack
759-
run: corepack enable
760-
761-
- name: Get pnpm store directory
762-
shell: bash
763-
run: |
764-
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
765-
766-
- name: Setup pnpm cache
767-
uses: actions/cache@v6
768-
with:
769-
path: ${{ env.STORE_PATH }}
770-
key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }}
771-
restore-keys: |
772-
${{ runner.os }}-pnpm-store-v3-
773-
774-
- name: Install dependencies
775-
run: pnpm install --frozen-lockfile
776-
777-
- name: Check skill docs are generated from SKILL.md frontmatter
778-
run: pnpm --filter @objectstack/spec check:skill-docs
779-
780-
- name: Check spec-changes.json is regenerated with the ADR-0087 registries
781-
run: pnpm --filter @objectstack/spec check:spec-changes
782-
783-
- name: Check the protocol upgrade guide is regenerated with the ADR-0087 registries
784-
run: pnpm --filter @objectstack/spec check:upgrade-guide
785-
786-
# The authorable KEY surface — what a metadata author may write, which for
787-
# this platform is the third-party API. `api-surface.json` records exported
788-
# names and `api-surface-signatures.json` hashes factory types as TypeScript
789-
# PRINTS them (a reference, never structurally expanded), so neither sees a
790-
# key added to or removed from a schema. #3883 removed three authorable keys
791-
# with every witness green; #3733 did it by accident. ADR-0059 §5 deferred
792-
# this gate until a narrowing actually slipped both — it has.
793-
- name: Check the authorable key surface is recorded and nothing vanished
794-
run: pnpm --filter @objectstack/spec check:authorable-surface

.github/workflows/lint.yml

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -248,14 +248,55 @@ jobs:
248248
# while the wrapper AGENTS.md prescribes exited red on `main`, running zero
249249
# gates. Twice in three days: #4177 (`check:variant-docs`, fixed only when
250250
# #4194 collided with the same wall) and #4232 (`check:strictness-ledger`,
251-
# caught while wiring this step). Not in ci.yml's `check-generated` job:
252-
# its `generated` paths filter does not watch packages/spec/package.json —
253-
# the one file every offending PR must touch — so both offenders skipped
254-
# that job entirely. This job is unfiltered and required, so the meta-gate
255-
# cannot go dormant. Reads package.json only; no build, sub-second.
251+
# caught while wiring this step). It could not live in ci.yml's
252+
# `check-generated` job — that job was gated on a `generated` paths filter
253+
# that never watched packages/spec/package.json, the one file every
254+
# offending PR must touch. That job is gone now (see the four gates below);
255+
# this job is unfiltered and required, so the meta-gate cannot go dormant.
256+
# Reads package.json only; no build, sub-second.
256257
- name: "Check every check:/gen: script is classified in the check:generated ledger"
257258
run: pnpm --filter @objectstack/spec check:generated --reconcile-only
258259

260+
# The last four artifact gates, moved here from ci.yml's `check-generated`
261+
# job when that job and its `generated` paths filter were deleted (#4291).
262+
#
263+
# The filter was a hand-maintained duplicate of each gate's input set, and
264+
# nothing reconciled the two. It drifted three times on record, each found
265+
# by accident and written up in a comment rather than gated: #2584 moved a
266+
# generated page and the filter kept watching the old path, so hand-edits
267+
# went unchecked for months; #3855 listed specific spec paths but no schema
268+
# dirs, so `check:authorable-surface` went dormant on exactly the PRs that
269+
# remove an authorable key; and `json-schema.manifest.json` — the #2978
270+
# ratchet that is the ONLY durable record of every emitted schema, since
271+
# json-schema/ is gitignored — was never watched at all, so a PR retiring a
272+
# key from it skipped its own verifier. Six gates had already escaped to
273+
# this job one at a time, each with a comment saying the filter had failed
274+
# it; these four finish the migration and retire the duplicate ledger.
275+
#
276+
# Affordable because the work was already being done here: `check:docs`
277+
# below runs `gen:schema` — the same scripts/build-schemas.ts that backs
278+
# `check:authorable-surface` — and that whole step measures 4s in CI. All
279+
# four read source via tsx and need no build, so they run before the
280+
# workspace build, as the gates below already do.
281+
- name: Check skill docs are generated from SKILL.md frontmatter
282+
run: pnpm --filter @objectstack/spec check:skill-docs
283+
284+
- name: Check spec-changes.json is regenerated with the ADR-0087 registries
285+
run: pnpm --filter @objectstack/spec check:spec-changes
286+
287+
- name: Check the protocol upgrade guide is regenerated with the ADR-0087 registries
288+
run: pnpm --filter @objectstack/spec check:upgrade-guide
289+
290+
# The authorable KEY surface — what a metadata author may write, which for
291+
# this platform is the third-party API. `api-surface.json` records exported
292+
# names and `api-surface-signatures.json` hashes factory types as TypeScript
293+
# PRINTS them (a reference, never structurally expanded), so neither sees a
294+
# key added to or removed from a schema. #3883 removed three authorable keys
295+
# with every witness green; #3733 did it by accident. ADR-0059 §5 deferred
296+
# this gate until a narrowing actually slipped both — it has.
297+
- name: Check the authorable key surface is recorded and nothing vanished
298+
run: pnpm --filter @objectstack/spec check:authorable-surface
299+
259300
# Generated-docs gate: content/docs/references/** is generated from the spec
260301
# by `gen:schema && gen:docs` and committed. Nothing regenerated it in CI, so
261302
# it drifted silently — #3076 added RowCrudActionOverride to the spec and the
@@ -278,10 +319,12 @@ jobs:
278319
# drift is served straight to consumers' agents — 6 of 113 schema pointers named
279320
# files the spec had already deleted or renamed.
280321
#
281-
# Not in ci.yml's `check-generated` job for the reason above: its `generated`
282-
# filter lists specific spec paths (migrations/, conversions/, protocol-version)
283-
# but no schema dirs, so a PR touching src/data/** or src/ui/** — exactly what
284-
# drives these two artifacts — never triggers it. It is not required, either.
322+
# These moved here early, when ci.yml still had a filter-gated
323+
# `check-generated` job: its `generated` filter listed specific spec paths
324+
# (migrations/, conversions/, protocol-version) but no schema dirs, so a PR
325+
# touching src/data/** or src/ui/** — exactly what drives these two
326+
# artifacts — never triggered it, and it was not required either. That job
327+
# and its filter are gone (#4291); every artifact gate now lives here.
285328
#
286329
# Both read packages/spec/src via tsx and need no build (verified with every
287330
# workspace dist/ removed), so they run before the workspace build. check:skill-refs

AGENTS.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -229,10 +229,12 @@ Root also exports: `defineStack`, `composeStacks`, `defineView`, `defineApp`, `d
229229
### Touched `packages/spec`? Regenerate its artifacts BEFORE pushing
230230

231231
`packages/spec` has **eight** checked-in generated artifacts, each with its own CI gate.
232-
They live in two different jobs (`TypeScript Type Check` in `lint.yml`, `Check Generated
233-
Artifacts` in `ci.yml`), and each job runs its gates **sequentially** — so the first
234-
stale artifact masks every one behind it, and you get one red build per artifact instead
235-
of one for all of them. Match the change to the gate and regenerate up front:
232+
All of them live in one job — `TypeScript Type Check` in `lint.yml`, which is required and
233+
has no paths filter, so no gate can go dormant on the PR that breaks it (#4291 retired the
234+
filtered `Check Generated Artifacts` job for exactly that reason). That job runs its gates
235+
**sequentially**, so the first stale artifact masks every one behind it, and you get one
236+
red build per artifact instead of one for all of them. Match the change to the gate and
237+
regenerate up front:
236238

237239
| You changed | Gate that fails | Regenerate with `pnpm --filter @objectstack/spec …` |
238240
|:---|:---|:---|

packages/spec/scripts/check-generated.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
* Run **every** generated-artifact gate and report **all** stale artifacts in one
66
* pass.
77
*
8-
* CI runs these gates as separate sequential steps across two jobs, so the first
9-
* stale artifact masks the rest: you fix it, push, and discover the next one on
8+
* CI runs these gates as separate sequential steps, so the first stale artifact
9+
* masks the rest: you fix it, push, and discover the next one on
1010
* the following run. That happened twice on #4040 (`check:docs`, then
1111
* `check:api-surface`) and twice again on #4161 (`check:spec-changes`, then
1212
* `check:upgrade-guide`) — four pushes spent learning something one local run
@@ -153,11 +153,12 @@ const fix = process.argv.includes('--fix');
153153
// individual steps, so an unclassified `check:`/`gen:` script kept every CI gate
154154
// green while this wrapper exited red on `main` before running a single gate.
155155
// Twice in three days — #4177 (fixed only by colliding with #4194) and #4232
156-
// (caught wiring this flag in). ci.yml's `check-generated` job cannot host the
157-
// fix: its `generated` paths filter does not watch packages/spec/package.json,
158-
// the one file every offending PR must touch — both offenders skipped that job
159-
// entirely. So lint.yml's unfiltered, required "TypeScript Type Check" job runs
160-
// this mode instead. Reads package.json and the arrays above; no build, <1s.
156+
// (caught wiring this flag in). It could not go in ci.yml's `check-generated`
157+
// job: that job was gated on a `generated` paths filter that never watched
158+
// packages/spec/package.json, the one file every offending PR must touch, so
159+
// both offenders skipped it entirely. #4291 deleted that job and its filter and
160+
// moved every gate to lint.yml's unfiltered, required "TypeScript Type Check"
161+
// job, which runs this mode too. Reads package.json and the arrays above; <1s.
161162
const reconcileOnly = process.argv.includes('--reconcile-only');
162163
const scripts = JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf8')).scripts ?? {};
163164
reconcileLedger(scripts);

0 commit comments

Comments
 (0)