2026-09-09 09:58:09 +08:00
|
|
|
#!/usr/bin/env bash
|
2026-09-25 00:54:16 +08:00
|
|
|
# Deploy on the target Linux Docker host with configuration injected by Gitea.
|
|
|
|
|
# Returns nonzero on configuration/build/migration/health failure. Never removes data.
|
2026-09-09 09:58:09 +08:00
|
|
|
set -Eeuo pipefail
|
|
|
|
|
umask 077
|
|
|
|
|
|
|
|
|
|
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
|
|
|
|
# Fail before touching the host; never read a checkout's local .env file.
|
2026-09-25 00:54:16 +08:00
|
|
|
for key in WQ_EMAIL WQ_PASSWORD DATABASE_URL ADMIN_PASSWORD ENCRYPTION_KEY DATABASE_NETWORK PUBLIC_ORIGIN; do
|
2026-09-09 09:58:09 +08:00
|
|
|
if [[ -z "${!key:-}" ]]; then
|
|
|
|
|
echo "Missing required Gitea configuration: $key" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
done
|
2026-09-25 00:54:16 +08:00
|
|
|
export DEPLOY_TAG="${DEPLOY_TAG:-$(git rev-parse HEAD)}"
|
2026-09-09 09:58:09 +08:00
|
|
|
compose=(docker compose --env-file /dev/null -p wq-alpha-production -f compose.production.yaml)
|
2026-09-09 10:27:07 +08:00
|
|
|
lock_id=""
|
|
|
|
|
cleanup() {
|
|
|
|
|
local rc=$?
|
|
|
|
|
"${compose[@]}" --profile jobs ps -a || true
|
|
|
|
|
# Remove only the lock acquired by this process, never another deployment's lock.
|
|
|
|
|
if [[ -n "$lock_id" ]]; then
|
|
|
|
|
docker rm "$lock_id" >/dev/null || true
|
|
|
|
|
fi
|
|
|
|
|
exit "$rc"
|
|
|
|
|
}
|
|
|
|
|
trap cleanup EXIT
|
|
|
|
|
trap 'exit 130' INT
|
|
|
|
|
trap 'exit 143' TERM
|
2026-09-09 09:58:09 +08:00
|
|
|
|
|
|
|
|
"${compose[@]}" config --quiet
|
|
|
|
|
"${compose[@]}" --profile jobs config --quiet
|
2026-09-25 00:54:16 +08:00
|
|
|
"${compose[@]}" build backend web
|
2026-09-09 10:27:07 +08:00
|
|
|
# Docker enforces unique container names across runner jobs and checkout paths.
|
|
|
|
|
# The lock container is never started and receives no deployment credentials.
|
|
|
|
|
if ! lock_id=$(docker create --name wq-alpha-production-deploy-lock \
|
|
|
|
|
--label "wq.deploy.commit=$DEPLOY_TAG" \
|
2026-09-25 00:54:16 +08:00
|
|
|
--network none --entrypoint /bin/true "wq-alpha-production-backend:$DEPLOY_TAG"); then
|
2026-09-09 10:27:07 +08:00
|
|
|
echo 'Cannot acquire deployment lock; check Docker and other active deployments.' >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
2026-09-09 09:58:09 +08:00
|
|
|
# Validate secrets and DB connectivity before interrupting the running version.
|
2026-09-25 00:54:16 +08:00
|
|
|
"${compose[@]}" run --rm --no-deps backend python -c '
|
2026-09-09 09:58:09 +08:00
|
|
|
import asyncio
|
|
|
|
|
from app.config import Settings
|
|
|
|
|
from sqlalchemy import text
|
|
|
|
|
from sqlalchemy.ext.asyncio import create_async_engine
|
|
|
|
|
async def check():
|
|
|
|
|
settings = Settings()
|
|
|
|
|
engine = create_async_engine(settings.database_url)
|
|
|
|
|
try:
|
|
|
|
|
async with engine.connect() as connection:
|
|
|
|
|
await connection.execute(text("SELECT 1"))
|
|
|
|
|
finally:
|
|
|
|
|
await engine.dispose()
|
|
|
|
|
try:
|
|
|
|
|
asyncio.run(check())
|
|
|
|
|
except Exception:
|
|
|
|
|
raise SystemExit("Production configuration/database preflight failed; check env and database access.") from None
|
|
|
|
|
'
|
|
|
|
|
|
|
|
|
|
# Record immutable image references before switching; do not prune old images.
|
|
|
|
|
previous_images=$("${compose[@]}" images --quiet)
|
|
|
|
|
if [[ -n "$previous_images" ]]; then
|
2026-09-09 10:27:07 +08:00
|
|
|
echo "Previous deployment images (retain for rollback):"
|
|
|
|
|
docker image inspect --format '{{.Id}} {{json .RepoTags}}' $previous_images
|
2026-09-09 09:58:09 +08:00
|
|
|
fi
|
|
|
|
|
"${compose[@]}" stop web backend
|
2026-09-25 00:54:16 +08:00
|
|
|
"${compose[@]}" --profile jobs run --rm --no-deps migrate
|
|
|
|
|
"${compose[@]}" up -d --no-build --remove-orphans --wait --wait-timeout 180 backend web
|
2026-09-09 09:58:09 +08:00
|
|
|
echo "Production is healthy; release $DEPLOY_TAG"
|