diff --git a/src/cmlxc/driver_base.py b/src/cmlxc/driver_base.py index dc414d2..307a678 100644 --- a/src/cmlxc/driver_base.py +++ b/src/cmlxc/driver_base.py @@ -20,6 +20,7 @@ DNS_CONTAINER_NAME, BuilderContainer, DNSContainer, + SetupError, ) from cmlxc.incus import Incus @@ -46,6 +47,7 @@ def parse_source(value: str, default_url: str) -> SourceSpec: Accepted forms: @ref -- branch/tag on the default remote + @latest -- newest release tag /path or ./path -- local directory URL@ref -- custom remote at a given ref """ @@ -64,6 +66,19 @@ def parse_source(value: str, default_url: str) -> SourceSpec: raise ValueError(f"Invalid SOURCE: {value!r}. Use @ref, /path, ./path, or URL@ref.") +# Don't match pre-release and non-semver tags +_RELEASE_TAG_RE = re.compile(r"^v?\d+\.\d+\.\d+$") + + +def latest_release_tag(tag_output): + """Pick the first matching and thus newest release tag from + ``git tag -l --sort=-v:refname`` output.""" + for tag in (tag_output or "").splitlines(): + if _RELEASE_TAG_RE.match(tag): + return tag + return None + + _RELAY_NAME_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9-]*$") @@ -146,7 +161,10 @@ def add_cli_options(cls, parser, completer=None): "--source", default=f"@{cls.DEFAULT_REF}", metavar="SOURCE", - help=f"Driver source: @ref, /path, ./path, or URL@ref (default: @{cls.DEFAULT_REF}).", + help=( + "Driver source: @ref (branch, tag), @latest (newest release tag)," + f" /path, ./path, or URL@ref (default: @{cls.DEFAULT_REF})." + ), ) action = parser.add_argument( "name", @@ -235,7 +253,9 @@ def prep_builder(cls, ix, out, bld_ct): tmp_dest, exists = cls.get_git_main_path(bld_ct, out) if not exists: - source = parse_source(f"@{cls.DEFAULT_REF}", cls.DEFAULT_SOURCE_URL) + # Always main: this is the shared cache clone, and init_builder + # checks the requested ref out of its copy + source = parse_source("@main", cls.DEFAULT_SOURCE_URL) bld_ct.setup_repo(tmp_dest, out, source) else: out.print(f" Fetching {cls.REPO_NAME}-git-main from upstream ...") @@ -255,6 +275,15 @@ def init_builder(self, source): f" Copying {self.REPO_NAME}-git-main to {repo_path} on builder" ) self.bld_ct.bash(f"rm -rf {repo_path} && cp -a {tmp_dest} {repo_path}") + if source.ref == "latest": + # prep_builder already fetched --tags, update source to replace + # "latest" with the tag + source.ref = latest_release_tag( + self.bld_ct.bash(f"cd {repo_path} && git tag -l --sort=-v:refname") + ) + if not source.ref: + raise SetupError(f"No release tag found for {self.REPO_NAME}") + self.out.print(f" Resolved @latest to {source.ref}") if source.ref != "main": self.out.print(f" Checking out {source.ref!r} ...") self.bld_ct.bash(f""" diff --git a/tests/test_cli.py b/tests/test_cli.py index c954ee7..5c96729 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -4,7 +4,12 @@ import pytest -from cmlxc.driver_base import SourceSpec, parse_source, validate_relay_name +from cmlxc.driver_base import ( + SourceSpec, + latest_release_tag, + parse_source, + validate_relay_name, +) from cmlxc.driver_cmdeploy import get_ini_overrides from cmlxc.driver_madmail import release_asset_url @@ -17,6 +22,7 @@ ("@main", SourceSpec("remote", url=URL, ref="main")), ("@fix-dovecot", SourceSpec("remote", url=URL, ref="fix-dovecot")), ("@v2.1", SourceSpec("remote", url=URL, ref="v2.1")), + ("@latest", SourceSpec("remote", url=URL, ref="latest")), ("/home/me/relay", SourceSpec("local", path=Path("/home/me/relay"))), ("./relay", SourceSpec("local", path=Path("./relay"))), ("../relay", SourceSpec("local", path=Path("../relay"))), @@ -83,3 +89,21 @@ def test_release_asset_url(tag, arch, expected_asset): assert url is None else: assert url.endswith(f"/v2.23.0/{expected_asset}") + + +@pytest.mark.parametrize( + "tag_output, expected", + [ + # `git tag -l --sort=-v:refname` output, newest first + ("v2.23.0\nv2.22.1\nv2.2.2\n", "v2.23.0"), + # pre-releases and non-semver tags are skipped + ("v2.24.0-rc1\nv2.23.0\n", "v2.23.0"), + ("test\nlatest\nv1.0.0\n", "v1.0.0"), + # tags without the v prefix still count + ("2.23.0\n", "2.23.0"), + ("", None), + (None, None), + ], +) +def test_latest_release_tag(tag_output, expected): + assert latest_release_tag(tag_output) == expected