Skip to content

PR

This is the core workflow that orchestrates other reusable workflows to ensure all required checks run in each PR. This is a combination of:

Note

You may notice OpenTofu Apply (tofu apply) hasn't been mentioned above. This is because this is done following a merge to the base branch using GitHub Environments!

The workflow is presented below:

.github/workflows/pr.yml
name: PR

on:
  pull_request:
    types: [opened, reopened, synchronize]
    branches:
      - dev
      - main
    paths:
      - 'tofu/**'

permissions:
  contents: read

defaults:
  run:
    shell: bash

jobs:
  tofu-checks:
    permissions:
      contents: read
      id-token: write
      pull-requests: write
    uses: ./.github/workflows/tofu_checks.yml
    with:
      tf_var_file: "${{ github.base_ref == 'main' && 'prod' || 'dev' }}.tfvars"

  tofu-plan:
    permissions:
      contents: read
      id-token: write
      pull-requests: write
    uses: ./.github/workflows/tofu_plan.yml
    with:
      environment: ${{ github.base_ref == 'main' && 'prod' || 'dev' }}
      tf_var_file: "${{ github.base_ref == 'main' && 'prod' || 'dev' }}.tfvars"

  sast-checks:
    uses: ./.github/workflows/sast.yml

  linters:
    permissions:
      contents: read
      id-token: write
      pull-requests: write
    uses: ./.github/workflows/linters.yml

  terraform-docs:
    uses: ./.github/workflows/terraform_docs.yml
    with:
      config_file: ".terraform-docs.yml"
      directory: "tofu"
      tfdoc_version: "v0.19.0"

  infracost:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
    steps:
      - name: Setup Infracost
        uses: infracost/actions/setup@e9d6e6cd65e168e76b0de50ff9957d2fe8bb1832 # v3.0.1
        with:
          api-key: ${{ secrets.INFRACOST_API_KEY }}

      - name: Checkout base branch
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          ref: '${{ github.event.pull_request.base.ref }}'

      - name: Generate Infracost cost estimate baseline
        run: |
          infracost breakdown --path=. \
                              --format=json \
                              --out-file=/tmp/infracost-base.json \
                              --terraform-var-file "${{ github.base_ref == 'main' && 'prod' || 'dev' }}.tfvars"
        working-directory: ${{ github.workspace }}/tofu

      # Checkout the current PR branch so we can create a diff.
      - name: Checkout PR branch
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

      # Generate an Infracost diff and save it to a JSON file.
      - name: Generate Infracost diff
        run: |
          infracost diff --path=. \
                          --format=json \
                          --compare-to=/tmp/infracost-base.json \
                          --out-file=/tmp/infracost.json \
                          --terraform-var-file "${{ github.base_ref == 'main' && 'prod' || 'dev' }}.tfvars"
        working-directory: ${{ github.workspace }}/tofu

      - name: Post Infracost comment
        run: |
            infracost comment github --path=/tmp/infracost.json \
                                      --repo=$GITHUB_REPOSITORY \
                                      --github-token=${{ github.token }} \
                                      --pull-request=${{ github.event.pull_request.number }} \
                                      --behavior=update \
                                      --policy-path ./policies/cost.rego

Configuration

As this is the first Action we're seeing. Lets breakdown some commonality you'll see amongst all workflows:

Name

name: PR

Quite self explanatory - name of the GitHub Action.

On

1
2
3
4
5
6
7
8
on:
  pull_request:
    types: [opened, reopened, synchronize]
    branches:
      - dev
      - main
    paths:
      - 'tofu/**'

Without starting a fight on the right way of defining a list in yaml so I did both 😁, this workflow only runs under the following conditions:

  1. Is a pull request
  2. Whether that pull request is opened, reopened or synchronised (synchronize)
  3. If the target branch is either dev or main
  4. Only if the pull request has changes to anything inside the tofu/ directory

Info

All of these conditions must be true before the Action will execute!

Permissions

permissions:
  contents: read

permissions can be set at the workflow level and job level. With the job level taking precedence. This is a basic permissions, which gives the workflow permission to, say, use the checkout GitHub Action to pull the code down.

Defaults

1
2
3
defaults:
  run:
    shell: bash

defaults is used to set any default for all jobs and steps. In this case, it will ensure each step/command runs in the bash shell.

Jobs

# <truncated>
jobs:
  tofu-checks:
    permissions:
      contents: read
      id-token: write
      pull-requests: write
    uses: ./.github/workflows/tofu_checks.yml
    with:
      tf_var_file: "${{ github.base_ref == 'main' && 'prod' || 'dev' }}.tfvars"
# <truncated>

I won't go into too much detail about what jobs are and all the available arguments. You can find all that information out here: https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobs.

I will cover this first one, and then the rest of the document will follow the order of the PR workflow jobs covering each workflow and its intended purpose.

Job ID

tofu-checks is the name of the specific job.

Job Permissions

This reusable action also requires additional permissions, so like I mentioned before I have specified permissions at the job level - which takes precedence.

  1. contents is set at read so the PR can be checked out
  2. id-token is set as write so the JWT (JSON Web Token using OIDC (OpenID Connect)) can be requested to authenticate with AWS (this action requires tofu init to be ran)
  3. pull-requests is set as write as it will post a comment to the PR with status checks

Warning

I could technically place this at the root of the workflow, but that would mean ALL jobs would have these permissions. To adhere to the principal of least privilege, I'm only specifying additional permissions for each job that actually needs them!

Uses

The uses keyword points to another workflow (reusable) to run. It is defined at the following path: ./.github/workflows/tofu_checks.yml

Info

You might notice that a commit sha, or version is not specified. In this case, the workflow in GitHub Actions shows the following: kieran-lowe/gitops-2024/.github/workflows/tofu_plan.yml@refs/pull/57/merge - as this workflow is part of my repo anyway, I haven't specified anything:

uses: ./.github/workflows/tofu_checks.yml

With

The with keywords allows you to pass in inputs to the reusable workflow, in this case tf_var_file which the ./.github/workflows/tofu_checks.yml workflow expects.

with:
  tf_var_file: "${{ github.base_ref == 'main' && 'prod' || 'dev' }}.tfvars"

As there are two environments: dev and prod we use some conditional logic using the github context. The conditional works like this: ${{ condition && is_truthy || is_false }}

Now lets go into the jobs! We have 6 in total for this workflow:

  1. tofu-checks
  2. tofu-plan
  3. sast-checks
  4. linters
  5. terraform-docs
  6. infracost

Note

Each additional job/workflow has been documented in its own area to avoid making this page huge. The links for each are below!

tofu-checks

tofu-plan

sast-checks

linters

terraform-docs

infracost

Info

You may notice additional workflows in the navigation menu. These are not called by this main orchestrator and run on their own cadences!