From 05d05a6af060443f84bccc7f305d448b8b41023d Mon Sep 17 00:00:00 2001 From: Tom Thorogood Date: Wed, 15 Jul 2026 16:05:28 +0930 Subject: [PATCH] Merge aws-cdk.yml's prepare and cdk jobs Previously aws-cdk.yml was split into two separate jobs, one containing the 'preparation' steps and one performing the actual work. Between these two jobs it shared the node_modules directory as an artifact. This packing, uploading, downloading and unpacking step adds needless overhead and may take upwards of 30s to complete. It also consumes artifact storage space, which has caused persistent pipeline failures on one of our client hosted projects. This separation serves no meaningful purpose so we merge them into one single job. It also gives us the slight advantage of having safe-chain installed throughout the entire workflow rather than just the prepare job. This is a useful defence in depth measure, though it is unlikely to offer any real security benefit. --- .github/workflows/aws-cdk.yml | 286 ++++++++-------------------------- 1 file changed, 69 insertions(+), 217 deletions(-) diff --git a/.github/workflows/aws-cdk.yml b/.github/workflows/aws-cdk.yml index 66aff00..015fae6 100644 --- a/.github/workflows/aws-cdk.yml +++ b/.github/workflows/aws-cdk.yml @@ -109,22 +109,17 @@ on: value: ${{ jobs.cdk.outputs.deployment-status }} jobs: - prepare: - name: 🔍 Prepare CDK Deployment + cdk: + name: â˜ī¸ CDK Operations runs-on: ${{ inputs.runs-on }} + permissions: + id-token: write + contents: read + pull-requests: write environment: ${{ inputs.github-environment }} outputs: - node-version: ${{ steps.node-version.outputs.version }} - package-manager: ${{ steps.detect-package-manager.outputs.manager }} - context-args: ${{ steps.context-config.outputs.args }} - stack-name: ${{ steps.resolve-stack-name.outputs.stack-name }} - sanitised-cdk-stack-name: ${{ steps.sanitise.outputs.sanitised-cdk-stack-name }} - cdk-bootstrap-cmd: ${{ steps.parse-cdk-config.outputs.bootstrap-cmd }} - cdk-synth-cmd: ${{ steps.parse-cdk-config.outputs.synth-cmd }} - cdk-diff-cmd: ${{ steps.parse-cdk-config.outputs.diff-cmd }} - cdk-deploy-cmd: ${{ steps.parse-cdk-config.outputs.deploy-cmd }} - auth-mode: ${{ steps.validate-inputs.outputs.auth-mode }} - role-session-name: ${{ steps.resolve-session-name.outputs.role-session-name }} + deployment-status: ${{ steps.deploy.outputs.status }} + stack-outputs: ${{ steps.deploy.outputs.stack-outputs }} steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 @@ -160,7 +155,7 @@ jobs: echo "✅ Detected pnpm" else echo "manager=npm" >> $GITHUB_OUTPUT - echo "✅ Detected npm" + echo "✅ Assuming npm" fi - name: Setup Node.js @@ -190,9 +185,11 @@ jobs: uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e #v6.4.0 with: node-version: ${{ steps.node-version.outputs.version }} - # yamllint disable rule:line-length - cache: ${{ steps.detect-package-manager.outputs.manager == 'yarn-berry' && 'yarn' || (steps.detect-package-manager.outputs.manager == 'yarn-classic' && 'yarn' || steps.detect-package-manager.outputs.manager) }} - # yamllint enable rule:line-length + cache: |- + ${{ case( + startsWith(steps.detect-package-manager.outputs.manager, 'yarn-'), 'yarn', + steps.detect-package-manager.outputs.manager + ) }} - name: Install safe-chain run: | @@ -201,35 +198,30 @@ jobs: - name: Install dependencies env: + FLAG_VERBOSE: ${{ case(inputs.debug == true, '--verbose', '') }} PACKAGE_MANAGER: ${{ steps.detect-package-manager.outputs.manager }} - DEBUG_MODE: ${{ inputs.debug }} run: | echo "đŸ“Ļ Installing dependencies with $PACKAGE_MANAGER..." - verbose="" - if [ "$DEBUG_MODE" = "true" ]; then - verbose="--verbose" - fi - case "$PACKAGE_MANAGER" in "npm") - npm ci $verbose + npm ci $FLAG_VERBOSE ;; "yarn-classic") - yarn install --frozen-lockfile $verbose + yarn install --frozen-lockfile $FLAG_VERBOSE ;; "yarn-berry") - yarn install --immutable $verbose + yarn install --immutable $FLAG_VERBOSE ;; "pnpm") - pnpm install --frozen-lockfile $verbose + pnpm install --frozen-lockfile $FLAG_VERBOSE ;; esac - name: Export extra environment variables env: - EXTRA_VARS: ${{ vars.EXTRA_VARS }} EXTRA_SECRETS: ${{ secrets.EXTRA_SECRETS }} + EXTRA_VARS: ${{ vars.EXTRA_VARS }} run: | if [ -n "$EXTRA_VARS" ]; then while IFS= read -r line; do @@ -242,62 +234,21 @@ jobs: done <<< "$EXTRA_SECRETS" fi - - name: Set CDK commands - id: parse-cdk-config - env: - BOOTSTRAP_CMD: ${{ inputs.bootstrap-command }} - SYNTH_CMD: ${{ inputs.synth-command }} - DIFF_CMD: ${{ inputs.diff-command }} - DEPLOY_CMD: ${{ inputs.deploy-command }} - run: | - echo "✅ CDK commands:" - echo " bootstrap: $BOOTSTRAP_CMD" - echo " synth: $SYNTH_CMD" - echo " diff: $DIFF_CMD" - echo " deploy: $DEPLOY_CMD" - - echo "bootstrap-cmd=$BOOTSTRAP_CMD" >> $GITHUB_OUTPUT - echo "synth-cmd=$SYNTH_CMD" >> $GITHUB_OUTPUT - echo "diff-cmd=$DIFF_CMD" >> $GITHUB_OUTPUT - echo "deploy-cmd=$DEPLOY_CMD" >> $GITHUB_OUTPUT - - - name: Resolve stack name - id: resolve-stack-name - env: - INPUT_STACK_NAME: ${{ inputs.stack-name }} - VAR_STACK_NAME: ${{ vars.STACK_NAME }} - run: | - # Input takes priority over variable - if [ -n "$INPUT_STACK_NAME" ]; then - STACK_NAME="$INPUT_STACK_NAME" - else - STACK_NAME="$VAR_STACK_NAME" - fi - echo "stack-name=$STACK_NAME" >> $GITHUB_OUTPUT - - name: Validate required inputs id: validate-inputs env: - INPUT_ENVIRONMENT: ${{ inputs.github-environment }} - INPUT_STACK_NAME: ${{ steps.resolve-stack-name.outputs.stack-name }} - VAR_AWS_ACCESS_KEY_ID: ${{ vars.AWS_ACCESS_KEY_ID }} - SECRET_AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - VAR_AWS_ROLE_ARN: ${{ vars.AWS_ROLE_ARN }} - ENVIRONMENT_TARGET: ${{ inputs.environment-target }} CONTEXT_VALUES: ${{ inputs.context-values }} - INPUT_SYNTH: ${{ inputs.synth }} - INPUT_DIFF: ${{ inputs.diff }} + ENVIRONMENT_TARGET: ${{ inputs.environment-target }} + ENVIRONMENT: ${{ inputs.github-environment || 'Repository' }} INPUT_DEPLOY: ${{ inputs.deploy }} + INPUT_DIFF: ${{ inputs.diff }} + INPUT_SYNTH: ${{ inputs.synth }} + SECRET_AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + STACK_NAME: ${{ inputs.stack-name || vars.STACK_NAME }} + VAR_AWS_ACCESS_KEY_ID: ${{ vars.AWS_ACCESS_KEY_ID }} + VAR_AWS_ROLE_ARN: ${{ vars.AWS_ROLE_ARN }} run: | echo "🔍 Validating deployment configuration..." - - ENVIRONMENT="$INPUT_ENVIRONMENT" - STACK_NAME="$INPUT_STACK_NAME" - - if [ -z "$ENVIRONMENT" ]; then - ENVIRONMENT="Repository" - fi - echo "â„šī¸ Using variables from $ENVIRONMENT environment" if [ -z "$STACK_NAME" ]; then @@ -368,9 +319,9 @@ jobs: id: resolve-session-name if: steps.validate-inputs.outputs.auth-mode == 'oidc' env: - ROLE_SESSION_NAME: ${{ inputs.role-session-name }} COMMIT_SHA: ${{ github.sha }} REPOSITORY_NAME: ${{ github.event.repository.name }} + ROLE_SESSION_NAME: ${{ inputs.role-session-name }} RUN_NUMBER: ${{ github.run_number }} run: | SESSION_NAME="$ROLE_SESSION_NAME" @@ -385,8 +336,8 @@ jobs: - name: Configure CDK context id: context-config env: - ENVIRONMENT_TARGET: ${{ inputs.environment-target }} CONTEXT_VALUES: ${{ inputs.context-values }} + ENVIRONMENT_TARGET: ${{ inputs.environment-target }} run: | echo "âš™ī¸ Configuring CDK context..." @@ -395,11 +346,8 @@ jobs: # Add environment-specific context context_args="$context_args --context environment=$ENVIRONMENT_TARGET" - # Add custom context values (using process substitution to avoid subshell) if [ "$CONTEXT_VALUES" != "{}" ]; then - while read -r ctx; do - context_args="$context_args $ctx" - done < <(echo "$CONTEXT_VALUES" | jq -r 'to_entries[] | "--context \(.key)=\(.value)"') + context_args="$context_args$(jq -j 'to_entries[] | " --context \(.key)=\(.value)"' <<< "$CONTEXT_VALUES")" fi echo "args=$context_args" >> $GITHUB_OUTPUT @@ -408,91 +356,13 @@ jobs: - name: Sanitise stack name id: sanitise env: - STACK_NAME: ${{ steps.resolve-stack-name.outputs.stack-name }} + STACK_NAME: ${{ inputs.stack-name || vars.STACK_NAME }} run: | sanitised_cdk_stack_name=$(echo "$STACK_NAME" | tr -cd '[:alnum:]-_') echo "sanitised-cdk-stack-name=$sanitised_cdk_stack_name" >> $GITHUB_OUTPUT - - name: Package node_modules for artifact - run: | - echo "đŸ“Ļ Packaging node_modules..." - tar -czf node_modules.tar.gz node_modules/ - echo "✅ node_modules packaged ($(du -h node_modules.tar.gz | cut -f1))" - - - name: Upload node_modules artifact - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1 - with: - name: node_modules-${{ steps.sanitise.outputs.sanitised-cdk-stack-name }} - path: node_modules.tar.gz - retention-days: 1 - - cdk: - name: â˜ī¸ CDK Operations - runs-on: ${{ inputs.runs-on }} - needs: [prepare] - permissions: - id-token: write - contents: read - pull-requests: write - environment: ${{ inputs.github-environment }} - outputs: - stack-outputs: ${{ steps.deploy.outputs.stack-outputs }} - deployment-status: ${{ steps.deploy.outputs.status }} - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 - with: - lfs: ${{ inputs.lfs }} - persist-credentials: false - - - name: Setup Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e #v6.4.0 - with: - node-version: ${{ needs.prepare.outputs.node-version }} - - - name: Enable Corepack - if: needs.prepare.outputs.package-manager == 'yarn-berry' - run: | - echo "🔧 Enabling Corepack for Yarn Berry..." - corepack enable - - # If no packageManager field, default to stable Berry - if ! grep -q '"packageManager"' package.json; then - echo "đŸ“Ļ No packageManager found, preparing stable Yarn..." - corepack prepare yarn@stable --activate - fi - echo "✅ Corepack enabled" - - - name: Download node_modules artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c #v8.0.1 - with: - name: node_modules-${{ needs.prepare.outputs.sanitised-cdk-stack-name }} - - - name: Extract node_modules - run: | - echo "đŸ“Ļ Extracting node_modules..." - tar -xzf node_modules.tar.gz - rm node_modules.tar.gz - echo "✅ node_modules extracted" - - - name: Export extra environment variables - env: - EXTRA_VARS: ${{ vars.EXTRA_VARS }} - EXTRA_SECRETS: ${{ secrets.EXTRA_SECRETS }} - run: | - if [ -n "$EXTRA_VARS" ]; then - while IFS= read -r line; do - [ -n "$line" ] && echo "$line" | tr -d '\r' >> "$GITHUB_ENV" - done <<< "$EXTRA_VARS" - fi - if [ -n "$EXTRA_SECRETS" ]; then - while IFS= read -r line; do - [ -n "$line" ] && echo "$line" | tr -d '\r' >> "$GITHUB_ENV" - done <<< "$EXTRA_SECRETS" - fi - - name: Configure AWS credentials (Static) - if: needs.prepare.outputs.auth-mode == 'static' + if: steps.validate-inputs.outputs.auth-mode == 'static' uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 #v6.1.0 with: aws-access-key-id: ${{ vars.AWS_ACCESS_KEY_ID }} @@ -501,46 +371,38 @@ jobs: role-to-assume: ${{ vars.CFN_EXECUTION_ROLE || secrets.CFN_EXECUTION_ROLE }} - name: Configure AWS credentials (OIDC) - if: needs.prepare.outputs.auth-mode == 'oidc' + if: steps.validate-inputs.outputs.auth-mode == 'oidc' uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 #v6.1.0 with: role-to-assume: ${{ vars.AWS_ROLE_ARN }} - role-session-name: ${{ needs.prepare.outputs.role-session-name }} + role-session-name: ${{ steps.resolve-session-name.outputs.role-session-name }} aws-region: ${{ inputs.aws-region }} - name: Bootstrap CDK environment if: inputs.bootstrap == true env: - DEBUG_MODE: ${{ inputs.debug }} - BOOTSTRAP_CMD: ${{ needs.prepare.outputs.cdk-bootstrap-cmd }} - CFN_EXECUTION_ROLE: ${{ secrets.CFN_EXECUTION_ROLE }} AWS_REGION: ${{ inputs.aws-region }} + BOOTSTRAP_CMD: ${{ inputs.bootstrap-command }} + CFN_EXECUTION_ROLE: ${{ secrets.CFN_EXECUTION_ROLE }} EXTRA_ARGUMENTS: ${{ inputs.extra-arguments }} - CDK_BOOTSTRAP_CMD: ${{ needs.prepare.outputs.cdk-bootstrap-cmd }} + FLAG_CFN_EXECUTION_POLICY: |- + ${{ case( + secrets.CFN_EXECUTION_ROLE != '', + format('--cloudformation-execution-policies {0}', secrets.CFN_EXECUTION_ROLE), + '' + ) }} + FLAG_VERBOSE: ${{ case(inputs.debug == true, '--verbose', '') }} run: | echo "đŸĨž Bootstrapping CDK environment..." - verbose="" - if [ "$DEBUG_MODE" = "true" ]; then - verbose="--verbose" - fi - - BOOTSTRAP_CMD="${CDK_BOOTSTRAP_CMD}" - # Check if using custom command from config or default if [ "$BOOTSTRAP_CMD" = "npx cdk bootstrap" ]; then - # Default command - add AWS-specific arguments - role_args="" - if [ -n "$CFN_EXECUTION_ROLE" ]; then - role_args="--cloudformation-execution-policies $CFN_EXECUTION_ROLE" - fi - $BOOTSTRAP_CMD \ - aws://$(aws sts get-caller-identity --query Account --output text)/${AWS_REGION} \ - $role_args \ - $verbose + "aws://$(aws sts get-caller-identity --query Account --output text)/$AWS_REGION" \ + $FLAG_CFN_EXECUTION_POLICY \ + $FLAG_VERBOSE else - $BOOTSTRAP_CMD $EXTRA_ARGUMENTS $verbose + $BOOTSTRAP_CMD $EXTRA_ARGUMENTS $FLAG_VERBOSE fi echo "✅ CDK environment bootstrapped successfully" @@ -548,23 +410,18 @@ jobs: - name: Synthesize CDK application if: inputs.synth == true env: - DEBUG_MODE: ${{ inputs.debug }} - SYNTH_CMD: ${{ needs.prepare.outputs.cdk-synth-cmd }} - STACK_NAME: ${{ needs.prepare.outputs.stack-name }} - CONTEXT_ARGS: ${{ needs.prepare.outputs.context-args }} + CONTEXT_ARGS: ${{ steps.context-config.outputs.args }} EXTRA_ARGUMENTS: ${{ inputs.extra-arguments }} + FLAG_VERBOSE: ${{ case(inputs.debug == true, '--verbose', '') }} + STACK_NAME: ${{ inputs.stack-name || vars.STACK_NAME }} + SYNTH_CMD: ${{ inputs.synth-command }} run: | echo "🔨 Synthesizing CDK application..." - verbose="" - if [ "$DEBUG_MODE" = "true" ]; then - verbose="--verbose" - fi - $SYNTH_CMD $STACK_NAME \ $CONTEXT_ARGS \ $EXTRA_ARGUMENTS \ - $verbose + $FLAG_VERBOSE echo "✅ CDK synthesis completed successfully" @@ -572,7 +429,7 @@ jobs: if: inputs.synth == true uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1 with: - name: cdk-synthesis-${{ needs.prepare.outputs.sanitised-cdk-stack-name }} + name: cdk-synthesis-${{ steps.sanitise.outputs.sanitised-cdk-stack-name }} path: cdk.out/ retention-days: 7 @@ -580,10 +437,10 @@ jobs: if: inputs.diff == true id: diff-analysis env: - DIFF_CMD: ${{ needs.prepare.outputs.cdk-diff-cmd }} - STACK_NAME: ${{ needs.prepare.outputs.stack-name }} - CONTEXT_ARGS: ${{ needs.prepare.outputs.context-args }} + CONTEXT_ARGS: ${{ steps.context-config.outputs.args }} + DIFF_CMD: ${{ inputs.diff-command }} EXTRA_ARGUMENTS: ${{ inputs.extra-arguments }} + STACK_NAME: ${{ inputs.stack-name || vars.STACK_NAME }} run: | echo "📊 Analysing deployment changes..." @@ -618,7 +475,7 @@ jobs: if: inputs.diff == true uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1 with: - name: deployment-diff-${{ needs.prepare.outputs.sanitised-cdk-stack-name }} + name: deployment-diff-${{ steps.sanitise.outputs.sanitised-cdk-stack-name }} path: deployment-diff.txt retention-days: 7 @@ -626,9 +483,9 @@ jobs: if: inputs.diff == true && github.event_name == 'pull_request' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 #v9.0.0 env: - STACK_NAME: ${{ needs.prepare.outputs.stack-name }} ENVIRONMENT: ${{ inputs.github-environment }} HAS_CHANGES: ${{ steps.diff-analysis.outputs.has-changes }} + STACK_NAME: ${{ inputs.stack-name || vars.STACK_NAME }} with: script: | const fs = require('fs'); @@ -724,17 +581,12 @@ jobs: if: inputs.deploy == true id: deploy env: - DEBUG_MODE: ${{ inputs.debug }} - DEPLOY_CMD: ${{ needs.prepare.outputs.cdk-deploy-cmd }} - STACK_NAME: ${{ needs.prepare.outputs.stack-name }} - CONTEXT_ARGS: ${{ needs.prepare.outputs.context-args }} + CONTEXT_ARGS: ${{ steps.context-config.outputs.args }} + DEPLOY_CMD: ${{ inputs.deploy-command }} EXTRA_ARGUMENTS: ${{ inputs.extra-arguments }} + FLAG_VERBOSE: ${{ case(inputs.debug == true, '--verbose', '') }} + STACK_NAME: ${{ inputs.stack-name || vars.STACK_NAME }} run: | - verbose="" - if [ "$DEBUG_MODE" = "true" ]; then - verbose="--verbose" - fi - echo "🚀 Deploying CDK stack: $STACK_NAME" $DEPLOY_CMD $STACK_NAME \ @@ -742,7 +594,7 @@ jobs: $EXTRA_ARGUMENTS \ --require-approval never \ --outputs-file stack-outputs.json \ - $verbose + $FLAG_VERBOSE # Extract stack outputs if [ -f "stack-outputs.json" ]; then @@ -760,19 +612,19 @@ jobs: if: inputs.deploy == true && steps.deploy.outputs.status == 'success' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1 with: - name: deployment-outputs-${{ needs.prepare.outputs.sanitised-cdk-stack-name }} + name: deployment-outputs-${{ steps.sanitise.outputs.sanitised-cdk-stack-name }} path: stack-outputs.json retention-days: 7 - name: Display deployment summary if: inputs.deploy == true && steps.deploy.outputs.status == 'success' env: - STACK_NAME: ${{ needs.prepare.outputs.stack-name }} - ENVIRONMENT: ${{ inputs.github-environment }} AWS_REGION: ${{ inputs.aws-region }} DEPLOY_STATUS: ${{ steps.deploy.outputs.status }} - NODE_VERSION: ${{ needs.prepare.outputs.node-version }} - PACKAGE_MANAGER: ${{ needs.prepare.outputs.package-manager }} + ENVIRONMENT: ${{ inputs.github-environment }} + NODE_VERSION: ${{ steps.node-version.outputs.version }} + PACKAGE_MANAGER: ${{ steps.detect-package-manager.outputs.manager }} + STACK_NAME: ${{ inputs.stack-name || vars.STACK_NAME }} run: | echo "📋 Deployment Summary" echo "===================="