gabriel / musehub public
push.sh bash
207 lines 8.0 KB
Raw
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ breaking 153 days ago
1 #!/usr/bin/env bash
2 # MuseHub deploy orchestrator — build, push to ECR, trigger blue-green via SSM.
3 #
4 # Usage:
5 # bash deploy/push.sh staging # deploy to staging only
6 # bash deploy/push.sh prod # deploy to prod only
7 # bash deploy/push.sh staging prod # staging first, then prod
8 #
9 # What it does:
10 # 1. Builds a linux/amd64 Docker image from the local repo.
11 # 2. Tags it with <commit-hash>-<timestamp> for traceability.
12 # 3. Pushes the image to ECR (musehub/musehub).
13 # 4. Sends an SSM command to each target instance to run deploy.sh,
14 # which pulls the image and performs a zero-downtime blue-green swap.
15 # 5. Polls SSM until the deploy completes or fails, streaming the output.
16 #
17 # Prerequisites (one-time, already done):
18 # - AWS CLI configured (musehub-infra as default profile)
19 # - Docker Desktop running
20 # - musehub-infra has ecr push permissions (musehub-ecr-push policy)
21 # - musehub-ec2-ssm role has ecr pull permissions (musehub-ecr-pull policy)
22 # - AWS CLI installed on instances (run deploy/bootstrap-instance.sh once)
23 #
24 # Rollback to a previous image:
25 # IMAGE_TAG=<previous-tag> bash deploy/push.sh staging
26 # (skips build+push, triggers SSM with the specified tag directly)
27
28 set -euo pipefail
29
30 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
31 REPO_DIR="$(dirname "$SCRIPT_DIR")"
32 ECOSYSTEM_DIR="$(dirname "$REPO_DIR")"
33
34 ECR_REGISTRY="992382692655.dkr.ecr.us-east-1.amazonaws.com"
35 ECR_REPO="musehub/musehub"
36 ECR_IMAGE="${ECR_REGISTRY}/${ECR_REPO}"
37 REGION="us-east-1"
38
39 STAGING_INSTANCE="i-07547cd20bee2dea5"
40 PROD_INSTANCE="i-0855d6efe7fa1a49d"
41
42 # ── Parse targets ─────────────────────────────────────────────────────────────
43
44 if [ $# -eq 0 ]; then
45 echo "Usage: bash deploy/push.sh [staging] [prod]"
46 echo " staging deploy to staging.musehub.ai"
47 echo " prod deploy to musehub.ai"
48 echo " staging prod staging first, then prod"
49 echo ""
50 echo "Rollback (skips build+push, redeploys a previous tag):"
51 echo " IMAGE_TAG=<tag> bash deploy/push.sh staging"
52 exit 1
53 fi
54
55 TARGETS=()
56 for arg in "$@"; do
57 case "$arg" in
58 staging|prod) TARGETS+=("$arg") ;;
59 *) echo "Unknown target: $arg (must be staging or prod)" >&2; exit 1 ;;
60 esac
61 done
62
63 # ── Helpers ───────────────────────────────────────────────────────────────────
64
65 log() { echo "[push] $*"; }
66 die() { echo "[push] ERROR: $*" >&2; exit 1; }
67
68 # ── Image tag ─────────────────────────────────────────────────────────────────
69
70 # If IMAGE_TAG is already set (rollback mode), skip build+push.
71 if [ -n "${IMAGE_TAG:-}" ]; then
72 log "Rollback mode — using existing tag: $IMAGE_TAG"
73 SKIP_BUILD=true
74 else
75 COMMIT_HASH=$(muse -C "$REPO_DIR" rev-parse HEAD --json 2>/dev/null \
76 | python3 -c "import sys,json; cid=json.load(sys.stdin)['commit_id']; print(cid.removeprefix('sha256:')[:8])" \
77 2>/dev/null || echo "local")
78 IMAGE_TAG="${COMMIT_HASH}-$(date +%Y%m%d%H%M%S)"
79 SKIP_BUILD=false
80 fi
81
82 FULL_IMAGE="${ECR_IMAGE}:${IMAGE_TAG}"
83 log "Target image: $FULL_IMAGE"
84
85 # ── Build ─────────────────────────────────────────────────────────────────────
86
87 if [ "$SKIP_BUILD" = false ]; then
88 log "[1/3] Building image for linux/amd64..."
89 docker build \
90 --platform linux/amd64 \
91 --tag "$FULL_IMAGE" \
92 --tag "${ECR_IMAGE}:latest" \
93 -f "$REPO_DIR/Dockerfile" \
94 "$ECOSYSTEM_DIR"
95 log "Build complete."
96
97 # ── Push to ECR via crane ─────────────────────────────────────────────────
98 # crane bypasses Docker Desktop's VPNKit proxy, which drops connections
99 # mid-upload on large layer pushes. Never use `docker push` to ECR.
100
101 log "[2/3] Saving image and pushing to ECR via crane..."
102 CRANE_TAR="/tmp/musehub-${IMAGE_TAG}.tar"
103 docker save "$FULL_IMAGE" -o "$CRANE_TAR"
104 aws ecr get-login-password --region "$REGION" | \
105 crane auth login "$ECR_REGISTRY" --username AWS --password-stdin
106 crane push "$CRANE_TAR" "$FULL_IMAGE"
107 crane push "$CRANE_TAR" "${ECR_IMAGE}:latest"
108 rm -f "$CRANE_TAR"
109 log "Push complete. Tag: $IMAGE_TAG"
110 else
111 log "[1/3] Skipping build (rollback mode)."
112 log "[2/3] Skipping push (rollback mode)."
113 fi
114
115 # ── Trigger deploy via SSM ────────────────────────────────────────────────────
116
117 log "[3/3] Triggering deploy on: ${TARGETS[*]}"
118
119 deploy_to() {
120 local env="$1"
121 local instance_id="$2"
122
123 log ""
124 log "→ Deploying to $env ($instance_id)..."
125
126 # Sync deploy.sh and nginx-cf.conf to the instance before running.
127 # This ensures the instance always runs current versions — not stale copies
128 # left over from initial provisioning or a previous deploy.
129 local deploy_sh_b64
130 deploy_sh_b64=$(base64 -i "$SCRIPT_DIR/deploy.sh" | tr -d '\n')
131
132 local nginx_conf_b64
133 nginx_conf_b64=$(base64 -i "$SCRIPT_DIR/nginx-cf.conf" | tr -d '\n')
134
135 local cmd_id
136 cmd_id=$(aws ssm send-command \
137 --region "$REGION" \
138 --instance-ids "$instance_id" \
139 --document-name "AWS-RunShellScript" \
140 --parameters "commands=[
141 \"echo '${deploy_sh_b64}' | base64 -d > /opt/musehub/deploy/deploy.sh && chmod +x /opt/musehub/deploy/deploy.sh\",
142 \"echo '${nginx_conf_b64}' | base64 -d > /opt/musehub/deploy/nginx-cf.conf\",
143 \"export ECR_IMAGE=${ECR_IMAGE}\",
144 \"export IMAGE_TAG=${IMAGE_TAG}\",
145 \"bash /opt/musehub/deploy/deploy.sh\"
146 ]" \
147 --comment "musehub ${IMAGE_TAG} → ${env}" \
148 --timeout-seconds 600 \
149 --query "Command.CommandId" \
150 --output text)
151
152 log " SSM command ID: $cmd_id"
153 log " Polling for completion (up to 10 min)..."
154
155 local status
156 for i in $(seq 1 120); do
157 sleep 5
158 status=$(aws ssm get-command-invocation \
159 --region "$REGION" \
160 --command-id "$cmd_id" \
161 --instance-id "$instance_id" \
162 --query "Status" \
163 --output text 2>/dev/null || echo "Pending")
164
165 case "$status" in
166 Success)
167 log " ✅ $env deploy succeeded."
168 echo ""
169 aws ssm get-command-invocation \
170 --region "$REGION" \
171 --command-id "$cmd_id" \
172 --instance-id "$instance_id" \
173 --query "StandardOutputContent" \
174 --output text 2>/dev/null || true
175 return 0
176 ;;
177 Failed|Cancelled|TimedOut|Cancelling)
178 log " ❌ $env deploy FAILED (status: $status)"
179 echo ""
180 aws ssm get-command-invocation \
181 --region "$REGION" \
182 --command-id "$cmd_id" \
183 --instance-id "$instance_id" \
184 --query "[StandardOutputContent, StandardErrorContent]" \
185 --output text 2>/dev/null || true
186 return 1
187 ;;
188 InProgress|Pending|Delayed)
189 printf "."
190 ;;
191 esac
192 done
193
194 die "$env deploy timed out after 10 min. Check SSM: $cmd_id"
195 }
196
197 for target in "${TARGETS[@]}"; do
198 case "$target" in
199 staging) deploy_to "staging" "$STAGING_INSTANCE" ;;
200 prod) deploy_to "prod" "$PROD_INSTANCE" ;;
201 esac
202 done
203
204 log ""
205 log "All done."
206 log " Tag: $IMAGE_TAG"
207 log " Image: $FULL_IMAGE"
File History 1 commit
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor 153 days ago