Runs
More actions
Your workflow calls the action once in each job, with step set to the job it runs in. The apply job's environment has the same name as the workspace.
By default it applies after the merge, in HCP Terraform's order: a pull request only plans, and once it merges, the push to the default branch plans again and applies when the environment approves, or at once for a workspace in auto-apply. With apply-before-merge set to "true" on the pending, apply and merge steps, a pull request is applied first and merges once the apply succeeds.
Steps
step |
Job | What it does |
|---|---|---|
plan |
plan, before planning | Sets the plan output to "false" when a pull request changes nothing in the workspace's directory, the calling workflow or shared-paths, so the later steps can be skipped with if: steps.<id>.outputs.plan == 'true'. With require-up-to-date, refuses a pull request that is behind its base. Restores the providers dflook downloads, cached by the workspace's .terraform.lock.hcl, and saves them when the job ends.
|
changes |
plan, after planning | Marks the workspace as having changes to apply. |
pending |
one job after the plans | Lists the workspaces to apply, and refuses an environment that is missing, or has no required reviewers unless its workspace is in auto-apply. Cancels older runs of the same branch that still wait for approval, unless one of their jobs is running. On a pull request it lists nothing, unless apply-before-merge, and then fails a pull request high in a stack that changes more than one workspace.
|
apply |
apply, first step | Restores the providers the plan job cached, so the apply does not download them again. Records who approved, or that nobody had to. On a push, refuses unless the commit is still the head of the branch. On a pull request, refuses unless apply-before-merge, and then refuses when the pull request moved or merged, targets another branch outside a native stack, or with require-up-to-date fell behind.
|
result |
apply, last step, with if: always() |
Writes what the apply changed and whether it succeeded. |
gate |
the required check | Fails unless every job it needs succeeded. The apply job may be skipped. |
merge |
after the gate, with apply-before-merge |
Merges the pull request through the async merge API, which also merges every pull request below it in a stack. When this run has nothing to apply, merges only if an earlier run of it was approved or applied an auto-apply workspace. Fails when the pull request has not merged 2 minutes after the apply, since production is then ahead of the default branch. Does nothing without apply-before-merge.
|
collapse |
plan, last step, with if: always() |
Hides, as outdated, every plan comment of dflook/tofu-plan or dflook/terraform-plan but the newest of each workspace. Comments starting with **<workspace> apply: are hidden the same way.
|
Plan comments
The plan job's dflook/tofu-plan comments each workspace's plan on the pull request. Pushing again plans again, and the collapse step hides the older plan comments as outdated, so only the newest of each workspace stays open.
The apply job should make a fresh plan and apply it only when it matches the approved one, as dflook/tofu-apply does without plan_path or auto_approve. On a push, dflook's plan updates the merged pull request's plan comment with the plan of the default branch, and the apply compares against that comment.
Which commit applies
Only the newest commit of the default branch applies, since its plan carries the changes of every commit before it that has not been applied. So the push run has to plan every workspace that has changes, and the plan step's plan output is always "true" outside a pull request. A change made since the plan, such as a workflow_dispatch apply, makes the fresh plan differ and stops the apply; "Re-run all jobs" plans again. A commit pushed to the default branch without a pull request has no plan comment, so apply it with workflow_dispatch.
Job summaries
Each step writes a summary on the run page, so a reviewer can see what they are approving without opening the logs.
step |
Summary |
|---|---|
changes |
How many resources the plan adds, changes, destroys, moves and imports, the plan itself folded, and whether it waits for an approval. |
pending |
Each environment to apply and who can approve it. |
apply |
Who approved, or who started a run that needed no approval, and the commit it applies. |
result |
Whether the apply succeeded, with the same counts and plan as changes.
|
gate |
Each job it needs and how it ended. |
merge |
Whether the pull request merged, and if not, why. |
Full workflow
This is the workflow of Get started with every step that runs after the merge. A workflow_dispatch run applies the chosen workspace from its branch once the environment approves, the collapse step hides outdated plan comments, and the job summaries show each plan folded.
name: tofu
on:
pull_request:
push:
branches: [main]
workflow_dispatch:
inputs:
workspace:
description: The workspace to apply from this branch, once its environment approves.
required: true
type: choice
options: &workspaces [network, dns]
permissions: {}
jobs:
plan:
name: ${{ matrix.workspace }} plan
if: github.event_name != 'workflow_dispatch'
strategy:
fail-fast: false
matrix:
workspace: *workspaces
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: femiwiki/terraform-github-tacos@v0.7.0
with:
step: plan
workspace: ${{ matrix.workspace }}
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: dflook/tofu-plan@c7416238983bdd94c5486fb856bb7358f2bcc935 # v3.1.0
id: plan
env:
GITHUB_TOKEN: ${{ github.token }}
with:
path: ${{ matrix.workspace }}
- uses: femiwiki/terraform-github-tacos@v0.7.0
with:
step: changes
workspace: ${{ matrix.workspace }}
changes: ${{ steps.plan.outputs.changes }}
plan-json: ${{ steps.plan.outputs.json_plan_path }}
plan-text: ${{ steps.plan.outputs.text_plan_path }}
- uses: femiwiki/terraform-github-tacos@v0.7.0
if: always()
with:
step: collapse
pending:
needs: plan
if: >-
!cancelled() && (github.event_name == 'workflow_dispatch'
|| (github.event_name == 'push' && needs.plan.result == 'success'))
runs-on: ubuntu-latest
permissions:
actions: write
checks: read
outputs:
workspaces: ${{ steps.pending.outputs.workspaces }}
steps:
- uses: femiwiki/terraform-github-tacos@v0.7.0
id: pending
with:
step: pending
workspaces: ${{ inputs.workspace && format('["{0}"]', inputs.workspace) || '' }}
apply:
name: ${{ matrix.workspace }} apply
needs: pending
if: >-
!cancelled() && needs.pending.result == 'success'
&& needs.pending.outputs.workspaces != '[]'
strategy:
fail-fast: false
matrix:
workspace: ${{ fromJSON(needs.pending.outputs.workspaces) }}
runs-on: ubuntu-latest
environment: ${{ matrix.workspace }}
permissions:
actions: read
contents: read
pull-requests: write
steps:
- uses: femiwiki/terraform-github-tacos@v0.7.0
id: approver
with:
step: apply
workspace: ${{ matrix.workspace }}
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: dflook/tofu-apply@e3e5dfc8f935d4b152c767801046f63abf8a77d3 # v3.1.0
id: apply
env:
GITHUB_TOKEN: ${{ github.token }}
with:
path: ${{ matrix.workspace }}
auto_approve: ${{ github.event_name == 'workflow_dispatch' }}
- uses: femiwiki/terraform-github-tacos@v0.7.0
if: always()
with:
step: result
workspace: ${{ matrix.workspace }}
outcome: ${{ steps.apply.outcome }}
plan-json: ${{ steps.apply.outputs.json_plan_path }}
plan-text: ${{ steps.apply.outputs.text_plan_path }}
approver: ${{ steps.approver.outputs.approver }}
# Make this the one required check
gate:
name: ${{ github.event_name == 'pull_request' && 'tofu gate' || 'tofu gate (pull requests only)' }}
needs: plan
if: always() && github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions: {}
steps:
- uses: femiwiki/terraform-github-tacos@v0.7.0
with:
step: gate
needs: ${{ toJSON(needs) }}