Skip to content

IaC Promotion And PR Plans

Use this for a repo like forge-tenants-iac-aws or forge-infra-iac-aws: pull requests run plans, merges to main run applies, and all Terragrunt units are discovered from the folder tree.

Change these values before copying:

Value Where
k8s Runner label
dev, prod GitHub environment names and Terraform environment folders
eu-west-1 Default region
000000000000 AWS account IDs
forge-dev, forge-prod AWS profile names
owner Role name

Create .github/workflows/regression-tests.yml:

---
# yamllint disable rule:comments
# yamllint disable rule:truthy
name: Regression tests

on:
  pull_request:
    types:
      - opened
      - synchronize
      - reopened
    branches:
      - main
    paths:
      - .github/actions/**
      - .github/workflows/**
      - release_versions.y*ml
      - terraform/**

permissions: {}

concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

jobs:
  terragrunt-plan:
    name: Terragrunt plan - ${{ matrix.env }}
    permissions:
      contents: read
      pull-requests: write
    strategy:
      fail-fast: false
      matrix:
        include:
          - env: dev
            aws-region: eu-west-1
            aws-account-id: '000000000000'
            aws-profile: forge-dev
            tf-target-env: dev
          - env: prod
            aws-region: eu-west-1
            aws-account-id: '000000000000'
            aws-profile: forge-prod
            tf-target-env: prod
    uses: ./.github/workflows/rw-terragrunt.yml
    with:
      aws-region: ${{ matrix.aws-region }}
      aws-account-id: ${{ matrix.aws-account-id }}
      aws-profile: ${{ matrix.aws-profile }}
      role-name: owner
      tf-target-env: ${{ matrix.tf-target-env }}
      env: ${{ matrix.env }}
      run-apply: 'false'

Create .github/workflows/promotion.yml:

---
# yamllint disable rule:comments
# yamllint disable rule:truthy
name: Promotion

on:
  push:
    branches:
      - main
    paths:
      - .github/actions/**
      - .github/workflows/**
      - release_versions.y*ml
      - terraform/**
  workflow_dispatch:

permissions: {}

concurrency:
  group: forge-terraform-promotion
  cancel-in-progress: false

jobs:
  terragrunt-apply:
    name: Terragrunt apply - ${{ matrix.env }}
    permissions:
      contents: read
      pull-requests: write
    strategy:
      fail-fast: false
      matrix:
        include:
          - env: dev
            aws-region: eu-west-1
            aws-account-id: '000000000000'
            aws-profile: forge-dev
            tf-target-env: dev
          - env: prod
            aws-region: eu-west-1
            aws-account-id: '000000000000'
            aws-profile: forge-prod
            tf-target-env: prod
    uses: ./.github/workflows/rw-terragrunt.yml
    with:
      aws-region: ${{ matrix.aws-region }}
      aws-account-id: ${{ matrix.aws-account-id }}
      aws-profile: ${{ matrix.aws-profile }}
      role-name: owner
      tf-target-env: ${{ matrix.tf-target-env }}
      env: ${{ matrix.env }}
      run-apply: 'true'

Create .github/workflows/rw-terragrunt.yml:

---
# yamllint disable rule:comments
# yamllint disable rule:truthy
name: Reusable Terragrunt Workflow

on:
  workflow_call:
    inputs:
      aws-region:
        type: string
        required: true
      aws-account-id:
        type: string
        required: true
      aws-profile:
        type: string
        required: true
      role-name:
        type: string
        required: true
      tf-target-env:
        type: string
        required: true
      run-apply:
        type: string
        required: false
        default: 'false'
      env:
        type: string
        required: true

permissions: {}

jobs:
  discover:
    name: Discover Terragrunt units
    runs-on: k8s
    permissions:
      contents: read
    environment: ${{ inputs.run-apply == 'true' && inputs.env || '' }}
    outputs:
      units: ${{ steps.set-matrix.outputs.units }}
    steps:
      - name: Checkout repository
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false

      - id: set-matrix
        name: Build Terragrunt matrix
        shell: bash
        env:
          TF_TARGET_ENV: ${{ inputs.tf-target-env }}
        run: |
          set -euo pipefail

          tf_path="terraform/environments/${TF_TARGET_ENV}"
          if [[ ! -d "$tf_path" ]]; then
            echo "Missing Terragrunt environment path: $tf_path" >&2
            exit 1
          fi

          units_json=$(
            find "$tf_path" -name terragrunt.hcl -not -path '*/.terragrunt-cache/*' |
              sed 's#/terragrunt.hcl$##' |
              sort |
              jq -R -s -c 'split("\n") | map(select(length > 0)) | map({path: .})'
          )

          echo "$units_json" | jq .
          echo "units=$units_json" >> "$GITHUB_OUTPUT"

  terragrunt:
    name: Terragrunt - ${{ matrix.unit.path }}
    needs:
      - discover
    runs-on: k8s
    permissions:
      contents: read
      pull-requests: write
    environment: ${{ inputs.run-apply == 'true' && inputs.env || '' }}
    strategy:
      fail-fast: false
      max-parallel: 1
      matrix:
        unit: ${{ fromJSON(needs.discover.outputs.units) }}
    steps:
      - name: Checkout repository
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false

      - name: Run Terragrunt
        uses: ./.github/actions/terragrunt-deployment-action
        with:
          aws-region: ${{ inputs.aws-region }}
          aws-account-id: ${{ inputs.aws-account-id }}
          aws-profile: ${{ inputs.aws-profile }}
          role-name: ${{ inputs.role-name }}
          tf-target-env: ${{ inputs.tf-target-env }}
          tf-path: ${{ matrix.unit.path }}
          run-apply: ${{ inputs.run-apply }}

Expected Repo Layout

terraform/
└── environments/
    ├── dev/
    │   └── regions/
    │       └── eu-west-1/
    └── prod/
        └── regions/
            └── eu-west-1/

Every folder containing terragrunt.hcl becomes one matrix item. Keep max-parallel: 1 until the repo has explicit dependency-safe grouping.