#!/bin/bash set -euo pipefail # Usage: # ./deployment.sh staging deploy staging (branch staging, port 4001) # ./deployment.sh production deploy production (branch main, port 4000) # ./deployment.sh production --rollback kembali ke image sebelum deploy terakhir # Tambahkan --yes untuk melewati konfirmasi production. APP_NAME="apskel-pos" LEGACY_CONTAINER="apskel-pos" # nama lama sebelum nama container memakai environment usage() { echo "Usage: $0 [--rollback] [--yes]" exit 1 } fail() { echo "❌ $*" exit 1 } # ─── Argumen ────────────────────────────────────────────────────────────────── ENV_MODE="${1:-}" shift || true ROLLBACK=false ASSUME_YES=false for arg in "$@"; do case "$arg" in --rollback) ROLLBACK=true ;; --yes|-y) ASSUME_YES=true ;; *) usage ;; esac done # Environment wajib disebut eksplisit, tidak lagi ditebak dari branch aktif case "$ENV_MODE" in production) EXPECTED_BRANCH="main" PORT="4000" ;; staging) EXPECTED_BRANCH="staging" PORT="4001" ;; *) usage ;; esac CONTAINER_NAME="$APP_NAME-$ENV_MODE" IMAGE_NAME="$APP_NAME:$ENV_MODE" PREVIOUS_IMAGE="$APP_NAME:$ENV_MODE-previous" CONFIG_FILE="infra/$ENV_MODE.yaml" # ─── Validasi repo ──────────────────────────────────────────────────────────── CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) if [ "$CURRENT_BRANCH" = "HEAD" ]; then fail "Repo sedang detached HEAD. Jalankan 'git checkout $EXPECTED_BRANCH' dulu." fi if [ "$CURRENT_BRANCH" != "$EXPECTED_BRANCH" ]; then fail "Deploy $ENV_MODE harus dari branch '$EXPECTED_BRANCH', folder ini di branch '$CURRENT_BRANCH'. Pakai folder terpisah per environment, jangan pindah branch di folder yang sedang dipakai container lain." fi if [ -n "$(git status --porcelain --untracked-files=no)" ]; then fail "Ada perubahan yang belum di-commit di folder ini. Bersihkan dulu sebelum deploy." fi [ -f "$CONFIG_FILE" ] || fail "Config file '$CONFIG_FILE' tidak ditemukan." # ─── Pastikan port tidak dipakai container environment lain ────────────────── OTHER_ON_PORT=$(docker ps --filter "publish=$PORT" --format '{{.Names}}' \ | grep -vx "$CONTAINER_NAME" \ | { if [ "$ENV_MODE" = "production" ]; then grep -vx "$LEGACY_CONTAINER"; else cat; fi; } \ || true) if [ -n "$OTHER_ON_PORT" ]; then fail "Port $PORT sedang dipakai container lain: $OTHER_ON_PORT" fi echo "📦 Environment : $ENV_MODE" echo "🌿 Branch : $CURRENT_BRANCH" echo "🐳 Container : $CONTAINER_NAME" echo "🔌 Port : $PORT" echo "📁 Folder : $(pwd)" [ "$ROLLBACK" = true ] && echo "⏪ Mode : ROLLBACK ke $PREVIOUS_IMAGE" echo "" # ─── Konfirmasi production ──────────────────────────────────────────────────── if [ "$ENV_MODE" = "production" ] && [ "$ASSUME_YES" != true ]; then read -r -p "⚠️ Ini PRODUCTION. Ketik 'production' untuk lanjut: " CONFIRM [ "$CONFIRM" = "production" ] || fail "Dibatalkan." fi run_container() { local image="$1" echo "🛑 Stopping and removing old container..." docker rm -f "$CONTAINER_NAME" 2>/dev/null || true # Container production lama masih bernama 'apskel-pos'; staging tidak boleh menyentuhnya if [ "$ENV_MODE" = "production" ]; then docker rm -f "$LEGACY_CONTAINER" 2>/dev/null || true fi echo "🚀 Running container from $image..." docker run -d --name "$CONTAINER_NAME" \ --restart unless-stopped \ -p "$PORT:4000" \ -e TZ=Asia/Jakarta \ -e ENV_MODE="$ENV_MODE" \ -v "$(pwd)/infra":/infra:ro \ -v "$(pwd)/templates":/templates:ro \ "$image" >/dev/null } wait_healthy() { echo "🩺 Waiting for healthcheck..." for _ in $(seq 1 36); do local status status=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$CONTAINER_NAME" 2>/dev/null || echo "missing") case "$status" in healthy) return 0 ;; unhealthy|exited|dead|missing) return 1 ;; esac sleep 5 done return 1 } # ─── Rollback ───────────────────────────────────────────────────────────────── if [ "$ROLLBACK" = true ]; then docker image inspect "$PREVIOUS_IMAGE" >/dev/null 2>&1 || fail "Image '$PREVIOUS_IMAGE' tidak ada, tidak bisa rollback." run_container "$PREVIOUS_IMAGE" wait_healthy || fail "Container hasil rollback tidak healthy. Cek: docker logs $CONTAINER_NAME" docker tag "$PREVIOUS_IMAGE" "$IMAGE_NAME" echo "✅ Rollback $ENV_MODE complete." exit 0 fi # ─── Deploy ─────────────────────────────────────────────────────────────────── echo "🔄 Pulling latest code (origin/$EXPECTED_BRANCH)..." git fetch origin "$EXPECTED_BRANCH" git merge --ff-only "origin/$EXPECTED_BRANCH" echo " Commit: $(git log -1 --format='%h %s')" # Simpan image yang sedang jalan untuk rollback if docker image inspect "$IMAGE_NAME" >/dev/null 2>&1; then docker tag "$IMAGE_NAME" "$PREVIOUS_IMAGE" fi echo "🐳 Building Docker image ($ENV_MODE)..." docker build --target production -t "$IMAGE_NAME" . run_container "$IMAGE_NAME" if ! wait_healthy; then echo "❌ Container baru tidak healthy. Log terakhir:" docker logs --tail 50 "$CONTAINER_NAME" || true if docker image inspect "$PREVIOUS_IMAGE" >/dev/null 2>&1; then echo "⏪ Rolling back ke $PREVIOUS_IMAGE..." run_container "$PREVIOUS_IMAGE" wait_healthy || fail "Rollback juga tidak healthy. Cek manual: docker logs $CONTAINER_NAME" # Jangan biarkan image rusak jadi 'previous' di deploy berikutnya docker tag "$PREVIOUS_IMAGE" "$IMAGE_NAME" fail "Deploy gagal, $ENV_MODE sudah dikembalikan ke image sebelumnya." fi fail "Deploy gagal dan tidak ada image sebelumnya untuk rollback." fi echo "" echo "✅ Deployment $ENV_MODE complete." echo " Container : $CONTAINER_NAME" echo " Port : $PORT" echo " Rollback : $0 $ENV_MODE --rollback"