Skip to content

How it works

The change lifecycle

A change to the landing zone is a pull request. From there the engine drives the whole flow.

sequenceDiagram
  actor Eng as Engineer
  participant PR as Pull request
  participant Res as dispatch / resolve (hosted)
  participant Run as plan / apply (self-hosted)
  participant Az as Azure
  Eng->>PR: open PR touching a unit
  PR->>Res: lint gate, parse event, select impacted units
  Res->>Run: matrix of units to plan
  Run->>Az: OIDC login, terraform plan
  Az-->>Run: plan result
  Run->>PR: post plan comment + gate output
  Eng->>PR: review, comment /apply
  PR->>Res: approval gate (required reviews)
  Res->>Run: apply in dependency order
  Run->>Az: terraform apply
  Run->>PR: post apply comment
  PR->>PR: merge gate turns green
  Eng->>PR: merge

What each step does

  1. Open or update a PR. The PR caller's pre-commit job runs the repo's own hooks first and gates everything behind it. Then the engine parses the event, works out which files changed, and selects the impacted units. In Terragrunt mode it uses native discovery to include a changed unit plus every downstream dependent, in dependency order. On a /plan or /apply comment it checks out the PR head before discovery, so a unit that exists only on the PR branch is selected. Planning a unit also takes a cross-PR lock on it, so two pull requests cannot fight over the same state.
  2. Plan. The plan job fans out one job per impacted unit, in parallel, on the self-hosted runner. Each unit logs in to Azure with the read-only plan identity, runs terraform plan, and runs the post_plan gates.
  3. Report. Each unit posts a comment on the PR, led by a one-line result headline, then the plan output, a change summary table, apply instructions, and the validation checks (collapsed when clean, expanded on a finding). A check row named tf-pr-ops / <command> (<label>) #<run> appears per unit, and the engine adds a 👀 reaction to the command comment to show it was picked up. A newer run of the same unit and command supersedes the old one: the old check row turns neutral ("Outdated: superseded by #N") and the old report is minimized, so the PR surface always reads as the latest state.
  4. Review and apply. A reviewer approves. Someone comments /apply. The approval gate confirms the PR meets the repo's required reviews, then a single apply job applies the units in dependency order, one at a time. It re-plans each unit first and refuses when the plan changed since review; a unit whose fresh plan shows no changes is skipped ("No changes; apply skipped") unless it sits downstream of a unit that just applied changes, and a unit whose dependency failed is skipped too.
  5. Merge gate. A check run named tf-pr-ops / merge-gate is green only when the PR has no Terraform changes, is a no-op, or has been fully applied, and every changed Terraform path belongs to a planned unit. A changed path no unit accounts for fails the gate ("N Terraform path(s) changed outside the planned units"), so an unapplied net-new unit, or a stray edit beside the tree, cannot merge green; gate_ignore: in projects.yml names the deliberate exceptions. Deleting a unit is allowed through when the removal accounts for every changed path: the engine reports the removal in a comment carrying the manual destroy commands. Make the gate a required check and unapplied changes cannot reach the main branch.

Commands

Most of the flow is automatic. The commands you type are comments on the PR:

Comment Effect
/plan Re-plan all changed units.
/plan -p <label> Plan one unit and its dependents.
/apply Apply all changed units in dependency order.
/apply -p <label> Apply exactly one unit, no dependents.
/apply --force Apply without the zero-change skip.
/unlock Release the PR's cross-PR unit locks.

See the full Command reference, including /help, which lists the commands as a reply on the pull request.

One PR per unit at a time

The first pull request to plan a unit owns it. A second PR touching the same unit gets a red row reading "Locked by PR #n" and a blocking gate verdict ("Unit locked by another PR") until the holder closes or releases with /unlock. Locks live in a small table on the state storage account, release automatically when the holder closes, and the daily drift run sweeps up any lock whose holder is gone. See Plan and apply.

Drift, in the background

Separately, a scheduled job plans every unit read-only against the live tenant once a day and opens a GitHub Issue for each unit that has drifted, closing it again when the unit comes back clean. See Drift detection.

Gates, on every plan

Every plan (and every drift plan) runs three checks against the plan: a conftest policy check, a checkov security scan, and an infracost cost estimate. They start advisory and you enforce them when ready. See Policy, security, and cost gates.