feat(deploy): port padelnomics deploy.sh improvements to web/deploy.sh
- Auto-install sops + age binaries to web/bin/ if not present - Generate age keypair at repo root age-key.txt if missing (prints public key with instructions to add to .sops.yaml, then exits) - Decrypt .env.prod.sops → web/.env at deploy time (no CI secrets needed) - Backup SQLite DB before migration (timestamped, keeps last 3) - Rollback on health check failure: dump logs + restore DB backup - Reset nginx router to current slot before --wait to avoid upstream errors - Remove web/scripts/deploy.sh (duplicate) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
122
web/deploy.sh
122
web/deploy.sh
@@ -1,6 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ── Ensure sops + age are installed ───────────────────────
|
||||
APP_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
BIN_DIR="$APP_DIR/bin"
|
||||
mkdir -p "$BIN_DIR"
|
||||
export PATH="$BIN_DIR:$PATH"
|
||||
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
x86_64) ARCH_SOPS="amd64"; ARCH_AGE="amd64" ;;
|
||||
aarch64) ARCH_SOPS="arm64"; ARCH_AGE="arm64" ;;
|
||||
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
|
||||
esac
|
||||
|
||||
if ! command -v age &>/dev/null; then
|
||||
echo "==> Installing age to $BIN_DIR..."
|
||||
AGE_VERSION="v1.3.1"
|
||||
curl -fsSL "https://dl.filippo.io/age/${AGE_VERSION}?for=linux/${ARCH_AGE}" -o /tmp/age.tar.gz
|
||||
tar -xzf /tmp/age.tar.gz -C "$BIN_DIR" --strip-components=1 age/age age/age-keygen
|
||||
chmod +x "$BIN_DIR/age" "$BIN_DIR/age-keygen"
|
||||
rm /tmp/age.tar.gz
|
||||
fi
|
||||
|
||||
if ! command -v sops &>/dev/null; then
|
||||
echo "==> Installing sops to $BIN_DIR..."
|
||||
SOPS_VERSION="v3.12.1"
|
||||
curl -fsSL "https://github.com/getsops/sops/releases/download/${SOPS_VERSION}/sops-${SOPS_VERSION}.linux.${ARCH_SOPS}" -o "$BIN_DIR/sops"
|
||||
chmod +x "$BIN_DIR/sops"
|
||||
fi
|
||||
|
||||
# ── Ensure age keypair exists ─────────────────────────────
|
||||
# Key file lives at repo root (one level up from web/)
|
||||
AGE_KEY_FILE="${SOPS_AGE_KEY_FILE:-$APP_DIR/../age-key.txt}"
|
||||
AGE_KEY_FILE="$(realpath "$AGE_KEY_FILE")"
|
||||
export SOPS_AGE_KEY_FILE="$AGE_KEY_FILE"
|
||||
|
||||
if [ ! -f "$AGE_KEY_FILE" ]; then
|
||||
echo "==> Generating age keypair at $AGE_KEY_FILE..."
|
||||
age-keygen -o "$AGE_KEY_FILE" 2>&1
|
||||
chmod 600 "$AGE_KEY_FILE"
|
||||
AGE_PUB=$(grep "public key:" "$AGE_KEY_FILE" | awk '{print $NF}')
|
||||
echo ""
|
||||
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
||||
echo "!! NEW SERVER — add this public key to .sops.yaml: !!"
|
||||
echo "!! !!"
|
||||
echo "!! $AGE_PUB !!"
|
||||
echo "!! !!"
|
||||
echo "!! Then run: sops updatekeys .env.prod.sops !!"
|
||||
echo "!! Commit, push, and re-deploy. !!"
|
||||
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Decrypt secrets ───────────────────────────────────────
|
||||
echo "==> Decrypting secrets from .env.prod.sops..."
|
||||
sops --input-type dotenv --output-type dotenv -d "$APP_DIR/../.env.prod.sops" > "$APP_DIR/.env"
|
||||
chmod 600 "$APP_DIR/.env"
|
||||
|
||||
COMPOSE="docker compose -f docker-compose.prod.yml"
|
||||
LIVE_FILE=".live-slot"
|
||||
ROUTER_CONF="router/default.conf"
|
||||
@@ -22,28 +80,29 @@ echo "==> Current: $CURRENT → Deploying: $TARGET"
|
||||
echo "==> Building $TARGET..."
|
||||
$COMPOSE --profile "$TARGET" build
|
||||
|
||||
# ── Backup DB before migration ────────────────────────────────
|
||||
|
||||
BACKUP_TAG="pre-deploy-$(date +%Y%m%d-%H%M%S)"
|
||||
echo "==> Backing up database (${BACKUP_TAG})..."
|
||||
$COMPOSE run --rm --entrypoint "" "${TARGET}-app" \
|
||||
sh -c "cp /app/data/app.db /app/data/app.db.${BACKUP_TAG} 2>/dev/null || true"
|
||||
|
||||
# ── Migrate ─────────────────────────────────────────────────
|
||||
|
||||
echo "==> Running migrations..."
|
||||
$COMPOSE --profile "$TARGET" run --rm "${TARGET}-app" \
|
||||
python -m beanflows.migrations.migrate
|
||||
|
||||
# ── Start & health check ───────────────────────────────────
|
||||
# ── Ensure router points to current live slot before --wait ──
|
||||
# nginx resolves upstream hostnames — if config points to a stopped slot,
|
||||
# the health check fails. Reset router to current slot while target starts.
|
||||
|
||||
echo "==> Starting $TARGET (waiting for health check)..."
|
||||
if ! $COMPOSE --profile "$TARGET" up -d --wait; then
|
||||
echo "!!! Health check failed — rolling back"
|
||||
$COMPOSE stop "${TARGET}-app" "${TARGET}-worker" "${TARGET}-scheduler"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Switch router ───────────────────────────────────────────
|
||||
|
||||
echo "==> Switching router to $TARGET..."
|
||||
_write_router_conf() {
|
||||
local SLOT="$1"
|
||||
mkdir -p "$(dirname "$ROUTER_CONF")"
|
||||
cat > "$ROUTER_CONF" <<NGINX
|
||||
upstream app {
|
||||
server ${TARGET}-app:5000;
|
||||
server ${SLOT}-app:5000;
|
||||
}
|
||||
|
||||
server {
|
||||
@@ -58,11 +117,46 @@ server {
|
||||
}
|
||||
}
|
||||
NGINX
|
||||
}
|
||||
|
||||
# Ensure router is running, then reload
|
||||
$COMPOSE up -d router
|
||||
if [ "$CURRENT" != "none" ]; then
|
||||
echo "==> Resetting router to current slot ($CURRENT)..."
|
||||
_write_router_conf "$CURRENT"
|
||||
$COMPOSE restart router
|
||||
fi
|
||||
|
||||
# ── Start & health check ───────────────────────────────────
|
||||
|
||||
echo "==> Starting $TARGET (waiting for health check)..."
|
||||
if ! $COMPOSE --profile "$TARGET" up -d --wait; then
|
||||
echo "!!! Health check failed — dumping logs"
|
||||
echo "--- ${TARGET}-app logs ---"
|
||||
$COMPOSE --profile "$TARGET" logs --tail=60 "${TARGET}-app" 2>&1 || true
|
||||
echo "--- router logs ---"
|
||||
$COMPOSE logs --tail=10 router 2>&1 || true
|
||||
echo "!!! Rolling back"
|
||||
$COMPOSE stop "${TARGET}-app" "${TARGET}-worker" "${TARGET}-scheduler"
|
||||
LATEST=$($COMPOSE run --rm --entrypoint "" "${TARGET}-app" \
|
||||
sh -c "ls -t /app/data/app.db.pre-deploy-* 2>/dev/null | head -1")
|
||||
if [ -n "$LATEST" ]; then
|
||||
echo "==> Restoring database from ${LATEST}..."
|
||||
$COMPOSE run --rm --entrypoint "" "${TARGET}-app" \
|
||||
sh -c "cp '${LATEST}' /app/data/app.db"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Write router config and reload (new slot is healthy) ────
|
||||
|
||||
echo "==> Switching router to $TARGET..."
|
||||
_write_router_conf "$TARGET"
|
||||
$COMPOSE exec router nginx -s reload
|
||||
|
||||
# ── Cleanup old pre-deploy backups (keep last 3) ─────────────
|
||||
|
||||
$COMPOSE run --rm --entrypoint "" "${TARGET}-app" \
|
||||
sh -c "ls -t /app/data/app.db.pre-deploy-* 2>/dev/null | tail -n +4 | xargs rm -f" || true
|
||||
|
||||
# ── Stop old slot ───────────────────────────────────────────
|
||||
|
||||
if [ "$CURRENT" != "none" ]; then
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# BeanFlows Deployment Script
|
||||
|
||||
echo "🚀 Deploying BeanFlows..."
|
||||
|
||||
# Pull latest code
|
||||
git pull origin main
|
||||
|
||||
# Build and restart containers
|
||||
docker compose build
|
||||
docker compose up -d
|
||||
|
||||
# Run migrations
|
||||
docker compose exec app uv run python -m beanflows.migrations.migrate
|
||||
|
||||
# Health check
|
||||
sleep 5
|
||||
curl -f http://localhost:5000/health || exit 1
|
||||
|
||||
echo "✅ Deployment complete!"
|
||||
Reference in New Issue
Block a user