From 7280544e3a5b98bc1b552956b4a25d16d9e142b6 Mon Sep 17 00:00:00 2001 From: claude-hdb Date: Fri, 10 Jul 2026 20:37:08 +0000 Subject: [PATCH 1/8] chore: repo birth --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 18d5cf3..1087f76 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # deployor -Box-plumbing CLI: curl-install on a pristine Debian server, bootstrap it into a hardened tailnet-joined node + +Box-plumbing CLI. Full README lands with the initial-CLI PR. -- 2.45.2 From db60771c6a7abbabe9dc1e65649ca3d17e5ad8cd Mon Sep 17 00:00:00 2001 From: claude-hdb Date: Fri, 10 Jul 2026 20:40:07 +0000 Subject: [PATCH 2/8] feat: deployor dispatcher + dependency-free test harness --- bin/deployor | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ test/cli.sh | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100755 bin/deployor create mode 100644 test/cli.sh diff --git a/bin/deployor b/bin/deployor new file mode 100755 index 0000000..ed41c69 --- /dev/null +++ b/bin/deployor @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)" + +usage() { + cat <<'EOF' +usage: deployor [args] + +commands: + bootstrap [--hostname ] [--ts-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 + Pinned Coolify install (AUTOUPDATE=false). Control-plane box only. + +install/upgrade: + curl -fsSL https://raw.githubusercontent.com/claude-hdb/deployor/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 'deployor: unknown command: %s\n' "$cmd" >&2 + usage >&2 + exit 2 + ;; +esac diff --git a/test/cli.sh b/test/cli.sh new file mode 100644 index 0000000..cb5d25f --- /dev/null +++ b/test/cli.sh @@ -0,0 +1,36 @@ +#!/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 +# 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 "$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/deployor" +check "--help exits 0" 0 "usage:" "$ROOT/bin/deployor" --help +check "help exits 0" 0 "usage:" "$ROOT/bin/deployor" help +check "unknown command exits 2" 2 "unknown command" "$ROOT/bin/deployor" frobnicate +check "bare coolify shows usage, exit 2" 2 "usage:" "$ROOT/bin/deployor" coolify + +echo "---" +echo "$PASS passed, $FAIL failed" +[ "$FAIL" -eq 0 ] -- 2.45.2 From aa4185f9ed9c239437fbd7c0995612d842e92f8b Mon Sep 17 00:00:00 2001 From: claude-hdb Date: Fri, 10 Jul 2026 20:43:24 +0000 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20bootstrap=20command=20=E2=80=94=20h?= =?UTF-8?q?ardening,=20unattended-upgrades,=20tailscale=20join?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- commands/bootstrap.sh | 108 ++++++++++++++++++++++++++++++++++++++++++ test/cli.sh | 11 +++++ 2 files changed, 119 insertions(+) create mode 100755 commands/bootstrap.sh diff --git a/commands/bootstrap.sh b/commands/bootstrap.sh new file mode 100755 index 0000000..5f7b22f --- /dev/null +++ b/commands/bootstrap.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# deployor bootstrap — OS plumbing for a pristine Debian box. +# Convergent: safe to re-run; a second run changes nothing. +set -euo pipefail + +log() { printf 'deployor-bootstrap: %s\n' "$*"; } +warn() { printf 'deployor-bootstrap: WARNING: %s\n' "$*" >&2; } +die() { printf 'deployor-bootstrap: ERROR: %s\n' "$*" >&2; exit "${2:-1}"; } + +usage() { + cat <<'EOF' +usage: deployor bootstrap [--hostname ] [--ts-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-deployor.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: deployor coolify install --version " +fi diff --git a/test/cli.sh b/test/cli.sh index cb5d25f..94decc6 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -31,6 +31,17 @@ check "help exits 0" 0 "usage:" "$ROOT/bin/deployor" help check "unknown command exits 2" 2 "unknown command" "$ROOT/bin/deployor" frobnicate check "bare coolify shows usage, exit 2" 2 "usage:" "$ROOT/bin/deployor" 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 + echo "---" echo "$PASS passed, $FAIL failed" [ "$FAIL" -eq 0 ] -- 2.45.2 From c19bb7d99aba211b3c984c9855d470613d0aa5c3 Mon Sep 17 00:00:00 2001 From: claude-hdb Date: Fri, 10 Jul 2026 20:48:36 +0000 Subject: [PATCH 4/8] fix: die() prints only the message, not the exit code --- commands/bootstrap.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/bootstrap.sh b/commands/bootstrap.sh index 5f7b22f..817e4c4 100755 --- a/commands/bootstrap.sh +++ b/commands/bootstrap.sh @@ -5,7 +5,7 @@ set -euo pipefail log() { printf 'deployor-bootstrap: %s\n' "$*"; } warn() { printf 'deployor-bootstrap: WARNING: %s\n' "$*" >&2; } -die() { printf 'deployor-bootstrap: ERROR: %s\n' "$*" >&2; exit "${2:-1}"; } +die() { printf 'deployor-bootstrap: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; } usage() { cat <<'EOF' -- 2.45.2 From 33c27452ac20c2813b36240b1627283deafe72c7 Mon Sep 17 00:00:00 2001 From: claude-hdb Date: Fri, 10 Jul 2026 20:50:41 +0000 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20coolify=20install=20command=20?= =?UTF-8?q?=E2=80=94=20required=20version=20pin,=20AUTOUPDATE=3Dfalse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- commands/coolify-install.sh | 42 +++++++++++++++++++++++++++++++++++++ test/cli.sh | 10 +++++++++ 2 files changed, 52 insertions(+) create mode 100755 commands/coolify-install.sh diff --git a/commands/coolify-install.sh b/commands/coolify-install.sh new file mode 100755 index 0000000..5ef862e --- /dev/null +++ b/commands/coolify-install.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# deployor 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 'deployor-coolify: %s\n' "$*"; } +die() { printf 'deployor-coolify: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; } + +usage() { + cat <<'EOF' +usage: deployor coolify install --version + +Installs Coolify at exactly (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 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)" diff --git a/test/cli.sh b/test/cli.sh index 94decc6..a6ba548 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -42,6 +42,16 @@ 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 ] -- 2.45.2 From d4697a0ad51b68a38ffc3cd01c7c9477edfb5082 Mon Sep 17 00:00:00 2001 From: claude-hdb Date: Fri, 10 Jul 2026 20:53:32 +0000 Subject: [PATCH 6/8] fix: pass check substrings to grep with -e so dash-leading patterns assert --- test/cli.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cli.sh b/test/cli.sh index a6ba548..6cd0ee0 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -17,7 +17,7 @@ check() { printf '%s\n' "$out" | sed 's/^/ /' FAIL=$((FAIL + 1)); return fi - if [ -n "$substr" ] && ! printf '%s' "$out" | grep -qF "$substr"; then + 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 -- 2.45.2 From 924090a427ef50034dd16de62b75680dfa5c12ec Mon Sep 17 00:00:00 2001 From: claude-hdb Date: Fri, 10 Jul 2026 20:56:05 +0000 Subject: [PATCH 7/8] feat: curl installer, shellcheck+tests CI, living README --- .github/workflows/ci.yml | 14 ++++++++ README.md | 72 +++++++++++++++++++++++++++++++++++++++- install.sh | 70 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 install.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bfbaec2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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/deployor commands/*.sh test/cli.sh + - name: cli tests + run: bash test/cli.sh diff --git a/README.md b/README.md index 1087f76..ac5ccd8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,73 @@ # deployor -Box-plumbing CLI. Full README lands with the initial-CLI PR. +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**. deployor 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/claude-hdb/deployor/main/install.sh | bash +``` + +Installs the tree to `~/.local/share/deployor` and links `deployor` onto your +PATH (`/usr/local/bin` when root). Re-run any time to upgrade. + +## Commands + +### `deployor bootstrap ` + +Run as root on the fresh box (over SSH). Convergent — safe to re-run; a +second run changes nothing. + +```sh +deployor bootstrap control-plane --hostname my-coolify-box +deployor bootstrap workload --hostname my-prod-box +``` + +- `--hostname ` — tailnet hostname (default: the role name) +- `--ts-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 — deployor 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. + +### `deployor coolify install --version ` + +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 deployor 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 + deployor needs arrives as arguments or an interactive prompt. +- **Manage deployments** — deploy manifests/executors are separate concerns. + (Planned: the `apply`/`diff` executor half joins deployor 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. diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..3191097 --- /dev/null +++ b/install.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +set -euo pipefail + +# deployor installer — intended for: curl -fsSL .../install.sh | bash +# +# Downloads the deployor repo tarball, installs the whole tree under $DEST, +# and puts a `deployor` symlink on PATH via $BINDIR. Re-run any time to +# upgrade. + +REPO="${DEPLOYOR_REPO:-claude-hdb/deployor}" +REF="${DEPLOYOR_REF:-main}" +DEST="${DEPLOYOR_HOME:-$HOME/.local/share/deployor}" +if [ "$(id -u)" -eq 0 ]; then + BINDIR="${DEPLOYOR_BIN:-/usr/local/bin}" +else + BINDIR="${DEPLOYOR_BIN:-$HOME/.local/bin}" +fi + +log() { printf 'deployor-install: %s\n' "$*"; } +warn() { printf 'deployor-install: WARNING: %s\n' "$*" >&2; } +die() { printf 'deployor-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 deployor ($REPO@$REF)" +log "downloading $URL" +curl -fsSL "$URL" -o "$TMPDIR/deployor.tar.gz" \ + || die "failed to download $URL" + +log "extracting archive" +tar -xzf "$TMPDIR/deployor.tar.gz" -C "$TMPDIR" \ + || die "failed to extract archive" + +# GitHub archives extract to a single top-level dir like deployor-/ +EXTRACTED="$(find "$TMPDIR" -maxdepth 1 -type d -name 'deployor-*' | head -n1)" +[ -n "$EXTRACTED" ] || die "could not find extracted deployor-* directory in archive" +[ -f "$EXTRACTED/bin/deployor" ] || die "archive does not contain bin/deployor — 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/deployor" "$DEST"/commands/*.sh + +# --- put deployor on PATH ------------------------------------------------------ +mkdir -p "$BINDIR" +ln -sf "$DEST/bin/deployor" "$BINDIR/deployor" +log "linked $BINDIR/deployor -> $DEST/bin/deployor" + +# --- PATH check ---------------------------------------------------------------- +case ":$PATH:" in + *":$BINDIR:"*) : ;; + *) + warn "$BINDIR is not on your PATH." + warn " add: export PATH=\"$BINDIR:\$PATH\"" + ;; +esac + +log "done — try: deployor --help" -- 2.45.2 From df17851108345605c9fbe6d343c08e2e3ceed7ca Mon Sep 17 00:00:00 2001 From: claude-hdb Date: Sat, 11 Jul 2026 08:25:48 +0000 Subject: [PATCH 8/8] refactor: rename deployor to rig; canonical heavy-duty/rig URLs --- .github/workflows/ci.yml | 2 +- README.md | 24 +++++++++---------- bin/{deployor => rig} | 6 ++--- commands/bootstrap.sh | 14 +++++------ commands/coolify-install.sh | 8 +++---- install.sh | 46 ++++++++++++++++++------------------- test/cli.sh | 10 ++++---- 7 files changed, 55 insertions(+), 55 deletions(-) rename bin/{deployor => rig} (84%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bfbaec2..fc50074 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,6 @@ jobs: steps: - uses: actions/checkout@v4 - name: shellcheck - run: shellcheck install.sh bin/deployor commands/*.sh test/cli.sh + run: shellcheck install.sh bin/rig commands/*.sh test/cli.sh - name: cli tests run: bash test/cli.sh diff --git a/README.md b/README.md index ac5ccd8..c3ff72d 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,33 @@ -# deployor +# rig 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**. deployor carries plumbing logic only — no +**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/claude-hdb/deployor/main/install.sh | bash +curl -fsSL https://raw.githubusercontent.com/heavy-duty/rig/main/install.sh | bash ``` -Installs the tree to `~/.local/share/deployor` and links `deployor` onto your +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 -### `deployor bootstrap ` +### `rig bootstrap ` Run as root on the fresh box (over SSH). Convergent — safe to re-run; a second run changes nothing. ```sh -deployor bootstrap control-plane --hostname my-coolify-box -deployor bootstrap workload --hostname my-prod-box +rig bootstrap control-plane --hostname my-coolify-box +rig bootstrap workload --hostname my-prod-box ``` - `--hostname ` — tailnet hostname (default: the role name) @@ -40,28 +40,28 @@ 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 — deployor never writes a credential to disk. +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. -### `deployor coolify install --version ` +### `rig coolify install --version ` 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 deployor deliberately does NOT do +## 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 - deployor needs arrives as arguments or an interactive prompt. + rig needs arrives as arguments or an interactive prompt. - **Manage deployments** — deploy manifests/executors are separate concerns. - (Planned: the `apply`/`diff` executor half joins deployor as commands that + (Planned: the `apply`/`diff` executor half joins rig as commands that run on operator machines, never on boxes.) ## Testing diff --git a/bin/deployor b/bin/rig similarity index 84% rename from bin/deployor rename to bin/rig index ed41c69..286ab12 100755 --- a/bin/deployor +++ b/bin/rig @@ -5,7 +5,7 @@ ROOT="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)" usage() { cat <<'EOF' -usage: deployor [args] +usage: rig [args] commands: bootstrap [--hostname ] [--ts-tag ] @@ -16,7 +16,7 @@ commands: Pinned Coolify install (AUTOUPDATE=false). Control-plane box only. install/upgrade: - curl -fsSL https://raw.githubusercontent.com/claude-hdb/deployor/main/install.sh | bash + curl -fsSL https://raw.githubusercontent.com/heavy-duty/rig/main/install.sh | bash EOF } @@ -45,7 +45,7 @@ case "$cmd" in exit 2 ;; *) - printf 'deployor: unknown command: %s\n' "$cmd" >&2 + printf 'rig: unknown command: %s\n' "$cmd" >&2 usage >&2 exit 2 ;; diff --git a/commands/bootstrap.sh b/commands/bootstrap.sh index 817e4c4..4b82fc5 100755 --- a/commands/bootstrap.sh +++ b/commands/bootstrap.sh @@ -1,15 +1,15 @@ #!/usr/bin/env bash -# deployor bootstrap — OS plumbing for a pristine Debian box. +# rig bootstrap — OS plumbing for a pristine Debian box. # Convergent: safe to re-run; a second run changes nothing. set -euo pipefail -log() { printf 'deployor-bootstrap: %s\n' "$*"; } -warn() { printf 'deployor-bootstrap: WARNING: %s\n' "$*" >&2; } -die() { printf 'deployor-bootstrap: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; } +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: deployor bootstrap [--hostname ] [--ts-tag ] +usage: rig bootstrap [--hostname ] [--ts-tag ] --hostname tailnet hostname (default: the role name) --ts-tag tailnet tag to advertise (default: tag:server) @@ -75,7 +75,7 @@ APT::Periodic::Unattended-Upgrade "1"; EOF # --- sshd hardening (restart only when the drop-in actually changed) --------- -DROPIN=/etc/ssh/sshd_config.d/99-deployor.conf +DROPIN=/etc/ssh/sshd_config.d/99-rig.conf TMP="$(mktemp)" cat > "$TMP" <<'EOF' PermitRootLogin prohibit-password @@ -104,5 +104,5 @@ fi log "done — role ${ROLE}, hostname ${TS_HOSTNAME}" if [ "$ROLE" = "control-plane" ]; then - log "next: deployor coolify install --version " + log "next: rig coolify install --version " fi diff --git a/commands/coolify-install.sh b/commands/coolify-install.sh index 5ef862e..4e6d915 100755 --- a/commands/coolify-install.sh +++ b/commands/coolify-install.sh @@ -1,15 +1,15 @@ #!/usr/bin/env bash -# deployor coolify install — pinned Coolify install; AUTOUPDATE=false so the +# 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 'deployor-coolify: %s\n' "$*"; } -die() { printf 'deployor-coolify: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; } +log() { printf 'rig-coolify: %s\n' "$*"; } +die() { printf 'rig-coolify: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; } usage() { cat <<'EOF' -usage: deployor coolify install --version +usage: rig coolify install --version Installs Coolify at exactly (e.g. 4.1.2) with AUTOUPDATE=false. Control-plane box only. The version pin is required — you state the floor diff --git a/install.sh b/install.sh index 3191097..e10f9db 100644 --- a/install.sh +++ b/install.sh @@ -1,24 +1,24 @@ #!/usr/bin/env bash set -euo pipefail -# deployor installer — intended for: curl -fsSL .../install.sh | bash +# rig installer — intended for: curl -fsSL .../install.sh | bash # -# Downloads the deployor repo tarball, installs the whole tree under $DEST, -# and puts a `deployor` symlink on PATH via $BINDIR. Re-run any time to +# 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="${DEPLOYOR_REPO:-claude-hdb/deployor}" -REF="${DEPLOYOR_REF:-main}" -DEST="${DEPLOYOR_HOME:-$HOME/.local/share/deployor}" +REPO="${RIG_REPO:-heavy-duty/rig}" +REF="${RIG_REF:-main}" +DEST="${RIG_HOME:-$HOME/.local/share/rig}" if [ "$(id -u)" -eq 0 ]; then - BINDIR="${DEPLOYOR_BIN:-/usr/local/bin}" + BINDIR="${RIG_BIN:-/usr/local/bin}" else - BINDIR="${DEPLOYOR_BIN:-$HOME/.local/bin}" + BINDIR="${RIG_BIN:-$HOME/.local/bin}" fi -log() { printf 'deployor-install: %s\n' "$*"; } -warn() { printf 'deployor-install: WARNING: %s\n' "$*" >&2; } -die() { printf 'deployor-install: ERROR: %s\n' "$*" >&2; exit 1; } +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." @@ -31,19 +31,19 @@ trap cleanup EXIT URL="https://github.com/$REPO/archive/refs/heads/$REF.tar.gz" -log "installing deployor ($REPO@$REF)" +log "installing rig ($REPO@$REF)" log "downloading $URL" -curl -fsSL "$URL" -o "$TMPDIR/deployor.tar.gz" \ +curl -fsSL "$URL" -o "$TMPDIR/rig.tar.gz" \ || die "failed to download $URL" log "extracting archive" -tar -xzf "$TMPDIR/deployor.tar.gz" -C "$TMPDIR" \ +tar -xzf "$TMPDIR/rig.tar.gz" -C "$TMPDIR" \ || die "failed to extract archive" -# GitHub archives extract to a single top-level dir like deployor-/ -EXTRACTED="$(find "$TMPDIR" -maxdepth 1 -type d -name 'deployor-*' | head -n1)" -[ -n "$EXTRACTED" ] || die "could not find extracted deployor-* directory in archive" -[ -f "$EXTRACTED/bin/deployor" ] || die "archive does not contain bin/deployor — is $REPO@$REF correct?" +# GitHub archives extract to a single top-level dir like rig-/ +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" @@ -51,12 +51,12 @@ rm -rf "$DEST" mkdir -p "$(dirname "$DEST")" mv "$EXTRACTED" "$DEST" -chmod +x "$DEST/bin/deployor" "$DEST"/commands/*.sh +chmod +x "$DEST/bin/rig" "$DEST"/commands/*.sh -# --- put deployor on PATH ------------------------------------------------------ +# --- put rig on PATH ------------------------------------------------------ mkdir -p "$BINDIR" -ln -sf "$DEST/bin/deployor" "$BINDIR/deployor" -log "linked $BINDIR/deployor -> $DEST/bin/deployor" +ln -sf "$DEST/bin/rig" "$BINDIR/rig" +log "linked $BINDIR/rig -> $DEST/bin/rig" # --- PATH check ---------------------------------------------------------------- case ":$PATH:" in @@ -67,4 +67,4 @@ case ":$PATH:" in ;; esac -log "done — try: deployor --help" +log "done — try: rig --help" diff --git a/test/cli.sh b/test/cli.sh index 6cd0ee0..df81a0f 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -25,11 +25,11 @@ check() { echo "ok: $desc"; PASS=$((PASS + 1)) } -check "no args shows usage, exit 2" 2 "usage:" "$ROOT/bin/deployor" -check "--help exits 0" 0 "usage:" "$ROOT/bin/deployor" --help -check "help exits 0" 0 "usage:" "$ROOT/bin/deployor" help -check "unknown command exits 2" 2 "unknown command" "$ROOT/bin/deployor" frobnicate -check "bare coolify shows usage, exit 2" 2 "usage:" "$ROOT/bin/deployor" coolify +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 -- 2.45.2