2025-08-08 22:48:57 +07:00
|
|
|
#!/bin/bash
|
2025-09-01 16:18:34 +07:00
|
|
|
set -euo pipefail
|
2025-08-08 22:48:57 +07:00
|
|
|
|
2026-09-30 15:32:25 +07:00
|
|
|
# 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.
|
|
|
|
|
|
2025-08-08 22:48:57 +07:00
|
|
|
APP_NAME="apskel-pos"
|
2026-09-30 15:32:25 +07:00
|
|
|
LEGACY_CONTAINER="apskel-pos" # nama lama sebelum nama container memakai environment
|
2026-07-09 22:11:03 +07:00
|
|
|
|
2026-09-30 15:32:25 +07:00
|
|
|
usage() {
|
|
|
|
|
echo "Usage: $0 <staging|production> [--rollback] [--yes]"
|
|
|
|
|
exit 1
|
|
|
|
|
}
|
2026-07-09 22:11:03 +07:00
|
|
|
|
2026-09-30 15:32:25 +07:00
|
|
|
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"
|
2026-07-09 22:11:03 +07:00
|
|
|
PORT="4000"
|
|
|
|
|
;;
|
|
|
|
|
staging)
|
2026-09-30 15:32:25 +07:00
|
|
|
EXPECTED_BRANCH="staging"
|
2026-07-09 22:11:03 +07:00
|
|
|
PORT="4001"
|
|
|
|
|
;;
|
|
|
|
|
*)
|
2026-09-30 15:32:25 +07:00
|
|
|
usage
|
2026-07-09 22:11:03 +07:00
|
|
|
;;
|
|
|
|
|
esac
|
|
|
|
|
|
2026-09-30 15:32:25 +07:00
|
|
|
CONTAINER_NAME="$APP_NAME-$ENV_MODE"
|
2026-07-09 22:11:03 +07:00
|
|
|
IMAGE_NAME="$APP_NAME:$ENV_MODE"
|
2026-09-30 15:32:25 +07:00
|
|
|
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
|
2026-07-09 22:11:03 +07:00
|
|
|
|
|
|
|
|
echo "📦 Environment : $ENV_MODE"
|
|
|
|
|
echo "🌿 Branch : $CURRENT_BRANCH"
|
|
|
|
|
echo "🐳 Container : $CONTAINER_NAME"
|
|
|
|
|
echo "🔌 Port : $PORT"
|
2026-09-30 15:32:25 +07:00
|
|
|
echo "📁 Folder : $(pwd)"
|
|
|
|
|
[ "$ROLLBACK" = true ] && echo "⏪ Mode : ROLLBACK ke $PREVIOUS_IMAGE"
|
2026-07-09 22:11:03 +07:00
|
|
|
echo ""
|
|
|
|
|
|
2026-09-30 15:32:25 +07:00
|
|
|
# ─── Konfirmasi production ────────────────────────────────────────────────────
|
|
|
|
|
if [ "$ENV_MODE" = "production" ] && [ "$ASSUME_YES" != true ]; then
|
|
|
|
|
read -r -p "⚠️ Ini PRODUCTION. Ketik 'production' untuk lanjut: " CONFIRM
|
|
|
|
|
[ "$CONFIRM" = "production" ] || fail "Dibatalkan."
|
2026-07-09 22:11:03 +07:00
|
|
|
fi
|
2025-08-08 22:48:57 +07:00
|
|
|
|
2026-09-30 15:32:25 +07:00
|
|
|
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
|
2025-08-08 22:48:57 +07:00
|
|
|
|
2026-07-09 22:11:03 +07:00
|
|
|
echo "🐳 Building Docker image ($ENV_MODE)..."
|
|
|
|
|
docker build --target production -t "$IMAGE_NAME" .
|
2025-08-08 22:48:57 +07:00
|
|
|
|
2026-09-30 15:32:25 +07:00
|
|
|
run_container "$IMAGE_NAME"
|
2025-08-08 22:48:57 +07:00
|
|
|
|
2026-09-30 15:32:25 +07:00
|
|
|
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
|
2025-08-08 22:48:57 +07:00
|
|
|
|
2026-07-09 22:11:03 +07:00
|
|
|
echo ""
|
|
|
|
|
echo "✅ Deployment $ENV_MODE complete."
|
|
|
|
|
echo " Container : $CONTAINER_NAME"
|
|
|
|
|
echo " Port : $PORT"
|
2026-09-30 15:32:25 +07:00
|
|
|
echo " Rollback : $0 $ENV_MODE --rollback"
|