Skip to content

Commit e1825aa

Browse files
committed
ci(lint): run check:migration-registry in Lint & Repo Gates, scoped by the gate-family selector
registry.ts is the generated concatenation of packages/spec/src/migrations/ entries/** and ships in the spec package's dist; check:migration-registry is the only proof the two agree and it ran in no CI step. It becomes a step in the required Lint & Repo Gates job, behind a new migration_registry family of scripts/ci/select-gate-families.sh whose read-set is the migration tree, the generator, and the spec package.json and tsconfig.json. The selector's fail-open branches run it like every other family; the self-test pins the family's verdicts and the workflow step. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
1 parent a5afe38 commit e1825aa

3 files changed

Lines changed: 186 additions & 17 deletions

File tree

‎.github/workflows/lint.yml‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,13 +248,15 @@ jobs:
248248
# over the ten merge-group runs measured for #16496; the PM dispatch-gates
249249
# self-test alone 597 s), and none of its expensive steps read a
250250
# merge group's file surface: a docs-only group paid the full battery.
251-
# So on `merge_group` and `pull_request` the NINE scoped families below
251+
# So on `merge_group` and `pull_request` the TEN scoped families below
252252
# -- each step carrying `if: steps.gate-families.outputs.<id> != 'skip'`
253253
# -- run only when the changed paths touch the files that family reads.
254+
# One of the ten, `migration_registry` (#19753), is a gate this job did
255+
# not run before at all; the other nine were scoped away from it.
254256
# `push` on main and the scheduled full run keep the whole battery: the
255257
# script runs everything for any event it does not scope.
256258
#
257-
# #19498 added four of those nine (`entry_guard`,
259+
# #19498 added four of the nine (`entry_guard`,
258260
# `declared_population_live`, `bare_root_worklist`,
259261
# `self_test_workflow_commands`) and narrowed `pm_dispatch_gates`, on
260262
# ruling #208 on #19491 (R4) and the maintainer's sentence quoted in the
@@ -362,6 +364,25 @@ jobs:
362364
- name: ADR anchors + number uniqueness (governed code names its decision)
363365
run: pnpm check:adr-anchors
364366

367+
# Migration registry matches its entry files (#19753). `registry.ts` is
368+
# the generated concatenation of `packages/spec/src/migrations/entries/`,
369+
# it ships in the spec package's `dist`, and this check is the only
370+
# proof the two agree -- `check:generated --reconcile-only` (in the
371+
# `Type Check · source gates` job) proves the script is classified,
372+
# never that the artifact is current. It ran
373+
# in no CI step, so an entry edited without `gen:migration-registry`
374+
# shipped the old text with every required check green.
375+
#
376+
# Scoped, on the maintainer's ruling for that card: a step in THIS
377+
# required job, run when the change touches what the check reads
378+
# (`migration_registry` in the selector above). ⛔ Not a separate
379+
# `paths:`-filtered job: one that never reports on an unrelated PR
380+
# leaves a required context pending forever. Cheap -- tsx over one
381+
# directory, no build (~1.4 s measured locally) -- so it sits early.
382+
- name: Migration registry matches its entry files
383+
if: steps.gate-families.outputs.migration_registry != 'skip'
384+
run: pnpm --filter @objectstack/spec check:migration-registry
385+
365386
# Enforces the no-restricted-imports guard against @objectstack/spec root
366387
# namespace imports (the dormant rule was never run in CI). Syntactic
367388
# only, so no build step needed.

‎scripts/ci/select-gate-families.selftest.sh‎

Lines changed: 124 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@
2020
# edit, an added test, an added changeset) must skip every tooling self-test,
2121
# while the tool's own inputs must still run them.
2222
#
23+
# Since #19753 it covers migration_registry, the one family that ADDS a gate
24+
# to the PR path: an entry, the generated registry, the generator and the two
25+
# package files that decide how it runs must each select it; a spec source
26+
# outside the migration tree, a sibling spec script and a lookalike directory
27+
# must not; and a deletion, an empty diff or an unresolvable base still run it.
28+
#
2329
# The last section reads the REAL lint.yml and pins the YAML half of the
2430
# contract: the selector step exists under the id the `if:` lines name, every
2531
# scoped step spells `!= 'skip'` (an absent output runs the step), and the set
@@ -57,7 +63,7 @@ git_q() {
5763
git -c user.name=selftest -c user.email=selftest@example.invalid -c commit.gpgsign=false "$@"
5864
}
5965

60-
ALL='slot_lookup query_options_erasure entry_guard comment_mask_corpus pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands verify_lock'
66+
ALL='migration_registry slot_lookup query_options_erasure entry_guard comment_mask_corpus pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands verify_lock'
6167

6268
# ── Fixture repositories ────────────────────────────────────────────────────
6369
# C0 carries one representative file of every class the classifier names, so
@@ -67,7 +73,8 @@ mkdir -p "$UP"
6773
git_q -C "$UP" init -q
6874
git_q -C "$UP" symbolic-ref HEAD refs/heads/main
6975
mkdir -p "$UP/packages/a/scripts" "$UP/packages/a/src" "$UP/apps/site/src" "$UP/docs" "$UP/content/docs" \
70-
"$UP/scripts/pm" "$UP/scripts/ci" "$UP/.github/workflows" "$UP/.claude/agents" "$UP/skills/x" "$UP/.changeset"
76+
"$UP/scripts/pm" "$UP/scripts/ci" "$UP/.github/workflows" "$UP/.claude/agents" "$UP/skills/x" "$UP/.changeset" \
77+
"$UP/packages/spec/src/migrations/entries/semantic" "$UP/packages/spec/scripts"
7178
printf '{"name":"fixture","private":true}\n' > "$UP/package.json"
7279
printf 'packages:\n - packages/*\n' > "$UP/pnpm-workspace.yaml"
7380
printf '{"name":"a"}\n' > "$UP/packages/a/package.json"
@@ -92,6 +99,14 @@ printf '# skill\n' > "$UP/skills/x/SKILL.md"
9299
printf '# rules\n' > "$UP/AGENTS.md"
93100
printf '# readme\n' > "$UP/README.md"
94101
printf -- '---\n"a": patch\n---\nchange\n' > "$UP/.changeset/first.md"
102+
# The migration_registry read-set (#19753), and its nearest neighbours outside it.
103+
printf '{"name":"spec"}\n' > "$UP/packages/spec/package.json"
104+
printf '{}\n' > "$UP/packages/spec/tsconfig.json"
105+
printf 'export const index = 1;\n' > "$UP/packages/spec/src/index.ts"
106+
printf 'export const registry = [];\n' > "$UP/packages/spec/src/migrations/registry.ts"
107+
printf 'export const entry = 1;\n' > "$UP/packages/spec/src/migrations/entries/semantic/17.x.ts"
108+
printf 'export const gen = 1;\n' > "$UP/packages/spec/scripts/build-migration-registry.ts"
109+
printf 'export const schemas = 1;\n' > "$UP/packages/spec/scripts/build-schemas.ts"
95110
git_q -C "$UP" add -A
96111
git_q -C "$UP" commit -q -m 'C0: root'
97112
C0=$(git_q -C "$UP" rev-parse HEAD)
@@ -254,7 +269,7 @@ cases=$((cases + 1)); RT="$FIX/rt-$cases"; mkdir -p "$RT"
254269
rc=$?
255270
echo "case: --families prints the family ids in job order"
256271
expect_rc 0
257-
expect_file_is 'the nine ids' "$RT/out.txt" "$(printf '%s\n' $ALL)"
272+
expect_file_is 'the ten ids' "$RT/out.txt" "$(printf '%s\n' $ALL)"
258273

259274
# ── events that are not scoped ──────────────────────────────────────────────
260275
S=$(scenario M:docs/guide.md)
@@ -265,7 +280,8 @@ expect_all_run
265280
expect_reason pm_dispatch_gates "event 'push' is not scoped"
266281
expect_reason entry_guard "event 'push' is not scoped"
267282
expect_reason bare_root_worklist "event 'push' is not scoped"
268-
expect_line 'Gate families: 9 run, 0 skipped'
283+
expect_reason migration_registry "event 'push' is not scoped"
284+
expect_line 'Gate families: 10 run, 0 skipped'
269285

270286
run_case 'schedule: every family runs (the hourly full run keeps the battery)' "$REPO" schedule '' ''
271287
expect_rc 0
@@ -291,7 +307,7 @@ expect_reason pm_dispatch_gates 'scripts/pm/tool.mjs (M, scripts)'
291307
expect_reason self_test_workflow_commands 'scripts/pm/tool.mjs (M, scripts)'
292308
expect_changed 'one M' "M scripts/pm/tool.mjs"
293309
expect_line "Gate-family diff base: $C0 (the merge group's base_sha)"
294-
expect_line 'Gate families: 6 run, 3 skipped'
310+
expect_line 'Gate families: 6 run, 4 skipped'
295311

296312
S=$(scenario A:scripts/pm/new-tool.mjs)
297313
run_case 'merge_group: an ADDED file inside the read-set still runs it -- the narrowing is about the class, never the status' "$REPO" merge_group '' "$C0"
@@ -305,7 +321,7 @@ run_case 'merge_group: a scripts/pm prose change runs the tooling self-tests, an
305321
expect_rc 0
306322
expect_warnings '' ''
307323
expect_verdicts entry_guard pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands
308-
expect_line 'Gate families: 5 run, 4 skipped'
324+
expect_line 'Gate families: 5 run, 5 skipped'
309325

310326
S=$(scenario M:docs/guide.md M:content/docs/page.mdx M:.changeset/first.md M:README.md)
311327
run_case 'merge_group: a docs-only group skips every family, and prints it' "$REPO" merge_group '' "$C0"
@@ -314,11 +330,13 @@ expect_warnings '' ''
314330
expect_verdicts
315331
expect_reason pm_dispatch_gates 'no changed path is in its read-set'
316332
expect_reason entry_guard 'no changed path is in its read-set'
317-
expect_line 'Gate families: 0 run, 9 skipped'
333+
expect_reason migration_registry 'no changed path is in its read-set'
334+
expect_line 'Gate families: 0 run, 10 skipped'
318335
expect_line 'skip pm_dispatch_gates'
319336
expect_line 'skip comment_mask_corpus'
320337
expect_line 'skip self_test_workflow_commands'
321-
if grep -q '^| `pm_dispatch_gates` | skip |' "$RT/step-summary" && grep -q '^## Gate families: 0 run, 9 skipped' "$RT/step-summary"; then
338+
expect_line 'skip migration_registry'
339+
if grep -q '^| `pm_dispatch_gates` | skip |' "$RT/step-summary" && grep -q '^## Gate families: 0 run, 10 skipped' "$RT/step-summary"; then
322340
ok 'the step summary lists the skipped families'
323341
else
324342
bad 'the step summary lists the skipped families' "$(tr '\n' ' ' < "$RT/step-summary")"
@@ -380,6 +398,7 @@ run_case 'merge_group: an EMPTY diff runs every family rather than selecting not
380398
expect_rc 0
381399
expect_warnings '#10057' "::warning::The diff against $C0 listed no changed files; every gate family runs rather than selecting nothing (#10057)."
382400
expect_all_run
401+
expect_reason migration_registry "the diff against $C0 listed no changed files"
383402
expect_changed 'empty' ''
384403

385404
S=$(scenario M:docs/guide.md)
@@ -388,12 +407,14 @@ expect_rc 0
388407
expect_warnings 'no base' '::warning::This merge_group event carries no base_sha, so the gate-family diff base cannot be computed; every family runs.'
389408
expect_all_run
390409
expect_reason pm_dispatch_gates 'no base_sha in the merge_group event'
410+
expect_reason migration_registry 'no base_sha in the merge_group event'
391411

392412
run_case 'merge_group: an unfetchable base_sha warns twice and runs everything' "$REPO" merge_group '' "$ZEROS"
393413
expect_rc 0
394414
expect_warnings 'fetch + resolve' "::warning::Could not fetch the merge group's base $ZEROS; the resolution below will decide.
395415
::warning::Could not resolve the merge group's base '$ZEROS' in this checkout; every gate family runs rather than guessing which paths changed (#16453)."
396416
expect_all_run
417+
expect_reason migration_registry "merge group base '$ZEROS' unresolvable"
397418

398419
if git_q -C "$SHALLOW" cat-file -e "$C0^{commit}" 2>/dev/null; then
399420
bad 'precondition: the shallow clone lacks C0 before the fetch case'
@@ -442,7 +463,8 @@ expect_reason declared_population_live 'no changed path is in its read-set'
442463
expect_reason bare_root_worklist 'no changed path is in its read-set'
443464
expect_reason self_test_workflow_commands 'no changed path is in its read-set'
444465
expect_reason entry_guard 'no changed path is in its read-set'
445-
expect_line 'Gate families: 3 run, 6 skipped'
466+
expect_reason migration_registry 'no changed path is in its read-set'
467+
expect_line 'Gate families: 3 run, 7 skipped'
446468

447469
S=$(scenario M:apps/site/src/page.tsx)
448470
run_case 'merge_group: an apps TSX edit is outside the ratchets (packages/** only) and inside the corpus alone' "$REPO" merge_group '' "$C0"
@@ -487,10 +509,11 @@ expect_verdicts entry_guard pm_dispatch_gates declared_population_live bare_root
487509
expect_reason verify_lock '(M, verify-lock)'
488510

489511
S=$(scenario M:scripts/helper.mjs)
490-
run_case 'merge_group: a top-level scripts module is imported by the ratchets AND the lock preflight, and is a masked source' "$REPO" merge_group '' "$C0"
512+
run_case 'merge_group: a top-level scripts module is imported by the ratchets AND the lock preflight, and is a masked source -- every family but the migration registry, which reads no repo-level script' "$REPO" merge_group '' "$C0"
491513
expect_rc 0
492-
expect_all_run
514+
expect_verdicts slot_lookup query_options_erasure entry_guard comment_mask_corpus pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands verify_lock
493515
expect_reason verify_lock scripts/helper.mjs
516+
expect_reason migration_registry 'no changed path is in its read-set'
494517

495518
S=$(scenario M:scripts/slot-lookup-baseline.json)
496519
run_case 'merge_group: a ratchet baseline runs the ratchets and every family that reads scripts/' "$REPO" merge_group '' "$C0"
@@ -523,7 +546,81 @@ S=$(scenario M:docs/guide.md M:packages/a/src/index.test.ts M:.github/workflows/
523546
run_case 'merge_group: a mixed group runs the union of what its paths reach' "$REPO" merge_group '' "$C0"
524547
expect_rc 0
525548
expect_verdicts slot_lookup query_options_erasure comment_mask_corpus pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands
526-
expect_line 'Gate families: 7 run, 2 skipped'
549+
expect_line 'Gate families: 7 run, 3 skipped'
550+
551+
# ── merge_group: the migration registry family (#19753) ────────────────────
552+
# The #19523 shape first: an entry edited and the registry NOT regenerated.
553+
# The entry is TypeScript under packages/, so the two ratchets and the corpus
554+
# walk ride along; what these cases pin is the migration_registry verdict.
555+
S=$(scenario M:packages/spec/src/migrations/entries/semantic/17.x.ts)
556+
run_case 'merge_group: an entry edited without the registry (the #19523 shape) runs the migration registry' "$REPO" merge_group '' "$C0"
557+
expect_rc 0
558+
expect_warnings '' ''
559+
expect_verdicts migration_registry slot_lookup query_options_erasure comment_mask_corpus
560+
expect_reason migration_registry packages/spec/src/migrations/entries/semantic/17.x.ts
561+
expect_reason migration_registry '(M, workspace)'
562+
expect_line 'Gate families: 4 run, 6 skipped'
563+
564+
S=$(scenario A:packages/spec/src/migrations/entries/retired-keys/17.ui__X__y.ts)
565+
run_case 'merge_group: an ADDED entry runs it -- a new directory under the tree is still the tree' "$REPO" merge_group '' "$C0"
566+
expect_rc 0
567+
expect_warnings '' ''
568+
expect_verdicts migration_registry slot_lookup query_options_erasure comment_mask_corpus
569+
expect_reason migration_registry '(A, workspace)'
570+
571+
S=$(scenario A:packages/spec/src/migrations/entries/semantic/notes.txt)
572+
run_case 'merge_group: a non-TypeScript file in an entry directory runs it and nothing else -- the gate refuses a stray file there' "$REPO" merge_group '' "$C0"
573+
expect_rc 0
574+
expect_verdicts migration_registry
575+
expect_line 'Gate families: 1 run, 9 skipped'
576+
577+
S=$(scenario M:packages/spec/src/migrations/registry.ts)
578+
run_case 'merge_group: the generated registry edited alone runs it -- a hand edit is the other half of the drift' "$REPO" merge_group '' "$C0"
579+
expect_rc 0
580+
expect_verdicts migration_registry slot_lookup query_options_erasure comment_mask_corpus
581+
582+
S=$(scenario M:packages/spec/scripts/build-migration-registry.ts)
583+
run_case 'merge_group: the generator runs it, and every family that reads a package-local script' "$REPO" merge_group '' "$C0"
584+
expect_rc 0
585+
expect_verdicts migration_registry slot_lookup query_options_erasure comment_mask_corpus pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands
586+
expect_reason migration_registry packages/spec/scripts/build-migration-registry.ts
587+
588+
S=$(scenario M:packages/spec/scripts/build-schemas.ts)
589+
run_case 'merge_group: a SIBLING spec script is not the generator -- the arm names one file' "$REPO" merge_group '' "$C0"
590+
expect_rc 0
591+
expect_verdicts slot_lookup query_options_erasure comment_mask_corpus pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands
592+
expect_reason migration_registry 'no changed path is in its read-set'
593+
594+
S=$(scenario M:packages/spec/package.json)
595+
run_case 'merge_group: the spec manifest runs it -- pnpm resolves the check script through it' "$REPO" merge_group '' "$C0"
596+
expect_rc 0
597+
expect_verdicts migration_registry pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands verify_lock
598+
expect_reason migration_registry '(M, workspace)'
599+
600+
S=$(scenario M:packages/spec/tsconfig.json)
601+
run_case 'merge_group: the spec tsconfig runs it alone -- tsx loads it from the package directory' "$REPO" merge_group '' "$C0"
602+
expect_rc 0
603+
expect_verdicts migration_registry
604+
expect_line 'Gate families: 1 run, 9 skipped'
605+
606+
S=$(scenario M:packages/spec/src/index.ts)
607+
run_case 'merge_group: a spec source OUTSIDE the migration tree skips it' "$REPO" merge_group '' "$C0"
608+
expect_rc 0
609+
expect_warnings '' ''
610+
expect_verdicts slot_lookup query_options_erasure comment_mask_corpus
611+
expect_reason migration_registry 'no changed path is in its read-set'
612+
613+
S=$(scenario A:packages/spec/src/migrations-notes/README.md)
614+
run_case 'merge_group: a lookalike directory beside the tree skips it -- the arm is anchored on the separator' "$REPO" merge_group '' "$C0"
615+
expect_rc 0
616+
expect_verdicts
617+
expect_reason migration_registry 'no changed path is in its read-set'
618+
619+
S=$(scenario D:packages/spec/src/migrations/entries/semantic/17.x.ts)
620+
run_case 'merge_group: a DELETED entry runs it, with everything else (structural)' "$REPO" merge_group '' "$C0"
621+
expect_rc 0
622+
expect_all_run
623+
expect_reason migration_registry 'structural change (D packages/spec/src/migrations/entries/semantic/17.x.ts)'
527624

528625
# ── pull_request ────────────────────────────────────────────────────────────
529626
git_q -C "$REPO" checkout -q -B feature "$C0"
@@ -534,6 +631,7 @@ run_case 'pull_request: decided against merge-base(origin/main, HEAD)' "$REPO" p
534631
expect_rc 0
535632
expect_warnings '' ''
536633
expect_verdicts
634+
expect_reason migration_registry 'no changed path is in its read-set'
537635
expect_line "Gate-family diff base: $C0 (merge-base of origin/main and HEAD)"
538636
expect_changed 'the feature edit only, not C1' "M docs/guide.md"
539637

@@ -549,7 +647,17 @@ expect_rc 0
549647
expect_warnings '' ''
550648
expect_verdicts slot_lookup query_options_erasure comment_mask_corpus
551649
expect_reason pm_dispatch_gates 'no changed path is in its read-set'
552-
expect_line 'Gate families: 3 run, 6 skipped'
650+
expect_line 'Gate families: 3 run, 7 skipped'
651+
652+
git_q -C "$REPO" checkout -q -B feature-19753 "$C0"
653+
printf 'export const entry = 2;\n' > "$REPO/packages/spec/src/migrations/entries/semantic/17.x.ts"
654+
git_q -C "$REPO" commit -q -am 'F3: an entry edited on a feature branch, registry not regenerated'
655+
run_case 'pull_request: an entry edited on a feature branch runs the migration registry, decided against the merge base' "$REPO" pull_request main ''
656+
expect_rc 0
657+
expect_warnings '' ''
658+
expect_verdicts migration_registry slot_lookup query_options_erasure comment_mask_corpus
659+
expect_reason migration_registry '(M, workspace)'
660+
expect_line "Gate-family diff base: $C0 (merge-base of origin/main and HEAD)"
553661
git_q -C "$REPO" checkout -q -B feature "$F1"
554662

555663
run_case 'pull_request: no base branch in the payload' "$REPO" pull_request '' ''
@@ -577,6 +685,7 @@ expect_rc 0
577685
expect_warnings 'fetch + #6195' '::warning::Could not fetch origin/nope; the merge-base resolution below will decide.
578686
::warning::Could not resolve merge-base(origin/nope, HEAD); every gate family runs rather than guessing which paths changed (#6195).'
579687
expect_all_run
688+
expect_reason migration_registry 'merge-base(origin/nope, HEAD) unresolvable'
580689

581690
# ── The YAML half of the contract ───────────────────────────────────────────
582691
# Read from the real workflow: the selector step, the `if:` spellings, and
@@ -640,10 +749,11 @@ pin_step self_test_workflow_commands 'node scripts/check-self-test-workflow-comm
640749
# that no workflow passes.
641750
pin_step verify_lock 'bash scripts/pm/os-verify-lock.sh'
642751
pin_step comment_mask_corpus 'node scripts/check-comment-mask-corpus.mjs'
752+
pin_step migration_registry 'pnpm --filter @objectstack/spec check:migration-registry'
643753

644754
# ── Verdict ─────────────────────────────────────────────────────────────────
645755
# #4690: a battery that ran nothing is a failure, never a pass.
646-
if [ "$cases" -lt 42 ] || [ "$checks" -lt 220 ]; then
756+
if [ "$cases" -lt 56 ] || [ "$checks" -lt 293 ]; then
647757
echo "SELFTEST FAILED: only $cases case(s) / $checks check(s) ran -- the battery is short"
648758
exit 1
649759
fi

0 commit comments

Comments
 (0)