mirror of
https://github.com/Sea-Haven-Industries/afterhours-shift-manager.git
synced 2026-09-30 16:03:12 +00:00
245 lines
8.5 KiB
YAML
245 lines
8.5 KiB
YAML
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
|