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 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 != '' }} run: dotnet publish ${{ inputs.dotnet-publish-project }} --configuration Release --runtime linux-arm64 --self-contained false --output $(dirname ${{ inputs.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 != '' }} run: | for req in $(find . -name requirements.txt -not -path '*/node_modules/*'); do pip install -r "$req" done - uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # 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: npx -y cdk deploy $STACKS --require-approval never - name: Post-deploy script if: ${{ inputs.post-deploy-script != '' }} run: bash ${{ inputs.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."