CI's shellcheck sweep never lints .github/scripts/*.sh — globstar does not descend into dot-directories #116

Closed
opened 2026-07-19 23:12:29 +00:00 by dan-claude-bot · 0 comments
dan-claude-bot commented 2026-07-19 23:12:29 +00:00 (Migrated from github.com)

Found during the 0.8.0 release drill. CI's shellcheck sweep does not lint .github/scripts/*.sh — which is where the release machinery lives, including the guard #110 just shipped.

The gap

.github/workflows/ci.yml:18-21:

shopt -s globstar
files=(bin/* **/*.sh)
printf 'shellcheck: %s\n' "${files[@]}"
shellcheck -x "${files[@]}"

globstar makes ** descend into subdirectories, but globs still do not match dot-prefixed names without dotglob. .github is dot-prefixed, so **/ never descends into it.

Measured on main:

$ bash -c 'shopt -s globstar; files=(bin/* **/*.sh); printf "%s\n" "${files[@]}"' | wc -l
15
$ git ls-files '*.sh' | wc -l
17
$ comm -13 <globbed> <all>
.github/scripts/changelog-armed.sh
.github/scripts/labels-reconcile.sh
.github/scripts/release-notes.sh

Why it matters more than the count suggests

The three unlinted files are not incidental — they are the release path:

  • changelog-armed.sh is the guard #108/#110 added to stop a release disarming the changelog. It is brand new, it gates every PR, and it has never been linted.
  • release-notes.sh produces the published release body. release.yml runs it at :128 and :208.
  • labels-reconcile.sh drives the label state machine.

The step's own comment states the intent this defeats:

globstar so a script in a new subdirectory is linted without anyone remembering to edit this list

That is exactly the promise being broken: a script added under .github/scripts/ is silently unlinted, and the comment tells the next author it is covered.

Current state: latent, not broken

All three pass today:

$ shellcheck -x .github/scripts/changelog-armed.sh .github/scripts/labels-reconcile.sh .github/scripts/release-notes.sh
$ echo $?
0

So this is not a live breakage — it is that nothing would catch a regression in them. Worth stating plainly rather than overselling.

Suggested fix

Add dotglob alongside globstar:

shopt -s globstar dotglob

Careful: dotglob also makes bin/* and **/*.sh match other dot-prefixed paths, so check what else it pulls in (a .git directory is not an issue since it holds no .sh, but confirm rather than assume). An explicit .github/scripts/*.sh entry is the alternative — less elegant, and it reintroduces exactly the "remember to edit this list" problem the comment was written to avoid.

Whichever way, worth asserting the count so the gap cannot reopen silently: compare the globbed set against git ls-files '*.sh' and fail if any tracked script is unlinted. That is the same shape as the eof_guard_sweep in #112 — a class check rather than a one-time fix.

Siblings have it too

Same defect, same cause, and in each case it is the release-critical script that escapes:

  • heavy-duty/rig — misses .github/scripts/release-lib.sh (which holds changelog_section, the extraction release.yml publishes from) and labels-reconcile.sh
  • heavy-duty/cast — misses .github/scripts/release-notes.sh and labels-reconcile.sh

Filed separately in each so the record lives where the fix goes.

Refs

Found during the release: 0.8.0 drill (#114). Related: #110 (added changelog-armed.sh), #112 (the eof_guard_sweep class-check precedent).

Found during the 0.8.0 release drill. CI's shellcheck sweep does not lint `.github/scripts/*.sh` — which is where the release machinery lives, including the guard #110 just shipped. ## The gap `.github/workflows/ci.yml:18-21`: ```bash shopt -s globstar files=(bin/* **/*.sh) printf 'shellcheck: %s\n' "${files[@]}" shellcheck -x "${files[@]}" ``` `globstar` makes `**` descend into subdirectories, but **globs still do not match dot-prefixed names** without `dotglob`. `.github` is dot-prefixed, so `**/` never descends into it. Measured on `main`: ``` $ bash -c 'shopt -s globstar; files=(bin/* **/*.sh); printf "%s\n" "${files[@]}"' | wc -l 15 $ git ls-files '*.sh' | wc -l 17 $ comm -13 <globbed> <all> .github/scripts/changelog-armed.sh .github/scripts/labels-reconcile.sh .github/scripts/release-notes.sh ``` ## Why it matters more than the count suggests The three unlinted files are not incidental — they are the release path: - **`changelog-armed.sh`** is the guard #108/#110 added to stop a release disarming the changelog. It is brand new, it gates every PR, and it has never been linted. - **`release-notes.sh`** produces the published release body. `release.yml` runs it at :128 and :208. - **`labels-reconcile.sh`** drives the label state machine. The step's own comment states the intent this defeats: > globstar so a script in a new subdirectory is linted without anyone remembering to edit this list That is exactly the promise being broken: a script added under `.github/scripts/` is silently unlinted, and the comment tells the next author it is covered. ## Current state: latent, not broken All three pass today: ``` $ shellcheck -x .github/scripts/changelog-armed.sh .github/scripts/labels-reconcile.sh .github/scripts/release-notes.sh $ echo $? 0 ``` So this is not a live breakage — it is that nothing would catch a regression in them. Worth stating plainly rather than overselling. ## Suggested fix Add `dotglob` alongside `globstar`: ```bash shopt -s globstar dotglob ``` Careful: `dotglob` also makes `bin/*` and `**/*.sh` match other dot-prefixed paths, so check what else it pulls in (a `.git` directory is not an issue since it holds no `.sh`, but confirm rather than assume). An explicit `.github/scripts/*.sh` entry is the alternative — less elegant, and it reintroduces exactly the "remember to edit this list" problem the comment was written to avoid. Whichever way, worth asserting the count so the gap cannot reopen silently: compare the globbed set against `git ls-files '*.sh'` and fail if any tracked script is unlinted. That is the same shape as the `eof_guard_sweep` in #112 — a class check rather than a one-time fix. ## Siblings have it too Same defect, same cause, and in each case it is the release-critical script that escapes: - `heavy-duty/rig` — misses `.github/scripts/release-lib.sh` (which holds `changelog_section`, the extraction `release.yml` publishes from) and `labels-reconcile.sh` - `heavy-duty/cast` — misses `.github/scripts/release-notes.sh` and `labels-reconcile.sh` Filed separately in each so the record lives where the fix goes. ## Refs Found during the `release: 0.8.0` drill (#114). Related: #110 (added `changelog-armed.sh`), #112 (the `eof_guard_sweep` class-check precedent).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/box#116
No description provided.