Skip to content

fix(release): guard the MSI cross-release ordering hazard at a MAJOR bump - #544

Merged
MichaelTaylor3d merged 5 commits into
mainfrom
fix/540-msi-cross-release-ordering
Sep 3, 2026
Merged

fix(release): guard the MSI cross-release ordering hazard at a MAJOR bump#544
MichaelTaylor3d merged 5 commits into
mainfrom
fix/540-msi-cross-release-ordering

Conversation

@MichaelTaylor3d

@MichaelTaylor3d MichaelTaylor3d commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

DO NOT MERGE -- gate round in progress.

Task

Closes #540: scripts/package-version.sh's MINOR-overflow carry (added by #537, resolving #521 and #522) is
monotonic for dig-node's whole MAJOR==0 lifetime, but the SPEC itself flagged a cross-release
hazard it cannot see: a carried release like 0.511.0 (MSI 1.255.0) can compare HIGHER under
msiexec's numeric ordering than a later, perfectly legal 1.0.0 (MSI 1.0.0). msiexec would
either refuse the "upgrade" (DowngradeErrorMessage) or leave the older payload installed.

Measurement first

Checked every published dig-node tag: the highest MINOR ever shipped is 252 (0.252.11), well
under the 255 threshold where the carry even activates. The hazard has never been reached. But
"never reached yet" is not "unreachable" -- minor climbs every feature release and is 3 bumps from
255 -- so I did not close this as not-applicable; I closed the gap before it can be reached.

What changed

scripts/package-version.sh gains an optional second argument, PREV_VERSION (the previous
stable release's bare X.Y.Z). When supplied, the script folds it with the identical rule and
refuses to emit a version whose MSI tuple compares LOWER than PREV_VERSION's -- turning "pick a
MAJOR-bump number high enough to clear history" from an unchecked human decision into a machine
check. An EQUAL tuple is NOT rejected (that's the existing AllowSameVersionUpgrades="yes"
same-version-upgrade case, not a downgrade).

Omitting the argument is byte-for-byte unchanged -- all three existing package.yml callsites
pass a single arg today and are untouched by this PR.

Sequencing with #542

#542 (open, draft, "gate round in progress") adds a step to ensure-version-increment.yml's
required Check version increment job that calls package-version.sh with the head version only.
That job already checks out both main and the PR head, so wiring this guard in is a one-line
follow-up once #542 merges: pass main's Cargo.toml version as the second argument to the same
call. I deliberately did NOT touch ensure-version-increment.yml in this PR to avoid conflicting
with #542's in-flight branch on the same file -- @mt-dev/orchestrator, please sequence: land #542,
then a 1-line addition wires this guard into that job.

Also fixed (cheap, same files already open)

#521/#522 were referenced as dig_ecosystem#521/dig_ecosystem#522 in the script, its test
file, and SPEC.md -- those are dig-node's OWN issues (verified: "Version scheme has exhausted the
MSI minor field..." / "fix(release)!: the 0.x version scheme hits the MSI 255 ceiling..."), not the
unrelated dig_ecosystem tickets of the same numbers. Corrected to bare #521/#522 in the three
files this PR already touches.

Verification

Test COUNTS, not exit status:

  • scripts/tests/package-version.test.sh: 58 ok / 0 fail (was 49 before this PR; +9 new cases
    for the cross-release guard).
  • Revert-proof: reverted package-version.sh to origin/main's version (script only, kept the
    new tests) and re-ran -- 4 genuine failures, including the ticket's exact scenario:
    FAIL the ticket's own example -- 0.511.0 outranks a naive 1.0.0: accepted 1.0.0 over prev 0.511.0 (exit 0). Restored the fix -> back to 58/0. The test suite demonstrably catches the defect.
  • Manual spot checks:
    • package-version.sh 1.0.0 0.511.0 -> package-version: '1.0.0' (MSI 1.0.0) compares LOWER than the previous release '0.511.0' (MSI 1.255.0) ... exit=1 (the hazard, refused)
    • package-version.sh 2.0.0 0.511.0 -> msi_product_version=2.0.0 exit=0 (a number that clears
      history, accepted)
    • package-version.sh 0.511.0 (no 2nd arg, an existing callsite shape) -> unchanged,
      msi_product_version=1.255.0 exit=0
  • shellcheck on both touched shell files: only pre-existing SC2016 info notes on lines this PR
    did not touch (deliberate single-quoted injection-test literals).

Version bump

0.254.40 -> 0.254.51 (patch -- script/test/spec hardening, no behaviour change for any existing
caller). Re-bumped once: main advanced to 0.254.41 via #535 while this PR was in flight, so I
merged origin/main forward (not rebase) and re-bumped. The queue across five open PRs sequences
this one last on purpose -- #542 0.254.42, #539 0.254.43, #543 0.254.44, #536 0.254.50, this PR
0.254.51 -- so by the time this merges, #542 (touching the same ensure-version-increment.yml this
PR's follow-up targets) is already on main. Root Cargo.toml + cargo update -w --offline
(confirmed --dry-run -> Locking 0 packages after the real update; version read back from
Cargo.toml on disk, not the commit log).

Closes #540

MichaelTaylor3d and others added 3 commits September 3, 2026 12:33
…bump

scripts/package-version.sh's MINOR-overflow carry (#537, closing #521/#522) folds an
overflowing MINOR into the idle MSI major field and is monotonic for the whole
MAJOR==0 lifetime, but it cannot see a cross-release hazard: a carried release like
0.511.0 (MSI 1.255.0) can compare HIGHER under msiexec's numeric ordering than a
later, perfectly legal 1.0.0 (MSI 1.0.0). An in-version guard can't catch this --
1.0.0 alone is not illegal, only a specific predecessor makes it a downgrade.

Add an optional second argument, PREV_VERSION (the previous stable release's bare
X.Y.Z). When supplied, the script folds it with the same rule and refuses to emit
a version whose MSI tuple compares LOWER than PREV_VERSION's -- turning the "pick a
high-enough MAJOR-bump number" decision into a machine-checked one instead of an
unchecked human call. Omitting it (every existing package.yml callsite) is
byte-for-byte unchanged; ensure-version-increment.yml already checks out both the
PR head and main, so a follow-up can wire main's version in at zero extra cost
(sequenced separately against #542, which is adding a step to that same job).

Measured (dig-node#540): MINOR has never exceeded 255 in any released dig-node
version, so nothing shipped is affected today -- this closes the hazard before it
can ever be reached rather than reacting once it is.

Also corrects "dig_ecosystem#521/#522" references in this script/test/SPEC to
"#521/#522" -- those are dig-node's own issues, not the unrelated dig_ecosystem
tickets of the same numbers.

Revert-proof: stripping the guard reproduces the exact hazard as an accepted exit 0
(0.511.0 -> 1.0.0), and reinstating it passes the full suite (58 ok / 0 fail).

Closes #540

Co-Authored-By: Claude <noreply@anthropic.com>
Merged origin/main forward (7042f89, #535) rather than rebasing -- the queue
across the five open PRs sequences this one last (#542 0.254.42, #539 0.254.43,
#543 0.254.44, #536 0.254.50, this PR 0.254.51), deliberately: #542 touches
ensure-version-increment.yml, which this PR's follow-up note also targets, so
landing last means the final shape is on main rather than guessed at.

Re-read the version from Cargo.toml on disk (not the commit log) after the
merge, per the standing caution that a rebase can silently drop a bump commit
as "already upstream" -- this was a merge, and the version file itself
confirms 0.254.51 post-bump.

Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d added a commit that referenced this pull request Sep 3, 2026
Per the main lane: main advanced to exactly 0.254.41 after #535's rebase,
tying this branch's version. 0.254.43 clears main and every sibling PR in
the version-bump queue (#542=0.254.42, #543=0.254.44, #536=0.254.50,
#544=0.254.51).

Cargo.lock re-synced with `git checkout origin/main -- Cargo.lock` followed
by `cargo update -w --offline` (never hand-editing lock conflict markers),
confirmed clean with `--dry-run` -> `Locking 0 packages`.

Co-Authored-By: Claude <noreply@anthropic.com>
…ease-ordering

# Conflicts:
#	Cargo.lock
#	Cargo.toml
MichaelTaylor3d added a commit that referenced this pull request Sep 3, 2026
#539)

* fix(wallet): discipline reservation liveness against a monotonic clock

Reservation deadlines (#502/#525/#528) are anchored entirely on wall-clock
readings. #528 closes the case where the clock is already wrong at the
moment a reservation is FIRST written. It does not close the general form
(#532): a wall clock stepped FORWARD while a reservation is already live,
mid-hold -- an NTP step, a VM pause/resume, an operator setting the clock --
produces no self-contradiction for #528's check to catch, yet the very next
prune reads the jump as elapsed time and can retire a bundle's hold while it
is still genuinely in flight, with no bound on how far forward the step
goes (the #348/#497 double-spend direction).

Add ClockGovernor: it disciplines every reservation-lifecycle "now" reading
so it cannot advance, between two observations, faster than a monotonic
clock says real time has actually elapsed. A forward wall-clock jump is
absorbed rather than trusted and the disciplined clock simply runs behind
until real time catches up, at which point it resumes tracking the wall
clock with no special unfreeze step. A backward step is passed straight
through unclamped, since it can only lengthen a hold, never shorten one --
the safe direction #502/#528 already accept elsewhere.

The governor lives for the process's lifetime and is not persisted: a
restart re-seeds it from the wall clock at that moment, so a clock already
wrong at boot remains #528's write-time contradiction check's problem, not
this one's.

Closes #532

Co-Authored-By: Claude <noreply@anthropic.com>

* chore(release): bump workspace version to 0.254.20

Root workspace version, per the main lane -- the minor field's version scheme
is being fixed separately under #521/#522; this is the interim number to
carry PR #539 (dig-node#532) through the version-increment gate.

Cargo.lock refreshed in the same commit (cargo update -w --offline) so
dig-node-service's locked entry matches -- every CI job runs --locked, and a
manifest-only bump here fails Clippy/Test+coverage/all three package builds
together on a change that cannot otherwise break a build.

Co-Authored-By: Claude <noreply@anthropic.com>

* chore(release): bump workspace version to 0.254.43

Per the main lane: main advanced to exactly 0.254.41 after #535's rebase,
tying this branch's version. 0.254.43 clears main and every sibling PR in
the version-bump queue (#542=0.254.42, #543=0.254.44, #536=0.254.50,
#544=0.254.51).

Cargo.lock re-synced with `git checkout origin/main -- Cargo.lock` followed
by `cargo update -w --offline` (never hand-editing lock conflict markers),
confirmed clean with `--dry-run` -> `Locking 0 packages`.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
…eep 0.254.72)

# Conflicts:
#	Cargo.lock
#	Cargo.toml
@MichaelTaylor3d
MichaelTaylor3d marked this pull request as ready for review September 3, 2026 20:37
@MichaelTaylor3d
MichaelTaylor3d merged commit 2b5f4da into main Sep 3, 2026
13 checks passed
@MichaelTaylor3d
MichaelTaylor3d deleted the fix/540-msi-cross-release-ordering branch September 3, 2026 20:37
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.

MSI mapping: a carried MINOR-overflow can outrank a later real 1.0.0, breaking upgrade at the 1.x transition

1 participant