ci(release): refuse to publish a build from a modified checkout - #147
Conversation
The v26.8.1 Windows installer reports `v26.8.1-dirty` in About. That string is `git describe --tags --always --dirty`, baked in by `src-tauri/build.rs` and displayed by `app_version_detail`, so it says the binary users installed was built from a tree that did not match the tag. The flag is trustworthy. `git describe --dirty` calls `refresh_index()` before `run_diff_index()` (git's `builtin/describe.c`, unchanged since v2.20), so stat-only skew cannot produce it: bumping a file's mtime leaves describe clean, and this repo has no CRLF-committed or mixed-eol tracked files, no tracked symlinks, and no paths that fail to check out on Windows. Something in the `windows-latest` job modified tracked content. What, is still unknown. So don't paper over the marker — the first version of this change suppressed `-dirty` for tag-exact builds, which would have made releases stop reporting a real defect. Fail the release job instead when tracked files differ from HEAD, and print them. An artifact that cannot be mapped back to a commit should not reach users. The check runs twice, because `build.rs` reads git state *during* the build: once before, to fail cheaply, and once after, since that is the run that can claim the uploaded assets match the tag (cargo rewriting the tracked `src-tauri/Cargo.lock` would land in that window). A post-build failure fails the `build` job that `publish` needs, so the release stays a draft. `git diff-index` is plumbing that compares cached stat data without a content fallback, so the script refreshes the index first — otherwise a fresh clone's mtimes alone can fail a pristine tree. This gate would have failed v26.8.1's Windows job, so the next tag may fail until the cause is found. With `git status` in the log that should be short. `.gitattributes` pins `*.sh` to LF: the gate runs through `bash` on windows-latest, where Git for Windows would otherwise check the script out with CRLF and break it on its first line. Every tracked `.sh` blob is already LF, so nothing renormalises. The same puzzle has a second half: the published Linux `.deb` and macOS `.app` for v26.8.1 contain the literal `26.8.1` exactly once and no `v26.8.1` anywhere, meaning no describe string was baked on those platforms at all while Windows baked a dirty one. Nothing in the build log distinguished those outcomes, so `build.rs` now reports what it baked — or why it baked nothing — as a `cargo:warning` when `CI` is set.
|
First data from the new instrumentation, this PR's own CI run (30829601896), both jobs green: Two things follow.
|
The v26.8.1 Windows installer reports
v26.8.1-dirtyin Settings → About. This does not sanitise that string — it stops us shipping the artifact that earns it.What the string means
src-tauri/build.rsbakesgit describe --tags --always --dirtyintoCLAI_GIT_DESCRIBE;app_version_detailprefers it overCARGO_PKG_VERSIONwhenever it looks version-like. So the release job's checkout state is what users read in About.The flag is truthful.
git describe --dirtycallsrefresh_index()beforerun_diff_index()(gitbuiltin/describe.c:802-815@ v2.55.0, present since v2.20), so stat-only skew cannot produce it — bumping a file's mtime leaves describe clean (reproduced). Also ruled out on this repo: no CRLF-committed or mixed-eol tracked files (git ls-files --eol: 338i/lf, 37 binary, 1none), no tracked symlinks, no paths that fail to check out on Windows, and a clone withcore.autocrlf=truestays clean. Something in thewindows-latestjob really modified tracked content. What, is still unknown.My first attempt at this suppressed
-dirtyfor tag-exact builds. Review killed it: it would have made releases stop reporting a real defect. Reverted —src-tauri/src/commands/app_info.rsis untouched.What this does
scripts/assert-pristine-tree.sh— fails when tracked files differ fromHEAD, printinggit status+git diff --stat HEAD. It runsgit update-index -q --refreshfirst, becausegit diff-indexis plumbing that compares cached stat data with no content fallback: without the refresh, a fresh clone's mtimes alone fail a pristine tree (reproduced both ways).release.yml— before the build (fail cheaply) and after it (the run that can honestly claim the uploaded assets match the tag; cargo rewriting the trackedsrc-tauri/Cargo.lockwould land in that window). A post-build failure fails thebuildjob thatpublishneeds, so the release stays a draft andsync-to-r2never runs..gitattributes:*.sh text eol=lf— the gate runs throughbashonwindows-latest, where Git for Windows would otherwise check the script out with CRLF and kill it onset -euo pipefail. Verified: in acore.autocrlf=trueclone the.shfiles stay LF whileMakefile/*.tsdo convert, and no file renormalises.build.rsreports what it baked as acargo:warningwhenCIis set — the value, or the reason nothing was baked.Heads-up before merging
This gate would have failed v26.8.1's Windows job, so the next tag may fail until the cause is found. That is the intent — an artifact that cannot be mapped back to a commit should not reach users — but it is a policy call, hence draft. With
git statusin the log the investigation should be short.The second half of the puzzle
The published Linux
.deband macOS.appfor v26.8.1 contain the literal26.8.1exactly once and nov26.8.1anywhere, i.e. no describe string was baked on those platforms at all, while Windows baked a dirty one. One tag, three platforms, three outcomes, and nothing in the build log said which. Not caused here, not fixed here; item 4 makes the next release say so out loud. (Cache staleness is excluded:swatinem/rust-cache@v2does not cache workspace crates, and cargo replays build-script warnings.)Verification
cargo fmt;cargo clippy --lib;cargo test --lib app_info(unchanged code, still green);CI=1 cargo build --libemits exactly one warning, withoutCInone.npm run buildthencargo build --release --lib(which runstauri_build::build()), gate clean after each.reviews/version-stamp-*.md. Round 1 found the original approach wrong, rounds 3 and 4 returnedproduction_quality.CI has not run on this branch yet at the time of writing.