0
0
Fork 0

fix(bump_project_version): cross-check pi.version against GVL, take the max

Recurring "GVL misread" bug in release_project_version: when
Project Information.Version drifts BEHIND the runtime-anchor GVL
(_MCP_PROJECT_VERSION.sVersion), the bump used the stale pi.version
as the resume point and silently regressed the version, often
colliding with an existing v* tag.

Observed twice on the MCPTest2 sandbox:

  1. v1.0.4.0 (2026-04-26): bump from on-disk 1.2.0.0 read pi.version
     as 1.0.3.0 -> revision -> 1.0.4.0. Tag deleted; recovered as
     v1.2.1.0 via manual finish script.

  2. v1.1.0.0 collision (2026-04-26): bump from on-disk 1.2.1.0 read
     pi.version as 1.0.0.0 -> minor -> 1.1.0.0. Tag already existed,
     git tag step failed, release pipeline aborted. Recovered by
     two manual minor bumps (1.1.0.0 -> 1.2.0.0 -> 1.3.0.0) and an
     amended commit, released as v1.3.0.0.

Root cause: the MCPTest2 v1.2.0.0 and v1.2.1.0 releases were
finished by external (non-MCP) Node scripts that updated the GVL
via inject-once but never wrote pi.version back through the
bump_project_version pathway. So pi.version stayed pinned at
whatever value the LAST true bump_project_version run left it at
(in MCPTest2's case, ~1.0.0.0), while the GVL kept moving forward.

Fix: in the pi-present branch, read both pi.version and the GVL
sVersion, parse both as 4-tuples, and take the max as the resume
point. The max is always safe: both sides only ever move forward
in the normal case, so the higher of the two is by construction
the true latest version. When a drift is detected (pi behind GVL),
emit a WARNING and self-heal pi.version forward to the GVL value
before the bump so the warning doesn't recur on the next call.

The pi-missing branch is unchanged (still falls back to GVL).

Documented inline in the function with the regression scenario for
future maintainers. No new test (the affected logic runs inside
CODESYS's IronPython and doesn't have a unit-test scaffold here);
the inline comment + this commit message are the regression record.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Karstein Kvistad 2026-04-26 16:19:27 +02:00
parent c50970b15e
commit b42e10411f

View file

@ -221,7 +221,40 @@ try:
if before_raw:
print("DEBUG: Project Information missing, resuming from GVL: %s" % before_raw)
else:
before_raw = pi.version
# Cross-check pi.version against the runtime-anchor GVL and take the
# max of the two as the resume point. Drift between the two sides
# happens when a release is finished by an external (non-MCP) script
# that updates one but not the other -- the GVL gets refreshed via
# inject-once writes, and pi.version gets refreshed via direct .project
# binary edits, but a script that touches only one leaves the other
# stale. Without the cross-check, the next bump would silently regress
# the version (observed: pi.version stuck at 1.0.0.0 while GVL was at
# 1.2.1.0 -> minor bump gave 1.1.0.0, colliding with an existing tag).
# Taking the max is always safe: the GVL only ever moves forward (set
# by maintain_version_gvl on every bump), and pi.version only ever
# moves forward (set by pi.version assignment). The higher of the
# two is the true latest version regardless of which side drifted.
pi_raw = pi.version
gvl_raw = read_version_from_gvl(primary_project)
pi_parts = parse_version(pi_raw) if pi_raw is not None else (0, 0, 0, 0)
gvl_parts = parse_version(gvl_raw) if gvl_raw else (0, 0, 0, 0)
if pi_parts >= gvl_parts:
before_raw = pi_raw
else:
print("WARNING: Project Information.Version (%s) is BEHIND the runtime anchor "
"GVL (%s) -- this happens when a previous release was finished by an "
"external script that updated the GVL but not the .project metadata. "
"Using the GVL value as the resume point so the bump doesn't regress." % (
pi_raw, gvl_raw))
before_raw = gvl_raw
# Heal pi.version forward to the GVL value before the bump so this
# warning doesn't recur on the next call.
try:
pi.version = gvl_raw
print("DEBUG: healed Project Information.Version: %s -> %s (matching GVL)" % (
pi_raw, gvl_raw))
except Exception as heal_e:
print("WARNING: could not heal Project Information.Version: %s" % heal_e)
before_str = str(before_raw) if before_raw is not None else None
# First-run convention: if no version is set yet, seed at 1.0.0.0 instead