feat(release): parse attempt refs and abandon marker refs

An attempt ref is rc.<N>-vX.Y.Z and its abandon marker is
abandoned-rc.<N>-vX.Y.Z. The ref grammar only anchors: version_from_tag
still owns the version shape, so CalVer and canary identities stay out.
This commit is contained in:
ethernet
2026-09-23 00:28:55 -04:00
parent 4bfba0e582
commit 2fa376bbec
2 changed files with 76 additions and 0 deletions

View File

@@ -67,6 +67,42 @@ def version_from_tag(ref: str) -> str | None:
return ref[1:]
# The ref grammar only anchors; ``version_from_tag`` owns the version shape.
_ATTEMPT_REF_RE = re.compile(r"rc\.(?P<attempt>[1-9][0-9]*)-(?P<tag>v[^/]+)")
_MARKER_PREFIX = "abandoned-"
def parse_attempt_ref(ref: str) -> tuple[str, int] | None:
"""``rc.<N>-vX.Y.Z`` to ``(version, N)``, or None for anything else.
The attempt comes first so the ref can never read as a SemVer prerelease
of the version it claims.
"""
match = _ATTEMPT_REF_RE.fullmatch(ref) if isinstance(ref, str) else None
if match is None:
return None
version = version_from_tag(match.group("tag"))
return (version, int(match.group("attempt"))) if version else None
def parse_marker_ref(ref: str) -> tuple[str, int] | None:
"""``abandoned-rc.<N>-vX.Y.Z``, the record that clears one attempt."""
if not isinstance(ref, str) or not ref.startswith(_MARKER_PREFIX):
return None
return parse_attempt_ref(ref[len(_MARKER_PREFIX):])
def attempt_ref(version: str, attempt: int) -> str:
ref = f"rc.{attempt}-v{version}"
if parse_attempt_ref(ref) != (version, attempt):
raise ValueError(f"invalid attempt ref: {version!r} attempt {attempt!r}")
return ref
def marker_ref(version: str, attempt: int) -> str:
return _MARKER_PREFIX + attempt_ref(version, attempt)
def _claim_version(tag: str) -> str | None:
if isinstance(tag, str) and tag.endswith("-rc"):
return version_from_tag(tag[:-3])

View File

@@ -27,6 +27,46 @@ def test_non_final_refs_are_not_versions(ref):
assert version_from_tag(ref) is None
def test_attempt_ref_carries_version_and_attempt():
from scripts.releases.versioning import parse_attempt_ref
assert parse_attempt_ref("rc.1-v0.21.5") == ("0.21.5", 1)
assert parse_attempt_ref("rc.12-v1.0.0") == ("1.0.0", 12)
def test_marker_ref_names_the_attempt_it_clears():
from scripts.releases.versioning import parse_marker_ref
assert parse_marker_ref("abandoned-rc.1-v0.21.5") == ("0.21.5", 1)
assert parse_marker_ref("rc.1-v0.21.5") is None
@pytest.mark.parametrize("ref", [
"v0.21.5-rc", "v0.21.5-rc.1", "rc.01-v0.21.5", "rc.0-v0.21.5",
"rc.1-v2026.9.21", "v0.21.5", "abandoned-rc.1-v0.21.5",
"rc.1-v0.21.5+canary.20260922T001400Z", "rc.1-v0.21", "rc.-v0.21.5",
])
def test_attempt_ref_rejects_other_shapes(ref):
from scripts.releases.versioning import parse_attempt_ref
assert parse_attempt_ref(ref) is None
def test_attempt_and_marker_refs_round_trip():
from scripts.releases.versioning import attempt_ref, marker_ref, parse_attempt_ref, parse_marker_ref
assert parse_attempt_ref(attempt_ref("0.21.5", 3)) == ("0.21.5", 3)
assert parse_marker_ref(marker_ref("0.21.5", 3)) == ("0.21.5", 3)
@pytest.mark.parametrize("version, attempt", [("2026.9.21", 1), ("0.21.5", 0), ("0.21", 1)])
def test_attempt_ref_refuses_what_it_could_not_parse(version, attempt):
from scripts.releases.versioning import attempt_ref
with pytest.raises(ValueError):
attempt_ref(version, attempt)
def test_claim_advances_the_line_but_is_not_a_final_tag():
assert version_from_tag("v0.21.5-rc") is None
assert derive_next_version(published=None, claims=["v0.21.5-rc"], bump="patch") == "0.21.6"