Skip to content

Workflows and discovery config

The repository holds no engine code. Two caller workflows in .github/ invoke the nrit-tf-pr-ops reusable workflows, and projects.yml at the root tells the engine what to discover and which gates to run. This page covers those files plus the CODEOWNERS and pull request template that ship alongside them.

.github/workflows/terraform-pr-ops.yml

The plan and apply caller. It is about thirty lines and it is stable: engine changes never need an edit here.

It triggers on three things:

  • pull_request with types opened, synchronize, reopened, which plans every impacted unit, plus closed, which releases the PR's cross-PR unit locks on merge or close.
  • issue_comment with type created, which is how /plan, /apply, and /unlock comments reach the engine.
  • workflow_dispatch, with a command choice input (plan or apply, default plan) and an optional project input to target a single unit.

The permissions block is a ceiling, not a grant to this file. A reusable workflow cannot ask for more than the caller holds, so the caller has to declare everything the engine's jobs use:

permissions:
  contents: read
  pull-requests: write
  checks: write
  statuses: write
  id-token: write

pull-requests: write posts and edits the run comment and its reactions, checks: write and statuses: write publish the merge-gate check, and id-token: write mints the OIDC token for Azure. Cut any of them and the engine fails inside the reusable workflow, not here.

The single job calls the reusable workflow:

jobs:
  engine:
    uses: nrit-solutions/nrit-tf-pr-ops/.github/workflows/terraform-pr-ops.yml@v1.5.0
    with:
      command: ${{ inputs.command || 'plan' }}
      project: ${{ inputs.project || '' }}
      engine_ref: v1.5.0   # must match the uses: ref above
    secrets: inherit

The two pins move together

Every caller carries the engine version twice: the uses: ref and the engine_ref input. uses: selects the reusable-workflow body. engine_ref selects the engine scripts the workflow checks out at run time. Both sit at an exact version, so a run only changes when someone commits a new one. The resolve job logs the exact engine commit it checked out.

Bump both pins together

If the pins skew, the engine checkout does not match the reusable workflow body. The workflow runs one version's orchestration against another version's scripts, so the failure is a mismatch inside the run, not a clear version error. Change uses: ...@vX and engine_ref: vX in the same commit, in both caller files.

See The PR-ops engine for how the pin is verified at run time and what branch protection to set.

secrets: inherit

Both callers pass secrets: inherit. That hands the reusable workflow every secret in the repository, with no explicit mapping and no per-secret allowlist. It keeps the caller stable when the engine starts using a new secret, and it means the engine sees any secret you add to the repository for an unrelated reason. Worth knowing before you store an unrelated credential in this repository. The Security model page covers the blast radius.

.github/workflows/drift.yml

The daily drift sweep. It runs on cron: "17 6 * * *", so 06:17 UTC every day, and also on workflow_dispatch with no inputs.

Its permissions are deliberately different and narrower:

permissions:
  contents: read
  issues: write
  id-token: write

Drift reports as GitHub issues, not as a pull request comment, so it needs issues: write and does not need pull-requests, checks, or statuses at all. The job takes one input, engine_ref: v1.5.0, matching its own uses: ref, and it also uses secrets: inherit.

.github/CODEOWNERS

Ships as a single catch-all rule:

*       @nrit-solutions/platform-engineering

That is the template's owner, not the customer's. Replace it with the customer's real owning teams during onboarding, or every review request routes to NRIT.

.github/pull_request_template.md

Three sections. What and why is free text. Scope asks for the units impacted, the subscription or landing zone, and any version pins moved, which the template spells out as the AVM version in main.tf, the policy library ref, and the two engine pins.

Then a seven-item checklist:

  • Plan comment reviewed for every impacted unit, including deletes and replaces
  • Policy, security, and cost gate output reviewed
  • If an engine pin moved, the uses: ref and engine_ref match in both workflows
  • Affected subscription.hcl / region.hcl values confirmed
  • Reviewer from the required approver group requested
  • /apply run after approval and the apply succeeded
  • tf-pr-ops / merge-gate is green

The last two matter for the merge gate. It stays red until the change is applied, so applying is part of the pull request, not something that happens after merge.

projects.yml

This is the repository's actual discovery config, not the schema. For every field and all three discovery modes, see projects.yml schema.

terragrunt:
  root: live
  include_dependents: true

steps:
  post_plan:
    - bash "$TFPR_ENGINE_DIR/scripts/conftest-gate.sh"
    - CHECKOV_SOFT_FAIL=1 bash "$TFPR_ENGINE_DIR/scripts/checkov-gate.sh"
    - bash "$TFPR_ENGINE_DIR/scripts/infracost-gate.sh"

root: live points discovery at the directory holding root.hcl. include_dependents: true means an impacted unit pulls its downstream dependents into the same run, so a plan shows the full effect of the change and an apply runs in dependency order.

A unit is impacted when its own directory changed, when a file it reads changed, or when it is downstream of a unit that is. The read-files part matters here: every unit reads root.hcl, tenant.hcl, and the subscription.hcl and region.hcl above it, so a one-line change to live/root.hcl impacts the whole tree. See Plan and apply.

There is no exclude. The foundation is governed like any other unit: plan review, required approvals, and the merge gate. Excluding it would hide the highest-impact units from the same scrutiny, so it stays in.

The three post_plan hooks are the policy, security, and cost gates, and they run on every plan and every drift plan. What they do and how to enforce them is on Policy, security, and cost gates.

$TFPR_ENGINE_DIR is exported into the hook environment by the engine and points at the engine checkout made at engine_ref. The gate scripts live in the engine, not in this repository, so hooks resolve them there rather than in the consumer workspace. That also means a pull request cannot edit a gate script to weaken its own gate.

Next: The live tree.