chant components
Synopsis
Section titled “Synopsis”chant components release <env> --component <name> --digest <sha256:...> [--git-sha <sha>] [--run-id <id>] [--actor <name>] [--json]chant components status [env] [--live] [--json] [--compare-to <env>]chant components export <env> --component <name> [--digest <manifestDigest>] -o <dir> [--json]chant components fan-out (--base <ref> [--head <ref>] [--include-dependents] | --from-affected <file>) [--env <env>] [--gate <name> | --wave-gate <name> [--canary <name>] [--wave <n>]] [--dry-run] [--json] [--resume <file>] [--seed-outputs <file>] [--dump-outputs <file>] [--progress-json | --events-json]chant components pr-plan --base <ref> --pr <n> [--env <env>] [--gate <name>] [--forge github|gitlab|forgejo] [--member <name>] [--output <dir>] [--json] [--events-json] [--report-url <url>]chant components pr-apply --base <ref> [--pr <n>] [--env <env>] [--gate <name>] [--forge github|gitlab|forgejo] [--require-review] [--member <name>] [--resume <file>] [--output <dir>] [--json] [--events-json] [--report-url <url>]chant components promote --to <env> [--from <env>] [--allow-skip] [--component <name> [--digest <sha256:...>] | --digest <component>=<sha256:...> ...] [--dry-run] [--json] [--seed-outputs <file>] [--run-id <id>] [--actor <name>] [--events-json]chant components rollback <env> (--component <name> [--digest <sha256:...>] | --release <label|digest>) [--dry-run] [--json] [--seed-outputs <file>] [--run-id <id>] [--actor <name>] [--events-json]chant components redeploy <env> --component <name> [--digest <sha256:...>] [--dry-run] [--json] [--seed-outputs <file>] [--run-id <id>] [--actor <name>] [--events-json]Description
Section titled “Description”chant components holds the release ledger, status surface, and build-archive export for component deploys: the recorded half of build & deploy observability. See Observability for the conceptual model. The release ledger lives on the same chant/lifecycle orphan branch chant lifecycle snapshots already use, at <env>/releases.jsonl, with one immutable JSON line per recorded deploy.
components fan-out is the odd one out on this page. It runs rather than records, taking a change signal and applying the components downstream of it in a derived order. It sits here because it is the component subsystem’s own dispatch path, next to the ledger those deploys write to.
components release below is the standalone, explicit way to append a record. chant run --components auto-emits one on every successful deploy, so most projects never call components release directly. Use it for CI compositions and Verify phases outside chant run’s own execution.
Subcommands
Section titled “Subcommands”components release <env>
Section titled “components release <env>”Append one immutable release record: (component, env, artifact digest, git sha, run id, timestamp, actor).
chant components release prod \ --component search-service \ --digest sha256:9fae3d7c... \ --git-sha "$(git rev-parse HEAD)" \ --run-id "$GITHUB_RUN_ID" \ --actor "$GITHUB_ACTOR"| Flag | Meaning |
|---|---|
--component <name> | Required. The component this deploy released. |
--digest <sha256:...> | Required. The promoted artifact’s content digest — the join key with the build archive/ledger. |
--git-sha <sha> | Git commit the deploy was built from. Defaults to the current HEAD. |
--run-id <id> | Orchestrator/CI run identifier. Defaults to $GITHUB_RUN_ID or $CI_PIPELINE_ID, else a locally generated id. |
--actor <name> | Who/what triggered the deploy. Defaults to $GITHUB_ACTOR, $GITLAB_USER_LOGIN, or $USER — but must resolve to something; an unattributed record fails rather than falls back to a placeholder. |
--json | Print the recorded record as JSON instead of a human summary. |
The timestamp is always a real new Date() taken by the CLI at record time, never threaded in from elsewhere or re-derived later. A record is never edited or replaced: recording again appends a new line.
Pushes to the remote afterward using the same --force-with-lease protocol snapshots use; a concurrent write from another operator is rejected with a recovery hint rather than silently overwritten.
components status [env]
Section titled “components status [env]”Reconcile the release ledger against live truth (when --live is passed) or report the ledger alone.
chant components status prod # ledger only — no live querychant components status prod --live # reconcile against live + ownershipchant components status prod --live --json # stable JSON contractchant components status prod --compare-to staging # cross-env digest comparisonchant components status # every environment with release recordsWithout --live, every recorded component reports unknown reconciliation, since the ledger alone can’t confirm live state. With --live, chant components status reuses the same ownership-aware classification lifecycle plan already computes (buildChangeSet) to produce one of:
| Reconciliation | Meaning |
|---|---|
reconciled | Recorded, and live evidence confirms the component is present and chant-owned. |
unrecorded | Live and owned, but no release record exists — deployed outside the recorded path. |
stale | Recorded, but nothing observed live now. |
drifted | Recorded and live, but what was observed contradicts the release (see below). |
unknown | No live evidence requested, or live state could not be read. |
A component is drifted when a resource it owns was read live and any of these hold:
- the resource reports
attributes.digestand it matches neither the recordeddigestnor itsinputDigest, or it reportsattributes.gitShaand it differs from the recordedgitSha(an abbreviated sha of 7+ characters matches by prefix); - its configuration changed against the last lifecycle snapshot, the same
updateverdictlifecycle plangives, and that snapshot was taken at or after the release (a snapshot older than the release would count the release’s own changes); - since that snapshot, chant’s ownership marker disappeared (
ownedtoforeign) or now names a different stack or environment.
A resource that could not be read reports unknown, never drifted. A resource that reports no identity attribute, or unknown ownership, is no evidence either way. Lexicons opt in to the identity check by setting those two attributes.
--compare-to <env> answers “which build is in <env>, and is it the one tested in <compare-to>” as a single query, per component:
chant components status prod --compare-to staging --json{ "rows": [ /* ... */ ], "comparisons": [ { "component": "search-service", "envA": "prod", "envB": "staging", "digestA": "sha256:9fae3d...", "digestB": "sha256:9fae3d...", "same": true } ]}| Flag | Meaning |
|---|---|
--live | Query lexicon plugins’ describeResources() now and reconcile against ownership + drift, instead of reporting the ledger alone. |
--json | Emit a stable array of rows (or { rows, comparisons } with --compare-to). |
--compare-to <env> | Cross-check the requested env’s recorded digest against another environment’s, per component. |
components export <env>
Section titled “components export <env>”Materialize a persisted build archive manifest to a portable, downloadable directory: every image/template/asset/sbom entry copied byte-for-byte from where the build that produced it left it on disk (its archive-relative path, resolved against the current working directory) into -o <dir>, with a self-describing manifest.json beside them. It copies what a prior chant build/run --components produced and regenerates nothing, so the export is byte-identical to what was built (promote-by-digest holds).
chant components export prod --component search-service -o ./dist/search-servicechant components export --digest sha256:9fae3d7c... -o ./dist/search-service --jsonManifest resolution:
--digest <manifestDigest>reads the manifest directly, independent of any environment or release record.- Without
--digest, resolves “the manifest behind this component’s most recent successful run” in<env>: the latest release record’s ownmanifestDigestwhen one was recorded, else a reverse lookup by its promoted artifact digest, the same joinchant components statusperforms.
| Flag | Meaning |
|---|---|
--component <name> | Required unless --digest is given. Which component’s most recent recorded build to export. |
--digest <manifestDigest> | Export this build archive manifest directly, bypassing environment/component resolution. |
-o, --output <dir> | Required. Directory the archive is materialized into. |
--json | Emit a structured per-entry summary instead of a human-readable list. |
A source file missing on disk (the build ran somewhere else, or its output was since cleaned) is reported per entry, and the command exits non-zero, so an incomplete export is never mistaken for a complete one. Run it from the same checkout the build that produced the manifest ran in.
{ "component": "search-service", "manifestDigest": "sha256:9fae3d...", "outDir": "/abs/path/dist/search-service", "entries": [ { "kind": "template", "path": "search.template.json", "digest": "sha256:...", "status": "materialized" }, { "kind": "image", "path": "dist/app.tar", "digest": "sha256:...", "status": "materialized" } ]}components fan-out
Section titled “components fan-out”Run a change out across the components downstream of it, in an order chant derives from the source. See Orchestration for the model behind it.
chant components fan-out --base origin/main --env prod --dry-runchant components fan-out --base origin/main --env prod --gate release --resume .chant/fan-out.jsonchant approve fan-out release --plan jcs1-sha256:9fae3d7c...chant components fan-out --base origin/main --env prod --gate release --resume .chant/fan-out.jsonWhere the change signal comes from
Section titled “Where the change signal comes from”Exactly one of --base and --from-affected is required. Both at once is refused, and so is neither, because defaulting one way or the other would let a stale file silently beat a fresh diff, or a fresh diff silently beat a file somebody meant to use.
--base <ref> derives the signal in place, running the same artifact diff chant lifecycle affected runs. That is the one-command shape for a developer working in a checkout. --from-affected <file> reads the JSON that chant lifecycle affected --base <ref> --json already wrote, which is what a CI job wants when an earlier step ran the diff and there is no reason to build twice. Reading a file builds nothing, so this form works in a project whose lexicon plugins are not installed.
Either way the signal names stacks. A component claims a stack through its own deploy steps, so the join to components needs no fan-out config restating a relationship the composition already makes. A changed stack that no component deploys is reported as a warning naming that stack, because it is a hole in the fan-out’s coverage.
--include-dependents is handed to the diff and folded into the changed set before the join.
A unit that is not chant source never shows in the artifact diff. The terraform lexicon answers for its roots from the changed paths instead. A root in terraform.roots is changed when a changed file sits in its directory, in a local module it calls, or is one of its var files. --base adds those roots to the signal.
The printed derivation
Section titled “The printed derivation”--dry-run prints the derivation and dispatches nothing. A dispatching run prints the same block first, so what ran always follows why it was picked.
fan-out: 3 selected, 4 unaffected, 0 indeterminate wave 1: cluster-a wave 2: app-one, app-two seeded from an earlier run: net not running (4): app-three unaffected billing unaffected cluster-b unaffected net unaffected plan: jcs1-sha256:9fae3d7c... approve: chant approve fan-out release --plan jcs1-sha256:9fae3d7c...Each line answers a question an operator would otherwise have to ask.
- The waves are the derived order shown as a graph. A flat list could not show that the order came out of each component’s
dependsOnrather than out of somebody’s hand. - Every component that is not running is named with its reason, one of
unaffected,indeterminate,already-appliedorblocked. Ablockedrow also names the failure it was reached from, asblocked by "cluster-a". Absent from a list and deliberately held back would read identically without this. indeterminatehas a count on the first line, and components a source diff could not judge that the walk never reached get a line of their own (a source diff cannot judge these, and the walk did not reach them: ...). It is the third outcome, and a two-state render would silently fold it into one of the other two.seeded from an earlier runlists dependencies of selected components that are not themselves running. Their outputs are taken on trust from a run that happened earlier and somewhere else, which is the price of running a subset instead of the whole graph.plan:carries the digest verbatim and unabbreviated (shortened above to fit this page). It is the exact stringchant approve --plantakes, so truncating it for width would make the line useless for the one job it has.approve:appears only when--gate <name>was given, and is the whole command to run.
--json puts JSON on stdout, writes nothing else there, and replaces the human render rather than accompanying it. Under --dry-run the payload is the plan. Without it the payload is the run result, which carries the plan it dispatched under plan. A gated run still prints its approve line on stderr, so a CI job reading stdout as JSON sees the pending gate in its log.
One gate over the whole set
Section titled “One gate over the whole set”--gate <name> puts a single approval over the ordered set, under the op name fan-out. Twenty components behind one fan-out is one approval, not twenty. The gate is evaluated once, before the first wave, bound to the plan’s digest by the same plan-bound gate mechanism a gated Op uses.
Nothing runs while it stands open. The run prints the pending fact and exits 3.
gated: nothing ran. Waiting on "release" on "fan-out".approve : chant approve fan-out release --plan jcs1-sha256:9fae3d7c...The digest identifies the derivation. It covers the ordered selection and the waves, plus the seeds and the edges each component came from. It does not cover what each component would apply, so editing a template without changing the graph leaves the digest, and a standing approval, where they were. planFanOut takes per-component input digests when a caller has them, and this command does not supply any yet. A resolution recorded against a different digest does not let this fan-out through, since a fan-out that derives differently is a different change. Each component’s own authored gates still decide per component. This adds a gate over the set without removing the ones inside it.
One gate per wave
Section titled “One gate per wave”With --wave-gate <name>, each wave gets a gate of its own instead of one gate over the set. For wave n it is <name>-wave-<n> under the op fan-out, bound to that wave’s set digest. Use it when a change is too large or too risky to approve in one read. A module bump across many Terraform roots is the usual case. A canary goes first, and the rest wait until it applies.
chant components fan-out --base origin/main --env prod --wave-gate release --canary app-dev --resume .chant/fan-out.jsonchant approve fan-out release-wave-1 --plan jcs1-sha256:4c1e...chant components fan-out --base origin/main --env prod --wave-gate release --canary app-dev --resume .chant/fan-out.jsonchant approve fan-out release-wave-2 --plan jcs1-sha256:b07a...A wave is planned only when it is reached, after the wave before it applied. Each component in the wave is asked what it would change: every step whose capability can plan (terraform-apply can) is planned with its wiring resolved against the outputs the earlier waves wrote. A later wave planned up front would read outputs that do not exist yet, and its digest would describe values nobody will deploy. The member for a planned step is what it planned, the root for terraform-apply, and its digest is the one a gate on that root alone binds. A component with no step that can plan is a member under its own name, with the composition digest its own gates bind. The wave’s digest is the change-set digest of its members. The apply then uses the plan that was approved.
The run stops at the first wave nobody approved, exits 3 and prints the approve line. Earlier waves in the same run have applied by then. An approval names a digest. One recorded with no digest, or with another wave’s digest, does not let a wave through. If the set changed after someone approved it, the run stops at that wave and applies nothing in it. The message names both digests.
wave 1 changed after it was approved, so nothing in it was applied. approved: jcs1-sha256:4c1e...; planned now: jcs1-sha256:e93d.... Read the new plan, then approve it: chant approve fan-out release-wave-1 --plan jcs1-sha256:e93d...--canary <name> (repeatable, or comma-separated) puts components in wave 1 whatever the graph order. The remaining waves follow the graph. A canary the change does not select has nothing to apply and is left out.
Wave numbers come from the derivation, so on a resumed attempt wave 3 is still wave 3 and its approval still applies. With --resume, the record keeps one entry per wave it reached. An entry names the wave’s gate and its set digest. It lists each member’s plan digest and says whether the wave applied, failed or is waiting. The re-run plans each remaining wave afresh, since the gate is about the plan now, and never applies a component the record says applied. A failed component blocks its dependents, as in the whole-set mode. Its retry is planned again and needs a fresh approval, because what is left is a different set from the one approved.
--wave <n> runs wave n and no other, which is the shape for CI with one job per wave. It refuses while an earlier wave has work left, and a wave past the last one the change has exits 0. chant build --components --generate gitlab|github|forgejo --wave-gate <name> writes that pipeline. Job wave-k runs --wave k and needs wave-(k-1). It hands the --resume record on as an artifact, even when it stops at a gate. Approve, then re-run the job that stopped. The pipeline measures the change from HEAD~1. Pass --base <ref> to the build command to measure it from somewhere else; every job must use the same ref, so it is a fixed one.
--gate and --wave-gate are refused together. Use --gate when one person reads the whole change once, the fan-out is small, and its components do not read each other’s fresh outputs at plan time. Use --wave-gate when the change should apply in stages, when a later root’s plan depends on what an earlier one writes, or when the set is too large to review as one.
Failure semantics
Section titled “Failure semantics”A failed component stops its own subtree and nothing else. Components beneath it record as blocked naming the failure rather than as failed, because they did not fail, which keeps the next attempt readable. Branches sharing no edge with the failure run to completion.
applied: app-three, cluster-b, netfailed: cluster-a app-one: blocked by "cluster-a", so it never ran app-two: blocked by "cluster-a", so it never ranfan-out failed: 3 applied, 1 failed, 2 blockedWith --events-json, each blocked component settles in the stream as blocked with blockedBy naming the failure, right after the failing wave settles. On a resumed run each component the earlier attempt applied settles as already-applied before the first wave. See run events.
This is where a fan-out departs from chant run --components all, which stops the whole run at the first failed component and goes on doing so. There the user asked for everything, so a failure means the estate is in a state nobody described. Here the order was derived from the source, so a branch independent of the failure is known to be independent rather than assumed to be, and stopping it buys nothing while costing a second approval to finish.
Finishing an attempt that stopped
Section titled “Finishing an attempt that stopped”--resume <file> names an attempt record. The run reads it before it starts. It rewrites the record as each component settles and once more when it finishes, including after a failure and after a gate, because the next attempt needs to know what this one got through. A fan-out killed partway through, by a cancelled CI job or ctrl-c, leaves a record naming every component that had applied by then. The record is replaced through a rename, so a kill during the write leaves the previous record intact.
Each completed component’s outputs are saved alongside its name. A component still to run that reads one of them through stackOutput() gets the saved value, so the re-run needs no --seed-outputs for work this fan-out already did. A saved output wins over a --seed-outputs entry for the same component. Stack outputs end up in this file, so keep it wherever --dump-outputs files are kept.
Repeating the identical command finishes the work. The record narrows the plan before the gate is decided, and the digest is carried rather than recomputed, so an approval already given still stands. The re-run prints what it is skipping as already-applied and dispatches only the rest.
Only the earlier attempt’s completions are carried forward. Its failures are recorded in the file for whoever has to read it, but feeding them back as progress would block their subtrees again on the run meant to retry them.
--dry-run reads the record too, so a plan-only invocation alongside --resume shows what is left and prints the digest the earlier approval was given for.
A record whose digest does not match the fan-out derived now is refused with a warning, and its progress is not applied. The derivation changed, which makes this a different change, and carrying cluster-a already applied into it would be a claim about work nobody did.
Seeding and progress
Section titled “Seeding and progress”--seed-outputs <file> (repeatable) loads component outputs before the run, so a stackOutput() or @<dep>.publish.* reference into a component the plan lists under seeds resolves. --dump-outputs <file> writes what the run holds afterwards. --progress-json streams one NDJSON progress record per line to stdout as the waves execute, and --events-json streams the run-event stream instead. These behave as they do on chant run --components.
| Flag | Meaning |
|---|---|
--base <ref> | Derive the change signal here, by diffing built artifacts against this git ref. Required unless --from-affected is given. |
--head <ref> | Head ref for that diff. Defaults to the working tree. |
--include-dependents | Add downstream consumers of changed stacks to the signal before the join to components. |
--from-affected <file> | Read the change signal from JSON chant lifecycle affected --json already wrote. Required unless --base is given. |
--env <env> | Environment each selected component deploys into. Defaults to local. |
--gate <name> | One approval over the whole derived set, answered by chant approve fan-out <name> --plan <digest>. |
--wave-gate <name> | One approval per wave, each bound to its wave’s set digest and answered by chant approve fan-out <name>-wave-<n> --plan <digest>. Not with --gate. |
--canary <name> | With --wave-gate, components that form wave 1. Repeatable, or comma-separated. |
--wave <n> | With --wave-gate, run wave n and no other: one CI job per wave. |
--dry-run | Print the derivation and dispatch nothing. |
--json | Emit the plan (with --dry-run) or the run result on stdout, and nothing else on stdout. |
--resume <file> | Attempt record read before the run and written after it, so repeating the command finishes what stopped. |
--seed-outputs <file> | Load component outputs before the run. Repeatable; a later file wins on a key two files share. |
--dump-outputs <file> | Write the outputs the run holds to this file afterwards. |
--progress-json | Stream one NDJSON progress record per line to stdout while the waves execute. |
--events-json | Stream the run’s events to stdout, one run-event.schema.json line each. |
components pr-plan and components pr-apply
Section titled “components pr-plan and components pr-apply”The pull-request loop. pr-plan runs on a pull request. It plans the components the change reaches and the ones that depend on them, and shows the plan. pr-apply runs after the merge. It plans the same members again and applies them only if a reviewer approved that plan. Generated CI for pull requests writes the pipeline that runs both on GitHub, GitLab and Forgejo.
chant components pr-plan --base origin/main --pr 12 --env prodchant approve pr-12 pr-apply --plan jcs1-sha256:3e43... --approver github:alice --signchant components pr-apply --base "$BEFORE_SHA" --pr 12 --env prodBoth measure the change from the merge base of --base and HEAD, and select components the way fan-out --base does. Every selected member is planned before anything applies. A member that reads another’s output through stackOutput() is planned with the value that output has now.
The gate is pr-apply under the op pr-<n>, so one pull request’s approval never answers another’s. It binds the change-set digest of every planned member. pr-plan reads the gate ledger to report where the approval stands and writes nothing to it. pr-apply decides the gate against the digest it plans. When an approval stands for another digest, the plan changed after review. Then it applies nothing and exits 3 with both digests named.
Before it decides the gate, pr-apply stores the change set it planned on chant/lifecycle under its digest, each entry mapped onto the member’s graph (#4022). chant change-set show <digest> prints it after the job is gone, before an approval and after one. The mapping runs the member’s chant graph --format ir once per new digest. Source that fails chant lint has no graph to read. The change set is then stored with no nodes, and pr-apply prints a warning.
The plan changed after review, so nothing was applied. approved: jcs1-sha256:d181...; planned now: jcs1-sha256:cef9.... Read the new plan, and if it is right, approve it: chant approve pr-2 pr-apply --plan jcs1-sha256:cef9... --approver <you> --sign; then run the apply again.Each run writes three files under --output (default .chant/pr). pr-plan.json or pr-apply.json is the report, under the pr-report schema. change-set.json is the change-set document, which chant change-set summary reads. pr-note.md is the note rendered from the report. It shows a row per member and the digest, then the approve command and the grouped summary.
pr-apply prints a line on stderr as each wave starts and as each component settles, with or without --events-json, so a job log shows the waves in order while they run:
wave 1/3: net net: appliedwave 2/3: app, api app: failed api: applied web: blocked by app, not run--report-url <url> copies the report, the change set and, with --events-json, the stage’s run events to <url>/<pr>/<commit>/, where a reader outside CI can fetch them. The events go up again as each wave and component settles, so the URL shows an apply in progress. <url> is s3://<bucket>/<prefix>, uploaded with the AWS CLI and the credentials in the job’s environment, or file://<directory>. A failed upload is a warning and the stage’s outcome stands. Pull request reports gives the layout.
With --events-json each stage also keeps its stream in a file on the host, as chant run events describes. In CI, set CHANT_RUN_EVENTS_DIR to a directory the job keeps as an artifact, such as .chant/pr/run-events, since a CI checkout’s git directory is gone with the job.
Pass --forge to post the note on the pull request and set the chant/plan or chant/apply commit status. The note carries a marker per environment, so every push and the apply update one note. When pr-apply has no --pr, it asks the forge which pull request merged HEAD. A forge that refuses a call is a warning, and the outcome stands.
Finishing an apply that failed partway
Section titled “Finishing an apply that failed partway”pr-apply --resume <file> keeps the same attempt record fan-out --resume keeps, with the change set the gate approved in it. The apply writes it before the first member runs and again as each component settles. When a member fails, repeat the same command on the same commit with the same file:
chant components pr-apply --base "$BEFORE_SHA" --pr 12 --env prod --resume .chant/pr/attempt.jsonThe re-run skips the components that applied and plans the rest against the outputs the approved plans read. It then checks each member it plans against the approved change set. A member is covered when it plans to the approved digest, or when every change it plans is one the approved plan made: the same address and action (or the create or delete left of an approved replace), and each attribute written to the approved value or to a value the approved plan knew only after apply. A sensitive value is covered only where the approved plan wrote it as sensitive too. When every member is covered and the approval still stands for the approved digest, the rest applies with no fresh approval, and the report’s resumed.applied and the note name what the earlier attempt applied.
The record carries nothing when it was written for another pull request, gate or commit, or for a change that selects other components, or when the approval no longer stands, or when a member left over plans a change the approved plan did not make. Then pr-apply prints why and runs as it would with no record: it plans every member, and the new digest needs an approval of its own. A record whose change set does not hash to the digest it names carries nothing either.
The generated pull-request pipeline passes --resume .chant/pr-resume/pr-apply.json and keeps the file in the CI cache under the pushed commit, so re-running a failed apply job finishes it.
| Flag | Meaning |
|---|---|
--base <ref> | The commit the change is measured from. On a pull request, the target branch’s commit. After a merge, the commit the push replaced. Required. |
--pr <n> | The pull or merge request. Required for pr-plan. |
--env <env> | Environment each member deploys into. Defaults to local. |
--gate <name> | The gate’s name. Defaults to pr-apply. |
--forge <kind> | github, gitlab or forgejo. Post the note and the status, with the client read from the CI job’s environment. |
--require-review | pr-apply only. Count an approval only from someone with a standing approving review on the pull request, named the way the forge names them (github:<login>, gitlab:<login>, forgejo@<host>:<login>). Needs --forge. |
--member <name> | A workspace member’s loop, run in the member’s directory. The gate is recorded under pr-<n>-<member>, the note and the statuses carry the member’s name, and the report gains member. The name must be the member that owns the current directory. The member’s generated pipeline passes it. |
--resume <file> | pr-apply only. Write the attempt record here, and finish an earlier attempt it records under the same approval. See above. |
--output <dir> | Where the report, the change set and the note go. Defaults to .chant/pr. |
--seed-outputs <file> | Outputs to use instead of reading them, as on fan-out. |
--events-json | Stream the stage’s events to stdout, one run-event.schema.json line each: a plan phase with a step per component, the gate’s decision as a gate phase, and on pr-apply the apply’s waves and components, a component it did not start settling blocked or already-applied. This is the stages’ stream; they take no --progress-json. Refused beside --json, which prints the report to stdout too. See run events. |
--report-url <url> | Copy the report, the change set and, with --events-json, the events to <url>/<pr>/<commit>/, an s3:// or file:// URL. See above. |
--json | Print the report on stdout. |
components promote
Section titled “components promote”Deploy the artifact digests one environment’s release ledger records to another environment, without a build.
chant components promote --to prod --dry-runchant components promote --from staging --to prod --dry-runchant components promote --from staging --to prodchant components promote --from staging --to prod --component search-service --digest sha256:9fae3d7c...chant components promote --from staging --to prod --digest search-service=sha256:9fae3d7c... --digest web=sha256:41c0...Without --component, every component this checkout declares that has a release in the --from ledger is promoted, each at its latest recorded digest. A recorded component the checkout no longer declares is listed as not promoted and left alone. --component <name> promotes that one component, and --digest picks an earlier release of it. Without --component, --digest <component>=<digest> can be repeated. It promotes exactly the components it names in one run, in dependency order, each at the release that recorded that digest. That is how a generated promote job promotes the releases its own pipeline run built, since the latest release in --from may belong to another run. A digest the --from ledger never recorded for the component fails before anything runs, and the error lists the digests it did record. So does <component>= with no digest, which is what a pipeline passes when the job that deployed the component recorded no release.
A promote runs the component’s deploy composition against the --to environment with its build-time steps taken out: docker-build, zip-package, jvm-build, generate-sbom and extract-config-bom. Everything else runs the way chant run --components <name> --env <to> would run it:
- The publish step still runs. It moves the archived bytes to the target environment, which is how the build archive promotes by digest. Its output is checked against the recorded digest, and a publish that yields any other digest fails the component before the steps after it run. That happens when the archive on disk is not the one the source release was built from, so run a promote where that archive is present: the job that built it, or one that restored it. A pipeline from
chant build --components --generate <lexicon> --promote-to <env>restores it for you: each component job uploads its archive, and a separate promote job downloads it before it runs the promote. This works for github, gitlab and forgejo. - Gates run. A gate in the composition stops the promote with exit 3 and the
chant approveline, the same as any deploy to that environment, and running the same promote again after the approval continues past it. The approval is bound to the--toenvironment and to this promote’s plan, which includes the digest being promoted. An approval for a plain deploy, or for a promote of another digest, does not pass the gate. - The remaining steps run against the target environment as they are written in this checkout. The artifact comes from the source release but the deploy steps come from here, so the promote warns when this checkout is not the commit the artifact was built from.
Promoting a release
Section titled “Promoting a release”--release <label|digest> deploys under a release cut from a green commit (#4026). The release must have a target for --to. Each template a promoted component’s deploy reads is then the one that target recorded, not the file in this checkout. Its bytes are read from chant/lifecycle and checked against the target’s digest, the way the publish output is checked against the recorded digest, and written under .chant/release-templates/<digest>/ for the step to read, then removed when the promote finishes. The promote refuses before anything runs when a template’s bytes do not hash to the target’s digest, when they are missing from the branch, and when the deploy reads a template the target does not record, which means this checkout’s deploy is not the one the release was compiled from. Each deploy record the promote writes carries releaseDigest, naming the release, and chant release list then lists --to among the environments that run it. A release cut from a commit that has since been revoked is refused with release-revoked before anything runs (#4033); the revoked tags are read locally, so fetch them with git fetch origin --tags. See revoked releases. A release of a project with no components has no component to deploy, so --release refuses it and names chant release export, which writes the recorded build output for the project’s own deploy to read. Without --release a promote works as before.
chant components promote --to prod --release v1.4.0 --dry-runThe promote also warns when the deploy it promotes was built from other bytes than the release’s. It reads the build manifest the source deploy record names (manifestDigest) and compares the entry at each of the release’s artifact paths with the release’s artifactDigests. A source deploy that names the release in releaseDigest matches. This is a warning rather than a refusal because the environment promoted from was built by its own chant run --components, and an image build is not reproducible, so its digest differs from the cut’s for the same commit.
The release gate
Section titled “The release gate”A gate a promote under a release reaches is a release gate (#4045, ws-104 questions 2 and 6). When the promoted compositions declare a gate, the promote first plans the release’s change set for --to and stores it on chant/lifecycle under its digest, which it prints. Each gate then binds the release in releaseDigest, the environment in environment and the change-set document’s digest in planDigest. An approval counts only when all three match.
Every release gate is recorded under the op release, whichever component reaches it, with the gate name its composition gives it. Components that use one gate name share one gate per environment, so one approval of the release’s change set passes all of them:
chant approve release prod-release --env prodThe approval copies the release, the environment and the digest off the pending fact. chant approve refuses a release gate whose release was cut from a revoked commit, with release-revoked, and one whose release file can’t be read, with release-unreadable. An approval for another release, another environment or another change set does not pass the gate. --component promotes a subset of the release, whose change set has another digest and needs its own approval. A change set with a provisional member is refused before anything deploys, since no approval covers it, and so is one with a member that failed to plan.
A component whose step can plan, such as terraform-apply, is planned by that step. Any other component is compared with what --to runs now, its latest deploy record and the release that record names: the artifact digest, each template the release records for the component, and the target’s parameters. Each value that differs is an attribute of one update entry, or of a create when --to has no deploy of the component. A deploy that names no release, or names one that can’t be read, is a hole, since nothing says what the environment runs.
An environment whose change set is empty passes its gates without an approval. A change set is empty when every member planned with no hole, and no entry is anything but no-op or read. A failed or provisional member is never empty. The promote writes a resolution for each gate with empty: true, the actor running the promote in resolvedBy, and the release, environment and digest. The gate counts it only after it reads the stored change set back and finds it empty, and quorum and signing rules don’t apply to it. The deploy record for each component then names the release, so a flag flip for prod alone moves through staging with no approval, and staging’s ledger still says which release it runs. A promote whose compositions declare no gate plans and stores nothing.
Some compositions cannot be pinned to one recorded digest, and a promote refuses them before anything runs: a component with no publish step (nothing in its deploy carries the digest), one with more than one publish step (a release record carries one digest), and one whose remaining steps read an output of a removed build step, such as @Build.archivePath.
A dependsOn edge to a component outside the promotion is dropped, as a single-component chant run --components drops it. --seed-outputs <file> loads the outputs such a dependency produced, for stackOutput() references into it.
The promote appends one record to the --to ledger for every component it deployed. The record carries the source release’s digest, git sha and archive digests, a fresh run id and actor, the approver of any gate it passed, and promotedFrom, which names the source release:
{ "component": "search-service", "env": "prod", "digest": "sha256:9fae3d...", "gitSha": "a1b2c3d", "runId": "4822", "timestamp": "2026-07-03T09:12:40.000Z", "actor": "octocat", "promotedFrom": { "env": "staging", "runId": "4821", "timestamp": "2026-07-02T18:04:11.000Z" }, "version": 1}A component that did not finish gets no record. The components that did deploy keep theirs. A ledger write that fails after a deploy exits 1 and says the deploy happened.
When a promote fails partway, the component’s in-run unwind undoes the steps it ran, and the --to ledger still records the release that was there before. If the unwind could not put everything back, chant components redeploy <to> --component <name> deploys that recorded release again. Once the cause is fixed, running the same promote again is the other way forward.
| Flag | Meaning |
|---|---|
--from <env> | The environment whose release ledger holds the digests. Defaults to the environment before --to in the member’s promotion order; required when the member declares none. |
--to <env> | Required. The environment to deploy them to. Must differ from --from. |
--allow-skip | Promote past a step of the promotion order, for example dev straight to prod. |
--release <label|digest> | Deploy under this release: the templates come from its target for --to, checked against their digests, and each record names the release in releaseDigest. See Promoting a release. |
--component <name> | Promote only this component. |
--digest <sha256:...> | With --component, promote this recorded digest rather than the latest one. |
--digest <component>=<sha256:...> | Without --component, promote only the named components, each at this recorded digest. Repeatable. chant run --components --digest-file writes the value. |
--dry-run | Print the plan (each component, its digest, the source release and the steps that will not run) and deploy nothing. |
--json | Emit the plan, the run result and the records written as one JSON document on stdout. |
--seed-outputs <file> | Load outputs from dependencies outside the promotion. Repeatable. |
--run-id <id> | Run id for the new records. Defaults as components release does. |
--actor <name> | Who ran the promote. Defaults as components release does, and must resolve before anything deploys. |
--events-json | Stream the deploy’s events to stdout, one run-event.schema.json line each, named by the run id the new records carry. Rollback and redeploy take it too. |
The promotion order
Section titled “The promotion order”The order of the environments array in the member’s chant.config.ts is its promotion order. With it declared:
--to prodwithout--fromreads the environment beforeprodin the array.- A
--fromthat is more than one step behind--tois refused, and nothing is deployed. The refusal names the skipped steps and ends with a fix line. Pass--allow-skipto promote it anyway. - A
--fromor--tothat is not a step of the order, such as apr-42environment matched by apr-*pattern, is not judged. Without--from, a--tooutside the order is refused, since there is no step before it.
The refusals carry a stable code in square brackets at the end of the message:
| Code | When |
|---|---|
promote-skips-environment | --from is more than one step behind --to, and --allow-skip was not given. |
promote-no-previous-environment | --from was left out and --to is the first step of the order. |
promote-to-not-in-order | --from was left out and --to is not a step of the order. |
chant build --components --generate --promote-to applies the same rule when it writes the promote job, so a skip fails at generation time and --allow-skip on the build is passed to the job. A member that declares no environments has no order. Promote requires --from and refuses nothing, as before. A step marked canary: true is recorded in the order and does nothing else yet.
components rollback <env>
Section titled “components rollback <env>”Redeploy an earlier release of one component in <env> from the digest its release record carries. With --release, redeploy every component of an earlier release at once.
chant components rollback prod --component search-service --dry-runchant components rollback prod --component search-servicechant components rollback prod --component search-service --digest sha256:4c1e0b...chant components rollback prod --release v1.4.0By default the rollback restores the newest release in <env>’s ledger with a digest other than the current one. A redeploy of the current digest therefore does not count as an earlier release. With --digest, it restores the latest release that recorded that digest instead. Naming the digest that is already current, or one the ledger never recorded for the component, is refused before anything runs.
The rollback runs the component’s composition against <env> with its build steps taken out, as a promote does, and it also replaces the publish step with a recorded-digest step that returns the recorded digest. The environment received that artifact when the release was first deployed, and the build archive it came from is usually gone by now. The recorded digest has to still be where that release put it, so a registry that has expired the image will fail the apply. Only the digest is known, so a composition whose steps read another publish output, such as @Publish.uri, is refused. The other refusals are the ones promote makes: no publish step, more than one, or a step reading a removed build step’s output.
Gates in the composition apply to a rollback exactly as they do to a deploy, including exit 3 and the chant approve line. The approval is bound to <env> and to the release being restored, so one given for a deploy or for another rollback does not pass the gate.
When the rollback succeeds, <env>’s ledger gains a record with the restored release’s artifact identities and a restores field naming that release. Run id, actor and approver are filled in as a promote fills them: When the release restored came from a release, the new record copies its releaseDigest, so the environment still reads as running that release (#4043).
{ "component": "search-service", "env": "prod", "digest": "sha256:4c1e0b...", "restores": { "env": "prod", "runId": "4790", "timestamp": "2026-06-28T11:20:05.000Z" }}This is a different mechanism from the saga unwind inside a single run, which undoes that run’s own steps when it fails. See Rolling back to an earlier release for how the two relate.
A rollback always moves to a release other than the current one, so it cannot recover from a deploy that failed partway. A failed deploy records nothing, and the ledger still names the last good release as current while the environment may be running part of the failed one. components redeploy puts the current release back. The same applies when a rollback itself fails: the ledger still names the release that was current before it.
A rollback or a redeploy that would put back a release cut from a revoked commit is refused before anything runs, with release-revoked, as promote --release refuses one (#4033). The refusal names a release to restore instead: the newest one <env> ran that is still kept and not revoked.
Rolling back a release
Section titled “Rolling back a release”--release <label|digest> restores every component of an earlier release in one run (#4035). It is a promote of that release into <env>, and it runs the way promote --release runs:
- Each component of the release is restored at the newest record in
<env>’s ledger that names the release, so the digest deployed is the one<env>ran from it. Its deploy reads the templates the release recorded for<env>, checked against their digests. - When a composition declares a gate, the release’s change set is planned against what
<env>runs now and stored onchant/lifecycle, and every gate binds it under the oprelease. An approval of it counts only for this release and<env>, and an empty change set passes without one. - One change set covers the whole rollback, so one approval passes it.
A release that <env> never ran one of its components from is refused, since no recorded digest says what to restore; promote it instead. A release <env> already runs is refused, unless a run left one of its components failed or gated (see When a deploy applies partly). Then the rollback is what puts that component back, and the component is a hole in the change set, so its gate waits for an approval. A revoked release is refused with release-revoked. A component of the release this checkout does not declare is listed and left alone. --release takes neither --component nor --digest.
When a deploy applies partly
Section titled “When a deploy applies partly”A promote, rollback or redeploy that deploys some components and not others, or fails on one, appends a deploy outcome to <env>/outcomes.jsonl on chant/lifecycle, beside the environment’s releases.jsonl and under the same member prefix (#4035). It names each component the run planned and what happened to it: deployed, failed, gated (stopped at a gate after the steps before it ran) or not-run, with the digest and release the run was deploying. A run that stops at a gate before deploying anything writes none, because the environment did not change. The command prints which components ended which way and the redeploy line for each one it left failed or gated, and --json gains outcome.
chant workspace status reads the outcomes with the ledger and shows the environment as mixed until each of those components has a newer release record: a redeploy, a rollback, or the same command run again.
| Flag | Meaning |
|---|---|
--component <name> | The component to roll back. Required unless --release is given. |
--digest <sha256:...> | Restore the release that recorded this digest, rather than the one before the current release. Needs --component. |
--release <label|digest> | Restore every component of that release through the release gate. See Rolling back a release. |
--dry-run | Print the release that would be restored and the steps that will not run, and deploy nothing. |
--json | Emit the plan, the run result and the record written as one JSON document on stdout. |
--seed-outputs <file> | Load outputs from dependencies. Repeatable. |
--run-id <id> | Run id for the new record. Defaults as components release does. |
--actor <name> | Who ran the rollback. Defaults as components release does, and must resolve before anything deploys. |
components redeploy <env>
Section titled “components redeploy <env>”Redeploy the release of one component that <env>’s ledger records as current. Use it after a deploy that failed partway, from chant run --components, components promote or components rollback.
chant components redeploy prod --component search-service --dry-runchant components redeploy prod --component search-servicechant components redeploy prod --component search-service --digest sha256:9fae3d7c...A deploy writes its release record only after it succeeds, so a failed one leaves the ledger on the last good release. The run’s own unwind undoes the steps it ran, but a best-effort compensation, or a crash during the unwind, can leave the environment running something else. components rollback cannot help here, because it refuses the release that is already current.
The redeploy picks the newest record for the component in <env> and runs it the way a rollback runs an earlier release. Build steps are taken out and the publish step is replaced by a recorded-digest step, so a composition a rollback refuses is refused here too. A gate in the composition stops a redeploy the same way it stops a rollback, with exit 3 and the chant approve command to run before trying again. Its approval counts only for <env> and the current release. An approval given for a plain deploy is refused, and so is one given while another release was current. Since the composition it runs is exactly what a rollback to that release runs, approving either one covers both. With --digest, the redeploy checks that the digest is the current one and refuses anything else before it runs, which catches a deploy recorded since you last read the ledger. An earlier digest is a job for components rollback --digest, and the error says so.
A successful redeploy appends a record to <env>’s ledger that carries the current release’s artifact identities, plus a redeploys field pointing at that release. The current digest stays the same, and the record shows who put the release back and when. A record that restores a deploy made under a cut carries that deploy’s releaseDigest as well:
{ "component": "search-service", "env": "prod", "digest": "sha256:9fae3d7c...", "redeploys": { "env": "prod", "runId": "4821", "timestamp": "2026-07-02T18:04:11.000Z" }}A redeploy that fails writes no release record, like any other deploy, and can be run again. It does append a deploy outcome naming the component failed, so the environment reads as mixed until the component is recorded again (see When a deploy applies partly).
| Flag | Meaning |
|---|---|
--component <name> | Required. The component to redeploy. |
--digest <sha256:...> | The digest you expect to be current. Anything else is refused. |
--dry-run | Print the release that would be redeployed and the steps that will not run, and deploy nothing. |
--json | Emit the plan, the run result and the record written as one JSON document on stdout. |
--seed-outputs <file> | Load outputs from dependencies. Repeatable. |
--run-id <id> | Run id for the new record. Defaults as components release does. |
--actor <name> | Who ran the redeploy. Defaults as components release does, and must resolve before anything deploys. |
Exit Codes
Section titled “Exit Codes”| Code | Meaning |
|---|---|
| 0 | Success (for fan-out and pr-apply, every selected component applied; pr-plan exits 0 whenever every member planned) |
| 1 | Error (missing required flags, unresolvable actor, concurrent ledger write, no manifest found, an export entry missing on disk; for fan-out, a component failed, or the change signal was unusable; for promote, rollback and redeploy, a digest missing from the ledger or not current, a composition that cannot be pinned, or a failed deploy) |
| 3 | pr-apply: refused, and nothing applied. No approval stands for the plan, the plan changed after review, the approver has not approved the pull request, or no pull request merged the commit. fan-out, promote, rollback and redeploy: stopped at a gate nobody has approved. For fan-out with --gate nothing ran, and with --wave-gate nothing in the gated wave or after it ran; for the others the steps before the gate ran, and only components that finished were recorded. |
See Also
Section titled “See Also”- Observability — the release ledger, build-ledger referrer discovery, and the reconciliation model this command implements.
chant lifecycle— snapshots,diff --live, andplan, whose ownership/drift classificationcomponents status --livereuses.- Build Archive — the content-addressed manifest and digest identity the release ledger joins against, and what
components exportmaterializes. - Orchestration covers the derived fan-out as a model, and where the order comes from.