#!/bin/sh # # automations.sh -- one command to run or deploy anything in this repo. # # Run it two ways: # # 1. One-liner on a fresh target host (clones the repo, then launches): # curl -fsSL https://git.anomalous.dev/57_Wolve/automations/raw/branch/main/automations.sh \ # | REPO_URL=https://git.anomalous.dev/57_Wolve/automations.git sh # # 2. From a clone: # ./automations.sh # # It opens a Gum wizard (auto-installed) that lets you: # • Mode: deploy on THIS host, or build deploy.sh artifacts locally. # • Pick any deployment (pocket-id, beszel, headscale, webfinger, squid, # copyparty, simplex, openbao, ergo) or any generic script (setup-host, # harden-ssh, harden-jumphost, sshuser, auto-update). # Shared defaults come from globals/ (see globals/README.md). # # Non-interactive: set SKIP_PROMPTS=1 plus the needed vars and pipe the menu # choices in, or just call the underlying deployments//deploy.sh # directly -- they all honor SKIP_PROMPTS=1. # ============================================================================ # PROLOGUE -- POSIX sh only. Everything below the "exec bash" handoff is bash. # # The shebang is /bin/sh, not bash, on purpose: a stock Alpine box has busybox # ash and NO bash at all, so a `#!/usr/bin/env bash` launcher dies before it # can install anything ("env: 'bash': No such file or directory"). This part # therefore has to parse and run under ash: no [[ ]], no arrays, no # BASH_SOURCE, no printf -v. It locates (or clones) the repo, makes sure bash # exists, and re-execs this same file under bash -- which then skips the # prologue via BASH_VERSION and runs the real launcher. # ============================================================================ set -eu _boot_log() { printf '\033[1;32m[+]\033[0m %s\n' "$*"; } _boot_die() { printf '\033[1;31m[x]\033[0m %s\n' "$*" >&2; exit 1; } # Install packages with whichever manager this distro has -- apk (Alpine), # apt-get (Debian/Ubuntu), dnf/yum (Alma/RHEL). oslib.sh's pkg_install can't # help here: it's bash, and on the piped path it isn't even on disk yet. _boot_install() { if command -v apk >/dev/null 2>&1; then apk add -q "$@" 2>/dev/null && return 0 apk update -q >/dev/null 2>&1 || true # stale/absent index on a fresh box apk add -q "$@" elif command -v apt-get >/dev/null 2>&1; then apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "$@" elif command -v dnf >/dev/null 2>&1; then dnf install -y -q "$@" elif command -v yum >/dev/null 2>&1; then yum install -y -q "$@" else return 1 fi } _boot_need() { # _boot_need [package] -> 0 if it's available afterwards if command -v "$1" >/dev/null 2>&1; then return 0; fi _boot_log "$1 not found; installing it..." _boot_install "${2:-$1}" || true command -v "$1" >/dev/null 2>&1 } # ---------------------------------------------------------------------------- # Self-locate, or bootstrap by cloning the repo (one-liner / piped form). # ---------------------------------------------------------------------------- ROOT="" # Strip the last path component ourselves rather than calling dirname: busybox # dirname takes its first argument literally, so `dirname -- "$0"` would answer # "." on Alpine. `$0` is "sh"/"bash" (no slash) when we're piped from curl. case "$0" in */*) _dir="${0%/*}" ;; *) _dir="." ;; esac _dir="$(CDPATH= cd "$_dir" 2>/dev/null && pwd)" || _dir="" if [ -n "$_dir" ] && [ -f "$_dir/scripts/lib.sh" ]; then ROOT="$_dir" else # Piped via curl: we don't have the repo on disk. Clone it, then hand off. : "${REPO_URL:=}" : "${REPO_BRANCH:=main}" [ -n "$REPO_URL" ] || _boot_die "Running standalone (piped). Set REPO_URL=... so I can clone the repo." _boot_need git || _boot_die "git is required to clone the repo, but it isn't installed and I couldn't install it automatically (need root + a supported package manager). Install git, then re-run." _tmp="$(mktemp -d -t automations.XXXXXX)" _boot_log "Cloning $REPO_URL ($REPO_BRANCH)..." git clone --depth 1 --branch "$REPO_BRANCH" "$REPO_URL" "$_tmp" ROOT="$_tmp" fi # ---------------------------------------------------------------------------- # Hand off to bash. Needed when we're running under ash/dash, and when the body # we want is the freshly cloned copy rather than the piped stdin we came from. # ---------------------------------------------------------------------------- if [ -z "${BASH_VERSION:-}" ] || [ ! -f "$0" ] || [ "$ROOT" != "$_dir" ]; then if [ "${_AUTOMATIONS_REEXEC:-0}" = 1 ]; then # Already handed off once. If we're in bash the handoff worked and only # the path comparison differs (symlinked checkout) -- just continue. [ -n "${BASH_VERSION:-}" ] || _boot_die "Re-exec under bash did not take effect. Run it explicitly: bash $ROOT/automations.sh" else # The launcher, everything it sources (scripts/lib.sh, scripts/oslib.sh), # and every deploy.sh it invokes are bash. Alpine images routinely ship # without it, so install it before going any further. _boot_need bash || _boot_die "bash is required, but it isn't installed and I couldn't install it automatically (need root + a supported package manager). Install bash, then re-run." _AUTOMATIONS_REEXEC=1; export _AUTOMATIONS_REEXEC exec bash "$ROOT/automations.sh" "$@" fi fi # ============================================================================ # Running under bash from here down. # ============================================================================ set -euo pipefail # shellcheck source=scripts/lib.sh . "$ROOT/scripts/lib.sh" load_globals DEPLOYMENTS=(pocket-id beszel headscale webfinger squid copyparty simplex openbao ergo) SCRIPTS=(setup-host harden-ssh harden-jumphost sshuser auto-update) # ---------------------------------------------------------------------------- # Prompt helpers (gum). `ask` records each answer in ENVS for passing onward. # ---------------------------------------------------------------------------- ENVS=() ask() { #