From 5537fa6561be9a7fc141356aa7f1da69907faa66 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 23:29:30 +0000 Subject: [PATCH] ci(lint): add a `.sh` arm to select-gate-families's pm_dispatch_gates read-set The dispatch-gates self-test grew a live census that reads the CONTENT of every tracked `.sh` file (comment-masked watch-hint extraction), but the selector's pm_dispatch_gates read-set had no `*.sh` rule. A modified shell script under packages/**, apps/**, examples/**, docs/** or content/** that does not sit in a scripts/ subdirectory classified to a class whose arm skips the gate, silently disarming a required merge check for the one path shape `*/scripts/*` does not already cover. Add `case "$path" in *.sh) return 0 ;; esac` beside the existing `.gitignore` arm, before the class switch, so any `.sh` file runs the family whatever class it sits in -- a strict superset of what already ran, so it can only widen coverage, never narrow it. Add a self-test case that drives the window directly: a `packages/a/foo.sh` modified at status M now selects pm_dispatch_gates (asserted red against the unmodified script, green after), with a negative control confirming a docs-class file at status M still skips every family. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU --- scripts/ci/select-gate-families.selftest.sh | 8 ++++++ scripts/ci/select-gate-families.sh | 29 ++++++++++++--------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/scripts/ci/select-gate-families.selftest.sh b/scripts/ci/select-gate-families.selftest.sh index cb812fc28e..ae75c1548c 100644 --- a/scripts/ci/select-gate-families.selftest.sh +++ b/scripts/ci/select-gate-families.selftest.sh @@ -67,6 +67,7 @@ printf '{"name":"a"}\n' > "$UP/packages/a/package.json" printf 'export const a = 1;\n' > "$UP/packages/a/src/index.ts" printf 'export const t = 1;\n' > "$UP/packages/a/src/index.test.ts" printf '{"rows":[]}\n' > "$UP/packages/a/src/data.json" +printf '#!/usr/bin/env bash\necho foo\n' > "$UP/packages/a/foo.sh" printf 'dist/\n' > "$UP/packages/a/.gitignore" printf 'console.log(1);\n' > "$UP/packages/a/scripts/build.mjs" printf 'export const site = 1;\n' > "$UP/apps/site/src/page.tsx" @@ -428,6 +429,13 @@ expect_rc 0 expect_warnings '' '' expect_verdicts +S=$(scenario M:packages/a/foo.sh) +run_case 'merge_group: a workspace shell script outside scripts/ is still gate source, not a skipped non-source workspace file (#16769)' "$REPO" merge_group '' "$C0" +expect_rc 0 +expect_warnings '' '' +expect_verdicts pm_dispatch_gates +expect_reason pm_dispatch_gates packages/a/foo.sh + S=$(scenario M:packages/a/scripts/build.mjs) run_case 'merge_group: a package-local script is a gate source (PM) and a masked source (corpus)' "$REPO" merge_group '' "$C0" expect_rc 0 diff --git a/scripts/ci/select-gate-families.sh b/scripts/ci/select-gate-families.sh index 95aad78301..b260018ab6 100644 --- a/scripts/ci/select-gate-families.sh +++ b/scripts/ci/select-gate-families.sh @@ -61,17 +61,17 @@ # `AGENTS.md`, `CLAUDE.md`, `tsconfig.json`, # .gitignore, and sweeps `git ls-files` for hint # reachability and test-file residue. It also reads -# the CONTENT of every JS/TS file in the tree: the -# compound-anchor census of `function ...SelfTest...(` -# declarations asserts none is unlisted, and the -# exposed-scratch-dir sweep reads every -# mkdtempSync/mkdirSync caller and consults nested -# .gitignore files. So any masked source file and any -# .gitignore runs it, and because the name sweep reads -# the tracked NAME set an ADDED file anywhere runs it -# too; only modifications of docs, changesets and -# non-source workspace files that are neither a -# manifest nor a script skip it. +# the CONTENT of every JS/TS and shell (`.sh`) file in +# the tree: the compound-anchor census of +# `function ...SelfTest...(` declarations asserts none +# is unlisted, and the exposed-scratch-dir sweep reads +# every mkdtempSync/mkdirSync caller and consults +# nested .gitignore files. So any masked source file, +# any `.sh` file, and any .gitignore runs it, and +# because the name sweep reads the tracked NAME set an +# ADDED file anywhere runs it too; only modifications +# of docs, changesets and non-source workspace files +# that are neither a manifest nor a script skip it. # query_options_erasure `pnpm check:query-options-erasure`. Lints # packages/**/*.{ts,tsx,mts,cts} under # `eslint.config.mjs`, reads its baseline @@ -314,9 +314,12 @@ family_reads() { # The self-test reads the CONTENT of every JS/TS file in the tree (the # compound-anchor census of `function ...SelfTest...(` declarations, the # exposed-scratch-dir sweep of every mkdtempSync/mkdirSync caller) and - # consults nested .gitignore files, so any masked source and any - # .gitignore runs it whatever class it sits in. + # every tracked `.sh` file (the same watch-hint extraction, run through + # the comment mask), and consults nested .gitignore files, so any masked + # source, any `.sh` file, and any .gitignore runs it whatever class it + # sits in. is_masked_source "$path" && return 0 + case "$path" in *.sh) return 0 ;; esac case "$path" in */.gitignore) return 0 ;; esac case "$class" in docs|changeset) return 1 ;;