Skip to content

ci(examples): fail a PR whose committed generator output is stale - #180

Open
sksizer wants to merge 1 commit into
mainfrom
ci/examples-drift-check
Open

sksizer wants to merge 1 commit into
mainfrom
ci/examples-drift-check

Conversation

@sksizer

@sksizer sksizer commented Sep 26, 2026

Copy link
Copy Markdown
Owner

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.rs only 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, and iron-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, so git diff afterwards 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:

::error title=Stale generated output::examples/notes-kb has committed generator
output that no longer matches the generators. Run 'just regen-examples' and commit.

Files that would change:
 examples/notes-kb/src/store/generated/note.rs | 4 ++++

+    pub async fn count_notes(&self) -> Result<u64, AppError> {
+        Ok(self.vault().list_paths(NOTES_DIR).map_err(AppError::from)?.len() as u64)
+    }

That output is real — captured by running the guard on main, where it correctly caught the exact count_notes drift that went unnoticed since #159.

Scope of the check

Only paths containing generated are 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-examples

The fix the failure points at. Rebuilds all four (alias rex). Its --list summary is written as the last comment line, since that is the line just surfaces — 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.yml and 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 -n clean, justfile parses, executable bit committed as 100755 since the workflow invokes the script directly.

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.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Deploying ontogen with  Cloudflare Pages  Cloudflare Pages

Latest commit: 8d37934
Status: ✅  Deploy successful!
Preview URL: https://a342f0d0.ontogen.pages.dev
Branch Preview URL: https://ci-examples-drift-check.ontogen.pages.dev

View logs

fi

example="${1%/}"
pathspec="${example}/*generated*"

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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%/}"

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread justfile
# Examples workflow installs. It builds as-is on macOS.
#
# Rebuild every example's committed generator output, then commit what moves.
regen-examples:

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant