diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7435ecf --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,60 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + syntax: + name: Bash syntax check + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Validate syntax with bash -n + run: | + set -euo pipefail + status=0 + while IFS= read -r -d '' file; do + printf '==> %s\n' "$file" + if ! bash -n "$file"; then + status=1 + fi + done < <(find openscripts.sh scripts -type f -name '*.sh' -print0) + exit "$status" + + shellcheck: + name: ShellCheck + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install ShellCheck + run: | + sudo apt-get update + sudo apt-get install -y shellcheck + + - name: Run ShellCheck on all scripts + run: | + set -euo pipefail + mapfile -d '' files < <(find openscripts.sh scripts -type f -name '*.sh' -print0) + shellcheck --severity=warning "${files[@]}" + + bats: + name: Bats unit tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install bats + run: | + sudo apt-get update + sudo apt-get install -y bats + + - name: Run bats tests + run: bats tests diff --git a/tests/dispatcher.bats b/tests/dispatcher.bats new file mode 100644 index 0000000..8b6af02 --- /dev/null +++ b/tests/dispatcher.bats @@ -0,0 +1,87 @@ +#!/usr/bin/env bats + +# Smoke tests for the openscripts.sh dispatcher. + +setup() { + REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/.." && pwd)" + DISPATCHER="$REPO_ROOT/openscripts.sh" +} + +@test "dispatcher exists and is executable" { + [ -x "$DISPATCHER" ] +} + +@test "no arguments prints usage and exits non-zero" { + run "$DISPATCHER" + [ "$status" -ne 0 ] + [[ "$output" == *"Usage:"* ]] + [[ "$output" == *"Available commands:"* ]] +} + +@test "help subcommand prints usage and exits zero" { + run "$DISPATCHER" help + [ "$status" -eq 0 ] + [[ "$output" == *"Usage:"* ]] + [[ "$output" == *"Available commands:"* ]] +} + +@test "--help flag prints usage and exits zero" { + run "$DISPATCHER" --help + [ "$status" -eq 0 ] + [[ "$output" == *"Usage:"* ]] +} + +@test "-h flag prints usage and exits zero" { + run "$DISPATCHER" -h + [ "$status" -eq 0 ] + [[ "$output" == *"Usage:"* ]] +} + +@test "unknown command prints error and exits non-zero" { + run "$DISPATCHER" definitely-not-a-real-command + [ "$status" -ne 0 ] + [[ "$output" == *"unknown command"* ]] +} + +@test "every registered command points to an existing executable script" { + while IFS='|' read -r name path desc; do + [ -n "$name" ] || continue + [ -f "$REPO_ROOT/$path" ] || { + echo "Missing script for '$name': $path" + return 1 + } + [ -x "$REPO_ROOT/$path" ] || { + echo "Script for '$name' is not executable: $path" + return 1 + } + done < <( + awk ' + /^COMMANDS=\(/ { in_block = 1; next } + in_block && /^\)/ { exit } + in_block { + line = $0 + sub(/^[[:space:]]*"/, "", line) + sub(/"[[:space:]]*$/, "", line) + if (length(line) > 0) print line + } + ' "$DISPATCHER" + ) +} + +@test "every script under scripts/ starts with a valid shell shebang" { + while IFS= read -r -d '' file; do + head -n 1 "$file" | grep -Eq '^#!(/bin/bash|/usr/bin/env bash|/bin/sh|/usr/bin/env sh)' || { + echo "Missing or invalid shebang in $file" + return 1 + } + done < <(find "$REPO_ROOT/scripts" -type f -name '*.sh' -print0) +} + +@test "every script under scripts/ is executable" { + while IFS= read -r -d '' file; do + [ -x "$file" ] || { + echo "Script is not executable: $file" + return 1 + } + done < <(find "$REPO_ROOT/scripts" -type f -name '*.sh' -print0) +}