All checks were successful
Build + push backend image (Forgejo registry) / build-push (push) Successful in 1m23s
Build + push frontend image (Forgejo registry) / build-push (push) Successful in 1m22s
Sync infra to hosts / sync-beta (push) Successful in 9s
Sync infra to hosts / sync-prod (push) Successful in 8s
secrets-guard / encrypted (push) Successful in 6s
shell-lint / shellcheck (push) Successful in 9s
Deploy backend to beta VPS / deploy (push) Successful in 2m4s
Deploy frontend to beta VPS / deploy (push) Successful in 1m8s
Adds .forgejo/workflows/shell-lint.yml (pinned shellcheck v0.11.0 + sha256, -x, default severity, fail on any finding, not path-filtered) and drives all 26 scripts to zero findings. Two defects shellcheck cannot see: render-secrets.sh left DECRYPTED vault contents in /tmp whenever a sops decrypt failed -- the caller's set -e aborted the function before either cleanup ran. Now removed on every exit path, with `|| return 1` on both sops calls so a decrypt failure can never write a partial /etc/thermograph.env regardless of the caller's shell options. Explicitly not a `trap ... RETURN`: such a trap set in a sourced function persists into the caller's shell and re-fires when the caller's next `.`/source completes, where the function-local tmp is unset -- fatal and silent under deploy.sh's set -u. The file now records that reasoning. autoscale.sh ran `set -eu` without pipefail while piping docker stats into awk, so a failed left side was swallowed and the loop autoscaled on empty input. Promoted to pipefail with a missed sample treated as a skip; verified busybox ash in docker:27-cli supports it. Also: capture-fixtures.sh's `jq . || cat` ran cat after jq had consumed stdin, silently writing truncated fixtures; deploy.sh/deploy-stack.sh `# shellcheck source=` paths corrected for the monorepo layout.
75 lines
3.4 KiB
YAML
75 lines
3.4 KiB
YAML
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
|