From 5b930da6402472fd480e0a8154c8f5c54ad7ac9a Mon Sep 17 00:00:00 2001 From: ITSMESB <131141975+ITSMERNB@users.noreply.github.com> Date: Thu, 10 Sep 2026 21:37:51 +0200 Subject: [PATCH 1/3] fix(ci): make ShellCheck coverage fail closed Signed-off-by: ITSMESB <131141975+ITSMERNB@users.noreply.github.com> --- .github/workflows/ci.yml | 3 ++ .github/workflows/e2e.yml | 8 ++-- scripts/shellcheck-files.sh | 35 ++++++++++++++ tests/release/shellcheck-coverage.test.sh | 58 +++++++++++++++++++++++ 4 files changed, 101 insertions(+), 3 deletions(-) create mode 100755 scripts/shellcheck-files.sh create mode 100755 tests/release/shellcheck-coverage.test.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23dce4d4..06d354f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,6 +92,9 @@ jobs: - name: Check tracked files use portable line endings run: bash tests/release/tracked-eol.test.sh + - name: Check ShellCheck covers every tracked shell file + run: bash tests/release/shellcheck-coverage.test.sh + - name: Check systemd directory modes match tmpfiles run: bash tests/release/systemd-directory-modes.test.sh diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 72217af8..2edef3ba 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -76,9 +76,11 @@ jobs: - name: ShellCheck maintained scripts run: | - find tests/e2e tests/release scripts assets/demo \ - -type f -name '*.sh' -print0 \ - | xargs -0 shellcheck --severity=warning + file_list="$(mktemp)" + trap 'rm -f "$file_list"' EXIT + scripts/shellcheck-files.sh >"$file_list" + mapfile -d '' files <"$file_list" + shellcheck --severity=warning "${files[@]}" - name: Install Rust toolchain uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable diff --git a/scripts/shellcheck-files.sh b/scripts/shellcheck-files.sh new file mode 100755 index 00000000..d2849f41 --- /dev/null +++ b/scripts/shellcheck-files.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail + +if (($# > 0)); then + roots=("$@") +else + roots=(tests/e2e tests/release scripts assets/demo .githooks) +fi + +for root in "${roots[@]}"; do + [[ -d "$root" ]] || { + printf 'shellcheck-files: missing search root: %s\n' "$root" >&2 + exit 1 + } +done + +all_files="$(mktemp)" +trap 'rm -f "$all_files"' EXIT +find "${roots[@]}" -type f -print0 >"$all_files" + +files=() +while IFS= read -r -d '' file; do + first='' + IFS= read -r first <"$file" || true + if [[ "$first" == '#!'* && ("$first" == *bash* || "$first" == */sh) ]]; then + files+=("$file") + fi +done <"$all_files" + +((${#files[@]} > 0)) || { + printf 'shellcheck-files: no shell files found\n' >&2 + exit 1 +} + +printf '%s\0' "${files[@]}" diff --git a/tests/release/shellcheck-coverage.test.sh b/tests/release/shellcheck-coverage.test.sh new file mode 100755 index 00000000..ad3ded1b --- /dev/null +++ b/tests/release/shellcheck-coverage.test.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$repo_root" + +fail() { + printf 'FAIL: %s\n' "$*" >&2 + exit 1 +} + +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT + +scripts/shellcheck-files.sh >"$tmp/scanned" +mapfile -d '' scanned <"$tmp/scanned" +((${#scanned[@]} > 150)) || fail "shellcheck scan covers only ${#scanned[@]} files" + +declare -A scanned_set=() +for file in "${scanned[@]}"; do + scanned_set["$file"]=1 +done + +missing=() +tracked_shell=0 +git ls-files -z >"$tmp/tracked" +while IFS= read -r -d '' file; do + [[ -f "$file" ]] || continue + first='' + IFS= read -r first <"$file" || true + is_shell=false + if [[ "$file" == *.sh ]]; then + is_shell=true + elif [[ "$first" == '#!'* && ("$first" == *bash* || "$first" == */sh) ]]; then + is_shell=true + fi + "$is_shell" || continue + tracked_shell=$((tracked_shell + 1)) + [[ -n "${scanned_set[$file]:-}" ]] || missing+=("$file") +done <"$tmp/tracked" + +((tracked_shell > 150)) || fail "tracked shell-file floor collapsed to $tracked_shell" +((${#missing[@]} == 0)) || fail "tracked shell files missing from ShellCheck scan: ${missing[*]}" +[[ -n "${scanned_set[.githooks/pre-commit]:-}" ]] || fail '.githooks/pre-commit is not ShellCheck-covered' +[[ -n "${scanned_set[.githooks/pre-push]:-}" ]] || fail '.githooks/pre-push is not ShellCheck-covered' + +if scripts/shellcheck-files.sh "$tmp/missing-root" >"$tmp/out" 2>"$tmp/err"; then + fail 'missing search root was accepted' +fi +grep -Fq 'missing search root:' "$tmp/err" || fail 'missing-root failure did not name the missing root' + +mkdir "$tmp/empty-root" +if scripts/shellcheck-files.sh "$tmp/empty-root" >"$tmp/out" 2>"$tmp/err"; then + fail 'empty search root was accepted' +fi +grep -Fq 'no shell files found' "$tmp/err" || fail 'empty-root failure did not explain the zero-file scan' + +printf 'ok: ShellCheck covers %d tracked shell files and refuses missing/empty roots\n' "$tracked_shell" From 430fc0f77b9ca9766b4f1a26abcfb660f204bc4d Mon Sep 17 00:00:00 2001 From: ITSMESB <131141975+ITSMERNB@users.noreply.github.com> Date: Tue, 15 Sep 2026 10:07:19 +0200 Subject: [PATCH 2/3] fix(ci): trigger ShellCheck for git hooks Signed-off-by: ITSMESB <131141975+ITSMERNB@users.noreply.github.com> --- .github/workflows/e2e.yml | 2 ++ scripts/shellcheck-files.sh | 9 ++++++++- tests/release/shellcheck-coverage.test.sh | 7 +++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 2edef3ba..d547384f 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -30,6 +30,7 @@ on: - "tests/release/**" - "scripts/**" - "assets/demo/**" + - ".githooks/**" - "apps/sysknife-cli/**" # scripts-lint builds sysknife-cli, which depends on every crate under # crates/. Without this the job could not be triggered by a change to the @@ -46,6 +47,7 @@ on: - "tests/release/**" - "scripts/**" - "assets/demo/**" + - ".githooks/**" - "apps/sysknife-cli/**" # scripts-lint builds sysknife-cli, which depends on every crate under # crates/. Without this the job could not be triggered by a change to the diff --git a/scripts/shellcheck-files.sh b/scripts/shellcheck-files.sh index d2849f41..44140b8b 100755 --- a/scripts/shellcheck-files.sh +++ b/scripts/shellcheck-files.sh @@ -1,10 +1,17 @@ #!/usr/bin/env bash set -euo pipefail +default_roots=(tests/e2e tests/release scripts assets/demo .githooks) + +if [[ "${1:-}" == "--print-roots" ]]; then + printf '%s\0' "${default_roots[@]}" + exit 0 +fi + if (($# > 0)); then roots=("$@") else - roots=(tests/e2e tests/release scripts assets/demo .githooks) + roots=("${default_roots[@]}") fi for root in "${roots[@]}"; do diff --git a/tests/release/shellcheck-coverage.test.sh b/tests/release/shellcheck-coverage.test.sh index ad3ded1b..cdeec746 100755 --- a/tests/release/shellcheck-coverage.test.sh +++ b/tests/release/shellcheck-coverage.test.sh @@ -44,6 +44,13 @@ done <"$tmp/tracked" [[ -n "${scanned_set[.githooks/pre-commit]:-}" ]] || fail '.githooks/pre-commit is not ShellCheck-covered' [[ -n "${scanned_set[.githooks/pre-push]:-}" ]] || fail '.githooks/pre-push is not ShellCheck-covered' +scripts/shellcheck-files.sh --print-roots >"$tmp/roots" +while IFS= read -r -d '' root; do + trigger="$root/**" + trigger_count="$(grep -F -c -- "- \"$trigger\"" .github/workflows/e2e.yml || true)" + ((trigger_count == 2)) || fail "ShellCheck root $root must trigger both push and pull_request e2e jobs" +done <"$tmp/roots" + if scripts/shellcheck-files.sh "$tmp/missing-root" >"$tmp/out" 2>"$tmp/err"; then fail 'missing search root was accepted' fi From 65b6e6189796f577220efac1c36148f7a11fad38 Mon Sep 17 00:00:00 2001 From: ITSMESB <131141975+ITSMERNB@users.noreply.github.com> Date: Tue, 15 Sep 2026 19:46:53 +0200 Subject: [PATCH 3/3] fix(ci): keep ShellCheck coverage non-vacuous Signed-off-by: ITSMESB <131141975+ITSMERNB@users.noreply.github.com> --- scripts/ci-local.sh | 8 +++++--- tests/release/shellcheck-coverage.test.sh | 8 ++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/scripts/ci-local.sh b/scripts/ci-local.sh index 0ec09919..4f60253e 100755 --- a/scripts/ci-local.sh +++ b/scripts/ci-local.sh @@ -264,9 +264,11 @@ hygiene_yamllint() ( # since every script this task adds/touches must stay shellcheck-clean. hygiene_shellcheck() ( cd "$repo_root" || exit 1 - find tests/e2e tests/release scripts assets/demo \ - -type f -name '*.sh' -print0 \ - | xargs -0 shellcheck --severity=warning + file_list="$(mktemp)" + trap 'rm -f "$file_list"' EXIT + scripts/shellcheck-files.sh >"$file_list" || exit 1 + mapfile -d '' files <"$file_list" + shellcheck --severity=warning "${files[@]}" ) run_shell_tests() { diff --git a/tests/release/shellcheck-coverage.test.sh b/tests/release/shellcheck-coverage.test.sh index cdeec746..bce0ab35 100755 --- a/tests/release/shellcheck-coverage.test.sh +++ b/tests/release/shellcheck-coverage.test.sh @@ -45,11 +45,19 @@ done <"$tmp/tracked" [[ -n "${scanned_set[.githooks/pre-push]:-}" ]] || fail '.githooks/pre-push is not ShellCheck-covered' scripts/shellcheck-files.sh --print-roots >"$tmp/roots" +roots_seen=0 while IFS= read -r -d '' root; do + roots_seen=$((roots_seen + 1)) trigger="$root/**" trigger_count="$(grep -F -c -- "- \"$trigger\"" .github/workflows/e2e.yml || true)" ((trigger_count == 2)) || fail "ShellCheck root $root must trigger both push and pull_request e2e jobs" done <"$tmp/roots" +((roots_seen > 0)) || fail 'shellcheck-files.sh --print-roots produced no roots' + +grep -Fq 'scripts/shellcheck-files.sh >"$file_list"' scripts/ci-local.sh || fail 'ci-local shellcheck must use scripts/shellcheck-files.sh' +if grep -Fq -- "-name '*.sh'" scripts/ci-local.sh; then + fail "ci-local shellcheck must not maintain its own '*.sh' find" +fi if scripts/shellcheck-files.sh "$tmp/missing-root" >"$tmp/out" 2>"$tmp/err"; then fail 'missing search root was accepted'