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
/unlockon that PR (not yours) to force-release its locks. - If locks strand on a closed PR, the caller workflow is probably missing the
closedtrigger type. Checkon.pull_request.typesin.github/workflows/terraform-pr-ops.ymlagainst the canonical caller. - See Cross-PR unit locks.
The merge gate never turns green¶
- Make sure
tf-pr-ops / merge-gateis a required status check and that the name matches exactly. - The gate is red while a unit has unapplied changes. Run
/applyand 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
/planto 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
/applyagain. - "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_APPLYrepository variable totrue. 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).