From 04e6e2e3cb02d7f8212b3dff2edaaf4a07fed7a5 Mon Sep 17 00:00:00 2001 From: MRegra Date: Sun, 27 Sep 2026 08:22:24 +0200 Subject: [PATCH 1/3] ci: add shellcheck, gitleaks and script smoke-test workflow The repo is public with no CI, and both systemd units failed with status=203/EXEC (F-0024) after scripts moved into subdirectories, because ExecStart pointed at a path that no longer existed. Add .github/workflows/ci.yml running, on push and pull_request: shellcheck over every *.sh, a gitleaks secret scan, and a smoke job (bash -n plus scripts/test-smoke.sh). The smoke test guards the 203/EXEC failure class: it asserts each documented entrypoint exists, has a shebang, parses, and answers --help with exit 0 without sudo. To make entrypoints smoke-testable, add safe --help handling to all six shell entrypoints (no behavioural change otherwise), and clear the three pre-existing shellcheck info findings (array in nightly-launch; justified SC2086/SC2009 disables). Actions are pinned to full SHAs. Co-Authored-By: Claude Opus 4.8 Fleet-Item: F-0600 Co-Authored-By: Claude --- .github/workflows/ci.yml | 46 ++++++++++++++ README.md | 18 ++++++ bug-bounty-agent/install-tools.sh | 13 ++++ bug-bounty-agent/nightly-launch.sh | 20 +++++- fedora-maintenance/fedora-maintenance.sh | 7 +++ scripts/test-smoke.sh | 78 ++++++++++++++++++++++++ soft-reboot/soft-reboot.sh | 22 +++++++ system-audit/system-audit.sh | 20 ++++++ system-health/system-health-check.sh | 14 +++++ 9 files changed, 235 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 scripts/test-smoke.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bc6123e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: CI + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + shellcheck: + name: shellcheck + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Run shellcheck on all shell scripts + run: | + find . -type f -name '*.sh' -not -path './.git/*' -print0 \ + | xargs -0 --no-run-if-empty shellcheck + + gitleaks: + name: gitleaks + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + - name: Scan for committed secrets + uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2.3.9 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + smoke: + name: script smoke test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Syntax check every script (bash -n) + run: | + find . -type f -name '*.sh' -not -path './.git/*' -print0 \ + | xargs -0 --no-run-if-empty -n1 bash -n + - name: Smoke each entrypoint (--help, no sudo) + run: bash scripts/test-smoke.sh diff --git a/README.md b/README.md index 0469887..7143c8c 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,21 @@ sudo ln -s /home/mregra/dev/fedora-workstation-scripts/system-health/system-heal ``` Then follow the per-project READMEs for configuration and automation details. + +## Continuous integration + +Every push and pull request runs `.github/workflows/ci.yml`: + +- **shellcheck** — static analysis of every `*.sh`. +- **gitleaks** — scans for committed secrets. +- **script smoke test** — `bash -n` on every script plus `scripts/test-smoke.sh`, + which runs each entrypoint with `--help` (no `sudo`, no side effects) and + asserts it exists and is runnable. This guards against the class of failure + where a moved or renamed script breaks a systemd unit's `ExecStart` + (`status=203/EXEC`). + +Every runnable script therefore supports `--help`. Run the smoke test locally with: + +```bash +bash scripts/test-smoke.sh +``` diff --git a/bug-bounty-agent/install-tools.sh b/bug-bounty-agent/install-tools.sh index 47972c5..a5a4e83 100755 --- a/bug-bounty-agent/install-tools.sh +++ b/bug-bounty-agent/install-tools.sh @@ -3,6 +3,19 @@ # Run as your normal user (not root) — tools install to ~/go/bin and ~/.local/bin. set -Eeuo pipefail +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + cat <<'EOF' +install-tools.sh — install security tooling for the bug-bounty orchestrator. + +Usage: + bash install-tools.sh Install Python deps, Go tools, nmap, sqlmap, etc. + bash install-tools.sh --help Show this help and exit. + +Run as your normal user (not root). Tools install to ~/go/bin and ~/.local/bin. +EOF + exit 0 +fi + say() { printf '\n=== %s ===\n' "$1"; } ok() { printf ' OK: %s\n' "$1"; } skip() { printf ' SKIP: %s (already installed)\n' "$1"; } diff --git a/bug-bounty-agent/nightly-launch.sh b/bug-bounty-agent/nightly-launch.sh index 6d4fd5a..4595f2c 100644 --- a/bug-bounty-agent/nightly-launch.sh +++ b/bug-bounty-agent/nightly-launch.sh @@ -9,9 +9,23 @@ # Output and logs go to ./output/ and ./nightly.log set -Eeuo pipefail +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + cat <<'EOF' +nightly-launch.sh — queue tonight's orchestrator run and detach it. + +Usage: + bash nightly-launch.sh scope.yaml + bash nightly-launch.sh scope.yaml --phases recon scan + bash nightly-launch.sh --help + +Output and logs go to ./output/ and ./nightly.log; PID is written to nightly.pid. +EOF + exit 0 +fi + SCOPE="${1:-scope.yaml}" shift || true -EXTRA_ARGS="${*}" +EXTRA_ARGS=("$@") LOG="nightly.log" PID_FILE="nightly.pid" @@ -32,11 +46,11 @@ if [[ -f "$PID_FILE" ]]; then rm -f "$PID_FILE" fi -echo "Starting overnight run: scope=$SCOPE extra=${EXTRA_ARGS:-none}" +echo "Starting overnight run: scope=$SCOPE extra=${EXTRA_ARGS[*]:-none}" echo "Log: $LOG" echo "Kill with: kill \$(cat $PID_FILE)" -nohup python3 orchestrator.py --scope "$SCOPE" $EXTRA_ARGS \ +nohup python3 orchestrator.py --scope "$SCOPE" "${EXTRA_ARGS[@]}" \ > "$LOG" 2>&1 & echo $! > "$PID_FILE" diff --git a/fedora-maintenance/fedora-maintenance.sh b/fedora-maintenance/fedora-maintenance.sh index 76d2326..bf5c074 100755 --- a/fedora-maintenance/fedora-maintenance.sh +++ b/fedora-maintenance/fedora-maintenance.sh @@ -198,6 +198,8 @@ cleanup_system() { say " OK (nothing to remove)" log "No old kernels to remove" else + # shellcheck disable=SC2086 # intentional word splitting: a space/newline + # separated list of kernel package names must expand to multiple args. if dnf -y remove $old_kernels >>"$LOG_FILE" 2>&1; then ok else @@ -316,6 +318,11 @@ main() { local mode="${1:-}" [[ -n "$mode" ]] || { usage; exit 1; } + if [[ "$mode" == "-h" || "$mode" == "--help" || "$mode" == "help" ]]; then + usage + exit 0 + fi + MODE="$mode" need_root "$@" mkdir -p "$(dirname "$LOG_FILE")" diff --git a/scripts/test-smoke.sh b/scripts/test-smoke.sh new file mode 100644 index 0000000..0841569 --- /dev/null +++ b/scripts/test-smoke.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# Smoke test for every shell entrypoint in this repository. +# +# Regression guard for F-0024 +# --------------------------- +# The `fedora-maintenance` and `system-health` systemd units failed with +# `status=203/EXEC` because their `ExecStart=` pointed at a script path that no +# longer existed after the scripts were reorganised into per-tool subdirectories +# (`fedora-maintenance.sh` -> `fedora-maintenance/fedora-maintenance.sh`). +# +# `203/EXEC` means "the executable could not be run": the file is missing, has no +# executable bit, or has no valid interpreter. This test reproduces that failure +# class by asserting, for each documented entrypoint, that it: +# * exists at its canonical path, +# * starts with a shebang, +# * parses cleanly (`bash -n`), and +# * answers `--help` with exit code 0 without root and without side effects. +# +# If a script is ever moved or renamed without updating its callers, or an +# entrypoint loses its `--help` contract, this test goes red. +# +# Usage: bash scripts/test-smoke.sh +set -uo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" || exit 2 + +# Canonical entrypoints — the paths referenced by the READMEs and the systemd +# unit / cron examples. Keep this list in sync when adding a runnable script. +ENTRYPOINTS=( + "fedora-maintenance/fedora-maintenance.sh" + "system-health/system-health-check.sh" + "soft-reboot/soft-reboot.sh" + "system-audit/system-audit.sh" + "bug-bounty-agent/nightly-launch.sh" + "bug-bounty-agent/install-tools.sh" +) + +fail=0 +pass() { printf 'PASS %s\n' "$1"; } +bad() { printf 'FAIL %s\n' "$1"; fail=1; } + +for script in "${ENTRYPOINTS[@]}"; do + if [[ ! -f "$script" ]]; then + bad "$script: missing (F-0024 class: an ExecStart target would 203/EXEC)" + continue + fi + + if ! head -n 1 "$script" | grep -q '^#!'; then + bad "$script: no shebang (would 203/EXEC under systemd)" + continue + fi + + if ! bash -n "$script"; then + bad "$script: bash -n reported a syntax error" + continue + fi + + # A missing executable bit is the literal cause of 203/EXEC when the script is + # symlinked into /usr/local/bin and invoked by systemd. Warn (do not fail): the + # README documents `chmod +x`, and CI cannot assert the committed file mode. + if [[ ! -x "$script" ]]; then + printf 'WARN %s: not marked executable (run: chmod +x %s)\n' "$script" "$script" + fi + + if bash "$script" --help >/dev/null 2>&1; then + pass "$script: --help exits 0" + else + bad "$script: --help did not exit 0 (not smoke-testable without root)" + fi +done + +echo "----" +if [[ "$fail" -ne 0 ]]; then + echo "SMOKE TEST FAILED" + exit 1 +fi +echo "ALL SMOKE TESTS PASSED" diff --git a/soft-reboot/soft-reboot.sh b/soft-reboot/soft-reboot.sh index bb9e199..e7a2632 100755 --- a/soft-reboot/soft-reboot.sh +++ b/soft-reboot/soft-reboot.sh @@ -19,6 +19,23 @@ CURRENT_STEP="" command_exists() { command -v "$1" >/dev/null 2>&1; } +usage() { + cat <<'EOF' +soft-reboot.sh — replicate common reboot cleanup effects without rebooting. + +Usage: + sudo ./soft-reboot.sh Flush buffers, drop caches, compact swap, restart + stale services, clean /tmp, report reboot status. + ./soft-reboot.sh --help Show this help and exit. + +Env vars: + TMP_AGE_DAYS=7 Remove stale /tmp files older than N days. + COMPACT_SWAP=auto auto|yes|no — auto only compacts if swap > 20% used. + DROP_CACHES=1 1=page cache, 2=dentries+inodes, 3=all (1 is safest). + NOTIFY_USER=you Desktop notification target (optional). +EOF +} + need_root() { [[ "${EUID:-$(id -u)}" -eq 0 ]] || { say "Run as root: sudo $0"; exit 1; } } @@ -206,6 +223,11 @@ reboot_still_needed() { } main() { + if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 + fi + need_root mkdir -p "$(dirname "$LOG_FILE")" touch "$LOG_FILE" diff --git a/system-audit/system-audit.sh b/system-audit/system-audit.sh index 8c95912..0da712a 100755 --- a/system-audit/system-audit.sh +++ b/system-audit/system-audit.sh @@ -402,6 +402,8 @@ sec_suspicious_procs() { # Processes with suspicious command lines (base64, curl piped to bash, etc.) if $IS_ROOT; then local suspicious_cmds + # shellcheck disable=SC2009 # need full `ps aux` command-line text to regex-match + # attack patterns (pipes, redirects); pgrep matches process names only. suspicious_cmds="$(ps aux 2>/dev/null \ | grep -E 'base64.*decode|curl.*[|].*bash|wget.*[|].*bash|bash.*-i.*>&|/dev/tcp|/dev/shm' \ | grep -v grep || true)" @@ -688,7 +690,25 @@ summary() { # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- +print_usage() { + cat <<'EOF' +system-audit.sh — read-only system performance and security audit. + +Usage: + ./system-audit.sh Run the audit (diffs against a saved baseline). + ./system-audit.sh --save-baseline Run the audit, then save current state as the baseline. + ./system-audit.sh --help Show this help and exit. + +Runs as either root (full scan) or an unprivileged user (reduced scan). +EOF +} + main() { + if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + print_usage + exit 0 + fi + mkdir -p "$LOG_DIR" touch "$LOG_FILE" _log_plain "SYSTEM AUDIT — $(date) — host: $(hostname)" diff --git a/system-health/system-health-check.sh b/system-health/system-health-check.sh index 13bd9d8..e3ac8ec 100755 --- a/system-health/system-health-check.sh +++ b/system-health/system-health-check.sh @@ -1,5 +1,19 @@ #!/bin/bash +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + cat <<'EOF' +system-health-check.sh — warn on memory pressure via a desktop notification. + +Usage: + system-health-check Check swap/RAM, notify if thresholds exceeded. + system-health-check --help Show this help and exit. + +Triggers a notification when swap used > 1500 MB or available RAM < 800 MB. +Requires libnotify (notify-send) for the notification. +EOF + exit 0 +fi + SWAP_USED=$(free -m | awk '/Swap:/ {print $3}') RAM_AVAIL=$(free -m | awk '/Mem:/ {print $7}') From bb6fa4806ff161c030d47314013a53dd1782ac15 Mon Sep 17 00:00:00 2001 From: MRegra Date: Sun, 27 Sep 2026 11:30:08 +0200 Subject: [PATCH 2/3] ci: fix shellcheck red, harden F-0024 smoke test, add actionlint The runner's shellcheck (ubuntu-24.04 apt, 0.9.x) flags SC2015 on `[[ -t 1 ]] && printf || true` in system-audit.sh, which local 0.11 does not, so the shellcheck job was red on the PR head. Rewrite it as an if, pin the runner image and print the shellcheck version. The smoke test now reads the symlink and ExecStart paths from the READMEs and fails (not warns) when a symlinked script is missing, has no shebang or is not committed as 100755: the actual 203/EXEC causes behind F-0024. A self-test proves it goes red on each failure mode. Push CI runs only on main to avoid duplicate runs, and actionlint lints the workflow. Co-Authored-By: Claude Opus 5.5 Fleet-Item: F-0600 Co-Authored-By: Claude --- .github/workflows/ci.yml | 29 ++++++-- README.md | 24 ++++--- scripts/test-smoke-selftest.sh | 89 ++++++++++++++++++++++++ scripts/test-smoke.sh | 122 +++++++++++++++++++++++++-------- system-audit/system-audit.sh | 2 +- 5 files changed, 222 insertions(+), 44 deletions(-) create mode 100644 scripts/test-smoke-selftest.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc6123e..0a463ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,7 @@ name: CI on: push: + branches: [main] pull_request: permissions: @@ -10,18 +11,34 @@ permissions: jobs: shellcheck: name: shellcheck - runs-on: ubuntu-latest + # Pinned runner image: shellcheck comes from its apt repo, so the image + # decides the version (ubuntu-24.04 ships 0.9.x, stricter on SC2015 than + # 0.11). Moving to a newer image is a deliberate change. + runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Show shellcheck version + run: shellcheck --version - name: Run shellcheck on all shell scripts run: | find . -type f -name '*.sh' -not -path './.git/*' -print0 \ - | xargs -0 --no-run-if-empty shellcheck + | xargs -0 --no-run-if-empty shellcheck --severity=style + + actionlint: + name: actionlint + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Lint workflows + # `go run module@version` fetches a fixed release, verified against the + # Go checksum database (sum.golang.org). + run: go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.7 -color gitleaks: name: gitleaks - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -34,7 +51,7 @@ jobs: smoke: name: script smoke test - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -42,5 +59,7 @@ jobs: run: | find . -type f -name '*.sh' -not -path './.git/*' -print0 \ | xargs -0 --no-run-if-empty -n1 bash -n - - name: Smoke each entrypoint (--help, no sudo) + - name: Smoke test self-test (goes red on each F-0024 failure mode) + run: bash scripts/test-smoke-selftest.sh + - name: Smoke each entrypoint (--help, no sudo) and README symlink targets run: bash scripts/test-smoke.sh diff --git a/README.md b/README.md index 7143c8c..31451cb 100644 --- a/README.md +++ b/README.md @@ -30,18 +30,26 @@ Then follow the per-project READMEs for configuration and automation details. ## Continuous integration -Every push and pull request runs `.github/workflows/ci.yml`: +Every push to `main` and every pull request runs `.github/workflows/ci.yml`: -- **shellcheck** — static analysis of every `*.sh`. +- **shellcheck** — static analysis of every `*.sh` (the runner's shellcheck is + older than Fedora's and a little stricter; the version is printed in the log). +- **actionlint** — lints the workflow files. - **gitleaks** — scans for committed secrets. - **script smoke test** — `bash -n` on every script plus `scripts/test-smoke.sh`, - which runs each entrypoint with `--help` (no `sudo`, no side effects) and - asserts it exists and is runnable. This guards against the class of failure - where a moved or renamed script breaks a systemd unit's `ExecStart` - (`status=203/EXEC`). - -Every runnable script therefore supports `--help`. Run the smoke test locally with: + which guards against the systemd `status=203/EXEC` failure (a moved, renamed + or non-executable script behind a unit's `ExecStart`). It reads the symlink + lines and `ExecStart=` lines from the READMEs and fails if a symlinked script + is missing, has no shebang or is not committed as executable (`100755`), or if + an `ExecStart` binary is not created by any README symlink. It also runs every + `*.sh` outside `scripts/` with `--help` (no `sudo`, no side effects). + `scripts/test-smoke-selftest.sh` proves the smoke test goes red on each of + those failures. + +Every runnable script therefore supports `--help`. When you move a script, +update its README symlink line in the same change. Run the checks locally with: ```bash bash scripts/test-smoke.sh +bash scripts/test-smoke-selftest.sh ``` diff --git a/scripts/test-smoke-selftest.sh b/scripts/test-smoke-selftest.sh new file mode 100644 index 0000000..55df58b --- /dev/null +++ b/scripts/test-smoke-selftest.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# Self-test for scripts/test-smoke.sh: proves it goes red on each F-0024 +# failure mode (systemd `status=203/EXEC`) and stays green on a healthy tree. +# Each case builds a throwaway fixture tree in a temp dir and runs the smoke +# test against it with SMOKE_ROOT. +# +# Usage: bash scripts/test-smoke-selftest.sh +set -uo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SMOKE="$HERE/test-smoke.sh" +WORK="$(mktemp -d)" +trap 'rm -rf -- "$WORK"' EXIT + +REPO="/home/user/dev/fedora-workstation-scripts" +status=0 + +# write_tool : a minimal script that honours --help. +write_tool() { + mkdir -p "$1/$(dirname "$2")" + cat >"$1/$2" <<'EOF' +#!/usr/bin/env bash +if [[ "${1:-}" == "--help" ]]; then echo usage; exit 0; fi +exit 1 +EOF + chmod "$3" "$1/$2" +} + +# write_readme +write_readme() { + cat >"$1/README.md" < +expect() { + local name="$1" want="$2" pattern="$3" out rc + out="$(SMOKE_ROOT="$WORK/$name" bash "$SMOKE" 2>&1)"; rc=$? + if [[ "$want" == pass && "$rc" -eq 0 ]] \ + || [[ "$want" == fail && "$rc" -ne 0 && ( -z "$pattern" || "$out" == *"$pattern"* ) ]]; then + printf 'ok %s (exit %d)\n' "$name" "$rc" + else + printf 'NOT OK %s: wanted %s (output matching "%s"), got exit %d\n%s\n' \ + "$name" "$want" "$pattern" "$rc" "$out" + status=1 + fi +} + +# healthy: symlink target exists, is 755, answers --help, ExecStart is linked. +write_tool "$WORK/healthy" tool/tool.sh 755 +write_readme "$WORK/healthy" tool/tool.sh tool tool +expect healthy pass '' + +# moved: the README still points at the old path (the literal F-0024 cause). +write_tool "$WORK/moved" tool/tool.sh 755 +write_readme "$WORK/moved" tool.sh tool tool +expect moved fail 'tool.sh: missing' + +# noexec: the symlinked script lost its executable bit. +write_tool "$WORK/noexec" tool/tool.sh 644 +write_readme "$WORK/noexec" tool/tool.sh tool tool +expect noexec fail 'not committed as executable' + +# unlinked: ExecStart runs a binary no README ln -s line creates. +write_tool "$WORK/unlinked" tool/tool.sh 755 +write_readme "$WORK/unlinked" tool/tool.sh tool other-tool +expect unlinked fail 'no README' + +# nohelp: an entrypoint that does not answer --help. +write_tool "$WORK/nohelp" tool/tool.sh 755 +write_readme "$WORK/nohelp" tool/tool.sh tool tool +mkdir -p "$WORK/nohelp/other" +printf '#!/usr/bin/env bash\nexit 3\n' >"$WORK/nohelp/other/other.sh" +expect nohelp fail 'other/other.sh: --help did not exit 0' + +echo "----" +if [[ "$status" -ne 0 ]]; then + echo "SMOKE SELF-TEST FAILED" + exit 1 +fi +echo "SMOKE SELF-TEST PASSED" diff --git a/scripts/test-smoke.sh b/scripts/test-smoke.sh index 0841569..351932f 100644 --- a/scripts/test-smoke.sh +++ b/scripts/test-smoke.sh @@ -9,45 +9,108 @@ # (`fedora-maintenance.sh` -> `fedora-maintenance/fedora-maintenance.sh`). # # `203/EXEC` means "the executable could not be run": the file is missing, has no -# executable bit, or has no valid interpreter. This test reproduces that failure -# class by asserting, for each documented entrypoint, that it: -# * exists at its canonical path, -# * starts with a shebang, -# * parses cleanly (`bash -n`), and -# * answers `--help` with exit code 0 without root and without side effects. -# -# If a script is ever moved or renamed without updating its callers, or an -# entrypoint loses its `--help` contract, this test goes red. +# executable bit, or has no valid interpreter. The units run the scripts through +# the symlinks the READMEs tell you to create (`ln -s / /usr/local/bin/`), +# so this test reads those paths from the READMEs themselves, not from a list kept +# here. It fails when: +# * a README `ln -s` source path does not exist in the repo (script moved/renamed), +# * that symlinked script has no shebang or is not committed as executable (100755), +# * a README `ExecStart=` runs a `/usr/local/bin/` (or `~/.local/bin/`) +# that no README `ln -s` line creates, +# * any entrypoint (every `*.sh` outside `scripts/`) fails `bash -n` or does not +# exit 0 on `--help` without root. # # Usage: bash scripts/test-smoke.sh +# SMOKE_ROOT= bash scripts/test-smoke.sh # check another tree (self-test) set -uo pipefail -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ROOT="${SMOKE_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" cd "$ROOT" || exit 2 -# Canonical entrypoints — the paths referenced by the READMEs and the systemd -# unit / cron examples. Keep this list in sync when adding a runnable script. -ENTRYPOINTS=( - "fedora-maintenance/fedora-maintenance.sh" - "system-health/system-health-check.sh" - "soft-reboot/soft-reboot.sh" - "system-audit/system-audit.sh" - "bug-bounty-agent/nightly-launch.sh" - "bug-bounty-agent/install-tools.sh" -) +REPO_MARKER="/fedora-workstation-scripts/" fail=0 pass() { printf 'PASS %s\n' "$1"; } bad() { printf 'FAIL %s\n' "$1"; fail=1; } -for script in "${ENTRYPOINTS[@]}"; do - if [[ ! -f "$script" ]]; then - bad "$script: missing (F-0024 class: an ExecStart target would 203/EXEC)" +# Committed mode wins over the working-tree mode: the file mode is what a fresh +# clone (and therefore the owner's /usr/local/bin symlink) gets. +is_executable() { + local f="$1" mode="" + if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + mode="$(git ls-files -s -- "$f" | awk '{print $1}')" + fi + if [[ -n "$mode" ]]; then + [[ "$mode" == "100755" ]] + else + [[ -x "$f" ]] + fi +} + +has_shebang() { head -n 1 "$1" | grep -q '^#!'; } + +# --------------------------------------------------------------------------- +# 1. Symlink targets documented in the READMEs (what systemd actually executes) +# --------------------------------------------------------------------------- +declare -A LINK_SRC=() # link name -> repo-relative script path +while read -r src dst; do + [[ "$src" == *"$REPO_MARKER"* ]] || continue + rel="${src#*"$REPO_MARKER"}" + name="$(basename "$dst")" + if [[ -n "${LINK_SRC[$name]:-}" && "${LINK_SRC[$name]}" != "$rel" ]]; then + bad "READMEs link '$name' to two different scripts: ${LINK_SRC[$name]} and $rel" continue fi + LINK_SRC["$name"]="$rel" +done < <(grep -rhoE --include='*.md' --exclude-dir=.git 'ln -s +[^ ]+ +[^ ]+' . \ + | awk '{print $(NF-1), $NF}') + +if [[ "${#LINK_SRC[@]}" -eq 0 ]]; then + bad "no README 'ln -s /