Skip to content

fix: stop the cross-era upgrade guard and version tracking from mishandling brew --HEAD stamps - #5625

Open
anisoptera wants to merge 6 commits into
gastownhall:mainfrom
anisoptera:fix/homebrew-migration
Open

fix: stop the cross-era upgrade guard and version tracking from mishandling brew --HEAD stamps#5625
anisoptera wants to merge 6 commits into
gastownhall:mainfrom
anisoptera:fix/homebrew-migration

Conversation

@anisoptera

Copy link
Copy Markdown
Contributor

Closes #5603.

A Homebrew --HEAD install stamps HEAD-<shortsha> (or bare HEAD, optionally with a _<revision> suffix) into main.Version, which is not semver. That shape leaked into every version-string consumer with a different failure in each:

  • Upgrade guard (the Cross-era upgrade guard bricks brew --HEAD workspaces: HEAD-<sha> in .local_version fails currentVersionWitness #5603 brick): currentVersionWitness rejected the stamp, so an explicit-server workspace with a local Dolt root was refused as a legacy schema. doctor.IsBrewHeadVersion is now the canonical recognizer for the stamp (bounded to git's 7–40-char hex abbreviation range, digits-only revision suffix), and the guard delegates to it. versionCore also strips pre-release/build suffixes uniformly so a suffixed stamp cannot land outside both era buckets.
  • Version tracking: CompareVersions reads HEAD stamps as 0.0.0, so a changed stamp never registered as an upgrade; a changed stamp with a HEAD build on either side is now treated as one. The pre-v56 recovery path (which deletes .dolt) is gated on an actual semver predecessor (normalized through versionCore) so a HEAD stamp can't read as "pre-0.56".
  • bd doctor: the metadata tracking check reported the stamp as a permanently-unfixable invalid format, and CheckCLIVersion warned "update available: brew upgrade beads" forever (which would move the user off HEAD). Both now treat HEAD builds as healthy.
  • bd upgrade status/review: a HEAD-stamp previous version matched no changelog entry and dumped the entire release history, with a malformed vHEAD-<sha> banner label. HEAD stamps now yield no changelog delta and render verbatim.

The docs table in docs/getting-started/upgrading.md is updated to describe the current-era witness shapes the guard admits.

Table-driven tests cover the witness predicates, the stamp recognizer (including signed-revision and dirty-suffix rejections), HEAD-stamp upgrade detection, and the guard's admit/refuse matrix. Reviewed with /simplify and /code-review (fixes from both applied as their own commits).

🤖 Generated with Claude Code

https://claude.ai/code/session_01G95EoTDVi5EdEFk4wJgRuG

anisoptera and others added 6 commits August 10, 2026 18:12
Homebrew --HEAD installs of the core beads formula stamp main.Version as
HEAD-<shortsha>, and bd writes that string verbatim into .local_version.
currentVersionWitness demanded a strict x.y.z with major >= 1, so every
server-mode workspace with a local dolt root whose last run was a brew
--HEAD build was refused as a legacy Dolt server workspace. The same
parse gap refused real pre-release witnesses such as 1.1.0-rc.1.

Accepting HEAD-<hex> is not fail-open: the legacy-era tap formula
(0.55-0.62) shipped prebuilt GoReleaser tarballs with baked-in semver
and had no head spec, and make build / go install only override
main.Build, so no legacy-era channel could ever have written HEAD-<sha>
into the witness file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G95EoTDVi5EdEFk4wJgRuG
Code review of the witness fix surfaced one serious interaction and
several hardening gaps, applied here:

- Gate the destructive pre-v56 recovery path on IsValidSemver: a
  HEAD-<sha> witness compares as version 0, which previously routed a
  current workspace into RecoverPreV56DoltDir and os.RemoveAll(.dolt).
- Route legacyVersionMinor through the same suffix-stripping versionCore
  as currentVersionWitness, so legacy snapshot stamps like 0.62.1-next
  are still classified (and refused) as legacy rather than neither era.
- Accept the full set of real Homebrew HEAD stamps (bare HEAD and
  HEAD-<sha>_<revision>), delegate hex checking to isHexObjectID, and
  bound the sha to 7-40 chars.
- Update the cross-era decision table in upgrading.md to the actual
  admit rule, since the refusal error string sends operators there.
- Replace duplicated guard subtests with a table covering both admit
  and refusal boundaries, and add predicate-level tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G95EoTDVi5EdEFk4wJgRuG
CompareVersions reads a HEAD-<sha> stamp as 0.0.0, so a brew --HEAD
reinstall (HEAD-423afdc -> HEAD-f925f3f) or a release-to-HEAD move never
set versionUpgradeDetected, and the one-shot post-upgrade reconciliation
— eager migrate-before-open, workspace version markers, the proxied
reconcile, the upgrade notification — silently skipped for every --HEAD
install. Treat any changed witness with a HEAD stamp on either side as
an upgrade: brew reinstalls only move forward, and if that assumption is
ever wrong the workspace-side reconciler refuses to move the markers and
the schema forward-drift guard fails the open.

The pre-v56 recovery path stays unreachable from a HEAD stamp because it
is gated on IsValidSemver(previousVersion). Actual schema migrations
were never broken here — MigrateUp keys off schema_migrations on every
writable open — this restores the marker bookkeeping and eager
reconciliation around them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G95EoTDVi5EdEFk4wJgRuG
bd doctor's version-tracking check called any non-semver .local_version
malformed, so every --HEAD install warned permanently — with fix advice
("run any bd command") that just rewrites the same stamp. A Homebrew
HEAD stamp is a healthy current-era marker; report it as OK, noting the
staleness heuristic cannot apply to an unordered stamp.

The predicate moves to doctor.IsBrewHeadVersion as the canonical
definition and the legacy upgrade guard delegates to it, so the guard's
admit rule and the doctor's health rule cannot drift apart.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G95EoTDVi5EdEFk4wJgRuG
…hanges

Parse the major version once in currentVersionWitness and drop the
negative-part guard made unreachable by versionCore's suffix truncation;
replace the manual hex loop in IsBrewHeadVersion with strings.Trim; share
one trackingActive DoctorCheck across the three identical OK returns in
CheckMetadataVersionTracking.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G95EoTDVi5EdEFk4wJgRuG
…and CLI version check

From /code-review: getVersionsSince returns nothing for a HEAD-stamp
previous version instead of the whole changelog; upgrade banners render
HEAD stamps verbatim via displayVersion instead of a malformed v prefix;
CheckCLIVersion skips the release comparison for HEAD builds rather than
warning forever; the HEAD _<revision> parse is digits-only so signed
forms no longer pass the legacy guard's witness check; the pre-v56
recovery gate normalizes through versionCore so v-prefixed or suffixed
legacy stamps still recover.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G95EoTDVi5EdEFk4wJgRuG
@julianknutsen

Copy link
Copy Markdown
Collaborator

Not superseding this — flagging an overlap and a suggestion, from an independent hit on the same defect.

I chased the identical refusal in production this week from a different writer: a Go pseudo-version, v1.1.1-0.20260805093327-bf97b73749ac, stamped into main.Version and written straight through to .beads/.local_version. Five cities, every bd invocation failing, 644 gate-sweep failures in 6 hours before the witness files were hand-rewritten. Your root-cause writeup in #5603 is exactly right, including the 1.1.0-rc.1 case — versionCore fixes my string too.

I opened #5650, which overlaps yours on currentVersionWitness but takes a different cut at the policy rather than the shapes:

  • Parse with golang.org/x/mod/semver (already an indirect dep) instead of cutting at the first -/+. Strict superset of versionCore for the semver shapes, and it lets legacyVersionMinor share one parse so a pre-1.0 pseudo-version classifies as legacy instead of falling through unrecognized.
  • Split "unparseable" out of "legacy" entirely. A witness that is present but unreadable becomes its own era: warn and open. Missing witness and pre-1.0 witness still refuse. The argument is the one you already make in Cross-era upgrade guard bricks brew --HEAD workspaces: HEAD-<sha> in .local_version fails currentVersionWitness #5603 — every pre-1.0 bd wrote a plain X.Y.Z through this same writer, so a string that fails the parse is evidence against a legacy workspace — generalized from HEAD-<sha> to the whole class, so the next unanticipated stamp doesn't brick anyone.

Both are still worth landing, and I did not duplicate yours. Under the unknown era a brew --HEAD workspace opens (I added your three stamps as coverage and credited you and #5603 in the test), but it warns on every run, because a HEAD stamp is the one shape trackBdVersion rewrites identically each time — it never self-heals the way a garbled witness does. Silencing that needs IsBrewHeadVersion, i.e. this PR. Your doctor changes (the forever-"update available" warning and the unfixable "invalid version format") are orthogonal to the guard and not touched by mine.

Suggestion, entirely yours to take or leave: if both land, the guard side of this PR could shrink — with the unknown era in place currentVersionWitness no longer needs the isBrewHeadVersion branch to keep the workspace open, only to keep it quiet, so IsBrewHeadVersion could stay purely a "known-good stable stamp, don't warn" recognizer and versionCore could drop out in favor of the semver parse. Happy to rebase #5650 on top of this instead if you'd rather land yours first — say the word and I'll do that rather than make you deal with a conflict.

🤖 Generated with Claude Code

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.

Cross-era upgrade guard bricks brew --HEAD workspaces: HEAD-<sha> in .local_version fails currentVersionWitness

2 participants