mirror of
https://github.com/Sea-Haven-Industries/.github.git
synced 2026-09-30 23:23:11 +00:00
202 lines
8.1 KiB
YAML
202 lines
8.1 KiB
YAML
name: CD — CDK Deploy
|
|
|
|
on:
|
|
workflow_call:
|
|
inputs:
|
|
node-version:
|
|
description: "Node.js version to use"
|
|
type: string
|
|
default: "24"
|
|
python-version:
|
|
description: "Python version for Python CDK repos (leave empty for TypeScript CDK)"
|
|
type: string
|
|
default: ""
|
|
dotnet-version:
|
|
description: "Optional .NET SDK version for repos with .NET assets"
|
|
type: string
|
|
default: ""
|
|
dotnet-publish-project:
|
|
description: "Optional .NET project path to publish before CDK deploy"
|
|
type: string
|
|
default: ""
|
|
region:
|
|
description: "AWS region"
|
|
type: string
|
|
default: "us-east-1"
|
|
cdk-dir:
|
|
description: "Directory containing cdk.json"
|
|
type: string
|
|
default: "."
|
|
enable-qemu:
|
|
description: "Enable QEMU for cross-platform Docker builds (arm64 on x86 runners)"
|
|
type: boolean
|
|
default: false
|
|
stack-name:
|
|
description: "CloudFormation stack name (for pre-flight checks)"
|
|
type: string
|
|
default: ""
|
|
stacks:
|
|
description: "CDK stack selector(s) to deploy (space-separated construct ids/patterns). Default deploys every stack in the app; set per job when a multi-account app splits deploys across roles."
|
|
type: string
|
|
default: "--all"
|
|
post-deploy-script:
|
|
description: "Optional path to a script to run after CDK deploy (e.g. web build, S3 sync)"
|
|
type: string
|
|
default: ""
|
|
secrets:
|
|
deploy-role-arn:
|
|
description: "OIDC deploy role ARN"
|
|
required: true
|
|
|
|
permissions:
|
|
id-token: write
|
|
contents: read
|
|
|
|
jobs:
|
|
deploy:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
# Serialise per deploy target so two pushes cannot deploy over each other.
|
|
# The target is the stack selector, NOT stack-name: a multi-account app can
|
|
# call this workflow from several jobs in one run (seahaven-org-baseline
|
|
# runs five, one per AWS account) and stack-name is optional, so keying on
|
|
# it alone would collapse the selector-only jobs into one group and
|
|
# serialise deploys that are genuinely independent. `stacks` always defaults
|
|
# to "--all", so the group is never empty and back-to-back deploys of the
|
|
# same target still queue.
|
|
# cancel-in-progress is FALSE on purpose: unlike CI, aborting midway can
|
|
# leave a stack mid-update.
|
|
concurrency:
|
|
group: cd-cdk-${{ inputs.region }}-${{ inputs.stacks }}-${{ inputs.stack-name }}
|
|
cancel-in-progress: false
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4
|
|
if: ${{ inputs.enable-qemu }}
|
|
|
|
- uses: actions/setup-dotnet@v6
|
|
if: ${{ inputs.dotnet-version != '' }}
|
|
with:
|
|
dotnet-version: ${{ inputs.dotnet-version }}
|
|
|
|
- name: Publish .NET project
|
|
if: ${{ inputs.dotnet-publish-project != '' }}
|
|
# Env-var indirection (not inline expression interpolation) so shell
|
|
# metacharacters in the input are never parsed as script; the input is a
|
|
# single project path, so it stays quoted (no word-split) — an unquoted
|
|
# $(dirname ...) split the output path on whitespace.
|
|
env:
|
|
DOTNET_PUBLISH_PROJECT: ${{ inputs.dotnet-publish-project }}
|
|
run: |
|
|
dotnet publish "$DOTNET_PUBLISH_PROJECT" \
|
|
--configuration Release \
|
|
--runtime linux-arm64 \
|
|
--self-contained false \
|
|
--output "$(dirname "$DOTNET_PUBLISH_PROJECT")/bin/Release/net8.0/linux-arm64/publish"
|
|
|
|
- uses: actions/setup-node@v7
|
|
with:
|
|
node-version: ${{ inputs.node-version }}
|
|
|
|
- uses: actions/setup-python@v7
|
|
if: ${{ inputs.python-version != '' }}
|
|
with:
|
|
python-version: ${{ inputs.python-version }}
|
|
|
|
- name: Install Node dependencies
|
|
# Only when a lockfile exists (TS CDK repos). Python CDK repos have no
|
|
# package.json and use `npx -y cdk`, so skip rather than fail npm ci.
|
|
if: ${{ hashFiles(format('{0}/package-lock.json', inputs.cdk-dir)) != '' }}
|
|
working-directory: ${{ inputs.cdk-dir }}
|
|
run: npm ci
|
|
|
|
- name: Install Python dependencies
|
|
if: ${{ inputs.python-version != '' }}
|
|
# NUL-delimited read loop rather than `for req in $(find ...)`: the
|
|
# command-substitution form word-splits and globs every path it finds.
|
|
shell: bash
|
|
run: |
|
|
while IFS= read -r -d '' req; do
|
|
pip install -r "$req"
|
|
done < <(find . -name requirements.txt -not -path '*/node_modules/*' -print0)
|
|
|
|
- uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6
|
|
with:
|
|
role-to-assume: ${{ secrets.deploy-role-arn }}
|
|
aws-region: ${{ inputs.region }}
|
|
|
|
- name: Pre-flight checks
|
|
if: ${{ inputs.stack-name != '' }}
|
|
run: |
|
|
echo "Pre-flight: checking stack ${{ inputs.stack-name }}..."
|
|
STATUS=$(aws cloudformation describe-stacks \
|
|
--stack-name "${{ inputs.stack-name }}" \
|
|
--query 'Stacks[0].StackStatus' --output text 2>/dev/null || echo "NOT_FOUND")
|
|
case "$STATUS" in
|
|
*ROLLBACK_COMPLETE|*FAILED)
|
|
echo "::error::Stack ${{ inputs.stack-name }} is in $STATUS — manual intervention required."
|
|
exit 1
|
|
;;
|
|
*IN_PROGRESS)
|
|
echo "::error::Stack ${{ inputs.stack-name }} has an operation in progress ($STATUS) — wait for it to complete."
|
|
exit 1
|
|
;;
|
|
NOT_FOUND)
|
|
echo "Pre-flight: stack not found — will be created on first deploy."
|
|
;;
|
|
*)
|
|
echo "Pre-flight: stack status is $STATUS — OK to deploy."
|
|
;;
|
|
esac
|
|
|
|
- name: CDK deploy
|
|
working-directory: ${{ inputs.cdk-dir }}
|
|
# Env-var indirection (not inline expression interpolation) so shell
|
|
# metacharacters in the input are never parsed as script; unquoted
|
|
# $STACKS deliberately word-splits multiple selectors.
|
|
env:
|
|
STACKS: ${{ inputs.stacks }}
|
|
run: |
|
|
# $STACKS is deliberately unquoted: it carries one or more
|
|
# space-separated CDK stack selectors (default "--all") that must reach
|
|
# `cdk deploy` as separate argv entries. Quoting it would collapse them
|
|
# into one bogus selector and break every multi-stack deploy.
|
|
# shellcheck disable=SC2086
|
|
npx -y cdk deploy $STACKS --require-approval never
|
|
|
|
- name: Post-deploy script
|
|
if: ${{ inputs.post-deploy-script != '' }}
|
|
# Env-var indirection (not inline expression interpolation) so shell
|
|
# metacharacters in the input are never parsed as script; the input is a
|
|
# single script path, so $POST_DEPLOY_SCRIPT is quoted (no word-split).
|
|
env:
|
|
POST_DEPLOY_SCRIPT: ${{ inputs.post-deploy-script }}
|
|
run: bash "$POST_DEPLOY_SCRIPT"
|
|
|
|
- name: Post-deploy health check
|
|
if: ${{ inputs.stack-name != '' }}
|
|
run: |
|
|
echo "Health check: verifying stack ${{ inputs.stack-name }}..."
|
|
|
|
STATUS=$(aws cloudformation describe-stacks \
|
|
--stack-name "${{ inputs.stack-name }}" \
|
|
--query 'Stacks[0].StackStatus' --output text)
|
|
if [[ "$STATUS" != *"COMPLETE" ]] || [[ "$STATUS" == *"ROLLBACK"* ]]; then
|
|
echo "::error::Stack ${{ inputs.stack-name }} ended in $STATUS after deploy."
|
|
exit 1
|
|
fi
|
|
echo "Stack status: $STATUS"
|
|
|
|
echo "Stack outputs:"
|
|
aws cloudformation describe-stacks \
|
|
--stack-name "${{ inputs.stack-name }}" \
|
|
--query 'Stacks[0].Outputs[*].[OutputKey,OutputValue]' --output table
|
|
|
|
# Run project-specific health check if it exists
|
|
if [[ -f scripts/health-check.sh ]]; then
|
|
echo "Running project health check..."
|
|
bash scripts/health-check.sh "${{ inputs.stack-name }}" "${{ inputs.region }}"
|
|
fi
|
|
|
|
echo "Health check passed."
|