name: Deploy API # Fargate image CD (PLAT-216). GitHub Actions builds the Flask 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 -> dev, at github.sha # release: published -> prod, at the release tag # workflow_dispatch -> chosen environment at a chosen ref # # Releases are cut by a human with `gh release create vX.Y.Z --target main`. # Nothing here creates an HCP run. on: push: branches: [main] paths-ignore: - "terraform/**" - "docs/**" - "*.md" - ".github/workflows/ci.yaml" - ".github/workflows/labeler.yml" - ".github/workflows/dependency-review.yml" release: types: [published] workflow_dispatch: inputs: environment: description: "Target Environment" required: true type: choice options: [dev, prod] ref: description: "Git ref to build and deploy (tag, 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 }} RELEASE_TAG: ${{ github.event.release.tag_name }} REPO: ${{ github.repository }} GH_TOKEN: ${{ github.token }} 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}" ;; release) environment=prod ref="${RELEASE_TAG}" status="$(gh api "repos/${REPO}/compare/main...${RELEASE_TAG}" --jq .status)" if [ "${status}" != "behind" ] && [ "${status}" != "identical" ]; then echo "release tag ${RELEASE_TAG} is not on main (compare status: ${status})" >&2 exit 1 fi ;; workflow_dispatch) environment="${INPUT_ENVIRONMENT}" 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 /afterhours-shift-manager/deploy/cluster) SERVICE=$(get_param /afterhours-shift-manager/deploy/service) FAMILY=$(get_param /afterhours-shift-manager/deploy/task-family) ECR=$(get_param /afterhours-shift-manager/deploy/ecr-repository) CONTAINER=$(get_param /afterhours-shift-manager/deploy/container-name) API_URL=$(get_param /afterhours-shift-manager/deploy/api-url) { echo "cluster=${CLUSTER}" echo "service=${SERVICE}" echo "family=${FAMILY}" echo "ecr=${ECR}" echo "container=${CONTAINER}" echo "api_url=${API_URL}" } >> "${GITHUB_OUTPUT}" - name: Set up QEMU uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 with: platforms: arm64 - name: Set up Docker Buildx uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Login to Amazon ECR uses: aws-actions/amazon-ecr-login@03f1aad4c6c7ffd436567f42f9384779290529bd # v2.1.7 - name: Build and push image env: ECR: ${{ steps.deploy.outputs.ecr }} GIT_SHA: ${{ steps.commit.outputs.sha }} ENVIRONMENT: ${{ needs.target.outputs.environment }} run: | set -euo pipefail docker buildx build \ --platform linux/arm64 \ --build-arg "GIT_SHA=${GIT_SHA}" \ -t "${ECR}:${GIT_SHA}" \ -t "${ECR}:${ENVIRONMENT}" \ --push \ . - name: Register task definition 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 aws ecs describe-task-definition \ --task-definition "${FAMILY}" \ --query taskDefinition \ --output json \ | python3 -c ' import json, os, sys td = json.load(sys.stdin) for key in ( "taskDefinitionArn", "revision", "status", "requiresAttributes", "compatibilities", "registeredAt", "registeredBy", "deregisteredAt", ): td.pop(key, None) image = os.environ["IMAGE"] sha = os.environ["GIT_SHA"] name = os.environ["CONTAINER"] for container in td["containerDefinitions"]: if container["name"] != name: continue container["image"] = image env = {item["name"]: item["value"] for item in container.get("environment", [])} env["GIT_SHA"] = sha container["environment"] = [{"name": key, "value": value} for key, value in env.items()] container.pop("command", None) json.dump(td, sys.stdout) ' > /tmp/task-def.json REV="$(aws ecs register-task-definition --cli-input-json file:///tmp/task-def.json --query taskDefinition.revision --output text)" 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 health SHA env: API_URL: ${{ steps.deploy.outputs.api_url }} EXPECTED_SHA: ${{ steps.commit.outputs.sha }} run: | set -euo pipefail for _ in 1 2 3 4 5 6; do BODY="$(curl -fsS "${API_URL}/api/health" || true)" echo "${BODY}" if echo "${BODY}" | python3 -c 'import json,os,sys; d=json.load(sys.stdin); sys.exit(0 if d.get("sha")==os.environ["EXPECTED_SHA"] else 1)'; then exit 0 fi sleep 10 done echo "health SHA did not match ${EXPECTED_SHA}" >&2 exit 1