Files
sencho/docs/features/gitops-drift-model.mdx
T
Anso 26f77cac96 docs: v1 docs refresh (batch 9) (#2181)
* docs(gitops): complete the overview's source states, placement limits, and control locations

List every source state the app can show, with the order in which the most
serious one wins. Add the pin-driven and unreadable-evidence cases to what
the placement policy hands back to an operator, and name the controls where
each policy is set.

* docs(git-sources): document the polling controls, Manual behavior, and pending-update indicators

State the per-node polling interval and its range, that a Manual source is
never polled or retried but still stages a plan when a webhook delivers a
commit, and where the pending-update icon and button lead. Add common setups
and what Git sources do not do, and correct the create form's alt text.

* docs(git-authentication): cover the Git error cases and current Bitbucket credentials

Add the failures a Git source can report that were missing from
troubleshooting (embedded credentials, SSH without a deploy key, blocked
hosts, rate limits, certificate expiry and hostname mismatch, network
errors), state that the token is only given to the repository's host, and
replace the deprecated Bitbucket app password guidance.

* docs(git-webhooks): state the GitHub content type, remote-node failures, and limits

A form-encoded GitHub delivery fails verification and answers 404, so say to
set the content type to JSON. Add the 500 a delivery gets when the owning
node is unreachable, correct how the hub forwards to a remote node, and list
what provider hooks do not cover.

* docs(git-secrets): document linking an encrypted repository and where encrypted values work

A repository that is already encrypted cannot be linked, because the identity
belongs to a linked source. Encrypted values belong in env files and
file-backed secrets, not in image references or published ports, which
Sencho resolves before decrypting.

* docs(blueprints): correct permissions, remote deploys, and roadmap wording

Describe the permissions Blueprint actions need instead of an admin-only
rule, say that a remote node runs the deploy itself with its own policy and
scanner state, and list all three drift notifications. Remove the repeated
marker, policy gate, and snapshot explanations, the raw apply request
format, and statements about future features.

* docs(git-managed-blueprints): add who can take each step and the limits

List the permission each authority step and rollout control needs, say that
webhooks do not fetch a Blueprint's source so Pull source and polling bring in
new commits, and gather the constraints that were scattered through the page.

* docs(gitops-portfolio): name the row menu actions and state what the portfolio does not do

List the exact row menu items for Direct and Blueprint applications, and
add the limits: hub only, navigation from row menus, and how unreachable
nodes appear.

* docs(gitops-drift): describe the drift row as it appears and add a screenshot

The row shows the class, the deriving component, evidence age, reason, and
expected against observed; the resolving action lives on the application's
status. Name the retry-interval setting, drop rollout states Sencho never
reports, and replace a hedge about which classes are reported.

* docs(git-transport): refresh the live attestations against the 0.98.0 release

Re-run all 21 live attestations (hosted public repositories, the direct
proxy and Pilot node paths, and disposable Gitea and Forgejo instances over
HTTPS with a private CA and over SSH with a deploy key) against the 0.98.0
image, record the new date, commit, and image digest, and regenerate the
matrix page. Describe unlisted hosts without claiming more than was tested,
and add the procedure the attestation record refers to.
2026-10-23 12:51:36 -04:00

164 lines
17 KiB
Plaintext

---
title: "GitOps Drift"
description: "The drift classes Sencho derives for every Git-managed stack and Blueprint, what each one proves, and which policy decides what happens next."
---
Sencho reports drift in two layers. The **spatial** layer compares the effective Compose model against the containers running right now and is covered in [Drift Detection](/features/stack-drift). The **GitOps** layer compares what you intended against what Sencho has proven, accepted, and observed over time, for anything managed by a Git source or a [Blueprint](/features/blueprint-model).
This page is the reference for that second layer: the classes it reports, the evidence each one rests on, and the policy that decides what happens after a class is reported. The vocabulary (generation, stage, authority step) is defined in the [GitOps overview](/features/gitops-overview).
## The seven classes
Every GitOps drift item carries a class. The class says which part of the chain diverged, and that decides who owns the fix.
| Class | The comparison it is made from | A mismatch is reported when |
|---|---|---|
| **Source** | The accepted generation's commit against the commit the configured ref names. | The two differ, or the last fetch or validation failed. |
| **Managed project** | The stack's recorded managed-project manifest against the accepted generation. | They differ, or the manifest is missing or unreadable. |
| **Placement** | The approved blast against the nodes that hold applied authority. | A recorded approval no longer binds the current intent, or a node still holds authority outside the required set after the rollout settled. |
| **Health** | The latest stack-scoped health run for a node against the generation that node runs. | The run failed for exactly that generation. |
| **Runtime** | What is running against what was approved: the artifact identity, and the generation each node was asked to run. | A node reports another artifact identity, or runs a generation other than the one it was asked for. |
| **Rollout** | A required rollout target's observed artifact identity against the authorized rollout set. | A required target reports another identity. |
| **Invocation** | The compose invocation a node recorded when the stack was applied, against the invocation that generation was applied with. | The node's compose file order, project name, project directory, or env file order is not the one it was given. |
A class appears only where Sencho holds the evidence for it and the rows prove a mismatch. A class whose evidence is not held is absent rather than approximated, and [What Sencho cannot prove](#what-sencho-cannot-prove) lists the cases where a caveat appears in its place.
The [Drift tab](/features/stack-drift) and the application sheet show the classes they have evidence for. The portfolio's drift summary counts them by class, and its attention queue names the ones that are waiting on you.
## Reading an item
Each item is one line of comparison. The drift row shows the class, the owner, how old the evidence is, the reason, and the expected identity beside the observed one.
<Frame caption="A drift item on the Drift tab of a Git source: the class, the component that derived it, the age of its evidence, the reason, and the expected generation beside an unknown observation.">
<img src="/images/gitops-drift-model/drift-row.png" alt="Drift tab of a Git source sheet with one item: a managed project chip, the owner GITPROJECTMANIFESTSERVICE, evidence 22M AGO, the reason that the applied managed project has no usable manifest, and expected generation 7f75337a to observed unknown" />
</Frame>
- **Class** names the part of the chain that diverged.
- **Owner** names the component or evidence source that derived the item, such as the Git source service, the Blueprint reconciler, or the observed artifact identity. It tells you which part of Sencho made the comparison; the node it concerns is on the application's target cards.
- **Evidence age** is when the evidence was recorded. An old observation is weaker proof than a new one, so the age travels with the item. An item derived from two pointers rather than a recorded observation reads **evidence undated**.
- **Reason** says in one line what differs.
- **Expected** is the identity Sencho was meant to run: a commit, a generation, an artifact set, an intent, or a health run.
- **Observed** is what it found: the same kind of identity, or `unknown` when Sencho found nothing to compare.
The step that resolves an item is not on the row. It is the action on the application's status, for example **Retry**, **Review update**, or **Authorize rollout**, and it is offered only when that step is legal right now. A paused or failed node gets no action rather than one that cannot run.
## When Sencho will not repair it
A Blueprint whose drift mode is **Enforce** can restore runtime drift on its own, but only the state the target was already authorized to run: the generation that target acknowledged and the image identity approved for it. A newer accepted generation never changes what an older target restores, because the repair answers "put this node back" rather than "deploy the newest thing".
Holding is an **Enforce** decision, made fresh each tick. Observe and Suggest still detect and report the same drift, because "auto-fix was declined" is not an answer for someone who never asked for one. And a hold never suppresses detection: Sencho still inspects the node and still records what is running, it only declines to overwrite it.
Two things a held row does not tell you. Its reason is the hold, not the drift, so read the Drift panel for what is actually running. And a row held under Enforce keeps reading **Repair held** if you switch to Observe or Suggest while the drift cannot be classified, because the next check has nothing to record until the cause clears. The deployment row reads **Repair held** with the reason, the attention queue names it, and the workload is left untouched. A held target is still drifted: it is never counted as up to date.
The reasons a repair is held are the states this model already treats as uncertainty rather than divergence: a stateful or unclassifiable Blueprint, a rollout that has moved on, on-node evidence that does not name a generation, an unreachable target or stale connectivity, an interrupted change, a recovery or rollback that owns the target, and a disagreement between the rollout's frozen pair and the target's own expectation.
Most holds clear themselves. Sencho keeps re-checking the target, so once the cause is resolved, by a node returning or an interrupted change finishing, the next reconciliation repairs the drift normally.
One does not: a hold because the rollout moved on stays until a later rollout includes that target. Superseding is a one-way fact, so nothing reopens that rollout on its own, and Git-managed Blueprints have no manual redeploy to fall back on, so the rollout is the only way out.
## Where drift is shown
| Surface | What it shows |
|---|---|
| **Drift** tab on a stack | The spatial findings plus the GitOps state for that stack, one card per node, with the per-service digest comparison under each node. |
| **Application sheet** in the portfolio | The same drift items for one application, alongside its source, artifact, placement, and rollout state. |
| **Portfolio application table** | A count and the classes involved, so you can narrow the list to one class. |
The same projection feeds all three, so a class never reads differently depending on where you look.
## Per-service digests
When the approved artifact set and the node's observation both carry per-service image evidence, each target's card shows the approved digest beside the running one, per service, and marks each one matched, drifted, or unverified. The comparison uses the child digest for the platform that node runs, because a multi-platform image has one child per platform and a node only ever proves its own.
Where evidence is missing, the card says so. It never renders an empty comparison, because an empty table reads as agreement.
When a Blueprint is first applied, Sencho freezes the exact image identity it was approved at by resolving each image against its registry. If that resolution cannot reach the registry, the target has no approved identity to compare against and its drift reads as unverified rather than as agreement.
For a Blueprint applied from stored content, Sencho retries that resolution on its own, so the target recovers on its own schedule instead of waiting for you to redeploy it. A caveat under the state names the condition while it lasts, and clears once the identity is provable. The interval is **Retry an unresolved image identity** under **Settings → GitOps**, 5 minutes by default, and it has no effect while every target's identity is already resolved. See the [Settings reference](/reference/settings).
The retry resolves the images the Blueprint was approved at, not whatever compose happens to be in the stack directory at the time. If someone edits a file on the node directly, that change is drift to be reported, never an approved identity to be adopted.
Some Compose shapes are not recovered this way, and Sencho says so rather than guessing. The approved images are read from what you wrote, while a deploy resolves them the way Compose does, and the two can disagree for `profiles`, `extends`, `include`, and YAML merge keys. For those, the retry stands down and the target stays unverified until you redeploy the Blueprint.
The tick that resolves a target waits before judging it. It reports unverified that pass, and compares on the following one. That is one reconciler interval, not a review window: if the registry resolves a tag that moved while the identity was unproven, the next tick reports it as drift, and Enforce mode repairs against it.
<Note>
A digest is compared, never pulled. When Sencho repairs a digest divergence it redeploys against the approved identity, so authored tags on disk stay as you wrote them.
</Note>
## What is not drift
Work in progress is not drift, and the classes above stay silent while it happens:
- a commit that has been fetched but not yet accepted, including one waiting on review
- a placement or authorization decision that is still open
- a rollout that is queued, advancing target by target, or waiting on its health verdict
- a manifest being written as part of an apply that has not landed on the node yet
- a node that is deploying, interrupted, or recovering
- a drift that Enforce is repairing on its current pass, which is why the Blueprint deployment row reads as correcting rather than drifted
- a target that has not been deployed yet, or has never had one recorded
Those states have their own surfaces, and each one names the next step. A node running a different generation than the one it was asked for is a separate class of its own, and is reported whether or not something else about that node is in flight.
One more case sits here by decision rather than by accident. Source drift compares the accepted commit against the commit the ref names. When those still agree but the materialization settings changed, for example a compose path, an env path, or a context directory, there is no expected-versus-observed pair of commits to report, so no source drift item is created. Instead the source shows as **reconcile required** in the attention queue, and for a Direct application that is where you fetch again: the fetch rebuilds the candidate under the settings now in force, and accepting that candidate produces the new generation. This is a choice about what the word drift means, not a claim that the materialized content was proven unchanged. Changing where Compose is read from can change the content, which is why Sencho asks rather than assumes.
## Invocation
Invocation drift answers one question: is this stack being brought up the way this generation said it would be?
When a generation is applied, Sencho reads back how Compose was actually invoked on the node: the compose files in order, the project name, the project directory, and the env files in order. That record is compared against the invocation the accepted generation was applied with. It is the only evidence in the model that can contradict the authored invocation at all, because reading the command back would only confirm what Sencho already believed.
What it catches in practice is a project that came up differently from the way the generation named it. The common case is a stack directory holding a compose file the generation did not name: Compose resolves it, the node records more files than were asked for, and the difference is reported. A node that answers with a different project name, project directory, or file order is reported the same way.
Two limits worth knowing, both about when the evidence is taken:
- **The record is read after the apply finishes.** A stack brought up by hand *before* an apply is overwritten by that apply, so the apply reads back its own work. A hand-made invocation that the apply replaced is not what you see here.
- **It is a snapshot, not a watch.** Nothing re-reads it on a timer. An invocation changed on the node after the apply is not reported until the next apply records a new one. Every item carries the time its evidence was taken, so you can see how old it is.
A stack whose invocation has not been recorded yet reports nothing and says why, rather than showing you an empty comparison that would read as agreement. The first apply records it and the caveat clears.
## What Sencho cannot prove
Drift is derived from what Sencho has recorded, so missing evidence is never rounded up to agreement. A few cases worth knowing:
- A target on a node Sencho does not host has no health verdict here, so health drift stays silent for it rather than claiming a verdict nobody wrote.
- A Git-managed Blueprint deployment that opens no generation-bound health gate produces no health item.
- A required-target set Sencho cannot read records a caveat naming the unreadable record, instead of quietly reporting no placement drift.
- A stack whose invocation has not been recorded yet records a caveat saying so, rather than reporting no invocation drift.
- A stack whose generation names no compose file records a caveat, rather than reporting a difference against a file list that was never written.
- A node that was withdrawn while a recovery on it had failed records a caveat, because the withdrawal ended what Sencho tracks there and the failed recovery is no longer reported as a state.
- A Blueprint deployment is not compared at all. Each of its targets is brought up from the Blueprint rather than from a recorded compose invocation, so there is no expected invocation to compare against, and the class stays silent for them rather than reporting every one as different.
- A Blueprint target whose approved image identity could not be resolved records a caveat and is not compared against what is running, rather than reporting no drift. For a Blueprint applied from stored content, Sencho retries the resolution on its own, so this clears without a redeploy.
- A service built on the node has no published digest to compare, so the per-service view labels it **not compared** rather than matched or drifted.
- A service running something the approved set does not name is outside the comparison, so it reads **not compared** rather than drift.
Each of those is listed as a caveat under the state it applies to, on the Drift tab and in the application sheet, saying what could not be proven. The per-service digest view says the same in its own words: a target with no per-service evidence on either side says nothing rather than showing an empty comparison that would read as agreement.
## Which policy acts on a class
A drift class reports; a policy decides. Each policy is independent, and none authorizes another. They are defined together in the [GitOps overview](/features/gitops-overview#who-decides-each-step). The two that act after something has drifted:
| Policy | What it decides |
|---|---|
| **Drift mode** (Observe, Suggest, Enforce) | What happens when a Blueprint's own drift is detected: surface it, notify you, or redeploy it |
| **Health rollout policy** | Whether a deploy must prove healthy before it finishes, and what a failure does next. See [Health rollout policy](/features/health-rollout-policy) |
Two rules keep them honest. A digest repair redeploys against the approved identity rather than pulling whatever the tag now points at, and a stateful change always re-enters operator confirmation, whatever the drift mode says.
## Related
<CardGroup cols={2}>
<Card title="Drift Detection" icon="magnifying-glass" href="/features/stack-drift">
The spatial layer: effective Compose model against the containers running now.
</Card>
<Card title="GitOps overview" icon="route" href="/features/gitops-overview">
The chain from commit to container, and who decides each step.
</Card>
<Card title="Blueprints" icon="layers" href="/features/blueprint-model">
Drift modes and digest drift on a fleet template.
</Card>
<Card title="GitOps portfolio" icon="chart-pie" href="/features/gitops-portfolio">
Every application in one table, with drift by class and the attention queue.
</Card>
</CardGroup>