Template
116 lines
3.4 KiB
Bash
Executable File
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"
|