name: Deploy API # Fargate image CD. GitHub Actions builds the API image, pushes to ECR, and # registers a new task definition. Terraform owns the cluster, service, ALB, # and ignores container_definitions / task_definition. # # push to main -> GitHub Environment dev, at github.sha # workflow_dispatch -> GitHub Environment dev at a chosen ref # # Cluster, service, ECR, and task env come from SSM after assuming the # Environment's DEPLOY_ROLE_ARN. Terraform owns /seahaven-ap/deploy/task-environment; # this workflow applies that JSON and writes GIT_SHA. Nothing here creates an HCP run. # Prod is AP-12. on: push: branches: [main] paths: - "packages/api/**" - "packages/shared/**" - "package.json" - "package-lock.json" - "Dockerfile" - ".dockerignore" - "scripts/patch-ecs-task-def.py" - ".github/workflows/deploy-api.yaml" workflow_dispatch: inputs: environment: description: "Target Environment" required: true type: choice options: [dev] ref: description: "Git ref to build and deploy (branch or SHA). Empty means the workflow ref." required: false type: string default: "" permissions: contents: read jobs: target: name: Resolve target runs-on: ubuntu-latest timeout-minutes: 5 outputs: environment: ${{ steps.resolve.outputs.environment }} ref: ${{ steps.resolve.outputs.ref }} steps: - id: resolve env: EVENT_NAME: ${{ github.event_name }} GITHUB_REF_NAME_IN: ${{ github.ref }} GITHUB_SHA_IN: ${{ github.sha }} INPUT_ENVIRONMENT: ${{ inputs.environment }} INPUT_REF: ${{ inputs.ref }} run: | set -euo pipefail case "${EVENT_NAME}" in push) if [ "${GITHUB_REF_NAME_IN}" != "refs/heads/main" ]; then echo "push deploys only run from main" >&2 exit 1 fi environment=dev ref="${GITHUB_SHA_IN}" ;; workflow_dispatch) environment="${INPUT_ENVIRONMENT:-dev}" if [ "${environment}" != "dev" ]; then echo "only GitHub Environment dev is allowed" >&2 exit 1 fi ref="${INPUT_REF:-${GITHUB_SHA_IN}}" ;; *) echo "unsupported event ${EVENT_NAME}" >&2 exit 1 ;; esac { echo "environment=${environment}" echo "ref=${ref}" } >> "${GITHUB_OUTPUT}" echo "Deploying ${ref} to ${environment}" deploy: name: Deploy API to ${{ needs.target.outputs.environment }} needs: target runs-on: ubuntu-latest timeout-minutes: 30 environment: ${{ needs.target.outputs.environment }} concurrency: group: deploy-api-${{ needs.target.outputs.environment }} cancel-in-progress: false permissions: contents: read id-token: write env: AWS_REGION: us-east-1 DEPLOY_ROLE_ARN: ${{ vars.DEPLOY_ROLE_ARN }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ needs.target.outputs.ref }} persist-credentials: false - name: Resolve commit id: commit run: | set -euo pipefail sha="$(git rev-parse HEAD)" echo "sha=${sha}" >> "${GITHUB_OUTPUT}" echo "Building ${sha}" - name: Configure AWS credentials using OIDC uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0 with: role-to-assume: ${{ env.DEPLOY_ROLE_ARN }} aws-region: us-east-1 audience: sts.amazonaws.com - name: Get deploy parameters id: deploy run: | set -euo pipefail get_param() { aws ssm get-parameter --name "$1" --query Parameter.Value --output text } CLUSTER=$(get_param /seahaven-ap/deploy/cluster) SERVICE=$(get_param /seahaven-ap/deploy/service) FAMILY=$(get_param /seahaven-ap/deploy/task-family) ECR=$(get_param /seahaven-ap/deploy/ecr-repository) CONTAINER=$(get_param /seahaven-ap/deploy/container-name) DIST_ID=$(get_param /seahaven-ap/deploy/distribution-id) DOMAIN=$(aws cloudfront get-distribution --id "${DIST_ID}" --query Distribution.DomainName --output text) { echo "cluster=${CLUSTER}" echo "service=${SERVICE}" echo "family=${FAMILY}" echo "ecr=${ECR}" echo "container=${CONTAINER}" echo "site_url=https://${DOMAIN}" } >> "${GITHUB_OUTPUT}" - name: Login to Amazon ECR uses: aws-actions/amazon-ecr-login@03f1aad4c6c7ffd436567f42f9384779290529bd # v2.1.7 - name: Build image env: ECR: ${{ steps.deploy.outputs.ecr }} GIT_SHA: ${{ steps.commit.outputs.sha }} ENVIRONMENT: ${{ needs.target.outputs.environment }} run: | set -euo pipefail docker build \ --platform linux/amd64 \ --build-arg "GIT_SHA=${GIT_SHA}" \ -t "${ECR}:${GIT_SHA}" \ -t "${ECR}:${ENVIRONMENT}" \ . - name: Push image env: ECR: ${{ steps.deploy.outputs.ecr }} GIT_SHA: ${{ steps.commit.outputs.sha }} ENVIRONMENT: ${{ needs.target.outputs.environment }} run: | set -euo pipefail docker push "${ECR}:${GIT_SHA}" docker push "${ECR}:${ENVIRONMENT}" - name: Register task definition, migrate, and update service env: CLUSTER: ${{ steps.deploy.outputs.cluster }} SERVICE: ${{ steps.deploy.outputs.service }} FAMILY: ${{ steps.deploy.outputs.family }} CONTAINER: ${{ steps.deploy.outputs.container }} IMAGE: ${{ steps.deploy.outputs.ecr }}:${{ steps.commit.outputs.sha }} GIT_SHA: ${{ steps.commit.outputs.sha }} run: | set -euo pipefail TASK_ENV_JSON="$(aws ssm get-parameter \ --name /seahaven-ap/deploy/task-environment \ --with-decryption \ --query Parameter.Value \ --output text)" export TASK_ENV_JSON aws ecs describe-task-definition \ --task-definition "${FAMILY}" \ --query taskDefinition \ --output json \ | python3 scripts/patch-ecs-task-def.py > /tmp/task-def.json REV="$(aws ecs register-task-definition --cli-input-json file:///tmp/task-def.json --query taskDefinition.revision --output text)" NET="$(aws ecs describe-services --cluster "${CLUSTER}" --services "${SERVICE}" \ --query 'services[0].networkConfiguration.awsvpcConfiguration' --output json)" export NET SUBNETS="$(python3 -c 'import json,os; print(",".join(json.loads(os.environ["NET"])["subnets"]))')" SGS="$(python3 -c 'import json,os; print(",".join(json.loads(os.environ["NET"])["securityGroups"]))')" TASK_ARN="$(aws ecs run-task \ --cluster "${CLUSTER}" \ --task-definition "${FAMILY}:${REV}" \ --launch-type FARGATE \ --network-configuration "awsvpcConfiguration={subnets=[${SUBNETS}],securityGroups=[${SGS}],assignPublicIp=ENABLED}" \ --overrides "{\"containerOverrides\":[{\"name\":\"${CONTAINER}\",\"command\":[\"node\",\"packages/api/dist/db/migrate.js\"],\"environment\":[{\"name\":\"DEV_AUTH_BYPASS\",\"value\":\"false\"}]}]}" \ --query 'tasks[0].taskArn' --output text)" aws ecs wait tasks-stopped --cluster "${CLUSTER}" --tasks "${TASK_ARN}" EXIT="$(aws ecs describe-tasks --cluster "${CLUSTER}" --tasks "${TASK_ARN}" \ --query 'tasks[0].containers[0].exitCode' --output text)" if [ "${EXIT}" != "0" ]; then echo "migrate task ${TASK_ARN} exited ${EXIT}" >&2 exit 1 fi aws ecs update-service \ --cluster "${CLUSTER}" \ --service "${SERVICE}" \ --task-definition "${FAMILY}:${REV}" \ --force-new-deployment \ >/dev/null aws ecs wait services-stable --cluster "${CLUSTER}" --services "${SERVICE}" - name: Verify API health env: SITE_URL: ${{ steps.deploy.outputs.site_url }} EXPECTED_SHA: ${{ steps.commit.outputs.sha }} run: bash scripts/verify-api-health.sh