Found while resyncing a downstream repo against hub 79f4f17, re-verified against 20616e0.
The defect
.github/workflows/validate-task.yml, the "Check shell scripts step":
- name: Check shell scripts step
run: |
set -Eeuo pipefail
mapfile -t scripts < <(git ls-files '*.sh')
docker run --rm --pull=always -v "$PWD":/mnt --workdir /mnt koalaman/shellcheck:stable "${scripts[@]}"
When a repository tracks no .sh files the array is empty, "${scripts[@]}" expands to nothing, and shellcheck runs with no file arguments. Measured:
$ docker run --rm koalaman/shellcheck:stable ; echo "exit=$?"
No files specified.
Usage: shellcheck [OPTIONS...] FILES...
exit=3
set -Eeuo pipefail then fails the step, the lint job, and the ruleset-bound aggregator. A repository with nothing to check fails the check for having nothing to check.
The bash half is not the problem and is worth ruling out explicitly: an empty array expansion under set -u is fine on bash 4.4 and later, which ubuntu-latest has. The failure is entirely shellcheck's argument handling.
Why this is asymmetric rather than merely unhandled
The PowerShell step directly below it is built the same way from the same git ls-files pattern, and it does guard:
$files = $env:PS_SCRIPTS -split "\s+" | Where-Object { $_ }
if (-not $files) { Write-Host "no PowerShell scripts are tracked"; exit 0 }
Its comment even says it is "the peer of the step above, built the same way". The two disagree on the empty case, and the shell one is the half that fails.
How a repo reaches the empty case
Not hypothetically. The retirement of the carried repo-config/configure.sh recorded in spec/divergences.json removes the only tracked .sh file from a config-style repo. The downstream repo this was found in went from one tracked shell script to zero on exactly that change, confirmed by repo_gate.py:
[ok ] eol-coverage 0 issue(s)
note: read 0 LF pin(s), 0 of them forward-declared, over 0 shebang file(s) in 52 tracked file(s).
That repo did not hit the failure only because its validate-task.yml carries the four doc linters it actually needs rather than the full hub set. A repo that carries the step verbatim, and any repo whose last .sh is retired later, hits it.
Suggested fix
Give the shell step the same guard its PowerShell peer already has, so the two agree:
mapfile -t scripts < <(git ls-files '*.sh')
if [ ${#scripts[@]} -eq 0 ]; then echo "no shell scripts are tracked"; exit 0; fi
docker run --rm --pull=always -v "$PWD":/mnt --workdir /mnt koalaman/shellcheck:stable "${scripts[@]}"
Echoing the count on the success path would also match the reasoning already written into the PowerShell step's comments, that a checker which read no files reports the same clean as one that read them all.
Reported by an agent during a downstream resync. Hub read at 20616e0.
Found while resyncing a downstream repo against hub
79f4f17, re-verified against20616e0.The defect
.github/workflows/validate-task.yml, the "Check shell scripts step":When a repository tracks no
.shfiles the array is empty,"${scripts[@]}"expands to nothing, andshellcheckruns with no file arguments. Measured:set -Eeuo pipefailthen fails the step, thelintjob, and the ruleset-bound aggregator. A repository with nothing to check fails the check for having nothing to check.The bash half is not the problem and is worth ruling out explicitly: an empty array expansion under
set -uis fine on bash 4.4 and later, whichubuntu-latesthas. The failure is entirely shellcheck's argument handling.Why this is asymmetric rather than merely unhandled
The PowerShell step directly below it is built the same way from the same
git ls-filespattern, and it does guard:Its comment even says it is "the peer of the step above, built the same way". The two disagree on the empty case, and the shell one is the half that fails.
How a repo reaches the empty case
Not hypothetically. The retirement of the carried
repo-config/configure.shrecorded inspec/divergences.jsonremoves the only tracked.shfile from a config-style repo. The downstream repo this was found in went from one tracked shell script to zero on exactly that change, confirmed byrepo_gate.py:That repo did not hit the failure only because its
validate-task.ymlcarries the four doc linters it actually needs rather than the full hub set. A repo that carries the step verbatim, and any repo whose last.shis retired later, hits it.Suggested fix
Give the shell step the same guard its PowerShell peer already has, so the two agree:
Echoing the count on the success path would also match the reasoning already written into the PowerShell step's comments, that a checker which read no files reports the same clean as one that read them all.
Reported by an agent during a downstream resync. Hub read at
20616e0.