feat(deploy): scripts/deploy-api-only.sh + Hetzner doc for manual code-only redeploys
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

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>
This commit is contained in:
2026-05-29 22:55:05 +02:00
parent c9fe717184
commit c13e8e29b5
2 changed files with 130 additions and 3 deletions

View File

@@ -559,7 +559,7 @@ docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}"
```
!!! note "After the reset"
`init_production.py` re-creates the four admin users with their **default** passwords (see `init_production.py:280-300`). Any admin-side configuration that lives in the `admin_settings` table (e.g. the manual SMTP overrides under `/admin/settings`) is wiped and must be re-applied. The `/health` endpoint reads `.build-info` which is only regenerated by `scripts/deploy.sh`, so after a manual reset it will report the **previous** commit; harmless but worth knowing.
`init_production.py` re-creates the four admin users with their **default** passwords (see `init_production.py:280-300`). Any admin-side configuration that lives in the `admin_settings` table (e.g. the manual SMTP overrides under `/admin/settings`) is wiped and must be re-applied. The `/health` endpoint reads `.build-info` which is only regenerated by `scripts/deploy.sh` or `scripts/deploy-api-only.sh` (see [Step 16.5](#165-manual-deploy)), so after a manual reset it will report the **previous** commit; harmless but worth knowing.
### Seeded Data Summary
@@ -1197,14 +1197,51 @@ deploy:
script: cd ${{ secrets.DEPLOY_PATH }} && bash scripts/deploy.sh
```
### 16.5 Manual Fallback
### 16.5 Manual Deploy
If CI is down, deploy manually:
Two manual paths, pick the right one for the change you're shipping.
#### 16.5a — Code-only fix (default for ad-hoc manual deploys)
For frontend / template / api-only changes that don't touch the Dockerfile,
requirements.txt, docker-compose.yml, or alembic migrations. Rebuilds and
restarts **only** the api container — db, redis, celery-worker, celery-beat,
flower stay running.
```bash
cd ~/apps/orion && bash scripts/deploy-api-only.sh
```
What it does (`scripts/deploy-api-only.sh`):
1. Refuses if working tree is dirty (no silent stash → no risk of pop conflicts).
2. `git pull --ff-only`.
3. **Writes `.build-info`** — this is the critical step that ensures the
`?v=<commit-sha>` cache-bust query on every shared JS/CSS URL flips to the
new SHA. Without this, browsers happily keep serving the previous
deploy's cached assets even though the new code is in the image.
4. `docker compose -f docker-compose.yml --profile full up -d --build api`.
5. Health-check with a 30s budget (tight, since the DB/Redis weren't touched).
Exit codes: `0` success, `1` git pull / dirty tree, `2` docker build/up
failed, `3` health check failed.
#### 16.5b — Full deploy (use when CI is down)
Use this when you've also changed migrations, the Dockerfile,
requirements.txt, or docker-compose.yml itself — anything that needs the
full restart-everything + migrate cycle that the CI runs. Restarts EVERY
service in the `full` profile (db, redis, api, celery-worker, celery-beat,
flower) and runs `alembic upgrade heads`.
```bash
cd ~/apps/orion && bash scripts/deploy.sh
```
Heavier — brief DB downtime, Redis is blown away (sessions / rate-limit
counters / cached anything), in-flight Celery tasks killed — so don't use
it for code-only fixes.
### 16.6 Verify
```bash

90
scripts/deploy-api-only.sh Executable file
View File

@@ -0,0 +1,90 @@
#!/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