diff --git a/.github/scripts/actionlint-all.sh b/.github/scripts/actionlint-all.sh new file mode 100755 index 0000000..f1b1941 --- /dev/null +++ b/.github/scripts/actionlint-all.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(git rev-parse --show-toplevel)" + +mapfile -t files < <( + { + git ls-files '.github/workflows/*.yml' + git ls-files 'actions/*/action.yml' + } | sort -u +) + +[ "${#files[@]}" -gt 0 ] || { + echo "actionlint-all: found no workflow or action files — the sweep is broken" >&2 + exit 1 +} + +printf 'actionlint: linting %d files\n' "${#files[@]}" +printf ' %s\n' "${files[@]}" +actionlint "${files[@]}" + diff --git a/.github/scripts/shellcheck-all.sh b/.github/scripts/shellcheck-all.sh new file mode 100755 index 0000000..ded9a59 --- /dev/null +++ b/.github/scripts/shellcheck-all.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail + +# A glob can quietly stop guarding when a script moves under a dot-directory +# or loses its extension. Derive the lint set from the tracked tree instead, +# following cast#118's fix, and print it so a shrinking sweep is visible. +cd "$(git rev-parse --show-toplevel)" + +mapfile -t files < <( + { + git ls-files '*.sh' + git ls-files | while IFS= read -r file; do + case "$file" in *.sh) continue ;; esac + [ -f "$file" ] || continue + IFS= read -r line <"$file" || [ -n "$line" ] || continue + case "$line" in '#!'*) ;; *) continue ;; esac + interpreter="${line#\#!}" + interpreter="${interpreter%% -*}" + interpreter="${interpreter##*[ /]}" + case "$interpreter" in sh | bash | dash | ksh) printf '%s\n' "$file" ;; esac + done + } | sort -u +) + +[ "${#files[@]}" -gt 0 ] || { + echo "shellcheck-all: found no shell scripts — the sweep is broken" >&2 + exit 1 +} + +printf 'shellcheck: linting %d tracked scripts\n' "${#files[@]}" +printf ' %s\n' "${files[@]}" +shellcheck -x ${files[@]} + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6c14ad8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Shellcheck + run: bash .github/scripts/shellcheck-all.sh + - name: Install actionlint + env: + ACTIONLINT_VERSION: 1.7.12 + run: | + curl -fsSLo actionlint.tar.gz \ + "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_x86_64.tar.gz" + tar -xzf actionlint.tar.gz actionlint + sudo install actionlint /usr/local/bin/actionlint + - name: Actionlint + run: bash .github/scripts/actionlint-all.sh + - name: Tests + run: bash test/run.sh + diff --git a/actions/.gitkeep b/actions/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/actions/.gitkeep @@ -0,0 +1 @@ + diff --git a/docs/.gitkeep b/docs/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/docs/.gitkeep @@ -0,0 +1 @@ + diff --git a/lib/.gitkeep b/lib/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lib/.gitkeep @@ -0,0 +1 @@ + diff --git a/test/harness.sh b/test/harness.sh new file mode 100644 index 0000000..00e05f6 --- /dev/null +++ b/test/harness.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Sourced assertion helpers. Tests deliberately use set -u, not set -e: +# failing commands are behavior for the harness to inspect. + +PASS=0 +FAIL=0 + +# check +check() { + local desc="$1" want="$2" substring="$3" + shift 3 + local output rc + + output="$("$@" 2>&1)" + rc=$? + if [ "$rc" -ne "$want" ]; then + echo "FAIL: $desc — exit $rc, wanted $want" + printf '%s\n' "$output" | sed 's/^/ /' + FAIL=$((FAIL + 1)) + return + fi + if [ -n "$substring" ] && ! printf '%s' "$output" | grep -qF -e "$substring"; then + echo "FAIL: $desc — output missing '$substring'" + printf '%s\n' "$output" | sed 's/^/ /' + FAIL=$((FAIL + 1)) + return + fi + echo "ok: $desc" + PASS=$((PASS + 1)) +} + +summary() { + printf '%d passed, %d failed\n' "$PASS" "$FAIL" + [ "$FAIL" -eq 0 ] +} + diff --git a/test/run.sh b/test/run.sh new file mode 100755 index 0000000..2529021 --- /dev/null +++ b/test/run.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +shopt -s nullglob +tests=("$ROOT"/test/*.test.sh) + +printf 'test: discovered %d test files\n' "${#tests[@]}" +if [ "${#tests[@]}" -eq 0 ]; then + echo "test: 0 tests — nothing to run" + exit 0 +fi + +passed=0 +failed=0 +for test_file in "${tests[@]}"; do + printf '\n==> %s\n' "${test_file#"$ROOT"/}" + if bash "$test_file"; then + passed=$((passed + 1)) + else + failed=$((failed + 1)) + fi +done + +printf '\ntest files: %d passed, %d failed\n' "$passed" "$failed" +[ "$failed" -eq 0 ] +