Skip to content

chant lifecycle

chant lifecycle snapshot <env> [lexicon] [--src <dir>]
chant lifecycle show <env>
chant lifecycle diff <env> [--live] [--src <dir>]
chant lifecycle diff <env> --between <refA> <refB> [lexicon] [--json]
chant lifecycle plan <env> [lexicon] [--json] [--owned] [--report gitlab-mr] [--src <dir>]
chant lifecycle affected --base <ref> [--head <ref>] [--include-dependents] [--json]
chant lifecycle log [env]

chant lifecycle captures point-in-time snapshots of deployed infrastructure by querying the cloud provider API and saving the result to a git orphan branch. Snapshots enable drift detection — comparing what’s currently built against what was last deployed, and (with --live) against what’s actually in the cloud right now.

The environment name (e.g. dev, staging, prod) must be declared in chant.config.ts under environments.

snapshot, diff, and plan build your declarations to compare against live state. By default they build the project root (.). For a mixed-layout project — chant infrastructure in src/ alongside application code that has import side effects (starts a server, opens a connection) — set sourceDir in chant.config.ts so the lifecycle build only synthesizes the infra:

chant.config.ts
export default { lexicons: ["k8s"], sourceDir: "src" };

The --src <dir> flag overrides sourceDir for a single invocation. Use the same source root for snapshot and diff so their build digests stay comparable.

For the conceptual model behind these commands — observational vs. authoritative state, resources vs. artifacts, when drift detection earns its keep — see Drift Detection.

Query the provider API for the current deployed state, then save a snapshot to the chant/lifecycle orphan branch.

Terminal window
chant lifecycle snapshot dev
chant lifecycle snapshot prod aws # snapshot only the aws lexicon

Print the latest saved snapshot for an environment.

Terminal window
chant lifecycle show dev

Two modes, depending on whether --live is set.

Default (digest mode). Build the current project, fingerprint the resource declarations, and compare against the digest stored in the last snapshot. Fast, offline, and useful as a fast-feedback check inside CI — but only catches changes you’ve made in source. Ignores the cloud entirely.

Terminal window
chant lifecycle diff dev

Reports added, removed, changed, and unchanged resources at the declaration level.

--live (drift mode). Query the cloud provider API right now via each lexicon’s describeResources() plugin method, then compare the live result against both the previous snapshot and the current build. This is the path that actually catches external mutations (someone scaled a node pool by hand, deleted a bucket, etc.).

Terminal window
chant lifecycle diff prod --live

Output is grouped into six categories per lexicon:

CategoryMeaning
missingDeclared in source, but not present in the cloud right now
orphanPresent in the cloud, but not declared in source
disappearedIn the previous snapshot, but gone now
newly observedDeclared and observed now, but not in any prior snapshot
driftedObserved in both snapshots; status, physical ID, or attributes changed
unchangedObserved in both snapshots; metadata identical

Drifted entries include attribute-level deltas (status, physicalId, lastUpdated, and each attributes.* key).

--between <refA> <refB> — two saved snapshots

Section titled “--between <refA> <refB> — two saved snapshots”

Every snapshot is a commit on the chant/lifecycle orphan branch (list them with lifecycle log). --between diffs two of them against each other — a purely historical, read-only comparison (no build, no cloud query). It answers “what changed in the cloud between Tuesday and Thursday” independent of source.

Terminal window
chant lifecycle log prod # find the two commits
chant lifecycle diff prod --between <shaA> <shaB>
chant lifecycle diff prod --between <shaA> <shaB> aws --json

Per lexicon (all declared, or the one you name), it reports a two-way delta: added (in B, not A), removed (in A, not B), changed (in both, metadata differs, with the attribute paths), and unchanged. --json emits { environment, refA, refB, lexicons }. This feeds the deployment-lanes frame-pair diff.

Promote the live diff to a typed, read-only change set. Where lifecycle diff --live reports six observation categories, lifecycle plan classifies each resource into one action you can act on:

Terminal window
chant lifecycle plan prod
chant lifecycle plan prod --json # emit the ChangeSet as JSON
ActionMeaning
createDeclared in source, absent from the cloud
updateDeclared and live, but the live config drifted
deleteA chant-owned resource that is live but no longer declared
adoptLive but undeclared, ownership not established — a candidate to pull back into source, never an auto-delete
noopDeclared and live with no drift, or already reconciled

create and update are precise from declared-vs-live. delete is only ever proposed for an undeclared resource whose live ownership marker (ownership marking) confirms it is chant’s; a foreign orphan is adopt, and an orphan with no marker data is adopt, never delete. Pass --owned to restrict the query to chant-owned resources.

The classification reads ownership from the live marker, never from the snapshot, so the snapshot never becomes load-bearing: the delete decision is identical whether or not a snapshot exists. The moment a mutation trusted the snapshot, the snapshot would have become an authoritative state file under a different name — which chant deliberately avoids.

lifecycle plan is strictly read-only: it builds, queries, and classifies. It never mutates the cloud and never deploys — chant build stays pure.

Crosswalk: observation categories → plan actions

Section titled “Crosswalk: observation categories → plan actions”

The six diff --live categories and the five plan actions describe the same signal on different axes. Observation categories key on three sets — declared in source, observed now, in the last snapshot. The plan collapses the snapshot axis (it’s evidence, never authority) and adds an ownership axis. The mapping:

Observation category (diff --live)Plan action (plan)Why
missingcreateDeclared, absent from the cloud
driftedupdate (when declared)Declared and live, attributes changed since the snapshot
orphanadopt, or delete when the live ownership marker confirms it’s chant’sThe one decision the marker makes — never the snapshot
newly observednoopDeclared and live; no snapshot baseline to drift against yet
unchangednoop (when declared)Declared and live, identical
disappearedcreate if still declared, else noopA snapshot-history signal; the plan reads live + source, not snapshot age

The “when declared” qualifiers are where the axes diverge: an undeclared resource that also drifted or is unchanged is still an orphan to the plan — the snapshot history doesn’t change that it’s live-but-undeclared, so ownership decides between adopt and delete.

--report gitlab-mr emits the plan as the JSON GitLab reads for its merge-request plan widget:

Terminal window
chant lifecycle plan prod --report gitlab-mr > tfplan.json
# {"create":3,"update":1,"delete":0}

Declare the file as artifacts:reports:terraform and GitLab renders “3 to add, 1 to change, 0 to delete” on the MR. The widget format is generic — any tool that emits this JSON gets it — so the plan maps onto it directly. Only the mutating actions count: adopt and noop are excluded, since the widget has no column for live-but-undeclared or no-change.

The widget label always reads “Terraform” — that is GitLab’s fixed string, not a claim chant makes. The GitLab lexicon ships an MrPlanReport composite that wires the job for you.

A single lexicon may report both resources (entity-keyed, via describeResources()) and artifacts (context-keyed, via listArtifacts()); lifecycle diff --live shows them in separate sections. Artifacts use a four-category two-way diff (no “declared” axis):

CategoryMeaning
artifacts addedObserved now, not in last snapshot
artifacts removedIn last snapshot, gone now
artifacts changedIn both; metadata changed
artifacts unchangedIn both; metadata identical

For the conceptual distinction between resources and artifacts, see Drift Detection — Resources and artifacts.

See the Runtime observation coverage matrix on the Lexicons overview page for the canonical version with query-mechanism notes. Summary:

LexicondescribeResources()listArtifacts()
AWS
Temporal
K8s
GCP (via Config Connector)
Azure
Helm
Docker
GitHub / GitLabN/A — see lexicon READMEs

Lexicons that implement neither method are warn-skipped — --live doesn’t fail the whole command for them.

The Temporal lexicon resolves its connection via the chant config: lifecycle snapshot <env> (or --live against <env>) looks up temporal.profiles.<env> in your chant.config.ts and falls back to temporal.defaultProfile. The same profile model is used by chant run. With #39’s entity-prop pass-through, namespaces/schedules/search-attributes are mapped back to chant entity names when the declared props.name (etc.) match the server-side identifier.

The K8s lexicon shells out to kubectl get <kind> <name> [-n <namespace>] -o json per declared entity. The cluster context comes from the user’s kubeconfig — there’s no separate profile model. Twenty common resource types (Deployment, Service, ConfigMap, Secret, Namespace, Pod, PVC, ServiceAccount, Job, CronJob, Ingress, NetworkPolicy, RBAC roles + bindings, ReplicaSet, StatefulSet, DaemonSet) are covered out of the box; CRDs and uncommon types are warn-skipped.

The GCP lexicon uses the same kubectl shell-out path against Config Connector CRDs. The entity type’s GVK derivation matches the serializer (GCP::Storage::Bucketstoragebucket.storage.cnrm.cloud.google.com), so any of the ~300 chant-supported GCP resources work without per-type configuration. Ready=True on the resource’s status is mapped to READY; otherwise the condition’s reason is surfaced.

The Azure lexicon shells out to az resource show --resource-group <env> --name <name> --resource-type <type> -o json per declared entity. The <env> argument is treated as the Azure resource group name. properties.provisioningState becomes the status. Top-level resources only — nested ARM types (e.g. Microsoft.Storage/storageAccounts/blobServices) are warn-skipped since az resource show doesn’t accept compound type names directly.

Snapshots are pushed to the orphan chant/lifecycle branch with git push --force-with-lease. If two operators (or a CI job + a human) take a snapshot for the same remote at the same time, the second push is rejected rather than silently overwriting the first. You’ll see:

error: Another snapshot completed for chant/lifecycle after this run started (env: prod).
hint: Pull and retry: `git fetch origin chant/lifecycle:chant/lifecycle` && `chant lifecycle snapshot prod`.

Pull the remote ref and re-run — the first snapshot is preserved, the second proceeds against the updated baseline.

In a multi-stack project, answer “which stacks does this change affect?” without planning everything. Read-only — it returns the set; fanning lifecycle plan / an ApplyOp over it is an Op you compose.

Terminal window
chant lifecycle affected --base origin/main
chant lifecycle affected --base origin/main --include-dependents --json

“Affected” is two distinct things, because chant serializes cross-stack references symbolically:

  • Directly changed — the stack’s built artifact differs between base and head. Caught by artifact diff over deterministic builds, so a comment-only or refactor-with-no-output-change edit is correctly excluded.
  • Operationally affected (dependents) — a stack whose own artifact is unchanged but which consumes an upstream export whose value changed. Its bytes don’t move, yet it may need re-apply. Caught by walking the cross-stack graph (chant graph --stacks) from the directly-changed set, with --include-dependents.

A stack whose inputs come from outside synthesis (deploy-time parameters) cannot be judged from a source diff — it is reported as indeterminate (“external-input — cannot confirm from source”), not silently included or excluded.

Builds are obtained lazily: head is the in-place build of the working tree (or --head <ref>); base is built from --base <ref> in a single throwaway worktree (auto-removed), or skipped entirely if you supply pre-built sources via the library. Because build is deterministic, a cached or supplied base is as trustworthy as a rebuild.

--json emits { changed, dependents, indeterminate }. The same logic is exported as affectedStacks() for programmatic use.

Composing it. chant returns the set; you drive the fan-out. For example, plan only the affected stacks in CI:

Terminal window
chant lifecycle affected --base "$BASE_SHA" --include-dependents --json \
| jq -r '.changed + .dependents | unique[]' \
| while read stack; do chant lifecycle plan "$ENV" "$stack"; done

The same shape works from an Op you write — affected is the primitive; the fan-out (plan, gate, apply) is yours to compose. chant never blindly applies the set.

FlagMeaning
--base <ref>Base git ref to diff against (required).
--head <ref>Head ref (default: the working tree).
--include-dependentsAdd downstream consumers of changed stacks.
--jsonEmit the result as JSON.

List the history of snapshots. Omit env to show all environments.

Terminal window
chant lifecycle log # all environments
chant lifecycle log dev # just dev
CodeMeaning
0Success
1Error (invalid environment, API failure, no snapshot found)
  • chant build — build current output for diffing
  • Configuration — declare environments in chant.config.ts
  • Watching Lifecycle — wire lifecycle snapshot + lifecycle diff --live to a TemporalSchedule via the WatchOp composite
  • Implementing Observation — for lexicon authors, how to plug into --live via describeResources() / listArtifacts()