diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d5bce0..1342e3d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,10 +14,35 @@ jobs: # globstar so a script in a new subdirectory is linted without anyone # remembering to edit this list; bin/* covers the extensionless entrypoint # (bin/box). The file list is printed so under-coverage shows up in the log. + # + # dotglob is not decoration (#116): globstar makes `**` descend, but a + # glob still does not MATCH a dot-prefixed name, so `**/` never entered + # `.github/` — and the whole release path (changelog-armed.sh, which + # gates every PR, release-notes.sh, labels-reconcile.sh) went unlinted + # while the comment above told the next author it was covered. + # Measured on this tree: dotglob adds exactly those three and nothing + # else — a checkout's `.git` carries no `*.sh` (its hooks ship as + # `*.sample`), so `**/*.sh` does not wander into it. + # + # The sweep below is the CLASS check, same shape as the eof_guard_sweep + # in test/cli.sh (#112): the one-time fix is `dotglob`, but what keeps + # the gap from reopening is asserting that every TRACKED script is in + # the set actually handed to shellcheck. `git ls-files` is the authority + # on what the repo contains; if the glob ever drifts from it again — + # another dot-directory, another shopt subtlety — CI says which files + # escaped instead of quietly linting a subset and passing. run: | - shopt -s globstar + shopt -s globstar dotglob files=(bin/* **/*.sh) printf 'shellcheck: %s\n' "${files[@]}" + missing="$(comm -13 \ + <(printf '%s\n' "${files[@]}" | sort -u) \ + <(git ls-files '*.sh' | sort -u))" + if [ -n "$missing" ]; then + echo "tracked scripts the shellcheck sweep does not cover (#116):" + printf ' %s\n' $missing + exit 1 + fi shellcheck -x "${files[@]}" - name: cli tests run: bash test/cli.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 492b4f6..448b74b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,28 @@ which records not just what changed but what each drill run proved. ## 0.8.0 — 2026-07-19 +### Fixed + +- **CI's shellcheck sweep never lints `.github/scripts/*.sh`** (#116) — + `globstar` makes `**` descend into subdirectories, but a glob still does + not *match* a dot-prefixed name, so `**/` never entered `.github/`. The + three scripts that escaped are the release path: `changelog-armed.sh` (the + #108/#110 guard that gates every PR, and had never been linted), + `release-notes.sh` (which produces the published release body), and + `labels-reconcile.sh` (the label state machine) — while the step's own + comment promised that "a script in a new subdirectory is linted without + anyone remembering to edit this list". Latent, not broken: all three pass + shellcheck as-is, so this lands as a no-op on current code and the fix is + that a regression in them would now be caught. `dotglob` alongside + `globstar` closes it, measured rather than assumed — it adds exactly those + three and nothing else, a checkout's `.git` carrying no `*.sh` (its hooks + ship as `*.sample`). Paired with a CLASS check in the same shape as the + `eof_guard_sweep` of #112: the sweep now compares the globbed set against + `git ls-files '*.sh'` and fails naming any tracked script it does not + cover, so the gap cannot reopen silently the next time a dot-directory or + a shopt subtlety hides one. `eof_guard_sweep` itself carried the identical + blind spot — it rebuilds the same glob — and is widened the same way. + ### Added - **Merging the release PR IS the release — and the release re-arms main diff --git a/test/cli.sh b/test/cli.sh index 2857c5b..69fa9e8 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -967,7 +967,13 @@ fi # guard is not. eof_guard_sweep() { local f n line bad=0 files - files="$(cd "$ROOT" && shopt -s globstar && printf '%s\n' bin/* ./**/*.sh | sed 's|^\./||' | sort -u)" + # dotglob alongside globstar for the same reason CI's shellcheck step carries + # it (#116): globstar descends, but a glob does not MATCH a dot-prefixed name, + # so this sweep skipped '.github/scripts/*.sh' — the release path — exactly as + # the linter did. Those three set errexit, so they are in scope for this class + # by construction; today none of them reads at all, which is why widening the + # set is a no-op on current code rather than a bug fix. + files="$(cd "$ROOT" && shopt -s globstar dotglob && printf '%s\n' bin/* ./**/*.sh | sed 's|^\./||' | sort -u)" while IFS= read -r f; do [ -f "$ROOT/$f" ] || continue grep -qE '^[[:space:]]*set[[:space:]]+-[a-zA-Z]*e' "$ROOT/$f" || continue