Skip to content

validate-task.yml shellcheck step fails a repo that tracks no .sh files #749

Description

@ptr727

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions