rig's CI story was GitHub-shaped end to end. This makes it work against a self-hosted Forgejo, in three pieces. The registry fetch becomes forge-aware. templates_resolve hardcoded three github.com archive URLs; RIG_TEMPLATES_HOST now selects the grammar, because the forges genuinely differ — GitHub serves refs/tags, refs/heads and bare paths, Forgejo serves exactly one, and emitting the other two there would mean two guaranteed 404s per fetch and a failure message listing URLs that never could have worked. Measured against forgejo.heavyduty.builders, not inferred. The default stays GitHub, so every existing caller is unchanged. install.sh's snapshot reads the same variable through a byte-identical copy of the builder, diffed by the tests: a snapshot cached from a forge converge would never fetch from is worse than no snapshot, and the pin-in-the-name staleness guard cannot catch a wrong-ORIGIN snapshot, only an old one. ci-box is a tenant, not a machine role. The topology is a fleet machine hosting boxes, one of which runs CI — a '-box' guest by rig's own family rule. That also deletes the docker-in-docker layer the usual setup needs: bootstrap-tenant.sh already installs Docker and adds the tenant user to the group, and the isolation a privileged dind sidecar buys is already paid for by a box that is network-isolated, inbound-less and disposable. rig runner install refuses Docker for good reason — it converges a MACHINE, where the blast radius is the machine. Here it is a guest that gets thrown away. rig forgejo-runner is a new family beside rig runner, which is untouched. Forgejo registers against an INSTANCE and the token carries the scope, so there is no --repo to converge toward and nothing to compare; folding that into one command would make every guard bimodal to share a flag name while the contract underneath differs. assert_runner_instance asks the same trust-boundary question about the axis Forgejo actually has. There is no repoint and no --local, and both absences are explained where an operator arriving from the GitHub sibling will hit them. Forgejo's .runner holds the runner's own long-lived token, unlike GitHub's, so it is installed 0600 and the mode is re-asserted on every converge — a mode that drifted leaks the secret silently, since nothing fails and the runner keeps working. status reports it and never prints the token. Both downloads verify the published .sha256 before installing: this binary lands as root and is executed by a systemd unit. bootstrap --undo learns the guard for the same hazard on the other forge, and it matters more here — Forgejo has no deregistration endpoint, so the ghost it would strand has to be deleted by hand. Known prerequisite, documented rather than assumed: the fetch is unauthenticated by contract, and a Forgejo with REQUIRE_SIGNIN_VIEW=true answers 404 for repos it reports as public. Hosting a registry there needs FORGEJO__service__REQUIRE_SIGNIN_VIEW=false. The refusal names that case, because it is indistinguishable from a wrong ref. forgejo#109 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
553 lines
23 KiB
Bash
Executable file
553 lines
23 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)"
|
|
|
|
log() { printf 'rig: %s\n' "$*"; }
|
|
warn() { printf 'rig: WARNING: %s\n' "$*" >&2; }
|
|
die() { printf 'rig: ERROR: %s\n' "$*" >&2; exit 1; }
|
|
version() { echo "rig $(cat "$ROOT/VERSION" 2>/dev/null || echo unknown) ($ROOT)"; }
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
usage: rig <command> [args]
|
|
|
|
commands:
|
|
bootstrap <control-plane-server|workload-server|runner-server|staging-server|
|
|
dev-server|workstation|custom>
|
|
(--users <path> | --no-users)
|
|
[--hostname <name>] [--class <human|server>] [--host <yes|no>]
|
|
[--join <authkey|login>]
|
|
OS plumbing on a pristine Debian box: hardening, unattended-upgrades,
|
|
tailscale join, then this box's operators. --users is REQUIRED on
|
|
every role — it converges the users file as bootstrap's last phase
|
|
(identical to 'rig users apply --file <path>', marker written first)
|
|
so one command leaves a box with its people on it; --no-users is the
|
|
deliberate opt-out and leaves root as the only door. The tenant roles
|
|
below take neither. Roles are presets over the three traits; any flag
|
|
overrides its trait, and custom states all of them. 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 —
|
|
only control-plane-server and workload-server may carry tag:server.
|
|
The '-server' suffix names the FAMILY (a fleet machine, vs the '-box'
|
|
tenant roles); custom and workstation take none. join=login
|
|
(workstation) needs no key: interactive login, node must come up
|
|
untagged. Run as root.
|
|
bootstrap --undo
|
|
Leave the tailnet only when /etc/rig/role proves rig performed the
|
|
join, then remove that marker. Refuses for pre-existing or old unknown
|
|
joins, and while a GitHub runner is installed. Run as root.
|
|
bootstrap <role>-box [--user <name>]
|
|
The box TENANT roles: converge a box-minted guest. The '-box' suffix
|
|
names the family (a guest, vs the '-server' machine roles above).
|
|
The agent tenants (claude-box, codex-box, grok-box, kimi-box, …) are
|
|
DEFINED in the heavy-duty/rig-templates registry — resolved via
|
|
RIG_TEMPLATES_DIR > RIG_TEMPLATES_REF > the in-tree pin — and land
|
|
the agent toolbelt (git, gh, tmux, docker), the agent's CLI on the
|
|
system PATH, and the agent-context file (with the box#80 guard:
|
|
never run box setup-host inside a box). staging-box, in rig's own
|
|
tree, lands box#69's server posture — docker + sshd hardening; its
|
|
tailnet join stays operator-run via 'rig bootstrap workload-server'.
|
|
Creds-free and non-interactive: box auto-runs these at mint. Run as
|
|
root, inside the box.
|
|
coolify install --version <pin>
|
|
Pinned Coolify install (AUTOUPDATE=false). Control-plane box only.
|
|
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.
|
|
db <dump|restore> ...
|
|
Ad-hoc PostgreSQL dump/restore for a container on this box. `dump`
|
|
writes a gzipped SQL artifact (--no-owner --no-acl, so it restores
|
|
onto a different instance); `restore` loads one back, connecting as
|
|
the container's own superuser, behind a confirm gate. Run as root.
|
|
platform
|
|
What is this machine: hostname, OS, kernel, CPU, memory, disk and
|
|
virtualization, then rig's own provenance (which rig, when, and the
|
|
role marker bootstrap wrote). Computed at run time from /proc, uname,
|
|
/etc/os-release, df and systemd-detect-virt and stored NOWHERE — a
|
|
spec goes stale the moment someone adds RAM, so there is nothing to
|
|
go stale here. Writes nothing, needs no root, makes no network call,
|
|
and therefore also runs on a pristine Debian box rig has never
|
|
bootstrapped, where the provenance block reads 'not bootstrapped'.
|
|
runner install --repo <owner/repo> [options]
|
|
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.
|
|
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.
|
|
forgejo-runner install --instance <url> [options]
|
|
Forgejo Actions runner as a systemd service under an unprivileged
|
|
user — outbound-only, jobs in containers on this box's own dockerd.
|
|
Its home is a ci-box tenant, where 'rig bootstrap ci-box' already
|
|
installed that daemon. Registers to an INSTANCE: whether the runner
|
|
serves that instance, one org or one repo is a property of the
|
|
registration token, so there is no --repo. Prompts for the token
|
|
(FORGEJO_RUNNER_TOKEN env overrides). Run as root.
|
|
forgejo-runner status [--user <name>]
|
|
Which instance this box's Forgejo runner is registered to: instance,
|
|
name, labels, unit. Reads the box only — no token, no network call,
|
|
and never prints the registration secret .runner holds. Run as root.
|
|
forgejo-runner remove [--user <name>]
|
|
Take the service down and wipe this box's registration. Always
|
|
local-only: Forgejo has no deregistration endpoint, so the runner
|
|
stays listed offline until you delete it in the instance. Needs no
|
|
token. Run as root.
|
|
users apply --file <path>
|
|
Converge named operator accounts from a declarative users file, on
|
|
every class: groups by role (admin/rig/box), passwords locked always,
|
|
authorized_keys made exact, visudo-gated sudoers rules. Users dropped
|
|
from the file are locked, never deleted. '-' reads stdin. Run as root.
|
|
users status
|
|
Roles (derived from actual group membership), key counts and lock
|
|
state for the rig-managed users. Reads the box only. Run as root.
|
|
users close-root
|
|
Shut root SSH on a class=human box once an admin key works. Refuses
|
|
on class=server — root there is the control plane's automation door —
|
|
and while no admin holds a key. Run as root.
|
|
template-lint <role-dir>...
|
|
Validate tenant-role definitions (the heavy-duty/rig-templates
|
|
shape): template.env against the allowlist (data, never sourced),
|
|
install.sh present with a shebang, creds.md non-blank. Every refusal
|
|
names the failing key. The registry repo's CI runs this on every PR;
|
|
no root, no network, no writes.
|
|
manifest [<key>]
|
|
Print /etc/rig/manifest — which rig converged this machine and when
|
|
(bootstrapped_by/_at pin the FIRST convergence forever; converged_by/_at
|
|
name the latest). With a key, print that value alone for shell callers.
|
|
Written by 'rig bootstrap' only; read-only here, and no root needed.
|
|
Provenance is DECIDED, so it is stored; the machine's specs are
|
|
OBSERVED, so they are not — those are 'rig platform'.
|
|
versions
|
|
List the installed rig versions — install.sh lands each one side by
|
|
side at <root>/versions/<v>, a 'current' symlink tracks the default
|
|
(what the rig on your PATH runs). The default is marked (current);
|
|
the tree answering this command is marked (running).
|
|
use <version>
|
|
Switch the default rig version — repoint 'current' (and the PATH
|
|
symlink riding it) at an installed version, atomically, then assert
|
|
the flip took. WARNS on a bootstrapped host (/etc/rig/role exists):
|
|
switching the rig under a converged host changes what a re-converge
|
|
would do.
|
|
uninstall [<version>|--all] [--force]
|
|
Remove one NON-current installed version, or --all: every version,
|
|
'current', and the PATH symlinks. Asks first (--force or RIG_YES=1
|
|
skips the prompt) and ENDS with an absence assert — every removed
|
|
path is re-checked, and any survivor makes it exit 1 naming the
|
|
leftovers instead of reporting a clean uninstall that wasn't.
|
|
--version
|
|
Print the running rig's version (its tree's own VERSION file) and
|
|
where it runs from.
|
|
|
|
install/upgrade:
|
|
curl -fsSL https://raw.githubusercontent.com/heavy-duty/rig/main/install.sh | bash
|
|
Installs the latest RELEASE (RIG_REF=<tag> pins one, RIG_REF=main
|
|
tracks the development tree). Re-run any time: an installed version
|
|
converges (no-op), a new one installs side by side at
|
|
<root>/versions/<v> and becomes the default.
|
|
EOF
|
|
}
|
|
|
|
# --- the versioned install (box#79's layout, ported — #35) -------------------
|
|
# install.sh lands each version at <install-root>/versions/<v>, with a
|
|
# 'current' symlink naming the default and $BINDIR/rig pointing through it.
|
|
# $ROOT (readlink -f, line 4) already resolved the whole chain, so a versioned
|
|
# install always runs from .../versions/<v> — and a git checkout does not,
|
|
# which is how these verbs know to refuse instead of uninstalling somebody's
|
|
# working copy.
|
|
install_root() {
|
|
local vdir; vdir="$(dirname "$ROOT")"
|
|
[ "$(basename "$vdir")" = versions ] || return 1
|
|
dirname "$vdir"
|
|
}
|
|
|
|
# A version is a DIRECTORY NAME under versions/ — nothing else. One strict
|
|
# gate for every caller that builds a path from one (the installer's new_ver,
|
|
# migration's flat_ver, and bin/rig's 'use'/single-version uninstall): only
|
|
# [A-Za-z0-9._+-], no leading '.' or '-'. That forbids '/', '..'-escapes,
|
|
# spaces and option-lookalikes by construction — a crafted version dies HERE,
|
|
# never in an rm -rf or an ln. install.sh carries a byte-identical copy;
|
|
# test/cli.sh diffs the two so the gates cannot drift.
|
|
valid_version() {
|
|
case "$1" in
|
|
''|.*|-*) return 1 ;;
|
|
*[!A-Za-z0-9._+-]*) return 1 ;;
|
|
esac
|
|
return 0
|
|
}
|
|
|
|
# The flip gate, rig's shape (#35): box refuses version flips under existing
|
|
# boxes; rig's stake is the converged HOST — /etc/rig/role marks a box that
|
|
# bootstrap has made into something. Switching the default rig under it
|
|
# changes what a re-converge would do, which is worth a warning, not a
|
|
# refusal: there is no user state a flip can strand, and flipping versions on
|
|
# a bootstrapped host is the normal upgrade. RIG_ROLE_MARKER overrides the
|
|
# path so tests point it at fixtures (repo precedent: the coolify marker
|
|
# gate). install.sh carries a byte-identical copy; test/cli.sh diffs the two.
|
|
warn_bootstrapped() { # $1 = what is about to happen
|
|
local marker="${RIG_ROLE_MARKER:-/etc/rig/role}"
|
|
[ -e "$marker" ] || return 0
|
|
warn "this host is bootstrapped ($(head -n1 "$marker" 2>/dev/null || echo "role marker at $marker"))"
|
|
warn "$1 changes what a re-converge (rig bootstrap, users apply) would do — proceeding."
|
|
}
|
|
|
|
# The PATH symlinks that could ride this install: the one this invocation came
|
|
# in on, RIG_BIN's, and the tier default's. Candidates only — every consumer
|
|
# checks where a link actually points before touching it, so a symlink that is
|
|
# somebody else's (another install root, a hand-rolled wrapper) is never moved.
|
|
bin_links() {
|
|
local c=()
|
|
[ -L "${BASH_SOURCE[0]}" ] && c+=("${BASH_SOURCE[0]}")
|
|
[ -n "${RIG_BIN:-}" ] && c+=("$RIG_BIN/rig")
|
|
if [ "$(id -u)" -eq 0 ]; then c+=(/usr/local/bin/rig); else c+=("$HOME/.local/bin/rig"); fi
|
|
printf '%s\n' "${c[@]}" | awk '!seen[$0]++'
|
|
}
|
|
|
|
converge_bin_links() { # $1 = install root: point our PATH symlinks through current
|
|
local ir="$1" p t
|
|
while IFS= read -r p; do
|
|
[ -L "$p" ] || continue
|
|
t="$(readlink -f "$p" 2>/dev/null || true)"
|
|
[ -n "$t" ] || t="$(readlink "$p" 2>/dev/null || true)"
|
|
case "$t" in
|
|
"$ir"/*) ln -sfn "$ir/current/bin/rig" "$p" ;;
|
|
esac
|
|
done < <(bin_links)
|
|
}
|
|
|
|
cmd_versions() {
|
|
local ir cur d v mark
|
|
ir="$(install_root)" || die "this rig runs from a working tree ($ROOT), not a versioned install — nothing to list"
|
|
cur="$(readlink -f "$ir/current" 2>/dev/null || true)"
|
|
echo "VERSIONS ($ir)"
|
|
for d in "$ir/versions"/*/; do
|
|
[ -d "$d" ] || continue
|
|
v="$(basename "$d")"
|
|
mark=""
|
|
[ "$(readlink -f "$d")" = "$cur" ] && mark=" (current)"
|
|
[ "$(readlink -f "$d")" = "$ROOT" ] && mark="$mark (running)"
|
|
printf ' %s%s\n' "$v" "$mark"
|
|
done
|
|
echo
|
|
echo "switch the default: rig use <version>"
|
|
echo "install another: re-run install.sh (versions land side by side)"
|
|
}
|
|
|
|
cmd_use() {
|
|
local v="${1:-}" ir eff expect out
|
|
if [ -z "$v" ]; then
|
|
printf 'rig: use needs a version (see: rig versions)\n' >&2
|
|
usage >&2
|
|
exit 2
|
|
fi
|
|
ir="$(install_root)" || die "this rig runs from a working tree ($ROOT), not a versioned install — nothing to switch"
|
|
valid_version "$v" || die "not a sane version name: '$v' (a version is a directory name under versions/ — see 'rig versions')"
|
|
[ -d "$ir/versions/$v" ] || die "no such version: $v (see 'rig versions')"
|
|
warn_bootstrapped "switching the default rig version to $v"
|
|
# An atomic flip, not unlink+create: ln -sfn leaves a window where current
|
|
# is missing; a rename over it does not.
|
|
ln -sfn "versions/$v" "$ir/current.new.$$" && mv -Tf "$ir/current.new.$$" "$ir/current"
|
|
converge_bin_links "$ir"
|
|
# Assert the EFFECTIVE result, not the intent: current must resolve to the
|
|
# version asked for, and the chain's own binary must answer that version —
|
|
# a flip that "worked" while the operator's rig still runs the old tree is
|
|
# exactly the flakiness this verb exists to end.
|
|
eff="$(basename "$(readlink -f "$ir/current" 2>/dev/null || true)")"
|
|
[ "$eff" = "$v" ] || die "the flip did not take — current resolves to '${eff:-nothing}', not $v"
|
|
expect="$(cat "$ir/versions/$v/VERSION" 2>/dev/null || true)"
|
|
if [ -n "$expect" ]; then
|
|
out="$("$ir/current/bin/rig" --version 2>&1 || true)"
|
|
case "$out" in
|
|
*"$expect"*) : ;;
|
|
*) die "current/bin/rig answers '$out', not version $expect — the symlink chain is broken" ;;
|
|
esac
|
|
fi
|
|
log "switched to $v (current -> versions/$v)"
|
|
}
|
|
|
|
# The uninstall's own confirmation: --force, or RIG_YES=1, or a TTY. RIG_YES
|
|
# is the installer-family consent contract — how automation says yes without
|
|
# a terminal; without any of the three we refuse rather than assume consent.
|
|
uninstall_confirm() { # $1 = question
|
|
[ "$force" -eq 1 ] && return 0
|
|
[ -n "${RIG_YES:-}" ] && return 0
|
|
if [ ! -t 0 ]; then
|
|
printf 'rig: refusing to %s without --force (no terminal to confirm on; RIG_YES=1 also means yes)\n' "$1" >&2
|
|
exit 2
|
|
fi
|
|
local reply
|
|
printf 'rig: %s? [y/N] ' "$1"
|
|
# `|| reply=""` is load-bearing under `set -e`: Ctrl-D makes read return
|
|
# non-zero, and as a plain-statement call site this function would die HERE,
|
|
# before the case — the abort would be real but completely silent. Empty
|
|
# falls through to the `*)` arm, so EOF aborts with exactly one message,
|
|
# the same spelling commands/db.sh uses for the same [y/N] shape.
|
|
read -r reply || reply=""
|
|
case "$reply" in y|Y|yes|YES|Yes) return 0 ;; *) die "aborted." ;; esac
|
|
}
|
|
|
|
# 'rig uninstall' — the real uninstall, replacing the undocumented rm -rf
|
|
# prose. Trees and symlinks, and it ENDS by PROVING the absence — the last
|
|
# word is a re-check, not a hope.
|
|
cmd_uninstall() {
|
|
local ir a ver="" all=0 force=0 cur p t leftover=""
|
|
local targets=()
|
|
for a in "$@"; do
|
|
case "$a" in
|
|
--all) all=1 ;;
|
|
--force) force=1 ;;
|
|
-*)
|
|
printf 'rig: unknown option: %s\n' "$a" >&2
|
|
usage >&2
|
|
exit 2
|
|
;;
|
|
*)
|
|
if [ -n "$ver" ]; then
|
|
printf 'rig: uninstall takes one version, or --all\n' >&2
|
|
usage >&2
|
|
exit 2
|
|
fi
|
|
ver="$a"
|
|
;;
|
|
esac
|
|
done
|
|
ir="$(install_root)" || die "this rig runs from a working tree ($ROOT), not a versioned install — nothing to uninstall (a checkout is removed with plain rm)"
|
|
[ -w "$ir" ] || die "cannot write $ir — uninstall as the user that installed it (or root: sudo rig uninstall)"
|
|
|
|
# -- one version -----------------------------------------------------------
|
|
if [ -n "$ver" ] && [ "$all" -eq 0 ]; then
|
|
valid_version "$ver" || die "not a sane version name: '$ver' (a version is a directory name under versions/ — see 'rig versions')"
|
|
[ -d "$ir/versions/$ver" ] || die "no such version: $ver (see 'rig versions')"
|
|
cur="$(basename "$(readlink -f "$ir/current" 2>/dev/null || true)")"
|
|
# A broken current makes the CURRENT guard below unfireable (cur empty
|
|
# when the link is missing; cur naming a non-directory when it dangles —
|
|
# readlink -f resolves a link whose last component does not exist). Heal
|
|
# first, then decide; never delete around a broken default.
|
|
{ [ -n "$cur" ] && [ -d "$ir/versions/$cur" ]; } \
|
|
|| die "current is dangling — 'rig use <version>' to repoint the default first (refusing to remove versions while it is broken)"
|
|
[ "$ver" != "$cur" ] || die "$ver is the CURRENT version — 'rig use <other>' first, or 'rig uninstall --all' for everything"
|
|
uninstall_confirm "remove rig version $ver from $ir"
|
|
# rm's exit code is not the verdict — the absence re-check below is (a
|
|
# half-removed tree must be reported as INCOMPLETE, not as a crash).
|
|
rm -rf "${ir:?}/versions/$ver" || true
|
|
if [ -e "$ir/versions/$ver" ] || [ -L "$ir/versions/$ver" ]; then
|
|
echo "rig: uninstall INCOMPLETE — still present: $ir/versions/$ver" >&2
|
|
exit 1
|
|
fi
|
|
log "removed version $ver (the default stays $cur)"
|
|
return 0
|
|
fi
|
|
if [ -n "$ver" ]; then
|
|
printf 'rig: a version and --all together is ambiguous\n' >&2
|
|
usage >&2
|
|
exit 2
|
|
fi
|
|
|
|
# -- everything (bare 'rig uninstall' and '--all' both mean all of it) -----
|
|
warn_bootstrapped "removing rig entirely"
|
|
uninstall_confirm "remove the ENTIRE rig install at $ir (every version)"
|
|
|
|
# The removal set, gathered BEFORE anything is deleted, so the absence
|
|
# assert below re-checks exactly what was promised gone. PATH symlinks are
|
|
# removed only when they resolve into (or dangle at) THIS install root.
|
|
targets+=("$ir")
|
|
while IFS= read -r p; do
|
|
[ -L "$p" ] || continue
|
|
t="$(readlink -f "$p" 2>/dev/null || true)"
|
|
[ -n "$t" ] || t="$(readlink "$p" 2>/dev/null || true)"
|
|
case "$t" in "$ir"/*) targets+=("$p") ;; esac
|
|
done < <(bin_links)
|
|
mapfile -t targets < <(printf '%s\n' "${targets[@]}" | awk '!seen[$0]++')
|
|
|
|
# rm's exit code is not the verdict — the absence assert below is (a
|
|
# half-removed tree must be reported as INCOMPLETE by name, not as a crash).
|
|
for p in "${targets[@]}"; do rm -rf "$p" || true; done
|
|
|
|
# END WITH THE ABSENCE ASSERT: every path re-checked — file, dir OR symlink.
|
|
# A leftover makes this exit 1 by name; "uninstalled" is a claim, and claims
|
|
# get verified.
|
|
for p in "${targets[@]}"; do
|
|
if [ -e "$p" ] || [ -L "$p" ]; then leftover="$leftover $p"; fi
|
|
done
|
|
if [ -n "$leftover" ]; then
|
|
echo "rig: uninstall INCOMPLETE — still present:$leftover" >&2
|
|
echo "rig: remove them by hand, and re-check each path is really gone." >&2
|
|
exit 1
|
|
fi
|
|
echo "rig: uninstalled — removed:"
|
|
for p in "${targets[@]}"; do echo "rig: · $p"; done
|
|
}
|
|
|
|
cmd="${1:-}"
|
|
case "$cmd" in
|
|
bootstrap)
|
|
shift
|
|
exec "$ROOT/commands/bootstrap.sh" "$@"
|
|
;;
|
|
coolify)
|
|
shift
|
|
sub="${1:-}"
|
|
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
|
|
;;
|
|
db)
|
|
shift
|
|
case "${1:-}" in
|
|
dump|restore|-h|--help)
|
|
exec "$ROOT/commands/db.sh" "$@"
|
|
;;
|
|
*)
|
|
usage >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
;;
|
|
platform)
|
|
shift
|
|
exec "$ROOT/commands/platform.sh" "$@"
|
|
;;
|
|
runner)
|
|
shift
|
|
sub="${1:-}"
|
|
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
|
|
;;
|
|
forgejo-runner)
|
|
shift
|
|
sub="${1:-}"
|
|
case "$sub" in
|
|
install)
|
|
shift
|
|
exec "$ROOT/commands/forgejo-runner-install.sh" "$@"
|
|
;;
|
|
status)
|
|
shift
|
|
exec "$ROOT/commands/forgejo-runner-status.sh" "$@"
|
|
;;
|
|
remove)
|
|
shift
|
|
exec "$ROOT/commands/forgejo-runner-remove.sh" "$@"
|
|
;;
|
|
repoint)
|
|
# The GitHub sibling HAS this verb, so an operator will try it. Say why
|
|
# it cannot exist here rather than printing usage and leaving them to
|
|
# infer it: 'repoint' is atomic because GitHub lets rig deregister from
|
|
# the old repo; Forgejo has no such endpoint, so the honest shape is
|
|
# two acts, and one of them leaves a stale entry behind.
|
|
printf 'rig: forgejo-runner has no repoint: Forgejo has no deregistration endpoint, so a move cannot be one atomic act. Use "rig forgejo-runner remove" then "rig forgejo-runner install --instance <url>", and delete the stale runner in the old instance under Actions > Runners.\n' >&2
|
|
exit 2
|
|
;;
|
|
*)
|
|
usage >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
;;
|
|
users)
|
|
shift
|
|
sub="${1:-}"
|
|
case "$sub" in
|
|
apply)
|
|
shift
|
|
exec "$ROOT/commands/users-apply.sh" "$@"
|
|
;;
|
|
status)
|
|
shift
|
|
exec "$ROOT/commands/users-status.sh" "$@"
|
|
;;
|
|
close-root)
|
|
shift
|
|
exec "$ROOT/commands/users-close-root.sh" "$@"
|
|
;;
|
|
*)
|
|
usage >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
;;
|
|
template-lint)
|
|
shift
|
|
exec "$ROOT/commands/template-lint.sh" "$@"
|
|
;;
|
|
manifest)
|
|
shift
|
|
exec "$ROOT/commands/manifest.sh" "$@"
|
|
;;
|
|
versions)
|
|
shift
|
|
cmd_versions "$@"
|
|
;;
|
|
use)
|
|
shift
|
|
cmd_use "$@"
|
|
;;
|
|
uninstall)
|
|
shift
|
|
cmd_uninstall "$@"
|
|
;;
|
|
-V|--version)
|
|
version
|
|
exit 0
|
|
;;
|
|
-h|--help|help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
"")
|
|
usage >&2
|
|
exit 2
|
|
;;
|
|
*)
|
|
printf 'rig: unknown command: %s\n' "$cmd" >&2
|
|
usage >&2
|
|
exit 2
|
|
;;
|
|
esac
|