fix(deploy): require explicit environment and isolate containers per env
Deploying staging from a folder checked out on main replaced the production container, because the environment was inferred from the current branch and both environments shared the container name "apskel-pos". - Environment is now a required argument (staging|production) - Refuse to deploy when the branch doesn't match, HEAD is detached, or the tree is dirty - Container name per environment; the legacy "apskel-pos" container is only removed by production deploys - Abort if the port is held by another environment's container - Confirmation prompt for production (--yes to skip) - Fast-forward-only pull from origin/<branch> - Keep the previous image and roll back automatically when the new container is not healthy; --rollback for manual rollback Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
582dc75543
commit
9b21af8892
+142
-27
@@ -1,62 +1,177 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -euo pipefail
|
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"
|
APP_NAME="apskel-pos"
|
||||||
|
LEGACY_CONTAINER="apskel-pos" # nama lama sebelum nama container memakai environment
|
||||||
|
|
||||||
# ─── Deteksi environment dari branch aktif ───────────────────────────────────
|
usage() {
|
||||||
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
echo "Usage: $0 <staging|production> [--rollback] [--yes]"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
case "$CURRENT_BRANCH" in
|
fail() {
|
||||||
main)
|
echo "❌ $*"
|
||||||
ENV_MODE="production"
|
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"
|
PORT="4000"
|
||||||
;;
|
;;
|
||||||
staging)
|
staging)
|
||||||
ENV_MODE="staging"
|
EXPECTED_BRANCH="staging"
|
||||||
PORT="4001"
|
PORT="4001"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "❌ Branch '$CURRENT_BRANCH' tidak dikenali untuk deployment."
|
usage
|
||||||
echo " Gunakan branch 'main' (production) atau 'staging' (staging)."
|
|
||||||
exit 1
|
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
CONTAINER_NAME="$APP_NAME"
|
CONTAINER_NAME="$APP_NAME-$ENV_MODE"
|
||||||
IMAGE_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 "📦 Environment : $ENV_MODE"
|
||||||
echo "🌿 Branch : $CURRENT_BRANCH"
|
echo "🌿 Branch : $CURRENT_BRANCH"
|
||||||
echo "🐳 Container : $CONTAINER_NAME"
|
echo "🐳 Container : $CONTAINER_NAME"
|
||||||
echo "🔌 Port : $PORT"
|
echo "🔌 Port : $PORT"
|
||||||
|
echo "📁 Folder : $(pwd)"
|
||||||
|
[ "$ROLLBACK" = true ] && echo "⏪ Mode : ROLLBACK ke $PREVIOUS_IMAGE"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# ─── Pastikan config file ada ─────────────────────────────────────────────────
|
# ─── Konfirmasi production ────────────────────────────────────────────────────
|
||||||
CONFIG_FILE="infra/$ENV_MODE.yaml"
|
if [ "$ENV_MODE" = "production" ] && [ "$ASSUME_YES" != true ]; then
|
||||||
if [ ! -f "$CONFIG_FILE" ]; then
|
read -r -p "⚠️ Ini PRODUCTION. Ketik 'production' untuk lanjut: " CONFIRM
|
||||||
echo "❌ Config file '$CONFIG_FILE' tidak ditemukan."
|
[ "$CONFIRM" = "production" ] || fail "Dibatalkan."
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "🔄 Pulling latest code..."
|
run_container() {
|
||||||
git pull
|
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)..."
|
echo "🐳 Building Docker image ($ENV_MODE)..."
|
||||||
docker build --target production -t "$IMAGE_NAME" .
|
docker build --target production -t "$IMAGE_NAME" .
|
||||||
|
|
||||||
echo "🛑 Stopping and removing old container..."
|
run_container "$IMAGE_NAME"
|
||||||
docker rm -f "$CONTAINER_NAME" 2>/dev/null || true
|
|
||||||
|
|
||||||
echo "🚀 Running new container..."
|
if ! wait_healthy; then
|
||||||
docker run -d --name "$CONTAINER_NAME" \
|
echo "❌ Container baru tidak healthy. Log terakhir:"
|
||||||
-p "$PORT:4000" \
|
docker logs --tail 50 "$CONTAINER_NAME" || true
|
||||||
-e TZ=Asia/Jakarta \
|
if docker image inspect "$PREVIOUS_IMAGE" >/dev/null 2>&1; then
|
||||||
-e ENV_MODE="$ENV_MODE" \
|
echo "⏪ Rolling back ke $PREVIOUS_IMAGE..."
|
||||||
-v "$(pwd)/infra":/infra:ro \
|
run_container "$PREVIOUS_IMAGE"
|
||||||
-v "$(pwd)/templates":/templates:ro \
|
wait_healthy || fail "Rollback juga tidak healthy. Cek manual: docker logs $CONTAINER_NAME"
|
||||||
"$IMAGE_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 ""
|
||||||
echo "✅ Deployment $ENV_MODE complete."
|
echo "✅ Deployment $ENV_MODE complete."
|
||||||
echo " Container : $CONTAINER_NAME"
|
echo " Container : $CONTAINER_NAME"
|
||||||
echo " Port : $PORT"
|
echo " Port : $PORT"
|
||||||
|
echo " Rollback : $0 $ENV_MODE --rollback"
|
||||||
|
|||||||
Reference in New Issue
Block a user