deploy-lightsail #717
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy to Lightsail | |
| on: | |
| repository_dispatch: | |
| types: [deploy-lightsail] | |
| concurrency: | |
| group: deploy-${{ github.event.client_payload.project }} | |
| cancel-in-progress: false | |
| jobs: | |
| deploy: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout deployment-hub | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| - name: Validate project | |
| id: validate | |
| env: | |
| PROJECT: ${{ github.event.client_payload.project }} | |
| run: | | |
| CONFIG="projects/${PROJECT}.yml" | |
| if [ ! -f "$CONFIG" ]; then | |
| echo "::error::Unknown project: $PROJECT" | |
| exit 1 | |
| fi | |
| echo "config_path=$CONFIG" >> $GITHUB_OUTPUT | |
| echo "project=$PROJECT" >> $GITHUB_OUTPUT | |
| - name: Load project config | |
| id: config | |
| run: | | |
| CONFIG="${{ steps.validate.outputs.config_path }}" | |
| echo "name=$(yq '.name' $CONFIG)" >> $GITHUB_OUTPUT | |
| echo "region=$(yq '.region' $CONFIG)" >> $GITHUB_OUTPUT | |
| echo "secrets_manager_id=$(yq '.aws.secrets_manager_id' $CONFIG)" >> $GITHUB_OUTPUT | |
| echo "instance_name=$(yq '.lightsail.instance_name' $CONFIG)" >> $GITHUB_OUTPUT | |
| echo "container_name=$(yq '.lightsail.container_name' $CONFIG)" >> $GITHUB_OUTPUT | |
| # container_port: 컨테이너 내부에서 Spring/앱이 listen 하는 포트 | |
| # host_port: Lightsail 인스턴스에서 외부로 노출되는 포트 (생략 시 container_port 와 동일 → 이전 동작 유지) | |
| CONTAINER_PORT=$(yq '.lightsail.container_port // .lightsail.port' $CONFIG) | |
| HOST_PORT=$(yq '.lightsail.host_port // .lightsail.container_port // .lightsail.port' $CONFIG) | |
| echo "container_port=$CONTAINER_PORT" >> $GITHUB_OUTPUT | |
| echo "host_port=$HOST_PORT" >> $GITHUB_OUTPUT | |
| echo "health_path=$(yq '.lightsail.health_check.path' $CONFIG)" >> $GITHUB_OUTPUT | |
| echo "health_interval=$(yq '.lightsail.health_check.interval' $CONFIG)" >> $GITHUB_OUTPUT | |
| echo "health_timeout=$(yq '.lightsail.health_check.timeout' $CONFIG)" >> $GITHUB_OUTPUT | |
| echo "health_retries=$(yq '.lightsail.health_check.retries' $CONFIG)" >> $GITHUB_OUTPUT | |
| # reverse_proxy 블록 (optional) — 있으면 Caddy 사이드카로 HTTPS 종단 | |
| RP_ENABLED=$(yq '.lightsail.reverse_proxy.enabled // false' $CONFIG) | |
| RP_DOMAIN=$(yq '.lightsail.reverse_proxy.domain // ""' $CONFIG) | |
| RP_EMAIL=$(yq '.lightsail.reverse_proxy.tls.email // ""' $CONFIG) | |
| echo "rp_enabled=$RP_ENABLED" >> $GITHUB_OUTPUT | |
| echo "rp_domain=$RP_DOMAIN" >> $GITHUB_OUTPUT | |
| echo "rp_email=$RP_EMAIL" >> $GITHUB_OUTPUT | |
| ENV_VARS=$(yq -o=json -I=0 '.lightsail.env // {}' $CONFIG) | |
| echo "env_vars<<EOF" >> $GITHUB_OUTPUT | |
| echo "$ENV_VARS" >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| - name: Create GitHub Deployment | |
| id: deployment | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 | |
| env: | |
| SHA: ${{ github.event.client_payload.sha }} | |
| VERSION: ${{ github.event.client_payload.version }} | |
| with: | |
| github-token: ${{ secrets.PAT }} | |
| script: | | |
| const deployment = await github.rest.repos.createDeployment({ | |
| owner: '${{ github.repository_owner }}', | |
| repo: '${{ steps.validate.outputs.project }}', | |
| ref: process.env.SHA, | |
| environment: 'production', | |
| auto_merge: false, | |
| required_contexts: [], | |
| description: `Deploy v${process.env.VERSION} to AWS Lightsail` | |
| }); | |
| return deployment.data.id; | |
| result-encoding: string | |
| - name: Set Deployment In Progress | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 | |
| with: | |
| github-token: ${{ secrets.PAT }} | |
| script: | | |
| await github.rest.repos.createDeploymentStatus({ | |
| owner: '${{ github.repository_owner }}', | |
| repo: '${{ steps.validate.outputs.project }}', | |
| deployment_id: ${{ steps.deployment.outputs.result }}, | |
| state: 'in_progress', | |
| description: 'Deploying to AWS Lightsail...' | |
| }); | |
| - name: Configure AWS credentials | |
| uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6 | |
| with: | |
| aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| aws-region: ${{ steps.config.outputs.region }} | |
| - name: Apply ECR lifecycle policy | |
| env: | |
| PROJECT: ${{ steps.validate.outputs.project }} | |
| CONFIG: ${{ steps.validate.outputs.config_path }} | |
| run: | | |
| # config 에 ecr.lifecycle.keep_count 가 있으면 적용 (없으면 skip). | |
| # tagged 최근 keep_count 개(latest + 직전)만 보관, 나머지 + untagged(1일) 만료. | |
| KEEP=$(yq '.ecr.lifecycle.keep_count // 0' "$CONFIG") | |
| if [ "$KEEP" -gt 0 ]; then | |
| POLICY=$(jq -n --argjson keep "$KEEP" '{ | |
| rules: [ | |
| {rulePriority:1, description:"untagged 만료 (orphan 정리)", | |
| selection:{tagStatus:"untagged",countType:"sinceImagePushed",countUnit:"days",countNumber:1}, | |
| action:{type:"expire"}}, | |
| {rulePriority:2, description:"tagged 최근 N개만 보관 (latest + 직전)", | |
| selection:{tagStatus:"tagged",tagPatternList:["*"],countType:"imageCountMoreThan",countNumber:$keep}, | |
| action:{type:"expire"}} | |
| ] | |
| }') | |
| aws ecr put-lifecycle-policy --repository-name "$PROJECT" --lifecycle-policy-text "$POLICY" | |
| echo "ECR lifecycle 적용: tagged 최근 $KEEP 개 보관" | |
| else | |
| echo "ecr.lifecycle.keep_count 없음 — lifecycle skip" | |
| fi | |
| - name: Build ECR Image URI | |
| id: ecr | |
| env: | |
| PROJECT: ${{ steps.validate.outputs.project }} | |
| REGION: ${{ steps.config.outputs.region }} | |
| VERSION: ${{ github.event.client_payload.version }} | |
| run: | | |
| ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) | |
| REGISTRY="${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com" | |
| # 실행 태그는 명시 버전 우선 — latest 만 쓰면 롤백/재배포 시 항상 최신을 띄워 | |
| # 특정 버전으로 되돌릴 수 없다. version 누락 시에만 latest fallback. | |
| if [ -n "$VERSION" ] && [ "$VERSION" != "null" ]; then | |
| IMAGE_TAG="$VERSION" | |
| else | |
| IMAGE_TAG="latest" | |
| fi | |
| echo "image_uri=${REGISTRY}/${PROJECT}:${IMAGE_TAG}" >> $GITHUB_OUTPUT | |
| echo "registry=$REGISTRY" >> $GITHUB_OUTPUT | |
| - name: Resolve static env block | |
| id: envblock | |
| env: | |
| STATIC_ENV: ${{ steps.config.outputs.env_vars }} | |
| REGION: ${{ steps.config.outputs.region }} | |
| run: | | |
| # 정적 env만 주입 — Secrets Manager는 컨테이너 내부의 Spring Cloud AWS가 | |
| # 인스턴스에 마운트된 IAM 자격증명으로 직접 호출함. | |
| ENV_FROM_STATIC=$(echo "$STATIC_ENV" | jq -r 'to_entries | map("\(.key)=\(.value|tostring)") | .[]') | |
| { | |
| echo "env_block<<__ENV_EOF__" | |
| echo "$ENV_FROM_STATIC" | |
| echo "AWS_REGION=$REGION" | |
| echo "__ENV_EOF__" | |
| } >> $GITHUB_OUTPUT | |
| - name: Get Lightsail instance public IP | |
| id: lightsail | |
| env: | |
| INSTANCE: ${{ steps.config.outputs.instance_name }} | |
| run: | | |
| IP=$(aws lightsail get-instance --instance-name "$INSTANCE" --query 'instance.publicIpAddress' --output text) | |
| if [ -z "$IP" ] || [ "$IP" = "None" ]; then | |
| echo "::error::Could not resolve public IP for instance $INSTANCE" | |
| exit 1 | |
| fi | |
| echo "ip=$IP" >> $GITHUB_OUTPUT | |
| - name: Setup SSH | |
| env: | |
| SSH_KEY: ${{ secrets.LIGHTSAIL_SSH_KEY }} | |
| IP: ${{ steps.lightsail.outputs.ip }} | |
| run: | | |
| mkdir -p ~/.ssh | |
| echo "$SSH_KEY" > ~/.ssh/id_rsa | |
| chmod 600 ~/.ssh/id_rsa | |
| ssh-keyscan -H "$IP" >> ~/.ssh/known_hosts 2>/dev/null | |
| ssh -o BatchMode=yes ubuntu@"$IP" "echo ok" | |
| - name: Ensure swap (blue-green 공존 burst 흡수) | |
| env: | |
| IP: ${{ steps.lightsail.outputs.ip }} | |
| run: | | |
| # blue-green 배포 중 구·신 컨테이너가 잠깐 공존하면 RAM(2GB)을 초과할 수 있다. | |
| # swap 으로 그 순간의 burst 를 흡수해 OOMKill 방지. idempotent + 영구(fstab) + | |
| # swappiness=10 으로 평상시엔 거의 swap 안 씀(성능 영향 최소, burst 때만 사용). | |
| ssh ubuntu@"$IP" bash -s <<'REMOTE' | |
| set -euo pipefail | |
| SWAPFILE=/swapfile | |
| SWAP_SIZE_MB=2048 | |
| if ! sudo swapon --show | grep -q "$SWAPFILE"; then | |
| if [ ! -f "$SWAPFILE" ]; then | |
| echo "swap 파일 생성 (${SWAP_SIZE_MB}MB)" | |
| sudo fallocate -l "${SWAP_SIZE_MB}M" "$SWAPFILE" 2>/dev/null || \ | |
| sudo dd if=/dev/zero of="$SWAPFILE" bs=1M count="$SWAP_SIZE_MB" | |
| sudo chmod 600 "$SWAPFILE" | |
| sudo mkswap "$SWAPFILE" | |
| fi | |
| sudo swapon "$SWAPFILE" | |
| echo "swap 활성화됨" | |
| else | |
| echo "swap 이미 활성 — skip" | |
| fi | |
| # 재부팅 후 유지 | |
| if ! grep -q "$SWAPFILE" /etc/fstab; then | |
| echo "$SWAPFILE none swap sw 0 0" | sudo tee -a /etc/fstab > /dev/null | |
| fi | |
| # 평상시 swap 회피 (burst 때만) | |
| sudo sysctl -w vm.swappiness=10 > /dev/null | |
| if ! grep -q "vm.swappiness" /etc/sysctl.conf; then | |
| echo "vm.swappiness=10" | sudo tee -a /etc/sysctl.conf > /dev/null | |
| fi | |
| free -h | |
| REMOTE | |
| - name: Write env file to instance | |
| env: | |
| IP: ${{ steps.lightsail.outputs.ip }} | |
| CONTAINER_NAME: ${{ steps.config.outputs.container_name }} | |
| ENV_BLOCK: ${{ steps.envblock.outputs.env_block }} | |
| run: | | |
| ssh ubuntu@"$IP" "sudo mkdir -p /etc/${CONTAINER_NAME} && sudo chmod 750 /etc/${CONTAINER_NAME}" | |
| printf '%s\n' "$ENV_BLOCK" | ssh ubuntu@"$IP" "sudo tee /etc/${CONTAINER_NAME}/env > /dev/null && sudo chmod 640 /etc/${CONTAINER_NAME}/env" | |
| - name: Bootstrap Caddy reverse proxy (if reverse_proxy enabled) | |
| if: steps.config.outputs.rp_enabled == 'true' | |
| env: | |
| IP: ${{ steps.lightsail.outputs.ip }} | |
| RP_DOMAIN: ${{ steps.config.outputs.rp_domain }} | |
| RP_EMAIL: ${{ steps.config.outputs.rp_email }} | |
| PROJECT: ${{ steps.validate.outputs.project }} | |
| CONTAINER_NAME: ${{ steps.config.outputs.container_name }} | |
| CONTAINER_PORT: ${{ steps.config.outputs.container_port }} | |
| INSTANCE: ${{ steps.config.outputs.instance_name }} | |
| REGION: ${{ steps.config.outputs.region }} | |
| run: | | |
| # Caddy = 인스턴스당 1개 (idempotent). 메인 Caddyfile은 sites.d/*.caddy 를 import. | |
| # 프로젝트별 라우트는 sites.d/<project>.caddy 단편. | |
| if [ -z "$RP_DOMAIN" ] || [ -z "$RP_EMAIL" ]; then | |
| echo "::error::reverse_proxy.enabled=true 는 reverse_proxy.domain + reverse_proxy.tls.email 필수" | |
| exit 1 | |
| fi | |
| # 원격 부트스트랩 스크립트 — heredoc 없이 변수에 직접 담아 stdin 으로 전달. | |
| # ssh 는 `host VAR=val cmd` 로 env 전달 불가 (SendEnv 미설정) → 변수를 export 문으로 인라인. | |
| # GitHub Actions env (RP_EMAIL 등) 가 셸 확장으로 값 치환됨. printf 로 파일 생성. | |
| REMOTE_SCRIPT=" | |
| set -euo pipefail | |
| export RP_EMAIL='${RP_EMAIL}' | |
| export RP_DOMAIN='${RP_DOMAIN}' | |
| export PROJECT='${PROJECT}' | |
| export CONTAINER_NAME='${CONTAINER_NAME}' | |
| export CONTAINER_PORT='${CONTAINER_PORT}' | |
| sudo mkdir -p /etc/caddy/sites.d | |
| if [ ! -f /etc/caddy/Caddyfile ]; then | |
| printf '{\n\temail %s\n}\nimport /etc/caddy/sites.d/*.caddy\n' \"\$RP_EMAIL\" | sudo tee /etc/caddy/Caddyfile > /dev/null | |
| fi | |
| sudo docker network create proxy_net 2>/dev/null || true | |
| # Caddy 는 desired-state 로 관리 — 실행 스펙(이미지+DNS+포트+볼륨) 전체를 한 변수에 정의하고 | |
| # sha256 해시를 label(miner.caddy.spec) 로 컨테이너에 박는다. 다음 배포에서 desired 해시와 | |
| # 현재 컨테이너 label 이 다르면(스펙 변경) 또는 컨테이너가 없으면 재생성. 같으면 그대로 유지. | |
| # --dns 필수 이유: 호스트 systemd-resolved stub(127.0.0.53)은 컨테이너에서 접근 불가 → | |
| # ACME DNS 해석 실패 → 인증서 미발급 → 443 미listen. | |
| CADDY_IMAGE='caddy:2-alpine' | |
| CADDY_ARGS='--restart unless-stopped --dns 8.8.8.8 --dns 1.1.1.1 --network proxy_net -p 80:80 -p 443:443 -v /etc/caddy/Caddyfile:/etc/caddy/Caddyfile:ro -v /etc/caddy/sites.d:/etc/caddy/sites.d:ro -v caddy_data:/data -v caddy_config:/config' | |
| CADDY_SPEC=\$(printf '%s|%s' \"\$CADDY_IMAGE\" \"\$CADDY_ARGS\" | sha256sum | cut -c1-12) | |
| CADDY_CURRENT=\$(sudo docker inspect caddy --format '{{index .Config.Labels \"miner.caddy.spec\"}}' 2>/dev/null || echo '') | |
| if [ \"\$CADDY_CURRENT\" != \"\$CADDY_SPEC\" ]; then | |
| echo \"caddy 스펙 변경 (현재=\$CADDY_CURRENT desired=\$CADDY_SPEC) — 재생성\" | |
| sudo docker rm -f caddy 2>/dev/null || true | |
| sudo docker run -d --name caddy --label \"miner.caddy.spec=\$CADDY_SPEC\" \$CADDY_ARGS \"\$CADDY_IMAGE\" | |
| else | |
| echo \"caddy 스펙 동일 (\$CADDY_SPEC) — 유지\" | |
| fi | |
| printf '%s {\n\treverse_proxy %s:%s\n\tlog {\n\t\toutput stdout\n\t\tformat console\n\t}\n}\n' \"\$RP_DOMAIN\" \"\$CONTAINER_NAME\" \"\$CONTAINER_PORT\" | sudo tee /etc/caddy/sites.d/\"\$PROJECT\".caddy > /dev/null | |
| " | |
| printf '%s\n' "$REMOTE_SCRIPT" | ssh ubuntu@"$IP" bash -s | |
| # Lightsail firewall 443 OPEN (idempotent) | |
| aws lightsail open-instance-public-ports \ | |
| --instance-name "$INSTANCE" \ | |
| --region "$REGION" \ | |
| --port-info fromPort=443,toPort=443,protocol=TCP 2>/dev/null || true | |
| - name: Deploy (health-gated blue-green, zero-downtime) | |
| env: | |
| IP: ${{ steps.lightsail.outputs.ip }} | |
| REGISTRY: ${{ steps.ecr.outputs.registry }} | |
| IMAGE_URI: ${{ steps.ecr.outputs.image_uri }} | |
| CONTAINER_NAME: ${{ steps.config.outputs.container_name }} | |
| HOST_PORT: ${{ steps.config.outputs.host_port }} | |
| CONTAINER_PORT: ${{ steps.config.outputs.container_port }} | |
| HEALTH_PATH: ${{ steps.config.outputs.health_path }} | |
| INTERVAL: ${{ steps.config.outputs.health_interval }} | |
| RETRIES: ${{ steps.config.outputs.health_retries }} | |
| REGION: ${{ steps.config.outputs.region }} | |
| RP_ENABLED: ${{ steps.config.outputs.rp_enabled }} | |
| run: | | |
| ssh ubuntu@"$IP" REGISTRY="$REGISTRY" IMAGE_URI="$IMAGE_URI" CONTAINER_NAME="$CONTAINER_NAME" \ | |
| HOST_PORT="$HOST_PORT" CONTAINER_PORT="$CONTAINER_PORT" HEALTH_PATH="$HEALTH_PATH" \ | |
| INTERVAL="$INTERVAL" RETRIES="$RETRIES" REGION="$REGION" RP_ENABLED="$RP_ENABLED" bash -s <<'REMOTE' | |
| set -euo pipefail | |
| NEW="${CONTAINER_NAME}-new" | |
| ATTEMPTS=$((RETRIES * 6)) | |
| STOP_TIMEOUT=30 # graceful stop: SIGTERM 후 이 시간까지 대기, 초과 시 SIGKILL | |
| aws ecr get-login-password --region "$REGION" | sudo docker login --username AWS --password-stdin "$REGISTRY" | |
| sudo docker pull "$IMAGE_URI" | |
| # 이전 배포 실패로 남은 -new 잔재 정리 (있으면) | |
| sudo docker rm -f "$NEW" 2>/dev/null || true | |
| # 1) 새 컨테이너를 -new 로 기동 — 기존 $CONTAINER_NAME 은 그대로 트래픽 받는 중. | |
| # /home/ubuntu/.aws ro 마운트 → Spring Cloud AWS 가 Secrets Manager 직접 호출. | |
| if [ "$RP_ENABLED" = "true" ]; then | |
| # reverse_proxy: -new 는 proxy_net 참여, host 포트 미노출 (Caddy 가 외부 종단). | |
| sudo docker run -d \ | |
| --name "$NEW" \ | |
| --restart unless-stopped \ | |
| --network proxy_net \ | |
| --env-file /etc/"$CONTAINER_NAME"/env \ | |
| -v /home/ubuntu/.aws:/root/.aws:ro \ | |
| --expose "$CONTAINER_PORT" \ | |
| "$IMAGE_URI" | |
| else | |
| # 기본 모드: 기존이 host_port 를 점유 중이므로 -new 는 임시 포트로 띄워 검증. | |
| # host 포트 충돌 없이 health 확인 후 정식 포트로 교체. | |
| TMP_PORT=$((HOST_PORT + 20000)) | |
| sudo docker run -d \ | |
| --name "$NEW" \ | |
| --restart unless-stopped \ | |
| --env-file /etc/"$CONTAINER_NAME"/env \ | |
| -v /home/ubuntu/.aws:/root/.aws:ro \ | |
| -p "$TMP_PORT":"$CONTAINER_PORT" \ | |
| "$IMAGE_URI" | |
| fi | |
| # 2) -new 내부 health 폴링. 실패 시 -new 만 제거하고 기존 유지 → 무중단 보장. | |
| healthy=false | |
| for i in $(seq 1 "$ATTEMPTS"); do | |
| STATUS=$(sudo docker exec "$NEW" sh -c "wget -q -O /dev/null -S http://localhost:${CONTAINER_PORT}${HEALTH_PATH} 2>&1 | awk '/HTTP/{print \$2}' | tail -1" 2>/dev/null || echo 000) | |
| if [ "$STATUS" = "200" ]; then | |
| echo "New container healthy (HTTP 200) on attempt $i/$ATTEMPTS" | |
| healthy=true | |
| break | |
| fi | |
| echo "Attempt $i/$ATTEMPTS: new container HTTP $STATUS — retrying in ${INTERVAL}s" | |
| sleep "$INTERVAL" | |
| done | |
| if [ "$healthy" != "true" ]; then | |
| echo "::error::New container failed health check — keeping existing container, aborting deploy" | |
| echo "--- new container logs (tail) ---" | |
| sudo docker logs --tail 50 "$NEW" 2>&1 || true | |
| sudo docker rm -f "$NEW" 2>/dev/null || true | |
| exit 1 | |
| fi | |
| # 3) 교체 — 기존을 graceful stop 후, -new 를 정식 이름으로 승격. | |
| sudo docker stop --time "$STOP_TIMEOUT" "$CONTAINER_NAME" 2>/dev/null || true | |
| sudo docker rm "$CONTAINER_NAME" 2>/dev/null || true | |
| if [ "$RP_ENABLED" = "true" ]; then | |
| # 이름만 정식으로 변경 — proxy_net/spec 동일하므로 rename 으로 충분. | |
| sudo docker rename "$NEW" "$CONTAINER_NAME" | |
| # Caddy 가 컨테이너명→IP 를 재해석하도록 reload (rename 으로 IP 가 바뀜). | |
| sudo docker exec caddy caddy reload --config /etc/caddy/Caddyfile 2>/dev/null || \ | |
| sudo docker restart caddy | |
| else | |
| # 기본 모드: 임시 포트로 검증된 -new 를 정식 포트로 재기동 (포트 변경은 rename 불가). | |
| # 같은 이미지/env 라 검증된 것과 동일하지만, 정식 포트 기동분도 health 한 번 재확인. | |
| sudo docker rm -f "$NEW" 2>/dev/null || true | |
| sudo docker run -d \ | |
| --name "$CONTAINER_NAME" \ | |
| --restart unless-stopped \ | |
| --env-file /etc/"$CONTAINER_NAME"/env \ | |
| -v /home/ubuntu/.aws:/root/.aws:ro \ | |
| -p "$HOST_PORT":"$CONTAINER_PORT" \ | |
| "$IMAGE_URI" | |
| promoted=false | |
| for i in $(seq 1 "$ATTEMPTS"); do | |
| STATUS=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "http://localhost:${HOST_PORT}${HEALTH_PATH}" 2>/dev/null || echo 000) | |
| if [ "$STATUS" = "200" ]; then promoted=true; break; fi | |
| sleep "$INTERVAL" | |
| done | |
| if [ "$promoted" != "true" ]; then | |
| echo "::error::Promoted container failed health check on host port" | |
| sudo docker logs --tail 50 "$CONTAINER_NAME" 2>&1 || true | |
| exit 1 | |
| fi | |
| fi | |
| sudo docker image prune -af | |
| echo "Deploy complete — $CONTAINER_NAME now running $IMAGE_URI" | |
| REMOTE | |
| - name: Set Deployment Success | |
| if: success() | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 | |
| env: | |
| VERSION: ${{ github.event.client_payload.version }} | |
| with: | |
| github-token: ${{ secrets.PAT }} | |
| script: | | |
| await github.rest.repos.createDeploymentStatus({ | |
| owner: '${{ github.repository_owner }}', | |
| repo: '${{ steps.validate.outputs.project }}', | |
| deployment_id: ${{ steps.deployment.outputs.result }}, | |
| state: 'success', | |
| description: `Deployed v${process.env.VERSION} to production` | |
| }); | |
| - name: Set Deployment Failure | |
| if: failure() | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 | |
| with: | |
| github-token: ${{ secrets.PAT }} | |
| script: | | |
| await github.rest.repos.createDeploymentStatus({ | |
| owner: '${{ github.repository_owner }}', | |
| repo: '${{ steps.validate.outputs.project }}', | |
| deployment_id: ${{ steps.deployment.outputs.result }}, | |
| state: 'failure', | |
| description: 'Deployment failed' | |
| }); | |
| - name: Create GitHub Release | |
| if: success() | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 | |
| env: | |
| CHANGELOG: ${{ github.event.client_payload.changelog }} | |
| VERSION: ${{ github.event.client_payload.version }} | |
| SHA: ${{ github.event.client_payload.sha }} | |
| with: | |
| github-token: ${{ secrets.PAT }} | |
| script: | | |
| const version = process.env.VERSION; | |
| const tagName = `v${version}`; | |
| const project = '${{ steps.validate.outputs.project }}'; | |
| const sha = process.env.SHA; | |
| try { | |
| await github.rest.git.getRef({ | |
| owner: '${{ github.repository_owner }}', | |
| repo: project, | |
| ref: `tags/${tagName}` | |
| }); | |
| console.log(`Tag ${tagName} already exists, skipping release creation`); | |
| return; | |
| } catch (e) { | |
| // Tag doesn't exist, create release | |
| } | |
| const changelog = process.env.CHANGELOG || ''; | |
| const footer = `\n---\n**Tag:** ${tagName}\n**Commit:** ${sha.substring(0, 7)}`; | |
| const body = changelog ? changelog + footer : `Deployed to production via AWS Lightsail.${footer}`; | |
| await github.rest.repos.createRelease({ | |
| owner: '${{ github.repository_owner }}', | |
| repo: project, | |
| tag_name: tagName, | |
| name: tagName, | |
| body: body, | |
| draft: false, | |
| prerelease: false, | |
| target_commitish: sha | |
| }); |