2026-07-10 20:40:07 +00:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
# Dependency-free CLI assertions. Run: bash test/cli.sh
|
|
|
|
|
# Deliberately no `set -e` — the harness asserts on failing commands.
|
|
|
|
|
set -u
|
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
|
|
|
PASS=0 FAIL=0
|
|
|
|
|
|
|
|
|
|
# check <desc> <want_exit> <want_substr> <cmd...>
|
|
|
|
|
# Runs cmd, asserts exit code and (if non-empty) that combined output
|
|
|
|
|
# contains want_substr.
|
|
|
|
|
check() {
|
|
|
|
|
local desc="$1" want="$2" substr="$3"; shift 3
|
|
|
|
|
local out rc
|
|
|
|
|
out="$("$@" 2>&1)"; rc=$?
|
|
|
|
|
if [ "$rc" -ne "$want" ]; then
|
|
|
|
|
echo "FAIL: $desc — exit $rc, wanted $want"
|
|
|
|
|
printf '%s\n' "$out" | sed 's/^/ /'
|
|
|
|
|
FAIL=$((FAIL + 1)); return
|
|
|
|
|
fi
|
2026-07-10 20:53:32 +00:00
|
|
|
if [ -n "$substr" ] && ! printf '%s' "$out" | grep -qF -e "$substr"; then
|
2026-07-10 20:40:07 +00:00
|
|
|
echo "FAIL: $desc — output missing '$substr'"
|
|
|
|
|
printf '%s\n' "$out" | sed 's/^/ /'
|
|
|
|
|
FAIL=$((FAIL + 1)); return
|
|
|
|
|
fi
|
|
|
|
|
echo "ok: $desc"; PASS=$((PASS + 1))
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-11 08:25:48 +00:00
|
|
|
check "no args shows usage, exit 2" 2 "usage:" "$ROOT/bin/rig"
|
|
|
|
|
check "--help exits 0" 0 "usage:" "$ROOT/bin/rig" --help
|
|
|
|
|
check "help exits 0" 0 "usage:" "$ROOT/bin/rig" help
|
|
|
|
|
check "unknown command exits 2" 2 "unknown command" "$ROOT/bin/rig" frobnicate
|
|
|
|
|
check "bare coolify shows usage, exit 2" 2 "usage:" "$ROOT/bin/rig" coolify
|
2026-07-10 20:40:07 +00:00
|
|
|
|
2026-07-10 20:43:24 +00:00
|
|
|
check "bootstrap: role required, exit 2" 2 "role required" "$ROOT/commands/bootstrap.sh"
|
|
|
|
|
check "bootstrap: --help exits 0" 0 "usage:" "$ROOT/commands/bootstrap.sh" --help
|
|
|
|
|
check "bootstrap: unknown role exits 2" 2 "unknown role" "$ROOT/commands/bootstrap.sh" potato
|
|
|
|
|
check "bootstrap: unknown flag exits 2" 2 "unknown flag" "$ROOT/commands/bootstrap.sh" workload --nope
|
|
|
|
|
check "bootstrap: hostname needs value" 2 "needs a value" "$ROOT/commands/bootstrap.sh" workload --hostname
|
2026-07-11 18:25:47 +00:00
|
|
|
check "bootstrap: runner refuses tag:server" 2 "must not advertise tag:server" "$ROOT/commands/bootstrap.sh" runner --ts-tag tag:server
|
2026-07-10 20:43:24 +00:00
|
|
|
if [ "$(id -u)" -ne 0 ]; then
|
|
|
|
|
check "bootstrap: refuses non-root" 1 "must run as root" env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" workload
|
2026-07-11 18:25:47 +00:00
|
|
|
check "bootstrap: runner role parses, refuses non-root" 1 "must run as root" env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" runner
|
2026-07-10 20:43:24 +00:00
|
|
|
else
|
2026-07-11 18:25:47 +00:00
|
|
|
echo "skip: bootstrap non-root refusals (running as root)"
|
2026-07-10 20:43:24 +00:00
|
|
|
fi
|
|
|
|
|
|
2026-07-10 20:50:41 +00:00
|
|
|
check "coolify: version required, exit 2" 2 "--version" "$ROOT/commands/coolify-install.sh"
|
|
|
|
|
check "coolify: --help exits 0" 0 "usage:" "$ROOT/commands/coolify-install.sh" --help
|
|
|
|
|
check "coolify: version needs value" 2 "needs a value" "$ROOT/commands/coolify-install.sh" --version
|
|
|
|
|
check "coolify: unknown flag exits 2" 2 "unknown flag" "$ROOT/commands/coolify-install.sh" --nope
|
|
|
|
|
if [ "$(id -u)" -ne 0 ]; then
|
|
|
|
|
check "coolify: refuses non-root" 1 "must run as root" "$ROOT/commands/coolify-install.sh" --version 4.1.2
|
|
|
|
|
else
|
|
|
|
|
echo "skip: coolify non-root refusal (running as root)"
|
|
|
|
|
fi
|
|
|
|
|
|
feat(coolify): install the control-plane dump as a systemd timer
The Coolify control-plane database holds the GitHub App private key, every
registered server's SSH key, and every environment value for every environment
it manages. Backing it up was a manual runbook step, and the dump script lived
in cast — the off-box tool, whose src never references it. It runs on the box,
as root, under a scheduler: that is rig's job description.
It matters beyond tidiness. The dump is forensics, not a restore path — a lost
control plane is rebuilt fresh and reconciled from the manifest. So there will
be a next control-plane box, and as a runbook step it was born un-backed-up,
depending on someone remembering mid-incident. Now it is backed up from birth.
rig installs the machinery and templates /etc/coolify-dump.env empty at 0600,
never reading it back — no credential passes through rig. The script's own
guards make an unfilled file fail the unit loudly rather than ship plaintext.
systemd timer over cron: EnvironmentFile is the right idiom for 0600 secrets,
failures surface in systemctl status instead of being mailed into the void, and
Persistent=true catches a run missed while the box was down.
Two hazards the cast script missed, carried into the unit:
- aws-cli >= 2.23 enables default upload checksums that S3-compatible backends
reject; Debian 13 ships 2.23.6, so the unit defaults both checksum knobs to
when_required.
- A failed pg_dump piped into age still yields a valid, tiny, encrypted file
that uploads cleanly every night and looks exactly like a working backup. The
script now refuses to upload an empty artifact.
Closes #8
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 19:14:07 +00:00
|
|
|
check "bare coolify backup shows usage, exit 2" 2 "usage:" "$ROOT/bin/rig" coolify backup
|
|
|
|
|
check "coolify backup: bad subcommand exits 2" 2 "usage:" "$ROOT/bin/rig" coolify backup frobnicate
|
|
|
|
|
check "coolify backup: --help exits 0" 0 "usage:" "$ROOT/commands/coolify-backup-install.sh" --help
|
|
|
|
|
check "coolify backup: schedule needs value" 2 "needs a value" "$ROOT/commands/coolify-backup-install.sh" --schedule
|
|
|
|
|
check "coolify backup: pg-container needs value" 2 "needs a value" "$ROOT/commands/coolify-backup-install.sh" --pg-container
|
|
|
|
|
check "coolify backup: unknown flag exits 2" 2 "unknown flag" "$ROOT/commands/coolify-backup-install.sh" --nope
|
|
|
|
|
if [ "$(id -u)" -ne 0 ]; then
|
|
|
|
|
check "coolify backup: refuses non-root" 1 "must run as root" "$ROOT/commands/coolify-backup-install.sh"
|
|
|
|
|
else
|
|
|
|
|
echo "skip: coolify backup non-root refusal (running as root)"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-07-11 17:41:09 +00:00
|
|
|
check "bare runner shows usage, exit 2" 2 "usage:" "$ROOT/bin/rig" runner
|
|
|
|
|
check "runner: --help exits 0" 0 "usage:" "$ROOT/commands/runner-install.sh" --help
|
|
|
|
|
check "runner: repo required, exit 2" 2 "--repo" "$ROOT/commands/runner-install.sh" --version 2.335.1
|
2026-07-11 18:44:43 +00:00
|
|
|
check "runner: version needs value" 2 "needs a value" "$ROOT/commands/runner-install.sh" --repo acme/widgets --version
|
2026-07-11 17:41:09 +00:00
|
|
|
check "runner: repo needs value" 2 "needs a value" "$ROOT/commands/runner-install.sh" --repo
|
|
|
|
|
check "runner: rejects bad repo slug" 2 "owner/repo" "$ROOT/commands/runner-install.sh" --repo not-a-slug --version 2.335.1
|
|
|
|
|
check "runner: refuses --user root" 2 "must not be root" "$ROOT/commands/runner-install.sh" --repo acme/widgets --version 2.335.1 --user root
|
|
|
|
|
check "runner: unknown flag exits 2" 2 "unknown flag" "$ROOT/commands/runner-install.sh" --nope
|
|
|
|
|
if [ "$(id -u)" -ne 0 ]; then
|
|
|
|
|
check "runner: refuses non-root" 1 "must run as root" env RUNNER_TOKEN=x "$ROOT/commands/runner-install.sh" --repo acme/widgets --version 2.335.1
|
|
|
|
|
else
|
|
|
|
|
echo "skip: runner non-root refusal (running as root)"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-07-11 19:37:48 +00:00
|
|
|
# Regression: /etc/os-release defines VERSION (e.g. "13 (trixie)" on Debian);
|
|
|
|
|
# sourcing it in the main shell clobbers a script's $VERSION and splices the
|
|
|
|
|
# OS string into download URLs. It must only ever be sourced in a subshell.
|
|
|
|
|
check "no main-shell os-release sourcing" 1 "" \
|
|
|
|
|
grep -rnE '^[[:space:]]*\.[[:space:]]+/etc/os-release' "$ROOT/commands"
|
|
|
|
|
|
2026-07-10 20:40:07 +00:00
|
|
|
echo "---"
|
|
|
|
|
echo "$PASS passed, $FAIL failed"
|
|
|
|
|
[ "$FAIL" -eq 0 ]
|