feat: rig CLI — bootstrap, coolify install, curl installer #1
7 changed files with 416 additions and 2 deletions
14
.github/workflows/ci.yml
vendored
Normal file
14
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
name: ci
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: shellcheck
|
||||||
|
run: shellcheck install.sh bin/rig commands/*.sh test/cli.sh
|
||||||
|
- name: cli tests
|
||||||
|
run: bash test/cli.sh
|
||||||
75
README.md
75
README.md
|
|
@ -1,2 +1,73 @@
|
||||||
# deployor
|
# rig
|
||||||
Box-plumbing CLI: curl-install on a pristine Debian server, bootstrap it into a hardened tailnet-joined node
|
|
||||||
|
A CLI that turns a **pristine Debian server into a hardened, tailnet-joined
|
||||||
|
node** — one curl, one command. A second command installs a version-pinned
|
||||||
|
Coolify on a control-plane box.
|
||||||
|
|
||||||
|
Philosophy (shared with [claudebox](https://github.com/heavy-duty/claudebox)):
|
||||||
|
**public tool, private state**. rig carries plumbing logic only — no
|
||||||
|
hostnames, no bindings, no secrets, nothing about *your* infrastructure. It
|
||||||
|
takes arguments, does its work, and stores no credential, ever.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/heavy-duty/rig/main/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Installs the tree to `~/.local/share/rig` and links `rig` onto your
|
||||||
|
PATH (`/usr/local/bin` when root). Re-run any time to upgrade.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### `rig bootstrap <control-plane|workload>`
|
||||||
|
|
||||||
|
Run as root on the fresh box (over SSH). Convergent — safe to re-run; a
|
||||||
|
second run changes nothing.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
rig bootstrap control-plane --hostname my-coolify-box
|
||||||
|
rig bootstrap workload --hostname my-prod-box
|
||||||
|
```
|
||||||
|
|
||||||
|
- `--hostname <name>` — tailnet hostname (default: the role name)
|
||||||
|
- `--ts-tag <tag>` — tailnet tag to advertise (default: `tag:server`)
|
||||||
|
|
||||||
|
What it does: installs `curl ca-certificates unattended-upgrades` (and
|
||||||
|
enables periodic unattended upgrades); writes an sshd hardening drop-in
|
||||||
|
(`PermitRootLogin prohibit-password`, `PasswordAuthentication no`); installs
|
||||||
|
tailscale and joins your tailnet.
|
||||||
|
|
||||||
|
**The pre-auth key:** provide it via the `TS_AUTHKEY` env var or type it at
|
||||||
|
the interactive prompt. Use a **single-use, tagged, short-expiry** key. It
|
||||||
|
lives in process memory only — rig never writes a credential to disk.
|
||||||
|
|
||||||
|
The two roles are identical today except the default hostname; they exist
|
||||||
|
because control-plane and workload boxes diverge over time, and because the
|
||||||
|
next command applies to exactly one of them.
|
||||||
|
|
||||||
|
### `rig coolify install --version <pin>`
|
||||||
|
|
||||||
|
Control-plane box only. Installs Coolify at exactly the pinned version with
|
||||||
|
`AUTOUPDATE=false` — your deploy tooling is verified against an API surface;
|
||||||
|
the platform must never move underneath it on its own. Upgrading is an
|
||||||
|
explicit re-run with a new pin. The pin is required; there is no default.
|
||||||
|
|
||||||
|
## What rig deliberately does NOT do
|
||||||
|
|
||||||
|
- **Provider firewalls** — Docker publishes ports past host firewalls, so
|
||||||
|
the real boundary is your cloud provider's firewall, configured outside
|
||||||
|
this tool.
|
||||||
|
- **Fetch your config** — boxes never receive repo credentials. Everything
|
||||||
|
rig needs arrives as arguments or an interactive prompt.
|
||||||
|
- **Manage deployments** — deploy manifests/executors are separate concerns.
|
||||||
|
(Planned: the `apply`/`diff` executor half joins rig as commands that
|
||||||
|
run on operator machines, never on boxes.)
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
`bash test/cli.sh` (dependency-free assertions) + shellcheck run in CI. The
|
||||||
|
end-to-end rehearsal is a throwaway VM/container: pristine Debian → install →
|
||||||
|
`bootstrap workload` with a real single-use key → assert the sshd drop-in,
|
||||||
|
tailnet join, and a no-op second run → destroy, remove the node from the
|
||||||
|
tailnet.
|
||||||
|
|
|
||||||
52
bin/rig
Executable file
52
bin/rig
Executable file
|
|
@ -0,0 +1,52 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
usage: rig <command> [args]
|
||||||
|
|
||||||
|
commands:
|
||||||
|
bootstrap <control-plane|workload> [--hostname <name>] [--ts-tag <tag>]
|
||||||
|
OS plumbing on a pristine Debian box: hardening, unattended-upgrades,
|
||||||
|
tailscale join. Prompts for a single-use tailnet pre-auth key
|
||||||
|
(TS_AUTHKEY env overrides the prompt). Run as root.
|
||||||
|
coolify install --version <pin>
|
||||||
|
Pinned Coolify install (AUTOUPDATE=false). Control-plane box only.
|
||||||
|
|
||||||
|
install/upgrade:
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/heavy-duty/rig/main/install.sh | bash
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd="${1:-}"
|
||||||
|
case "$cmd" in
|
||||||
|
bootstrap)
|
||||||
|
shift
|
||||||
|
exec "$ROOT/commands/bootstrap.sh" "$@"
|
||||||
|
;;
|
||||||
|
coolify)
|
||||||
|
shift
|
||||||
|
sub="${1:-}"
|
||||||
|
if [ "$sub" != "install" ]; then
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
exec "$ROOT/commands/coolify-install.sh" "$@"
|
||||||
|
;;
|
||||||
|
-h|--help|help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
"")
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf 'rig: unknown command: %s\n' "$cmd" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
108
commands/bootstrap.sh
Executable file
108
commands/bootstrap.sh
Executable file
|
|
@ -0,0 +1,108 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# rig bootstrap — OS plumbing for a pristine Debian box.
|
||||||
|
# Convergent: safe to re-run; a second run changes nothing.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
log() { printf 'rig-bootstrap: %s\n' "$*"; }
|
||||||
|
warn() { printf 'rig-bootstrap: WARNING: %s\n' "$*" >&2; }
|
||||||
|
die() { printf 'rig-bootstrap: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; }
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
usage: rig bootstrap <control-plane|workload> [--hostname <name>] [--ts-tag <tag>]
|
||||||
|
|
||||||
|
--hostname tailnet hostname (default: the role name)
|
||||||
|
--ts-tag tailnet tag to advertise (default: tag:server)
|
||||||
|
|
||||||
|
Provide the single-use tailscale pre-auth key via the TS_AUTHKEY env var, or
|
||||||
|
enter it at the interactive prompt. It is used once and never written to disk.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- args (validated before the root check, so errors are testable) ---------
|
||||||
|
ROLE="${1:-}"
|
||||||
|
case "$ROLE" in
|
||||||
|
control-plane|workload) shift ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
"") usage >&2; die "role required (control-plane|workload)" 2 ;;
|
||||||
|
*) die "unknown role: $ROLE (want control-plane|workload)" 2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
TS_HOSTNAME="$ROLE"
|
||||||
|
TS_TAG="tag:server"
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--hostname)
|
||||||
|
[ $# -ge 2 ] || die "--hostname needs a value" 2
|
||||||
|
TS_HOSTNAME="$2"; shift 2 ;;
|
||||||
|
--ts-tag)
|
||||||
|
[ $# -ge 2 ] || die "--ts-tag needs a value" 2
|
||||||
|
TS_TAG="$2"; shift 2 ;;
|
||||||
|
*) die "unknown flag: $1" 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- guards ------------------------------------------------------------------
|
||||||
|
[ "$(id -u)" -eq 0 ] || die "must run as root"
|
||||||
|
if [ -r /etc/os-release ]; then
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
. /etc/os-release
|
||||||
|
case "${ID:-} ${ID_LIKE:-}" in
|
||||||
|
*debian*) ;;
|
||||||
|
*) warn "not a Debian-family system (ID=${ID:-unknown}); proceeding anyway" ;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
warn "cannot read /etc/os-release; proceeding anyway"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- pre-auth key (env override, else prompt; never touches disk) ------------
|
||||||
|
if [ -z "${TS_AUTHKEY:-}" ]; then
|
||||||
|
read -rsp "tailscale pre-auth key (single-use, tagged, <=1h expiry): " TS_AUTHKEY
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
[ -n "$TS_AUTHKEY" ] || die "empty pre-auth key"
|
||||||
|
|
||||||
|
# --- packages ----------------------------------------------------------------
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
log "installing base packages"
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y -qq curl ca-certificates unattended-upgrades
|
||||||
|
|
||||||
|
# enable periodic unattended upgrades (canonical file; idempotent overwrite)
|
||||||
|
cat > /etc/apt/apt.conf.d/20auto-upgrades <<'EOF'
|
||||||
|
APT::Periodic::Update-Package-Lists "1";
|
||||||
|
APT::Periodic::Unattended-Upgrade "1";
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# --- sshd hardening (restart only when the drop-in actually changed) ---------
|
||||||
|
DROPIN=/etc/ssh/sshd_config.d/99-rig.conf
|
||||||
|
TMP="$(mktemp)"
|
||||||
|
cat > "$TMP" <<'EOF'
|
||||||
|
PermitRootLogin prohibit-password
|
||||||
|
PasswordAuthentication no
|
||||||
|
EOF
|
||||||
|
if ! cmp -s "$TMP" "$DROPIN" 2>/dev/null; then
|
||||||
|
install -m 0644 "$TMP" "$DROPIN"
|
||||||
|
systemctl restart ssh
|
||||||
|
log "sshd hardening drop-in installed"
|
||||||
|
else
|
||||||
|
log "sshd hardening drop-in already in place"
|
||||||
|
fi
|
||||||
|
rm -f "$TMP"
|
||||||
|
|
||||||
|
# --- tailscale ----------------------------------------------------------------
|
||||||
|
if ! command -v tailscale >/dev/null 2>&1; then
|
||||||
|
log "installing tailscale"
|
||||||
|
curl -fsSL https://tailscale.com/install.sh | sh
|
||||||
|
fi
|
||||||
|
if tailscale status >/dev/null 2>&1; then
|
||||||
|
log "tailnet already joined; skipping tailscale up"
|
||||||
|
else
|
||||||
|
log "joining tailnet as ${TS_HOSTNAME} (${TS_TAG})"
|
||||||
|
tailscale up --authkey="$TS_AUTHKEY" --hostname="$TS_HOSTNAME" --advertise-tags="$TS_TAG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "done — role ${ROLE}, hostname ${TS_HOSTNAME}"
|
||||||
|
if [ "$ROLE" = "control-plane" ]; then
|
||||||
|
log "next: rig coolify install --version <pin>"
|
||||||
|
fi
|
||||||
42
commands/coolify-install.sh
Executable file
42
commands/coolify-install.sh
Executable file
|
|
@ -0,0 +1,42 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# rig coolify install — pinned Coolify install; AUTOUPDATE=false so the
|
||||||
|
# platform never self-updates underneath its operators. Upgrades are an
|
||||||
|
# explicit act.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
log() { printf 'rig-coolify: %s\n' "$*"; }
|
||||||
|
die() { printf 'rig-coolify: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; }
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
usage: rig coolify install --version <pin>
|
||||||
|
|
||||||
|
Installs Coolify at exactly <pin> (e.g. 4.1.2) with AUTOUPDATE=false.
|
||||||
|
Control-plane box only. The version pin is required — you state the floor
|
||||||
|
your tooling is verified against; there is no default.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
VERSION=""
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--version)
|
||||||
|
[ $# -ge 2 ] || die "--version needs a value" 2
|
||||||
|
VERSION="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) die "unknown flag: $1" 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
usage >&2
|
||||||
|
die "--version <pin> is required" 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ "$(id -u)" -eq 0 ] || die "must run as root"
|
||||||
|
|
||||||
|
export AUTOUPDATE=false
|
||||||
|
log "installing coolify ${VERSION} (AUTOUPDATE=false)"
|
||||||
|
curl -fsSL https://cdn.coollabs.io/coolify/install.sh -o /tmp/coolify-install.sh
|
||||||
|
bash /tmp/coolify-install.sh "$VERSION"
|
||||||
|
log "coolify ${VERSION} installed with AUTOUPDATE=false"
|
||||||
|
log "next: your bootstrap runbook (admin user, API token, GitHub App, S3 destination)"
|
||||||
70
install.sh
Normal file
70
install.sh
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# rig installer — intended for: curl -fsSL .../install.sh | bash
|
||||||
|
#
|
||||||
|
# Downloads the rig repo tarball, installs the whole tree under $DEST,
|
||||||
|
# and puts a `rig` symlink on PATH via $BINDIR. Re-run any time to
|
||||||
|
# upgrade.
|
||||||
|
|
||||||
|
REPO="${RIG_REPO:-heavy-duty/rig}"
|
||||||
|
REF="${RIG_REF:-main}"
|
||||||
|
DEST="${RIG_HOME:-$HOME/.local/share/rig}"
|
||||||
|
if [ "$(id -u)" -eq 0 ]; then
|
||||||
|
BINDIR="${RIG_BIN:-/usr/local/bin}"
|
||||||
|
else
|
||||||
|
BINDIR="${RIG_BIN:-$HOME/.local/bin}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log() { printf 'rig-install: %s\n' "$*"; }
|
||||||
|
warn() { printf 'rig-install: WARNING: %s\n' "$*" >&2; }
|
||||||
|
die() { printf 'rig-install: ERROR: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# --- prerequisites -----------------------------------------------------------
|
||||||
|
command -v curl >/dev/null 2>&1 || die "curl is required but was not found."
|
||||||
|
command -v tar >/dev/null 2>&1 || die "tar is required but was not found."
|
||||||
|
|
||||||
|
# --- temp workspace ----------------------------------------------------------
|
||||||
|
TMPDIR="$(mktemp -d)"
|
||||||
|
cleanup() { rm -rf "$TMPDIR"; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
URL="https://github.com/$REPO/archive/refs/heads/$REF.tar.gz"
|
||||||
|
|
||||||
|
log "installing rig ($REPO@$REF)"
|
||||||
|
log "downloading $URL"
|
||||||
|
curl -fsSL "$URL" -o "$TMPDIR/rig.tar.gz" \
|
||||||
|
|| die "failed to download $URL"
|
||||||
|
|
||||||
|
log "extracting archive"
|
||||||
|
tar -xzf "$TMPDIR/rig.tar.gz" -C "$TMPDIR" \
|
||||||
|
|| die "failed to extract archive"
|
||||||
|
|
||||||
|
# GitHub archives extract to a single top-level dir like rig-<ref>/
|
||||||
|
EXTRACTED="$(find "$TMPDIR" -maxdepth 1 -type d -name 'rig-*' | head -n1)"
|
||||||
|
[ -n "$EXTRACTED" ] || die "could not find extracted rig-* directory in archive"
|
||||||
|
[ -f "$EXTRACTED/bin/rig" ] || die "archive does not contain bin/rig — is $REPO@$REF correct?"
|
||||||
|
|
||||||
|
# --- atomically replace $DEST --------------------------------------------------
|
||||||
|
log "installing into $DEST"
|
||||||
|
rm -rf "$DEST"
|
||||||
|
mkdir -p "$(dirname "$DEST")"
|
||||||
|
mv "$EXTRACTED" "$DEST"
|
||||||
|
|
||||||
|
chmod +x "$DEST/bin/rig" "$DEST"/commands/*.sh
|
||||||
|
|
||||||
|
# --- put rig on PATH ------------------------------------------------------
|
||||||
|
mkdir -p "$BINDIR"
|
||||||
|
ln -sf "$DEST/bin/rig" "$BINDIR/rig"
|
||||||
|
log "linked $BINDIR/rig -> $DEST/bin/rig"
|
||||||
|
|
||||||
|
# --- PATH check ----------------------------------------------------------------
|
||||||
|
case ":$PATH:" in
|
||||||
|
*":$BINDIR:"*) : ;;
|
||||||
|
*)
|
||||||
|
warn "$BINDIR is not on your PATH."
|
||||||
|
warn " add: export PATH=\"$BINDIR:\$PATH\""
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
log "done — try: rig --help"
|
||||||
57
test/cli.sh
Normal file
57
test/cli.sh
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
#!/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
|
||||||
|
if [ -n "$substr" ] && ! printf '%s' "$out" | grep -qF -e "$substr"; then
|
||||||
|
echo "FAIL: $desc — output missing '$substr'"
|
||||||
|
printf '%s\n' "$out" | sed 's/^/ /'
|
||||||
|
FAIL=$((FAIL + 1)); return
|
||||||
|
fi
|
||||||
|
echo "ok: $desc"; PASS=$((PASS + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
else
|
||||||
|
echo "skip: bootstrap non-root refusal (running as root)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
echo "---"
|
||||||
|
echo "$PASS passed, $FAIL failed"
|
||||||
|
[ "$FAIL" -eq 0 ]
|
||||||
Loading…
Reference in a new issue