Drift
Optional: hand this page to your coding agentThe steps work by hand too.Show the whole prompt
The drift watch reported drift in the environment I name. Following https://intentius.io/sql-yodeler/drift/, run `npx yodel drift <env>` with the reader's credentials and tell me what changed out of band.
Then ask me whether to put the live object back by hand or keep the change. To keep it, declare it in src/, run `npx yodel new <name>` and `npx yodel lint`, and open a pull request.
Never run `yodel apply` against a shared environment, never run `chant approve`, `yodel approve` or `yodel override`, never edit the `chant/lifecycle` branch or `.chant/allowed_signers`, never merge; approvals and applies belong to people.Drift is a declared object changed out of band (by hand, by another tool) or gone. yodel drift <env> compares the declared objects with the live ones, through chant (chant lifecycle diff <env> --live). An object the project does not declare is never reported.
What the server is compared with (--against):
history, the default when the project has migrations: the schema recorded by the newest migration the environment’s history records as applied (migration.json’sschema). A migration merged but not applied yet, waiting at its gate or refused, is not drift; a change made by hand is. With no migration applied yet there is no recorded schema, and nothing is reported.src, the default without migrations: the declared schema insrc/.
Exit codes: 0 no drift, 2 drift, 1 the drift could not be read, or a declared object could not be read (its state is then unknown, not clean). --json prints the report as JSON, with what it was compared with (against) and a fingerprint of the drift: a hash of the environment and what drifted, the same for the same drift, which the tracking issue uses to tell an unchanged drift from a new one.
In the ClickHouse example, someone changes a column default by hand:
ALTER TABLE shop.events MODIFY COLUMN source LowCardinality(String) DEFAULT 'app'$ npx yodel drift devDrift in dev (clickhouse 26.8.15.10 at 127.0.0.1:8123)
Compared with the schema 20261010T1723-add-source records, the newest migration the history records as applied.
Changed out of band: events (ClickHouse::Table) column source default.expr: declared 'web', live 'app'[exit 2]Put back, there is none:
$ npx yodel drift devDrift in dev (clickhouse 26.8.15.10 at 127.0.0.1:8123)
Compared with the schema 20261010T1723-add-source records, the newest migration the history records as applied.
No drift: every declared object matches the server.The output quoted here is from the examples’ runs.
On a schedule: the WatchOp
Section titled “On a schedule: the WatchOp”ops/watch-<env>.op.ts declares the same check as a chant WatchOp with a cron schedule:
import { WatchOp } from "@intentius/chant/op";
// Every hour at :17, read dev and report drift: a declared object changed out// of band, or gone. Objects src/ does not declare are never reported.// `chant run watch-dev` runs it once, by hand.export const { op } = WatchOp({ name: "watch-dev", env: "dev", schedule: "17 * * * *" });The watch job in the template’s pipelines runs it on its schedule (chant run watch-dev, underneath). A run takes a snapshot, diffs declared against live, and reports:
$ npx chant run watch-devSnapshot saved to chant/lifecycle (sql(2))
sql — environment: dev0 missing, 0 orphan, 0 disappeared, 0 newly observed, 0 drifted, 2 unchanged--------------------------------------------------------------------------------
sql (properties)1 property drift across 1 resource(s), 0 accepted, 1 unchanged--------------------------------------------------------------------------------
PROPERTY DRIFT (declared vs live; baseline shown where one exists): - events (ClickHouse::Table) columns[4].default.expr: 'web' → 'app' [from: authored][phase] Snapshot ✓ lifecycleSnapshot(env=dev) 923ms[phase] Diff ✓ lifecycleDiff(env=dev, live=true) 781ms [outcome] Drift=trueOp "watch-dev" completed in 1.8schant run exits 0 when the watch completes, drift or not. The drift is the run’s Drift outcome, in its run record on the local chant/lifecycle branch. The WatchOp compares with src/, so in the starter templates’ watch job it is followed by yodel drift <env> --issue, which compares with the history, fails the job on drift, and keeps the tracking issue below.
The starter templates render a scheduled pipeline per watch for each forge (npm run ci writes them from ops/): a GitHub Actions or Forgejo Actions workflow on the watch’s cron, and a GitLab CI job run by a pipeline schedule you create with the cron the file notes (GitLab keeps no cron in the file). The job reads with the reader’s credentials and never pushes: its snapshot and run record stay in the job. Its token writes only issues: GitHub’s job token gets issues: write, Forgejo’s can write already, and GitLab uses GITLAB_TOKEN, the project access token the plan comment uses.
The tracking issue
Section titled “The tracking issue”yodel drift <env> --issue keeps one issue for the project and environment on the forge, found again by a marker in its body (<!-- yodel:drift project=<dir> env=<env> -->, where <dir> is the project’s directory in its repository, . at the root):
- drift found and no issue open: it opens one, titled with the environment and the objects (
Drift in dev: events, daily), its body the report as markdown, what it was compared with, the CI run that found it and the fingerprint; - drift found and the issue open: it updates the issue when the fingerprint changed, and leaves it when it did not;
- no drift: it comments that the drift is gone and closes the issue. Drift found again later opens a new one;
- the drift could not be read, or a declared object could not be (exit 1): it leaves the issue as it is, and says so.
The forge, repository and token come from the CI job’s environment, as for yodel plan --comment (Approval): GITHUB_TOKEN on GitHub Actions, GITLAB_TOKEN on GitLab CI, FORGEJO_TOKEN or GITHUB_TOKEN on Forgejo Actions. --forge, --repo, --api-url and --token-env set them outside CI. yodel drift prints what it did to the issue on stderr, and --json adds it as issue (opened, updated, unchanged, closed, none or left, with the number and link). An issue the forge refuses fails the run with exit 1 unless it found drift (exit 2).
The Postgres example’s drift and watch are in its section 8.
Proven by
Section titled “Proven by”The scenario claims below run what this page describes against a real server, once plain (it passes) and once with the behaviour broken (the claim catches it). Claims status lists every claim.
| Claim | What it says | Plain, broken | Last run |
|---|---|---|---|
drift |
yodel drift reports a declared object changed out of band, naming the property, and one dropped; on the versioned path it compares with the newest applied migration’s recorded schema, so a pending migration is not drift | ClickHouse: pass, caught; Postgres: pass, caught | c6f58a4, 2026-10-10 |
