name: shell-lint # Shellcheck over every *.sh in the tree. These scripts run as root over SSH # against production hosts (deploy, secrets provisioning, host bootstrap) with # no test suite in front of them -- a quoting bug or an unset-variable typo is # found *on the host* or not at all. The tree was driven to zero findings in # one pass; this guard keeps it there. # # Deliberately NOT path-filtered (same call as secrets-guard): a backstop that # only runs when you expect it to isn't a backstop, and shellcheck over the # ~26 scripts here is seconds. Scripts are discovered with `find`, not listed, # so a new script is covered the moment it lands -- that is the entire point. # # Shellcheck is installed from the pinned official static-binary release, NOT # `apt-get install shellcheck` (the observability-validate precedent): the # distro version drifts with the job image, and a drifted shellcheck can grow # NEW warnings that fail CI on an unrelated push. The pin (v0.11.0, matching # the koalaman/shellcheck:stable image the zero-findings pass was run against) # plus the sha256 makes the check reproducible; bump both together, and expect # to fix any new findings in the same PR as the bump. on: pull_request: push: branches: [dev, main, release] env: SHELLCHECK_VERSION: v0.11.0 SHELLCHECK_SHA256: 8c3be12b05d5c177a04c29e3c78ce89ac86f1595681cab149b65b97c4e227198 jobs: shellcheck: runs-on: docker steps: - uses: actions/checkout@v4 - name: Install shellcheck ${{ env.SHELLCHECK_VERSION }} (pinned static binary) run: | set -euo pipefail # node:20-bookworm (this runner's job image) ships curl, tar and xz, # so the pinned tarball needs no apt-get at all -- faster than the # distro install it replaces. curl -fsSL -o /tmp/shellcheck.tar.xz \ "https://github.com/koalaman/shellcheck/releases/download/${SHELLCHECK_VERSION}/shellcheck-${SHELLCHECK_VERSION}.linux.x86_64.tar.xz" echo "${SHELLCHECK_SHA256} /tmp/shellcheck.tar.xz" | sha256sum -c - tar -xJf /tmp/shellcheck.tar.xz -C /tmp install -m 0755 "/tmp/shellcheck-${SHELLCHECK_VERSION}/shellcheck" /usr/local/bin/shellcheck shellcheck --version - name: Shellcheck every *.sh in the tree run: | set -euo pipefail mapfile -t files < <(find . -name '*.sh' -not -path './.git/*' | sort) if [ ${#files[@]} -eq 0 ]; then echo "no *.sh files yet — nothing to lint" exit 0 fi echo "shellcheck over ${#files[@]} scripts" # -x follows sourced files: several scripts carry # `# shellcheck source=` directives that only resolve with it. # Default severity, no excludes: the tree is at zero findings, so # anything shellcheck reports is a regression, not noise. if shellcheck -x -f gcc "${files[@]}" > /tmp/findings.txt; then echo "ok: all scripts clean" exit 0 fi # gcc format is file:line:col: level: message — reshape each line # into a ::error annotation so findings land on the diff in the PR # view. `rest` soaks up any further colons inside the message. while IFS=: read -r f l _ rest; do [ -n "$l" ] || continue echo "::error file=${f#./},line=${l}::${rest# }" done < /tmp/findings.txt echo "shellcheck found problems in the files above" exit 1