fix: clean version stamps on release builds (no -dirty markers)#50
Merged
StefanSteiner merged 2 commits intoMay 27, 2026
Merged
Conversation
release-please bumps `[workspace.package].version` in Cargo.toml but does NOT update Cargo.lock — the lockfile isn't in the action's extra-files list. When CI checks out a release tag and runs `cargo build --release`, cargo silently reconciles the lockfile in-place to match the bumped Cargo.toml. That dirties the worktree; build.rs's `git status --porcelain -uno` then trips, and the released binary gets stamped `<hash>-dirty-<timestamp>` even though the source matches the tag perfectly. Fix: add `:(exclude,top)Cargo.lock` to both build.rs git-status invocations so a lockfile-only modification doesn't trip the dirty marker. Any other modified tracked file (including a deliberately- edited Cargo.lock alongside a code change) still trips it because git status will list the other file. The companion fix to release-please.yml that prevents the lockfile from drifting in the first place lands in a follow-up commit; this build.rs change is the safety net that ensures clean release binaries even if the workflow change has rough edges. Empirically validated: with Cargo.toml at 0.2.1 and Cargo.lock pinning members at 0.1.3, `cargo metadata --format-version=1` rewrites the lockfile and `git status --porcelain -uno` reports `M Cargo.lock`. With the pathspec, `git status --porcelain -uno -- ':(exclude,top)Cargo.lock'` returns empty. A real source-file modification still trips the marker.
Root cause of the `0.2.1.r<hash>-dirty-<timestamp>` markers on the
npm-published binaries: release-please bumps `[workspace.package].version`
in Cargo.toml on every release, but its `extra-files` config doesn't
include Cargo.lock. So the release tag points at a tree where Cargo.toml
is at the new version (e.g. 0.2.1) but Cargo.lock still pins workspace
members at the previous version (e.g. 0.1.3). When CI checks out that
tag and runs `cargo build --release`, cargo silently rewrites Cargo.lock
in-place to match — the worktree goes dirty, and `build.rs`'s
`git status --porcelain -uno` stamps `-dirty-<timestamp>` onto the
released binary.
Fix: add follow-up steps to release-please.yml that fire whenever a
release PR exists (created or updated) and reconcile Cargo.lock onto
the same release-please branch:
1. Checkout the release-please branch (`steps.release.outputs.pr` JSON
carries `headBranchName`).
2. Setup Rust stable.
3. Run `cargo metadata --format-version=1 > /dev/null` — empirically
verified to flip exactly the 7 workspace member version rows on
this workspace and touch no external dep rows.
4. Sentinel step: parse the lockfile diff and fail loudly if any
non-workspace package version changed. Defense against future
cargo behavioral drift.
5. Commit Cargo.lock as `github-actions[bot]` and push.
Why `cargo metadata` and not `cargo update --workspace [--offline]`:
the `--workspace` flag does NOT prevent external dep re-resolution
from the local cache, and `--offline` makes that brittle.
`cargo metadata` is a more focused primitive — it just reconciles
the lockfile against current Cargo.tomls.
Also adds a workflow-level `concurrency: { group: release-please,
cancel-in-progress: false }` to serialize runs and avoid racing the
lockfile push against a parallel release-please invocation.
Pairs with the build.rs Cargo.lock exclusion in the prior commit:
that fix is the safety net for any case where this workflow
misbehaves; this fix is the real source-of-truth solution that
prevents the lockfile from drifting in the first place.
6 tasks
StefanSteiner
added a commit
that referenced
this pull request
May 27, 2026
The follow-up steps added in PR #50 reference `fromJson(steps.release.outputs.pr).headBranchName` directly in `ref:` (checkout) and `env:` (push) blocks. GitHub Actions evaluates every `${{ }}` expression in the workflow at job-load time, BEFORE per-step `if:` gates short-circuit. When release-please runs on a push that doesn't produce a release PR (e.g. a PR merge whose conventional- commit was already covered by an earlier release), `pr` is the empty string, `fromJson('')` errors out, and the entire job fails with: ##[error]The template is not valid. .../release-please.yml (Line: 203, Col: 19): Error reading JToken from JsonReader. Path '', line 0, position 0. This blocks release-please from ever opening the next release PR. Observed in run 26494686197 on the merge of PR #50 itself. Fix: extract the branch name in a new "Resolve release-please branch name" step (id=`branch`) that does the parse via `jq` inside a `run:` block — that block only runs when the step's `if:` gate passes, so the parse is never attempted on empty input. The downstream `actions/checkout` `ref:` and the final `git push` `env: BRANCH:` both reference `steps.branch.outputs.name`, which evaluates safely to the empty string when the step was skipped. Add a doc comment explaining the GHA expression-evaluation timing trap so the next person reading the workflow doesn't accidentally re-introduce the same pattern.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The npm-published
hyperdb-mcp@0.2.1binary stamps its version as0.2.1.rb57f0c37-dirty-20260527T015408Z— the-dirty-<timestamp>marker should never appear on a release build.Root cause (verified end-to-end):
release-please-config.jsonpatchesCargo.tomland per-crateCargo.tomls but NOTCargo.lock. So thev0.2.1tag points at a tree whereCargo.tomlsays0.2.1andCargo.lockstill pins workspace members at0.1.3. When CI checks out the tag and runscargo build --release, cargo silently rewrites the lockfile in-place to reconcile, dirtying the worktree.build.rs'sgit status --porcelain -unothen trips and stamps the dirty marker.Fix — two-pronged
1.
fix(build): exclude Cargo.lock from dirty-marker detection(hyperdb-mcp/build.rs,hyperdb-bootstrap/build.rs)Add
:(exclude,top)Cargo.lockto bothgit status --porcelain -unoinvocations. A lockfile-only modification no longer trips the dirty marker. Any other modified tracked file (including a deliberately-editedCargo.lockalongside a real code change) still trips it because git status will list the other file.2.
fix(ci): regenerate Cargo.lock in release-please PR(.github/workflows/release-please.yml)Add follow-up steps that fire whenever a release PR exists (
pr != ''). They check out the release-please branch, runcargo metadata --format-version=1 > /dev/nullto reconcile the lockfile, verify the diff only touches workspace-member rows, and push the lockfile commit asgithub-actions[bot]. Plus a workflow-levelconcurrency: { group: release-please, cancel-in-progress: false }block to serialize concurrent runs.The two prongs are deliberate defense-in-depth: the build.rs change ensures the released binary stamps cleanly even if the workflow ever misfires; the workflow change keeps the release-tag tree in sync so cargo doesn't have to rewrite at build time.
Why
cargo metadata(notcargo update --workspaceorcargo generate-lockfile)Empirically tested on the live workspace with
Cargo.toml@0.2.1andCargo.lock@0.1.3:cargo metadata --format-version=1 > /dev/nullflips exactly the 7 workspace-member version rows and touches no external dep rows. ✓cargo generate-lockfiledoes a full re-resolve and bumps unrelated transitive deps (crypto-common 0.1.6 → 0.1.7,matchit 0.8.4 → 0.8.6, etc.) plus changes the lockfile format version. Noisy in a release commit. ✗cargo update --workspace [--offline]'s--workspaceflag does NOT prevent external dep re-resolution from the local cache. ✗A two-layer sentinel verifies the lockfile diff post-sync: Layer 1 catches lockfile format-version flips (
^[+-]version = [0-9]+$, unquoted), Layer 2 enumerates workspace members at runtime viacargo metadata --no-deps | jq(no hard-coded list to maintain) and asserts every changed package name is in that set. If either layer detects something unexpected, the workflow aborts and blocks the release.Acknowledged residual risks
chore: sync Cargo.lockcommit. The follow-up step re-fires on every re-run via thepr != ''gate, so the FINAL state of the release PR (when it merges) is always lockfile-synced. A maintainer who merges in a tiny window between release-please's force-push and our follow-up landing could still get a drifted tag — thebuild.rsCargo.lock pathspec exclusion is the safety net for that case.0.2.1npm packages stay dirty. Not retroactively fixed. The next release (this PR'sfix:title triggers v0.2.2) will be the first clean one.Test plan
chore: sync Cargo.lock with bumped workspace versionscommit.Cargo.lockshows the workspace member rows bumped to v0.2.2.cargo build --releasedoes NOT rewrite Cargo.lock (worktree stays clean).cargo:rustc-env=HYPERDB_GIT_HASH=<hash>with NO-dirtysubstring.npx -y hyperdb-mcp@0.2.2 --versionreturns0.2.2.r<hash>with no-dirty-...suffix.Empirical validation (this branch, this machine)
Cargo.toml@0.2.1andCargo.lock@0.1.3,cargo metadata --format-version=1 > /dev/nullrewrites the lockfile andgit status --porcelain -unoreportsM Cargo.lock.git status --porcelain -uno -- ':(exclude,top)Cargo.lock'returns empty (clean).echo "" >> hyperdb-mcp/src/main.rs) still trips the dirty marker — verified by inspecting the binary's--versionoutput:0.2.1.rac35ed73-dirty-20260527T055538Z.cargo metadataexclusively flipped the 7 workspace member rows:hyperdb-api,hyperdb-api-core,hyperdb-api-node,hyperdb-api-salesforce,hyperdb-bootstrap,hyperdb-mcp,sea-query-hyperdb. No external deps changed.bash -c(matching the GitHub Actions runner default shell) and produced the expected workspace-only summary.feature-dev:code-reviewer+aisuite:code-review) signed off; the deeper reviewer's MAJOR findings (hard-coded workspace list, awk attribution by proximity, missing protoc comment) are addressed.