Files
automations/build-bundle.sh
T
57_WolveandClaude Opus 5 f71f8d615a fix(launcher): run on a bare Alpine host (POSIX prologue, self-installs bash)
automations.sh was #!/usr/bin/env bash and written in bash, but a stock Alpine
box has busybox ash and no bash at all -- so the launcher died before it could
install anything:

  ./automations.sh        -> env: 'bash': No such file or directory
  curl ... | bash         -> bash: not found
  sh automations.sh       -> syntax errors

cloud-init/base.yml already installs bash/git/curl before touching the repo;
the launcher never got the same treatment.

Give it a #!/bin/sh shebang and a strictly-POSIX prologue that ash can parse:
self-locate via ${0%/*} (busybox dirname takes `--` as the filename, so
`dirname -- "$0"` would answer "."), clone on the piped path as before, install
git/bash via apk/apt-get/dnf/yum, then exec bash on this same file. The bash
pass skips the prologue via BASH_VERSION, and an exported _AUTOMATIONS_REEXEC
guard rules out an exec loop. Everything below the handoff is the unchanged
bash launcher.

The generated bundle stub had the identical bug -- it's the other fresh-host
entry point -- so make it POSIX sh too and have it install bash before
extracting.

Docs: the one-liner now pipes to `sh`, bundle examples use `sh`.

Still bash-only on a bare Alpine host: the scp'd deployments/*/deploy.sh
artifacts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 17:13:30 -05:00

121 lines
4.6 KiB
Bash

#!/usr/bin/env bash
#
# build-bundle.sh -- package the whole repo into a single self-extracting
# script, dist/automations-bundle.sh. Host that one file on a webserver (or a
# public Gitea release) and run it on a target host -- no repo/git access
# needed.
#
# Usage:
# ./build-bundle.sh # bundle the committed tree at HEAD
# ./build-bundle.sh v1.2.0 # bundle a specific tag/ref
# ./build-bundle.sh worktree # bundle the current working tree
# # (tracked + new files, minus ignored)
# OUT=/tmp/x.sh ./build-bundle.sh # custom output path
#
# The payload excludes ignored files (dist/, .env, globals.env,
# ssh-notify.conf, keys, backups) so no secrets are embedded.
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REF="${1:-HEAD}"
OUT="${OUT:-$DIR/dist/automations-bundle.sh}"
MARKER="__ARCHIVE_BELOW__"
command -v git >/dev/null 2>&1 || { echo "git is required." >&2; exit 1; }
mkdir -p "$(dirname "$OUT")"
# ---------------------------------------------------------------------------
# Build the repo tarball (gzipped), then base64-encode it.
# ---------------------------------------------------------------------------
make_tar() {
case "$REF" in
worktree|WORKTREE|.)
# Tracked + untracked-but-not-ignored files = current state minus
# ignored (secrets, dist/). Respects .gitignore via --exclude-standard.
git -C "$DIR" ls-files -co --exclude-standard -z \
| tar -czf - -C "$DIR" --null -T - ;;
*)
# Clean tar of the committed tree at REF (ignored files never tracked).
git -C "$DIR" archive --format=tar "$REF" | gzip -9 ;;
esac
}
echo "Packaging repo ($REF)..."
PAYLOAD="$(make_tar | base64 | tr -d '\n')"
# ---------------------------------------------------------------------------
# Emit the self-extracting stub, then the payload after the marker.
# ---------------------------------------------------------------------------
{
cat <<'STUB'
#!/bin/sh
#
# automations-bundle.sh -- self-extracting bundle of the automations repo.
# Generated by build-bundle.sh. Download, then run (it can't extract from a
# pipe -- it needs to read itself as a file):
#
# curl -fsSLO https://your-host/automations-bundle.sh
# sh automations-bundle.sh # launcher wizard
# sh automations-bundle.sh bash scripts/setup-host.sh # run a script
# SSH_PORT=2222 sh automations-bundle.sh bash scripts/harden-jumphost.sh
#
# POSIX sh, not bash: a stock Alpine host has busybox ash and no bash, so a
# bash stub would fail before it could install anything. It installs bash
# itself (everything inside the bundle is bash) and then runs the launcher.
#
# Env:
# INSTALL_DIR where to extract (default /opt/automations)
# BUNDLE_KEEP 1 to keep the extracted repo (default), 0 to use a temp dir
# and remove it after the command finishes
set -eu
: "${INSTALL_DIR:=/opt/automations}"
: "${BUNDLE_KEEP:=1}"
SELF="$0"
if [ ! -f "$SELF" ]; then
echo "[x] Run me as a downloaded file, not via a pipe:" >&2
echo " curl -fsSLO <url>/automations-bundle.sh && sh automations-bundle.sh" >&2
exit 1
fi
# The repo contents -- launcher, scripts/, deployments/ -- are all bash.
if ! command -v bash >/dev/null 2>&1; then
echo "[+] bash not found; installing it..." >&2
if command -v apk >/dev/null 2>&1; then { apk add -q bash 2>/dev/null || { apk update -q && apk add -q bash; }; } || true
elif command -v apt-get >/dev/null 2>&1; then { apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq bash; } || true
elif command -v dnf >/dev/null 2>&1; then dnf install -y -q bash || true
elif command -v yum >/dev/null 2>&1; then yum install -y -q bash || true
fi
command -v bash >/dev/null 2>&1 || {
echo "[x] 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." >&2
exit 1
}
fi
if [ "$BUNDLE_KEEP" != "1" ]; then
INSTALL_DIR="$(mktemp -d -t automations.XXXXXX)"
trap 'rm -rf "$INSTALL_DIR"' EXIT
fi
echo "[+] Extracting bundle to $INSTALL_DIR ..."
mkdir -p "$INSTALL_DIR"
sed -e '1,/^__ARCHIVE_BELOW__$/d' "$SELF" | base64 -d | tar -xz -C "$INSTALL_DIR"
cd "$INSTALL_DIR"
chmod +x automations.sh build-bundle.sh scripts/*.sh deployments/*/*.sh 2>/dev/null || true
if [ "$#" -gt 0 ]; then
exec "$@"
else
exec bash ./automations.sh
fi
__ARCHIVE_BELOW__
STUB
printf '%s\n' "$PAYLOAD"
} > "$OUT"
chmod +x "$OUT"
echo "Built $OUT ($(wc -c < "$OUT") bytes) from $REF"