mirror of
https://github.com/Sea-Haven-Industries/afterhours-shift-manager.git
synced 2026-09-30 20:43:12 +00:00
Prod is already a human GitHub Release. Remove the SAM tagging path so CHANGELOG.md stays App Home copy and leftover notifier IAM is destroyed on the next apply.
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@cbe3b392738ccf3f987d68400dafcf4b0624a56c # v6.2.4
|
|
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@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
|
with:
|
|
platforms: arm64
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
|
|
|
- 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
|