Skip to content

ci(shell): gate every tracked shell script on bash -n + ShellCheck - #76

Merged
prakashrj merged 1 commit into
mainfrom
chore/shellcheck-gate
Sep 3, 2026
Merged

ci(shell): gate every tracked shell script on bash -n + ShellCheck#76
prakashrj merged 1 commit into
mainfrom
chore/shellcheck-gate

Conversation

@prakashrj

Copy link
Copy Markdown
Contributor

Closes the v0.2 audit's "no shellcheck / bash -n anywhere" item.

The hole

This repo runs on shell guards — check-platform-floors.sh, check-review-notes.sh, check-release-commit.sh, check-demo-account.sh, verify-floor-runtime.sh, build-tailscalekit.sh, validate-xcframework.sh. None of them was ever linted, and nothing checked that any of them parses.

CI builds the app; it does not execute the ship path or source the guards. A PR that broke one outright would merge green and surface later, on a release.

What it found

Seven warning-level findings on the first run, all fixed here:

code where why it matters
SC2164 ×6 check-app-icon.sh, check-demo-account.sh, check-platform-floors.sh, check-release-commit.sh, check-review-notes.sh, verify-floor-runtime.sh cd "$(dirname "$0")/.." with no || exit
SC2046 ×1 ci/local-release-check.sh unquoted $($PATCH_MACOS_PLIST && echo "" || echo …) in the macOS archive call

All six SC2164 are in guards, which is what makes them worth fixing rather than silencing. If cd fails the script does not stop — it keeps running in the wrong directory and then checks, and passes, files that are not the ones it exists to check. That is the fail-open mode this repo has already been bitten by: an inert check-platform-floors.sh is exactly how the macOS floor stayed wrong in two of three files.

The SC2046 fix uses the ${ARR[@]+"${ARR[@]}"} idiom the two lines directly above it already use, so the "pass nothing" branch is genuinely zero arguments instead of one empty one that xcodebuild would see as a stray empty setting.

Deliberate calls

Capped at warning. The 21 remaining info/style findings print every run for visibility but never fail — dominated by SC2012 and SC1091, neither a defect here. The count differs between a local run (21) and the runner (22 upstream) because the ShellCheck versions differ; that variance is precisely why only warning is gated and the rest is advisory.

ci/lib/ is covered, not exempted — via shellcheck -s bash. Those files are pinned byte-for-byte by ci/lib/SHA256SUMS across this repo and the template, so adding a shell directive to them would force a re-pin here and in every other consumer for the sake of a comment.

Provenance

Authored upstream in apple-shipkit (#285, merged 0a47db1) and cherry-picked, since ci/check-shell.sh is scaffolding every fork needs. Upstream carried three findings this fork does not have (SC1087 ×3 in the Identity.xcconfig code, absent here) and one this fork does not carry (SC2088).

Mutation-verified upstream, because a gate that cannot fail is not evidence:

mutation result
deliberate syntax break fails
reintroduce the SC2164 cd bug fails, naming file and code
remove shellcheck from PATH fails — never a silent skip

It also caught a defect in its own header on first run: prose wrapped so a line began # shellcheck, which ShellCheck parses as a malformed directive.

Wired as pr.yml's shell scripts job (ubuntu, seconds, no paths filter) and as the first preflight in ci/local-check.sh — ahead of the other guards, since it is the one that checks they can run at all. shellcheck added to the Brewfile.

Local: gate passes 31/31; parser 9/9, Sh.stream 10/10, build number 11/11, demo-account 14/14.

Closes the v0.2 audit's "no shellcheck / bash -n anywhere" item.

This repo runs on shell guards — check-platform-floors.sh,
check-review-notes.sh, check-release-commit.sh, check-demo-account.sh,
verify-floor-runtime.sh, build-tailscalekit.sh, validate-xcframework.sh
— and none of them was ever linted, nor was anything checking that they
parse. CI builds the app; it does not execute the ship path or source
the guards, so a PR that broke one outright merged green.

Seven warning-level findings on the first run, all fixed:

- SC2164 x6, in six guards: `cd "$(dirname "$0")/.."` with no
  `|| exit`. This is the fail-open mode — cd fails, the script keeps
  running in the wrong directory, and then checks and PASSES files that
  are not the ones it exists to check.
- SC2046 x1 in local-release-check.sh's macOS archive invocation.
  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.

Severity capped at warning. The 21 info/style findings are printed each
run but never fail; the count differs between a local run and the runner
because the ShellCheck versions differ, which is exactly why only
warning is gated.

Authored upstream (apple-shipkit#285, merged 0a47db1) and cherry-picked:
ci/check-shell.sh is scaffolding every fork needs, and ci/lib/ is pinned
byte-for-byte across repos by ci/lib/SHA256SUMS — covered via
`shellcheck -s bash` rather than exempted, so no downstream re-pin.

Mutation-verified upstream: a syntax break fails it, reintroducing the
cd bug fails it, and removing shellcheck from PATH fails it rather than
skipping.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@prakashrj
prakashrj merged commit b528b92 into main Sep 3, 2026
14 checks passed
@prakashrj
prakashrj deleted the chore/shellcheck-gate branch September 3, 2026 07:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant