ai_qc/backend/scripts/deploy.sh
nickviljoen 57f1848eb1 Add manual deploy/rollback/health scripts; remove stale rsync deploy
deploy.sh [dev|prod <tag>] handles git pull or tag checkout, reinstalls
deps only if requirements.txt changed, restarts the service, runs a
smoke test, and auto-rolls back on failure. rollback.sh reverts to the
checkpoint written by the last deploy (or to an explicit commit).
health-check.sh is a one-liner for "is the app alive?" checks.

Replaces the placeholder-config rsync-based deploy-to-prod.sh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 18:34:26 +02:00

162 lines
4.3 KiB
Bash
Executable file

#!/bin/bash
# AI QC deploy script.
#
# Usage:
# deploy.sh dev Pull latest develop → restart service
# deploy.sh prod <tag> Check out a specific tag → restart service
# deploy.sh dev --dry-run Show what would change, make no changes
#
# Runs on the target server (not your laptop). Needs sudo for systemctl.
# Saves a rollback checkpoint to .last_deploy_rollback before changing anything,
# and auto-rolls back if the post-deploy smoke test fails.
set -euo pipefail
APP_DIR=/opt/ai_qc
SERVICE=ai-qc.service
HEALTH_URL=http://127.0.0.1:7183/health
ROLLBACK_FILE="$APP_DIR/.last_deploy_rollback"
MODE=${1:-}
shift || true
DRY_RUN=false
TARGET_TAG=""
case "$MODE" in
dev)
for arg in "$@"; do
[[ "$arg" == "--dry-run" ]] && DRY_RUN=true
done
;;
prod)
TARGET_TAG=${1:-}
shift || true
for arg in "$@"; do
[[ "$arg" == "--dry-run" ]] && DRY_RUN=true
done
if [[ -z "$TARGET_TAG" ]]; then
echo "Usage: $0 prod <tag> [--dry-run]"
exit 1
fi
;;
""|-h|--help)
cat <<EOF
Usage:
$(basename "$0") dev [--dry-run] Deploy latest develop to this server
$(basename "$0") prod <tag> [--dry-run] Deploy a specific tag to this server
Run on the target server. Requires sudo for systemctl restart.
EOF
exit 0
;;
*)
echo "Unknown mode: $MODE"
echo "Try: $(basename "$0") --help"
exit 1
;;
esac
cd "$APP_DIR"
if [[ ! -d .git ]]; then
echo "ERROR: $APP_DIR is not a git repo"
exit 1
fi
CURRENT_REV=$(git rev-parse HEAD)
CURRENT_SHORT=$(git rev-parse --short HEAD)
echo "============================================"
echo " AI QC deploy ($MODE)"
echo "============================================"
echo "Server: $(hostname)"
echo "Current: $CURRENT_SHORT $(git log -1 --format='%s' HEAD)"
echo ""
echo "Fetching latest refs..."
git fetch --tags --prune --quiet
if [[ "$MODE" == "dev" ]]; then
TARGET_REF="origin/develop"
else
if ! git rev-parse --verify --quiet "refs/tags/$TARGET_TAG^{commit}" > /dev/null; then
echo "ERROR: Tag '$TARGET_TAG' not found after fetch"
exit 1
fi
TARGET_REF="refs/tags/$TARGET_TAG"
fi
TARGET_REV=$(git rev-parse "$TARGET_REF")
TARGET_SHORT=$(git rev-parse --short "$TARGET_REF")
if [[ "$CURRENT_REV" == "$TARGET_REV" ]]; then
echo "Already at $TARGET_SHORT — nothing to do."
exit 0
fi
echo "Target: $TARGET_SHORT $(git log -1 --format='%s' "$TARGET_REF")"
echo ""
echo "Commits to apply:"
git log --oneline "$CURRENT_REV..$TARGET_REV" | head -20
CHANGE_COUNT=$(git log --oneline "$CURRENT_REV..$TARGET_REV" | wc -l | tr -d ' ')
if [[ $CHANGE_COUNT -gt 20 ]]; then
echo " ... and $((CHANGE_COUNT - 20)) more"
fi
echo ""
REQS_CHANGED=false
if git diff --name-only "$CURRENT_REV" "$TARGET_REV" | grep -qE "(^|/)requirements.txt$"; then
REQS_CHANGED=true
echo "Note: requirements.txt changed — pip install will run."
echo ""
fi
if [[ "$DRY_RUN" == "true" ]]; then
echo "Dry run — no changes made."
exit 0
fi
read -r -p "Proceed with deploy? (y/N): " confirm
if [[ ! $confirm =~ ^[Yy]$ ]]; then
echo "Cancelled."
exit 0
fi
echo "$CURRENT_REV" > "$ROLLBACK_FILE"
echo "Applying changes..."
git reset --hard "$TARGET_REV"
if [[ "$REQS_CHANGED" == "true" ]]; then
echo "Installing updated dependencies..."
"$APP_DIR/venv/bin/pip" install -q -r "$APP_DIR/requirements.txt"
fi
echo "Restarting $SERVICE..."
sudo systemctl restart "$SERVICE"
sleep 3
echo "Smoke testing $HEALTH_URL..."
if curl -sf -o /dev/null "$HEALTH_URL"; then
NEW_SHORT=$(git rev-parse --short HEAD)
echo ""
echo "Deploy OK. Now at $NEW_SHORT."
echo "Rollback target saved: $CURRENT_SHORT (run rollback.sh last to revert)"
exit 0
fi
echo ""
echo "Smoke test failed — rolling back to $CURRENT_SHORT..."
git reset --hard "$CURRENT_REV"
sudo systemctl restart "$SERVICE"
sleep 3
if curl -sf -o /dev/null "$HEALTH_URL"; then
echo "Rolled back successfully. Service healthy at $CURRENT_SHORT."
echo "Investigate: sudo journalctl -u $SERVICE -n 100"
exit 1
fi
echo "ROLLBACK ALSO FAILED. Service is in a broken state."
echo "sudo systemctl status $SERVICE"
echo "sudo journalctl -u $SERVICE -n 100"
exit 2