Conversation
The examples commit their generated trees so diffs stay reviewable, but nothing regenerated them — an example's build.rs only runs when that example is built, and the root workspace never builds them. So the trees drifted silently. By the time anyone looked they were four minor releases behind, still missing the `count_*` methods #159 added and still carrying ontogen 0.3.1 in a lockfile. The Examples workflow already builds all four on any `src/**` change, which is exactly when committed output can be invalidated. Building regenerates in place, so a `git diff` afterwards is precisely "what the committed output is missing" — the guard costs one step per job and no extra build time. `.github/scripts/check-generated-drift.sh <example-dir>` does the check, emitting a `::error::` annotation plus the diff so the failure is readable without opening the log. It looks only at paths containing "generated": cargo can touch a lockfile for reasons unrelated to codegen, and a check that goes red spuriously is one people learn to ignore. `just regen-examples` (alias `rex`) is the fix the failure points at.
Deploying ontogen with
|
| Latest commit: |
8d37934
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://a342f0d0.ontogen.pages.dev |
| Branch Preview URL: | https://ci-examples-drift-check.ontogen.pages.dev |
| fi | ||
|
|
||
| example="${1%/}" | ||
| pathspec="${example}/*generated*" |
There was a problem hiding this comment.
The *generated* pathspec misses the DTO output. Every example has .dtos("src/schema/dto") in its build.rs, and those files (examples/*/src/schema/dto/*.rs, headed //! Generated by ontogen. DO NOT EDIT.) have no "generated" in their path.
Failure: change the DTO generator in src/schema/** and don't regen. The build rewrites examples/notes-kb/src/schema/dto/note.rs, the guard reports "committed generator output is current", and the drift ships.
Suggested fix: instead of allowlisting paths by name, check the whole example tree and exclude only what's known to be noisy, e.g. git status --porcelain -- "$example" ':!:**/Cargo.lock'. That also covers any future output dir that isn't named generated.
| example="${1%/}" | ||
| pathspec="${example}/*generated*" | ||
|
|
||
| if git diff --quiet -- "$pathspec"; then |
There was a problem hiding this comment.
git diff ignores untracked files, so a generator that starts emitting a new file isn't detected. Examples: a new output module, a new per-entity file after an example's schema grows, or a split of generated/mod.rs.
Failure: generator change adds src/store/generated/foo.rs. The file is written but not committed, git diff --quiet exits 0, the check passes, and anyone who clones main gets a tree that differs from what the generator emits.
Suggested fix: use git status --porcelain -- "$pathspec" and test for non-empty output, or run git add -N first.
| # had fallen four minor releases behind before anyone noticed, still carrying | ||
| # ontogen 0.3.1 in a lockfile. | ||
| # | ||
| # The `src/**` path filter is what makes the guard meaningful — a generator |
There was a problem hiding this comment.
This claim doesn't hold. The generator also lives in crates/ontogen-core (ontogen re-exports naming, ir and model from it; pluralisation and IR decide the emitted names), crates/ontogen-ts (emits types.ts bindings) and crates/ontogen-macros. Output formatting also depends on the root rustfmt.toml, because rustfmt_string resolves config from the build-script cwd upward. None of these match the paths: filter (examples/**, crates/markdown-store/**, src/**).
Failure: a PR touching only crates/ontogen-ts/src/emit.rs changes generated-ts/types.ts output. The Examples workflow never runs, the drift guard never runs, and the committed trees go stale silently, which is the exact scenario this PR is meant to close.
Suggested fix: add crates/ontogen-core/**, crates/ontogen-ts/**, crates/ontogen-macros/** and rustfmt.toml to the path filter.
| # regenerates in place, so a `git diff` afterwards is exactly "what the | ||
| # committed output is missing". Without this the trees drift silently: they | ||
| # had fallen four minor releases behind before anyone noticed, still carrying | ||
| # ontogen 0.3.1 in a lockfile. |
There was a problem hiding this comment.
The drift this comment gives as the motivation (example lockfiles still pinning ontogen 0.3.1) is deliberately excluded by the script. The trigger for it isn't in the path filter either: release-plz bumps version in the root Cargo.toml, which isn't under src/**.
Failure: the next release bumps ontogen to 0.8.0. Every example Cargo.lock still records 0.7.0, cargo silently rewrites it on the next local build, and nothing in CI notices. The 0.3.1 situation comes back.
Suggested fix: build with --locked, so a stale lockfile fails loudly, and add Cargo.toml to the path filter. Or drop the lockfile claim from this comment so it doesn't overstate what the guard covers.
| exit 2 | ||
| fi | ||
|
|
||
| example="${1%/}" |
There was a problem hiding this comment.
The script passes silently when the pathspec matches nothing. That happens with a misspelled or renamed example dir, or when it's run from anywhere other than the repo root, because git pathspecs are cwd-relative. For example, cd examples/notes-kb && ../../.github/scripts/check-generated-drift.sh examples/notes-kb looks for examples/notes-kb/examples/notes-kb/*generated*.
Failure: someone renames examples/tasks-tracker and updates the build step but not this argument. The guard prints "✓ … current" forever.
Suggested fix: add [ -d "$example" ] || { echo "no such dir: $example" >&2; exit 2; } and anchor with cd "$(git rev-parse --show-toplevel)", or use a :(top) pathspec.
| @@ -24,6 +36,8 @@ jobs: | |||
| with: | |||
| toolchain: "stable" | |||
There was a problem hiding this comment.
This check is only reliable if CI and the developer who ran just regen-examples get the same rustfmt output. write_and_format pipes every generated .rs through rustfmt, and CI uses a floating stable (so does the root rust-toolchain.toml).
Failure: a developer on an older stable regenerates. CI's newer rustfmt changes wrapping or import sorting in a generated file, and the drift check goes red on a formatting-only diff that the developer can't reproduce locally. That's the "spurious red that people learn to ignore" the script header warns about.
Suggested fix: pin a specific toolchain version in rust-toolchain.toml and use the same version here.
| example="${1%/}" | ||
| pathspec="${example}/*generated*" | ||
|
|
||
| if git diff --quiet -- "$pathspec"; then |
There was a problem hiding this comment.
if git diff --quiet treats every non-zero exit as drift, but git diff --quiet exits 128 on a git error (not a repo, bad pathspec, a safe.directory refusal in a container).
Failure: git errors out. The script emits the "Stale generated output … Run just regen-examples" annotation, then set -e kills it on the next git diff --stat. The PR gets a misleading annotation telling the author to regenerate when the real problem is the environment.
Suggested fix: capture the status and distinguish 1 (drift) from anything else.
| # Examples workflow installs. It builds as-is on macOS. | ||
| # | ||
| # Rebuild every example's committed generator output, then commit what moves. | ||
| regen-examples: |
There was a problem hiding this comment.
Adding an example now means updating the list of examples in three places: the workflow build step, the workflow drift step, and this recipe. There are already 4 near-identical job pairs. If a new example misses the drift step, or misses this recipe, it's silently unguarded, or just regen-examples stops regenerating it.
Suggested fix: keep a single list. Either use a workflow matrix over {dir, manifest, cmd} that calls a shared recipe/script, or have a just check-examples-drift recipe iterate the same list regen-examples uses, and call that from CI.
Closes the gap that let the examples drift four minor releases behind without anyone noticing.
The gap
The examples commit their generated trees so diffs stay reviewable — but nothing regenerates them. An example's
build.rsonly runs when that example is built, and the root workspace never builds them (they live outside it deliberately).So drift accumulated in silence. By the time it surfaced, the trees were missing the
count_*methods #159 added, andiron-log's lockfile still pinned ontogen 0.3.1. #179 clears that backlog; this stops it rebuilding.The guard
The Examples workflow already builds all four examples on any
src/**change — which is exactly when committed output can be invalidated. Building regenerates in place, sogit diffafterwards is precisely "what the committed output is missing."That makes the check nearly free: one step per job, no extra build time.
.github/scripts/check-generated-drift.sh <example-dir>emits a GitHub::error::annotation plus the diff, so the reason is visible on the PR without opening the log:That output is real — captured by running the guard on
main, where it correctly caught the exactcount_notesdrift that went unnoticed since #159.Scope of the check
Only paths containing
generatedare inspected. Lockfiles and hand-written sources are deliberately excluded: cargo can touch a lockfile for reasons that have nothing to do with codegen, and a check that goes red spuriously is one people learn to ignore. Better to guard the thing that actually drifts.just regen-examplesThe fix the failure points at. Rebuilds all four (alias
rex). Its--listsummary is written as the last comment line, since that is the linejustsurfaces — worth knowing, as two existing recipes render a meaningless fragment for this reason. I left those alone rather than widen the diff.Not a required check
This inherits
examples-ci.yml's existing status: path-filtered, and therefore not required (a path-filtered required check leaves PRs that skip it pending forever). A failure shows as a red X on the PR but will not block merge.That is a deliberate limit, not an oversight — making it blocking means either solving the path-filter problem with an always-runs stub job, or moving the check into
ci.ymland paying iron-log's Tauri system-dependency install on every PR. Worth deciding separately; happy to do either.Verification
Exercised both paths locally: passes on a current tree, fails with the diff above on a stale one.
bash -nclean, justfile parses, executable bit committed as100755since the workflow invokes the script directly.