#!/usr/bin/env bash set -Eeuo pipefail deploy_root="${1:?deployment root is required}" release_id="${2:?release id is required}" archive="${3:?release archive is required}" if [[ "$deploy_root" != /* || "$deploy_root" == "/" ]]; then echo "DEPLOY_PATH must be an absolute directory other than /." >&2 exit 2 fi if [[ ! "$release_id" =~ ^[0-9a-f]{7,64}$ ]]; then echo "The release id must be a Git commit SHA." >&2 exit 2 fi if [[ ! -f "$archive" ]]; then echo "Release archive not found: $archive" >&2 exit 2 fi releases_dir="$deploy_root/releases" release_dir="$releases_dir/$release_id" current_link="$deploy_root/current" staging_dir="$releases_dir/.incoming-$release_id-$$" cleanup() { rm -f -- "$archive" if [[ -d "$staging_dir" ]]; then rm -rf -- "$staging_dir" fi } trap cleanup EXIT mkdir -p -- "$releases_dir" if [[ ! -d "$release_dir" ]]; then mkdir -- "$staging_dir" tar -xzf "$archive" -C "$staging_dir" mv -- "$staging_dir" "$release_dir" fi if [[ ! -f "$release_dir/compose.yaml" ]]; then echo "The release does not contain compose.yaml." >&2 exit 2 fi previous_release="" if [[ -L "$current_link" ]]; then previous_release="$(readlink -f -- "$current_link" || true)" fi compose() { local directory="$1" shift docker compose \ --project-name portfolio \ --file "$directory/compose.yaml" \ "$@" } wait_until_healthy() { local directory="$1" local container_id status container_id="$(compose "$directory" ps -q portfolio-app)" if [[ -z "$container_id" ]]; then echo "The portfolio-app container was not created." >&2 return 1 fi for _ in {1..30}; do status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container_id")" case "$status" in healthy|running) return 0 ;; unhealthy|exited|dead) echo "Container entered state: $status" >&2 return 1 ;; esac sleep 2 done echo "Container did not become healthy within 60 seconds." >&2 return 1 } deploy() { local directory="$1" compose "$directory" config --quiet compose "$directory" up --detach --build --remove-orphans wait_until_healthy "$directory" } if deploy "$release_dir"; then new_link="$deploy_root/.current-$release_id-$$" ln -s -- "$release_dir" "$new_link" mv -Tf -- "$new_link" "$current_link" echo "Deployment succeeded: $release_id" exit 0 fi echo "Deployment failed; attempting rollback." >&2 if [[ -n "$previous_release" && -f "$previous_release/compose.yaml" ]]; then deploy "$previous_release" echo "Rollback succeeded: $(basename "$previous_release")" >&2 else echo "No previous release is available to roll back to." >&2 fi exit 1