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"