ci(shell): gate every tracked shell script on bash -n + ShellCheck - #285
Merged
Conversation
This template's release path is shell — ci/local-release-check.sh alone
is ~470 lines of signing, packaging and re-signing — and nothing in CI
ever executed any of it. The app(...) cells build the app; they never
source bin/rename.sh or run the ship path. So a PR that broke a shell
script outright merged green and failed later, on someone's release.
Adds ci/check-shell.sh: bash -n over every tracked *.sh (a script that
cannot be parsed must never reach main), then ShellCheck at warning
severity for the tier above that — constructs that run but do the wrong
thing. Seven found on the first run, all fixed here:
- SC2164 x2 (check-app-icon.sh, check-identity.sh): `cd` with no
`|| exit`. In a guard this is the worst case — cd fails, the script
continues in the wrong directory, and passes files that are not the
ones it exists to check.
- SC1087 x3 (rename.sh, test-rename.sh x2): `$var[[:space:]]` parses as
array indexing rather than expand-then-literal-bracket.
- SC2046 x1 (local-release-check.sh): unquoted command substitution,
rewritten to the ${ARR[@]+"${ARR[@]}"} idiom the two lines above it
already use, so the "pass nothing" branch is zero arguments instead of
one empty one.
- SC2088 x1 (refork-smoketest.sh): false positive, a tilde in an error
message. Suppressed inline with the reason rather than reworded.
Severity capped at warning deliberately. info/style (19) are printed
every run for visibility but never fail — they are dominated by SC2012
and SC1091, neither a defect here, and a gate that cries wolf gets
switched off.
ci/lib/ is covered with `shellcheck -s bash` rather than exempted. Those
are sourced libraries with no shebang, pinned byte-for-byte by
ci/lib/SHA256SUMS across this template and every consumer, so adding a
shell directive to them would force a re-pin in every downstream repo
for a comment. The command-line flag gets the same coverage and touches
nothing.
Mutation-verified: a syntax break fails it, reintroducing the cd bug
fails it, and removing shellcheck from PATH fails it rather than
skipping. It also caught a defect in its own header on the first run —
prose wrapped so a line began `# shellcheck`, which ShellCheck reads as
a malformed directive.
Wired as pr.yml's `shell scripts` job (ubuntu, seconds, no paths filter)
and into ci/local-check.sh on the pre-push path. shellcheck added to the
Brewfile.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The hole
This template's release path is shell.
ci/local-release-check.shalone is ~470 lines of signing, packaging and re-signing — and nothing in CI ever executed any of it. Theapp (...)cells build the app; they never sourcebin/rename.shor run the ship path.So a PR that broke a shell script outright merged green, and failed later on somebody's release. There was no
shellcheckand nobash -nanywhere in this repo.The gate
ci/check-shell.sh, run on every PR:bash -nover every tracked*.sh— a script that cannot be parsed must never reach main.warning— the tier above that: constructs that run but do the wrong thing.Seven findings on the first run, all fixed in this PR:
check-app-icon.sh,check-identity.shcd "$(dirname "$0")/.."with no|| exit. In a guard this is the worst case —cdfails, the script keeps going in the wrong directory, and then passes files that are not the ones it exists to checkrename.sh,test-rename.sh×2$var[[:space:]]parses as array indexing, not expand-then-literal-bracketlocal-release-check.sh${ARR[@]+"${ARR[@]}"}idiom the two lines directly above it already use, so the "pass nothing" branch is zero arguments rather than one empty onerefork-smoketest.shTwo deliberate calls
Severity capped at
warning. The 19 remaininginfo/stylefindings are printed every run for visibility but never fail. They are dominated by SC2012 (ls \| grep) and SC1091 (cannot followsource), neither a defect here — and a gate that cries wolf gets switched off.ci/lib/is covered, not exempted — viashellcheck -s bash. Those are sourced libraries with no shebang, and they are pinned byte-for-byte byci/lib/SHA256SUMSacross this template and every consumer. Adding a# shellcheck shell=bashdirective would force a re-pin in every downstream repo for the sake of a comment. The command-line flag gets identical coverage and touches nothing.Verification
Mutation-verified, because a check that cannot fail is not evidence:
cdbugshellcheckfromPATHIt also caught a defect in its own header on the first run: prose wrapped so a line began
# shellcheck, which ShellCheck parses as a malformed directive (SC1073/SC1072). That is noted in the header now, since the next person writing a comment about shellcheck will hit it too.Portable to bash 3.2 (
mapfileavoided) so it behaves the same if it ever runs on a macOS runner. Sibling suites unaffected: parser 9/9,Sh.stream10/10, build number 11/11, xcconfig 51 checks / 0 failures, demo-account 14/14.Wired as pr.yml's
shell scriptsjob (ubuntu, seconds, no paths filter — a shell script is reachable from almost any change here) and intoci/local-check.shon the pre-push path.shellcheckadded to the Brewfile.