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

# Migration registry matches its entry files (#19753). `registry.ts` is
# the generated concatenation of `packages/spec/src/migrations/entries/`,
# it ships in the spec package's `dist`, and this check is the only
# proof the two agree -- `check:generated --reconcile-only` (in the
# `Type Check · source gates` job) proves the script is classified,
# never that the artifact is current. It ran
# in no CI step, so an entry edited without `gen:migration-registry`
# shipped the old text with every required check green.
#
# Scoped, on the maintainer's ruling for that card: a step in THIS
# required job, run when the change touches what the check reads
# (`migration_registry` in the selector above). ⛔ Not a separate
# `paths:`-filtered job: one that never reports on an unrelated PR
# leaves a required context pending forever. Cheap -- tsx over one
# directory, no build (~1.4 s measured locally) -- so it sits early.
- name: Migration registry matches its entry files
if: steps.gate-families.outputs.migration_registry != 'skip'
run: pnpm --filter @objectstack/spec check:migration-registry

# Enforces the no-restricted-imports guard against @objectstack/spec root
# namespace imports (the dormant rule was never run in CI). Syntactic
# only, so no build step needed.
Expand Down
138 changes: 124 additions & 14 deletions scripts/ci/select-gate-families.selftest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@
# edit, an added test, an added changeset) must skip every tooling self-test,
# while the tool's own inputs must still run them.
#
# Since #19753 it covers migration_registry, the one family that ADDS a gate
# to the PR path: an entry, the generated registry, the generator and the two
# package files that decide how it runs must each select it; a spec source
# outside the migration tree, a sibling spec script and a lookalike directory
# must not; and a deletion, an empty diff or an unresolvable base still run it.
#
# The last section reads the REAL lint.yml and pins the YAML half of the
# contract: the selector step exists under the id the `if:` lines name, every
# scoped step spells `!= 'skip'` (an absent output runs the step), and the set
Expand Down Expand Up @@ -57,7 +63,7 @@ git_q() {
git -c user.name=selftest -c user.email=selftest@example.invalid -c commit.gpgsign=false "$@"
}

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'
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'

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

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

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

S=$(scenario A:scripts/pm/new-tool.mjs)
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"
Expand All @@ -305,7 +321,7 @@ run_case 'merge_group: a scripts/pm prose change runs the tooling self-tests, an
expect_rc 0
expect_warnings '' ''
expect_verdicts entry_guard pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands
expect_line 'Gate families: 5 run, 4 skipped'
expect_line 'Gate families: 5 run, 5 skipped'

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

S=$(scenario M:docs/guide.md)
Expand All @@ -388,12 +407,14 @@ expect_rc 0
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.'
expect_all_run
expect_reason pm_dispatch_gates 'no base_sha in the merge_group event'
expect_reason migration_registry 'no base_sha in the merge_group event'

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

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

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

S=$(scenario M:scripts/helper.mjs)
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"
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"
expect_rc 0
expect_all_run
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
expect_reason verify_lock scripts/helper.mjs
expect_reason migration_registry 'no changed path is in its read-set'

S=$(scenario M:scripts/slot-lookup-baseline.json)
run_case 'merge_group: a ratchet baseline runs the ratchets and every family that reads scripts/' "$REPO" merge_group '' "$C0"
Expand Down Expand Up @@ -523,7 +546,81 @@ S=$(scenario M:docs/guide.md M:packages/a/src/index.test.ts M:.github/workflows/
run_case 'merge_group: a mixed group runs the union of what its paths reach' "$REPO" merge_group '' "$C0"
expect_rc 0
expect_verdicts slot_lookup query_options_erasure comment_mask_corpus pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands
expect_line 'Gate families: 7 run, 2 skipped'
expect_line 'Gate families: 7 run, 3 skipped'

# ── merge_group: the migration registry family (#19753) ────────────────────
# The #19523 shape first: an entry edited and the registry NOT regenerated.
# The entry is TypeScript under packages/, so the two ratchets and the corpus
# walk ride along; what these cases pin is the migration_registry verdict.
S=$(scenario M:packages/spec/src/migrations/entries/semantic/17.x.ts)
run_case 'merge_group: an entry edited without the registry (the #19523 shape) runs the migration registry' "$REPO" merge_group '' "$C0"
expect_rc 0
expect_warnings '' ''
expect_verdicts migration_registry slot_lookup query_options_erasure comment_mask_corpus
expect_reason migration_registry packages/spec/src/migrations/entries/semantic/17.x.ts
expect_reason migration_registry '(M, workspace)'
expect_line 'Gate families: 4 run, 6 skipped'

S=$(scenario A:packages/spec/src/migrations/entries/retired-keys/17.ui__X__y.ts)
run_case 'merge_group: an ADDED entry runs it -- a new directory under the tree is still the tree' "$REPO" merge_group '' "$C0"
expect_rc 0
expect_warnings '' ''
expect_verdicts migration_registry slot_lookup query_options_erasure comment_mask_corpus
expect_reason migration_registry '(A, workspace)'

S=$(scenario A:packages/spec/src/migrations/entries/semantic/notes.txt)
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"
expect_rc 0
expect_verdicts migration_registry
expect_line 'Gate families: 1 run, 9 skipped'

S=$(scenario M:packages/spec/src/migrations/registry.ts)
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"
expect_rc 0
expect_verdicts migration_registry slot_lookup query_options_erasure comment_mask_corpus

S=$(scenario M:packages/spec/scripts/build-migration-registry.ts)
run_case 'merge_group: the generator runs it, and every family that reads a package-local script' "$REPO" merge_group '' "$C0"
expect_rc 0
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
expect_reason migration_registry packages/spec/scripts/build-migration-registry.ts

S=$(scenario M:packages/spec/scripts/build-schemas.ts)
run_case 'merge_group: a SIBLING spec script is not the generator -- the arm names one file' "$REPO" merge_group '' "$C0"
expect_rc 0
expect_verdicts slot_lookup query_options_erasure comment_mask_corpus pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands
expect_reason migration_registry 'no changed path is in its read-set'

S=$(scenario M:packages/spec/package.json)
run_case 'merge_group: the spec manifest runs it -- pnpm resolves the check script through it' "$REPO" merge_group '' "$C0"
expect_rc 0
expect_verdicts migration_registry pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands verify_lock
expect_reason migration_registry '(M, workspace)'

S=$(scenario M:packages/spec/tsconfig.json)
run_case 'merge_group: the spec tsconfig runs it alone -- tsx loads it from the package directory' "$REPO" merge_group '' "$C0"
expect_rc 0
expect_verdicts migration_registry
expect_line 'Gate families: 1 run, 9 skipped'

S=$(scenario M:packages/spec/src/index.ts)
run_case 'merge_group: a spec source OUTSIDE the migration tree skips it' "$REPO" merge_group '' "$C0"
expect_rc 0
expect_warnings '' ''
expect_verdicts slot_lookup query_options_erasure comment_mask_corpus
expect_reason migration_registry 'no changed path is in its read-set'

S=$(scenario A:packages/spec/src/migrations-notes/README.md)
run_case 'merge_group: a lookalike directory beside the tree skips it -- the arm is anchored on the separator' "$REPO" merge_group '' "$C0"
expect_rc 0
expect_verdicts
expect_reason migration_registry 'no changed path is in its read-set'

S=$(scenario D:packages/spec/src/migrations/entries/semantic/17.x.ts)
run_case 'merge_group: a DELETED entry runs it, with everything else (structural)' "$REPO" merge_group '' "$C0"
expect_rc 0
expect_all_run
expect_reason migration_registry 'structural change (D packages/spec/src/migrations/entries/semantic/17.x.ts)'

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

Expand All @@ -549,7 +647,17 @@ expect_rc 0
expect_warnings '' ''
expect_verdicts slot_lookup query_options_erasure comment_mask_corpus
expect_reason pm_dispatch_gates 'no changed path is in its read-set'
expect_line 'Gate families: 3 run, 6 skipped'
expect_line 'Gate families: 3 run, 7 skipped'

git_q -C "$REPO" checkout -q -B feature-19753 "$C0"
printf 'export const entry = 2;\n' > "$REPO/packages/spec/src/migrations/entries/semantic/17.x.ts"
git_q -C "$REPO" commit -q -am 'F3: an entry edited on a feature branch, registry not regenerated'
run_case 'pull_request: an entry edited on a feature branch runs the migration registry, decided against the merge base' "$REPO" pull_request main ''
expect_rc 0
expect_warnings '' ''
expect_verdicts migration_registry slot_lookup query_options_erasure comment_mask_corpus
expect_reason migration_registry '(M, workspace)'
expect_line "Gate-family diff base: $C0 (merge-base of origin/main and HEAD)"
git_q -C "$REPO" checkout -q -B feature "$F1"

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

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

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