infra-prune · git:20261018.69734a2 · 2026-10-18 · sha256 e0a9245f005bd712
infra-prune git:20261018.69734a2A
Immutable. This exact content never changes and is served at /api/v1/blob/e0a9245f005bd712.
---
name: infra-prune
description: "Remove spent one-shot `import {}` and `moved {}` blocks after their apply landed, proving per block that state already holds the address and ending on a plan of No changes. Use once an import applied, never before."
allowed-tools: Read, Bash, Edit, Write, Glob, Grep, AskUserQuestion
---
<!--
AI-RULEZ :: GENERATED FILE — DO NOT EDIT
Content-Hash: blake3:d8009992872989327c421518b01f61892ca8a19d1ad3a21231f77524a6b46be6
Source-Hash: blake3:576c1bd119aa63ecaa609fe520d4bbea573a4c53b6216edd6fa0739eb0a25184
Schema-Version: v1
-->
# infra-prune
Delete the **one-shot instruction blocks** an adoption leaves behind. `import {}` and
`moved {}` execute once **against a given state**; after that run applies, the resources are
managed by their addresses and the blocks are inert. Nothing else removes them, so they
accumulate. Scope is one leaf at a time — blocks under `terraform/modules/` are out, see
guardrail 5.
This file is a **router**: the reusable machinery — actor model, handoff, resume,
preflight — lives in [`../infra/references/protocol.md`](../infra/references/protocol.md); the manifest in
[`../infra/references/steps.yaml`](../infra/references/steps.yaml) (**phase 5**, `prune-spent-imports`); the
canonical runbook in [`../infra/references/docs/prune.md`](../infra/references/docs/prune.md).
> **Why not the last step of `import`?** The apply has to land between the two, so they can
> never be the same pull request. `import` ends on *imports, not creates*; this ends on *No
> changes*. Two end states, days apart, usually two different sessions.
## Guardrails
The only workflow here that deletes, and both failure directions hit live infrastructure:
- **Pruning too early** — before the apply, the blocks still *are* the adoption
instructions, so the next plan proposes **creating** resources that already exist.
- **Pruning the wrong construct** — removing a `resource` block proposes a **destroy**. On
something like `google_project_service` that is an API disable on a live project.
So:
1. Touch **only** `import {}` and `moved {}` blocks. Never `resource`, `data`, `module`,
`provider`, `locals`, or `variable`. Removing a resource is `add`'s inverse and is not
this workflow.
2. Decide with **Terraform**, not a hunch: the plan after the edit must read
`No changes.` A block that has not run still shows up — a pending `import` prints
`will be imported`, a pending `moved` prints its rename — so the plan is what separates
spent from pending. How many plans, where they run, and whether `terraform state list`
can filter in front of them is the **backend's** answer and the runbook's to give; the
[prune-blocks operation](../infra/references/operations.md#prune-blocks) supplies the procedure. Membership is never the sole evidence
anywhere: an aggregate module target or a `count` added on one side defeats it.
3. **Refuse on a dirty plan.** If the leaf does not plan cleanly apart from the blocks you
are about to remove, stop and report rather than prune into an unrelated diff.
4. **One pull request per leaf.** Leaves have separate states and separate applies.
5. **Leaves only — never `terraform/modules/`.** Every consuming state reads a module's
blocks separately, and that set is not closed. A module's `moved` block is its upgrade
path, which Terraform says to retain; its `import` block is spent only per consumer, and
deleting it while one is behind makes that consumer plan a **create** against a live
resource. `prune-spent-imports` excludes the directory for the same reason, so a
retained module block never shows up as work. The runbook has the citation.
## Workflow
1. **Read config first** (shared protocol, Step 0) and export the org vars —
[`../infra/references/config.md`](../infra/references/config.md).
2. **Resume scan** over `prune-spent-imports` in
[`../infra/references/steps.yaml`](../infra/references/steps.yaml). Green means no one-shot blocks are
committed and there is nothing to do.
3. **Follow the runbook** [`../infra/references/docs/prune.md`](../infra/references/docs/prune.md): discover
candidates, prove each block spent by the evidence that backend allows — the runbook
says which — delete only the block, plan, open the PR.
4. If the before-plan still carries the block's own import or move, the apply has not
landed. Stop and say so — that is `infra-skills:infra-import` finishing its work, not a
prune.
## Validation
`terraform plan` for the leaf reports **`No changes. Your infrastructure matches the
configuration.`** with no `will be created`, `will be imported`, or `will be destroyed`.
Capture that output for the pull request body — it is the evidence that the removed blocks
were inert. **That plan is the leaf's completion signal**, not the manifest check:
`prune-spent-imports` scans every leaf at once, so it stays red until the last one is
pruned. With blocks in two leaves, a correct first PR leaves it red — that is the second
leaf reporting, not a failure of the first. Do not combine leaves to turn it green, and do
not withhold the PR waiting for it.
## Example
```text
# terraform/cloudflare — before (see the selected execution procedure)
$ grep -c '^import {' generated_dns.tf
99
get-plan output for the selected execution
No changes. Your infrastructure matches the configuration. # nothing pending
# after removing the 99 import blocks, nothing else
get-plan output for the committed deletion
No changes. Your infrastructure matches the configuration. # they were inert
```
A `will be imported` in the *first* plan means those blocks had not run yet — leave them.
A `will be created` in the second means one address was not what the block named — restore
it.