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:
@@ -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)
|
||||
)
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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", [
|
||||
|
||||
Reference in New Issue
Block a user