Files
orion/scripts/deploy-api-only.sh
Samir Boulahtit c13e8e29b5
Some checks failed
CI / pytest (push) Has been cancelled
CI / ruff (push) Successful in 18s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
feat(deploy): scripts/deploy-api-only.sh + Hetzner doc for manual code-only redeploys
Manual deploys had been using a bare `git pull && docker compose up -d
--build api` sequence, which works for the container itself but silently
skipped writing `.build-info`. The stale `.build-info` left
`?v=<commit-sha>` pointing at the previous deploy's SHA on every shared
JS/CSS URL — so browsers happily kept cached pre-fix assets even after
a successful rebuild. Bit us today: ~5 hours of "is this even deployed?"
debugging on the loyalty-dashboard redirect-flicker fix.

deploy.sh wasn't a substitute because it's a CI/CD script: stashes
working tree, runs alembic, restarts every service in the full profile
(db, redis, api, celery-worker, celery-beat, flower), 60s health budget.
Heavy and disruptive for an api-only hotfix.

New scripts/deploy-api-only.sh fills the gap with the narrow path:

  - Refuses if working tree is dirty (no silent stash → no pop conflicts).
  - git pull --ff-only.
  - Writes .build-info (the critical missing step).
  - docker compose -f docker-compose.yml --profile full up -d --build api
    (only the api service — db/redis/celery untouched).
  - Tight 30s health budget since DB doesn't need to come back up.
  - Exit codes 0/1/2/3 for clean automation.

docs/deployment/hetzner-server-setup.md §16.5 split into 16.5a
(code-only — points at the new script as the default) and 16.5b
(full deploy fallback — kept the existing deploy.sh path for migrations
/ Dockerfile / docker-compose / requirements changes). §12 footnote on
.build-info refreshed to mention both scripts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 22:55:05 +02:00

91 lines
4.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# =============================================================================
# Orion Manual API-Only Redeploy
# =============================================================================
# Usage: cd ~/apps/orion && bash scripts/deploy-api-only.sh
#
# Narrow, code-only redeploy of the api container for ad-hoc fixes. Compared
# to the full scripts/deploy.sh (used by Gitea CI/CD), this script:
# - Does NOT stash/pop local changes (fails loudly if working tree is dirty
# so you can decide what to do, instead of silently rolling them through).
# - Rebuilds and restarts ONLY the api service — leaves db, redis,
# celery-worker, celery-beat, flower untouched (no Redis flush, no
# in-flight Celery task kills, no DB downtime).
# - Skips alembic upgrade (manual redeploys are usually code-only).
# - Health-checks with a tighter 30s budget since the DB doesn't need to
# come back up.
#
# Use this when shipping a frontend/template fix or other api-only change.
# Use bash scripts/deploy.sh instead when running migrations, changing
# Dockerfile / requirements.txt / docker-compose.yml, or hitting any
# infrastructure config.
#
# Exit codes:
# 0 — success
# 1 — git pull failed (non-fast-forward, or dirty working tree blocking pull)
# 2 — docker compose build/up failed
# 3 — health check failed
# =============================================================================
set -euo pipefail
COMPOSE="docker compose -f docker-compose.yml --profile full"
HEALTH_URL="http://localhost:8001/health"
HEALTH_RETRIES=6
HEALTH_INTERVAL=5
log() { echo "[deploy-api] $(date '+%H:%M:%S') $*"; }
# ── 1. Refuse if working tree is dirty ──────────────────────────────────────
# Unlike deploy.sh we do not stash, because stash+pop has a history of
# producing conflicts when prod has hand-edited config files. Surface the
# state so you can deal with it manually.
if [ -n "$(git status --porcelain)" ]; then
log "ERROR: working tree has local changes — refusing to deploy."
log " Commit, stash, or revert them yourself, then re-run."
git status --short
exit 1
fi
# ── 2. Pull latest code (fast-forward only) ─────────────────────────────────
log "Pulling latest code …"
if ! git pull --ff-only; then
log "ERROR: git pull failed (non-fast-forward?). Resolve and re-run."
exit 1
fi
# ── 3. Write build info ─────────────────────────────────────────────────────
# This file is bind-mounted into the api container and read by
# app/core/build_info.py → templates_config._asset_version(). It drives
# the ?v=<sha> cache-bust on every shared JS/CSS URL. If it stays stale,
# browsers keep cached pre-fix assets even after a successful rebuild.
log "Writing build info …"
printf '{"commit":"%s","deployed_at":"%s"}\n' \
"$(git rev-parse --short=8 HEAD)" \
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
> .build-info
# ── 4. Rebuild + recreate api ONLY ───────────────────────────────────────────
log "Rebuilding api container …"
if ! $COMPOSE up -d --build api; then
log "ERROR: docker compose up --build api failed"
exit 2
fi
# ── 5. Health check (tight budget — DB/Redis weren't touched) ───────────────
log "Waiting for health check ($HEALTH_URL) …"
for i in $(seq 1 "$HEALTH_RETRIES"); do
if curl -sf "$HEALTH_URL" > /dev/null 2>&1; then
log "Health check passed (attempt $i/$HEALTH_RETRIES)"
log "Deploy complete. ?v=$(git rev-parse --short=8 HEAD) now live."
exit 0
fi
log "Health check attempt $i/$HEALTH_RETRIES failed, retrying in ${HEALTH_INTERVAL}s …"
sleep "$HEALTH_INTERVAL"
done
log "ERROR: health check failed after $HEALTH_RETRIES attempts"
log " Container may still be starting — check 'docker compose --profile full ps'"
log " and 'docker compose --profile full logs api --tail=50'."
exit 3