seahaven-ap/.github/workflows/deploy-api.yaml

271 lines
10 KiB
YAML

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. The workflow file must be main.
# inputs.ref is only the image source. Deploy scripts stay
# on github.sha, which is the trusted workflow commit.
#
# 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)
if [ "${GITHUB_REF_NAME_IN}" != "refs/heads/main" ]; then
echo "workflow_dispatch deploys only run from main" >&2
exit 1
fi
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:
- name: Checkout trusted workflow
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
persist-credentials: false
path: ci
- name: Checkout image source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.target.outputs.ref }}
persist-credentials: false
path: src
- name: Resolve commit
id: commit
working-directory: src
run: |
set -euo pipefail
sha="$(git rev-parse HEAD)"
echo "sha=${sha}" >> "${GITHUB_OUTPUT}"
echo "Building ${sha}"
- name: Require deploy role
run: |
set -euo pipefail
if [ -z "${DEPLOY_ROLE_ARN}" ]; then
echo "DEPLOY_ROLE_ARN is empty. Create GitHub Environment dev and set it after the seahaven-ap-dev apply, then re-run." >&2
exit 1
fi
- 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() {
local name="$1" err value
err="$(mktemp)"
if ! value="$(aws ssm get-parameter --name "${name}" --query Parameter.Value --output text 2>"${err}")"; then
if grep -q ParameterNotFound "${err}"; then
echo "SSM parameter ${name} does not exist yet. Apply the seahaven-ap-dev workspace, then re-run this workflow." >&2
else
cat "${err}" >&2
fi
rm -f "${err}"
exit 1
fi
rm -f "${err}"
printf '%s\n' "${value}"
}
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 }}
working-directory: src
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 "${GITHUB_WORKSPACE}/ci/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"]))')"
RUN_JSON="$(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\"}]}]}" \
--output json)"
TASK_ARN="$(printf '%s' "${RUN_JSON}" | python3 -c 'import json,sys; data=json.load(sys.stdin); tasks=data.get("tasks") or []; print(tasks[0].get("taskArn") or "" if tasks else "")')"
if [ -z "${TASK_ARN}" ] || [ "${TASK_ARN}" = "None" ]; then
echo "ecs run-task did not start a migrate task" >&2
printf '%s' "${RUN_JSON}" | python3 -c 'import json,sys; data=json.load(sys.stdin); print(json.dumps(data.get("failures") or [], indent=2))' >&2
exit 1
fi
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 "${GITHUB_WORKSPACE}/ci/scripts/verify-api-health.sh"