CI's shellcheck sweep never lints .github/scripts/*.sh — including release-notes.sh #118

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

cast-side record of heavy-duty/box#116. Same defect, same cause.

The gap

.github/workflows/ci.yml's shellcheck step uses a globstar sweep:

shopt -s globstar
files=(bin/* **/*.sh)
shellcheck -x "${files[@]}"

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

Measured on main, the tracked .sh files CI never lints:

.github/scripts/labels-reconcile.sh
.github/scripts/release-notes.sh

Why it matters here

release-notes.sh produces the published release bodyrelease.yml runs it to extract the section for the version being shipped, and test/release.test.ts calls the real script to assert the changelog guard (#113/#114). It is the script most load-bearing at release time and the one shellcheck never sees.

cast is less exposed than the siblings overall, since most of its logic is TypeScript and npm run check (biome) covers src/. But that is exactly why the shell gap is easy to miss: the repo's main quality gate does not look at shell at all, so these two files fall between biome and an unreachable shellcheck.

Current state: latent, not broken

Nothing is failing today. The defect is that a regression in either file would ship unlinted.

Suggested fix

shopt -s globstar dotglob, checking what else dotglob pulls in rather than assuming.

Worth pairing with an assertion that the globbed set covers git ls-files '*.sh', so this cannot reopen silently — the same class-check shape heavy-duty/box#112 used for its EOF-guard sweep, and the same reasoning behind cast's own disarmedBecause guard: catch the state, not the instance.

Siblings

  • heavy-duty/box#116 — misses changelog-armed.sh, release-notes.sh, labels-reconcile.sh
  • heavy-duty/rig — misses release-lib.sh, labels-reconcile.sh (filed separately)

Refs

Found during the release: 0.1.1 drill (#116), while replicating CI's exact sweep on a real host.

cast-side record of `heavy-duty/box#116`. Same defect, same cause. ## The gap `.github/workflows/ci.yml`'s shellcheck step uses a `globstar` sweep: ```bash shopt -s globstar files=(bin/* **/*.sh) shellcheck -x "${files[@]}" ``` `globstar` makes `**` descend into subdirectories, but **globs do not match dot-prefixed names** without `dotglob`. `.github` is dot-prefixed, so `**/` never descends into it. Measured on `main`, the tracked `.sh` files CI never lints: ``` .github/scripts/labels-reconcile.sh .github/scripts/release-notes.sh ``` ## Why it matters here `release-notes.sh` produces the **published release body** — `release.yml` runs it to extract the section for the version being shipped, and `test/release.test.ts` calls the real script to assert the changelog guard (#113/#114). It is the script most load-bearing at release time and the one shellcheck never sees. cast is less exposed than the siblings overall, since most of its logic is TypeScript and `npm run check` (biome) covers `src/`. But that is exactly why the shell gap is easy to miss: the repo's main quality gate does not look at shell at all, so these two files fall between biome and an unreachable shellcheck. ## Current state: latent, not broken Nothing is failing today. The defect is that a regression in either file would ship unlinted. ## Suggested fix `shopt -s globstar dotglob`, checking what else `dotglob` pulls in rather than assuming. Worth pairing with an assertion that the globbed set covers `git ls-files '*.sh'`, so this cannot reopen silently — the same class-check shape `heavy-duty/box#112` used for its EOF-guard sweep, and the same reasoning behind cast's own `disarmedBecause` guard: catch the state, not the instance. ## Siblings - `heavy-duty/box#116` — misses `changelog-armed.sh`, `release-notes.sh`, `labels-reconcile.sh` - `heavy-duty/rig` — misses `release-lib.sh`, `labels-reconcile.sh` (filed separately) ## Refs Found during the `release: 0.1.1` drill (#116), while replicating CI's exact sweep on a real host.
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/cast#118
No description provided.