Files
sencho/docs/features/stack-drift.mdx
T
Anso b21324f97a feat(stacks): persist a drift ledger with temporal source-change detection (#1333)
* feat(stacks): persist a drift ledger with temporal source-change detection

Build on the read-only compose-vs-runtime drift check so a stack's drift is
remembered over time, not just shown at a glance.

- Record a deploy baseline: on a successful deploy, update, or rollback, store
  the deployed compose file's source and rendered-model hashes on the stack so
  the Drift tab can tell whether the file has changed since the last deploy.
- Surface temporal drift in the Drift tab: "matches last deploy", "source
  changed since last deploy" (distinguishing a model change from a
  formatting-only edit), or "no deploy baseline yet".
- Persist findings into a drift ledger: a re-check reconciles the current
  findings, recording newly detected ones and resolving cleared ones, and shows
  a short drift history under the findings. The drift report read stays
  side-effect-free; only an explicit re-check (and a deploy) writes the ledger.
- Write drift detected/resolved events to the stack Activity timeline so the
  provenance sits alongside deploys and restarts.

Node-local and available on the Community tier. Reconciliation is skipped when
a check is not authoritative (Docker unreachable or a compose parse error) so an
open finding is never falsely cleared.

* fix(stacks): record the drift baseline for every deploy path and harden the ledger

Address review feedback on the drift ledger:

- Record the deploy baseline in ComposeService.deployStack/updateStack instead of
  only the manual route, so bulk, Git-source, App Store, scheduler, and webhook
  deploys all capture source/rendered hashes. Reconciliation stays on the explicit
  re-check.
- Store no rendered baseline when the local parser cannot model the compose (for
  example a file over the parse cap) rather than a sentinel that would make a later
  real change read as unchanged.
- Let temporal-overlay failures surface as a 500 instead of being hidden behind a
  neutral "no baseline"; only the compose read stays best-effort.
- Omit the temporal card entirely when a report (for example from an older remote
  node) carries no temporal data, instead of showing a misleading "no baseline".
- Keep drift_detected / drift_resolved history-only by excluding them from the
  routable-category whitelist, so they are never offered as a channel route that
  would never fire.
- Use a JSON separator for the finding identity key so the source file is plain
  text (no embedded control byte).

* fix(stacks): sanitize logged errors in the drift report handlers

The drift report and re-check handlers logged the caught error object
raw alongside the stack name, which a code scan flagged as a
log-injection vector: a crafted stack name surfacing inside an error
message or stack could forge log lines. Route the error through the log
sanitizer so control characters are stripped before writing. Render it
with util.inspect first so the stack trace, cause chain, and underlying
error codes are preserved for debugging.
2026-06-07 20:44:22 -04:00

79 lines
5.8 KiB
Plaintext

---
title: Drift Detection
description: See at a glance whether a stack's running containers still match the Compose file on disk, with specific, actionable reasons when they have diverged.
---
The **Drift** tab in the right-hand **Anatomy** panel answers a single day-two question: does what is actually running still match the Compose file on disk? Sencho treats your Compose file as the source of truth, so it compares the file against the live Docker runtime and reports exactly where the two have diverged.
The check is read-only. It tells you what changed and never alters a stack on its own, so you can trust the report before deciding what to do about it. Opening the tab builds the comparison fresh. When you deploy a stack through Sencho, it also records the Compose file it deployed as a baseline, so it can later tell you whether the file has changed since then, and it keeps a short history of the findings it has seen.
## Status
Every stack resolves to one of four states, shown as a badge at the top of the tab:
| Status | Meaning |
|--------|---------|
| **In sync** | The running containers match the Compose file: same services, images, and published ports. |
| **Drifted** | Something running differs from the file. The specific reasons are listed below the badge. |
| **Not running** | The stack is defined on disk but no containers are running. |
| **Unreachable** | Docker could not be reached, so drift cannot be assessed right now. |
## Since your last deploy
Below the status badge, a second line compares the Compose file on disk against the version you last deployed through Sencho:
| Signal | Meaning |
|--------|---------|
| **Matches last deploy** | The Compose file is unchanged since you last deployed it. |
| **Source changed** | The Compose file has been edited since the last deploy. If the change affects the model (an image, port, or service), Sencho says so; a comments or formatting only edit is called out separately. |
| **No deploy baseline** | This stack has not been deployed through Sencho yet, so there is nothing to compare against. Deploy it once to start tracking. |
This is independent of the runtime status above: a stack can be **In sync** with its running containers while its file has already **changed** for the next deploy.
## Findings
When a stack is drifted, each reason is listed against the service it affects:
| Finding | What it means |
|---------|---------------|
| **Service missing** | The Compose file declares a service, but it has no running container. |
| **Undeclared** | A container is running for the stack, but no matching service exists in the Compose file. |
| **Image** | A running container uses a different image than the Compose file declares. The expected and running values are shown side by side. |
| **Ports** | The published ports of a service differ from what the Compose file declares. |
Image references are compared after normalizing the implicit Docker Hub registry and a missing tag to `:latest`, so `nginx` and `docker.io/library/nginx:latest` are treated as the same image. A running container pinned to a digest is compared against the declared tag as written.
## Drift history
Each time you **re-check** a stack, and after every deploy, Sencho records the findings it sees. The **Drift history** list under the findings shows recent entries with when each was first **detected** and, once it clears, when it was **resolved**. This turns a point-in-time check into a short ledger of how a stack has drifted and recovered over time.
Drift that appears or clears is also written to the stack's **Activity** timeline, so **Drift detected** and **Drift resolved** events sit alongside deploys and restarts.
## Accessing the Drift tab
1. Click any stack in the left sidebar to open it.
2. Switch to the **Drift** tab in the Anatomy panel header.
3. Read the status badge and any findings. Use **re-check** to run the comparison again after you deploy or change something.
On a phone, the same report appears under the **Compose** section of the stack detail.
## Troubleshooting
<AccordionGroup>
<Accordion title="A stack I deliberately stopped shows as 'Not running'">
That is expected. Drift compares the file on disk against what is actually running, so a stack with no running containers reports **Not running** even when you stopped it on purpose. Deploy it to return it to **In sync**.
</Accordion>
<Accordion title="The image finding flags a stack I just updated">
Open **re-check** after the deploy finishes. During a rolling update, replicas can briefly run different images, and the report is a snapshot of that moment. Once every container is on the declared image, the finding clears.
</Accordion>
<Accordion title="A stack that uses a published port range shows a ports finding">
A Compose port range such as `8000-8002:8000-8002` is compared conservatively and can read as a ports difference even when the deployment is correct. Sencho errs toward surfacing a possible difference rather than hiding one. The status reflects this as drift you can confirm against the file.
</Accordion>
<Accordion title="The status says 'Unreachable'">
Sencho could not reach Docker on the active node, so it cannot compare the runtime. Confirm the Docker engine is running and the node is online, then use **re-check**. Other stacks on the same node will show the same state until Docker responds. While Docker is unreachable, Sencho does not change the recorded drift history, so an open finding is never cleared just because the check could not run.
</Accordion>
<Accordion title="The tab says 'No deploy baseline'">
Sencho records a baseline the first time you deploy a stack through it. A stack you imported or have not yet deployed from Sencho has nothing to compare against. Deploy it once from Sencho and the line changes to **Matches last deploy**.
</Accordion>
</AccordionGroup>