Run the entire CI workflow before Docker build and tests. Require Nix, native payload smoke tests, install/update E2E and signed-package upgrade acceptance before publishing. Keep Desktop Playwright E2E deferred. Archive tested Docker images and signed bundle candidates with provenance and hashes. Publishers consume those exact artifacts without rebuilding. Advance stable channels only after all required publications succeed. Keep canaries on their separate path and reject direct stable-builder publication that bypasses the gate. Move shared release transport, manifests and gates to Python. Keep native Electron adapters in JS and share feed/MIME facts as JSON. Replace the R2/feed JS implementation and move its protocol tests to Python. Verified targeted Python and JS tests, real loopback transport and CLI execution, temporary Git admission, workflow graph lint, and typechecks. No live stable release was run. Native signing, package upgrades and real registry/Store promotion still need their release-run receipts. Separate services cannot promote atomically. A promotion failure keeps the run red.
87 lines
2.9 KiB
Python
87 lines
2.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Derive a Debian package version (or channel) from a hermes-agent release tag.
|
|
|
|
Pure function; imported by scripts/termux/build_deb.sh and unit-tested by
|
|
tests/test_termux_deb_version.py (Task 4 of .hermes/plans/2026-08-31_termux-deb.md).
|
|
|
|
Mapping:
|
|
v1.2.3 -> 1.2.3-1
|
|
v1.2.3-canary.2026083112 -> 1.2.3~canary.2026083112-1
|
|
|
|
The ``~`` ranks the nightly below the corresponding stable in dpkg's version
|
|
ordering. The major version is capped at 3 digits (CalVer-style cap): a tag
|
|
with a 4+ digit major is rejected as malformed.
|
|
|
|
``--channel`` derives the release channel from the SAME tag regex: a tag with
|
|
a nightly timestamp is ``nightly``, everything else is ``stable``. This is the
|
|
single source of truth for the channel; workflows and other tooling must call
|
|
this instead of re-typing a case statement.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import sys
|
|
|
|
# The canary timestamp shape MUST match the canonical _CANARY_TAG_RE in
|
|
# hermes_cli/update_channel.py (exactly 8 or 14 digits, 20-prefixed) and
|
|
# channel_for_tag in scripts/releases/r2.py. Cross-referenced by
|
|
# tests/test_termux_deb_version.py::test_canary_tag_shape_matches_canonical.
|
|
_TAG_RE = re.compile(
|
|
r"^v(?P<major>0|[1-9]\d{0,2})\.(?P<minor>\d+)\.(?P<patch>\d+)"
|
|
r"(?:-canary\.(?P<ts>20\d{6}(?:\d{6})?))?$"
|
|
)
|
|
|
|
|
|
def _match_tag(tag: str) -> re.Match[str]:
|
|
m = _TAG_RE.match(tag)
|
|
if m is None:
|
|
raise ValueError(
|
|
f"malformed release tag {tag!r}: expected v<MAJOR>.<MINOR>.<PATCH> "
|
|
"or v<MAJOR>.<MINOR>.<PATCH>-canary.<timestamp>"
|
|
)
|
|
return m
|
|
|
|
|
|
def deb_version_for_tag(tag: str) -> str:
|
|
"""Map a release tag to its Debian version. Raises ValueError on malformed tags."""
|
|
m = _match_tag(tag)
|
|
base = f"{m.group('major')}.{m.group('minor')}.{m.group('patch')}"
|
|
ts = m.group("ts")
|
|
if ts is None:
|
|
return f"{base}-1"
|
|
return f"{base}~canary.{ts}-1"
|
|
|
|
|
|
def channel_for_tag(tag: str) -> str:
|
|
"""Map a release tag to its channel: 'canary' or 'stable'.
|
|
|
|
Derived from the same _TAG_RE as deb_version_for_tag, so the two can never
|
|
drift: a tag that yields a '~canary' deb version is canary, and the
|
|
malformed-tag rejection is identical.
|
|
"""
|
|
m = _match_tag(tag)
|
|
return "canary" if m.group("ts") is not None else "stable"
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
args = argv[1:]
|
|
channel_mode = False
|
|
if args and args[0] == "--channel":
|
|
channel_mode = True
|
|
args = args[1:]
|
|
if len(args) != 1:
|
|
mode = "deb_version.py --channel <tag>" if channel_mode else "deb_version.py <tag>"
|
|
print(f"usage: {mode}", file=sys.stderr)
|
|
return 2
|
|
try:
|
|
print(channel_for_tag(args[0]) if channel_mode else deb_version_for_tag(args[0]))
|
|
except ValueError as exc:
|
|
print(f"deb_version: {exc}", file=sys.stderr)
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main(sys.argv))
|