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¶
- Open or update a PR. The PR caller's
pre-commitjob 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/planor/applycomment 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. - 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 thepost_plangates. - 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. - 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. - Merge gate. A check run named
tf-pr-ops / merge-gateis 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:inprojects.ymlnames 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.