mirror of
https://github.com/Sea-Haven-Industries/seahaven-ap.git
synced 2026-09-30 20:53:18 +00:00
271 lines
10 KiB
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"
|