Skip to content

Troubleshooting

A job fails at startup with an OIDC or environment error

The OIDC subject binds the repository and the environment name. Only the Azure jobs (run, apply, drift-plan) bind a plan or apply environment and request an id-token; the trusted jobs (resolve, gate, report) hold only a read-only GitHub App token and request no token. The reusable workflow already keeps this split, so the caller does not need to. If a login fails, confirm the federated credential subject matches the repository and environment. Repos created after GitHub's immutable-id rollout use the repo:org@id/repo@id:environment:env form; the bootstrap builds subjects in that format by default and only uses the legacy name-only form when oidc_subject_uses_repo_ids is set false.

The engine checkout fails or resolve cannot read the engine

The reusable workflow checks the private engine out at engine_ref using a token from the GitHub App. If the App installation does not include nrit-tf-pr-ops, that checkout fails. Grant the App read access to nrit-tf-pr-ops, and confirm CATALOG_APP_CLIENT_ID (variable) and CATALOG_APP_PRIVATE_KEY (secret) are set. Those two names are historical: the App authenticates the engine checkout, not a module catalog.

The run uses the wrong engine version

Each caller pins the engine in two places: the uses: ref and the engine_ref input. They must match. If they skew, new workflow YAML runs against old scripts (or the reverse). The resolve job logs the requested engine_ref and the engine commit it checked out, at the top of the run. Bump both pins together.

A manual dispatch fails in resolve with no open PR

Dispatching terraform-pr-ops.yml by hand without an open pull request fails in the resolve job, because there is no PR context to resolve against. This is a known limitation of the current engine. Drive plan and apply from a pull request instead; use manual dispatch only where a PR exists.

A plan reports "Unit locked by another PR"

  • Another open pull request planned the same unit first and owns it until it merges or closes. The report comment names the owning PR.
  • Normal path: wait for the owning PR, or get it merged or closed. Its locks release automatically and the next plan on your PR acquires the unit.
  • If the owning PR is parked, comment /unlock on that PR (not yours) to force-release its locks.
  • If locks strand on a closed PR, the caller workflow is probably missing the closed trigger type. Check on.pull_request.types in .github/workflows/terraform-pr-ops.yml against the canonical caller.
  • See Cross-PR unit locks.

The merge gate never turns green

  • Make sure tf-pr-ops / merge-gate is a required status check and that the name matches exactly.
  • The gate is red while a unit has unapplied changes. Run /apply and let it finish.
  • If a plan or apply errored, the gate is red until the error is fixed and the unit re-planned or re-applied.
  • If the PR changed Terraform but no unit was selected, the gate blocks on purpose (for example a whole unit directory was removed). Comment /plan to see what is discovered, and confirm the changed unit is a real Terragrunt unit on the branch.

/apply says it is blocked

Read which of the two messages you got.

  • "required reviews not met". The PR needs approving reviews it does not have. Get the approvals the branch protection or ruleset requires, then comment /apply again.
  • "no reviews required by this repository". The repository requires no approving reviews at all, so GitHub reports no review decision and the engine refuses. There is nothing to approve here, so approvals cannot clear it. Either set a required approving review count on the main branch, or, if the repository requires none on purpose, set the TF_PR_OPS_ALLOW_UNREVIEWED_APPLY repository variable to true. See Secrets and variables.

The same distinction shows in the tf-pr-ops / approval status description.

A /plan or /apply comment is refused as unauthorized

The engine accepts commands only from a comment whose author_association is OWNER, MEMBER, or COLLABORATOR. Any other author gets a 😕 reaction and a reply naming the association it saw. Add the person as a repository collaborator or an organisation member, or have someone who already qualifies run the command. See Who can run a command.

The cost gate shows "skipped"

infracost needs a key. Set the INFRACOST_API_KEY secret and add infracost to the TFPR_EXTRA_TOOLS variable. Use a static key from the Infracost dashboard (the legacy line), not the newer OAuth CLI flow.

checkov reports "not installed"

checkov is baked into the self-hosted runner image. If you run on a hosted runner without the baked image, set TFPR_EXTRA_TOOLS to include checkov on a Python-capable runner, or use the baked runner image.

conftest reports "no policies found"

That is expected before you add any policy. Put .rego files in policy/ (or set CONFTEST_POLICY_DIR). The hook passes with a note until then.

A runner is OOM-killed and leaves a stale state lock

The heaviest ALZ policy-library plans need more than the AVM runner defaults (1 vCPU, 2Gi, 1800-second timeout). Undersized, the plan OOM-hangs and the replica timeout kills the runner mid-job, which can leave a stale state lock. The bootstrap sizes replicas at 2 vCPU, 4Gi, and a 7200-second timeout through container_app_container_cpu, container_app_container_memory, and container_app_replica_timeout. If you see this, confirm those values are applied. The runbook has the full procedure for clearing a stale lock safely.

The bootstrap plan always shows an ACR bypass revert

The bootstrap plan permanently shows azurerm_container_registry.this wanting to set network_rule_bypass_for_tasks_enabled back to false. Never apply it: the flag is owned by an azapi patch the runner-image build depends on, and reverting it breaks the next image build. The bootstrap README carries the exact restore command if it is ever applied by accident.

Plan cannot reach state

State sits behind a firewall that allows the runner's egress address. Confirm the job ran on the self-hosted runner (RUNNER_LABEL is set) and that the runner's egress address is still allowed on the state account firewall.

A version constraint fails

Pin Terraform and Terragrunt in mise.toml. The built-in fallback versions are only used when no version manager is present, and they may not satisfy a unit's constraint (for example a unit requiring >= 1.12).