Skip to content

Responses to pipeline events

llms.txtlists every page for an agent
Optional: hand this page to your coding agentThe steps work by hand too.
Show the whole prompt
Read https://intentius.io/terragucci/reference/responses/.
Set `respond:` in terragucci.yml only for the events I name, run `npx terragucci config check`, and open a pull request.
Run `terragucci respond` in its default dry-run mode only, and show me what each response would do.
Never apply, approve (a pull request review or `terragucci approve`), override a policy denial (`terragucci override`), use `--mode apply`, or merge; never touch `.chant/allowed_signers` or `chant/lifecycle`.

Every event has a default response that needs no model. No response runs a coding agent; people approve, apply and merge.

Event Response (default) Other choices
plan finished the grouped summary as the pull-request note none
wave refused a root-by-root diff of the approved plan against the current one, naming the attributes that moved off
apply failed triage from a table of known provider errors, each with its likely fix off
drift found a pull request writing the live value where the root sets a literal, and import blocks for resources the state does not hold attribute, which also names who changed each drifted attribute; off
tip raised one small pull request per tip: pin a provider from its lock file, add a lock file, add a canary off
format check failed a fmt commit on the pull request’s branch, on request off
module published release notes from conventional commits off
rollout wave merged with rollouts set, a scheduled job opens the next wave once the merged one applied off, which leaves the job out
module changed with no conventional commit none; tf-publish proposes a patch suggest: a suggested bump in a release pull request, from the typed-decision service
pull request opened or updated none check: a typed decision flags a description that leaves out what the plan destroys or replaces

Set a response per event under respond in terragucci.yml; a key you leave out keeps its default.

respond:
drift: pull-request # the default; attribute names who changed each attribute
apply-failed: triage # the default
plan: summary
Key Takes Default
plan summary summary
wave-refused diff, off diff
apply-failed triage, off triage
drift pull-request, attribute, off pull-request
tips pull-request, off pull-request
fmt commit, off commit
publish notes, off notes
rollout next-wave, off next-wave
version-bump suggest, off off
description off, check off

Each response is a command that dry-runs by default; --mode apply opens the pull request or pushes the commit.

Terminal window
terragucci respond plan --report terragucci-report
terragucci respond wave-refused --approved terragucci-report/approved --current terragucci-report/current --wave 2
terragucci respond apply-failed --log apply.log
terragucci respond drift --mode apply
terragucci respond drift --root envs/prod/orders --import aws_sqs_queue.extra=https://sqs.us-east-1.amazonaws.com/123456789012/extra --mode apply
terragucci respond tips --mode apply
terragucci respond tips --report terragucci-report --branch my-change --mode apply
terragucci respond fmt --branch my-change --mode apply
terragucci respond publish --module modules/network
terragucci respond version-bump --module modules/network --mode apply
terragucci respond rollout modules/network 1.4.0 --mode apply
terragucci respond rollout --mode apply

With no module, respond rollout finds the rollouts in flight from their pull requests’ terragucci/rollout/ branches and continues each. Without rollouts set, nothing opens the next wave until someone runs it.

A rollout’s newest wave respond rollout
merged and applied opens the next wave
merged, not yet applied waits
still open leaves it alone
closed without merging leaves it alone
the last wave, merged leaves it alone

--json prints one envelope, as every other command does.

An approval binds a wave’s set digest, so a wave whose root plan changed after approval applies nothing. The diff reads terragucci-report/approved and terragucci-report/current and lists each root whose plan digest moved. Approve again only once the new plan is the one you want.

Triage knows these provider error classes:

Class Matches Likely fix
access denied AccessDenied, AccessDeniedException, UnauthorizedOperation grant the permission the message names to the apply role
quota VpcLimitExceeded, LimitExceeded, TooManyBuckets, ServiceQuotaExceededException raise the quota or remove unused resources
throttling Throttling, ThrottlingException, RequestLimitExceeded run again; lower -parallelism if it repeats
already exists EntityAlreadyExists, ResourceAlreadyExistsException, BucketAlreadyOwnedByYou, InvalidGroup.Duplicate, QueueAlreadyExists import it, or give it another name
dependency DependencyViolation, DeleteConflict, BucketNotEmpty remove what depends on it first
state lock Error acquiring the state lock wait; a person runs force-unlock if no run holds it

The table covers AWS provider errors and Terraform’s state lock message; any other provider’s error is listed as unknown.

A refresh-only plan finds the drift (a full plan on a choudoufu root under live resource markers), and the pull request writes the live value where the root’s own resource block holds a literal. Merging it accepts the outside change, so read it first.

Other changes are only reported:

The value Why it stays
set from a variable or an expression the literal to change is elsewhere
set inside a module the module serves other roots too
on a count or for_each instance one literal sets every instance
never set in the root it comes from a default
on a resource deleted outside Terraform the next apply makes it again

In a Terragrunt repo a unit’s resources are in the module its terraform.source names, and what differs between units is their inputs. Where the module sets the drifted attribute to var.<name> and the unit’s own terragrunt.hcl sets <name> to a literal in inputs, the pull request writes the live value there, so only that unit changes. A literal in the module is only reported, since every unit calling the module shares it. The same goes for a module from outside the repo. The drift job plans again only the units its report shows drifted. --import names a root’s files, so it is refused in a Terragrunt repo.

Under synth the roots are the app’s output, so there is no literal to write: respond.drift: pull-request with a drift schedule is a config error, and respond drift refuses.

For a resource the state lacks, pass --import <address>=<id>. From Terraform 1.14.1, a root with a .tfquery.hcl file gets imports from terraform query.

The Codify drift pull request in Forgejo, from the terragucci/drift branch: its body lists the queue timeout written from its live value and the queue to importThe Codify drift pull request in Forgejo, from the terragucci/drift branch: its body lists the queue timeout written from its live value and the queue to import
The drift pull request's files: main.tf takes visibility_timeout_seconds = 45, terragucci_imports.tf adds an import block for the other queue, and terragucci_generated.tf holds that queue's generated configThe drift pull request's files: main.tf takes visibility_timeout_seconds = 45, terragucci_imports.tf adds an import block for the other queue, and terragucci_generated.tf holds that queue's generated config

With respond.drift: attribute, each drifted attribute goes through three steps in order; the first to answer wins.

Step Source Decides
1 a table of by-design writes: ECS task count, autoscaling and node group sizes, provisioned DynamoDB capacity, RDS minor upgrades, aws: tags the write is expected
2 CloudTrail LookupEvents over 14 days, for aws_ resources the newest write Terraform did not make: a service principal is a controller, a user is a person
3 a typed decision, when decide is set the attribute stays unattributed below the choice threshold; sensitive values are never shown
Needs Detail
the aws CLI, version 2 the generated drift job installs it against a pinned SHA-256 unless it is on the path; a pipeline you write installs it the same way
cloudtrail:LookupEvents on the job’s role
the region audit_region, else the CLI’s
roots or Terragrunt units terragucci stage tf-drift lists who changed each attribute in the drift issue, under its root or unit
decide.token_env received on GitHub and Forgejo

If step 2 cannot run, it says so and step 3 follows.

A drift issue in Forgejo whose Who changed it list puts the queue's visibility_timeout_seconds down to a person: SetQueueAttributes by alice, from the audit logA drift issue in Forgejo whose Who changed it list puts the queue's visibility_timeout_seconds down to a person: SetQueueAttributes by alice, from the audit log
Attributed to In the drift pull request Suggestion
a controller write left out add lifecycle { ignore_changes = [...] } to the resource
a provider default change left out pin the provider version that kept the old value, or set the attribute
a human edit kept, with the actor when known accept or revert it in the pull request

Nothing here applies or merges.

When respond.description: check and a decide block are set, the plan job asks the service whether the title and description fit the plan. The flag never blocks a merge and changes no gate.

Outcome The note intent.json
a yes at or above the threshold starts with a line naming the destroys and replacements the text omits records the decision
anything else unchanged says why
Terminal window
terragucci respond description --report terragucci-report --title "retag email" --description "Tags only." --mode apply
Tip The pull request
a provider taken by a range pins it in required_providers at the version the lock file holds, so the next plan changes nothing
a root with no lock file adds one written by providers lock, with hashes for Linux and macOS on amd64 and arm64 unless --platform names others
no canary adds waves.canary
a rename (--report) adds a moved block for each resource the plan destroys and creates with the same configuration (terragucci-moved), after the new block, in a pull request into --branch

tips --report reads the plans of a stage tf-plan report and proposes only the moved blocks; run it in a checkout of the branch that renamed the resources, after its plan. The generated tips job runs on the default branch after the applies, without --report.

Under synth the tips job runs the command first, and only the canary tip opens: the other two would edit files the command writes.

The pull request the tips job opens for a provider taken by a range: one file, main.tf, pinning hashicorp/aws at the version its lock file holdsThe pull request the tips job opens for a provider taken by a range: one file, main.tf, pinning hashicorp/aws at the version its lock file holds

fmt runs on request and pushes one commit to the pull request’s branch, and refuses the default branch. In a Terragrunt repo it runs terragrunt hcl fmt beside the binary’s fmt, and the commit is style: terragrunt hcl fmt when only .hcl files moved. The generated pipeline’s fmt job runs it after a failed check, so the check job holds no token that pushes.

The style: tofu fmt commit on a pull request's branch in Forgejo, realigning the locals in app/locals.tfThe style: tofu fmt commit on a pull request's branch in Forgejo, realigning the locals in app/locals.tf

Release notes cover the commits that touched a module between its last two tags, breaking changes first.

The bump comes from the version bump rules. Under version-bump: suggest, the typed-decision service picks the bump when no conventional commit landed since the last release; this needs decide.

Where What
a module’s releases its git tags, <module>/v1.2.0
a registry-only project pass --since <ref>; the version file at that ref is the last release (0.0.0 if none)
the version-bump job runs terragucci respond version-bump --mode apply on the default branch after every apply wave, with full history; it needs permission to push a branch and open a pull request; a failed response never fails the job
the release pull request writes the version file with the bump and its probability; merging confirms it and the next tf-publish publishes it
The release pull request the version-bump job opened on Forgejo, Release modules/queue 0.1.1: it proposes a patch bump, says no commit since 0.1.0 carries a conventional type and the decision service did not answer, explains that merging writes the module's version file for the next tf-publish run, and lists the commit since the last releaseThe release pull request the version-bump job opened on Forgejo, Release modules/queue 0.1.1: it proposes a patch bump, says no commit since 0.1.0 carries a conventional type and the decision service did not answer, explains that merging writes the module's version file for the next tf-publish run, and lists the commit since the last release

Responses run in CI with no model. To hand a result to your own agent, run the command with --json in a job whose token can only comment, as in Have an agent summarize a refused wave. The agent never holds an apply role or a gate’s signing key.

terragucci

These docs count page views and clicks with PostHog. They set no cookies, store nothing in your browser, and send nothing when your browser asks not to be tracked.