buildah can serve a stale COPY --from layer (cache keyed on stage name), so a Containerfile edit silently produced an unchanged image. The image now carries a cfhash label; a mismatch forces a --no-cache rebuild.
193 lines
6.9 KiB
Bash
Executable File
193 lines
6.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# deploy.sh — Deploy the serverAppConfigs repo to a target and restart its stack.
|
|
#
|
|
# Implements the git deployment workflow from AGENTS.md. Assumes local changes
|
|
# are committed and pushed, then:
|
|
# 1. Pushes main to origin
|
|
# 2. Pulls the repo on the target
|
|
# 3. Copies shared configs into place
|
|
# 4. Restarts the podman stack (podman-compose up -d)
|
|
# 5. Waits for the Caddy homepage to respond
|
|
#
|
|
# Usage:
|
|
# ./scripts/deploy.sh <target> [--dry-run]
|
|
#
|
|
# Targets:
|
|
# dankclaw - AI/search/dev/observability stack (dankclaw/compose.yaml)
|
|
# dankserver - media/library stack (dankserver/compose.yaml)
|
|
#
|
|
# Example:
|
|
# ./scripts/deploy.sh dankclaw
|
|
# ./scripts/deploy.sh dankserver --dry-run
|
|
set -euo pipefail
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Target configuration
|
|
# ---------------------------------------------------------------------------
|
|
declare -A REMOTE_USER REMOTE_HOST REMOTE_DIR COMPOSE HEALTH_URL COMPOSE_UP COPY_SHARED
|
|
|
|
# Caddy is consolidated on dankclaw: it fronts both hosts' services. The
|
|
# dankserver media stack runs as bare containers (not this repo's compose),
|
|
# so its deploy is pull-only; health is checked against the shared Caddy.
|
|
REMOTE_USER[dankclaw]=dank
|
|
REMOTE_HOST[dankclaw]=dankclaw
|
|
REMOTE_DIR[dankclaw]="/home/dank/serverAppConfigs"
|
|
COMPOSE[dankclaw]="dankclaw/compose.yaml"
|
|
HEALTH_URL[dankclaw]="http://127.0.0.1/"
|
|
COMPOSE_UP[dankclaw]=1
|
|
COPY_SHARED[dankclaw]=1
|
|
|
|
REMOTE_USER[dankserver]=dank
|
|
REMOTE_HOST[dankserver]=dankserver
|
|
REMOTE_DIR[dankserver]="/home/dank/serverAppConfigs"
|
|
COMPOSE[dankserver]="dankserver/compose.yaml"
|
|
HEALTH_URL[dankserver]="http://192.168.0.143/"
|
|
COMPOSE_UP[dankserver]=0
|
|
COPY_SHARED[dankserver]=0
|
|
|
|
TARGET="${1:-}"
|
|
DRY_RUN=0
|
|
if [ "${2:-}" = "--dry-run" ]; then
|
|
DRY_RUN=1
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Validation
|
|
# ---------------------------------------------------------------------------
|
|
if [ -z "${TARGET:-}" ]; then
|
|
echo "Usage: $0 <dankclaw|dankserver> [--dry-run]"
|
|
exit 1
|
|
fi
|
|
if [ -z "${REMOTE_HOST[$TARGET]:-}" ]; then
|
|
echo "Unknown target: $TARGET"
|
|
exit 1
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Local checks (pre-flight)
|
|
# ---------------------------------------------------------------------------
|
|
cd "$(dirname "$0")/.."
|
|
if ! git diff --quiet HEAD; then
|
|
echo "FAIL: Local working tree has uncommitted changes."
|
|
echo "Commit or stash them before deploying."
|
|
exit 1
|
|
fi
|
|
if [ "$(git rev-parse --abbrev-ref HEAD)" != "main" ]; then
|
|
echo "WARN: Not on 'main'; deploying a non-main branch."
|
|
fi
|
|
|
|
echo "=== Deploying to ${TARGET} ==="
|
|
echo " host: ${REMOTE_HOST[$TARGET]}"
|
|
echo " dir: ${REMOTE_DIR[$TARGET]}"
|
|
echo " compose: ${COMPOSE[$TARGET]}"
|
|
echo " health: ${HEALTH_URL[$TARGET]}"
|
|
echo ""
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Push (unless dry-run)
|
|
# ---------------------------------------------------------------------------
|
|
if [ "$DRY_RUN" = "1" ]; then
|
|
echo "[dry-run] Would push local main to origin"
|
|
echo "[dry-run] Would SSH to ${REMOTE_HOST[$TARGET]} and git pull"
|
|
else
|
|
echo "[1/4] Pushing local main to origin..."
|
|
git push origin main
|
|
fi
|
|
|
|
REMOTE="$(
|
|
printf '%s@%s' "${REMOTE_USER[$TARGET]}" "${REMOTE_HOST[$TARGET]}"
|
|
)"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Remote: pull + copy shared configs + restart stack
|
|
# ---------------------------------------------------------------------------
|
|
REMOTE_SCRIPT="
|
|
set -euo pipefail
|
|
cd '${REMOTE_DIR[$TARGET]}'
|
|
|
|
echo '[2/4] Pulling repo on ${TARGET}...'
|
|
git pull origin main
|
|
|
|
if [ -f .env ]; then
|
|
echo ' .env present, using it'
|
|
else
|
|
echo ' WARN: no .env on target; copy the .env.example and fill it in'
|
|
fi
|
|
|
|
if [ '${COPY_SHARED[$TARGET]}' = '1' ]; then
|
|
echo '[3/4] Copying shared configs into place...'
|
|
mkdir -p /opt/searxng/config /opt/prometheus/config /opt/loki/config /opt/alloy/config
|
|
cp shared/searxng/settings.yml /opt/searxng/config/settings.yml
|
|
cp shared/searxng/limiter.toml /opt/searxng/config/limiter.toml
|
|
cp shared/prometheus/prometheus.yml /opt/prometheus/config/prometheus.yml
|
|
cp shared/loki/loki-config.yaml /opt/loki/config/loki-config.yaml
|
|
cp shared/alloy/config.alloy /opt/alloy/config/config.alloy
|
|
|
|
echo ' Rendering Caddyfile (basicauth hash from .env)...'
|
|
# Extract the hash with grep/cut rather than sourcing .env: sourcing a
|
|
# bcrypt value in a set -u shell dies on its dollar-sign expansion.
|
|
CADDY_BASICAUTH_HASH=\"\$(grep -E '^CADDY_BASICAUTH_HASH=' .env | tail -1 | cut -d= -f2-)\"
|
|
if [ -z \"\${CADDY_BASICAUTH_HASH}\" ]; then
|
|
echo ' WARN: CADDY_BASICAUTH_HASH not set in .env; keeping placeholder'
|
|
else
|
|
# '|' delimiter: the bcrypt hash contains '/' characters.
|
|
mkdir -p /home/dank/Containers/caddy
|
|
sed \"s|REPLACE_WITH_HASH|\${CADDY_BASICAUTH_HASH}|g\" dankclaw/Caddyfile \\
|
|
> /home/dank/Containers/caddy/Caddyfile
|
|
fi
|
|
fi
|
|
|
|
if [ '${COMPOSE_UP[$TARGET]}' = '1' ]; then
|
|
echo '[4/4] Restarting stack...'
|
|
# Locally built images: caddy + porkbun module, and loki/alloy + a static
|
|
# busybox (their upstream images lack the wget the healthchecks need).
|
|
for dir in caddy loki alloy; do
|
|
img=franksdank/\$dir
|
|
[ \"\$dir\" = caddy ] && img=franksdank/caddy-porkbun
|
|
# Rebuild when the Containerfile changed. buildah's layer cache can
|
|
# serve a stale COPY --from layer (keyed on stage name), so a rebuild
|
|
# always runs with --no-cache.
|
|
cfhash=\$(sha256sum dankclaw/\$dir/Containerfile | cut -d' ' -f1)
|
|
oldhash=\$(podman image inspect \$img:latest --format '{{index .Config.Labels "cfhash"}}' 2>/dev/null || echo none)
|
|
if [ \"\$oldhash\" != \"\$cfhash\" ]; then
|
|
echo \" \$img image missing or stale; building it...\"
|
|
podman build --no-cache --label cfhash=\$cfhash -t \$img:latest -f dankclaw/\$dir/Containerfile .
|
|
fi
|
|
done
|
|
podman-compose -f '${COMPOSE[$TARGET]}' up -d
|
|
else
|
|
echo '[4/4] Skipping compose restart (stack runs as bare containers on this host)'
|
|
fi
|
|
"
|
|
|
|
if [ "$DRY_RUN" = "1" ]; then
|
|
echo "[dry-run] Remote commands would run:"
|
|
echo "$REMOTE_SCRIPT"
|
|
else
|
|
# shellcheck disable=SC2087
|
|
ssh "$REMOTE" bash -s <<REMOTE_EOF
|
|
${REMOTE_SCRIPT}
|
|
REMOTE_EOF
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Health check
|
|
# ---------------------------------------------------------------------------
|
|
if [ "$DRY_RUN" = "1" ]; then
|
|
echo "[dry-run] Would wait for health at ${HEALTH_URL[$TARGET]}"
|
|
else
|
|
echo "[5/4] Waiting for health endpoint ${HEALTH_URL[$TARGET]} (via ssh)..."
|
|
HEALTH="${HEALTH_URL[$TARGET]}"
|
|
for i in $(seq 1 60); do
|
|
if ssh "$REMOTE" "curl -sf '$HEALTH'" > /dev/null 2>&1; then
|
|
echo " Homepage healthy after approx $((i * 2))s"
|
|
echo ""
|
|
echo "=== Deploy complete: ${TARGET} ==="
|
|
exit 0
|
|
fi
|
|
sleep 2
|
|
done
|
|
echo "FAIL: Homepage did not become healthy in 120s"
|
|
exit 1
|
|
fi
|