magus v0.3.0 is out. See what's new
¶ View markdown source · ✎ Suggest an edit
3 min read

MGS1019: committed output records its own commit

magus doctor found a file that is both a declared output and committed, and whose bytes contain this repository's own HEAD commit.

self-staling outputs
  2 committed output file(s) record the commit that produced them, so regenerating
  after a commit always drifts; untrack them or drop the VCS stamp
    docs: gen/index.html is committed and records this repository's own HEAD commit
    docs: gen/changelog/index.html is committed and records this repository's own HEAD commit

Nothing is broken yet. The build simply cannot reach a clean state.

Why

Committing a source change moves HEAD. HEAD is an input to the generated file. So the file committed alongside that source is stale the moment it lands, and regenerating produces a diff. Commit the regenerated output and HEAD moves again. Amending is not an escape either: a new hash restales the footer that recorded the previous one.

The only fixed point is a second commit containing nothing but regenerated output, because a commit that does not touch a page's source does not change the commit that page records. A repository in this state grows a trail of "refresh generated metadata" commits after every real one. Those commits are the loop's resting place, not carelessness.

The cost is that every drift gate becomes noise. magus run generate fails after a perfectly ordinary commit, for a diff that contains only a hash, so the signal you built the gate for is buried under the one it manufactures.

Fix

Untrack the output and render where it is published. The generator keeps its provenance line, and the publishing job renders from source with the final commit already known, so there is nothing left to restale. This is what this repository does with its docs site: docs/gen/ is generated and gitignored, and .github/workflows/cd.yaml renders it on every push to main.

Or drop the VCS stamp. If the "Last updated" line is not worth the loop, remove it and the output becomes a pure function of its sources, which is what a drift gate wants anyway.

Keeping both and remembering to regenerate is not a third option. The loop is structural, so the discipline has to hold forever, and when it lapses the committed file quietly describes a commit it no longer sits in.

What it checks, and what it does not

The check reads each project's declared outputs (AllOutputs, so per-target ctx.writesFiles counts), keeps the ones the VCS tracks, and looks for HEAD's short or full hash in their bytes.

  • Tracked is the whole test. Untracked generated output records the same hash and is fine, because nothing commits it. A check that ignored trackedness would fire just as loudly on a repository that had already fixed the problem.
  • Only this repository's HEAD matches. A lockfile pinning some other project's commit, a vendored dependency, a fixture full of digests: none contain your current HEAD.
  • An abbreviated hash must stand alone. A 7-character id found inside a longer hex run is not a match, or every file holding a SHA-256 would report.
  • Large and binary files are skipped (over 1 MiB, or a NUL byte early in the file). A provenance stamp lands in rendered text; reading bundles to look for one costs time and finds nothing.
  • A backend that cannot report tracked paths skips the check. It needs types.TrackedFileReporter, which git implements. Guessing would be worse than saying nothing.

Not a bug when

The file is tracked on purpose and the hash is not this repository's. That is not this diagnostic; it matches HEAD specifically to avoid exactly that case.

You are mid-release and the stamp is intentional in a file you regenerate once per tag rather than per commit. The loop is still there, but it turns once a release instead of once a commit, and you may prefer that to losing the stamp.

See also

MGS1019magusfilegenerated filesdriftdoctorvcsoutputs
Last updated (a103255f)
Earlier changes on this page (1)

Full history ↗ · Blame source ↗

Glossary

Project

A directory magus recognizes as a unit of work (it has a magusfile); the unit of caching, scheduling, and dependency tracking. See workspace.

Magusfile

The magusfile.buzz that declares a project's targets (as export funs) and binds its spells. See targets.

Target

A named operation (build, test, ...) you invoke with magus run <target>; it may compose a spell's tool-native operations and depend on other targets. See targets.

Cache

The content-addressed store magus consults before running a target, so unchanged work is skipped. See cache.

CI

An ordinary magusfile-defined target you compose yourself with magus\needs - magus does not hardcode its stages. Magus.RunCI treats it specially only in that it strips the rw charm, it is the anchor magus affected ci keys off, and a selected scope with no project declaring it is a load error rather than a silent no-op. See targets.

Conventions

This page uses none of the site's convention markers. The full set is on the conventions page.