ci: convert onto org HCP reusables (PLAT-222) (#214)
Some checks are pending
Deploy API / Deploy API to dev (push) Waiting to run
Deploy API / Deploy API to prod (push) Waiting to run

* ci: convert onto org HCP reusables

Switch Fargate CD and CI to the v1.0.13 org workflows, emit ci-complete, and retarget githubdeploy OIDC to the reusable plus the thin caller.

* chore(security): retarget githubdeploy Checkov suppression

The OIDC dual-claim edit shifted CKV_AWS_111 from line 40 to 49. Permissions are unchanged.

* style: apply formatter

* ci: pin org reusables to v1.0.14

Drop collect-only and requirements from the python lint caller now that ci-python-app is lint-only.

* test(ci): probe autofix with a ruff format violation

* style: apply formatter

---------

Co-authored-by: sea-haven-auto-fix[bot] <5037331+sea-haven-auto-fix[bot]@users.noreply.github.com>
This commit is contained in:
Adam Moussa 2026-09-22 20:01:17 +00:00 • committed by GitHub
parent 7574fc471a
commit f632020b20
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 129 additions and 239 deletions

View file

@ -1,32 +0,0 @@
name: Terraform CI
on:
pull_request:
branches: [main]
paths:
- "terraform/**"
- ".github/workflows/ci-terraform.yaml"
permissions:
contents: read
jobs:
terraform:
runs-on: ubuntu-latest
defaults:
run:
working-directory: terraform
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
with:
terraform_version: "1.9.8"
- name: Terraform fmt
run: terraform fmt -check -recursive
- name: Terraform init
run: terraform init -backend=false
- name: Terraform validate
run: terraform validate

View file

@ -1,21 +1,37 @@
name: CI
on:
pull_request:
branches: [main]
branches: [main, hotfix/**, release/**]
merge_group:
push:
branches: [hotfix/**, release/**]
permissions:
contents: read
jobs:
ci:
uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-app.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
autofix:
if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork
uses: Sea-Haven-Industries/.github/.github/workflows/ci-autofix.yaml@fd4113241058d4709f80d5c29f7d5c43030ae6fa # v1.0.14
permissions:
contents: write
secrets: inherit
with:
format-command: ruff format .
lint-fix-command: ruff check --fix .
extra-command: terraform fmt -recursive terraform
terraform-version: "1.9.8"
lint:
needs: autofix
if: always() && !cancelled() && (needs.autofix.result == 'skipped' || needs.autofix.outputs.committed != 'true')
uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-app.yaml@fd4113241058d4709f80d5c29f7d5c43030ae6fa # v1.0.14
with:
python-version: "3.12.14"
requirements: "requirements-api.txt"
collect-only: false
template-js:
needs: autofix
if: always() && !cancelled() && (needs.autofix.result == 'skipped' || needs.autofix.outputs.committed != 'true')
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
@ -38,6 +54,8 @@ jobs:
run: npm run openapi:lint
test:
needs: autofix
if: always() && !cancelled() && (needs.autofix.result == 'skipped' || needs.autofix.outputs.committed != 'true')
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
@ -63,3 +81,30 @@ jobs:
- name: Run tests
run: pytest
terraform:
needs: autofix
if: always() && !cancelled() && (needs.autofix.result == 'skipped' || needs.autofix.outputs.committed != 'true')
uses: Sea-Haven-Industries/.github/.github/workflows/ci-terraform.yaml@fd4113241058d4709f80d5c29f7d5c43030ae6fa # v1.0.14
with:
terraform-version: "1.9.8"
ci-complete:
name: ci-complete
needs: [autofix, lint, template-js, test, terraform]
if: always() && !cancelled() && (needs.autofix.result == 'skipped' || needs.autofix.outputs.committed != 'true')
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Require portions
env:
LINT: ${{ needs.lint.result }}
TEMPLATE_JS: ${{ needs.template-js.result }}
TEST: ${{ needs.test.result }}
TERRAFORM: ${{ needs.terraform.result }}
run: |
set -euo pipefail
test "${LINT}" = success
test "${TEMPLATE_JS}" = success
test "${TEST}" = success
test "${TERRAFORM}" = success

View file

@ -1,8 +1,8 @@
name: Deploy API
# Fargate image CD (PLAT-215). 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.
# Fargate image CD. 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
@ -20,7 +20,6 @@ on:
- "*.md"
- ".github/workflows/weekly-menu.yml"
- ".github/workflows/ci.yml"
- ".github/workflows/ci-terraform.yaml"
release:
types: [published]
workflow_dispatch:
@ -40,199 +39,33 @@ 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
deploy-dev:
name: Deploy API to dev
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.environment == 'dev')
uses: Sea-Haven-Industries/.github/.github/workflows/cd-hcp-fargate.yaml@fd4113241058d4709f80d5c29f7d5c43030ae6fa # v1.0.14
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
secrets: inherit
with:
ref: ${{ needs.target.outputs.ref }}
persist-credentials: false
environment: dev
ref: ${{ inputs.ref }}
ssm-prefix: /meal-order-manager/deploy
docker-platform: linux/amd64
extra-task-env: '{"SENTRY_DSN_PARAM":"/meal-order-manager/sentry-dsn"}'
- 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
deploy-prod:
name: Deploy API to prod
if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.environment == 'prod')
uses: Sea-Haven-Industries/.github/.github/workflows/cd-hcp-fargate.yaml@fd4113241058d4709f80d5c29f7d5c43030ae6fa # v1.0.14
permissions:
contents: read
id-token: write
secrets: inherit
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 /meal-order-manager/deploy/cluster)
SERVICE=$(get_param /meal-order-manager/deploy/service)
FAMILY=$(get_param /meal-order-manager/deploy/task-family)
ECR=$(get_param /meal-order-manager/deploy/ecr-repository)
CONTAINER=$(get_param /meal-order-manager/deploy/container-name)
API_URL=$(get_param /meal-order-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: 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 build \
--build-arg "GIT_SHA=${GIT_SHA}" \
-t "${ECR}:${GIT_SHA}" \
-t "${ECR}:${ENVIRONMENT}" \
.
docker push "${ECR}:${GIT_SHA}"
docker push "${ECR}:${ENVIRONMENT}"
- 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
env["SENTRY_DSN_PARAM"] = "/meal-order-manager/sentry-dsn"
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
environment: prod
ref: ${{ github.event.release.tag_name || inputs.ref }}
ssm-prefix: /meal-order-manager/deploy
docker-platform: linux/amd64
ship-gate: true
extra-task-env: '{"SENTRY_DSN_PARAM":"/meal-order-manager/sentry-dsn"}'

View file

@ -9,8 +9,8 @@
"justification": "The meals API ALB is the CloudFront HTTP origin for orders.seahaven.com. TLS and WAF terminate at CloudFront. Restricting the security group to the CloudFront managed prefix list would block GitHub-hosted weekly-menu HMAC publish, which must call the origin with X-Meals-Publish-Key. Application gates are HMAC on /api/publish, Cognito or Google Bearer on admin, and public submit only. Accepted as the HTTP-origin design for PLAT-215; TLS on the ALB is a follow-up."
},
{
"id": "checkov-CKV_AWS_111-40",
"justification": "githubdeploy-meal-order-manager needs ecr:GetAuthorizationToken and ecs:RegisterTaskDefinition, both of which AWS documents as Resource *. Trust is pinned to environment:dev or environment:prod plus job_workflow_ref on deploy-api.yaml at refs/heads/main or refs/tags/v*. iam:PassRole is limited to the ECS task and execution roles. ECR push and SSM parameter reads are already resource-scoped."
"id": "checkov-CKV_AWS_111-49",
"justification": "LINE SHIFT ONLY: the OIDC dual-claim change adds a workflow_ref condition and retargets job_workflow_ref, shifting github_deploy from 40 to 49. The permission document is unchanged. Original justification: githubdeploy-meal-order-manager needs ecr:GetAuthorizationToken and ecs:RegisterTaskDefinition, both of which AWS documents as Resource *. Trust is pinned to environment:dev or environment:prod, job_workflow_ref on org cd-hcp-fargate.yaml@*, and workflow_ref on the thin deploy-api.yaml caller at refs/heads/main or refs/tags/v*. iam:PassRole is limited to the ECS task and execution roles. ECR push and SSM parameter reads are already resource-scoped."
}
]
}

View file

@ -188,8 +188,7 @@ meal-order-manager/
├── .github/workflows/
│ ├── weekly-menu.yml # Monday cron: scrape + HMAC publish + notify
│ ├── deploy-api.yaml # Image CD to Fargate
│ ├── ci.yml # PR checks (pytest, template JS, OpenAPI lint)
│ └── ci-terraform.yaml # terraform fmt / validate
│ └── ci.yml # PR checks (lint, pytest, template JS, terraform, ci-complete)
├── terraform/ # HCP Terraform (cluster, ALB, ECR, jobs queue)
├── openapi.yaml # Employee HTTP contract (Redocly recommended)
├── .redocly.yaml

View file

@ -1,4 +1,5 @@
# GitHub Actions OIDC role for .github/workflows/deploy-api.yaml.
# GitHub Actions OIDC role for the thin deploy-api.yaml caller of
# org reusable cd-hcp-fargate.yaml.
data "aws_iam_policy_document" "github_deploy_assume" {
statement {
@ -29,6 +30,14 @@ data "aws_iam_policy_document" "github_deploy_assume" {
condition {
test = "StringLike"
variable = "token.actions.githubusercontent.com:job_workflow_ref"
values = [
"Sea-Haven-Industries/.github/.github/workflows/cd-hcp-fargate.yaml@*",
]
}
condition {
test = "StringLike"
variable = "token.actions.githubusercontent.com:workflow_ref"
values = [
"Sea-Haven-Industries/meal-order-manager/.github/workflows/deploy-api.yaml@refs/heads/main",
"Sea-Haven-Industries/meal-order-manager/.github/workflows/deploy-api.yaml@refs/tags/v*",

View file

@ -0,0 +1,35 @@
"""githubdeploy OIDC trust pins the org reusable and the thin caller."""
from pathlib import Path
IAM = Path(__file__).resolve().parents[1] / "terraform" / "iam_github_deploy.tf"
def test_github_deploy_trust_uses_org_reusable_and_caller():
text = IAM.read_text()
assert "token.actions.githubusercontent.com:sub" in text
assert "repo:Sea-Haven-Industries/meal-order-manager:environment:dev" in text
assert "repo:Sea-Haven-Industries/meal-order-manager:environment:prod" in text
assert (
"Sea-Haven-Industries/.github/.github/workflows/cd-hcp-fargate.yaml@*" in text
)
assert "token.actions.githubusercontent.com:workflow_ref" in text
assert (
"Sea-Haven-Industries/meal-order-manager/.github/workflows/deploy-api.yaml@refs/heads/main"
in text
)
assert (
"Sea-Haven-Industries/meal-order-manager/.github/workflows/deploy-api.yaml@refs/tags/v*"
in text
)
assert "cd-hcp-spa.yaml" not in text
assert "token.actions.githubusercontent.com:workflow_ref" in text
assert (
"Sea-Haven-Industries/meal-order-manager/.github/workflows/deploy-api.yaml@refs/heads/main"
in text
)
assert (
"Sea-Haven-Industries/meal-order-manager/.github/workflows/deploy-api.yaml@refs/tags/v*"
in text
)
assert "cd-hcp-spa.yaml" not in text

View file

@ -36,4 +36,5 @@ def test_terraform_does_not_embed_a_sentry_dsn():
def test_deploy_api_injects_sentry_dsn_param():
workflow = (ROOT / ".github/workflows/deploy-api.yaml").read_text()
assert 'env["SENTRY_DSN_PARAM"] = "/meal-order-manager/sentry-dsn"' in workflow
assert "extra-task-env:" in workflow
assert '"SENTRY_DSN_PARAM":"/meal-order-manager/sentry-dsn"' in workflow