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