Files

116 lines
3.4 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Pull the latest commit, rebuild the image and restart the stack.
# Rolls back to the previous commit if the new container never turns healthy.
#
# Usage: ./scripts/update.sh [--force] [--branch <name>]
# --force rebuild and restart even when there is no new commit
# --branch branch to deploy (default: the currently checked out branch)
set -Eeuo pipefail
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_DIR"
FORCE=0
BRANCH=""
HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-120}"
while [ $# -gt 0 ]; do
case "$1" in
--force) FORCE=1; shift ;;
--branch) BRANCH="${2:?--branch needs a value}"; shift 2 ;;
*) echo "unknown argument: $1" >&2; exit 2 ;;
esac
done
log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
err() { printf '\033[1;31m!!\033[0m %s\n' "$*" >&2; }
compose() { docker compose "$@"; }
# --- preconditions -----------------------------------------------------------
command -v git >/dev/null || { err "git not found"; exit 1; }
docker compose version >/dev/null 2>&1 || { err "docker compose not available"; exit 1; }
[ -f .env ] || { err ".env missing — copy .env.example to .env and fill it in"; exit 1; }
if [ -n "$(git status --porcelain)" ]; then
err "working tree has uncommitted changes — refusing to update"
git status --short >&2
exit 1
fi
[ -n "$BRANCH" ] || BRANCH="$(git rev-parse --abbrev-ref HEAD)"
PREV_COMMIT="$(git rev-parse HEAD)"
# --- fetch -------------------------------------------------------------------
log "Fetching origin/$BRANCH"
git fetch --quiet origin "$BRANCH"
TARGET_COMMIT="$(git rev-parse "origin/$BRANCH")"
if [ "$PREV_COMMIT" = "$TARGET_COMMIT" ] && [ "$FORCE" -eq 0 ]; then
log "Already at $(git rev-parse --short HEAD) — nothing to do (use --force to rebuild anyway)"
exit 0
fi
log "Updating $(git rev-parse --short "$PREV_COMMIT") -> $(git rev-parse --short "$TARGET_COMMIT")"
git checkout --quiet "$BRANCH"
git merge --ff-only --quiet "origin/$BRANCH"
# --- health helper -----------------------------------------------------------
container_id() { compose ps -q app; }
wait_for_health() {
local deadline=$((SECONDS + HEALTH_TIMEOUT))
local cid status
while [ "$SECONDS" -lt "$deadline" ]; do
cid="$(container_id)"
if [ -n "$cid" ]; then
status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}' "$cid" 2>/dev/null || echo missing)"
case "$status" in
healthy|no-healthcheck) return 0 ;;
unhealthy) return 1 ;;
esac
fi
sleep 3
done
return 1
}
rollback() {
err "Deployment failed — rolling back to $(git rev-parse --short "$PREV_COMMIT")"
compose logs --tail 50 app >&2 || true
git reset --hard --quiet "$PREV_COMMIT"
compose build app
compose up -d app
if wait_for_health; then
err "Rolled back successfully. The stack runs the previous commit again."
else
err "ROLLBACK ALSO UNHEALTHY — manual intervention required."
fi
exit 1
}
# --- build & restart ---------------------------------------------------------
log "Building image"
compose build app || rollback
log "Restarting stack"
compose up -d app || rollback
log "Waiting for healthcheck (timeout ${HEALTH_TIMEOUT}s)"
wait_for_health || rollback
# --- cleanup -----------------------------------------------------------------
log "Pruning dangling images"
docker image prune -f >/dev/null
log "Deployed $(git rev-parse --short HEAD) — stack is healthy"