mirror of
https://github.com/Sea-Haven-Industries/.github.git
synced 2026-09-30 16:23:11 +00:00
Compare commits
50 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee5b843ca1 | ||
|
|
892580d7d7 | ||
|
|
c9134742ce | ||
|
|
8e6b8e8665 | ||
|
|
0a1010e632 | ||
|
|
6fc4ca31e1 | ||
|
|
bf14925fcf | ||
|
|
acaf2bfc0d | ||
|
|
2e2b3a282f | ||
|
|
61f15d78b5 | ||
|
|
fd41132410 | ||
|
|
216604ad67 | ||
|
|
22c47f924f | ||
|
|
08d8ca31a9 | ||
|
|
514552df92 | ||
|
|
9781774f04 | ||
|
|
9b7b464d5c | ||
|
|
4a6cbfd362 | ||
|
|
1a45bf286f | ||
|
|
2fe812e3f9 | ||
|
|
727627f1da | ||
|
|
6a35d89541 | ||
|
|
e5b0cf714d | ||
|
|
cc75356ed1 | ||
|
|
91ff9ed08e | ||
|
|
db5e1b4b19 | ||
|
|
dc56defea2 | ||
|
|
deff75a3bb | ||
|
|
08e8520c1f | ||
| 9fb1bb5549 | |||
| a04036962c | |||
| dce935ce31 | |||
|
|
f2f2d4066c | ||
|
|
8ec1f627fe | ||
|
|
af0f002e14 | ||
|
|
59c7b1f9a3 | ||
|
|
d37ca73ffa | ||
|
|
e5691d8a7f | ||
|
|
123366c3f1 | ||
|
|
9a2efffce3 | ||
|
|
9f2adabbea | ||
|
|
7ac3528750 | ||
|
|
12e70a2279 | ||
|
|
9c1ecf9428 | ||
|
|
b94062bd86 | ||
|
|
81cf168170 | ||
|
|
9a7171a855 | ||
| ca5dae6aff | |||
|
|
18e207a799 | ||
| c2c1b80b60 |
56 changed files with 2685 additions and 497 deletions
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -1,5 +1,5 @@
|
||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Internal IT support
|
- name: Jira — DEV / PLAT / SEC
|
||||||
url: https://seahaven.atlassian.net/jira/software/projects/INFRA
|
url: https://seahaven.atlassian.net/jira
|
||||||
about: For operational issues, file an INFRA Jira ticket instead.
|
about: File all org work in Jira (DEV, PLAT, or SEC). INFRA is a closed archive. GitHub Issues are active only on shoc-backend, shoc-frontend-new, and open-swe.
|
||||||
|
|
|
||||||
1
.github/ISSUE_TEMPLATE/feature_request.md
vendored
1
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
|
@ -15,7 +15,6 @@ assignees: amoussa1229
|
||||||
## AWS / integration impact
|
## AWS / integration impact
|
||||||
- New or changed AWS resources (Lambda, DynamoDB, S3, API Gateway):
|
- New or changed AWS resources (Lambda, DynamoDB, S3, API Gateway):
|
||||||
- Slack app(s) involved:
|
- Slack app(s) involved:
|
||||||
- Confluence Architecture Map update needed: yes / no
|
|
||||||
|
|
||||||
## Alternatives considered
|
## Alternatives considered
|
||||||
<!-- Other approaches and why they were rejected. -->
|
<!-- Other approaches and why they were rejected. -->
|
||||||
|
|
|
||||||
2
.github/ISSUE_TEMPLATE/infra-change.md
vendored
2
.github/ISSUE_TEMPLATE/infra-change.md
vendored
|
|
@ -21,6 +21,4 @@ assignees: amoussa1229
|
||||||
<!-- Exact steps to revert: prior stack version, DeletionPolicy considerations, data restore. -->
|
<!-- Exact steps to revert: prior stack version, DeletionPolicy considerations, data restore. -->
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
- [ ] Confluence Architecture Map (id 1540098) update queued
|
|
||||||
- [ ] README updated in same PR
|
- [ ] README updated in same PR
|
||||||
- [ ] Project memory entry queued
|
|
||||||
|
|
|
||||||
25
.github/PULL_REQUEST_TEMPLATE.md
vendored
25
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -1,8 +1,14 @@
|
||||||
<!--
|
<!--
|
||||||
PR conventions — see engineering-handbook/pull-requests.md
|
PR conventions
|
||||||
- Title: imperative mood, under 70 chars, describe the change not the ticket (e.g. "Add receipt parser Lambda", not "PROJ-123" or "Bug fix").
|
- Title format: type(scope): description (DEV-123)
|
||||||
- Scope: one logical change per PR. If the title needs an "and", split it.
|
- type ∈ feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert, release
|
||||||
- Jira: put the issue key in the branch name or this PR title (e.g. [PROJ-123]) to link the PR into the Jira issue's development panel. Omit if the work has no ticket.
|
- Maximum 120 characters, including the Jira suffix.
|
||||||
|
- Active Jira projects: DEV (product), PLAT (platform), SEC (security). INFRA is a closed archive.
|
||||||
|
- Put the Jira key at the end of the title in parentheses. A missing key is a warning, not a failure.
|
||||||
|
- Branch: feature/, fix/, hotfix/, chore/, docs/, refactor/, release/ + kebab-case description.
|
||||||
|
Branch names do not contain Jira keys.
|
||||||
|
- Scope: one logical change per PR. If the title needs "and", split it.
|
||||||
|
- Body: state verifiable facts about the change and validation. Do not cite the handbook or add AI-attribution footers.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
@ -15,13 +21,4 @@ PR conventions — see engineering-handbook/pull-requests.md
|
||||||
<!-- What tests were added, updated, or run. If no automated tests, explain the manual testing. -->
|
<!-- What tests were added, updated, or run. If no automated tests, explain the manual testing. -->
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
<!-- Anything reviewers should know: migration steps, deploy order, follow-ups, breaking changes. Delete this section if empty. -->
|
<!-- Anything reviewers should know: migration steps, deploy order, follow-ups, breaking changes. Use None. if empty. -->
|
||||||
|
|
||||||
## Sea Haven checklist
|
|
||||||
- [ ] CDK diff / SAM changeset reviewed (if infra change)
|
|
||||||
- [ ] Secrets added to Parameter Store / Secrets Manager (not hardcoded)
|
|
||||||
- [ ] DynamoDB PITR verified on new tables
|
|
||||||
- [ ] Slack notification tested in staging
|
|
||||||
- [ ] Confluence Architecture Map updated
|
|
||||||
- [ ] Memory update queued (if new repo/stack)
|
|
||||||
- [ ] Cross-review requested (if IAM or Lambda handler signature change)
|
|
||||||
|
|
|
||||||
11
.github/dependabot.yml
vendored
11
.github/dependabot.yml
vendored
|
|
@ -1,11 +0,0 @@
|
||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: "github-actions"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
groups:
|
|
||||||
minor-and-patch:
|
|
||||||
update-types:
|
|
||||||
- "minor"
|
|
||||||
- "patch"
|
|
||||||
|
|
@ -16,8 +16,8 @@ jobs:
|
||||||
dependency-review:
|
dependency-review:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
- uses: actions/dependency-review-action@v5
|
- uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
|
||||||
with:
|
with:
|
||||||
fail-on-severity: high
|
fail-on-severity: high
|
||||||
allow-ghsas: ${{ inputs.allow-ghsas }}
|
allow-ghsas: ${{ inputs.allow-ghsas }}
|
||||||
|
|
|
||||||
18
.github/workflows/callable-labeler.yaml
vendored
18
.github/workflows/callable-labeler.yaml
vendored
|
|
@ -53,6 +53,12 @@ jobs:
|
||||||
- '**/template.yaml'
|
- '**/template.yaml'
|
||||||
- 'samconfig.toml'
|
- 'samconfig.toml'
|
||||||
- 'infra/**'
|
- 'infra/**'
|
||||||
|
- 'Dockerfile'
|
||||||
|
- '**/Dockerfile'
|
||||||
|
- '.ebextensions/**'
|
||||||
|
- '**/.ebextensions/**'
|
||||||
|
- '.platform/**'
|
||||||
|
- '**/.platform/**'
|
||||||
app:
|
app:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file:
|
- any-glob-to-any-file:
|
||||||
|
|
@ -64,6 +70,9 @@ jobs:
|
||||||
- 'web/**'
|
- 'web/**'
|
||||||
- 'mobile/**'
|
- 'mobile/**'
|
||||||
- 'shared/**'
|
- 'shared/**'
|
||||||
|
- '**/*.cs'
|
||||||
|
- '**/*.cshtml'
|
||||||
|
- '**/*.razor'
|
||||||
content:
|
content:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file:
|
- any-glob-to-any-file:
|
||||||
|
|
@ -98,12 +107,12 @@ jobs:
|
||||||
tests:
|
tests:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file:
|
- any-glob-to-any-file:
|
||||||
# directory conventions (covers Java src/test, Ruby test/spec, etc.)
|
|
||||||
- '**/tests/**'
|
- '**/tests/**'
|
||||||
- '**/test/**'
|
- '**/test/**'
|
||||||
- '**/spec/**'
|
- '**/spec/**'
|
||||||
- '**/__tests__/**'
|
- '**/__tests__/**'
|
||||||
# JS / TS
|
- 'e2e/**'
|
||||||
|
- '**/e2e/**'
|
||||||
- '**/*.test.js'
|
- '**/*.test.js'
|
||||||
- '**/*.test.jsx'
|
- '**/*.test.jsx'
|
||||||
- '**/*.test.ts'
|
- '**/*.test.ts'
|
||||||
|
|
@ -112,21 +121,16 @@ jobs:
|
||||||
- '**/*.spec.jsx'
|
- '**/*.spec.jsx'
|
||||||
- '**/*.spec.ts'
|
- '**/*.spec.ts'
|
||||||
- '**/*.spec.tsx'
|
- '**/*.spec.tsx'
|
||||||
# Python
|
|
||||||
- '**/*_test.py'
|
- '**/*_test.py'
|
||||||
- '**/test_*.py'
|
- '**/test_*.py'
|
||||||
- '**/conftest.py'
|
- '**/conftest.py'
|
||||||
# .NET
|
|
||||||
- '**/*Tests.cs'
|
- '**/*Tests.cs'
|
||||||
- '**/*Test.cs'
|
- '**/*Test.cs'
|
||||||
- '**/*.Tests/**'
|
- '**/*.Tests/**'
|
||||||
# Java / JVM
|
|
||||||
- '**/*Test.java'
|
- '**/*Test.java'
|
||||||
- '**/*Tests.java'
|
- '**/*Tests.java'
|
||||||
- '**/*IT.java'
|
- '**/*IT.java'
|
||||||
# Go
|
|
||||||
- '**/*_test.go'
|
- '**/*_test.go'
|
||||||
# Ruby
|
|
||||||
- '**/*_spec.rb'
|
- '**/*_spec.rb'
|
||||||
- '**/*_test.rb'
|
- '**/*_test.rb'
|
||||||
EOF
|
EOF
|
||||||
|
|
|
||||||
12
.github/workflows/cd-cdk.yaml
vendored
12
.github/workflows/cd-cdk.yaml
vendored
|
|
@ -70,12 +70,12 @@ jobs:
|
||||||
group: cd-cdk-${{ inputs.region }}-${{ inputs.stacks }}-${{ inputs.stack-name }}
|
group: cd-cdk-${{ inputs.region }}-${{ inputs.stacks }}-${{ inputs.stack-name }}
|
||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4
|
- uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
|
||||||
if: ${{ inputs.enable-qemu }}
|
if: ${{ inputs.enable-qemu }}
|
||||||
|
|
||||||
- uses: actions/setup-dotnet@v6
|
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
|
||||||
if: ${{ inputs.dotnet-version != '' }}
|
if: ${{ inputs.dotnet-version != '' }}
|
||||||
with:
|
with:
|
||||||
dotnet-version: ${{ inputs.dotnet-version }}
|
dotnet-version: ${{ inputs.dotnet-version }}
|
||||||
|
|
@ -95,11 +95,11 @@ jobs:
|
||||||
--self-contained false \
|
--self-contained false \
|
||||||
--output "$(dirname "$DOTNET_PUBLISH_PROJECT")/bin/Release/net8.0/linux-arm64/publish"
|
--output "$(dirname "$DOTNET_PUBLISH_PROJECT")/bin/Release/net8.0/linux-arm64/publish"
|
||||||
|
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ inputs.node-version }}
|
node-version: ${{ inputs.node-version }}
|
||||||
|
|
||||||
- uses: actions/setup-python@v7
|
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
if: ${{ inputs.python-version != '' }}
|
if: ${{ inputs.python-version != '' }}
|
||||||
with:
|
with:
|
||||||
python-version: ${{ inputs.python-version }}
|
python-version: ${{ inputs.python-version }}
|
||||||
|
|
@ -121,7 +121,7 @@ jobs:
|
||||||
pip install -r "$req"
|
pip install -r "$req"
|
||||||
done < <(find . -name requirements.txt -not -path '*/node_modules/*' -print0)
|
done < <(find . -name requirements.txt -not -path '*/node_modules/*' -print0)
|
||||||
|
|
||||||
- uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6
|
- uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0
|
||||||
with:
|
with:
|
||||||
role-to-assume: ${{ secrets.deploy-role-arn }}
|
role-to-assume: ${{ secrets.deploy-role-arn }}
|
||||||
aws-region: ${{ inputs.region }}
|
aws-region: ${{ inputs.region }}
|
||||||
|
|
|
||||||
8
.github/workflows/cd-dotnet-eb.yaml
vendored
8
.github/workflows/cd-dotnet-eb.yaml
vendored
|
|
@ -4,7 +4,7 @@ name: CD — .NET Elastic Beanstalk
|
||||||
#
|
#
|
||||||
# jobs:
|
# jobs:
|
||||||
# deploy:
|
# deploy:
|
||||||
# uses: Sea-Haven-Industries/.github/.github/workflows/cd-dotnet-eb.yaml@<full-commit-sha> # main
|
# uses: Sea-Haven-Industries/.github/.github/workflows/cd-dotnet-eb.yaml@<full-commit-sha> # v1.0.4
|
||||||
# with:
|
# with:
|
||||||
# project: "Api.Example/Api.Example.csproj"
|
# project: "Api.Example/Api.Example.csproj"
|
||||||
# eb-application: "example-api"
|
# eb-application: "example-api"
|
||||||
|
|
@ -82,9 +82,9 @@ jobs:
|
||||||
run:
|
run:
|
||||||
working-directory: ${{ inputs.working-directory }}
|
working-directory: ${{ inputs.working-directory }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- uses: actions/setup-dotnet@v6
|
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: ${{ inputs.dotnet-version }}
|
dotnet-version: ${{ inputs.dotnet-version }}
|
||||||
|
|
||||||
|
|
@ -119,7 +119,7 @@ jobs:
|
||||||
cd ..
|
cd ..
|
||||||
echo "Bundle size: $(du -h bundle.zip | cut -f1)"
|
echo "Bundle size: $(du -h bundle.zip | cut -f1)"
|
||||||
|
|
||||||
- uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6
|
- uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0
|
||||||
with:
|
with:
|
||||||
role-to-assume: ${{ secrets.deploy-role-arn }}
|
role-to-assume: ${{ secrets.deploy-role-arn }}
|
||||||
aws-region: ${{ inputs.region }}
|
aws-region: ${{ inputs.region }}
|
||||||
|
|
|
||||||
441
.github/workflows/cd-hcp-fargate.yaml
vendored
Normal file
441
.github/workflows/cd-hcp-fargate.yaml
vendored
Normal file
|
|
@ -0,0 +1,441 @@
|
||||||
|
name: CD — HCP Fargate
|
||||||
|
|
||||||
|
# Reusable Fargate image CD for HCP app repos. The caller owns triggers and
|
||||||
|
# passes `environment` as a `with:` input. This job owns `environment:`,
|
||||||
|
# concurrency, OIDC, and `vars.DEPLOY_ROLE_ARN`. GitHub rejects `environment:`
|
||||||
|
# beside `uses:`.
|
||||||
|
#
|
||||||
|
# Caller example (one job per GitHub Environment):
|
||||||
|
# jobs:
|
||||||
|
# deploy-prod:
|
||||||
|
# uses: Sea-Haven-Industries/.github/.github/workflows/cd-hcp-fargate.yaml@<sha> # vX.Y.Z
|
||||||
|
# permissions: { contents: read, id-token: write }
|
||||||
|
# secrets: inherit
|
||||||
|
# with:
|
||||||
|
# environment: prod
|
||||||
|
# ref: ${{ github.event.release.tag_name || inputs.ref }}
|
||||||
|
# ssm-prefix: /meal-order-manager/deploy
|
||||||
|
# docker-platform: linux/amd64
|
||||||
|
# ship-gate: true
|
||||||
|
#
|
||||||
|
# apply-task-environment replaces the container env from
|
||||||
|
# ${prefix}/task-environment. sentry-project uploads image files before
|
||||||
|
# RegisterTaskDefinition. concurrency-suffix splits two deployables that
|
||||||
|
# share one SSM prefix. Empty defaults keep the previous behavior.
|
||||||
|
#
|
||||||
|
# Nothing here creates an HCP run. Terraform owns the cluster, service, ALB,
|
||||||
|
# and ignores container_definitions / task_definition.
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
environment:
|
||||||
|
description: "GitHub Environment to deploy to (dev, staging, prod)"
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
ref:
|
||||||
|
description: "Git ref to build. Empty means github.sha."
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
ssm-prefix:
|
||||||
|
description: "SSM prefix for deploy parameters (e.g. /meal-order-manager/deploy)"
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
docker-platform:
|
||||||
|
description: "docker build --platform value"
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: "linux/amd64"
|
||||||
|
health-path:
|
||||||
|
description: "Health endpoint path appended to SSM api-url"
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: "/api/health"
|
||||||
|
ship-gate:
|
||||||
|
description: "Require the ref to be on main or a legal hotfix/release tag"
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
extra-task-env:
|
||||||
|
description: "JSON object of extra container environment keys to merge (e.g. {\"SENTRY_DSN_PARAM\":\"/app/sentry-dsn\"})"
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: "{}"
|
||||||
|
apply-task-environment:
|
||||||
|
description: "Replace container env from SSM ${prefix}/task-environment. GIT_SHA wins."
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
sentry-org:
|
||||||
|
description: "Sentry org for BFF source map upload when sentry-project is set"
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: "seahaven"
|
||||||
|
sentry-project:
|
||||||
|
description: "Sentry project for BFF source map upload. Empty skips upload."
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
sentry-container-files:
|
||||||
|
description: "Comma-separated image paths to upload. Required when sentry-project is set."
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
health-attempts:
|
||||||
|
description: "Number of /api/health polls, 10 seconds apart, before failing"
|
||||||
|
type: number
|
||||||
|
required: false
|
||||||
|
default: 6
|
||||||
|
health-from-distribution:
|
||||||
|
description: "Build the health URL from SSM distribution-id and CloudFront GetDistribution. Leave false to read SSM api-url."
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
concurrency-suffix:
|
||||||
|
description: "Optional concurrency group suffix when two deployables share an SSM prefix"
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy Fargate to ${{ inputs.environment }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
environment: ${{ inputs.environment }}
|
||||||
|
concurrency:
|
||||||
|
group: ${{ inputs.concurrency-suffix != '' && format('deploy-{0}-{1}-{2}', inputs.ssm-prefix, inputs.concurrency-suffix, inputs.environment) || format('deploy-{0}-{1}', inputs.ssm-prefix, inputs.environment) }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
env:
|
||||||
|
AWS_REGION: us-east-1
|
||||||
|
DEPLOY_ROLE_ARN: ${{ vars.DEPLOY_ROLE_ARN }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref != '' && inputs.ref || github.sha }}
|
||||||
|
persist-credentials: false
|
||||||
|
fetch-tags: true
|
||||||
|
|
||||||
|
- name: Resolve commit
|
||||||
|
id: commit
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
sha="$(git rev-parse HEAD)"
|
||||||
|
echo "sha=${sha}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "Building ${sha}"
|
||||||
|
|
||||||
|
- name: Ship-gate
|
||||||
|
if: ${{ inputs.ship-gate }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
INPUT_REF: ${{ inputs.ref != '' && inputs.ref || github.sha }}
|
||||||
|
ENVIRONMENT: ${{ inputs.environment }}
|
||||||
|
HEAD_SHA: ${{ steps.commit.outputs.sha }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
status="$(gh api "repos/${REPO}/compare/main...${INPUT_REF}" --jq .status)"
|
||||||
|
if [ "${status}" = "behind" ] || [ "${status}" = "identical" ]; then
|
||||||
|
echo "ship-gate: ${INPUT_REF} is ${status} relative to main"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "ship-gate: ${INPUT_REF} is not on main (compare status: ${status}); checking hotfix/release path"
|
||||||
|
|
||||||
|
TAG="${INPUT_REF}"
|
||||||
|
if [[ ! "${TAG}" =~ ^v ]]; then
|
||||||
|
TAG="$(git describe --tags --exact-match "${HEAD_SHA}" 2>/dev/null || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${ENVIRONMENT}" = "staging" ]; then
|
||||||
|
PATTERN='^v[0-9]+\.[0-9]+\.[0-9]+-staging$'
|
||||||
|
else
|
||||||
|
PATTERN='^v[0-9]+\.[0-9]+\.[0-9]+$'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${TAG}" ]] || [[ ! "${TAG}" =~ ${PATTERN} ]]; then
|
||||||
|
echo "ship-gate: ref ${INPUT_REF} (tag ${TAG:-none}) does not match ${PATTERN} for environment ${ENVIRONMENT} and is not on main" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export PATTERN TAG
|
||||||
|
PREV="$(
|
||||||
|
gh api "repos/${REPO}/releases" --paginate --jq '.[].tag_name' | python3 -c '
|
||||||
|
import os, re, sys
|
||||||
|
pattern = re.compile(os.environ["PATTERN"])
|
||||||
|
current = os.environ["TAG"]
|
||||||
|
tags = [
|
||||||
|
line.strip()
|
||||||
|
for line in sys.stdin
|
||||||
|
if pattern.fullmatch(line.strip()) and line.strip() != current
|
||||||
|
]
|
||||||
|
def key(tag):
|
||||||
|
body = tag[1:]
|
||||||
|
core = body.split("-", 1)[0]
|
||||||
|
return tuple(int(part) for part in core.split("."))
|
||||||
|
tags.sort(key=key)
|
||||||
|
print(tags[-1] if tags else "")
|
||||||
|
'
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [ -z "${PREV}" ]; then
|
||||||
|
echo "ship-gate: no previous matching release tag; hotfix path requires a prior ${PATTERN} release" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ff_status="$(gh api "repos/${REPO}/compare/${PREV}...${TAG}" --jq .status)"
|
||||||
|
if [ "${ff_status}" != "ahead" ]; then
|
||||||
|
echo "ship-gate: ${TAG} is not a fast-forward of ${PREV} (status: ${ff_status})" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
from_train=false
|
||||||
|
TARGET_COMMITISH="$(gh api "repos/${REPO}/releases/tags/${TAG}" --jq .target_commitish 2>/dev/null || true)"
|
||||||
|
if [[ "${TARGET_COMMITISH}" == hotfix/* || "${TARGET_COMMITISH}" == release/* ]]; then
|
||||||
|
from_train=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${from_train}" = false ]; then
|
||||||
|
git fetch origin '+refs/heads/hotfix/*:refs/remotes/origin/hotfix/*' '+refs/heads/release/*:refs/remotes/origin/release/*' || true
|
||||||
|
if git branch -r --contains "${HEAD_SHA}" | grep -Eq 'origin/(hotfix|release)/'; then
|
||||||
|
from_train=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${from_train}" = false ]; then
|
||||||
|
echo "ship-gate: ${TAG} was not created from hotfix/* or release/* (target_commitish=${TARGET_COMMITISH:-none})" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "ship-gate: hotfix/release path accepted (${PREV} -> ${TAG} from ${TARGET_COMMITISH:-branch})"
|
||||||
|
|
||||||
|
- 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
|
||||||
|
env:
|
||||||
|
SSM_PREFIX: ${{ inputs.ssm-prefix }}
|
||||||
|
HEALTH_FROM_DISTRIBUTION: ${{ inputs.health-from-distribution }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
get_param() {
|
||||||
|
aws ssm get-parameter --name "$1" --query Parameter.Value --output text
|
||||||
|
}
|
||||||
|
prefix="${SSM_PREFIX%/}"
|
||||||
|
CLUSTER=$(get_param "${prefix}/cluster")
|
||||||
|
SERVICE=$(get_param "${prefix}/service")
|
||||||
|
FAMILY=$(get_param "${prefix}/task-family")
|
||||||
|
ECR=$(get_param "${prefix}/ecr-repository")
|
||||||
|
CONTAINER=$(get_param "${prefix}/container-name")
|
||||||
|
if [ "${HEALTH_FROM_DISTRIBUTION}" = "true" ]; then
|
||||||
|
DIST_ID=$(get_param "${prefix}/distribution-id")
|
||||||
|
DOMAIN=$(aws cloudfront get-distribution --id "${DIST_ID}" --query Distribution.DomainName --output text)
|
||||||
|
API_URL="https://${DOMAIN}"
|
||||||
|
else
|
||||||
|
API_URL=$(get_param "${prefix}/api-url")
|
||||||
|
fi
|
||||||
|
{
|
||||||
|
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@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0
|
||||||
|
|
||||||
|
- 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: ${{ inputs.environment }}
|
||||||
|
DOCKER_PLATFORM: ${{ inputs.docker-platform }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
docker buildx build \
|
||||||
|
--platform "${DOCKER_PLATFORM}" \
|
||||||
|
--build-arg "GIT_SHA=${GIT_SHA}" \
|
||||||
|
-t "${ECR}:${GIT_SHA}" \
|
||||||
|
-t "${ECR}:${ENVIRONMENT}" \
|
||||||
|
--push \
|
||||||
|
.
|
||||||
|
|
||||||
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
|
if: ${{ inputs.sentry-project != '' }}
|
||||||
|
with:
|
||||||
|
node-version: "24"
|
||||||
|
|
||||||
|
- name: Upload BFF source maps
|
||||||
|
if: ${{ inputs.sentry-project != '' }}
|
||||||
|
env:
|
||||||
|
ECR: ${{ steps.deploy.outputs.ecr }}
|
||||||
|
GIT_SHA: ${{ steps.commit.outputs.sha }}
|
||||||
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
|
SENTRY_URL: https://de.sentry.io
|
||||||
|
SENTRY_ORG: ${{ inputs.sentry-org }}
|
||||||
|
SENTRY_PROJECT: ${{ inputs.sentry-project }}
|
||||||
|
SENTRY_CONTAINER_FILES: ${{ inputs.sentry-container-files }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [ -z "${SENTRY_AUTH_TOKEN}" ]; then
|
||||||
|
echo "SENTRY_AUTH_TOKEN is required to upload BFF source maps" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ -z "${SENTRY_CONTAINER_FILES}" ]; then
|
||||||
|
echo "sentry-container-files is required when sentry-project is set" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
docker pull "${ECR}:${GIT_SHA}"
|
||||||
|
mkdir -p build/sentry
|
||||||
|
cid="$(docker create "${ECR}:${GIT_SHA}")"
|
||||||
|
cleanup() { docker rm "${cid}" >/dev/null 2>&1 || true; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
IFS=',' read -r -a files <<< "${SENTRY_CONTAINER_FILES}"
|
||||||
|
for path in "${files[@]}"; do
|
||||||
|
path="${path#"${path%%[![:space:]]*}"}"
|
||||||
|
path="${path%"${path##*[![:space:]]}"}"
|
||||||
|
if [ -z "${path}" ]; then
|
||||||
|
echo "sentry-container-files contains an empty path" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
base="$(basename "${path}")"
|
||||||
|
docker cp "${cid}:${path}" "build/sentry/${base}"
|
||||||
|
done
|
||||||
|
if [ -f build/sentry/server.js ]; then
|
||||||
|
grep -q "${GIT_SHA}" build/sentry/server.js
|
||||||
|
grep -q debugId build/sentry/server.js
|
||||||
|
fi
|
||||||
|
npx --yes @sentry/cli@2 sourcemaps upload \
|
||||||
|
--org "${SENTRY_ORG}" \
|
||||||
|
--project "${SENTRY_PROJECT}" \
|
||||||
|
--release "${GIT_SHA}" \
|
||||||
|
build/sentry
|
||||||
|
|
||||||
|
- 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 }}
|
||||||
|
EXTRA_TASK_ENV: ${{ inputs.extra-task-env }}
|
||||||
|
APPLY_TASK_ENVIRONMENT: ${{ inputs.apply-task-environment }}
|
||||||
|
SSM_PREFIX: ${{ inputs.ssm-prefix }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [ "${APPLY_TASK_ENVIRONMENT}" = "true" ]; then
|
||||||
|
prefix="${SSM_PREFIX%/}"
|
||||||
|
TASK_ENV_JSON="$(aws ssm get-parameter \
|
||||||
|
--name "${prefix}/task-environment" \
|
||||||
|
--with-decryption \
|
||||||
|
--query Parameter.Value \
|
||||||
|
--output text)"
|
||||||
|
export TASK_ENV_JSON
|
||||||
|
fi
|
||||||
|
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"]
|
||||||
|
extra_raw = os.environ.get("EXTRA_TASK_ENV") or "{}"
|
||||||
|
extra_env = json.loads(extra_raw)
|
||||||
|
if not isinstance(extra_env, dict):
|
||||||
|
sys.exit("extra-task-env must be a JSON object")
|
||||||
|
apply = os.environ.get("APPLY_TASK_ENVIRONMENT") == "true"
|
||||||
|
found = False
|
||||||
|
for container in td["containerDefinitions"]:
|
||||||
|
if container["name"] != name:
|
||||||
|
continue
|
||||||
|
found = True
|
||||||
|
container["image"] = image
|
||||||
|
if apply:
|
||||||
|
env_map = json.loads(os.environ["TASK_ENV_JSON"])
|
||||||
|
if not isinstance(env_map, dict) or not env_map:
|
||||||
|
sys.exit("task-environment must be a non-empty JSON object")
|
||||||
|
env = {str(key): str(value) for key, value in env_map.items()}
|
||||||
|
env.pop("GIT_SHA", None)
|
||||||
|
container["stopTimeout"] = 60
|
||||||
|
else:
|
||||||
|
env = {item["name"]: item["value"] for item in container.get("environment", [])}
|
||||||
|
for key, value in extra_env.items():
|
||||||
|
env[str(key)] = str(value)
|
||||||
|
env["GIT_SHA"] = sha
|
||||||
|
container["environment"] = [{"name": key, "value": value} for key, value in env.items()]
|
||||||
|
container.pop("command", None)
|
||||||
|
if not found:
|
||||||
|
sys.exit(f"container {name} not in task definition")
|
||||||
|
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 }}
|
||||||
|
HEALTH_PATH: ${{ inputs.health-path }}
|
||||||
|
EXPECTED_SHA: ${{ steps.commit.outputs.sha }}
|
||||||
|
HEALTH_ATTEMPTS: ${{ inputs.health-attempts }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
path="${HEALTH_PATH}"
|
||||||
|
case "${path}" in
|
||||||
|
/*) ;;
|
||||||
|
*) path="/${path}" ;;
|
||||||
|
esac
|
||||||
|
url="${API_URL%/}${path}"
|
||||||
|
if ! [[ "${HEALTH_ATTEMPTS}" =~ ^[1-9][0-9]*$ ]]; then
|
||||||
|
echo "health-attempts must be a positive integer" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
for _ in $(seq 1 "${HEALTH_ATTEMPTS}"); do
|
||||||
|
BODY="$(curl -fsS "${url}" || 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
|
||||||
274
.github/workflows/cd-hcp-lambda.yaml
vendored
Normal file
274
.github/workflows/cd-hcp-lambda.yaml
vendored
Normal file
|
|
@ -0,0 +1,274 @@
|
||||||
|
name: CD — HCP Lambda
|
||||||
|
|
||||||
|
# Reusable Lambda zip CD for HCP app repos. The caller owns triggers and
|
||||||
|
# passes `environment` as a `with:` input. This job owns `environment:`,
|
||||||
|
# concurrency, OIDC, and `vars.DEPLOY_ROLE_ARN`. GitHub rejects `environment:`
|
||||||
|
# beside `uses:`.
|
||||||
|
#
|
||||||
|
# Caller example (one job per GitHub Environment):
|
||||||
|
# jobs:
|
||||||
|
# deploy-prod:
|
||||||
|
# uses: Sea-Haven-Industries/.github/.github/workflows/cd-hcp-lambda.yaml@<sha> # vX.Y.Z
|
||||||
|
# permissions: { contents: read, id-token: write }
|
||||||
|
# secrets: inherit
|
||||||
|
# with:
|
||||||
|
# environment: prod
|
||||||
|
# ref: ${{ github.event.release.tag_name || inputs.ref }}
|
||||||
|
# ssm-prefix: /payments-dashboard/deploy
|
||||||
|
# function-keys: process_csv,slack_app_home
|
||||||
|
# ship-gate: true
|
||||||
|
#
|
||||||
|
# The caller repo must provide scripts/package_lambdas.mjs, which writes
|
||||||
|
# build/packages/<key>.zip and embeds the commit in src/buildInfo.js.
|
||||||
|
# Terraform owns the functions and ignores code attributes. SSM under
|
||||||
|
# ssm-prefix supplies artifacts-bucket and <key>-function-name.
|
||||||
|
#
|
||||||
|
# Nothing here creates an HCP run.
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
environment:
|
||||||
|
description: "GitHub Environment to deploy to (dev, staging, prod)"
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
ref:
|
||||||
|
description: "Git ref to build. Empty means github.sha."
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
ssm-prefix:
|
||||||
|
description: "SSM prefix for deploy parameters (e.g. /payments-dashboard/deploy)"
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
function-keys:
|
||||||
|
description: "Comma-separated package keys. Each maps to SSM <prefix>/<key>-function-name."
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
node-version:
|
||||||
|
description: "Node.js version for setup-node and the packager"
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: "24"
|
||||||
|
ship-gate:
|
||||||
|
description: "Require the ref to be on main or a legal hotfix/release tag"
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy Lambda to ${{ inputs.environment }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
environment: ${{ inputs.environment }}
|
||||||
|
concurrency:
|
||||||
|
group: deploy-${{ inputs.ssm-prefix }}-${{ inputs.environment }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
env:
|
||||||
|
AWS_REGION: us-east-1
|
||||||
|
DEPLOY_ROLE_ARN: ${{ vars.DEPLOY_ROLE_ARN }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref != '' && inputs.ref || github.sha }}
|
||||||
|
persist-credentials: false
|
||||||
|
fetch-tags: true
|
||||||
|
|
||||||
|
- name: Resolve commit
|
||||||
|
id: commit
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
sha="$(git rev-parse HEAD)"
|
||||||
|
echo "sha=${sha}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "Building ${sha}"
|
||||||
|
|
||||||
|
- name: Ship-gate
|
||||||
|
if: ${{ inputs.ship-gate }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
INPUT_REF: ${{ inputs.ref != '' && inputs.ref || github.sha }}
|
||||||
|
ENVIRONMENT: ${{ inputs.environment }}
|
||||||
|
HEAD_SHA: ${{ steps.commit.outputs.sha }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
status="$(gh api "repos/${REPO}/compare/main...${INPUT_REF}" --jq .status)"
|
||||||
|
if [ "${status}" = "behind" ] || [ "${status}" = "identical" ]; then
|
||||||
|
echo "ship-gate: ${INPUT_REF} is ${status} relative to main"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "ship-gate: ${INPUT_REF} is not on main (compare status: ${status}); checking hotfix/release path"
|
||||||
|
|
||||||
|
TAG="${INPUT_REF}"
|
||||||
|
if [[ ! "${TAG}" =~ ^v ]]; then
|
||||||
|
TAG="$(git describe --tags --exact-match "${HEAD_SHA}" 2>/dev/null || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${ENVIRONMENT}" = "staging" ]; then
|
||||||
|
PATTERN='^v[0-9]+\.[0-9]+\.[0-9]+-staging$'
|
||||||
|
else
|
||||||
|
PATTERN='^v[0-9]+\.[0-9]+\.[0-9]+$'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${TAG}" ]] || [[ ! "${TAG}" =~ ${PATTERN} ]]; then
|
||||||
|
echo "ship-gate: ref ${INPUT_REF} (tag ${TAG:-none}) does not match ${PATTERN} for environment ${ENVIRONMENT} and is not on main" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export PATTERN TAG
|
||||||
|
# Newest matching release that is an ancestor of TAG. The highest
|
||||||
|
# release overall is not that ancestor when a hotfix is cut from an
|
||||||
|
# older line (v2.0.0 exists, v1.2.1 is cut from v1.2.0).
|
||||||
|
CANDIDATES="$(
|
||||||
|
gh api "repos/${REPO}/releases" --paginate --jq '.[].tag_name' | python3 -c '
|
||||||
|
import os, re, sys
|
||||||
|
pattern = re.compile(os.environ["PATTERN"])
|
||||||
|
current = os.environ["TAG"]
|
||||||
|
tags = [
|
||||||
|
line.strip()
|
||||||
|
for line in sys.stdin
|
||||||
|
if pattern.fullmatch(line.strip()) and line.strip() != current
|
||||||
|
]
|
||||||
|
def key(tag):
|
||||||
|
body = tag[1:]
|
||||||
|
core = body.split("-", 1)[0]
|
||||||
|
return tuple(int(part) for part in core.split("."))
|
||||||
|
tags.sort(key=key, reverse=True)
|
||||||
|
print("\n".join(tags))
|
||||||
|
'
|
||||||
|
)"
|
||||||
|
|
||||||
|
PREV=""
|
||||||
|
if [ -n "${CANDIDATES}" ]; then
|
||||||
|
while IFS= read -r candidate; do
|
||||||
|
if [ -z "${candidate}" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
candidate_status="$(gh api "repos/${REPO}/compare/${candidate}...${TAG}" --jq .status)"
|
||||||
|
if [ "${candidate_status}" = "ahead" ]; then
|
||||||
|
PREV="${candidate}"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done <<< "${CANDIDATES}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${PREV}" ]; then
|
||||||
|
echo "ship-gate: no prior ${PATTERN} release is an ancestor of ${TAG}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "ship-gate: ${TAG} is ahead of ${PREV}"
|
||||||
|
|
||||||
|
from_train=false
|
||||||
|
TARGET_COMMITISH="$(gh api "repos/${REPO}/releases/tags/${TAG}" --jq .target_commitish 2>/dev/null || true)"
|
||||||
|
if [[ "${TARGET_COMMITISH}" == hotfix/* || "${TARGET_COMMITISH}" == release/* ]]; then
|
||||||
|
from_train=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${from_train}" = false ]; then
|
||||||
|
git fetch origin '+refs/heads/hotfix/*:refs/remotes/origin/hotfix/*' '+refs/heads/release/*:refs/remotes/origin/release/*' || true
|
||||||
|
if git branch -r --contains "${HEAD_SHA}" | grep -Eq 'origin/(hotfix|release)/'; then
|
||||||
|
from_train=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${from_train}" = false ]; then
|
||||||
|
echo "ship-gate: ${TAG} was not created from hotfix/* or release/* (target_commitish=${TARGET_COMMITISH:-none})" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "ship-gate: hotfix/release path accepted (${PREV} -> ${TAG} from ${TARGET_COMMITISH:-branch})"
|
||||||
|
|
||||||
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
|
with:
|
||||||
|
node-version: ${{ inputs.node-version }}
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Build function zips
|
||||||
|
env:
|
||||||
|
GIT_SHA: ${{ steps.commit.outputs.sha }}
|
||||||
|
FUNCTION_KEYS: ${{ inputs.function-keys }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [ -z "${FUNCTION_KEYS}" ]; then
|
||||||
|
echo "function-keys is required" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
keys=()
|
||||||
|
IFS=',' read -r -a raw_keys <<< "${FUNCTION_KEYS}"
|
||||||
|
for raw in "${raw_keys[@]}"; do
|
||||||
|
key="${raw#"${raw%%[![:space:]]*}"}"
|
||||||
|
key="${key%"${key##*[![:space:]]}"}"
|
||||||
|
if [ -z "${key}" ]; then
|
||||||
|
echo "function-keys contains an empty key" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ ! "${key}" =~ ^[A-Za-z0-9_]+$ ]]; then
|
||||||
|
echo "invalid function key: ${key}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
keys+=("${key}")
|
||||||
|
done
|
||||||
|
if [ "${#keys[@]}" -eq 0 ]; then
|
||||||
|
echo "function-keys is empty" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
clean="$(IFS=,; echo "${keys[*]}")"
|
||||||
|
echo "keys=${clean}" >> "${GITHUB_ENV}"
|
||||||
|
cmd=(node scripts/package_lambdas.mjs --git-sha "${GIT_SHA}" --out-dir build/packages)
|
||||||
|
for key in "${keys[@]}"; do
|
||||||
|
cmd+=(--only "${key}")
|
||||||
|
done
|
||||||
|
"${cmd[@]}"
|
||||||
|
FUNCTION_KEYS_CLEAN="${clean}" python3 - <<'PY'
|
||||||
|
import os, zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
sha = os.environ["GIT_SHA"]
|
||||||
|
keys = [part for part in os.environ["FUNCTION_KEYS_CLEAN"].split(",") if part]
|
||||||
|
for name in keys:
|
||||||
|
path = Path("build/packages") / f"{name}.zip"
|
||||||
|
if not path.is_file():
|
||||||
|
raise SystemExit(f"missing {path}")
|
||||||
|
with zipfile.ZipFile(path) as zf:
|
||||||
|
info = zf.read("src/buildInfo.js").decode()
|
||||||
|
if sha not in info:
|
||||||
|
raise SystemExit(f"{path} missing GIT_SHA {sha}")
|
||||||
|
print("zips ok")
|
||||||
|
PY
|
||||||
|
|
||||||
|
- 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: Upload zips and update function code
|
||||||
|
env:
|
||||||
|
SSM_PREFIX: ${{ inputs.ssm-prefix }}
|
||||||
|
GIT_SHA: ${{ steps.commit.outputs.sha }}
|
||||||
|
FUNCTION_KEYS_CLEAN: ${{ env.keys }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
prefix="${SSM_PREFIX%/}"
|
||||||
|
bucket="$(aws ssm get-parameter --name "${prefix}/artifacts-bucket" --query Parameter.Value --output text)"
|
||||||
|
IFS=',' read -r -a keys <<< "${FUNCTION_KEYS_CLEAN}"
|
||||||
|
for key in "${keys[@]}"; do
|
||||||
|
fn="$(aws ssm get-parameter --name "${prefix}/${key}-function-name" --query Parameter.Value --output text)"
|
||||||
|
s3_key="functions/${key}/${GIT_SHA}.zip"
|
||||||
|
aws s3 cp "build/packages/${key}.zip" "s3://${bucket}/${s3_key}"
|
||||||
|
aws lambda update-function-code \
|
||||||
|
--function-name "${fn}" \
|
||||||
|
--s3-bucket "${bucket}" \
|
||||||
|
--s3-key "${s3_key}" \
|
||||||
|
--query '{Function:FunctionName,Sha256:CodeSha256,Updated:LastModified}' \
|
||||||
|
--output table
|
||||||
|
aws lambda wait function-updated-v2 --function-name "${fn}"
|
||||||
|
done
|
||||||
409
.github/workflows/cd-hcp-spa.yaml
vendored
Normal file
409
.github/workflows/cd-hcp-spa.yaml
vendored
Normal file
|
|
@ -0,0 +1,409 @@
|
||||||
|
name: CD — HCP SPA
|
||||||
|
|
||||||
|
# Reusable CloudFront/S3 SPA CD for HCP app repos. The caller owns triggers
|
||||||
|
# and passes `environment` as a `with:` input. This job owns `environment:`,
|
||||||
|
# concurrency, OIDC, and `vars.DEPLOY_ROLE_ARN`. GitHub rejects `environment:`
|
||||||
|
# beside `uses:`.
|
||||||
|
#
|
||||||
|
# Build env comes from the GitHub Environment: every `vars.VITE_*` value plus
|
||||||
|
# `secrets.SENTRY_AUTH_TOKEN`. Pass `required-vite-vars` for keys that must
|
||||||
|
# be set before `npm run build`.
|
||||||
|
#
|
||||||
|
# Caller example (one job per GitHub Environment):
|
||||||
|
# jobs:
|
||||||
|
# deploy-prod:
|
||||||
|
# uses: Sea-Haven-Industries/.github/.github/workflows/cd-hcp-spa.yaml@<sha> # vX.Y.Z
|
||||||
|
# permissions: { contents: read, id-token: write }
|
||||||
|
# secrets: inherit
|
||||||
|
# with:
|
||||||
|
# environment: prod
|
||||||
|
# ref: ${{ github.event.release.tag_name || inputs.ref }}
|
||||||
|
# ssm-prefix: /internal-portal/deploy
|
||||||
|
# ship-gate: true
|
||||||
|
#
|
||||||
|
# concurrency-suffix splits two deployables that share one SSM prefix.
|
||||||
|
# verify-companion-api adds cache, asset, and /api/health checks after the
|
||||||
|
# index.html hash matches. Empty defaults keep the previous behavior.
|
||||||
|
#
|
||||||
|
# Nothing here creates an HCP run. Terraform owns the bucket and distribution.
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
environment:
|
||||||
|
description: "GitHub Environment to deploy to (dev, staging, prod)"
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
ref:
|
||||||
|
description: "Git ref to build. Empty means github.sha."
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
ssm-prefix:
|
||||||
|
description: "SSM prefix for deploy parameters (e.g. /internal-portal/deploy)"
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
ship-gate:
|
||||||
|
description: "Require the ref to be on main or a legal hotfix/release tag"
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
required-vite-vars:
|
||||||
|
description: "Comma-separated VITE_* GitHub Environment variable names that must be set"
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
verify-companion-api:
|
||||||
|
description: "After the index hash matches, check cache headers, hashed assets, and /api/health"
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
concurrency-suffix:
|
||||||
|
description: "Optional concurrency group suffix when two deployables share an SSM prefix"
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy SPA to ${{ inputs.environment }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 45
|
||||||
|
environment: ${{ inputs.environment }}
|
||||||
|
concurrency:
|
||||||
|
group: ${{ inputs.concurrency-suffix != '' && format('deploy-{0}-{1}-{2}', inputs.ssm-prefix, inputs.concurrency-suffix, inputs.environment) || format('deploy-{0}-{1}', inputs.ssm-prefix, inputs.environment) }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
env:
|
||||||
|
AWS_REGION: us-east-1
|
||||||
|
DEPLOY_ROLE_ARN: ${{ vars.DEPLOY_ROLE_ARN }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref != '' && inputs.ref || github.sha }}
|
||||||
|
persist-credentials: false
|
||||||
|
fetch-tags: true
|
||||||
|
|
||||||
|
- name: Resolve commit
|
||||||
|
id: commit
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
sha="$(git rev-parse HEAD)"
|
||||||
|
echo "sha=${sha}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "Building ${sha}"
|
||||||
|
|
||||||
|
- name: Ship-gate
|
||||||
|
if: ${{ inputs.ship-gate }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
INPUT_REF: ${{ inputs.ref != '' && inputs.ref || github.sha }}
|
||||||
|
ENVIRONMENT: ${{ inputs.environment }}
|
||||||
|
HEAD_SHA: ${{ steps.commit.outputs.sha }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
status="$(gh api "repos/${REPO}/compare/main...${INPUT_REF}" --jq .status)"
|
||||||
|
if [ "${status}" = "behind" ] || [ "${status}" = "identical" ]; then
|
||||||
|
echo "ship-gate: ${INPUT_REF} is ${status} relative to main"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "ship-gate: ${INPUT_REF} is not on main (compare status: ${status}); checking hotfix/release path"
|
||||||
|
|
||||||
|
TAG="${INPUT_REF}"
|
||||||
|
if [[ ! "${TAG}" =~ ^v ]]; then
|
||||||
|
TAG="$(git describe --tags --exact-match "${HEAD_SHA}" 2>/dev/null || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${ENVIRONMENT}" = "staging" ]; then
|
||||||
|
PATTERN='^v[0-9]+\.[0-9]+\.[0-9]+-staging$'
|
||||||
|
else
|
||||||
|
PATTERN='^v[0-9]+\.[0-9]+\.[0-9]+$'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${TAG}" ]] || [[ ! "${TAG}" =~ ${PATTERN} ]]; then
|
||||||
|
echo "ship-gate: ref ${INPUT_REF} (tag ${TAG:-none}) does not match ${PATTERN} for environment ${ENVIRONMENT} and is not on main" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export PATTERN TAG
|
||||||
|
PREV="$(
|
||||||
|
gh api "repos/${REPO}/releases" --paginate --jq '.[].tag_name' | python3 -c '
|
||||||
|
import os, re, sys
|
||||||
|
pattern = re.compile(os.environ["PATTERN"])
|
||||||
|
current = os.environ["TAG"]
|
||||||
|
tags = [
|
||||||
|
line.strip()
|
||||||
|
for line in sys.stdin
|
||||||
|
if pattern.fullmatch(line.strip()) and line.strip() != current
|
||||||
|
]
|
||||||
|
def key(tag):
|
||||||
|
body = tag[1:]
|
||||||
|
core = body.split("-", 1)[0]
|
||||||
|
return tuple(int(part) for part in core.split("."))
|
||||||
|
tags.sort(key=key)
|
||||||
|
print(tags[-1] if tags else "")
|
||||||
|
'
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [ -z "${PREV}" ]; then
|
||||||
|
echo "ship-gate: no previous matching release tag; hotfix path requires a prior ${PATTERN} release" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ff_status="$(gh api "repos/${REPO}/compare/${PREV}...${TAG}" --jq .status)"
|
||||||
|
if [ "${ff_status}" != "ahead" ]; then
|
||||||
|
echo "ship-gate: ${TAG} is not a fast-forward of ${PREV} (status: ${ff_status})" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
from_train=false
|
||||||
|
TARGET_COMMITISH="$(gh api "repos/${REPO}/releases/tags/${TAG}" --jq .target_commitish 2>/dev/null || true)"
|
||||||
|
if [[ "${TARGET_COMMITISH}" == hotfix/* || "${TARGET_COMMITISH}" == release/* ]]; then
|
||||||
|
from_train=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${from_train}" = false ]; then
|
||||||
|
git fetch origin '+refs/heads/hotfix/*:refs/remotes/origin/hotfix/*' '+refs/heads/release/*:refs/remotes/origin/release/*' || true
|
||||||
|
if git branch -r --contains "${HEAD_SHA}" | grep -Eq 'origin/(hotfix|release)/'; then
|
||||||
|
from_train=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${from_train}" = false ]; then
|
||||||
|
echo "ship-gate: ${TAG} was not created from hotfix/* or release/* (target_commitish=${TARGET_COMMITISH:-none})" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "ship-gate: hotfix/release path accepted (${PREV} -> ${TAG} from ${TARGET_COMMITISH:-branch})"
|
||||||
|
|
||||||
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
|
with:
|
||||||
|
node-version: "24"
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Build SPA
|
||||||
|
env:
|
||||||
|
VARS_JSON: ${{ toJSON(vars) }}
|
||||||
|
REQUIRED_VITE_VARS: ${{ inputs.required-vite-vars }}
|
||||||
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
|
TARGET_ENVIRONMENT: ${{ inputs.environment }}
|
||||||
|
GIT_SHA: ${{ steps.commit.outputs.sha }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
python3 -c '
|
||||||
|
import json, os, shlex, sys
|
||||||
|
required = [s.strip() for s in os.environ.get("REQUIRED_VITE_VARS", "").split(",") if s.strip()]
|
||||||
|
vars_obj = json.loads(os.environ["VARS_JSON"])
|
||||||
|
missing = [key for key in required if not vars_obj.get(key)]
|
||||||
|
if missing:
|
||||||
|
print("Missing required GitHub Environment vars: " + ", ".join(missing), file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
with open("/tmp/vite.env", "w", encoding="utf-8") as fh:
|
||||||
|
for key, value in vars_obj.items():
|
||||||
|
if key.startswith("VITE_") and value:
|
||||||
|
fh.write(f"export {key}={shlex.quote(str(value))}\n")
|
||||||
|
'
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source /tmp/vite.env
|
||||||
|
export VITE_SENTRY_ENVIRONMENT="${TARGET_ENVIRONMENT}"
|
||||||
|
export VITE_SENTRY_RELEASE="${GIT_SHA}"
|
||||||
|
npm ci
|
||||||
|
npm run build
|
||||||
|
test -f dist/index.html
|
||||||
|
find dist -name '*.map' -delete
|
||||||
|
if find dist -name '*.map' | grep -q .; then
|
||||||
|
echo "SPA source maps must not ship in dist/" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
index_sha="$(python3 -c 'import hashlib,pathlib; print(hashlib.sha256(pathlib.Path("dist/index.html").read_bytes()).hexdigest())')"
|
||||||
|
echo "INDEX_SHA256=${index_sha}" >> "${GITHUB_ENV}"
|
||||||
|
echo "dist/index.html sha256=${index_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
|
||||||
|
env:
|
||||||
|
SSM_PREFIX: ${{ inputs.ssm-prefix }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
prefix="${SSM_PREFIX%/}"
|
||||||
|
BUCKET=$(aws ssm get-parameter --name "${prefix}/bucket" --query Parameter.Value --output text)
|
||||||
|
DIST_ID=$(aws ssm get-parameter --name "${prefix}/distribution-id" --query Parameter.Value --output text)
|
||||||
|
DOMAIN=$(aws cloudfront get-distribution --id "${DIST_ID}" --query Distribution.DomainName --output text)
|
||||||
|
origin_paths="$(aws cloudfront get-distribution --id "${DIST_ID}" \
|
||||||
|
--query 'Distribution.DistributionConfig.Origins.Items[].OriginPath' --output text | tr -d '[:space:]')"
|
||||||
|
if [ -n "${origin_paths}" ]; then
|
||||||
|
echo "Distribution ${DIST_ID} still has a non-empty origin path (${origin_paths})." >&2
|
||||||
|
echo "Wait for the HCP apply that moves the origin to the bucket root, then re-run." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
{
|
||||||
|
echo "bucket=${BUCKET}"
|
||||||
|
echo "distribution_id=${DIST_ID}"
|
||||||
|
echo "site_url=https://${DOMAIN}"
|
||||||
|
} >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
- name: Sync dist/ to the bucket root
|
||||||
|
env:
|
||||||
|
SITE_BUCKET: ${{ steps.deploy.outputs.bucket }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
aws s3 sync dist/ "s3://${SITE_BUCKET}/" \
|
||||||
|
--exclude "index.html" \
|
||||||
|
--exclude "*.map" \
|
||||||
|
--cache-control "public,max-age=31536000,immutable"
|
||||||
|
aws s3 cp dist/index.html "s3://${SITE_BUCKET}/index.html" \
|
||||||
|
--cache-control "no-cache,no-store,must-revalidate" \
|
||||||
|
--content-type "text/html"
|
||||||
|
aws s3 sync dist/ "s3://${SITE_BUCKET}/" \
|
||||||
|
--delete \
|
||||||
|
--exclude "index.html" \
|
||||||
|
--exclude "*.map" \
|
||||||
|
--cache-control "public,max-age=31536000,immutable"
|
||||||
|
aws s3api head-object --bucket "${SITE_BUCKET}" --key index.html
|
||||||
|
|
||||||
|
- name: Invalidate CloudFront
|
||||||
|
env:
|
||||||
|
DISTRIBUTION_ID: ${{ steps.deploy.outputs.distribution_id }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
invalidation_id="$(aws cloudfront create-invalidation \
|
||||||
|
--distribution-id "${DISTRIBUTION_ID}" \
|
||||||
|
--paths "/*" \
|
||||||
|
--query Invalidation.Id --output text)"
|
||||||
|
echo "Invalidation ${invalidation_id} created; waiting"
|
||||||
|
aws cloudfront wait invalidation-completed \
|
||||||
|
--distribution-id "${DISTRIBUTION_ID}" \
|
||||||
|
--id "${invalidation_id}"
|
||||||
|
|
||||||
|
- name: Verify served release
|
||||||
|
env:
|
||||||
|
DISTRIBUTION_ID: ${{ steps.deploy.outputs.distribution_id }}
|
||||||
|
SITE_URL: ${{ steps.deploy.outputs.site_url }}
|
||||||
|
EXPECTED_INDEX_SHA256: ${{ env.INDEX_SHA256 }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
SITE_URL="${SITE_URL%/}"
|
||||||
|
sha256_of() {
|
||||||
|
python3 -c "import hashlib,sys; print(hashlib.sha256(sys.stdin.buffer.read()).hexdigest())"
|
||||||
|
}
|
||||||
|
last_status="Unknown"
|
||||||
|
last_hash="Unknown"
|
||||||
|
for attempt in $(seq 1 40); do
|
||||||
|
last_status="$(aws cloudfront get-distribution --id "${DISTRIBUTION_ID}" --query Distribution.Status --output text)"
|
||||||
|
if last_hash="$(curl -fsS --max-time 30 "${SITE_URL}/" | sha256_of)" && [ -n "${last_hash}" ]; then
|
||||||
|
:
|
||||||
|
else
|
||||||
|
last_hash="unreachable"
|
||||||
|
fi
|
||||||
|
echo "poll ${attempt}/40: status=${last_status} served_sha256=${last_hash}"
|
||||||
|
if [ "${last_status}" = "Deployed" ] && [ "${last_hash}" = "${EXPECTED_INDEX_SHA256}" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 15
|
||||||
|
done
|
||||||
|
echo "release did not converge: status=${last_status} served_sha256=${last_hash} expected=${EXPECTED_INDEX_SHA256}" >&2
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Verify companion API
|
||||||
|
if: ${{ inputs.verify-companion-api }}
|
||||||
|
env:
|
||||||
|
SITE_URL: ${{ steps.deploy.outputs.site_url }}
|
||||||
|
HEALTH_BUDGET: "20"
|
||||||
|
HEALTH_INTERVAL: "15"
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
SITE_URL="${SITE_URL%/}"
|
||||||
|
tmp="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "${tmp}"' EXIT
|
||||||
|
|
||||||
|
curl -fsS --max-time 30 "${SITE_URL}/" -o "${tmp}/index.html" -D "${tmp}/index.headers"
|
||||||
|
curl -fsS --max-time 30 "${SITE_URL}/signin" -o "${tmp}/signin.html"
|
||||||
|
curl -fsS --max-time 30 "${SITE_URL}/help" -o "${tmp}/route.html"
|
||||||
|
if ! grep -qiE 'cache-control:.*no-store' "${tmp}/index.headers"; then
|
||||||
|
echo "FAIL: HTML Cache-Control is missing no-store." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 -c '
|
||||||
|
import re, sys
|
||||||
|
html = open(sys.argv[1], encoding="utf-8").read()
|
||||||
|
seen = []
|
||||||
|
for path in re.findall(r"(?:src|href)=\"(/assets/[^\"]+\.(?:js|css))\"", html):
|
||||||
|
if path not in seen:
|
||||||
|
seen.append(path)
|
||||||
|
print(path)
|
||||||
|
' "${tmp}/index.html" > "${tmp}/asset-paths.txt"
|
||||||
|
|
||||||
|
if [ ! -s "${tmp}/asset-paths.txt" ]; then
|
||||||
|
echo "FAIL: served index.html has no /assets/ URL to check immutable caching." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
: > "${tmp}/assets.txt"
|
||||||
|
immutable_ok="no"
|
||||||
|
while IFS= read -r asset_path; do
|
||||||
|
curl -fsS --max-time 30 "${SITE_URL}${asset_path}" \
|
||||||
|
-o "${tmp}/asset-body" -D "${tmp}/asset.headers"
|
||||||
|
cat "${tmp}/asset-body" >> "${tmp}/assets.txt"
|
||||||
|
if [[ "${asset_path}" == *.js && "${immutable_ok}" == "no" ]]; then
|
||||||
|
if ! grep -qiE 'cache-control:.*immutable' "${tmp}/asset.headers"; then
|
||||||
|
echo "FAIL: hashed asset is missing Cache-Control immutable." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
immutable_ok="yes"
|
||||||
|
fi
|
||||||
|
done < "${tmp}/asset-paths.txt"
|
||||||
|
if [ "${immutable_ok}" != "yes" ]; then
|
||||||
|
echo "FAIL: served index.html has no hashed JS asset to check immutable caching." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cat "${tmp}/index.html" "${tmp}/assets.txt" > "${tmp}/served.txt"
|
||||||
|
if grep -Eiq 'https?://(localhost|127\.0\.0\.1):[0-9]+' "${tmp}/served.txt"; then
|
||||||
|
echo "FAIL: served assets contain forbidden URL localhost." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
health_code="000"
|
||||||
|
health_sha=""
|
||||||
|
health_attempt=0
|
||||||
|
while [ "${health_attempt}" -lt "${HEALTH_BUDGET}" ]; do
|
||||||
|
health_attempt=$((health_attempt + 1))
|
||||||
|
health_code="$(curl -sS --max-time 30 -o "${tmp}/health.json" -w '%{http_code}' "${SITE_URL}/api/health" || echo "000")"
|
||||||
|
echo "health poll ${health_attempt}/${HEALTH_BUDGET}: GET /api/health http=${health_code}"
|
||||||
|
if [ "${health_code}" = "200" ]; then
|
||||||
|
health_sha="$(python3 -c 'import json,sys
|
||||||
|
try:
|
||||||
|
print(json.load(open(sys.argv[1], encoding="utf-8")).get("sha") or "")
|
||||||
|
except Exception:
|
||||||
|
print("")
|
||||||
|
' "${tmp}/health.json")"
|
||||||
|
if [ -n "${health_sha}" ] && [ "${health_sha}" != "bootstrap" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo "health poll ${health_attempt}/${HEALTH_BUDGET}: sha=${health_sha:-missing} (waiting for Deploy API)"
|
||||||
|
fi
|
||||||
|
if [ "${health_attempt}" -lt "${HEALTH_BUDGET}" ]; then
|
||||||
|
sleep "${HEALTH_INTERVAL}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "${health_code}" != "200" ]; then
|
||||||
|
echo "FAIL: GET /api/health returned HTTP ${health_code} after ${HEALTH_BUDGET} polls." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ -z "${health_sha}" ] || [ "${health_sha}" = "bootstrap" ]; then
|
||||||
|
echo "FAIL: GET /api/health is still the bootstrap stub after ${HEALTH_BUDGET} polls." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
python3 -c 'import json,sys; body=json.load(open(sys.argv[1], encoding="utf-8")); raise SystemExit(0 if body.get("stage") and body.get("sha") else 1)' "${tmp}/health.json"
|
||||||
|
echo "PASS: companion API smoke checks passed."
|
||||||
312
.github/workflows/cd-hcp-static.yaml
vendored
Normal file
312
.github/workflows/cd-hcp-static.yaml
vendored
Normal file
|
|
@ -0,0 +1,312 @@
|
||||||
|
name: CD — HCP static site
|
||||||
|
|
||||||
|
# Reusable CloudFront/S3 CD for unfingerprinted static sites. The caller owns
|
||||||
|
# triggers and passes `environment` as a `with:` input. This job owns
|
||||||
|
# `environment:`, concurrency, OIDC, and `vars.DEPLOY_ROLE_ARN`. GitHub
|
||||||
|
# rejects `environment:` beside `uses:`.
|
||||||
|
#
|
||||||
|
# Assets are not content-hashed, so they get a one-day cache. HTML, XML, and
|
||||||
|
# text get no-cache. Do not point a hashed SPA at this workflow.
|
||||||
|
#
|
||||||
|
# Caller example:
|
||||||
|
# jobs:
|
||||||
|
# deploy-prod:
|
||||||
|
# uses: Sea-Haven-Industries/.github/.github/workflows/cd-hcp-static.yaml@<sha> # vX.Y.Z
|
||||||
|
# permissions: { contents: read, id-token: write }
|
||||||
|
# secrets: inherit
|
||||||
|
# with:
|
||||||
|
# environment: prod
|
||||||
|
# ref: ${{ inputs.ref }}
|
||||||
|
# ssm-prefix: /seahaven-site/deploy
|
||||||
|
# required-paths: _site/index.html,_site/contact/index.html,_site/404.html
|
||||||
|
# min-file-count: 40
|
||||||
|
# ship-gate: true
|
||||||
|
#
|
||||||
|
# Nothing here creates an HCP run. Terraform owns the bucket and distribution.
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
environment:
|
||||||
|
description: "GitHub Environment to deploy to (dev, staging, prod)"
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
ref:
|
||||||
|
description: "Git ref to build. Empty means github.sha."
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
ssm-prefix:
|
||||||
|
description: "SSM prefix for deploy parameters (e.g. /seahaven-site/deploy)"
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
ship-gate:
|
||||||
|
description: "Require the ref to be on main or a legal hotfix/release tag"
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
output-dir:
|
||||||
|
description: "Build output directory"
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: "_site"
|
||||||
|
required-paths:
|
||||||
|
description: "Comma-separated repo-relative files that must exist after the build"
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
min-file-count:
|
||||||
|
description: "Minimum file count under output-dir. Zero skips the count check."
|
||||||
|
type: number
|
||||||
|
required: false
|
||||||
|
default: 1
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy static site to ${{ inputs.environment }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 45
|
||||||
|
environment: ${{ inputs.environment }}
|
||||||
|
concurrency:
|
||||||
|
group: deploy-${{ inputs.ssm-prefix }}-${{ inputs.environment }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
env:
|
||||||
|
AWS_REGION: us-east-1
|
||||||
|
DEPLOY_ROLE_ARN: ${{ vars.DEPLOY_ROLE_ARN }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref != '' && inputs.ref || github.sha }}
|
||||||
|
persist-credentials: false
|
||||||
|
fetch-tags: true
|
||||||
|
|
||||||
|
- name: Resolve commit
|
||||||
|
id: commit
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
sha="$(git rev-parse HEAD)"
|
||||||
|
echo "sha=${sha}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "Building ${sha}"
|
||||||
|
|
||||||
|
- name: Ship-gate
|
||||||
|
if: ${{ inputs.ship-gate }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
INPUT_REF: ${{ inputs.ref != '' && inputs.ref || github.sha }}
|
||||||
|
ENVIRONMENT: ${{ inputs.environment }}
|
||||||
|
HEAD_SHA: ${{ steps.commit.outputs.sha }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
status="$(gh api "repos/${REPO}/compare/main...${INPUT_REF}" --jq .status)"
|
||||||
|
if [ "${status}" = "behind" ] || [ "${status}" = "identical" ]; then
|
||||||
|
echo "ship-gate: ${INPUT_REF} is ${status} relative to main"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "ship-gate: ${INPUT_REF} is not on main (compare status: ${status}); checking hotfix/release path"
|
||||||
|
|
||||||
|
TAG="${INPUT_REF}"
|
||||||
|
if [[ ! "${TAG}" =~ ^v ]]; then
|
||||||
|
TAG="$(git describe --tags --exact-match "${HEAD_SHA}" 2>/dev/null || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${ENVIRONMENT}" = "staging" ]; then
|
||||||
|
PATTERN='^v[0-9]+\.[0-9]+\.[0-9]+-staging$'
|
||||||
|
else
|
||||||
|
PATTERN='^v[0-9]+\.[0-9]+\.[0-9]+$'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${TAG}" ]] || [[ ! "${TAG}" =~ ${PATTERN} ]]; then
|
||||||
|
echo "ship-gate: ref ${INPUT_REF} (tag ${TAG:-none}) does not match ${PATTERN} for environment ${ENVIRONMENT} and is not on main" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export PATTERN TAG
|
||||||
|
PREV="$(
|
||||||
|
gh api "repos/${REPO}/releases" --paginate --jq '.[].tag_name' | python3 -c '
|
||||||
|
import os, re, sys
|
||||||
|
pattern = re.compile(os.environ["PATTERN"])
|
||||||
|
current = os.environ["TAG"]
|
||||||
|
tags = [
|
||||||
|
line.strip()
|
||||||
|
for line in sys.stdin
|
||||||
|
if pattern.fullmatch(line.strip()) and line.strip() != current
|
||||||
|
]
|
||||||
|
def key(tag):
|
||||||
|
body = tag[1:]
|
||||||
|
core = body.split("-", 1)[0]
|
||||||
|
return tuple(int(part) for part in core.split("."))
|
||||||
|
tags.sort(key=key)
|
||||||
|
print(tags[-1] if tags else "")
|
||||||
|
'
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [ -z "${PREV}" ]; then
|
||||||
|
echo "ship-gate: no previous matching release tag; hotfix path requires a prior ${PATTERN} release" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ff_status="$(gh api "repos/${REPO}/compare/${PREV}...${TAG}" --jq .status)"
|
||||||
|
if [ "${ff_status}" != "ahead" ]; then
|
||||||
|
echo "ship-gate: ${TAG} is not a fast-forward of ${PREV} (status: ${ff_status})" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
from_train=false
|
||||||
|
TARGET_COMMITISH="$(gh api "repos/${REPO}/releases/tags/${TAG}" --jq .target_commitish 2>/dev/null || true)"
|
||||||
|
if [[ "${TARGET_COMMITISH}" == hotfix/* || "${TARGET_COMMITISH}" == release/* ]]; then
|
||||||
|
from_train=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${from_train}" = false ]; then
|
||||||
|
git fetch origin '+refs/heads/hotfix/*:refs/remotes/origin/hotfix/*' '+refs/heads/release/*:refs/remotes/origin/release/*' || true
|
||||||
|
if git branch -r --contains "${HEAD_SHA}" | grep -Eq 'origin/(hotfix|release)/'; then
|
||||||
|
from_train=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${from_train}" = false ]; then
|
||||||
|
echo "ship-gate: ${TAG} was not created from hotfix/* or release/* (target_commitish=${TARGET_COMMITISH:-none})" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "ship-gate: hotfix/release path accepted (${PREV} -> ${TAG} from ${TARGET_COMMITISH:-branch})"
|
||||||
|
|
||||||
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
|
with:
|
||||||
|
node-version: "24"
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Build site
|
||||||
|
env:
|
||||||
|
OUTPUT_DIR: ${{ inputs.output-dir }}
|
||||||
|
REQUIRED_PATHS: ${{ inputs.required-paths }}
|
||||||
|
MIN_FILE_COUNT: ${{ inputs.min-file-count }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
npm ci --ignore-scripts
|
||||||
|
npm run build
|
||||||
|
python3 - <<'PY'
|
||||||
|
import os, sys
|
||||||
|
output_dir = os.environ["OUTPUT_DIR"]
|
||||||
|
if not os.path.isdir(output_dir):
|
||||||
|
print(f"build did not produce {output_dir}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
missing = []
|
||||||
|
for raw in os.environ.get("REQUIRED_PATHS", "").split(","):
|
||||||
|
path = raw.strip()
|
||||||
|
if path and not os.path.isfile(path):
|
||||||
|
missing.append(path)
|
||||||
|
if missing:
|
||||||
|
print("missing required build files: " + ", ".join(missing), file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
count = 0
|
||||||
|
for _root, _dirs, files in os.walk(output_dir):
|
||||||
|
count += len(files)
|
||||||
|
minimum = int(os.environ["MIN_FILE_COUNT"])
|
||||||
|
if minimum > 0 and count < minimum:
|
||||||
|
print(f"build produced only {count} files (expected >= {minimum})", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
index = os.path.join(output_dir, "index.html")
|
||||||
|
if not os.path.isfile(index):
|
||||||
|
print(f"missing {index}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
print(f"Build OK: {count} files.")
|
||||||
|
PY
|
||||||
|
index_sha="$(python3 -c 'import hashlib,os,pathlib; print(hashlib.sha256(pathlib.Path(os.environ["OUTPUT_DIR"], "index.html").read_bytes()).hexdigest())')"
|
||||||
|
echo "INDEX_SHA256=${index_sha}" >> "${GITHUB_ENV}"
|
||||||
|
echo "${OUTPUT_DIR}/index.html sha256=${index_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
|
||||||
|
env:
|
||||||
|
SSM_PREFIX: ${{ inputs.ssm-prefix }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
prefix="${SSM_PREFIX%/}"
|
||||||
|
BUCKET=$(aws ssm get-parameter --name "${prefix}/bucket" --query Parameter.Value --output text)
|
||||||
|
DIST_ID=$(aws ssm get-parameter --name "${prefix}/distribution-id" --query Parameter.Value --output text)
|
||||||
|
DOMAIN=$(aws cloudfront get-distribution --id "${DIST_ID}" --query Distribution.DomainName --output text)
|
||||||
|
origin_paths="$(aws cloudfront get-distribution --id "${DIST_ID}" \
|
||||||
|
--query 'Distribution.DistributionConfig.Origins.Items[].OriginPath' --output text | tr -d '[:space:]')"
|
||||||
|
if [ -n "${origin_paths}" ]; then
|
||||||
|
echo "Distribution ${DIST_ID} still has a non-empty origin path (${origin_paths})." >&2
|
||||||
|
echo "Wait for the HCP apply that moves the origin to the bucket root, then re-run." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
{
|
||||||
|
echo "bucket=${BUCKET}"
|
||||||
|
echo "distribution_id=${DIST_ID}"
|
||||||
|
echo "site_url=https://${DOMAIN}"
|
||||||
|
} >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
- name: Sync build output to the bucket root
|
||||||
|
env:
|
||||||
|
SITE_BUCKET: ${{ steps.deploy.outputs.bucket }}
|
||||||
|
OUTPUT_DIR: ${{ inputs.output-dir }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
aws s3 sync "${OUTPUT_DIR}/" "s3://${SITE_BUCKET}/" --no-progress \
|
||||||
|
--exclude "*.html" --exclude "*.xml" --exclude "*.txt" \
|
||||||
|
--cache-control "public, max-age=86400"
|
||||||
|
aws s3 sync "${OUTPUT_DIR}/" "s3://${SITE_BUCKET}/" --no-progress \
|
||||||
|
--exclude "*" --include "*.html" --include "*.xml" --include "*.txt" \
|
||||||
|
--cache-control "no-cache"
|
||||||
|
aws s3 sync "${OUTPUT_DIR}/" "s3://${SITE_BUCKET}/" --no-progress --delete
|
||||||
|
aws s3api head-object --bucket "${SITE_BUCKET}" --key index.html
|
||||||
|
|
||||||
|
- name: Invalidate CloudFront
|
||||||
|
env:
|
||||||
|
DISTRIBUTION_ID: ${{ steps.deploy.outputs.distribution_id }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
invalidation_id="$(aws cloudfront create-invalidation \
|
||||||
|
--distribution-id "${DISTRIBUTION_ID}" \
|
||||||
|
--paths "/*" \
|
||||||
|
--query Invalidation.Id --output text)"
|
||||||
|
echo "Invalidation ${invalidation_id} created; waiting"
|
||||||
|
aws cloudfront wait invalidation-completed \
|
||||||
|
--distribution-id "${DISTRIBUTION_ID}" \
|
||||||
|
--id "${invalidation_id}"
|
||||||
|
|
||||||
|
- name: Verify served release
|
||||||
|
env:
|
||||||
|
DISTRIBUTION_ID: ${{ steps.deploy.outputs.distribution_id }}
|
||||||
|
SITE_URL: ${{ steps.deploy.outputs.site_url }}
|
||||||
|
EXPECTED_INDEX_SHA256: ${{ env.INDEX_SHA256 }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
SITE_URL="${SITE_URL%/}"
|
||||||
|
sha256_of() {
|
||||||
|
python3 -c "import hashlib,sys; print(hashlib.sha256(sys.stdin.buffer.read()).hexdigest())"
|
||||||
|
}
|
||||||
|
last_status="Unknown"
|
||||||
|
last_hash="Unknown"
|
||||||
|
for attempt in $(seq 1 40); do
|
||||||
|
last_status="$(aws cloudfront get-distribution --id "${DISTRIBUTION_ID}" --query Distribution.Status --output text)"
|
||||||
|
if last_hash="$(curl -fsS --max-time 30 "${SITE_URL}/" | sha256_of)" && [ -n "${last_hash}" ]; then
|
||||||
|
:
|
||||||
|
else
|
||||||
|
last_hash="unreachable"
|
||||||
|
fi
|
||||||
|
echo "poll ${attempt}/40: status=${last_status} served_sha256=${last_hash}"
|
||||||
|
if [ "${last_status}" = "Deployed" ] && [ "${last_hash}" = "${EXPECTED_INDEX_SHA256}" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 15
|
||||||
|
done
|
||||||
|
echo "release did not converge: status=${last_status} served_sha256=${last_hash} expected=${EXPECTED_INDEX_SHA256}" >&2
|
||||||
|
exit 1
|
||||||
8
.github/workflows/cd-mobile-ios.yaml
vendored
8
.github/workflows/cd-mobile-ios.yaml
vendored
|
|
@ -69,20 +69,20 @@ jobs:
|
||||||
run:
|
run:
|
||||||
working-directory: ${{ inputs.working-directory }}
|
working-directory: ${{ inputs.working-directory }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6
|
- uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0
|
||||||
with:
|
with:
|
||||||
role-to-assume: ${{ secrets.deploy-role-arn }}
|
role-to-assume: ${{ secrets.deploy-role-arn }}
|
||||||
aws-region: ${{ inputs.region }}
|
aws-region: ${{ inputs.region }}
|
||||||
|
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ inputs.node-version }}
|
node-version: ${{ inputs.node-version }}
|
||||||
cache: npm
|
cache: npm
|
||||||
cache-dependency-path: ${{ inputs.cache-dependency-path }}
|
cache-dependency-path: ${{ inputs.cache-dependency-path }}
|
||||||
|
|
||||||
- uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1
|
- uses: ruby/setup-ruby@a0102e0972be65f351c307e2d64b9314a57c8073 # v1.324.0
|
||||||
with:
|
with:
|
||||||
ruby-version: ${{ inputs.ruby-version }}
|
ruby-version: ${{ inputs.ruby-version }}
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
|
|
|
||||||
10
.github/workflows/cd-sam.yaml
vendored
10
.github/workflows/cd-sam.yaml
vendored
|
|
@ -49,15 +49,15 @@ jobs:
|
||||||
group: cd-sam-${{ inputs.region }}-${{ inputs.stack-name }}
|
group: cd-sam-${{ inputs.region }}-${{ inputs.stack-name }}
|
||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- uses: actions/setup-python@v7
|
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ inputs.python-version }}
|
python-version: ${{ inputs.python-version }}
|
||||||
|
|
||||||
- uses: aws-actions/setup-sam@89ddb14d60e682855e3fea4be85b3c56485de310 # v3
|
- uses: aws-actions/setup-sam@89ddb14d60e682855e3fea4be85b3c56485de310 # v3.0.0
|
||||||
|
|
||||||
- uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6
|
- uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0
|
||||||
with:
|
with:
|
||||||
role-to-assume: ${{ secrets.deploy-role-arn }}
|
role-to-assume: ${{ secrets.deploy-role-arn }}
|
||||||
aws-region: ${{ inputs.region }}
|
aws-region: ${{ inputs.region }}
|
||||||
|
|
@ -69,7 +69,7 @@ jobs:
|
||||||
--stack-name "${{ inputs.stack-name }}" \
|
--stack-name "${{ inputs.stack-name }}" \
|
||||||
--query 'Stacks[0].StackStatus' --output text 2>/dev/null || echo "NOT_FOUND")
|
--query 'Stacks[0].StackStatus' --output text 2>/dev/null || echo "NOT_FOUND")
|
||||||
case "$STATUS" in
|
case "$STATUS" in
|
||||||
*ROLLBACK_COMPLETE|*FAILED)
|
ROLLBACK_COMPLETE|*FAILED)
|
||||||
echo "::error::Stack ${{ inputs.stack-name }} is in $STATUS — manual intervention required."
|
echo "::error::Stack ${{ inputs.stack-name }} is in $STATUS — manual intervention required."
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
|
|
|
||||||
295
.github/workflows/ci-autofix.yaml
vendored
Normal file
295
.github/workflows/ci-autofix.yaml
vendored
Normal file
|
|
@ -0,0 +1,295 @@
|
||||||
|
name: CI — Autofix
|
||||||
|
|
||||||
|
# Convenience formatter on pull_request. Keeps format:check / lint in the
|
||||||
|
# parallel portions as the fail-closed gate. GITHUB_TOKEN commits do not
|
||||||
|
# retrigger workflows, so this mints a GitHub App token.
|
||||||
|
#
|
||||||
|
# Skip forks, merge_group, push, and when the actor is the App (no loop).
|
||||||
|
# If the tree is dirty, commit `style: apply formatter` and push to the PR
|
||||||
|
# head, then set output committed=true so the caller skips portions on SHA_old.
|
||||||
|
# Do not --no-verify. Do not push to main.
|
||||||
|
#
|
||||||
|
# Presets run first, then any format-command / lint-fix-command / extra-command.
|
||||||
|
# prettier npm ci + npm run format (requires package-lock.json)
|
||||||
|
# eslint npm ci + npx eslint . --fix (opt-in; do not call npm run lint)
|
||||||
|
# ruff ruff format . + ruff check --fix . (ruff 0.15.22)
|
||||||
|
# terraform terraform fmt -recursive in terraform-working-directory
|
||||||
|
#
|
||||||
|
# Caller example:
|
||||||
|
# jobs:
|
||||||
|
# autofix:
|
||||||
|
# if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork
|
||||||
|
# uses: Sea-Haven-Industries/.github/.github/workflows/ci-autofix.yaml@<sha> # vX.Y.Z
|
||||||
|
# permissions: { contents: write }
|
||||||
|
# secrets: inherit
|
||||||
|
# with:
|
||||||
|
# presets: prettier,terraform
|
||||||
|
#
|
||||||
|
# Python callers pass presets: ruff,terraform. Add eslint only when that
|
||||||
|
# repo's CI lint step is ESLint itself and Prettier owns formatting.
|
||||||
|
# Do not pass `npm run lint -- --fix` (some apps chain Redocly into lint).
|
||||||
|
#
|
||||||
|
# Org secrets (names only): AUTOFMT_APP_ID, AUTOFMT_APP_PRIVATE_KEY.
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
presets:
|
||||||
|
description: "Comma-separated presets: prettier, eslint, ruff, terraform"
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
format-command:
|
||||||
|
description: "Optional write command run after presets (e.g. npm run format)"
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
lint-fix-command:
|
||||||
|
description: "Optional write lint-fix command run after presets"
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
extra-command:
|
||||||
|
description: "Optional extra write command run after presets"
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
node-version:
|
||||||
|
description: "Node.js version for the prettier or eslint preset, or an npm command"
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: "24"
|
||||||
|
terraform-version:
|
||||||
|
description: "Terraform version for the terraform preset or an extra-command that runs terraform"
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: "1.16.0"
|
||||||
|
terraform-working-directory:
|
||||||
|
description: "Directory for the terraform preset (terraform fmt -recursive)"
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: "terraform"
|
||||||
|
outputs:
|
||||||
|
committed:
|
||||||
|
description: "true when this job pushed a formatter commit"
|
||||||
|
value: ${{ jobs.autofix.outputs.committed }}
|
||||||
|
secrets:
|
||||||
|
AUTOFMT_APP_ID:
|
||||||
|
description: "GitHub App id for the formatter"
|
||||||
|
required: true
|
||||||
|
AUTOFMT_APP_PRIVATE_KEY:
|
||||||
|
description: "GitHub App private key for the formatter"
|
||||||
|
required: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
autofix:
|
||||||
|
name: autofix
|
||||||
|
if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
concurrency:
|
||||||
|
group: ci-autofix-${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
outputs:
|
||||||
|
committed: ${{ steps.result.outputs.committed }}
|
||||||
|
steps:
|
||||||
|
- name: Mint GitHub App token
|
||||||
|
id: app-token
|
||||||
|
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.AUTOFMT_APP_ID }}
|
||||||
|
private-key: ${{ secrets.AUTOFMT_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Skip App-authored synchronize
|
||||||
|
id: skip-bot
|
||||||
|
env:
|
||||||
|
ACTOR: ${{ github.actor }}
|
||||||
|
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
expected="${APP_SLUG}[bot]"
|
||||||
|
if [ "${ACTOR}" = "${expected}" ]; then
|
||||||
|
echo "skip=true" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "Actor is ${expected}; not reformatting an App push."
|
||||||
|
else
|
||||||
|
echo "skip=false" >> "${GITHUB_OUTPUT}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
if: ${{ steps.skip-bot.outputs.skip != 'true' }}
|
||||||
|
with:
|
||||||
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
ref: ${{ github.head_ref }}
|
||||||
|
persist-credentials: true
|
||||||
|
|
||||||
|
- name: Resolve presets
|
||||||
|
id: presets
|
||||||
|
env:
|
||||||
|
SKIP_BOT: ${{ steps.skip-bot.outputs.skip }}
|
||||||
|
PRESETS: ${{ inputs.presets }}
|
||||||
|
FORMAT_COMMAND: ${{ inputs.format-command }}
|
||||||
|
LINT_FIX_COMMAND: ${{ inputs.lint-fix-command }}
|
||||||
|
EXTRA_COMMAND: ${{ inputs.extra-command }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
write_outputs() {
|
||||||
|
{
|
||||||
|
echo "prettier=$1"
|
||||||
|
echo "eslint=$2"
|
||||||
|
echo "ruff=$3"
|
||||||
|
echo "terraform=$4"
|
||||||
|
} >> "${GITHUB_OUTPUT}"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "${SKIP_BOT}" = "true" ]; then
|
||||||
|
write_outputs false false false false
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
want_prettier=false
|
||||||
|
want_eslint=false
|
||||||
|
want_ruff=false
|
||||||
|
want_terraform=false
|
||||||
|
|
||||||
|
if [ -n "${PRESETS}" ]; then
|
||||||
|
IFS=',' read -ra parts <<< "${PRESETS}"
|
||||||
|
for raw in "${parts[@]}"; do
|
||||||
|
token=$(printf '%s' "${raw}" | tr -d '[:space:]')
|
||||||
|
case "${token}" in
|
||||||
|
"") ;;
|
||||||
|
prettier) want_prettier=true ;;
|
||||||
|
eslint) want_eslint=true ;;
|
||||||
|
ruff) want_ruff=true ;;
|
||||||
|
terraform) want_terraform=true ;;
|
||||||
|
*)
|
||||||
|
echo "Unknown preset: ${token}" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if { [ "${want_prettier}" = "true" ] || [ "${want_eslint}" = "true" ]; } && [ ! -f package-lock.json ]; then
|
||||||
|
echo "prettier and eslint presets require package-lock.json" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${want_prettier}" = "false" ] \
|
||||||
|
&& [ "${want_eslint}" = "false" ] \
|
||||||
|
&& [ "${want_ruff}" = "false" ] \
|
||||||
|
&& [ "${want_terraform}" = "false" ] \
|
||||||
|
&& [ -z "${FORMAT_COMMAND}" ] \
|
||||||
|
&& [ -z "${LINT_FIX_COMMAND}" ] \
|
||||||
|
&& [ -z "${EXTRA_COMMAND}" ]; then
|
||||||
|
echo "Set presets or a format, lint-fix, or extra command." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
write_outputs "${want_prettier}" "${want_eslint}" "${want_ruff}" "${want_terraform}"
|
||||||
|
|
||||||
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
|
if: ${{ steps.skip-bot.outputs.skip != 'true' && hashFiles('package-lock.json') != '' && (steps.presets.outputs.prettier == 'true' || steps.presets.outputs.eslint == 'true' || contains(inputs.format-command, 'npm') || contains(inputs.lint-fix-command, 'npm')) }}
|
||||||
|
with:
|
||||||
|
node-version: ${{ inputs.node-version }}
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install npm dependencies
|
||||||
|
if: ${{ steps.skip-bot.outputs.skip != 'true' && hashFiles('package-lock.json') != '' && (steps.presets.outputs.prettier == 'true' || steps.presets.outputs.eslint == 'true' || contains(inputs.format-command, 'npm') || contains(inputs.lint-fix-command, 'npm')) }}
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
|
if: ${{ steps.skip-bot.outputs.skip != 'true' && (steps.presets.outputs.ruff == 'true' || contains(inputs.format-command, 'ruff') || contains(inputs.lint-fix-command, 'ruff') || contains(inputs.extra-command, 'ruff')) }}
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Install ruff
|
||||||
|
if: ${{ steps.skip-bot.outputs.skip != 'true' && (steps.presets.outputs.ruff == 'true' || contains(inputs.format-command, 'ruff') || contains(inputs.lint-fix-command, 'ruff') || contains(inputs.extra-command, 'ruff')) }}
|
||||||
|
run: pip install 'ruff==0.15.22'
|
||||||
|
|
||||||
|
- uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
|
||||||
|
if: ${{ steps.skip-bot.outputs.skip != 'true' && (steps.presets.outputs.terraform == 'true' || contains(inputs.extra-command, 'terraform')) }}
|
||||||
|
with:
|
||||||
|
terraform_version: ${{ inputs.terraform-version }}
|
||||||
|
terraform_wrapper: false
|
||||||
|
|
||||||
|
- name: Apply formatter
|
||||||
|
if: ${{ steps.skip-bot.outputs.skip != 'true' }}
|
||||||
|
env:
|
||||||
|
PRETTIER: ${{ steps.presets.outputs.prettier }}
|
||||||
|
ESLINT: ${{ steps.presets.outputs.eslint }}
|
||||||
|
RUFF: ${{ steps.presets.outputs.ruff }}
|
||||||
|
TERRAFORM: ${{ steps.presets.outputs.terraform }}
|
||||||
|
TERRAFORM_DIR: ${{ inputs.terraform-working-directory }}
|
||||||
|
FORMAT_COMMAND: ${{ inputs.format-command }}
|
||||||
|
LINT_FIX_COMMAND: ${{ inputs.lint-fix-command }}
|
||||||
|
EXTRA_COMMAND: ${{ inputs.extra-command }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [ "${PRETTIER}" = "true" ]; then
|
||||||
|
npm run format
|
||||||
|
fi
|
||||||
|
if [ "${ESLINT}" = "true" ]; then
|
||||||
|
npx eslint . --fix
|
||||||
|
fi
|
||||||
|
if [ "${RUFF}" = "true" ]; then
|
||||||
|
ruff format .
|
||||||
|
ruff check --fix .
|
||||||
|
fi
|
||||||
|
if [ "${TERRAFORM}" = "true" ]; then
|
||||||
|
terraform -chdir="${TERRAFORM_DIR}" fmt -recursive
|
||||||
|
fi
|
||||||
|
if [ -n "${FORMAT_COMMAND}" ]; then
|
||||||
|
bash -euo pipefail -c "${FORMAT_COMMAND}"
|
||||||
|
fi
|
||||||
|
if [ -n "${LINT_FIX_COMMAND}" ]; then
|
||||||
|
bash -euo pipefail -c "${LINT_FIX_COMMAND}"
|
||||||
|
fi
|
||||||
|
if [ -n "${EXTRA_COMMAND}" ]; then
|
||||||
|
bash -euo pipefail -c "${EXTRA_COMMAND}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Commit and push if dirty
|
||||||
|
id: result
|
||||||
|
env:
|
||||||
|
SKIP_BOT: ${{ steps.skip-bot.outputs.skip }}
|
||||||
|
HEAD_REF: ${{ github.head_ref }}
|
||||||
|
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
|
||||||
|
APP_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ "${SKIP_BOT}" = "true" ]; then
|
||||||
|
echo "committed=false" >> "${GITHUB_OUTPUT}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${HEAD_REF}" ] || [ "${HEAD_REF}" = "main" ]; then
|
||||||
|
echo "Refusing to push formatter commits to ${HEAD_REF:-empty}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The noreply local-part must be the bot account id, not the App id.
|
||||||
|
# An App-id prefix still pushes, but GitHub does not link the commit
|
||||||
|
# to the bot, so the app logo is not used.
|
||||||
|
bot_id=$(curl -fsSL \
|
||||||
|
-H "Authorization: Bearer ${APP_TOKEN}" \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||||
|
"https://api.github.com/users/${APP_SLUG}%5Bbot%5D" | jq -er '.id')
|
||||||
|
git config user.name "${APP_SLUG}[bot]"
|
||||||
|
git config user.email "${bot_id}+${APP_SLUG}[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
if [ -z "$(git status --porcelain)" ]; then
|
||||||
|
echo "Tree is clean; no formatter commit."
|
||||||
|
echo "committed=false" >> "${GITHUB_OUTPUT}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
git add -A
|
||||||
|
git commit -m "style: apply formatter"
|
||||||
|
git push origin "HEAD:refs/heads/${HEAD_REF}"
|
||||||
|
echo "committed=true" >> "${GITHUB_OUTPUT}"
|
||||||
4
.github/workflows/ci-dotnet.yaml
vendored
4
.github/workflows/ci-dotnet.yaml
vendored
|
|
@ -34,9 +34,9 @@ jobs:
|
||||||
run:
|
run:
|
||||||
working-directory: ${{ inputs.working-directory }}
|
working-directory: ${{ inputs.working-directory }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- uses: actions/setup-dotnet@v6
|
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: ${{ inputs.dotnet-version }}
|
dotnet-version: ${{ inputs.dotnet-version }}
|
||||||
|
|
||||||
|
|
|
||||||
262
.github/workflows/ci-frontend.yaml
vendored
Normal file
262
.github/workflows/ci-frontend.yaml
vendored
Normal file
|
|
@ -0,0 +1,262 @@
|
||||||
|
name: CI — Frontend
|
||||||
|
|
||||||
|
# Parallel CI for bundled TypeScript front-end apps (Vite / React / Vue SPAs
|
||||||
|
# with vitest + Playwright). Jobs: guard, static, build, unit (optional shards),
|
||||||
|
# browser-smoke. The caller owns the `ci-complete` aggregator and ruleset check.
|
||||||
|
# Do not put these portion names in an org ruleset.
|
||||||
|
#
|
||||||
|
# Remaining-lane repos that still need the sequential `ci / ci` context should
|
||||||
|
# keep calling ci-typescript-frontend.yaml until they migrate.
|
||||||
|
#
|
||||||
|
# Caller example:
|
||||||
|
# jobs:
|
||||||
|
# frontend:
|
||||||
|
# uses: Sea-Haven-Industries/.github/.github/workflows/ci-frontend.yaml@<sha> # vX.Y.Z
|
||||||
|
# with:
|
||||||
|
# node-version: "24"
|
||||||
|
# unit-shards: 4
|
||||||
|
# run-e2e: true
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
node-version:
|
||||||
|
description: "Node.js version to use"
|
||||||
|
type: string
|
||||||
|
default: "24"
|
||||||
|
unit-shards:
|
||||||
|
description: "Vitest shard count (1-8). PR UI shows unit (1) .. unit (N)."
|
||||||
|
type: number
|
||||||
|
default: 1
|
||||||
|
run-e2e:
|
||||||
|
description: "Run the test:e2e script (Playwright browser smoke)"
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
required-scripts:
|
||||||
|
description: "Comma-separated npm scripts that must exist in package.json"
|
||||||
|
type: string
|
||||||
|
default: "format:check,lint,build,test,test:e2e"
|
||||||
|
working-directory:
|
||||||
|
description: "Directory to run npm/build/test commands from"
|
||||||
|
type: string
|
||||||
|
default: "."
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
guard:
|
||||||
|
name: guard
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
concurrency:
|
||||||
|
group: ci-frontend-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-guard
|
||||||
|
cancel-in-progress: true
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ${{ inputs.working-directory }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Verify unit-shards
|
||||||
|
env:
|
||||||
|
UNIT_SHARDS: ${{ inputs.unit-shards }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [ "${UNIT_SHARDS}" -lt 1 ] || [ "${UNIT_SHARDS}" -gt 8 ]; then
|
||||||
|
echo "unit-shards must be between 1 and 8 (got ${UNIT_SHARDS})" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Verify required npm scripts
|
||||||
|
env:
|
||||||
|
REQUIRED_SCRIPTS: ${{ inputs.required-scripts }}
|
||||||
|
RUN_E2E: ${{ inputs.run-e2e }}
|
||||||
|
run: |
|
||||||
|
node <<'NODE'
|
||||||
|
const { readFileSync } = require("node:fs");
|
||||||
|
const pkg = JSON.parse(readFileSync("package.json", "utf8"));
|
||||||
|
const required = (process.env.REQUIRED_SCRIPTS || "")
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter((script) => process.env.RUN_E2E !== "false" || script !== "test:e2e");
|
||||||
|
const missing = required.filter((script) => !pkg.scripts?.[script]);
|
||||||
|
|
||||||
|
if (missing.length > 0) {
|
||||||
|
console.error(`Missing required scripts: ${missing.join(", ")}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(`All required scripts present: ${required.join(", ")}`);
|
||||||
|
NODE
|
||||||
|
|
||||||
|
- name: Guard changed lines
|
||||||
|
env:
|
||||||
|
EVENT_NAME: ${{ github.event_name }}
|
||||||
|
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||||
|
PUSH_BEFORE: ${{ github.event.before }}
|
||||||
|
MERGE_GROUP_BASE_SHA: ${{ github.event.merge_group.base_sha }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ "${EVENT_NAME}" = "pull_request" ]; then
|
||||||
|
BASE_REF="${PR_BASE_SHA}"
|
||||||
|
elif [ "${EVENT_NAME}" = "merge_group" ]; then
|
||||||
|
BASE_REF="${MERGE_GROUP_BASE_SHA}"
|
||||||
|
else
|
||||||
|
BASE_REF="${PUSH_BEFORE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${BASE_REF}" ] || [ "${BASE_REF}" = "0000000000000000000000000000000000000000" ]; then
|
||||||
|
BASE_REF="$(git rev-parse HEAD~1 2>/dev/null || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${BASE_REF}" ]; then
|
||||||
|
echo "No base ref available; skipping changed-line guard."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
ADDED_LINES="$(git diff --unified=0 "${BASE_REF}" "${GITHUB_SHA}" | grep -E '^\+' | grep -vE '^\+\+\+' || true)"
|
||||||
|
|
||||||
|
if printf '%s\n' "${ADDED_LINES}" | grep -E 'Generated with (Claude [C]ode|[C]odex|Chat[G]PT)|--no-[v]erify|HUSKY[=]0'; then
|
||||||
|
echo "Found generated-tool footer or hook bypass wording in added lines."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if printf '%s\n' "${ADDED_LINES}" | grep -E 'A[K]IA[0-9A-Z]{16}|A[S]IA[0-9A-Z]{16}|AWS[_]SECRET[_]ACCESS[_]KEY|aws[_-]?secret[_-]?access[_-]?key|J[W]TAuthenticationHIGHsecuredPassword|P[a]ssword=|S[e]ndGrid.*A[p]iKey'; then
|
||||||
|
echo "Found a likely secret in added lines. Move sensitive values to the environment or secret manager."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Changed-line guard passed."
|
||||||
|
|
||||||
|
- name: Conventions check
|
||||||
|
working-directory: ${{ github.workspace }}
|
||||||
|
run: |
|
||||||
|
errors=0
|
||||||
|
fail() { echo "::error::$1"; errors=$((errors + 1)); }
|
||||||
|
[[ -f README.md ]] || fail "Missing README.md"
|
||||||
|
if [[ -f .gitignore ]]; then
|
||||||
|
grep -qE '^\.env$|^\.env\b' .gitignore || fail ".gitignore does not include .env"
|
||||||
|
else
|
||||||
|
fail "Missing .gitignore"
|
||||||
|
fi
|
||||||
|
if [[ $errors -gt 0 ]]; then
|
||||||
|
echo "Conventions check failed with $errors error(s)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Conventions check passed."
|
||||||
|
|
||||||
|
static:
|
||||||
|
name: static
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
concurrency:
|
||||||
|
group: ci-frontend-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-static
|
||||||
|
cancel-in-progress: true
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ${{ inputs.working-directory }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
|
with:
|
||||||
|
node-version: ${{ inputs.node-version }}
|
||||||
|
cache: npm
|
||||||
|
cache-dependency-path: ${{ inputs.working-directory == '.' && 'package-lock.json' || format('{0}/package-lock.json', inputs.working-directory) }}
|
||||||
|
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run format:check
|
||||||
|
- run: npm run lint
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
concurrency:
|
||||||
|
group: ci-frontend-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-build
|
||||||
|
cancel-in-progress: true
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ${{ inputs.working-directory }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
|
with:
|
||||||
|
node-version: ${{ inputs.node-version }}
|
||||||
|
cache: npm
|
||||||
|
cache-dependency-path: ${{ inputs.working-directory == '.' && 'package-lock.json' || format('{0}/package-lock.json', inputs.working-directory) }}
|
||||||
|
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run build
|
||||||
|
|
||||||
|
unit:
|
||||||
|
name: unit (${{ matrix.shard }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
concurrency:
|
||||||
|
group: ci-frontend-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-unit-${{ matrix.shard }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
shard: ${{ fromJSON(format('[{0}]', inputs.unit-shards == 1 && '1' || inputs.unit-shards == 2 && '1,2' || inputs.unit-shards == 3 && '1,2,3' || inputs.unit-shards == 4 && '1,2,3,4' || inputs.unit-shards == 5 && '1,2,3,4,5' || inputs.unit-shards == 6 && '1,2,3,4,5,6' || inputs.unit-shards == 7 && '1,2,3,4,5,6,7' || '1,2,3,4,5,6,7,8')) }}
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ${{ inputs.working-directory }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
|
with:
|
||||||
|
node-version: ${{ inputs.node-version }}
|
||||||
|
cache: npm
|
||||||
|
cache-dependency-path: ${{ inputs.working-directory == '.' && 'package-lock.json' || format('{0}/package-lock.json', inputs.working-directory) }}
|
||||||
|
|
||||||
|
- run: npm ci
|
||||||
|
- name: Unit tests
|
||||||
|
env:
|
||||||
|
SHARD: ${{ matrix.shard }}
|
||||||
|
SHARDS: ${{ inputs.unit-shards }}
|
||||||
|
run: npm test -- --shard="${SHARD}/${SHARDS}"
|
||||||
|
|
||||||
|
browser-smoke:
|
||||||
|
name: browser-smoke
|
||||||
|
if: ${{ inputs.run-e2e }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
concurrency:
|
||||||
|
group: ci-frontend-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-browser-smoke
|
||||||
|
cancel-in-progress: true
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ${{ inputs.working-directory }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
|
with:
|
||||||
|
node-version: ${{ inputs.node-version }}
|
||||||
|
cache: npm
|
||||||
|
cache-dependency-path: ${{ inputs.working-directory == '.' && 'package-lock.json' || format('{0}/package-lock.json', inputs.working-directory) }}
|
||||||
|
|
||||||
|
- run: npm ci
|
||||||
|
- name: Browser smoke
|
||||||
|
env:
|
||||||
|
CI: "true"
|
||||||
|
run: |
|
||||||
|
npx playwright install --with-deps chromium
|
||||||
|
npm run test:e2e
|
||||||
12
.github/workflows/ci-mobile-ios.yaml
vendored
12
.github/workflows/ci-mobile-ios.yaml
vendored
|
|
@ -35,7 +35,7 @@ name: CI — Mobile iOS
|
||||||
# Caller example:
|
# Caller example:
|
||||||
# jobs:
|
# jobs:
|
||||||
# ci:
|
# ci:
|
||||||
# uses: Sea-Haven-Industries/.github/.github/workflows/ci-mobile-ios.yaml@<full-commit-sha> # main
|
# uses: Sea-Haven-Industries/.github/.github/workflows/ci-mobile-ios.yaml@<full-commit-sha> # v1.0.4
|
||||||
# with:
|
# with:
|
||||||
# working-directory: mobile
|
# working-directory: mobile
|
||||||
# cache-dependency-path: mobile/package-lock.json
|
# cache-dependency-path: mobile/package-lock.json
|
||||||
|
|
@ -137,9 +137,9 @@ jobs:
|
||||||
run:
|
run:
|
||||||
working-directory: ${{ inputs.working-directory }}
|
working-directory: ${{ inputs.working-directory }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ inputs.node-version }}
|
node-version: ${{ inputs.node-version }}
|
||||||
cache: npm
|
cache: npm
|
||||||
|
|
@ -206,15 +206,15 @@ jobs:
|
||||||
run:
|
run:
|
||||||
working-directory: ${{ inputs.working-directory }}
|
working-directory: ${{ inputs.working-directory }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ inputs.node-version }}
|
node-version: ${{ inputs.node-version }}
|
||||||
cache: npm
|
cache: npm
|
||||||
cache-dependency-path: ${{ inputs.cache-dependency-path }}
|
cache-dependency-path: ${{ inputs.cache-dependency-path }}
|
||||||
|
|
||||||
- uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1
|
- uses: ruby/setup-ruby@a0102e0972be65f351c307e2d64b9314a57c8073 # v1.324.0
|
||||||
with:
|
with:
|
||||||
ruby-version: ${{ inputs.ruby-version }}
|
ruby-version: ${{ inputs.ruby-version }}
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
|
|
|
||||||
90
.github/workflows/ci-python-app.yaml
vendored
90
.github/workflows/ci-python-app.yaml
vendored
|
|
@ -1,19 +1,14 @@
|
||||||
name: CI — Python (app)
|
name: CI — Python (app)
|
||||||
|
|
||||||
# Reusable CI for plain Python apps / locally-run tooling that do NOT deploy via
|
# Reusable CI for plain Python apps / locally-run tooling that do NOT deploy via
|
||||||
# SAM or CDK (use ci-python-sam.yaml / ci-typescript-cdk.yaml for those). Beyond
|
# SAM or CDK (use ci-python-sam.yaml / ci-typescript-cdk.yaml for those).
|
||||||
# lint + format it adds two things such repos commonly need:
|
# Runs ruff check + format, plus an optional conventions audit.
|
||||||
# * a collect-only import check for a root suite whose live run needs secrets
|
|
||||||
# (verifies every test module imports cleanly without running them), and
|
|
||||||
# * an isolated full pytest run for a self-contained subproject dir whose tests
|
|
||||||
# package collides with the root tests/ package (e.g. a `tests/` under a
|
|
||||||
# subdir) and so must run in its own working directory.
|
|
||||||
#
|
#
|
||||||
# Naming is load-bearing (see this repo's ci.yaml): the org ruleset matches the
|
# Naming is load-bearing (see this repo's ci.yaml): the org ruleset matches the
|
||||||
# required `ci / ci` check against the JOB check-run name. A caller job keyed `ci`
|
# required `ci / ci` check against the JOB check-run name. A caller job keyed `ci`
|
||||||
# invoking this workflow reports each job here as `ci / <job>`, so the aggregator
|
# invoking this workflow reports each job here as `ci / <job>`, so the aggregator
|
||||||
# job below is keyed `ci` to emit exactly `ci / ci`. The aggregator gates on every
|
# job below is keyed `ci` to emit exactly `ci / ci`. The aggregator gates on lint,
|
||||||
# other job, so the single required check fails if any sub-job fails.
|
# so the single required check fails if lint fails.
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
|
|
@ -26,18 +21,6 @@ on:
|
||||||
description: "Space-separated directories for ruff (default: repo root)"
|
description: "Space-separated directories for ruff (default: repo root)"
|
||||||
type: string
|
type: string
|
||||||
default: "."
|
default: "."
|
||||||
requirements:
|
|
||||||
description: "Requirements file used for the pip cache key + install"
|
|
||||||
type: string
|
|
||||||
default: "requirements.txt"
|
|
||||||
collect-only:
|
|
||||||
description: "Run 'pytest --collect-only' at the repo root (imports resolve without secrets)"
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
subproject-dir:
|
|
||||||
description: "Optional self-contained subproject dir whose pytest suite runs in full"
|
|
||||||
type: string
|
|
||||||
default: ""
|
|
||||||
run-conventions-check:
|
run-conventions-check:
|
||||||
description: "Run the lightweight conventions audit (README + .gitignore covers .env)"
|
description: "Run the lightweight conventions audit (README + .gitignore covers .env)"
|
||||||
type: boolean
|
type: boolean
|
||||||
|
|
@ -52,17 +35,15 @@ jobs:
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
# The trailing segment of every group in this file is the job id written
|
# The trailing segment of every group in this file is the job id written
|
||||||
# out literally, NOT `${{ github.job }}`. In a called workflow that
|
# out literally, NOT `${{ github.job }}`. In a called workflow that
|
||||||
# expression evaluates to the CALLER's job id, so all four jobs here would
|
# expression evaluates to the CALLER's job id, so sibling jobs would
|
||||||
# resolve to one group and, with cancel-in-progress on, cancel each other.
|
# resolve to one group and, with cancel-in-progress on, cancel each other.
|
||||||
# Observed live in pr-reviewer: `lint` was cancelled one second in by a
|
|
||||||
# sibling and the aggregator failed on the cancelled dependency.
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ci-python-app-${{ github.workflow }}-${{ github.ref }}-${{ inputs.source-dirs }}-lint
|
group: ci-python-app-${{ github.workflow }}-${{ github.ref }}-${{ inputs.source-dirs }}-lint
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- uses: actions/setup-python@v7
|
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ inputs.python-version }}
|
python-version: ${{ inputs.python-version }}
|
||||||
|
|
||||||
|
|
@ -101,68 +82,19 @@ jobs:
|
||||||
fi
|
fi
|
||||||
echo "Conventions check passed."
|
echo "Conventions check passed."
|
||||||
|
|
||||||
test-collect:
|
|
||||||
if: ${{ inputs.collect-only }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 10
|
|
||||||
concurrency:
|
|
||||||
group: ci-python-app-${{ github.workflow }}-${{ github.ref }}-${{ inputs.source-dirs }}-test-collect
|
|
||||||
cancel-in-progress: true
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
|
|
||||||
- uses: actions/setup-python@v7
|
|
||||||
with:
|
|
||||||
python-version: ${{ inputs.python-version }}
|
|
||||||
cache: pip
|
|
||||||
cache-dependency-path: ${{ inputs.requirements }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
pip install -r "${{ inputs.requirements }}"
|
|
||||||
pip install pytest python-dotenv
|
|
||||||
|
|
||||||
- name: Pytest collect-only
|
|
||||||
run: pytest --collect-only -q
|
|
||||||
|
|
||||||
subproject-tests:
|
|
||||||
if: ${{ inputs.subproject-dir != '' }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 10
|
|
||||||
concurrency:
|
|
||||||
group: ci-python-app-${{ github.workflow }}-${{ github.ref }}-${{ inputs.source-dirs }}-subproject-tests
|
|
||||||
cancel-in-progress: true
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
|
|
||||||
- uses: actions/setup-python@v7
|
|
||||||
with:
|
|
||||||
python-version: ${{ inputs.python-version }}
|
|
||||||
cache: pip
|
|
||||||
cache-dependency-path: ${{ inputs.requirements }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
pip install -r "${{ inputs.requirements }}"
|
|
||||||
pip install pytest
|
|
||||||
|
|
||||||
- name: Run subproject suite
|
|
||||||
working-directory: ${{ inputs.subproject-dir }}
|
|
||||||
run: python -m pytest -q
|
|
||||||
|
|
||||||
ci:
|
ci:
|
||||||
# Aggregator — keyed `ci` so a caller job keyed `ci` reports `ci / ci`.
|
# Aggregator — keyed `ci` so a caller job keyed `ci` reports `ci / ci`.
|
||||||
needs: [lint, test-collect, subproject-tests]
|
needs: [lint]
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ci-python-app-${{ github.workflow }}-${{ github.ref }}-${{ inputs.source-dirs }}-ci
|
group: ci-python-app-${{ github.workflow }}-${{ github.ref }}-${{ inputs.source-dirs }}-ci
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- name: Require all jobs to have succeeded
|
- name: Require lint to have succeeded
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "true" ]; then
|
if [ "${{ needs.lint.result }}" != "success" ]; then
|
||||||
echo "A required CI job failed or was cancelled."
|
echo "lint failed or was cancelled."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "All CI jobs passed."
|
echo "All CI jobs passed."
|
||||||
|
|
|
||||||
10
.github/workflows/ci-python-sam.yaml
vendored
10
.github/workflows/ci-python-sam.yaml
vendored
|
|
@ -55,9 +55,9 @@ jobs:
|
||||||
group: ci-python-sam-${{ github.workflow }}-${{ github.ref }}-${{ inputs.source-dirs }}
|
group: ci-python-sam-${{ github.workflow }}-${{ github.ref }}-${{ inputs.source-dirs }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- uses: actions/setup-python@v7
|
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ inputs.python-version }}
|
python-version: ${{ inputs.python-version }}
|
||||||
|
|
||||||
|
|
@ -89,13 +89,13 @@ jobs:
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
if: ${{ inputs.run-cdk-synth }}
|
if: ${{ inputs.run-cdk-synth }}
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ inputs.node-version }}
|
node-version: ${{ inputs.node-version }}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
if: ${{ inputs.run-cdk-synth && inputs.enable-qemu }}
|
if: ${{ inputs.run-cdk-synth && inputs.enable-qemu }}
|
||||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4
|
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
|
||||||
|
|
||||||
- name: CDK synth
|
- name: CDK synth
|
||||||
if: ${{ inputs.run-cdk-synth }}
|
if: ${{ inputs.run-cdk-synth }}
|
||||||
|
|
@ -153,7 +153,7 @@ jobs:
|
||||||
|
|
||||||
- name: Setup SAM CLI
|
- name: Setup SAM CLI
|
||||||
if: ${{ inputs.run-sam-validate }}
|
if: ${{ inputs.run-sam-validate }}
|
||||||
uses: aws-actions/setup-sam@89ddb14d60e682855e3fea4be85b3c56485de310 # v3
|
uses: aws-actions/setup-sam@89ddb14d60e682855e3fea4be85b3c56485de310 # v3.0.0
|
||||||
|
|
||||||
- name: SAM validate
|
- name: SAM validate
|
||||||
if: ${{ inputs.run-sam-validate }}
|
if: ${{ inputs.run-sam-validate }}
|
||||||
|
|
|
||||||
6
.github/workflows/ci-static.yaml
vendored
6
.github/workflows/ci-static.yaml
vendored
|
|
@ -15,7 +15,7 @@ name: CI — Static Site
|
||||||
# Caller example (build mode):
|
# Caller example (build mode):
|
||||||
# jobs:
|
# jobs:
|
||||||
# ci:
|
# ci:
|
||||||
# uses: Sea-Haven-Industries/.github/.github/workflows/ci-static.yaml@<full-commit-sha> # main
|
# uses: Sea-Haven-Industries/.github/.github/workflows/ci-static.yaml@<full-commit-sha> # v1.0.4
|
||||||
# with:
|
# with:
|
||||||
# build-command: "npx @11ty/eleventy"
|
# build-command: "npx @11ty/eleventy"
|
||||||
# check-dir: "_site"
|
# check-dir: "_site"
|
||||||
|
|
@ -70,9 +70,9 @@ jobs:
|
||||||
CHECK_DIR: ${{ inputs.check-dir }}
|
CHECK_DIR: ${{ inputs.check-dir }}
|
||||||
BUILD_COMMAND: ${{ inputs.build-command }}
|
BUILD_COMMAND: ${{ inputs.build-command }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
if: ${{ inputs.run-htmlhint || inputs.build-command != '' }}
|
if: ${{ inputs.run-htmlhint || inputs.build-command != '' }}
|
||||||
with:
|
with:
|
||||||
node-version: ${{ inputs.node-version }}
|
node-version: ${{ inputs.node-version }}
|
||||||
|
|
|
||||||
57
.github/workflows/ci-terraform.yaml
vendored
Normal file
57
.github/workflows/ci-terraform.yaml
vendored
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
name: CI — Terraform
|
||||||
|
|
||||||
|
# Reusable Terraform fmt/init/validate for HCP app repos. Init uses
|
||||||
|
# `-backend=false` so CI does not need remote state credentials. The caller
|
||||||
|
# owns the `ci-complete` aggregator.
|
||||||
|
#
|
||||||
|
# Caller example:
|
||||||
|
# jobs:
|
||||||
|
# terraform:
|
||||||
|
# uses: Sea-Haven-Industries/.github/.github/workflows/ci-terraform.yaml@<sha> # vX.Y.Z
|
||||||
|
# with:
|
||||||
|
# terraform-version: "1.16.0"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
terraform-version:
|
||||||
|
description: "Terraform version to install"
|
||||||
|
type: string
|
||||||
|
default: "1.16.0"
|
||||||
|
working-directory:
|
||||||
|
description: "Directory containing Terraform sources"
|
||||||
|
type: string
|
||||||
|
default: "terraform"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
terraform:
|
||||||
|
name: terraform
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
concurrency:
|
||||||
|
group: ci-terraform-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ${{ inputs.working-directory }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
|
||||||
|
with:
|
||||||
|
terraform_version: ${{ inputs.terraform-version }}
|
||||||
|
terraform_wrapper: false
|
||||||
|
|
||||||
|
- name: Terraform fmt
|
||||||
|
run: terraform fmt -check -recursive
|
||||||
|
|
||||||
|
- name: Terraform init
|
||||||
|
run: terraform init -backend=false
|
||||||
|
|
||||||
|
- name: Terraform validate
|
||||||
|
run: terraform validate
|
||||||
10
.github/workflows/ci-typescript-cdk.yaml
vendored
10
.github/workflows/ci-typescript-cdk.yaml
vendored
|
|
@ -67,12 +67,12 @@ jobs:
|
||||||
group: ci-typescript-cdk-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}
|
group: ci-typescript-cdk-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4
|
- uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
|
||||||
if: ${{ inputs.enable-qemu }}
|
if: ${{ inputs.enable-qemu }}
|
||||||
|
|
||||||
- uses: actions/setup-dotnet@v6
|
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
|
||||||
if: ${{ inputs.dotnet-version != '' }}
|
if: ${{ inputs.dotnet-version != '' }}
|
||||||
with:
|
with:
|
||||||
dotnet-version: ${{ inputs.dotnet-version }}
|
dotnet-version: ${{ inputs.dotnet-version }}
|
||||||
|
|
@ -81,7 +81,7 @@ jobs:
|
||||||
if: ${{ inputs.dotnet-publish-project != '' }}
|
if: ${{ inputs.dotnet-publish-project != '' }}
|
||||||
run: dotnet publish ${{ inputs.dotnet-publish-project }} --configuration Release --runtime linux-arm64 --self-contained
|
run: dotnet publish ${{ inputs.dotnet-publish-project }} --configuration Release --runtime linux-arm64 --self-contained
|
||||||
|
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ inputs.node-version }}
|
node-version: ${{ inputs.node-version }}
|
||||||
cache: npm
|
cache: npm
|
||||||
|
|
@ -159,7 +159,7 @@ jobs:
|
||||||
|
|
||||||
- name: Setup SAM CLI
|
- name: Setup SAM CLI
|
||||||
if: ${{ inputs.run-sam-validate }}
|
if: ${{ inputs.run-sam-validate }}
|
||||||
uses: aws-actions/setup-sam@89ddb14d60e682855e3fea4be85b3c56485de310 # v3
|
uses: aws-actions/setup-sam@89ddb14d60e682855e3fea4be85b3c56485de310 # v3.0.0
|
||||||
|
|
||||||
- name: SAM validate
|
- name: SAM validate
|
||||||
if: ${{ inputs.run-sam-validate }}
|
if: ${{ inputs.run-sam-validate }}
|
||||||
|
|
|
||||||
16
.github/workflows/ci-typescript-frontend.yaml
vendored
16
.github/workflows/ci-typescript-frontend.yaml
vendored
|
|
@ -1,9 +1,11 @@
|
||||||
name: CI — TypeScript Frontend
|
name: CI — TypeScript Frontend
|
||||||
|
|
||||||
# Reusable CI for bundled TypeScript front-end apps (Vite / React / Vue SPAs
|
# Sequential reusable CI for remaining-lane TypeScript front-end apps that
|
||||||
# with vitest + Playwright). Emits the single `ci / ci` status context required
|
# still emit `ci / ci`. HCP app repos should call ci-frontend.yaml (parallel
|
||||||
# by the org branch-protection rulesets — keep the caller job id `ci` so the
|
# portions) plus a caller-owned `ci-complete` aggregator instead.
|
||||||
# context resolves to `ci / ci`.
|
#
|
||||||
|
# Emits the single `ci / ci` status context required by the unconverted-repo
|
||||||
|
# ruleset — keep the caller job id `ci` so the context resolves to `ci / ci`.
|
||||||
#
|
#
|
||||||
# Runs, in order: a Sea Haven standards gate (required npm scripts present, no
|
# Runs, in order: a Sea Haven standards gate (required npm scripts present, no
|
||||||
# AI-tool footers / hook bypasses / hardcoded secrets in the added lines),
|
# AI-tool footers / hook bypasses / hardcoded secrets in the added lines),
|
||||||
|
|
@ -13,7 +15,7 @@ name: CI — TypeScript Frontend
|
||||||
# Caller example:
|
# Caller example:
|
||||||
# jobs:
|
# jobs:
|
||||||
# ci:
|
# ci:
|
||||||
# uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-frontend.yaml@<full-commit-sha> # main
|
# uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-frontend.yaml@<full-commit-sha> # v1.0.4
|
||||||
# with:
|
# with:
|
||||||
# node-version: "24"
|
# node-version: "24"
|
||||||
|
|
||||||
|
|
@ -87,11 +89,11 @@ jobs:
|
||||||
run:
|
run:
|
||||||
working-directory: ${{ inputs.working-directory }}
|
working-directory: ${{ inputs.working-directory }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ inputs.node-version }}
|
node-version: ${{ inputs.node-version }}
|
||||||
cache: npm
|
cache: npm
|
||||||
|
|
|
||||||
3
.github/workflows/ci.yaml
vendored
3
.github/workflows/ci.yaml
vendored
|
|
@ -41,6 +41,7 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
merge_group:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
@ -50,7 +51,7 @@ jobs:
|
||||||
name: ci / ci
|
name: ci / ci
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: Install actionlint
|
- name: Install actionlint
|
||||||
env:
|
env:
|
||||||
|
|
|
||||||
3
.github/workflows/release-on-merge.yaml
vendored
3
.github/workflows/release-on-merge.yaml
vendored
|
|
@ -31,6 +31,7 @@ on:
|
||||||
- "!.github/workflows/ci.yaml"
|
- "!.github/workflows/ci.yaml"
|
||||||
- "!.github/workflows/labeler.yaml"
|
- "!.github/workflows/labeler.yaml"
|
||||||
- "!.github/workflows/release-on-merge.yaml"
|
- "!.github/workflows/release-on-merge.yaml"
|
||||||
|
- "!.github/workflows/auto-merge.yaml"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
|
|
@ -57,7 +58,7 @@ jobs:
|
||||||
outputs:
|
outputs:
|
||||||
version: ${{ steps.next.outputs.version }}
|
version: ${{ steps.next.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
# Tags are the input to the version calculation, so they must be
|
# Tags are the input to the version calculation, so they must be
|
||||||
# fetched; a shallow checkout without them would restart at 1.0.0.
|
# fetched; a shallow checkout without them would restart at 1.0.0.
|
||||||
|
|
|
||||||
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
|
|
@ -32,7 +32,7 @@ name: Release — Tag and GitHub Release
|
||||||
# Caller example:
|
# Caller example:
|
||||||
# jobs:
|
# jobs:
|
||||||
# release:
|
# release:
|
||||||
# uses: Sea-Haven-Industries/.github/.github/workflows/release.yaml@<full-commit-sha> # main
|
# uses: Sea-Haven-Industries/.github/.github/workflows/release.yaml@<full-commit-sha> # v1.0.4
|
||||||
# with:
|
# with:
|
||||||
# version: ${{ inputs.version }}
|
# version: ${{ inputs.version }}
|
||||||
#
|
#
|
||||||
|
|
@ -118,7 +118,7 @@ jobs:
|
||||||
released: ${{ steps.publish.outputs.released || 'false' }}
|
released: ${{ steps.publish.outputs.released || 'false' }}
|
||||||
url: ${{ steps.publish.outputs.url }}
|
url: ${{ steps.publish.outputs.url }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
# Full history + tags: the existing-tag guard reads local refs.
|
# Full history + tags: the existing-tag guard reads local refs.
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
|
||||||
220
README.md
220
README.md
|
|
@ -2,26 +2,79 @@
|
||||||
|
|
||||||
Organization-level GitHub configuration for Sea Haven Industries.
|
Organization-level GitHub configuration for Sea Haven Industries.
|
||||||
|
|
||||||
|
## Git and PR conventions
|
||||||
|
|
||||||
|
### Branch naming
|
||||||
|
|
||||||
|
`feature/`, `fix/`, `hotfix/`, `chore/`, `docs/`, `refactor/`, `release/` + kebab-case description. Branch names do not contain Jira keys.
|
||||||
|
|
||||||
|
### Commit format
|
||||||
|
|
||||||
|
`type(scope): description` — lowercase, imperative, no trailing period, header ≤ 72 chars. Types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, `revert`, `release`. Breaking change: `feat!:` + `BREAKING CHANGE:` footer.
|
||||||
|
|
||||||
|
### PR title
|
||||||
|
|
||||||
|
`type(scope): description (DEV-123)` — maximum 120 characters, including the Jira suffix. Put the Jira key at the end in parentheses. A missing key is a warning, not a failure. Active projects: **DEV** (product), **PLAT** (platform), **SEC** (security). INFRA is a closed archive.
|
||||||
|
|
||||||
|
### PR body
|
||||||
|
|
||||||
|
Exactly four headings in order: `## Summary`, `## Validation`, `## Tests`, `## Notes`. Use `None.` under Notes if empty.
|
||||||
|
|
||||||
|
### Deploy path
|
||||||
|
|
||||||
|
The two sanctioned deploy paths are merge to `main` triggering the pipeline and `workflow_dispatch` on that same pipeline. No manual workstation deploys to production.
|
||||||
|
|
||||||
|
### Merge queue
|
||||||
|
|
||||||
|
CI callers keep a `merge_group` trigger so native GitHub merge queues still run portions. Mergify YAML is not used. Do not put portion job names (`frontend / static`, `unit (1)`, …) in a ruleset.
|
||||||
|
|
||||||
|
### Required checks
|
||||||
|
|
||||||
|
Two org rulesets. A repo is on exactly one of them:
|
||||||
|
|
||||||
|
- **main branch protection** requires `ci / ci` for unconverted remaining-lane repos.
|
||||||
|
- **CI complete** requires `ci-complete` for converted HCP callers. It targets no repos until a cutover includes the repo and excludes it from the old ruleset in the same window.
|
||||||
|
|
||||||
|
The formatter GitHub App is not on the main-branch bypass list.
|
||||||
|
|
||||||
## What's in here
|
## What's in here
|
||||||
|
|
||||||
|
### Renovate preset
|
||||||
|
|
||||||
|
`default.json` is the file loaded by `local>Sea-Haven-Industries/.github`. It is intentionally empty of policy. Org Renovate rules live in `Sea-Haven-Industries/renovate-config` as `org-inherited-config.json`.
|
||||||
|
|
||||||
### Reusable Workflows
|
### Reusable Workflows
|
||||||
|
|
||||||
**`.github/workflows/ci-python-sam.yaml`** — Reusable CI workflow for Python / SAM repos. Runs `ruff check` + `ruff format --check`, optional `pytest`, and optional `sam validate --lint`. Also usable for Python CDK repos by disabling SAM validate.
|
**`.github/workflows/ci-python-sam.yaml`** — Reusable CI workflow for Python / SAM repos. Runs `ruff check` + `ruff format --check`, optional `pytest`, and optional `sam validate --lint`. Also usable for Python CDK repos by disabling SAM validate.
|
||||||
|
|
||||||
**`.github/workflows/ci-typescript-cdk.yaml`** — Reusable CI workflow for TypeScript / CDK repos. Runs `npm ci` + optional `tsc --noEmit`, optional ESLint, optional Jest, and optional `cdk synth`. Also supports Node.js SAM repos via an optional `sam validate` step.
|
**`.github/workflows/ci-typescript-cdk.yaml`** — Reusable CI workflow for TypeScript / CDK repos. Runs `npm ci` + optional `tsc --noEmit`, optional ESLint, optional Jest, and optional `cdk synth`. Also supports Node.js SAM repos via an optional `sam validate` step.
|
||||||
|
|
||||||
**`.github/workflows/ci-typescript-frontend.yaml`** — Reusable CI workflow for bundled TypeScript front-end apps (Vite / React / Vue SPAs). Runs a Sea Haven standards gate (required npm scripts present, no AI-tool footers / hook bypasses / hardcoded secrets in added lines), then `format:check`, `lint`, `build`, vitest unit tests, and an optional Playwright browser smoke. Emits the single `ci / ci` status context — keep the caller job id `ci`.
|
**`.github/workflows/ci-typescript-frontend.yaml`** — Sequential reusable CI for remaining-lane bundled TypeScript front-end apps that still emit `ci / ci`. HCP app repos should call `ci-frontend.yaml` plus a caller-owned `ci-complete` aggregator instead.
|
||||||
|
|
||||||
|
**`.github/workflows/ci-frontend.yaml`** — Parallel HCP frontend CI: `guard`, `static`, `build`, `unit` (optional shards), `browser-smoke`. The caller owns `ci-complete`. Do not put those portion names in a ruleset.
|
||||||
|
|
||||||
|
**`.github/workflows/ci-terraform.yaml`** — Terraform `fmt -check`, `init -backend=false`, and `validate`. Default version `1.16.0`.
|
||||||
|
|
||||||
|
**`.github/workflows/ci-autofix.yaml`** — Pull-request-only formatter. Mints a GitHub App token (`AUTOFMT_APP_ID`, `AUTOFMT_APP_PRIVATE_KEY`), runs the requested presets and any optional write commands, and pushes `style: apply formatter` only when the tree is dirty. Presets are `prettier` (`npm run format`), `eslint` (`npx eslint . --fix`, opt-in), `ruff` (`ruff format .` and `ruff check --fix .`), and `terraform` (`terraform fmt -recursive` in `terraform-working-directory`, default `terraform`). Output `committed` lets the caller skip portions on SHA_old. Does not `--no-verify` and does not push to `main`.
|
||||||
|
|
||||||
|
**`.github/workflows/cd-hcp-fargate.yaml`** — HCP Fargate image CD. Checkout at `ref` (empty means `github.sha`), OIDC, SSM cluster/service/family/ecr/container/api-url, docker build+push tagged `$sha` and `$environment`, patch `GIT_SHA`, RegisterTaskDefinition + UpdateService + services-stable, poll health SHA. `environment` is a `with:` input. The reusable job owns `environment:`, concurrency, OIDC, and `vars.DEPLOY_ROLE_ARN`.
|
||||||
|
|
||||||
|
**`.github/workflows/cd-hcp-spa.yaml`** — HCP SPA CD. Checkout at `ref`, Node 24, `npm ci` + `npm run build`, origin-path guard, hashed s3 sync then `index.html` last then prune, invalidate, verify. Build env comes from the GitHub Environment (`vars.VITE_*`, `secrets.SENTRY_AUTH_TOKEN`).
|
||||||
|
|
||||||
|
**`.github/workflows/cd-hcp-static.yaml`** — HCP static-site CD for unfingerprinted builds such as Eleventy. Checkout at `ref`, Node 24, `npm ci --ignore-scripts` + `npm run build`, one-day cache on assets, `no-cache` on HTML/XML/text, prune, invalidate, verify the served index hash. Reads `/<prefix>/bucket` and `/<prefix>/distribution-id`. Do not use this for a hashed SPA.
|
||||||
|
|
||||||
**`.github/workflows/cd-sam.yaml`** — Reusable CD workflow for SAM repos. Runs `sam build` + `sam deploy` with OIDC credentials and a CloudFormation execution role. Triggers via `workflow_call` from per-repo `deploy.yaml` on push to `main`.
|
**`.github/workflows/cd-sam.yaml`** — Reusable CD workflow for SAM repos. Runs `sam build` + `sam deploy` with OIDC credentials and a CloudFormation execution role. Triggers via `workflow_call` from per-repo `deploy.yaml` on push to `main`.
|
||||||
|
|
||||||
**`.github/workflows/cd-cdk.yaml`** — Reusable CD workflow for CDK repos (TypeScript and Python). Runs `cdk deploy --all` with OIDC credentials. Supports optional Python setup for Python CDK repos and QEMU emulation for cross-platform Docker builds.
|
**`.github/workflows/cd-cdk.yaml`** — Reusable CD workflow for CDK repos (TypeScript and Python). Runs `cdk deploy --all` with OIDC credentials. Supports optional Python setup for Python CDK repos and QEMU emulation for cross-platform Docker builds.
|
||||||
|
|
||||||
**`.github/workflows/ci-python-app.yaml`** — Reusable CI for non-SAM Python apps (ruff check + format, optional pytest; no SAM validate).
|
**`.github/workflows/ci-python-app.yaml`** — Reusable CI for non-SAM Python apps (ruff check + format and conventions; no pytest, no SAM validate). Pytest stays a caller-owned job.
|
||||||
|
|
||||||
**`.github/workflows/ci-dotnet.yaml`** — Reusable CI for .NET solutions (`dotnet build`, optional `dotnet test`; SDK version and solution path as inputs).
|
**`.github/workflows/ci-dotnet.yaml`** — Reusable CI for .NET solutions (`dotnet build`, optional `dotnet test`; SDK version and solution path as inputs).
|
||||||
|
|
||||||
**`.github/workflows/ci-static.yaml`** — Reusable CI for static sites (e.g. Eleventy builds for seahaven-site).
|
**`.github/workflows/ci-static.yaml`** — Reusable CI for static sites (e.g. Eleventy builds for seahaven-site).
|
||||||
|
|
||||||
|
**`.github/workflows/ci-mobile-ios.yaml`** — Reusable CI for React Native iOS apps: dependency install, typecheck, optional lint and unit tests, and an unsigned compile (nothing uploaded). Emits the aggregated `ci / ci` status context.
|
||||||
|
|
||||||
**`.github/workflows/cd-mobile-ios.yaml`** — Reusable CD for iOS apps via Fastlane to TestFlight (Node + Ruby setup inputs).
|
**`.github/workflows/cd-mobile-ios.yaml`** — Reusable CD for iOS apps via Fastlane to TestFlight (Node + Ruby setup inputs).
|
||||||
|
|
||||||
**`.github/workflows/cd-dotnet-eb.yaml`** — Reusable CD for .NET apps on AWS Elastic Beanstalk. Publishes the project, packages a bundle, uploads it, creates an application version, and updates an **existing** environment with OIDC credentials — it never creates an environment. Serialised per environment via a `concurrency` group, and the post-deploy check fails the job if EB rolls the deploy back. The caller owns branch-to-environment mapping.
|
**`.github/workflows/cd-dotnet-eb.yaml`** — Reusable CD for .NET apps on AWS Elastic Beanstalk. Publishes the project, packages a bundle, uploads it, creates an application version, and updates an **existing** environment with OIDC credentials — it never creates an environment. Serialised per environment via a `concurrency` group, and the post-deploy check fails the job if EB rolls the deploy back. The caller owns branch-to-environment mapping.
|
||||||
|
|
@ -30,19 +83,31 @@ Organization-level GitHub configuration for Sea Haven Industries.
|
||||||
|
|
||||||
**`.github/workflows/callable-dependency-review.yaml`** — Dependency review on PRs, failing on high severity. Requires Dependency Graph.
|
**`.github/workflows/callable-dependency-review.yaml`** — Dependency review on PRs, failing on high severity. Requires Dependency Graph.
|
||||||
|
|
||||||
|
**`.github/workflows/release.yaml`** — Reusable release workflow: creates an annotated git tag at a commit and publishes a GitHub Release pointing at it. The version is an input (not read from a manifest).
|
||||||
|
|
||||||
|
**`.github/workflows/release-on-merge.yaml`** — Repo automation (not callable): cuts a tag and GitHub Release for **this** repo whenever a merge to `main` changes a reusable workflow, so Dependabot has a release to advance consumer SHA pins to (see the pinning policy below).
|
||||||
|
|
||||||
|
**`.github/workflows/labeler.yaml`** — This repo's own thin caller of `callable-labeler.yaml`, so the labeler runs on `.github`'s own PRs.
|
||||||
|
|
||||||
**`.github/workflows/ci.yaml`** — Self-CI for this repo: actionlint (checksum-verified install) over all workflow files, emitting the required `ci / ci` status context. Its shellcheck integration is enabled, so `run:` bodies are shell-linted too; the two deploy steps that rely on intentional word-splitting (`sam deploy … $PARAMS`, `cdk deploy $STACKS`) carry a per-line, commented `# shellcheck disable=SC2086` rather than being quoted or globally exempted.
|
**`.github/workflows/ci.yaml`** — Self-CI for this repo: actionlint (checksum-verified install) over all workflow files, emitting the required `ci / ci` status context. Its shellcheck integration is enabled, so `run:` bodies are shell-linted too; the two deploy steps that rely on intentional word-splitting (`sam deploy … $PARAMS`, `cdk deploy $STACKS`) carry a per-line, commented `# shellcheck disable=SC2086` rather than being quoted or globally exempted.
|
||||||
|
|
||||||
### Workflow templates (`workflow-templates/`)
|
### Workflow templates (`workflow-templates/`)
|
||||||
|
|
||||||
Starter workflows offered on the org's **Actions → New workflow** page: `ci-python`, `ci-node`, `cdk-deploy`, `sam-deploy`, `dotnet-eb-deploy`, `dependency-review`, `labeler`, `triage`. Each is a thin caller of the corresponding reusable workflow above (`triage` is standalone). Every template has a paired `properties.json` (name, description, icon, `filePatterns` for auto-suggestion). Replace any `REPLACE-ME` placeholders before enabling.
|
Starter workflows offered on the org's **Actions → New workflow** page: `cdk-deploy`, `ci-dotnet`, `ci-hcp`, `ci-mobile-ios`, `ci-node`, `ci-python`, `ci-python-app`, `ci-static`, `ci-terraform`, `ci-typescript-frontend`, `dependency-review`, `dotnet-eb-deploy`, `hcp-fargate-deploy`, `hcp-spa-deploy`, `labeler`, `mobile-ios-deploy`, `release`, `sam-deploy`, `triage`. Each is a thin caller of the corresponding reusable workflow above (`triage` is standalone; `ci-hcp` is the converted-repo caller with autofix, frontend, terraform, and `ci-complete`). Every template has a paired `properties.json` (name, description, icon, `filePatterns` for auto-suggestion). Replace any `REPLACE-ME` placeholders before enabling. Templates are not scanned by Dependabot, so refresh their pinned SHAs opportunistically when editing one.
|
||||||
|
|
||||||
### Action pinning policy
|
### Ref pinning policy
|
||||||
|
|
||||||
Third-party action refs across the org follow a tiered policy:
|
All workflow refs across the org are pinned to full commit SHAs:
|
||||||
|
|
||||||
- **High-trust / high-blast-radius third-party actions are SHA-pinned** with a trailing version comment (e.g. `actions/labeler` in `callable-labeler.yaml`), and binary installs are checksum-verified (actionlint in `ci.yaml`). Dependabot keeps the SHA current via its trailing-comment mechanism.
|
- **Org reusable workflows** are referenced at a **full commit SHA** of this repo with a trailing comment naming the ref or release the pin tracks:
|
||||||
- **Common first-party actions** (`actions/checkout`, `actions/dependency-review-action`, `actions/github-script`) are pinned to a **major tag** (`@v7`, `@v5`, …) and kept current by Dependabot version updates gated by CI.
|
|
||||||
- **Org reusable workflows** are referenced at **`@main`** (`uses: Sea-Haven-Industries/.github/.github/workflows/…@main`). This is deliberate: caller and callable share one trust domain, and pinning callers to a SHA would freeze every consumer against central fixes. Templates in `workflow-templates/` follow the same `@main` convention.
|
```yaml
|
||||||
|
uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-sam.yaml@<full-commit-sha> # v1.0.3
|
||||||
|
```
|
||||||
|
|
||||||
|
Branch refs are mutable: a bad commit on this repo would flow instantly into every consumer's CI and deploy path, while a SHA pin turns the same change into a reviewable Dependabot PR. Two prerequisites keep pins advancing instead of freezing: every consumer repo's `dependabot.yml` must include the `github-actions` ecosystem (weekly), and Dependabot must be granted access to this repo at the org level (Org Settings → Advanced Security → Global settings → "Grant Dependabot access to repositories"); without the grant, update jobs fail with `git_dependencies_not_reachable` and pins freeze silently. `release-on-merge.yaml` tags this repo on every reusable-workflow change so Dependabot has releases to diff against. When adding a caller by hand, pin to the latest release commit (`gh api /repos/Sea-Haven-Industries/.github/commits/vX.Y.Z --jq .sha`), annotate it with `# vX.Y.Z`, and let Dependabot advance it from there.
|
||||||
|
- **Third-party and first-party actions** (`actions/checkout`, `actions/setup-python`, `actions/labeler`, …) — a subset are already SHA-pinned (e.g. `actions/labeler`, `aws-actions/*`, `docker/setup-qemu-action`, `ruby/setup-ruby`); the remainder (`actions/checkout`, `actions/setup-node`, `actions/setup-python`, `actions/setup-dotnet`, `actions/dependency-review-action`) currently use floating major-version tags. Full SHA pinning for this group is deferred (PLAT backlog); Dependabot will keep SHA and comment current once pins are set.
|
||||||
|
- **Binary installs are checksum-verified** (actionlint in `ci.yaml`).
|
||||||
|
|
||||||
### AWS deploy roles & IAM (`oidc-deploy-roles.yaml`)
|
### AWS deploy roles & IAM (`oidc-deploy-roles.yaml`)
|
||||||
|
|
||||||
|
|
@ -96,7 +161,7 @@ A function's effective permissions are the **intersection** of its own role poli
|
||||||
2. Redeploy the SAM stacks so their roles pick it up (while the exec role still permits it).
|
2. Redeploy the SAM stacks so their roles pick it up (while the exec role still permits it).
|
||||||
3. *Then* tighten the exec role.
|
3. *Then* tighten the exec role.
|
||||||
|
|
||||||
Wrong order breaks every SAM deploy. (History: INFRA-103 established the boundary, INFRA-97 scoped the role.) CDK repos are unaffected — they deploy via `cdk-hnb659fds-*` roles, not this execution role.
|
Wrong order breaks every SAM deploy. CDK repos are unaffected — they deploy via `cdk-hnb659fds-*` roles, not this execution role.
|
||||||
|
|
||||||
This ordering rule is about changing the **boundary** or the conditions that gate it. It does not apply to changes that only add permissions to the exec role.
|
This ordering rule is about changing the **boundary** or the conditions that gate it. It does not apply to changes that only add permissions to the exec role.
|
||||||
|
|
||||||
|
|
@ -107,7 +172,7 @@ This ordering rule is about changing the **boundary** or the conditions that gat
|
||||||
- **Removing `PermissionsBoundary` from an existing role fails by design.** CloudFormation issues `DeleteRolePermissionsBoundary` for that edit, gets `AccessDenied`, and the stack update rolls back. Removing the boundary from a SAM function is a security regression, so failing loudly is intended.
|
- **Removing `PermissionsBoundary` from an existing role fails by design.** CloudFormation issues `DeleteRolePermissionsBoundary` for that edit, gets `AccessDenied`, and the stack update rolls back. Removing the boundary from a SAM function is a security regression, so failing loudly is intended.
|
||||||
- **Rollback of an update that *adds* a boundary to an existing role would also fail**, landing the stack in `UPDATE_ROLLBACK_FAILED`. This is currently unreachable — all 26 IAM roles across the five SAM stacks already carry the boundary (verified 2026-07-27), so no update can add one. It becomes reachable again only if a role is created without the boundary and given one later.
|
- **Rollback of an update that *adds* a boundary to an existing role would also fail**, landing the stack in `UPDATE_ROLLBACK_FAILED`. This is currently unreachable — all 26 IAM roles across the five SAM stacks already carry the boundary (verified 2026-07-27), so no update can add one. It becomes reachable again only if a role is created without the boundary and given one later.
|
||||||
|
|
||||||
Recovery in either case is an administrator action, not a pipeline retry: clear the wedged stack with `aws cloudformation continue-update-rollback --stack-name <stack> --resources-to-skip <RoleLogicalId>`, or replace the role by renaming its logical id. Note `cd-sam`'s pre-flight hard-fails on `*ROLLBACK_COMPLETE`, so that repo's deploys stay blocked until it is cleared.
|
Recovery from `UPDATE_ROLLBACK_FAILED` is an administrator action, not a pipeline retry: clear the wedged stack with `aws cloudformation continue-update-rollback --stack-name <stack> --resources-to-skip <RoleLogicalId>`, or replace the role by renaming its logical id. A completed update rollback lands in `UPDATE_ROLLBACK_COMPLETE`, which is stable and can accept a corrective update; `cd-sam` blocks only first-create `ROLLBACK_COMPLETE` and failed or in-progress states.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
|
|
@ -118,8 +183,10 @@ Managed under **Organization Settings > Secrets and variables > Actions**. Each
|
||||||
| Secret | Value | Consumed by |
|
| Secret | Value | Consumed by |
|
||||||
|--------|-------|-------------|
|
|--------|-------|-------------|
|
||||||
| `ANTHROPIC_API_KEY` | Anthropic API key | `reviewer-eval.yml` in `open-swe` |
|
| `ANTHROPIC_API_KEY` | Anthropic API key | `reviewer-eval.yml` in `open-swe` |
|
||||||
|
| `AUTOFMT_APP_ID` | Formatter GitHub App id | `ci-autofix.yaml` |
|
||||||
|
| `AUTOFMT_APP_PRIVATE_KEY` | Formatter GitHub App private key | `ci-autofix.yaml` |
|
||||||
|
|
||||||
The CI and CD workflows below need no org secret — CD authenticates to AWS via OIDC using the per-repo `AWS_DEPLOY_ROLE_ARN` secret (see §3).
|
The remaining-lane CI and CD workflows below need no org secret — CD authenticates to AWS via OIDC using the per-repo `AWS_DEPLOY_ROLE_ARN` secret (see §3). HCP CD uses `vars.DEPLOY_ROLE_ARN` on the GitHub Environment after OIDC. Adam installs the formatter App (contents: write, metadata: read; not a main-branch ruleset bypass) and grants the two autofmt secrets before the first converted repo runs autofix.
|
||||||
|
|
||||||
### 2. Add CI to a repo
|
### 2. Add CI to a repo
|
||||||
|
|
||||||
|
|
@ -135,7 +202,7 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
ci:
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-sam.yaml@main
|
uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-sam.yaml@81cf168170f356d1423d7736f7ce93fd6611ad53 # v1.0.4
|
||||||
```
|
```
|
||||||
|
|
||||||
**TypeScript CDK repo** (e.g., seahaven-door-unlock-api, seahaven-slack-bot):
|
**TypeScript CDK repo** (e.g., seahaven-door-unlock-api, seahaven-slack-bot):
|
||||||
|
|
@ -148,7 +215,7 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
ci:
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@main
|
uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@81cf168170f356d1423d7736f7ce93fd6611ad53 # v1.0.4
|
||||||
```
|
```
|
||||||
|
|
||||||
**Node.js SAM repo** (e.g., payments-dashboard):
|
**Node.js SAM repo** (e.g., payments-dashboard):
|
||||||
|
|
@ -161,7 +228,7 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
ci:
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@main
|
uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@81cf168170f356d1423d7736f7ce93fd6611ad53 # v1.0.4
|
||||||
with:
|
with:
|
||||||
run-typecheck: false
|
run-typecheck: false
|
||||||
run-cdk-synth: false
|
run-cdk-synth: false
|
||||||
|
|
@ -178,19 +245,126 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
python:
|
python:
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-sam.yaml@main
|
uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-sam.yaml@81cf168170f356d1423d7736f7ce93fd6611ad53 # v1.0.4
|
||||||
with:
|
with:
|
||||||
source-dirs: "src"
|
source-dirs: "src"
|
||||||
run-sam-validate: false
|
run-sam-validate: false
|
||||||
typescript:
|
typescript:
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@main
|
uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@81cf168170f356d1423d7736f7ce93fd6611ad53 # v1.0.4
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**HCP app repo** (converted callers; required check is `ci-complete`):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: CI
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main, hotfix/**, release/**]
|
||||||
|
merge_group:
|
||||||
|
push:
|
||||||
|
branches: [hotfix/**, release/**]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
autofix:
|
||||||
|
if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork
|
||||||
|
uses: Sea-Haven-Industries/.github/.github/workflows/ci-autofix.yaml@<full-commit-sha> # vX.Y.Z
|
||||||
|
permissions: { contents: write }
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
presets: prettier,terraform
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
needs: autofix
|
||||||
|
if: always() && !cancelled() && (needs.autofix.result == 'skipped' || needs.autofix.outputs.committed != 'true')
|
||||||
|
uses: Sea-Haven-Industries/.github/.github/workflows/ci-frontend.yaml@<full-commit-sha> # vX.Y.Z
|
||||||
|
with:
|
||||||
|
node-version: "24"
|
||||||
|
unit-shards: 4
|
||||||
|
run-e2e: true
|
||||||
|
|
||||||
|
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@<full-commit-sha> # vX.Y.Z
|
||||||
|
with:
|
||||||
|
terraform-version: "1.16.0"
|
||||||
|
|
||||||
|
ci-complete:
|
||||||
|
name: ci-complete
|
||||||
|
needs: [autofix, frontend, 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:
|
||||||
|
FRONTEND: ${{ needs.frontend.result }}
|
||||||
|
TERRAFORM: ${{ needs.terraform.result }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
test "${FRONTEND}" = success
|
||||||
|
test "${TERRAFORM}" = success
|
||||||
|
```
|
||||||
|
|
||||||
|
Python HCP callers pass `presets: ruff,terraform`. The `eslint` preset is opt-in and runs `npx eslint . --fix`. Do not pass `npm run lint -- --fix`: several apps chain Redocly into `lint`. Enable `eslint` only when that repo's CI lint step is ESLint itself and Prettier owns formatting. Optional `format-command`, `lint-fix-command`, and `extra-command` still run after the presets. Flip org ruleset membership in the same window as this merge: include on `CI complete`, exclude from `main branch protection`. Never require both `ci / ci` and `ci-complete`. Do not edit native GitHub merge-queue rulesets.
|
||||||
|
|
||||||
### 3. Add CD to a repo
|
### 3. Add CD to a repo
|
||||||
|
|
||||||
Create `.github/workflows/deploy.yaml` in the target repo. Requires `AWS_DEPLOY_ROLE_ARN` repo secret.
|
**HCP Fargate** (one caller job per GitHub Environment; `environment` is a `with:` input):
|
||||||
|
|
||||||
**SAM repo** (e.g., afterhours-shift-manager):
|
```yaml
|
||||||
|
name: Deploy API
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths-ignore: [terraform/**, docs/**, "*.md"]
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
environment: { type: choice, options: [dev, prod] }
|
||||||
|
ref: { type: string, default: "" }
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
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@<full-commit-sha> # vX.Y.Z
|
||||||
|
permissions: { contents: read, id-token: write }
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
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"}'
|
||||||
|
|
||||||
|
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@<full-commit-sha> # vX.Y.Z
|
||||||
|
permissions: { contents: read, id-token: write }
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
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"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
SPA callers use `cd-hcp-spa.yaml` the same way. Pass `required-vite-vars` for Environment `VITE_*` keys that must be set before `npm run build`. Add `deploy-staging` only where that Environment exists. `DEPLOY_ROLE_ARN` is a GitHub Environment variable, not a repo secret. SHA-pinned org reusables change `job_workflow_ref` to `Sea-Haven-Industries/.github/.github/workflows/cd-hcp-fargate.yaml@<sha>` (and spa). Keep `workflow_ref` on the thin caller at `refs/heads/main` and `refs/tags/v*`. `sub` stays `repo:.../<app>:environment:<env>`. Adding a reusable is a cross-family IAM change.
|
||||||
|
|
||||||
|
Create `.github/workflows/deploy.yaml` in remaining SAM/CDK repos. Requires `AWS_DEPLOY_ROLE_ARN` repo secret.
|
||||||
|
|
||||||
|
**SAM repo** (e.g., remaining SAM stacks):
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
name: Deploy
|
name: Deploy
|
||||||
|
|
@ -200,7 +374,7 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/cd-sam.yaml@main
|
uses: Sea-Haven-Industries/.github/.github/workflows/cd-sam.yaml@81cf168170f356d1423d7736f7ce93fd6611ad53 # v1.0.4
|
||||||
with:
|
with:
|
||||||
stack-name: afterhours-shift-manager
|
stack-name: afterhours-shift-manager
|
||||||
cfn-role-arn: arn:aws:iam::328440206208:role/github-cfn-execution-role
|
cfn-role-arn: arn:aws:iam::328440206208:role/github-cfn-execution-role
|
||||||
|
|
@ -220,7 +394,7 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@main
|
uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@81cf168170f356d1423d7736f7ce93fd6611ad53 # v1.0.4
|
||||||
secrets:
|
secrets:
|
||||||
deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
|
deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
|
||||||
```
|
```
|
||||||
|
|
@ -235,7 +409,7 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@main
|
uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@81cf168170f356d1423d7736f7ce93fd6611ad53 # v1.0.4
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
cdk-dir: cdk
|
cdk-dir: cdk
|
||||||
|
|
@ -253,7 +427,7 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@main
|
uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@81cf168170f356d1423d7736f7ce93fd6611ad53 # v1.0.4
|
||||||
with:
|
with:
|
||||||
enable-qemu: true
|
enable-qemu: true
|
||||||
secrets:
|
secrets:
|
||||||
|
|
@ -270,7 +444,7 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/cd-dotnet-eb.yaml@main
|
uses: Sea-Haven-Industries/.github/.github/workflows/cd-dotnet-eb.yaml@81cf168170f356d1423d7736f7ce93fd6611ad53 # v1.0.4
|
||||||
with:
|
with:
|
||||||
project: Api.SeaHavenIndustries/Api.SeaHavenIndustries.csproj
|
project: Api.SeaHavenIndustries/Api.SeaHavenIndustries.csproj
|
||||||
eb-application: shoc-backend
|
eb-application: shoc-backend
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ Sea-Haven-Industries repositories are private and intended for internal Sea Have
|
||||||
|
|
||||||
## Where to go
|
## Where to go
|
||||||
|
|
||||||
- **Bugs, feature requests, infrastructure work** — file a ticket in Jira (**INFRA** project) or open an issue on the relevant repository.
|
- **Bugs and feature requests** — file a ticket in Jira (**DEV**, **PLAT**, or **SEC** depending on scope). GitHub Issues are active only on shoc-backend, shoc-frontend-new, and open-swe (contractor/fork intake).
|
||||||
|
- **Infrastructure and platform work** — use the **PLAT** project. Security issues go in **SEC**.
|
||||||
- **Operational or urgent issues** — contact Adam Moussa (adam@seahavenind.com).
|
- **Operational or urgent issues** — contact Adam Moussa (adam@seahavenind.com).
|
||||||
- **Engineering conventions and standards** — see the [engineering-handbook](https://github.com/Sea-Haven-Industries/engineering-handbook).
|
- **Engineering conventions and standards** — see the [engineering-handbook](https://github.com/Sea-Haven-Industries/engineering-handbook).
|
||||||
- **Security vulnerabilities** — follow [SECURITY.md](SECURITY.md) (do not open a public issue).
|
- **Security vulnerabilities** — follow [SECURITY.md](SECURITY.md) (do not open a public issue).
|
||||||
|
|
|
||||||
3
default.json
Normal file
3
default.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,10 @@ Parameters:
|
||||||
GitHubOrg:
|
GitHubOrg:
|
||||||
Type: String
|
Type: String
|
||||||
Default: Sea-Haven-Industries
|
Default: Sea-Haven-Industries
|
||||||
|
# No glob metacharacters: this value is interpolated into StringLike trust
|
||||||
|
# conditions, where a '*' override would silently open every role's trust
|
||||||
|
# to any GitHub org with a same-named repo.
|
||||||
|
AllowedPattern: "^[A-Za-z0-9-]+$"
|
||||||
CreateOIDCProvider:
|
CreateOIDCProvider:
|
||||||
Type: String
|
Type: String
|
||||||
Default: "false"
|
Default: "false"
|
||||||
|
|
@ -52,6 +56,7 @@ Resources:
|
||||||
# - DynamoDB CRUD (afterhours-shifts table)
|
# - DynamoDB CRUD (afterhours-shifts table)
|
||||||
# - secretsmanager:GetSecretValue (afterhours-shift-manager/*)
|
# - secretsmanager:GetSecretValue (afterhours-shift-manager/*)
|
||||||
# - ses:SendEmail (SES identity)
|
# - ses:SendEmail (SES identity)
|
||||||
|
# - sqs:SendMessage (paychex-checkcomponents in seahaven-prod, WeeklyPost)
|
||||||
# - CloudWatch Logs (all functions)
|
# - CloudWatch Logs (all functions)
|
||||||
#
|
#
|
||||||
# payments-dashboard
|
# payments-dashboard
|
||||||
|
|
@ -205,6 +210,25 @@ Resources:
|
||||||
Resource:
|
Resource:
|
||||||
- !Sub "arn:aws:sqs:us-east-1:${AWS::AccountId}:*"
|
- !Sub "arn:aws:sqs:us-east-1:${AWS::AccountId}:*"
|
||||||
|
|
||||||
|
# ── SQS cross-account send (afterhours WeeklyPost -> paychex) ─────
|
||||||
|
# afterhours-shift-manager WeeklyPostFunction enqueues the weekly
|
||||||
|
# after-hours pay payload onto paychex-integrations' checkcomponents
|
||||||
|
# queue in seahaven-prod (PLAT-135). Send only. This is a ceiling,
|
||||||
|
# not a grant: the function's inline policy already allows this ARN
|
||||||
|
# and the prod queue policy admits only WeeklyPostFunctionRole-*, so
|
||||||
|
# the boundary was the one missing piece. The PrincipalArn condition
|
||||||
|
# keeps the ceiling closed for every other role on this boundary even
|
||||||
|
# if the queue policy is later loosened.
|
||||||
|
- Sid: SQSPaychexCheckcomponentsSend
|
||||||
|
Effect: Allow
|
||||||
|
Action:
|
||||||
|
- sqs:SendMessage
|
||||||
|
Resource:
|
||||||
|
- arn:aws:sqs:us-east-1:011934824531:paychex-checkcomponents
|
||||||
|
Condition:
|
||||||
|
ArnLike:
|
||||||
|
aws:PrincipalArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/afterhours-shift-manager-WeeklyPostFunctionRole-*"
|
||||||
|
|
||||||
# ── Lambda invocation (payments, meal-order inter-function calls) ──
|
# ── Lambda invocation (payments, meal-order inter-function calls) ──
|
||||||
- Sid: LambdaInvoke
|
- Sid: LambdaInvoke
|
||||||
Effect: Allow
|
Effect: Allow
|
||||||
|
|
@ -334,30 +358,17 @@ Resources:
|
||||||
StringEquals:
|
StringEquals:
|
||||||
"iam:PermissionsBoundary": !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary"
|
"iam:PermissionsBoundary": !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary"
|
||||||
|
|
||||||
# Boundary management — SET the boundary only. DELETE is NOT
|
# Boundary management is SET-only. Granting delete would let this
|
||||||
# granted: for a delete, the iam:PermissionsBoundary condition key
|
# role create a boundary-gated role, strip the boundary, then pass an
|
||||||
# reflects the boundary CURRENTLY attached to the target role, so
|
# unconstrained role to Lambda. SAM creation and teardown need only
|
||||||
# a StringEquals condition on the boundary ARN MATCHES exactly the
|
# PutRolePermissionsBoundary and DeleteRole.
|
||||||
# roles the gate protects. Granting delete under that condition
|
|
||||||
# lets this role create a boundary-gated role with an inline *:*
|
|
||||||
# policy, strip the boundary, and pass the now-unbounded role to
|
|
||||||
# Lambda — defeating the primary escalation control. Verified live
|
|
||||||
# against the mgmt copy 2026-07-27 (simulate-principal-policy:
|
|
||||||
# iam:DeleteRolePermissionsBoundary = allowed).
|
|
||||||
#
|
#
|
||||||
# OPERATIONAL CONSEQUENCE — read before debugging a stuck stack.
|
# Removing a boundary therefore fails by design. A failed rollback
|
||||||
# SAM does not need the delete for the common paths: it SETS the
|
# reaches UPDATE_ROLLBACK_FAILED and needs admin recovery by skipping
|
||||||
# boundary on roles it creates, and stack teardown calls DeleteRole.
|
# or replacing the role. A successful rollback reaches the stable
|
||||||
# But there IS one path that now fails by design: updating an
|
# UPDATE_ROLLBACK_COMPLETE state and can accept a corrective update.
|
||||||
# existing AWS::IAM::Role to REMOVE its PermissionsBoundary property
|
# Removing a SAM function boundary is a security regression, so
|
||||||
# makes CloudFormation call DeleteRolePermissionsBoundary, which is
|
# failing loudly is intentional.
|
||||||
# denied. The stack update fails and rolls back, and because cd-sam's
|
|
||||||
# pre-flight hard-fails on *ROLLBACK_COMPLETE, that repo's deploys
|
|
||||||
# stay blocked until it is cleared. Recovery is an out-of-band admin
|
|
||||||
# action (remove the boundary directly, or replace the role by
|
|
||||||
# renaming its logical id) — not a pipeline retry. Removing the
|
|
||||||
# boundary from a SAM function is a security regression anyway, so
|
|
||||||
# failing loudly here is the intent.
|
|
||||||
- Sid: IAMPutPermissionsBoundary
|
- Sid: IAMPutPermissionsBoundary
|
||||||
Effect: Allow
|
Effect: Allow
|
||||||
Action:
|
Action:
|
||||||
|
|
@ -475,6 +486,19 @@ Resources:
|
||||||
StringEquals:
|
StringEquals:
|
||||||
"iam:PassedToService": "lambda.amazonaws.com"
|
"iam:PassedToService": "lambda.amazonaws.com"
|
||||||
|
|
||||||
|
# API Gateway assumes SAM authorizer invocation roles. Keep this
|
||||||
|
# separate from Lambda PassRole so each target service and role
|
||||||
|
# pattern remains independently constrained.
|
||||||
|
- Sid: IAMPassAuthorizerRole
|
||||||
|
Effect: Allow
|
||||||
|
Action:
|
||||||
|
- iam:PassRole
|
||||||
|
Resource:
|
||||||
|
- !Sub "arn:aws:iam::${AWS::AccountId}:role/cfn-managed/*AuthorizerInvokeRole-*"
|
||||||
|
Condition:
|
||||||
|
StringEquals:
|
||||||
|
"iam:PassedToService": "apigateway.amazonaws.com"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Shared CloudFormation execution role (SAM stacks) — INFRA-97 scoped
|
# Shared CloudFormation execution role (SAM stacks) — INFRA-97 scoped
|
||||||
#
|
#
|
||||||
|
|
@ -1027,146 +1051,6 @@ Resources:
|
||||||
Resource:
|
Resource:
|
||||||
- !GetAtt SamCfnExecutionRole.Arn
|
- !GetAtt SamCfnExecutionRole.Arn
|
||||||
|
|
||||||
FrontIntegrationsDeployRole:
|
|
||||||
Type: AWS::IAM::Role
|
|
||||||
Properties:
|
|
||||||
RoleName: githubdeploy-front-integrations
|
|
||||||
AssumeRolePolicyDocument:
|
|
||||||
Version: "2012-10-17"
|
|
||||||
Statement:
|
|
||||||
- Effect: Allow
|
|
||||||
Principal:
|
|
||||||
Federated: !Sub arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com
|
|
||||||
Action: sts:AssumeRoleWithWebIdentity
|
|
||||||
Condition:
|
|
||||||
StringEquals:
|
|
||||||
token.actions.githubusercontent.com:aud: sts.amazonaws.com
|
|
||||||
StringLike:
|
|
||||||
token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrg}/front-integrations:ref:refs/heads/main
|
|
||||||
Policies:
|
|
||||||
- PolicyName: sam-deploy
|
|
||||||
PolicyDocument:
|
|
||||||
Version: "2012-10-17"
|
|
||||||
Statement:
|
|
||||||
- Effect: Allow
|
|
||||||
Action:
|
|
||||||
- cloudformation:CreateChangeSet
|
|
||||||
- cloudformation:DeleteChangeSet
|
|
||||||
- cloudformation:DescribeChangeSet
|
|
||||||
- cloudformation:DescribeStackEvents
|
|
||||||
- cloudformation:DescribeStacks
|
|
||||||
- cloudformation:ExecuteChangeSet
|
|
||||||
- cloudformation:GetTemplate
|
|
||||||
- cloudformation:ListStackResources
|
|
||||||
- cloudformation:UpdateStack
|
|
||||||
- cloudformation:CreateStack
|
|
||||||
- cloudformation:TagResource
|
|
||||||
Resource:
|
|
||||||
- !Sub arn:aws:cloudformation:us-east-1:${AWS::AccountId}:stack/front-integrations/*
|
|
||||||
- Effect: Allow
|
|
||||||
Action:
|
|
||||||
- cloudformation:GetTemplateSummary
|
|
||||||
Resource: "*"
|
|
||||||
- Effect: Allow
|
|
||||||
Action:
|
|
||||||
- cloudformation:DescribeStacks
|
|
||||||
- cloudformation:CreateChangeSet
|
|
||||||
- cloudformation:DescribeChangeSet
|
|
||||||
- cloudformation:ExecuteChangeSet
|
|
||||||
- cloudformation:CreateStack
|
|
||||||
Resource:
|
|
||||||
- !Sub arn:aws:cloudformation:us-east-1:${AWS::AccountId}:stack/aws-sam-cli-managed-default/*
|
|
||||||
- Effect: Allow
|
|
||||||
Action:
|
|
||||||
- s3:PutObject
|
|
||||||
- s3:GetObject
|
|
||||||
- s3:ListBucket
|
|
||||||
- s3:GetBucketLocation
|
|
||||||
- s3:CreateBucket
|
|
||||||
- s3:PutBucketPolicy
|
|
||||||
- s3:GetBucketPolicy
|
|
||||||
- s3:PutLifecycleConfiguration
|
|
||||||
- s3:PutBucketVersioning
|
|
||||||
- s3:DeleteObject
|
|
||||||
Resource:
|
|
||||||
- arn:aws:s3:::aws-sam-cli-managed-default-samclisourcebucket-*
|
|
||||||
- arn:aws:s3:::aws-sam-cli-managed-default-samclisourcebucket-*/*
|
|
||||||
- Effect: Allow
|
|
||||||
Action:
|
|
||||||
- iam:PassRole
|
|
||||||
Resource:
|
|
||||||
- !GetAtt SamCfnExecutionRole.Arn
|
|
||||||
|
|
||||||
AfiBackupMonitorDeployRole:
|
|
||||||
Type: AWS::IAM::Role
|
|
||||||
Properties:
|
|
||||||
RoleName: githubdeploy-afi-backup-monitor
|
|
||||||
AssumeRolePolicyDocument:
|
|
||||||
Version: "2012-10-17"
|
|
||||||
Statement:
|
|
||||||
- Effect: Allow
|
|
||||||
Principal:
|
|
||||||
Federated: !Sub arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com
|
|
||||||
Action: sts:AssumeRoleWithWebIdentity
|
|
||||||
Condition:
|
|
||||||
StringEquals:
|
|
||||||
token.actions.githubusercontent.com:aud: sts.amazonaws.com
|
|
||||||
StringLike:
|
|
||||||
token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrg}/afi-backup-monitor:ref:refs/heads/main
|
|
||||||
Policies:
|
|
||||||
- PolicyName: sam-deploy
|
|
||||||
PolicyDocument:
|
|
||||||
Version: "2012-10-17"
|
|
||||||
Statement:
|
|
||||||
- Effect: Allow
|
|
||||||
Action:
|
|
||||||
- cloudformation:CreateChangeSet
|
|
||||||
- cloudformation:DeleteChangeSet
|
|
||||||
- cloudformation:DescribeChangeSet
|
|
||||||
- cloudformation:DescribeStackEvents
|
|
||||||
- cloudformation:DescribeStacks
|
|
||||||
- cloudformation:ExecuteChangeSet
|
|
||||||
- cloudformation:GetTemplate
|
|
||||||
- cloudformation:ListStackResources
|
|
||||||
- cloudformation:UpdateStack
|
|
||||||
- cloudformation:CreateStack
|
|
||||||
- cloudformation:TagResource
|
|
||||||
Resource:
|
|
||||||
- !Sub arn:aws:cloudformation:us-east-1:${AWS::AccountId}:stack/afi-backup-monitor/*
|
|
||||||
- Effect: Allow
|
|
||||||
Action:
|
|
||||||
- cloudformation:GetTemplateSummary
|
|
||||||
Resource: "*"
|
|
||||||
- Effect: Allow
|
|
||||||
Action:
|
|
||||||
- cloudformation:DescribeStacks
|
|
||||||
- cloudformation:CreateChangeSet
|
|
||||||
- cloudformation:DescribeChangeSet
|
|
||||||
- cloudformation:ExecuteChangeSet
|
|
||||||
- cloudformation:CreateStack
|
|
||||||
Resource:
|
|
||||||
- !Sub arn:aws:cloudformation:us-east-1:${AWS::AccountId}:stack/aws-sam-cli-managed-default/*
|
|
||||||
- Effect: Allow
|
|
||||||
Action:
|
|
||||||
- s3:PutObject
|
|
||||||
- s3:GetObject
|
|
||||||
- s3:ListBucket
|
|
||||||
- s3:GetBucketLocation
|
|
||||||
- s3:CreateBucket
|
|
||||||
- s3:PutBucketPolicy
|
|
||||||
- s3:GetBucketPolicy
|
|
||||||
- s3:PutLifecycleConfiguration
|
|
||||||
- s3:PutBucketVersioning
|
|
||||||
- s3:DeleteObject
|
|
||||||
Resource:
|
|
||||||
- arn:aws:s3:::aws-sam-cli-managed-default-samclisourcebucket-*
|
|
||||||
- arn:aws:s3:::aws-sam-cli-managed-default-samclisourcebucket-*/*
|
|
||||||
- Effect: Allow
|
|
||||||
Action:
|
|
||||||
- iam:PassRole
|
|
||||||
Resource:
|
|
||||||
- !GetAtt SamCfnExecutionRole.Arn
|
|
||||||
|
|
||||||
PaymentsDashboardDeployRole:
|
PaymentsDashboardDeployRole:
|
||||||
Type: AWS::IAM::Role
|
Type: AWS::IAM::Role
|
||||||
Properties:
|
Properties:
|
||||||
|
|
@ -1238,117 +1122,12 @@ Resources:
|
||||||
- !GetAtt SamCfnExecutionRole.Arn
|
- !GetAtt SamCfnExecutionRole.Arn
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# CDK deploy roles (4 repos)
|
# CDK deploy role for seahaven-org-baseline.
|
||||||
|
# Soaked githubdeploy roles for front-integrations, afi-backup-monitor,
|
||||||
|
# exec-aide, seahaven-door-unlock-api, and apm-wo-analysis are removed
|
||||||
|
# here (PLAT-232). Deploying this stack deletes those roles.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
ExecAideDeployRole:
|
|
||||||
Type: AWS::IAM::Role
|
|
||||||
Properties:
|
|
||||||
RoleName: githubdeploy-exec-aide
|
|
||||||
AssumeRolePolicyDocument:
|
|
||||||
Version: "2012-10-17"
|
|
||||||
Statement:
|
|
||||||
- Effect: Allow
|
|
||||||
Principal:
|
|
||||||
Federated: !Sub arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com
|
|
||||||
Action: sts:AssumeRoleWithWebIdentity
|
|
||||||
Condition:
|
|
||||||
StringEquals:
|
|
||||||
token.actions.githubusercontent.com:aud: sts.amazonaws.com
|
|
||||||
StringLike:
|
|
||||||
token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrg}/exec-aide:ref:refs/heads/main
|
|
||||||
Policies:
|
|
||||||
- PolicyName: cdk-deploy
|
|
||||||
PolicyDocument:
|
|
||||||
Version: "2012-10-17"
|
|
||||||
Statement:
|
|
||||||
- Effect: Allow
|
|
||||||
Action:
|
|
||||||
- sts:AssumeRole
|
|
||||||
Resource:
|
|
||||||
- !Sub arn:aws:iam::${AWS::AccountId}:role/cdk-hnb659fds-*
|
|
||||||
|
|
||||||
SeahavenDoorUnlockApiDeployRole:
|
|
||||||
Type: AWS::IAM::Role
|
|
||||||
Properties:
|
|
||||||
RoleName: githubdeploy-seahaven-door-unlock-api
|
|
||||||
AssumeRolePolicyDocument:
|
|
||||||
Version: "2012-10-17"
|
|
||||||
Statement:
|
|
||||||
- Effect: Allow
|
|
||||||
Principal:
|
|
||||||
Federated: !Sub arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com
|
|
||||||
Action: sts:AssumeRoleWithWebIdentity
|
|
||||||
Condition:
|
|
||||||
StringEquals:
|
|
||||||
token.actions.githubusercontent.com:aud: sts.amazonaws.com
|
|
||||||
StringLike:
|
|
||||||
token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrg}/seahaven-door-unlock-api:ref:refs/heads/main
|
|
||||||
Policies:
|
|
||||||
- PolicyName: cdk-deploy
|
|
||||||
PolicyDocument:
|
|
||||||
Version: "2012-10-17"
|
|
||||||
Statement:
|
|
||||||
- Effect: Allow
|
|
||||||
Action:
|
|
||||||
- sts:AssumeRole
|
|
||||||
Resource:
|
|
||||||
- !Sub arn:aws:iam::${AWS::AccountId}:role/cdk-hnb659fds-*
|
|
||||||
|
|
||||||
ProcurementIngestDeployRole:
|
|
||||||
Type: AWS::IAM::Role
|
|
||||||
Properties:
|
|
||||||
RoleName: githubdeploy-procurement-ingest
|
|
||||||
AssumeRolePolicyDocument:
|
|
||||||
Version: "2012-10-17"
|
|
||||||
Statement:
|
|
||||||
- Effect: Allow
|
|
||||||
Principal:
|
|
||||||
Federated: !Sub arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com
|
|
||||||
Action: sts:AssumeRoleWithWebIdentity
|
|
||||||
Condition:
|
|
||||||
StringEquals:
|
|
||||||
token.actions.githubusercontent.com:aud: sts.amazonaws.com
|
|
||||||
StringLike:
|
|
||||||
token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrg}/procurement-ingest:ref:refs/heads/main
|
|
||||||
Policies:
|
|
||||||
- PolicyName: cdk-deploy
|
|
||||||
PolicyDocument:
|
|
||||||
Version: "2012-10-17"
|
|
||||||
Statement:
|
|
||||||
- Effect: Allow
|
|
||||||
Action:
|
|
||||||
- sts:AssumeRole
|
|
||||||
Resource:
|
|
||||||
- !Sub arn:aws:iam::${AWS::AccountId}:role/cdk-hnb659fds-*
|
|
||||||
|
|
||||||
ApmWoAnalysisDeployRole:
|
|
||||||
Type: AWS::IAM::Role
|
|
||||||
Properties:
|
|
||||||
RoleName: githubdeploy-apm-wo-analysis
|
|
||||||
AssumeRolePolicyDocument:
|
|
||||||
Version: "2012-10-17"
|
|
||||||
Statement:
|
|
||||||
- Effect: Allow
|
|
||||||
Principal:
|
|
||||||
Federated: !Sub arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com
|
|
||||||
Action: sts:AssumeRoleWithWebIdentity
|
|
||||||
Condition:
|
|
||||||
StringEquals:
|
|
||||||
token.actions.githubusercontent.com:aud: sts.amazonaws.com
|
|
||||||
StringLike:
|
|
||||||
token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrg}/apm-wo-analysis:ref:refs/heads/main
|
|
||||||
Policies:
|
|
||||||
- PolicyName: cdk-deploy
|
|
||||||
PolicyDocument:
|
|
||||||
Version: "2012-10-17"
|
|
||||||
Statement:
|
|
||||||
- Effect: Allow
|
|
||||||
Action:
|
|
||||||
- sts:AssumeRole
|
|
||||||
Resource:
|
|
||||||
- !Sub arn:aws:iam::${AWS::AccountId}:role/cdk-hnb659fds-*
|
|
||||||
|
|
||||||
SeahavenAccountBaselineDeployRole:
|
SeahavenAccountBaselineDeployRole:
|
||||||
Type: AWS::IAM::Role
|
Type: AWS::IAM::Role
|
||||||
Properties:
|
Properties:
|
||||||
|
|
@ -1376,6 +1155,61 @@ Resources:
|
||||||
Resource:
|
Resource:
|
||||||
- !Sub arn:aws:iam::${AWS::AccountId}:role/cdk-hnb659fds-*
|
- !Sub arn:aws:iam::${AWS::AccountId}:role/cdk-hnb659fds-*
|
||||||
|
|
||||||
|
# MealOrderManagerWeeklyMenuRole removed 2026-08-20 (PLAT-70):
|
||||||
|
# weekly-menu OIDC role now lives in seahaven-prod as
|
||||||
|
# /tf-managed/githubdeploy-meal-order-manager-weekly-menu (HCP TF).
|
||||||
|
# GitHub secret AWS_WEEKLY_MENU_ROLE_ARN already points at the prod role.
|
||||||
|
# Mgmt role github-meal-order-manager-weekly-menu deleted with this stack update.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# PLAT-234 principal only. The checks are seahaven-org-baseline pull request
|
||||||
|
# 160: .github/workflows/ci.yaml job iam-policy-check and
|
||||||
|
# scripts/check_iam_policies.py. That job assumes this role. It asserts
|
||||||
|
# StringEquals on the bootstrap trust templates, no lambda write on the
|
||||||
|
# plan template, then ValidatePolicy and CheckNoNewAccess when this role
|
||||||
|
# can be assumed.
|
||||||
|
SeahavenOrgBaselinePolicyCheckRole:
|
||||||
|
Type: AWS::IAM::Role
|
||||||
|
Properties:
|
||||||
|
RoleName: githubdeploy-seahaven-org-baseline-policy-check
|
||||||
|
Description: Access Analyzer policy checks for seahaven-org-baseline CI. No deploy permissions.
|
||||||
|
AssumeRolePolicyDocument:
|
||||||
|
Version: "2012-10-17"
|
||||||
|
Statement:
|
||||||
|
- Effect: Allow
|
||||||
|
Principal:
|
||||||
|
Federated: !Sub arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com
|
||||||
|
Action: sts:AssumeRoleWithWebIdentity
|
||||||
|
# pull_request jobs with no environment use sub
|
||||||
|
# repo:ORG/seahaven-org-baseline:pull_request. refs/pull/N/merge is
|
||||||
|
# the ref claim, not sub. A second statement is required: StringEquals
|
||||||
|
# and StringLike in one condition are AND.
|
||||||
|
Condition:
|
||||||
|
StringEquals:
|
||||||
|
token.actions.githubusercontent.com:aud: sts.amazonaws.com
|
||||||
|
token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrg}/seahaven-org-baseline:pull_request
|
||||||
|
- Effect: Allow
|
||||||
|
Principal:
|
||||||
|
Federated: !Sub arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com
|
||||||
|
Action: sts:AssumeRoleWithWebIdentity
|
||||||
|
Condition:
|
||||||
|
StringEquals:
|
||||||
|
token.actions.githubusercontent.com:aud: sts.amazonaws.com
|
||||||
|
StringLike:
|
||||||
|
token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrg}/seahaven-org-baseline:ref:refs/heads/gh-readonly-queue/main/*
|
||||||
|
Policies:
|
||||||
|
- PolicyName: access-analyzer-policy-check
|
||||||
|
PolicyDocument:
|
||||||
|
Version: "2012-10-17"
|
||||||
|
Statement:
|
||||||
|
- Sid: AccessAnalyzerPolicyCheck
|
||||||
|
Effect: Allow
|
||||||
|
Action:
|
||||||
|
- access-analyzer:ValidatePolicy
|
||||||
|
- access-analyzer:CheckNoNewAccess
|
||||||
|
Resource: "*"
|
||||||
|
|
||||||
Outputs:
|
Outputs:
|
||||||
LambdaExecutionBoundaryArn:
|
LambdaExecutionBoundaryArn:
|
||||||
Value: !Ref LambdaExecutionBoundary
|
Value: !Ref LambdaExecutionBoundary
|
||||||
|
|
@ -1390,23 +1224,20 @@ Outputs:
|
||||||
Name: github-cfn-execution-role-arn
|
Name: github-cfn-execution-role-arn
|
||||||
AfterhoursShiftManagerDeployRoleArn:
|
AfterhoursShiftManagerDeployRoleArn:
|
||||||
Value: !GetAtt AfterhoursShiftManagerDeployRole.Arn
|
Value: !GetAtt AfterhoursShiftManagerDeployRole.Arn
|
||||||
FrontIntegrationsDeployRoleArn:
|
|
||||||
Value: !GetAtt FrontIntegrationsDeployRole.Arn
|
|
||||||
AfiBackupMonitorDeployRoleArn:
|
|
||||||
Value: !GetAtt AfiBackupMonitorDeployRole.Arn
|
|
||||||
PaymentsDashboardDeployRoleArn:
|
PaymentsDashboardDeployRoleArn:
|
||||||
Value: !GetAtt PaymentsDashboardDeployRole.Arn
|
Value: !GetAtt PaymentsDashboardDeployRole.Arn
|
||||||
# SeahavenSlackBotDeployRoleArn removed 2026-07-27: the role was deleted
|
# SeahavenSlackBotDeployRoleArn removed 2026-07-27: the role was deleted
|
||||||
# out-of-band on 2026-07-23, so this !GetAtt failed as a live IAM read and
|
# out-of-band on 2026-07-23, so this !GetAtt failed as a live IAM read and
|
||||||
# broke every stack update. Nothing imported it (the Output had no
|
# broke every stack update. Nothing imported it (the Output had no
|
||||||
# ExportName, and no stack imports any export from this stack).
|
# ExportName, and no stack imports any export from this stack).
|
||||||
ExecAideDeployRoleArn:
|
# ProcurementIngestDeployRoleArn removed 2026-08-07 (PLAT-88): HCP sole
|
||||||
Value: !GetAtt ExecAideDeployRole.Arn
|
# mutate path; prod githubdeploy role deleted; mgmt twin already gone.
|
||||||
SeahavenDoorUnlockApiDeployRoleArn:
|
# FrontIntegrations, AfiBackupMonitor, ExecAide, SeahavenDoorUnlockApi,
|
||||||
Value: !GetAtt SeahavenDoorUnlockApiDeployRole.Arn
|
# and ApmWoAnalysis deploy role outputs removed 2026-09-28 (PLAT-232).
|
||||||
ProcurementIngestDeployRoleArn:
|
# CloudTrail showed no successful mutation for 14 days. The roles are
|
||||||
Value: !GetAtt ProcurementIngestDeployRole.Arn
|
# deleted only when this stack is deployed. That deploy is not this change.
|
||||||
ApmWoAnalysisDeployRoleArn:
|
|
||||||
Value: !GetAtt ApmWoAnalysisDeployRole.Arn
|
|
||||||
SeahavenAccountBaselineDeployRoleArn:
|
SeahavenAccountBaselineDeployRoleArn:
|
||||||
Value: !GetAtt SeahavenAccountBaselineDeployRole.Arn
|
Value: !GetAtt SeahavenAccountBaselineDeployRole.Arn
|
||||||
|
SeahavenOrgBaselinePolicyCheckRoleArn:
|
||||||
|
Value: !GetAtt SeahavenOrgBaselinePolicyCheckRole.Arn
|
||||||
|
# MealOrderManagerWeeklyMenuRoleArn removed 2026-08-20 (PLAT-70): role deleted with resource.
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@555d07c3a240689a81668026787eba089df4c975 # main
|
uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
|
||||||
with:
|
with:
|
||||||
# Matches local dev (Node 24 / npm 11, lockfileVersion 3). This is also the
|
# Matches local dev (Node 24 / npm 11, lockfileVersion 3). This is also the
|
||||||
# reusable workflow's default — passed explicitly to pin against drift.
|
# reusable workflow's default — passed explicitly to pin against drift.
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ name: CI (.NET)
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
merge_group:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
ci:
|
||||||
|
|
@ -11,4 +12,4 @@ jobs:
|
||||||
# Every input is optional. Common overrides: `solution` (defaults to *.sln
|
# Every input is optional. Common overrides: `solution` (defaults to *.sln
|
||||||
# in the working directory), `working-directory`, and `dotnet-version`
|
# in the working directory), `working-directory`, and `dotnet-version`
|
||||||
# (defaults to 8.0.x). This reusable has no `node-version` input.
|
# (defaults to 8.0.x). This reusable has no `node-version` input.
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/ci-dotnet.yaml@555d07c3a240689a81668026787eba089df4c975 # main
|
uses: Sea-Haven-Industries/.github/.github/workflows/ci-dotnet.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
|
||||||
|
|
|
||||||
7
workflow-templates/ci-hcp.properties.json
Normal file
7
workflow-templates/ci-hcp.properties.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"name": "Sea Haven — CI (HCP)",
|
||||||
|
"description": "Parallel frontend + Terraform CI with autofix and a ci-complete aggregator for converted HCP app repos. Remaining-lane SPAs should keep the sequential TypeScript frontend template.",
|
||||||
|
"iconName": "octicon-checklist",
|
||||||
|
"categories": ["TypeScript", "JavaScript", "Continuous integration"],
|
||||||
|
"filePatterns": ["package\\.json$", "vite\\.config\\.[jt]s$", "terraform/.*\\.tf$"]
|
||||||
|
}
|
||||||
52
workflow-templates/ci-hcp.yml
Normal file
52
workflow-templates/ci-hcp.yml
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
name: CI
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main, hotfix/**, release/**]
|
||||||
|
merge_group:
|
||||||
|
push:
|
||||||
|
branches: [hotfix/**, release/**]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
autofix:
|
||||||
|
if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork
|
||||||
|
uses: Sea-Haven-Industries/.github/.github/workflows/ci-autofix.yaml@REPLACE-ME # vX.Y.Z
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
presets: prettier,terraform
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
needs: autofix
|
||||||
|
if: always() && !cancelled() && (needs.autofix.result == 'skipped' || needs.autofix.outputs.committed != 'true')
|
||||||
|
uses: Sea-Haven-Industries/.github/.github/workflows/ci-frontend.yaml@REPLACE-ME # vX.Y.Z
|
||||||
|
with:
|
||||||
|
node-version: "24"
|
||||||
|
unit-shards: 4
|
||||||
|
run-e2e: true
|
||||||
|
|
||||||
|
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@REPLACE-ME # vX.Y.Z
|
||||||
|
with:
|
||||||
|
terraform-version: "1.16.0"
|
||||||
|
|
||||||
|
ci-complete:
|
||||||
|
name: ci-complete
|
||||||
|
needs: [autofix, frontend, 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:
|
||||||
|
FRONTEND: ${{ needs.frontend.result }}
|
||||||
|
TERRAFORM: ${{ needs.terraform.result }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
test "${FRONTEND}" = success
|
||||||
|
test "${TERRAFORM}" = success
|
||||||
|
|
@ -2,12 +2,13 @@ name: CI (Mobile / iOS)
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
merge_group:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
ci:
|
||||||
# Job id MUST stay `ci`: the reusable's aggregator job is also `ci`, so the
|
# Job id MUST stay `ci`: the reusable's aggregator job is also `ci`, so the
|
||||||
# check context resolves to the required `ci / ci`.
|
# check context resolves to the required `ci / ci`.
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/ci-mobile-ios.yaml@9389e51c10c506caa55f204527452a9e29b0e438 # main
|
uses: Sea-Haven-Industries/.github/.github/workflows/ci-mobile-ios.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
|
||||||
with:
|
with:
|
||||||
# Matches local dev (Node 24 / npm 11, lockfileVersion 3). This is also
|
# Matches local dev (Node 24 / npm 11, lockfileVersion 3). This is also
|
||||||
# the reusable workflow's default — passed explicitly to pin against drift.
|
# the reusable workflow's default — passed explicitly to pin against drift.
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@ name: CI (Node / TypeScript)
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
merge_group:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
ci:
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@555d07c3a240689a81668026787eba089df4c975 # main
|
uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
|
||||||
with:
|
with:
|
||||||
# Matches local dev (Node 24 / npm 11, lockfileVersion 3). This is also the
|
# Matches local dev (Node 24 / npm 11, lockfileVersion 3). This is also the
|
||||||
# reusable workflow's default — passed explicitly to pin against drift.
|
# reusable workflow's default — passed explicitly to pin against drift.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "Sea Haven — CI (Python / app)",
|
"name": "Sea Haven — CI (Python / app)",
|
||||||
"description": "Runs ruff check, ruff format --check, a pytest collect-only import check, and an optional subproject suite via the org reusable workflow. For Python repos that do not deploy via SAM or CDK.",
|
"description": "Runs ruff check, ruff format --check, and a conventions audit via the org reusable workflow. For Python repos that do not deploy via SAM or CDK.",
|
||||||
"iconName": "octicon-checklist",
|
"iconName": "octicon-checklist",
|
||||||
"categories": ["Python", "Continuous integration"],
|
"categories": ["Python", "Continuous integration"],
|
||||||
"filePatterns": ["requirements.*\\.txt$", "pyproject\\.toml$"]
|
"filePatterns": ["requirements.*\\.txt$", "pyproject\\.toml$"]
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,12 @@ name: CI (Python / app)
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
merge_group:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
ci:
|
||||||
# Job id MUST stay `ci`: the reusable's aggregator job is also `ci`, so the
|
# Job id MUST stay `ci`: the reusable's aggregator job is also `ci`, so the
|
||||||
# check context resolves to the required `ci / ci`.
|
# check context resolves to the required `ci / ci`.
|
||||||
#
|
#
|
||||||
# Every input is optional. Common overrides: `source-dirs` (ruff targets),
|
# Every input is optional. Common override: `source-dirs` (ruff targets).
|
||||||
# `requirements` (non-default requirements file), `subproject-dir` (a
|
uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-app.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
|
||||||
# self-contained suite that must run in its own working directory).
|
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-app.yaml@555d07c3a240689a81668026787eba089df4c975 # main
|
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@ name: CI (Python / SAM)
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
merge_group:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
ci:
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-sam.yaml@555d07c3a240689a81668026787eba089df4c975 # main
|
uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-sam.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
|
||||||
with:
|
with:
|
||||||
run-tests: true
|
run-tests: true
|
||||||
# ci-python-sam.yaml declares a `node-version` input (default "24") that
|
# ci-python-sam.yaml declares a `node-version` input (default "24") that
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,13 @@ name: CI (Static Site)
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
merge_group:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
ci:
|
||||||
# Job id MUST stay `ci`: the reusable's job is also `ci`, so the check
|
# Job id MUST stay `ci`: the reusable's job is also `ci`, so the check
|
||||||
# context resolves to the required `ci / ci`.
|
# context resolves to the required `ci / ci`.
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/ci-static.yaml@555d07c3a240689a81668026787eba089df4c975 # main
|
uses: Sea-Haven-Industries/.github/.github/workflows/ci-static.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
|
||||||
with:
|
with:
|
||||||
# Pinned explicitly (handbook): local dev is Node 24 / npm 11, which
|
# Pinned explicitly (handbook): local dev is Node 24 / npm 11, which
|
||||||
# generates lockfileVersion 3. Being explicit avoids lockfile drift.
|
# generates lockfileVersion 3. Being explicit avoids lockfile drift.
|
||||||
|
|
|
||||||
7
workflow-templates/ci-terraform.properties.json
Normal file
7
workflow-templates/ci-terraform.properties.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"name": "Sea Haven — CI (Terraform)",
|
||||||
|
"description": "Runs terraform fmt -check, init -backend=false, and validate via the org reusable ci-terraform workflow. Prefer the HCP CI template when the repo also has a frontend.",
|
||||||
|
"iconName": "octicon-checklist",
|
||||||
|
"categories": ["Continuous integration"],
|
||||||
|
"filePatterns": ["terraform/.*\\.tf$"]
|
||||||
|
}
|
||||||
16
workflow-templates/ci-terraform.yml
Normal file
16
workflow-templates/ci-terraform.yml
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
name: Terraform CI
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main, hotfix/**, release/**]
|
||||||
|
merge_group:
|
||||||
|
push:
|
||||||
|
branches: [hotfix/**, release/**]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
terraform:
|
||||||
|
uses: Sea-Haven-Industries/.github/.github/workflows/ci-terraform.yaml@REPLACE-ME # vX.Y.Z
|
||||||
|
with:
|
||||||
|
terraform-version: "1.16.0"
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "Sea Haven — CI (TypeScript / frontend)",
|
"name": "Sea Haven — CI (TypeScript / frontend)",
|
||||||
"description": "Runs the Sea Haven standards gate, format:check, lint, build, unit tests, and a Playwright browser smoke for bundled Vite/React/Vue apps via the org reusable workflow.",
|
"description": "Sequential remaining-lane CI that emits ci / ci. Converted HCP SPAs should use the HCP CI template (ci-frontend plus ci-complete) instead.",
|
||||||
"iconName": "octicon-checklist",
|
"iconName": "octicon-checklist",
|
||||||
"categories": ["TypeScript", "JavaScript", "Continuous integration"],
|
"categories": ["TypeScript", "JavaScript", "Continuous integration"],
|
||||||
"filePatterns": ["package\\.json$", "vite\\.config\\.[jt]s$", "playwright\\.config\\.[jt]s$"]
|
"filePatterns": ["package\\.json$", "vite\\.config\\.[jt]s$", "playwright\\.config\\.[jt]s$"]
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,13 @@ name: CI (TypeScript / frontend)
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
merge_group:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
ci:
|
||||||
# Job id MUST stay `ci`: the reusable's job is also `ci`, so the check
|
# Job id MUST stay `ci`: the reusable's job is also `ci`, so the check
|
||||||
# context resolves to the required `ci / ci`.
|
# context resolves to the required `ci / ci`.
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-frontend.yaml@555d07c3a240689a81668026787eba089df4c975 # main
|
uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-frontend.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
|
||||||
with:
|
with:
|
||||||
# Pinned explicitly (handbook): local dev is Node 24 / npm 11, which
|
# Pinned explicitly (handbook): local dev is Node 24 / npm 11, which
|
||||||
# generates lockfileVersion 3. Being explicit avoids lockfile drift.
|
# generates lockfileVersion 3. Being explicit avoids lockfile drift.
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,4 @@ permissions:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
dependency-review:
|
dependency-review:
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/callable-dependency-review.yaml@555d07c3a240689a81668026787eba089df4c975 # main
|
uses: Sea-Haven-Industries/.github/.github/workflows/callable-dependency-review.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/cd-dotnet-eb.yaml@555d07c3a240689a81668026787eba089df4c975 # main
|
uses: Sea-Haven-Industries/.github/.github/workflows/cd-dotnet-eb.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
|
||||||
with:
|
with:
|
||||||
# Required: the project to publish, relative to the repo root.
|
# Required: the project to publish, relative to the repo root.
|
||||||
project: REPLACE-ME-project-csproj
|
project: REPLACE-ME-project-csproj
|
||||||
|
|
|
||||||
7
workflow-templates/hcp-fargate-deploy.properties.json
Normal file
7
workflow-templates/hcp-fargate-deploy.properties.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"name": "Sea Haven — Deploy (HCP Fargate)",
|
||||||
|
"description": "Deploys a Fargate image via the org reusable cd-hcp-fargate workflow. One caller job per GitHub Environment. Push to main deploys dev; a published Release deploys prod behind ship-gate.",
|
||||||
|
"iconName": "octicon-rocket",
|
||||||
|
"categories": ["Deployment", "Docker", "Continuous integration"],
|
||||||
|
"filePatterns": ["Dockerfile$", "terraform/.*\\.tf$"]
|
||||||
|
}
|
||||||
54
workflow-templates/hcp-fargate-deploy.yml
Normal file
54
workflow-templates/hcp-fargate-deploy.yml
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
name: Deploy API
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths-ignore: [terraform/**, docs/**, "*.md"]
|
||||||
|
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:
|
||||||
|
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@REPLACE-ME # vX.Y.Z
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
environment: dev
|
||||||
|
ref: ${{ inputs.ref }}
|
||||||
|
ssm-prefix: /REPLACE-ME-repo/deploy
|
||||||
|
docker-platform: linux/amd64
|
||||||
|
# extra-task-env: '{"SENTRY_DSN_PARAM":"/REPLACE-ME-repo/sentry-dsn"}'
|
||||||
|
|
||||||
|
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@REPLACE-ME # vX.Y.Z
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
environment: prod
|
||||||
|
ref: ${{ github.event.release.tag_name || inputs.ref }}
|
||||||
|
ssm-prefix: /REPLACE-ME-repo/deploy
|
||||||
|
docker-platform: linux/amd64
|
||||||
|
ship-gate: true
|
||||||
|
# extra-task-env: '{"SENTRY_DSN_PARAM":"/REPLACE-ME-repo/sentry-dsn"}'
|
||||||
7
workflow-templates/hcp-spa-deploy.properties.json
Normal file
7
workflow-templates/hcp-spa-deploy.properties.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"name": "Sea Haven — Deploy (HCP SPA)",
|
||||||
|
"description": "Deploys a Vite SPA to S3/CloudFront via the org reusable cd-hcp-spa workflow. One caller job per GitHub Environment. Add a deploy-staging job only when that Environment exists.",
|
||||||
|
"iconName": "octicon-rocket",
|
||||||
|
"categories": ["Deployment", "TypeScript", "JavaScript"],
|
||||||
|
"filePatterns": ["vite\\.config\\.[jt]s$", "package\\.json$"]
|
||||||
|
}
|
||||||
51
workflow-templates/hcp-spa-deploy.yml
Normal file
51
workflow-templates/hcp-spa-deploy.yml
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
name: Deploy Web
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths-ignore: [terraform/**, docs/**, "*.md"]
|
||||||
|
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:
|
||||||
|
deploy-dev:
|
||||||
|
name: Deploy SPA to dev
|
||||||
|
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.environment == 'dev')
|
||||||
|
uses: Sea-Haven-Industries/.github/.github/workflows/cd-hcp-spa.yaml@REPLACE-ME # vX.Y.Z
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
environment: dev
|
||||||
|
ref: ${{ inputs.ref }}
|
||||||
|
ssm-prefix: /REPLACE-ME-repo/deploy
|
||||||
|
|
||||||
|
deploy-prod:
|
||||||
|
name: Deploy SPA to prod
|
||||||
|
if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.environment == 'prod')
|
||||||
|
uses: Sea-Haven-Industries/.github/.github/workflows/cd-hcp-spa.yaml@REPLACE-ME # vX.Y.Z
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
environment: prod
|
||||||
|
ref: ${{ github.event.release.tag_name || inputs.ref }}
|
||||||
|
ssm-prefix: /REPLACE-ME-repo/deploy
|
||||||
|
ship-gate: true
|
||||||
|
# required-vite-vars: "VITE_SHIFTS_API_BASE,VITE_SENTRY_DSN"
|
||||||
|
|
@ -13,4 +13,4 @@ permissions:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
label:
|
label:
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/callable-labeler.yaml@555d07c3a240689a81668026787eba089df4c975 # main
|
uses: Sea-Haven-Industries/.github/.github/workflows/callable-labeler.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/cd-mobile-ios.yaml@555d07c3a240689a81668026787eba089df4c975 # main
|
uses: Sea-Haven-Industries/.github/.github/workflows/cd-mobile-ios.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
|
||||||
with:
|
with:
|
||||||
# Pinned explicitly (handbook): local dev is Node 24 / npm 11, which
|
# Pinned explicitly (handbook): local dev is Node 24 / npm 11, which
|
||||||
# generates lockfileVersion 3. Being explicit avoids lockfile drift.
|
# generates lockfileVersion 3. Being explicit avoids lockfile drift.
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ permissions:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/release.yaml@9389e51c10c506caa55f204527452a9e29b0e438 # main
|
uses: Sea-Haven-Industries/.github/.github/workflows/release.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
|
||||||
with:
|
with:
|
||||||
version: ${{ inputs.version }}
|
version: ${{ inputs.version }}
|
||||||
# Tags in this org are v-prefixed MAJOR.MINOR.PATCH; "v" is the default
|
# Tags in this org are v-prefixed MAJOR.MINOR.PATCH; "v" is the default
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/cd-sam.yaml@555d07c3a240689a81668026787eba089df4c975 # main
|
uses: Sea-Haven-Industries/.github/.github/workflows/cd-sam.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
|
||||||
with:
|
with:
|
||||||
# Required: the CloudFormation stack name (kebab-case, matches repo name).
|
# Required: the CloudFormation stack name (kebab-case, matches repo name).
|
||||||
# NOTE: this is a literal placeholder on purpose — starter-workflow variables
|
# NOTE: this is a literal placeholder on purpose — starter-workflow variables
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue