fix: lint the release path — globstar does not descend into dot-directories

CI's shellcheck sweep set globstar and globbed `bin/* **/*.sh`. globstar
makes `**` descend into subdirectories, but a glob still does not MATCH a
dot-prefixed name, so `**/` never entered `.github/` and three scripts were
never linted: changelog-armed.sh (the #108/#110 guard that gates every PR),
release-notes.sh (which produces the published release body), and
labels-reconcile.sh (the label state machine). That is the entire release
path, while the step's own comment promised the opposite — that a script in
a new subdirectory is linted without anyone editing a list.

Latent, not broken: all three pass shellcheck as-is, so this is a no-op on
current code. What changes is that a regression in them would be caught.

dotglob alongside globstar closes it, measured rather than assumed: it adds
exactly those three and nothing else — a checkout's .git carries no *.sh,
its hooks shipping as *.sample, so `**/*.sh` does not wander into it.

The one-time fix is dotglob; what keeps the gap shut is the 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 a future dot-directory or shopt subtlety
cannot silently lint a subset and pass.

eof_guard_sweep carried the identical blind spot — it rebuilds the same
glob — and is widened the same way. A no-op today: the three scripts set
errexit, so they are in that class by construction, but none of them reads.

Refs #116
This commit is contained in:
dan-claude-bot 2026-07-19 23:32:49 +00:00
parent 4c34facefe
commit 0b7ad21eca
3 changed files with 55 additions and 2 deletions

View file

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

View file

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

View file

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