push.sh
bash
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