2026-07-10 20:40:07 +00:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
ROOT="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)"
|
|
|
|
|
|
|
|
|
|
usage() {
|
|
|
|
|
cat <<'EOF'
|
2026-07-11 08:25:48 +00:00
|
|
|
usage: rig <command> [args]
|
2026-07-10 20:40:07 +00:00
|
|
|
|
|
|
|
|
commands:
|
2026-07-17 15:51:36 +00:00
|
|
|
bootstrap <control-plane|workload|runner|staging> [--hostname <name>]
|
2026-07-10 20:40:07 +00:00
|
|
|
OS plumbing on a pristine Debian box: hardening, unattended-upgrades,
|
2026-07-17 15:51:36 +00:00
|
|
|
tailscale join. Prompts for a single-use TAGGED tailnet pre-auth key
|
|
|
|
|
(TS_AUTHKEY env overrides the prompt); the key's tags are the tailnet
|
|
|
|
|
tag, verified after join. Run as root. Roles runner and staging
|
|
|
|
|
refuse tag:server.
|
2026-07-10 20:40:07 +00:00
|
|
|
coolify install --version <pin>
|
|
|
|
|
Pinned Coolify install (AUTOUPDATE=false). Control-plane box only.
|
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
|
|
|
coolify backup install [options]
|
|
|
|
|
Nightly age-encrypted dump of the control-plane database, as a
|
|
|
|
|
systemd timer. rig installs the machinery and templates an empty
|
|
|
|
|
0600 bindings file; you fill in the age recipient and S3 details.
|
|
|
|
|
Control-plane box only. Run as root.
|
feat(runner): status, remove, and repoint — the runner lifecycle verbs
runner install is convergent by skipping: it sees a registered runner and
leaves it alone. So rig could create a runner and never move or destroy one,
and re-pointing a box at a different repo meant hand-rolled config.sh/svc.sh
incantations against an install layout only rig knew about.
- status: repo, name, labels, dir, unit — read-only, no token, no network.
- remove: service down, then deregister. --local wipes the box without
contacting GitHub, leaving a stale entry to delete by hand.
- repoint: remove + re-register in one act, keeping the runner's name and
reusing the binary already on the box.
The service always comes down before deregistration in both paths: GitHub's
removal throws "Uninstall service first" while the service is configured, and
--local bypasses that check entirely, which would strand a running service
pointed at deleted config.
repoint collects both tokens up front — a token you turn out not to have must
fail while the runner is still registered, not halfway through the move.
Labels are the sharp edge: GitHub holds them, the runner does not persist
them, and they are what runs-on matches. install now records what it
registered with so repoint and status can read it back; a runner installed
before that has nothing to read, so repoint falls back to the ci-runner
default and warns before it touches anything.
2026-07-13 13:25:27 +00:00
|
|
|
runner install --repo <owner/repo> [options]
|
2026-07-11 17:41:09 +00:00
|
|
|
GitHub Actions runner as a systemd service under an unprivileged
|
|
|
|
|
user — outbound-only, no Docker. Prompts for the short-lived
|
|
|
|
|
registration token (RUNNER_TOKEN env overrides). Run as root.
|
feat(runner): status, remove, and repoint — the runner lifecycle verbs
runner install is convergent by skipping: it sees a registered runner and
leaves it alone. So rig could create a runner and never move or destroy one,
and re-pointing a box at a different repo meant hand-rolled config.sh/svc.sh
incantations against an install layout only rig knew about.
- status: repo, name, labels, dir, unit — read-only, no token, no network.
- remove: service down, then deregister. --local wipes the box without
contacting GitHub, leaving a stale entry to delete by hand.
- repoint: remove + re-register in one act, keeping the runner's name and
reusing the binary already on the box.
The service always comes down before deregistration in both paths: GitHub's
removal throws "Uninstall service first" while the service is configured, and
--local bypasses that check entirely, which would strand a running service
pointed at deleted config.
repoint collects both tokens up front — a token you turn out not to have must
fail while the runner is still registered, not halfway through the move.
Labels are the sharp edge: GitHub holds them, the runner does not persist
them, and they are what runs-on matches. install now records what it
registered with so repoint and status can read it back; a runner installed
before that has nothing to read, so repoint falls back to the ci-runner
default and warns before it touches anything.
2026-07-13 13:25:27 +00:00
|
|
|
runner status [--user <name>]
|
|
|
|
|
What this box's runner is registered to: repo, name, labels, unit.
|
|
|
|
|
Reads the box only — no token, no network call. Run as root.
|
|
|
|
|
runner remove [--local] [--user <name>]
|
|
|
|
|
Take the service down and deregister the runner. Prompts for the
|
|
|
|
|
short-lived removal token (RUNNER_REMOVE_TOKEN env overrides).
|
|
|
|
|
Run as root.
|
|
|
|
|
runner repoint --repo <owner/repo> [options]
|
|
|
|
|
Move an installed runner to another repository — deregister, then
|
|
|
|
|
re-register, reusing the binary already on the box. Needs a removal
|
|
|
|
|
token for the old repo and a registration token for the new one.
|
|
|
|
|
Run as root.
|
2026-07-10 20:40:07 +00:00
|
|
|
|
|
|
|
|
install/upgrade:
|
2026-07-11 08:25:48 +00:00
|
|
|
curl -fsSL https://raw.githubusercontent.com/heavy-duty/rig/main/install.sh | bash
|
2026-07-10 20:40:07 +00:00
|
|
|
EOF
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd="${1:-}"
|
|
|
|
|
case "$cmd" in
|
|
|
|
|
bootstrap)
|
|
|
|
|
shift
|
|
|
|
|
exec "$ROOT/commands/bootstrap.sh" "$@"
|
|
|
|
|
;;
|
|
|
|
|
coolify)
|
|
|
|
|
shift
|
|
|
|
|
sub="${1:-}"
|
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
|
|
|
case "$sub" in
|
|
|
|
|
install)
|
|
|
|
|
shift
|
|
|
|
|
exec "$ROOT/commands/coolify-install.sh" "$@"
|
|
|
|
|
;;
|
|
|
|
|
backup)
|
|
|
|
|
shift
|
|
|
|
|
if [ "${1:-}" != "install" ]; then
|
|
|
|
|
usage >&2
|
|
|
|
|
exit 2
|
|
|
|
|
fi
|
|
|
|
|
shift
|
|
|
|
|
exec "$ROOT/commands/coolify-backup-install.sh" "$@"
|
|
|
|
|
;;
|
|
|
|
|
*)
|
|
|
|
|
usage >&2
|
|
|
|
|
exit 2
|
|
|
|
|
;;
|
|
|
|
|
esac
|
2026-07-10 20:40:07 +00:00
|
|
|
;;
|
2026-07-11 17:41:09 +00:00
|
|
|
runner)
|
|
|
|
|
shift
|
|
|
|
|
sub="${1:-}"
|
feat(runner): status, remove, and repoint — the runner lifecycle verbs
runner install is convergent by skipping: it sees a registered runner and
leaves it alone. So rig could create a runner and never move or destroy one,
and re-pointing a box at a different repo meant hand-rolled config.sh/svc.sh
incantations against an install layout only rig knew about.
- status: repo, name, labels, dir, unit — read-only, no token, no network.
- remove: service down, then deregister. --local wipes the box without
contacting GitHub, leaving a stale entry to delete by hand.
- repoint: remove + re-register in one act, keeping the runner's name and
reusing the binary already on the box.
The service always comes down before deregistration in both paths: GitHub's
removal throws "Uninstall service first" while the service is configured, and
--local bypasses that check entirely, which would strand a running service
pointed at deleted config.
repoint collects both tokens up front — a token you turn out not to have must
fail while the runner is still registered, not halfway through the move.
Labels are the sharp edge: GitHub holds them, the runner does not persist
them, and they are what runs-on matches. install now records what it
registered with so repoint and status can read it back; a runner installed
before that has nothing to read, so repoint falls back to the ci-runner
default and warns before it touches anything.
2026-07-13 13:25:27 +00:00
|
|
|
case "$sub" in
|
|
|
|
|
install)
|
|
|
|
|
shift
|
|
|
|
|
exec "$ROOT/commands/runner-install.sh" "$@"
|
|
|
|
|
;;
|
|
|
|
|
status)
|
|
|
|
|
shift
|
|
|
|
|
exec "$ROOT/commands/runner-status.sh" "$@"
|
|
|
|
|
;;
|
|
|
|
|
remove)
|
|
|
|
|
shift
|
|
|
|
|
exec "$ROOT/commands/runner-remove.sh" "$@"
|
|
|
|
|
;;
|
|
|
|
|
repoint)
|
|
|
|
|
shift
|
|
|
|
|
exec "$ROOT/commands/runner-repoint.sh" "$@"
|
|
|
|
|
;;
|
|
|
|
|
*)
|
|
|
|
|
usage >&2
|
|
|
|
|
exit 2
|
|
|
|
|
;;
|
|
|
|
|
esac
|
2026-07-11 17:41:09 +00:00
|
|
|
;;
|
2026-07-10 20:40:07 +00:00
|
|
|
-h|--help|help)
|
|
|
|
|
usage
|
|
|
|
|
exit 0
|
|
|
|
|
;;
|
|
|
|
|
"")
|
|
|
|
|
usage >&2
|
|
|
|
|
exit 2
|
|
|
|
|
;;
|
|
|
|
|
*)
|
2026-07-11 08:25:48 +00:00
|
|
|
printf 'rig: unknown command: %s\n' "$cmd" >&2
|
2026-07-10 20:40:07 +00:00
|
|
|
usage >&2
|
|
|
|
|
exit 2
|
|
|
|
|
;;
|
|
|
|
|
esac
|