release: one strict stable-tag grammar shared by every selector

scripts/releases/semver.STABLE_TAG accepted any-width majors, so
is_valid_version('2026.9.21') was True and docker.require_stable_tag /
stable.py / release.py admitted the legacy CalVer tags that
hermes_cli.source_releases and get_last_tag() already refused. A
workflow_call carrying GitHub's current 'latest' (v2026.9.21) would have
passed the docker publish gate.

hermes_cli.update_channel already owns the canary tag shape; it now owns
STABLE_TAG_RE too (three-digit major cap, no leading zeros, no suffix)
and every stable selector imports it. release.py drops its private
_SEMVER_TAG_RE + CalVer exclusion pair, which the capped major makes
redundant.
This commit is contained in:
ethernet
2026-09-21 18:42:01 -04:00
parent 3dc75f5ab7
commit d7466aa3ba
9 changed files with 50 additions and 31 deletions

View File

@@ -10,7 +10,7 @@ import subprocess
import urllib.error
import urllib.request
from hermes_cli.update_channel import is_canary_tag
from hermes_cli.update_channel import STABLE_TAG_RE, is_canary_tag
logger = logging.getLogger(__name__)
_PUBLIC_BASE = "https://hermes-assets.nousresearch.com"
@@ -19,7 +19,6 @@ _GITHUB_ORIGIN = re.compile(
r"^(?:https://github\.com/|git@github\.com:|ssh://git@github\.com/)"
r"([A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+?)(?:\.git)?/?$", re.IGNORECASE,
)
_STABLE_TAG = re.compile(r"v(?:0|[1-9]\d{0,2})\.\d+\.\d+")
_SHA = re.compile(r"[0-9a-f]{40}")
@@ -175,7 +174,7 @@ class _BuildMetadata(HTMLParser):
def _valid_tag(tag, channel: str) -> bool:
if not isinstance(tag, str):
return False
return bool(_STABLE_TAG.fullmatch(tag)) if channel == "stable" else (
return bool(STABLE_TAG_RE.fullmatch(tag)) if channel == "stable" else (
tag == tag.strip() and is_canary_tag(tag)
)

View File

@@ -56,6 +56,13 @@ CHANNEL_CANARY = "canary"
# patch is accepted here.
_CANARY_TAG_RE = re.compile(r"^v(?:0|[1-9]\d*)\.\d+\.\d+-canary\.20\d{6}(?:\d{6})?$")
# A stable release tag: v<major>.<minor>.<patch>, no suffix. The major is
# capped at three digits so the historical CalVer tags (v2026.7.20) can never
# pass as SemVer and reach a stable feed, Docker publish, or the source
# updater. THIS is the single authority for the stable shape; every stable
# selector imports it rather than re-typing the rule.
STABLE_TAG_RE = re.compile(r"^v(?:0|[1-9]\d{0,2})\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$")
def is_canary_tag(tag: Any) -> bool:
"""True when ``tag`` is a canary release tag."""

View File

@@ -36,7 +36,7 @@ from pathlib import Path
# is import-light: only os/sys + version constants).
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from hermes_cli.update_channel import _CANARY_TAG_RE, canary_tag_for_date # noqa: E402
from hermes_cli.update_channel import _CANARY_TAG_RE, STABLE_TAG_RE, canary_tag_for_date # noqa: E402
REPO_ROOT = Path(__file__).resolve().parent.parent
VERSION_FILE = REPO_ROOT / "hermes_cli" / "__init__.py"
@@ -2163,8 +2163,7 @@ def dispatch_desktop_build(tag: str, gh_repo: str | None) -> bool:
Explicit dispatch also works for tags created by GITHUB_TOKEN.
"""
canary = _CANARY_TAG_RE.fullmatch(tag) is not None
from scripts.releases.semver import STABLE_TAG
if not canary and not STABLE_TAG.fullmatch(tag):
if not canary and not STABLE_TAG_RE.fullmatch(tag):
raise ValueError("Expected an exact stable or canary release tag")
workflow = "desktop-bundled-release.yml" if canary else "stable-release.yml"
cmd = ["gh", "workflow", "run", workflow, "--ref", "main" if canary else tag,
@@ -2249,15 +2248,12 @@ def remote_github_repo(remote: str) -> str | None:
return match.group(1) if match else None
# Cap the major at three digits, as in scripts/write_install_stamp.py.
# The legacy CalVer tags (v2026.7.20) must never match as SemVer.
_SEMVER_TAG_RE = re.compile(r"v(?:0|[1-9]\d{0,2})\.\d+\.\d+$")
_LEGACY_CALVER_TAG_RE = re.compile(r"v20\d{2}\.\d+\.\d+(?:\.\d+)?$")
# Canary prerelease tags are matched with _CANARY_TAG_RE, imported from
# hermes_cli.update_channel — the single authority for the canary tag
# shape (v<major>.<minor>.<any patch>-canary.<YYYYMMDDHHMMSS>, plus the
# legacy date-only form). The suffix keeps them out of every stable
# selector (all of which require the no-suffix SemVer shape above).
# Stable tags are matched with STABLE_TAG_RE and canary prerelease tags
# with _CANARY_TAG_RE, both imported from hermes_cli.update_channel — the
# single authority for both tag shapes (the stable major is capped at three
# digits so legacy CalVer tags like v2026.7.20 never match; the canary shape
# is v<major>.<minor>.<any patch>-canary.<YYYYMMDDHHMMSS>, plus the legacy
# date-only form). The suffix keeps canaries out of every stable selector.
# Second precision so manual fires can publish several canaries per day;
# the identifier is pure numeric and fixed-length, so semver prerelease
# comparison (numeric) and lexical sort both order it chronologically.
@@ -2274,7 +2270,7 @@ def get_last_tag():
if tags:
tag_list = tags.split("\n")
for tag in tag_list:
if _SEMVER_TAG_RE.fullmatch(tag) and not _LEGACY_CALVER_TAG_RE.fullmatch(tag):
if STABLE_TAG_RE.fullmatch(tag):
return tag
legacy_tags = git("tag", "--list", "v20*", "--sort=-v:refname")

View File

@@ -10,7 +10,7 @@ import sys
MANIFEST_SCHEMA = 1
SHA256 = re.compile(r"[a-f0-9]{64}")
GIT_SHA = re.compile(r"[a-f0-9]{40}")
from scripts.releases.semver import STABLE_TAG
from hermes_cli.update_channel import STABLE_TAG_RE
ARCHES = ("amd64", "arm64")
class DockerReleaseError(ValueError):
@@ -18,7 +18,7 @@ class DockerReleaseError(ValueError):
def require_stable_tag(tag: str) -> str:
if not isinstance(tag, str) or not STABLE_TAG.fullmatch(tag or ""):
if not isinstance(tag, str) or not STABLE_TAG_RE.fullmatch(tag or ""):
raise DockerReleaseError(f"Not a stable release tag: {tag!r}")
return tag

View File

@@ -1,10 +1,7 @@
"""Compare the stable and canary versions accepted by release feeds."""
from __future__ import annotations
import re
from hermes_cli.update_channel import _CANARY_TAG_RE
STABLE_TAG = re.compile(r"v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)")
from hermes_cli.update_channel import _CANARY_TAG_RE, STABLE_TAG_RE
def is_valid_version(version: str) -> bool:
@@ -14,10 +11,10 @@ def is_valid_version(version: str) -> bool:
if not isinstance(version, str):
return False
core, sep, tail = version.partition("-")
if sep and not STABLE_TAG.fullmatch("v" + core):
if sep and not STABLE_TAG_RE.fullmatch("v" + core):
return False
if not sep:
return bool(STABLE_TAG.fullmatch("v" + version))
return bool(STABLE_TAG_RE.fullmatch("v" + version))
if not _CANARY_TAG_RE.fullmatch("v" + version):
return False
# Canary timestamp is a fixed-length 14-digit numeric stamp.

View File

@@ -13,7 +13,7 @@ import urllib.request
from pathlib import Path
from urllib.parse import unquote, urlsplit
from scripts.releases.semver import STABLE_TAG
from hermes_cli.update_channel import STABLE_TAG_RE
SHA = re.compile(r"[a-f0-9]{40}")
DIGEST = re.compile(r"[a-f0-9]{64}")
DESKTOP_TARGETS = ("windows/x64", "windows/arm64", "macos/x64", "macos/arm64")
@@ -25,7 +25,7 @@ SMOKE_JOBS = {
def require_stable_identity(tag: str, commit: str, ref: str) -> None:
if not isinstance(tag, str) or not STABLE_TAG.fullmatch(tag) or not SHA.fullmatch(commit or "") or ref != f"refs/tags/{tag}":
if not isinstance(tag, str) or not STABLE_TAG_RE.fullmatch(tag) or not SHA.fullmatch(commit or "") or ref != f"refs/tags/{tag}":
raise ValueError("Stable release must run on its exact stable tag and commit")

View File

@@ -127,7 +127,8 @@ def test_semver_grammar_rejects_non_release_versions():
from scripts.releases.semver import compare, is_valid_version
assert is_valid_version("0.28.0") and is_valid_version("0.28.0-canary.20260904101010")
assert not is_valid_version("0.28") and is_valid_version("2026.7.20")
# Legacy CalVer (four-digit major) is not a release version anywhere.
assert not is_valid_version("0.28") and not is_valid_version("2026.7.20")
assert not is_valid_version("0.28.0-beta.1")
assert compare("0.28.0", "0.27.9") == 1
assert compare("0.28.0-canary.20260904101010", "0.28.0") == -1 # prerelease < release

View File

@@ -18,6 +18,23 @@ def test_release_tag_uses_the_semver_version():
assert release.release_tag_for_version("0.20.0") == "v0.20.0"
def test_every_stable_selector_rejects_legacy_calver_tags():
"""One shared stable grammar: a CalVer tag (v2026.9.21) must be refused by
every stable admission path, or a workflow_call carrying the old GitHub
'latest' tag would be admitted for docker/stable publication."""
from hermes_cli.source_releases import _valid_tag
from scripts.releases.docker import DockerReleaseError, require_stable_tag
from scripts.releases.semver import compare
for tag in ("v2026.9.21", "v1000.0.0", "v1.01.0", "v1.0.0-canary.20260921000000"):
assert not _valid_tag(tag, "stable")
with pytest.raises(DockerReleaseError):
require_stable_tag(tag)
assert _valid_tag("v1.0.0", "stable") and require_stable_tag("v999.0.0") == "v999.0.0"
with pytest.raises(ValueError):
compare("1.0.0", "2026.9.21")
@pytest.fixture
def release_repo(tmp_path, monkeypatch):
from tests.scripts.test_release_build_commit import git

View File

@@ -54,11 +54,13 @@ def test_canary_shape_matches_the_stable_shape_on_every_component():
from hermes_cli.update_channel import _CANARY_TAG_RE
from scripts.releases.semver import is_valid_version
assert _CANARY_TAG_RE.fullmatch("v2026.9.15-canary.20260916120000")
assert is_valid_version("2026.9.15-canary.20260916120000")
assert _CANARY_TAG_RE.fullmatch("v1.9.15-canary.20260916120000")
assert is_valid_version("1.9.15-canary.20260916120000")
# A legacy CalVer core is refused as a stable, so its canary is refused too.
assert not is_valid_version("2026.9.15") and not is_valid_version("2026.9.15-canary.20260916120000")
# And the malformed-tag negatives stay malformed.
assert not is_valid_version("2026.9.15-canary.2026091612")
assert not is_valid_version("2026.9.15-canary.20260916120000123")
assert not is_valid_version("1.9.15-canary.2026091612")
assert not is_valid_version("1.9.15-canary.20260916120000123")
@pytest.mark.parametrize("tag", [