From 0b7ad21eca3a940b7c23466260448a0c754759cf Mon Sep 17 00:00:00 2001 From: dan-claude-bot Date: Sun, 19 Jul 2026 23:32:49 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20lint=20the=20release=20path=20?= =?UTF-8?q?=E2=80=94=20globstar=20does=20not=20descend=20into=20dot-direct?= =?UTF-8?q?ories?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .github/workflows/ci.yml | 27 ++++++++++++++++++++++++++- CHANGELOG.md | 22 ++++++++++++++++++++++ test/cli.sh | 8 +++++++- 3 files changed, 55 insertions(+), 2 deletions(-) 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 -- 2.45.2 From c68c519558a7db366c3bc8d812971217ebdfaaa6 Mon Sep 17 00:00:00 2001 From: dan-claude-bot Date: Sun, 19 Jul 2026 23:49:06 +0000 Subject: [PATCH 2/2] fix: restore the 0.8.0 heading the changelog entry replaced MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Unreleased entry was written over `## 0.8.0 — 2026-07-19` instead of above it, folding the shipped 0.8.0 section into Unreleased: the release lost its extractable anchor, and the next cut would have republished its notes as new work. changelog-armed.sh passed on that tree — correctly, since it asks only whether the top section agrees with VERSION, and Unreleased was still on top. The gap it leaves is filed as #122. Also quote the class check's failure list instead of relying on unquoted word-splitting to print one path per line. Refs #116 --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1342e3d..76217b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: <(git ls-files '*.sh' | sort -u))" if [ -n "$missing" ]; then echo "tracked scripts the shellcheck sweep does not cover (#116):" - printf ' %s\n' $missing + printf '%s\n' "$missing" | sed 's/^/ /' exit 1 fi shellcheck -x "${files[@]}" diff --git a/CHANGELOG.md b/CHANGELOG.md index 448b74b..ee67ae2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,8 +59,6 @@ which records not just what changed but what each drill run proved. operator copy-pastes is as wrong as a role box executes, and it fails later and further from the cause. -## 0.8.0 — 2026-07-19 - ### Fixed - **CI's shellcheck sweep never lints `.github/scripts/*.sh`** (#116) — @@ -83,6 +81,9 @@ which records not just what changed but what each drill run proved. 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. + +## 0.8.0 — 2026-07-19 + ### Added - **Merging the release PR IS the release — and the release re-arms main -- 2.45.2