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>
121 lines
4.6 KiB
Bash
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"
|