From f253e39c2cfa796245e76199afa194b3150239a8 Mon Sep 17 00:00:00 2001 From: Deeman Date: Thu, 26 Feb 2026 10:59:07 +0100 Subject: [PATCH] feat(deploy): port padelnomics deploy.sh improvements to web/deploy.sh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- web/deploy.sh | 126 ++++++++++++++++++++++++++++++++++++------ web/scripts/deploy.sh | 22 -------- 2 files changed, 110 insertions(+), 38 deletions(-) delete mode 100644 web/scripts/deploy.sh diff --git a/web/deploy.sh b/web/deploy.sh index 153c709..4426b69 100644 --- a/web/deploy.sh +++ b/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..." -mkdir -p "$(dirname "$ROUTER_CONF")" -cat > "$ROUTER_CONF" < "$ROUTER_CONF" < 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 diff --git a/web/scripts/deploy.sh b/web/scripts/deploy.sh deleted file mode 100644 index cdce459..0000000 --- a/web/scripts/deploy.sh +++ /dev/null @@ -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!"