Skip to content

Plan Previews

llms.txt lists every page for an agent

A pull request’s plan (chant components pr-plan) and a gated wave ask each step of each component what it would change. Only a capability with plan can answer. This page lists every capability chant registers, says which of them plan, and says what the note shows for a component in each case.

The table is generated from the capability plugins that core and the lexicons register. plan says the capability can describe what its run would change. outputs says it can read the outputs it left when it last ran, which a dependent’s stackOutput() resolves to when the change does not plan the dependency. When it does, the dependent reads what the dependency’s plan says it will expose instead (Outputs).

CapabilityLexiconplanoutputsThe plan readsThe note shows
attest-provenancecorenononothingno entries for this step. See without plan
docker-buildcorenononothingno entries for this step. See without plan
ensure-secretcorenononothingno entries for this step. See without plan
generate-sbomcorenononothingno entries for this step. See without plan
health-gatecorenononothingno entries for this step. See without plan
jvm-buildcorenononothingno entries for this step. See without plan
r2-synccorenononothingno entries for this step. See without plan
scan-vulnerabilitiescorenononothingno entries for this step. See without plan
shellcorenononothingno entries for this step. See without plan
signcorenononothingno entries for this step. See without plan
verifycorenononothingno entries for this step. See without plan
vuln-gatecorenononothingno entries for this step. See without plan
wait-cluster-healthycorenononothingno entries for this step. See without plan
wait-endpointcorenononothingno entries for this step. See without plan
wrangler-deploycorenononothingno entries for this step. See without plan
wrangler-versions-promotecorenononothingno entries for this step. See without plan
zip-packagecorenononothingno entries for this step. See without plan
cdn-invalidateawsnononothingno entries for this step. See without plan
cfn-deployawsyesyesa CloudFormation change set for the template and parameters, deleted without executing ita create, update, replace or delete per logical id, and the names of the properties a change touches, without values. See cfn-deploy
code-deployawsnononothingno entries for this step. See without plan
copy-to-hostawsnononothingno entries for this step. See without plan
ecs-update-serviceawsnononothingno entries for this step. See without plan
emr-start-job-runawsnononothingno entries for this step. See without plan
emr-submit-stepawsnononothingno entries for this step. See without plan
extract-config-bomawsnononothingno entries for this step. See without plan
lambda-deployawsnononothingno entries for this step. See without plan
load-image-on-hostawsnononothingno entries for this step. See without plan
publish-artifactawsnononothingno entries for this step. See without plan
publish-imageawsnononothingno entries for this step. See without plan
remote-execawsnononothingno entries for this step. See without plan
rollback-previousawsnononothingno entries for this step. See without plan
run-migrationawsnononothingno entries for this step. See without plan
s3-syncawsnononothingno entries for this step. See without plan
snapshot-beforeawsnononothingno entries for this step. See without plan
wait-for-stackawsnononothingno entries for this step. See without plan
wait-jobawsnononothingno entries for this step. See without plan
wait-steady-stateawsnononothingno entries for this step. See without plan
arm-deployazureyesyeseach template resource from ARM by its id, compared with the body the apply would PUTa create, update, no-op or delete per resource, and each changed property path with its value before and after. See arm-deploy
fly-releaseflynononothingno entries for this step. See without plan
fly-rollbackflynononothingno entries for this step. See without plan
run-agentflynononothingno entries for this step. See without plan
gcp-applygcpyesyesa GET of each Config Connector resource on the applier’s REST transport, decided the way gcpApply decidesa create, update, no-op or delete per resource, and each changed top-level field with its value before and after. See gcp-apply
helm-upgradehelmnononothingno entries for this step. See without plan
argo-appk8snononothingno entries for this step. See without plan
flux-reconcilek8snononothingno entries for this step. See without plan
kubectl-applyk8syesyesa server-side dry run (dryRun=All) of the manifest, compared with each live objecta create, update, no-op or delete per object, and each changed field path with its value before and after. See kubectl-apply
kustomize-applyk8snononothingno entries for this step. See without plan
terraform-applyterraformyesyesterraform plan (or tofu or choudoufu) in the root, against its stateevery resource change with its action, and each changed top-level attribute with its value before and after. See terraform-apply

The note has a row per member with its status and its counts of creates, updates, replaces and deletes. Below the rows come the plan digest and the approve command. The grouped plan summary comes last and lists destroys, failures and holes before its groups. Entries with the action no-op or read are kept in the change set and left out of the counts.

The digest a reviewer approves is the change-set document’s digest. It covers each member’s plan digest, status and holes. It also covers every entry with its attributes and every side effect. On merge, pr-apply plans every member again before it applies any. When the digest it gets is not the approved one, it applies nothing and exits 3.

A step whose plan throws makes its component’s members plan failed, with the error in the summary’s failed list. A plan with a failed member applies nothing.

A component’s members come from its steps that have plan, one member per such step. Its other steps are not members. A component that builds an image with docker-build and deploys it with cfn-deploy has one member, the stack. The note says nothing about the image. A capability without plan covers the component none of whose steps can plan.

Five capabilities have plan. Each section below says what the plan reads and what the note shows. It also says what holds the apply to the approved plan. A step that finds its target moved since the approved plan applies nothing, and the command exits 3 (#3997).

lexicons/azure/src/components/arm-deploy.ts holds the plan and the apply. Its run hands the template to azApply. ApplyOp’s arm target calls the same applier, so a component step and an Op share one code path. One resource group is one member. A step without resourceGroup deploys into a group named for its component. The step reaches endpoint when it sets one and AZURE_ENDPOINT_URL otherwise, which is how it targets floci-az. subscriptionId and AZURE_SUBSCRIPTION_ID pick the subscription the same way.

ARM offers no change set, so the plan GETs each declared resource by its id at the template’s apiVersion. It compares the answer with the body azApply would PUT. Both sides go through the noise rules in lexicons/azure/src/deep-observe.ts first. A 404 makes the resource a create that lists every property it declares. Matching properties make it a no-op, and any difference makes it an update listing the paths that moved with both values. The ownership tags azApply stamps are never compared, since floci-az drops tags. A value read from reference() or listKeys() is shown as known after apply, and the plan never calls listKeys.

With prune: true the plan adds a delete for each resource in the group that carries chant’s ownership tag and that the template no longer declares. That is the set azApply would prune. Without prune the plan proposes no deletes.

A read that fails for any reason but a 404 leaves a hole naming the reason, and such a plan gives run nothing to hold it to. A step waiting on an upstream output reads nothing and gets a hole naming that upstream.

The digest binds the environment and the template’s bytes. It binds where the step deploys too, meaning the group and its location under one endpoint and subscription. The prune setting and every entry with its attributes are in it as well. On apply, run plans again first. A digest other than the approved one stops it before any PUT, with refusing to apply.

The template’s own outputs section is evaluated against a live read of its resources, by outputs and at the end of run, and an output that comes to nothing is skipped. Before apply, the plan gives the value of an output whose every resource read is planned as no-op, or which reads none.

The plan is planCfnDeploy in lexicons/aws/src/components/apply.ts.

The plan creates a change set for the stack’s template and resolved parameters, reads the changes it proposes and deletes it without executing it. The member is the stack’s name, taken from stack or else from the component. A stack that does not exist yet gets a CREATE change set, and the plan deletes the empty REVIEW_IN_PROGRESS stack CloudFormation made to hold it.

Each resource change is one entry, addressed by its logical id, or <nested stack>/<logical id> inside a nested stack. Add is a create, Modify an update, or a replace when CloudFormation marks it Replacement: True or Conditional, and Remove a delete. An entry’s attributes are the property names from the change’s details, with no values before or after, since a change set gives none. A change set with nothing to apply gives a member with no entries.

A change CloudFormation marks Replacement: True fails the member’s plan when onReplace is "block", which is the default. The error names the resources.

When a parameter reads another component’s output that has no value yet, such as an output of a stack that does not exist or one its upstream’s plan knows only after apply, the plan creates no change set. The member gets a hole saying inputs from <component>; re-checked on apply, and the note shows ? for its counts. A parameter that reads an earlier step of the same component has no value at plan time either, and its hole says inputs <names> are known only at apply.

The plan’s digest covers the environment and the stack. It also covers the template’s path and bytes, the resolved parameters and the components whose outputs were missing. The plan also hands run a digest of the changes it read. On apply, run creates a new change set and executes it only when its changes have that digest. Otherwise it deletes the change set and fails with refusing to apply. A member with a hole hands run nothing to compare, so its change set is executed as created.

outputs reads the stack’s outputs with describe-stacks, and gives none for a stack that does not exist. The plan also says which outputs the stack keeps: all of them when the change set has nothing to apply, and otherwise each output whose template Value reads only resources the change set modifies in place. Every other output is known only after apply.

The plan is planGcpApply in lexicons/gcp/src/components/gcp-apply.ts, and it decides each resource with decideResource in lexicons/gcp/src/op/activities/gcp-apply.ts, the function the applier acts on.

The step applies a built Config Connector manifest (manifest) through the gcpApply activity, the same REST mappers an Op and the floci-gcp path use. Its member takes the step’s stack, falling back to the component. GCP_ENDPOINT_URL, or the step’s endpoint, points the reads and the writes at floci-gcp. Each resource’s project is the step’s project, else its spec.projectRef.external, its cnrm.cloud.google.com/project-id annotation, GCP_PROJECT_ID, then GOOGLE_CLOUD_PROJECT.

The plan reads each declared resource with a GET on the applier’s own URL, or through its target’s getIamPolicy for an IAMPolicyMember, and writes nothing. A 404 makes a create entry. A resource whose live body already holds every field the manifest sets gets a no-op, as does one whose kind has no in-place update. Otherwise the entry is an update whose attributes are the top-level fields that differ, live value as before and manifest value as after. Apply sends an update only in that last case, so its report matches the plan. Setting prune: true also lists as deletes the live resources of a manifested kind labelled managed-by: chant that the manifest dropped.

Three cases get a hole instead of an entry. Apply skips a kind with no REST mapper. A read that answers neither 200 nor 404 can’t say whether the resource exists. When a compute field can’t change in place, apply reports replace required and leaves the resource alone.

A step whose inputs wait on another component’s outputs reads nothing, and its hole says inputs from <component>; re-checked on apply.

A gcp-apply plan’s digest is taken over its member in that environment. The manifest’s path and bytes go in too. So do the endpoint and the project override. The prune mode is included, as are the components whose outputs were missing. run gets a digest of the planned entries, new values included. On apply it plans again and refuses with refusing to apply when the two differ, and the command exits 3.

outputs reads what the API reports for the applied resources: <name>Uri for a RunService (its uri), <name>Address for a ComputeAddress and <name>IpAddress for a ComputeForwardingRule. run returns them once it applied. The plan knows the value of each resource that stays, and a resource it creates has its output known only after apply.

The plan and its entries are in lexicons/k8s/src/components/kubectl-apply.ts, in plan and changeSetPartOf.

Each document of the manifest goes to the API server as a server-side apply with dryRun=All, under the field manager chant:<stack> and with the release attributes the apply would set. It compares what the dry run returns with each live object. The member is named after the step’s stack, and after the component when the step has none.

An object with no live counterpart is a create, with every field the manifest declares. One whose dry run equals the live object is a no-op. One that differs is an update, with each changed field path and its value before and after. Server bookkeeping such as managedFields, resourceVersion and status is dropped before the comparison. A Secret’s data and stringData are marked sensitive and carry no values. An update under spec.template of a workload whose pods roll on such a change has the disruption rolling.

With delete: "owned-only" the plan adds a delete for each object carrying chant’s marker and the stack’s label that the manifest no longer declares. With the default delete: "never" there are no deletes.

When the API server refuses a change in the dry run, as it does for a Deployment’s spec.selector, the object is an update with unknown disruption. Its hole carries the server’s message, and the apply would fail the same way.

The digest covers the documents sent, the target they are sent to and the delete mode. For each object it also covers the action and the new value of every changed field. On apply, run dry-runs the approved documents again. It applies them only when that dry run gives the approved digest. Otherwise it fails before the cluster changes and names both digests.

kubectl-apply has outputs only when the step names them: outputs: { host: { object: "Service/prod/web", field: "status.loadBalancer.ingress.0.hostname" } }, each a field of an object the manifest declares. outputs reads those fields from the cluster, and run returns them once it applied. The plan knows a field the manifest sets, and any field of an object the dry run leaves unchanged.

The capability is in lexicons/terraform/src/components/capability-plugin.ts, and its entries come from lexicons/terraform/src/change-set.ts.

The plan runs init, then plan -input=false -detailed-exitcode -out=<plan file> in the root with its var files and the step’s vars, then show -json over the plan file. The binary is the one terraform.binary names: terraform, tofu or choudoufu. The member is the root’s name.

Every resource_changes entry is an entry with its address and action. create, update and delete keep their names, and a delete with a create in either order is a replace. Attributes are the top-level attributes the change writes, each with its value before and after. A value terraform knows only after apply shows as <known after apply>. A sensitive attribute carries no value. An attribute that forces the replacement is marked. Imports, forgets, deposed objects and, from Terraform 1.14, action invocations as side effects are kept too.

A root whose vars read an upstream output with no value yet is not planned, since terraform takes no unknown variable. Its member gets a hole saying inputs from <component>; planned again once they apply, with no plan file. Otherwise the plan has no holes.

The digest covers the plan’s resource changes, output changes and action invocations. Timestamps, versions and prior state are left out. run applies the plan file the plan wrote with apply <plan file>. When choudoufu refuses that plan file, the step fails as a moved plan and the command exits 3. Plain terraform and tofu refuse a stale plan file too, and the command exits 1, because chant does not tell that error from others.

A live choudoufu root that live-plan-set can plan as chant would, one with no var files, no vars and no backend config, is planned as a one-root set instead. run hands choudoufu that set and its digest with live-wave-apply, and choudoufu’s refusal of a moved set exits 3.

Its outputs runs output -json in the root after an init. The plan says the outputs the root will have: planned_values.outputs from show -json, less each output whose output_changes entry is unknown after apply.

A component with no step that has plan is one member under the component’s name. Its row reads planned with a count of 0 in every column, and its group in the plan summary says no changes. That row says nothing about what the component will change, since the plan had nothing to ask.

The member’s plan digest is the component’s composition digest, the one its own gates bind. It is computed over the component’s declaration and the environment with its vars. Every step’s input goes in as written, and since #4155 so does the content of each local file or directory a step deploys from. A commit pushed after review that changes a chart, a values file or a site’s tree moves the digest, and pr-apply exits 3 as it does for a moved cfn or k8s plan.

These are the paths the digest reads, from the step’s literal input (packages/core/src/components/deploy-inputs.ts):

StepsFields
any steptemplate, manifest, values
helm-upgradechart
kustomize-applydir
fly-release, fly-rollbackplan
s3-sync, r2-sync, copy-to-hostfrom
wrangler-deploy, wrangler-versions-promoteconfig
docker-buildcontext, and dockerfile under it
zip-packagesource
jvm-buildpath

A file is hashed by its bytes. A directory is hashed over the files under it that git does not ignore, each by its path and bytes, so a Docker context leaves out node_modules and dist. A value wired from another step, such as codeRef: "@Build.uri", and an archive: path are build outputs and are not read. A lambda-deploy is bound through its bundle step’s source instead, so a build that is not reproducible still applies under its approval. A path that does not exist is left out, and one that appears later moves the digest.

Some steps deploy what the digest can’t name. A shell or run-agent step runs content its input does not list, and a directory git ignores as a whole is a build output, such as s3-sync from dist. The member then has a hole for each such step, with a reason that starts unbound:, and the plan note lists it under Holes. A reviewer of such a member has to read the change itself.

  • Read what the step deploys in the pull request’s diff: the script a shell step runs, or the sources of the build that writes the synced directory.
  • Approve the pull request only once its head is the commit you read.
  • Turn on the forge’s setting that drops approvals when commits are pushed: “Dismiss stale pull request approvals when new commits are pushed” on GitHub, “Dismiss stale approvals” on Forgejo, “Remove all approvals when commits are added to the source branch” on GitLab. With the default requireReview, pr-apply counts only an approving review that still stands on the forge, so a push after your review needs a new one.

A gated wave treats such a component the same way: one member, the composition digest, and no entries.

A component that has a step with plan beside steps without one, such as a lambda-deploy beside a cfn-deploy, gets one more member, <component>#inputs. Its digest is taken over the files the steps without plan deploy from, and its holes are those steps’ unbound ones. The member is there only when those steps name a file or have a hole.

arm-deploy, cfn-deploy, gcp-apply, terraform-apply and kubectl-apply have outputs. A pull request’s plan reads them for each dependency of a planned member that the change does not plan, so a stackOutput() reference resolves to the value deployed now (deployedOutputs in the report). For a dependency the change plans too, it reads what that dependency’s plan says it will expose (plannedOutputs, #3999).

A reference neither can answer stays unresolved, and cfn-deploy, gcp-apply and terraform-apply report a hole for it. When the upstream is in the change, the member lists the reference under unknownInputs. The apply holds that member back until its upstream applied, plans it again, and asks for an approval of that plan at pr-apply-round-<n>. An artifact a build in the same change publishes, read as @<component>.publish.*, is listed under buildInputs instead. The build’s provenance binds it, so the member applies with it without a second approval. --seed-outputs <file> supplies values instead of either read.

chant