Observability
Two different questions, two different ledgers, joined by one key. Conflating them is where teams lose the thread.
- What’s been built — the build ledger. Identity is content-addressed: a
sha256:digest, never a moving tag. - What’s been deployed — the release ledger plus live truth. Two sub-questions: what we recorded deploying, and what’s actually running.
chant already had the live-truth half — ownership markers and lifecycle diff --live compare declared vs. live, and snapshots on the chant/lifecycle branch give a forensic timeline. What was missing was the recorded half. This page covers the three pieces that fill it in: the release ledger, build-ledger referrer discovery, and the chant components status surface that joins them.
The release ledger
Section titled “The release ledger”Each deploy appends one immutable record to the chant/lifecycle orphan branch — the same branch snapshots already live on, at <env>/releases.jsonl:
component, env, artifact digest, git sha, run id, timestamp, actorchant components release prod \ --component search-service \ --digest sha256:9fae3d... \ --git-sha "$(git rev-parse HEAD)" \ --run-id "$GITHUB_RUN_ID" \ --actor "$GITHUB_ACTOR"--git-sha defaults to the current HEAD, and --run-id/--actor fall back to common CI environment variables (GITHUB_RUN_ID/CI_PIPELINE_ID, GITHUB_ACTOR/GITLAB_USER_LOGIN/USER) — but --actor has no silent placeholder fallback; an unattributed release record defeats the point of a ledger, so the command fails rather than guess.
The binding key is the digest — the same identity publish-image/load-image-on-host promote by. Because publish never rebuilds, that identity threads build → dev → staging → prod unchanged, so comparing environments is a digest comparison, never a guess through CI logs.
Build ledger: SBOM and referrer discovery
Section titled “Build ledger: SBOM and referrer discovery”The build archive manifest is already content-addressed and already enumerates a build’s contents, including an sbom-kind entry when a component composed generate-sbom. On top of that, the build ledger discovers an image digest’s referrers — the SBOM, SLSA provenance, and signature attached to the same digest by tooling like oras discover <digest> / cosign tree.
SLSA provenance generation and cosign signing are covered separately — see Supply-Chain Attestations for the attest-provenance/sign/verify capabilities that produce the provenance and signature referrers this section discovers. Referrer discovery itself is surface-and-consume only: the lookup is injectable (ReferrerLookup), defaulting to a no-op so a build-ledger query never requires network/registry access unless a real implementation is wired in:
import { buildLedgerEntries, type ReferrerLookup } from "@intentius/chant/lifecycle";
const orasLookup: ReferrerLookup = { async discover(digest) { // shell out to `oras discover` / a registry referrers API, return Referrer[] },};
const entries = await buildLedgerEntries(manifest, orasLookup);// entries[i].referrers: [{ kind: "sbom" | "provenance" | "signature", digest, mediaType, location? }]// entries[i].sbom: { mediaType, packageCount?, generator?, source: "archive" | "referrer" } | undefinedentries[i].sbom reads whichever source is available — the archive-carried sbom entry first (works for every artifact type and every publish backend, registry or not), the registry-discovered referrer otherwise (image artifacts only). Both trace back to the same generate-sbom run for a given digest, so which one answers doesn’t change the answer.
Persisting the manifest
Section titled “Persisting the manifest”A BuildArchiveManifest only helps chant components status answer “what’s built” if something durable outlives the in-process build/run that assembled it. chant run --components <name> --env <env> (both local and --temporal) persists the run’s accumulated manifest — the same document docker-build/generate-sbom/extract-config-bom build up across a component’s Build phase — to the chant/lifecycle orphan branch, alongside the release ledger, keyed by the manifest’s own manifestDigest:
import { persistBuildManifest, readBuildManifest, findBuildManifestByArtifactDigest } from "@intentius/chant/lifecycle";
await persistBuildManifest(manifest); // write, keyed by manifest.manifestDigestconst same = await readBuildManifest(manifest.manifestDigest); // read back by manifest digestconst behindDeploy = await findBuildManifestByArtifactDigest(digest); // read back by a *promoted artifact* digestManifests live under a fixed top-level _builds/ directory on the orphan branch — a peer of the per-env directories the release ledger and snapshots use, never nested inside one. A build archive is promoted by digest across environments rather than owned by a single one, so it doesn’t belong under any single <env>/ path.
chant components status resolves build/componentBom for a recorded digest by calling findBuildManifestByArtifactDigest and deriving both from the result via buildLedgerEntries/componentBomSummary — the same functions covered above, now fed a real manifest instead of one built by hand. A digest with no persisted manifest (it predates this wiring, or was recorded via the standalone chant components release command with no corresponding build) simply reports null for both fields, exactly as before.
Reconciling with live
Section titled “Reconciling with live”chant components status doesn’t recompute drift logic — it reuses the same ownership-aware classification lifecycle plan already trusts (buildChangeSet), then joins it against the release ledger by component name and digest. Four reconciliation outcomes, plus unknown when --live is omitted:
| Reconciliation | Meaning |
|---|---|
| reconciled | A release record exists and live evidence confirms the component is present and chant-owned. |
| unrecorded | Something is live and owned, but no release record references it — a deploy that happened outside the recorded path. |
| stale | A release record exists, but nothing live corresponds to it anymore. |
| drifted | A release record exists, but the live configuration has drifted since (the same update verdict lifecycle plan would give). |
| unknown | No live evidence was requested (--live omitted) — the ledger alone doesn’t confirm reconciliation either way. |
chant components status
Section titled “chant components status”chant components status prod # ledger onlychant components status prod --live # reconcile against live + ownershipchant components status prod --live --json # stable machine-readable contractchant components status prod --compare-to staging # single-query cross-env digest checkchant components status # every environment with release records--json is a stable array of rows (or, with --compare-to, { rows, comparisons }):
[ { "component": "search-service", "env": "prod", "recorded": { "digest": "sha256:9fae3d...", "gitSha": "a1b2c3d", "runId": "4821", "timestamp": "2026-07-02T18:04:11.000Z", "actor": "octocat" }, "build": { "manifestDigest": "sha256:...", "referrers": ["sbom", "provenance"], "sbom": { "mediaType": "application/spdx+json", "packageCount": 142, "generator": "syft", "source": "archive" } }, "reconciliation": "reconciled", "detail": "recorded 2026-07-02T18:04:11.000Z (digest sha256:9fae3d...), live and consistent" }]“Which build is in prod, and is it the one tested in staging” is exactly --compare-to:
chant components status prod --compare-to staging --json{ "comparisons": [ { "component": "search-service", "envA": "prod", "envB": "staging", "digestA": "sha256:9fae3d...", "digestB": "sha256:9fae3d...", "same": true } ]}Both “what’s built” and “what’s deployed where” resolve to one query each, joined by digest — the property the release ledger exists to protect.
Read next
Section titled “Read next”- Build Archive — the manifest and digest identity this ledger joins against.
chant lifecycle— snapshots,diff --live, andplan, which the status surface reuses rather than reimplements.- Orchestration — where a
Verifyphase or CI job callschant components releaseafter a deploy completes.