CI/CD Workflows

Understand what happens after you push, how environments deploy in order, and how to interact with the GitHub Actions pipelines.


Overview

The starter kit ships with GitHub Actions workflows generated by Projen so every merge to your default branch produces a deterministic deploy. This guide explains the lifecycle from merge to rollout and how to interact with the pipelines.

All workflows are automatically generated from the configuration in .projenrc.ts by the createCdkDeploymentWorkflows and createCdkDiffPrWorkflow helper functions. See the Reference: Helpers page for implementation details.

Prerequisites

Before the workflows can function properly, you need to configure GitHub settings and secrets:

1. Create PROJEN_GITHUB_TOKEN

The build.yml and auto-approve.yml workflows require a Personal Access Token (PAT) to push commits and enable auto-merge.

Steps to create the token:

  1. Go to GitHub → SettingsDeveloper settingsPersonal access tokensTokens (classic)
  2. Click Generate new token (classic)
  3. Configure the token:
    • Note: PROJEN_GITHUB_TOKEN for AWS CDK Starter Kit
    • Expiration: Choose your preferred expiration (recommendation: 90 days or 1 year)
    • Scopes: Select:
      • repo (Full control of private repositories)
      • workflow (Update GitHub Action workflows)
  4. Click Generate token and copy the token value

Add the token to your repository:

  1. Go to your repository → SettingsSecrets and variablesActions
  2. Click New repository secret
  3. Configure the secret:
    • Name: PROJEN_GITHUB_TOKEN
    • Secret: Paste the token value you copied
  4. Click Add secret

2. Enable GitHub Actions permissions

The auto-approve.yml workflow needs permission to create and approve pull requests.

Steps to enable:

  1. Go to your repository → SettingsActionsGeneral
  2. Scroll to Workflow permissions
  3. Enable: ✅ Allow GitHub Actions to create and approve pull requests
  4. Click Save

Direct link for your repository:

https://github.com/<your-org>/<your-repo>/settings/actions

:::info Without these settings, the build workflow's self-mutation job will fail to push commits, and the auto-approve workflow won't be able to enable auto-merge for Dependabot PRs. :::

Trigger flow

  1. Push or merge into main: Projen creates a workflow named cdk-deploy-<env> for each environment. The first environment in environmentConfigs (typically test) runs on every push to main.
  2. Environment chaining: Subsequent environments trigger via workflow_run when the previous deploy finishes successfully. For example, cdk-deploy-production waits for cdk-deploy-test to succeed before starting.
  3. Pull requests: Branch deploy workflows named cdk-deploy-<env>-branch run when enableBranchDeploy is true for that environment. They deploy stacks with branch suffixes so previews stay isolated. Cleanup is handled by cdk-destroy-<env>-branch on branch deletion or PR close.
  4. CDK diff on pull requests: The cdk-diff-pr-comment workflow runs automatically on every pull request targeting main. It generates a CDK diff comparing your PR branch against the production environment and posts the results as a comment directly in the pull request. This gives reviewers visibility into infrastructure changes before merging.
  5. Manual runs: Every workflow exposes the workflow_dispatch trigger, enabling re-runs or ad-hoc deploys straight from the GitHub Actions UI.

Workflow configurations

All workflows in .github/workflows/ are generated by Projen based on your environment configuration. The sections below explain the default setup for each workflow type.

Environment deployment workflow

File: .github/workflows/cdk-deploy-<env>.yml (e.g., cdk-deploy-test.yml)

Purpose: Deploys CDK stacks to a specific environment on push to main or when the previous environment completes successfully.

Example configuration for test environment:

name: cdk-deploy-test
on:
  workflow_dispatch: {}        # Manual trigger from GitHub UI
  push:
    branches:
      - main                   # First environment triggers on push to main
  # OR for subsequent environments:
  # workflow_run:
  #   workflows: [cdk-deploy-test]
  #   types: [completed]       # Waits for previous environment to complete

concurrency:
  group: ${{ github.workflow }}-${{ github.ref_name }}
  cancel-in-progress: false    # Prevents concurrent deployments

jobs:
  deploy:
    name: Deploy CDK stacks to test AWS account
    runs-on: ubuntu-latest
    permissions:
      contents: read           # Read repository contents
      id-token: write          # Required for OIDC authentication
    environment: test          # GitHub environment for protection rules
    steps:
      - name: Checkout repository
        uses: actions/checkout@v5

      - name: Setup nodejs environment
        uses: actions/setup-node@v6
        with:
          node-version: ">=22.18.0"
          cache: npm

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::987654321012:role/GitHubActionsServiceRole
          aws-region: us-east-1

      - name: Install dependencies
        run: npm ci

      - name: Run CDK synth for the TEST environment
        run: npm run test:synth

      - name: Deploy CDK to the TEST environment on AWS account 987654321012
        run: npm run test:deploy:all

Key configuration details:

  • Trigger: First environment (test) triggers on push to main. Subsequent environments use workflow_run to chain after the previous environment completes.
  • Concurrency: Prevents multiple deployments to the same environment simultaneously with cancel-in-progress: false.
  • Permissions: Minimal permissions—contents: read for checkout and id-token: write for OIDC authentication.
  • Environment: Uses GitHub environments for protection rules and deployment tracking.
  • OIDC Role: Assumes the GitHubActionsServiceRole created by the FoundationStack for secure, keyless authentication.
  • Tasks: Generated by addCdkActionTask in .projenrc.ts:165. The test:synth and test:deploy:all commands are created with environment-specific variables.

Generation: Created by createCdkDeploymentWorkflow in src/bin/cicd-helper.ts:146-217.

Branch deployment workflow

File: .github/workflows/cdk-deploy-<env>-branch.yml (e.g., cdk-deploy-test-branch.yml)

Purpose: Deploys feature branch stacks with branch-suffixed names for isolated testing.

Example configuration:

name: cdk-deploy-test-branch
on:
  workflow_dispatch: {}
  push:
    branches:
      - "**"                   # All branches
      - "!main"                # Except main
      - "!hotfix/*"            # Except hotfix branches
      - "!github-actions/*"    # Except GitHub Actions branches
      - "!dependabot/**"       # Except Dependabot branches

concurrency:
  group: ${{ github.workflow }}-${{ github.ref_name }}
  cancel-in-progress: false

jobs:
  deploy:
    name: Deploy CDK stacks to test AWS account (Branch)
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    environment: test
    steps:
      - name: Checkout repository
        uses: actions/checkout@v5

      - name: Setup nodejs environment
        uses: actions/setup-node@v6
        with:
          node-version: ">=22.18.0"
          cache: npm

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::987654321012:role/GitHubActionsServiceRole
          aws-region: us-east-1

      - name: Install dependencies
        run: npm ci

      - name: Run CDK synth for the TEST environment
        run: npm run test:branch:synth

      - name: Deploy CDK to the TEST environment on AWS account 987654321012
        run: npm run test:branch:deploy:all

Key configuration details:

  • Trigger: Runs on push to any branch except main, hotfix/*, github-actions/*, and dependabot/**.
  • Branch tasks: Uses test:branch:synth and test:branch:deploy:all tasks that include the GIT_BRANCH_REF environment variable.
  • Stack naming: The GIT_BRANCH_REF variable is processed by extractCleanedBranchName to create a sanitized suffix (lowercase, max 25 chars, alphanumeric + hyphens).
  • Resource naming: Stack names are generated by createEnvResourceName with the branch suffix (e.g., MyStack-feature123).

Generation: Created by createCdkDeploymentWorkflow in src/bin/cicd-helper.ts:146-217 when deployForBranch=true.

Branch destroy workflow

File: .github/workflows/cdk-destroy-<env>-branch.yml (e.g., cdk-destroy-test-branch.yml)

Purpose: Removes branch deployment stacks when branches are deleted, PRs are closed, or manually triggered.

Example configuration:

name: cdk-destroy-test-branch
on:
  workflow_dispatch: {}        # Manual cleanup
  delete:
    branches:
      - "**"                   # All branches
      - "!main"                # Except main
      - "!hotfix/*"            # Except hotfix branches
      - "!github-actions/*"    # Except GitHub Actions branches
      - "!dependabot/**"       # Except Dependabot branches

jobs:
  destroy:
    name: Remove deployment of feature branch
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    environment: test
    if: github.head_ref != 'main' || (github.event.ref_type == 'branch' && github.event_name == 'delete') || github.event_name == 'workflow_dispatch'
    steps:
      - name: Checkout repository
        uses: actions/checkout@v5

      - name: Setup nodejs environment
        uses: actions/setup-node@v6
        with:
          node-version: ">=22.18.0"
          cache: npm

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::987654321012:role/GitHubActionsServiceRole
          aws-region: us-east-1

      - name: Install dependencies
        run: npm ci

      - name: Fetch Deleted Branch Name
        id: destroy-branch
        if: github.event.ref_type == 'branch' && github.event_name == 'delete'
        run: BRANCH=$(cat ${{ github.event_path }} | jq --raw-output '.ref'); echo "${{ github.repository }} has ${BRANCH} branch"; echo "DESTROY_BRANCH_NAME=$BRANCH" >> $GITHUB_OUTPUT

      - name: Destroy Branch Stack (Workflow Dispatch)
        if: github.event_name == 'workflow_dispatch'
        env:
          GIT_BRANCH_REF: ${{ github.ref_name }}
        run: npm run test:branch:destroy:all

      - name: Destroy Branch Stack (Branch Deletion)
        if: github.event.ref_type == 'branch' && github.event_name == 'delete'
        env:
          GIT_BRANCH_REF: ${{ steps.destroy-branch.outputs.DESTROY_BRANCH_NAME }}
        run: npm run test:branch:destroy:all

      - name: Destroy Branch Stack (Pull Request Closure)
        if: github.event_name == 'pull_request'
        env:
          GIT_BRANCH_REF: ${{ github.head_ref }}
        run: npm run test:branch:destroy:all

Key configuration details:

  • Triggers: Three scenarios—manual dispatch, branch deletion, or PR closure.
  • Branch extraction: For delete events, uses jq to extract the branch name from the GitHub event payload.
  • Conditional steps: Each destroy step runs conditionally based on the trigger event type.
  • Safety check: Job-level if condition prevents accidental main branch destruction.

Generation: Created by createCdkDestroyWorkflow in src/bin/cicd-helper.ts:235-303.

CDK diff PR comment workflow

File: .github/workflows/cdk-diff-pr-comment.yml

Purpose: Generates and posts CDK diff output as a PR comment for infrastructure change visibility.

Configuration:

name: cdk-diff-pr-comment
on:
  pull_request_target:         # Allows access to secrets while checking out PR code
    branches:
      - main

jobs:
  deploy:
    name: CDK diff PR branch with production environment (via main)
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
      pull-requests: write     # Required to post comments
    env:
      AWS_REGION: us-east-1
    steps:
      - name: Checkout repository
        uses: actions/checkout@v5
        with:
          ref: ${{ github.event.pull_request.head.sha }}  # Checkout PR branch

      - name: Setup nodejs environment
        uses: actions/setup-node@v6
        with:
          node-version: ">=22.18.0"
          cache: npm

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsServiceRole
          aws-region: us-east-1

      - name: Install dependencies
        run: npm ci

      - name: CDK diff and notify PR
        run: npm run production:diff:all > cdk-diff-output.txt 2>&1 || true

      - name: Post CDK Diff Comment in PR
        uses: towardsthecloud/aws-cdk-diff-pr-commenter@v1
        with:
          diff-file: cdk-diff-output.txt
          aws-region: us-east-1
          header: CDK Diff for production in us-east-1

Key configuration details:

  • Trigger: Uses pull_request_target for secure access to repository secrets while checking out PR code.
  • PR checkout: Explicitly checks out the PR branch SHA using github.event.pull_request.head.sha.
  • Diff environment: Compares against the highest environment (usually production) as defined in .projenrc.ts:199.
  • Diff capture: Redirects both stdout and stderr to cdk-diff-output.txt with || true to prevent failures from blocking the workflow.
  • Comment action: Uses towardsthecloud/aws-cdk-diff-pr-commenter@v1 to post formatted diff output as a PR comment.
  • Permissions: Requires pull-requests: write to post comments on PRs.

Generation: Created by createCdkDiffPrWorkflow in src/bin/cicd-helper.ts:29-94.

Build workflow

File: .github/workflows/build.yml

Purpose: Validates pull requests by running the Projen build process and detecting file mutations. Ensures generated files stay in sync with .projenrc.ts.

Configuration:

name: build
on:
  pull_request: {}
  workflow_dispatch: {}

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    env:
      CI: "true"
    steps:
      - name: Checkout
        uses: actions/checkout@v5
        with:
          ref: ${{ github.event.pull_request.head.ref }}
          repository: ${{ github.event.pull_request.head.repo.full_name }}

      - name: Setup Node.js
        uses: actions/setup-node@v5
        with:
          node-version: 22.18.0

      - name: Install dependencies
        run: npm install

      - name: build
        run: npx projen build

      - name: Find mutations
        id: self_mutation
        run: |-
          git add .
          git diff --staged --patch --exit-code > repo.patch || echo "self_mutation_happened=true" >> $GITHUB_OUTPUT

      - name: Upload patch
        if: steps.self_mutation.outputs.self_mutation_happened
        uses: actions/upload-artifact@v4.6.2
        with:
          name: repo.patch
          path: repo.patch
          overwrite: true

      - name: Fail build on mutation
        if: steps.self_mutation.outputs.self_mutation_happened
        run: |-
          echo "::error::Files were changed during build (see build log). If this was triggered from a fork, you will need to update your branch."
          cat repo.patch
          exit 1

  self-mutation:
    needs: build
    runs-on: ubuntu-latest
    permissions:
      contents: write
    if: always() && needs.build.outputs.self_mutation_happened && !(github.event.pull_request.head.repo.full_name != github.repository)
    steps:
      - name: Checkout
        uses: actions/checkout@v5
        with:
          token: ${{ secrets.PROJEN_GITHUB_TOKEN }}
          ref: ${{ github.event.pull_request.head.ref }}
          repository: ${{ github.event.pull_request.head.repo.full_name }}

      - name: Download patch
        uses: actions/download-artifact@v5
        with:
          name: repo.patch
          path: ${{ runner.temp }}

      - name: Apply patch
        run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."'

      - name: Set git identity
        run: |-
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

      - name: Push changes
        env:
          PULL_REQUEST_REF: ${{ github.event.pull_request.head.ref }}
        run: |-
          git add .
          git commit -s -m "chore: self mutation"
          git push origin "HEAD:$PULL_REQUEST_REF"

Key configuration details:

  • Trigger: Runs on all pull requests and supports manual dispatch
  • Build job: Runs npx projen build to compile TypeScript and regenerate files
  • Mutation detection: Checks if the build modified any files (indicates .projenrc.ts changes weren't committed)
  • Self-mutation job: Automatically commits and pushes changes back to the PR branch if mutations are detected
  • PROJEN_GITHUB_TOKEN: Required for the self-mutation job to push commits. See Prerequisites for setup instructions.
  • Fork handling: Self-mutation is skipped for PRs from forks (requires manual updates)

Generation: Created by Projen's built-in build workflow generator (enabled by default in AwsCdkTypeScriptApp).

Auto-approve workflow

File: .github/workflows/auto-approve.yml

Purpose: Automatically approves and enables auto-merge for Dependabot pull requests with the auto-approve label.

Configuration:

name: auto-approve
on:
  pull_request_target:
    types:
      - labeled
      - opened
      - synchronize
      - reopened
      - ready_for_review

jobs:
  approve:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
      contents: write
    if: contains(github.event.pull_request.labels.*.name, 'auto-approve') && (github.event.pull_request.user.login == 'dependabot' || github.event.pull_request.user.login == 'dependabot[bot]')
    steps:
      - uses: hmarr/auto-approve-action@f0939ea97e9205ef24d872e76833fa908a770363
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}

      - name: Checkout
        uses: actions/checkout@v5

      - name: Enable Pull Request Automerge
        run: gh pr merge --merge --auto "${{ github.event.pull_request.number }}"
        env:
          GH_TOKEN: ${{ secrets.PROJEN_GITHUB_TOKEN }}

Key configuration details:

  • Trigger: Runs on pull_request_target for Dependabot PRs with specific events
  • Conditions: Only runs for Dependabot PRs with the auto-approve label
  • Auto-approve: Uses hmarr/auto-approve-action to approve the PR
  • Auto-merge: Uses GitHub CLI to enable auto-merge on the PR
  • PROJEN_GITHUB_TOKEN: Required for enabling auto-merge. See Prerequisites for setup instructions.
  • Permissions: Requires pull-requests: write and contents: write

Generation: Created by Projen's auto-approve workflow with custom merge step added in .projenrc.ts:119-135.

Pull request lint workflow

File: .github/workflows/pull-request-lint.yml

Purpose: Validates that pull request titles follow Conventional Commits format.

Configuration:

name: pull-request-lint
on:
  pull_request_target:
    types:
      - labeled
      - opened
      - synchronize
      - reopened
      - ready_for_review
      - edited
  merge_group: {}

jobs:
  validate:
    name: Validate PR title
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
    if: (github.event_name == 'pull_request' || github.event_name == 'pull_request_target')
    steps:
      - uses: amannn/action-semantic-pull-request@v6
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          types: |-
            feat
            fix
            chore
          requireScope: false

Key configuration details:

  • Trigger: Runs on PR events and when PRs are edited
  • Validation: Enforces Conventional Commits format with types: feat, fix, chore
  • Scope: Scope is optional (requireScope: false)
  • Examples of valid titles:
    • feat: add user authentication
    • fix: resolve deployment timeout
    • chore(deps): update dependencies

Generation: Created by Projen's built-in pull request lint workflow (enabled by default).

Common workflow steps

All deployment workflows share these common steps, generated by getCommonWorkflowSteps in src/bin/cicd-helper.ts:362-378:

  1. Checkout repository: Uses actions/checkout@v5 to clone the repository code.
  2. Setup Node.js: Uses actions/setup-node@v6 with the version from .projenrc.ts:9 (currently 22.18.0) and enables npm caching.
  3. Configure AWS credentials: Uses aws-actions/configure-aws-credentials@v4 to assume the OIDC role for keyless authentication.
  4. Install dependencies: Runs npm ci for reproducible dependency installation from package-lock.json.

How workflows are generated

All GitHub Actions workflows are generated from the configuration in .projenrc.ts. Here's how the generation works:

1. Environment configuration

Define your environments in .projenrc.ts:149-152:

const environmentConfigs: (EnvironmentConfig & { name: Environment })[] = [
  { name: 'test', accountId: '987654321012', enableBranchDeploy: true },
  { name: 'production', accountId: '123456789012', enableBranchDeploy: false },
];

2. Task generation

For each environment, addCdkActionTask (.projenrc.ts:165) creates npm tasks with environment-specific variables:

addCdkActionTask(project, {
  CDK_DEFAULT_ACCOUNT: config.accountId,
  CDK_DEFAULT_REGION: awsRegion,
  ENVIRONMENT: config.name,
  GITHUB_DEPLOY_ROLE: githubRole,
  GIT_BRANCH_REF: '$(echo ${GIT_BRANCH_REF:-$(git rev-parse --abbrev-ref HEAD)})', // For branch deploys
});

This generates tasks like:

  • npm run test:synth - Synthesize test environment stacks
  • npm run test:deploy:all - Deploy all test environment stacks
  • npm run test:branch:deploy:all - Deploy all test environment stacks with branch suffix
  • npm run test:branch:destroy:all - Destroy all test environment branch stacks

See addCdkActionTask in src/bin/env-helper.ts:69-116 for the complete task generation logic.

3. Workflow generation

The createCdkDeploymentWorkflows function (.projenrc.ts:184-193) generates GitHub Actions workflows for each environment:

createCdkDeploymentWorkflows(
  project.github,
  config.accountId,
  awsRegion,
  config.name,
  githubRole,
  nodeVersion,
  config.enableBranchDeploy,  // Controls branch deploy workflow generation
  orderedEnvironments,         // Determines workflow chaining order
);

This creates:

  • cdk-deploy-<env>.yml - Main deployment workflow
  • cdk-deploy-<env>-branch.yml - Branch deployment workflow (if enableBranchDeploy: true)
  • cdk-destroy-<env>-branch.yml - Branch cleanup workflow (if enableBranchDeploy: true)

The CDK diff PR workflow is created separately by createCdkDiffPrWorkflow (.projenrc.ts:197-204), which compares against the highest environment (usually production).

4. Regenerate workflows

After modifying .projenrc.ts, run:

npx projen

This regenerates all workflow files in .github/workflows/ based on your updated configuration.

Monitoring and troubleshooting

  • GitHub Actions UI: track the workflow progress directly in Actions. Logs show the exact Projen task executing so you can reproduce locally.
  • CloudFormation stack events: The stack names match createEnvResourceName('StackName'), so failures are easy to locate in the AWS console.
  • Rerunning: Use workflow_dispatch or the "Re-run jobs" button after fixing issues. Just ensure the branch you rerun from still matches the intended state.

Before you push

  • Run through the Local Development guide to synth/diff locally and keep formatting tidy.
  • If you changed .projenrc.ts, run npx projen so the generated workflows stay in sync with the new environment layout.

Advanced configuration

  • Edit environmentConfigs in .projenrc.ts to change deployment order, AWS accounts, or to opt environments in/out of branch deploys.
  • Need additional stages (for example, staging)? Add them to the array, rerun npx projen, and new cdk-deploy-<env> workflows will appear automatically.
  • To adjust step logic or concurrency, modify createCdkDeploymentWorkflows and rerun Projen.

Ready to experiment with feature branches? Continue with Branch-based Development.