feat(release): start from the highest version across all sources
CI / security-codeql (push) Blocked by required conditions
CI / test-unit (push) Blocked by required conditions
CI / test-integration (push) Blocked by required conditions
CI / test-env-virtual (push) Blocked by required conditions
CI / test-env-nix (push) Blocked by required conditions
CI / test-e2e (push) Blocked by required conditions
CI / test-virgin-user (push) Blocked by required conditions
CI / test-virgin-root (push) Blocked by required conditions
CI / lint-shell (push) Blocked by required conditions
CI / lint-python (push) Blocked by required conditions
CI / lint-docker (push) Blocked by required conditions
Docker Linter / Lint Dockerfile (push) Blocked by required conditions
CodeQL Advanced / Check security (none, actions) (push) Blocked by required conditions
CodeQL Advanced / Check security (none, python) (push) Blocked by required conditions
Test Units / test-unit (push) Blocked by required conditions
Test Code Integration / test-integration (push) Blocked by required conditions
Test OS Containers / test-env-virtual (arch) (push) Blocked by required conditions
Test OS Containers / test-env-virtual (centos) (push) Blocked by required conditions
Test OS Containers / test-env-virtual (debian) (push) Blocked by required conditions
Test OS Containers / test-env-virtual (fedora) (push) Blocked by required conditions
Test OS Containers / test-env-virtual (ubuntu) (push) Blocked by required conditions
Test Virgin Nix (flake only) / test-env-nix (arch) (push) Blocked by required conditions
Test Virgin Nix (flake only) / test-env-nix (centos) (push) Blocked by required conditions
Test Virgin Nix (flake only) / test-env-nix (debian) (push) Blocked by required conditions
Test Virgin Nix (flake only) / test-env-nix (fedora) (push) Blocked by required conditions
Test Virgin Nix (flake only) / test-env-nix (ubuntu) (push) Blocked by required conditions
Test End-To-End / test-e2e (arch) (push) Blocked by required conditions
Test End-To-End / test-e2e (centos) (push) Blocked by required conditions
Test End-To-End / test-e2e (debian) (push) Blocked by required conditions
Test End-To-End / test-e2e (fedora) (push) Blocked by required conditions
Test End-To-End / test-e2e (ubuntu) (push) Blocked by required conditions
Test Virgin User / test-virgin-user (arch) (push) Blocked by required conditions
Test Virgin User / test-virgin-user (centos) (push) Blocked by required conditions
Test Virgin User / test-virgin-user (debian) (push) Blocked by required conditions
Test Virgin User / test-virgin-user (fedora) (push) Blocked by required conditions
Test Virgin User / test-virgin-user (ubuntu) (push) Blocked by required conditions
Test Virgin Root / test-virgin-root (arch) (push) Blocked by required conditions
Test Virgin Root / test-virgin-root (centos) (push) Blocked by required conditions
Test Virgin Root / test-virgin-root (debian) (push) Blocked by required conditions
Test Virgin Root / test-virgin-root (fedora) (push) Blocked by required conditions
Test Virgin Root / test-virgin-root (ubuntu) (push) Blocked by required conditions
ShellCheck / lint-shell (push) Blocked by required conditions
Ruff (Python code sniffer) / lint-python (push) Blocked by required conditions
CI / security-codeql (push) Blocked by required conditions
CI / test-unit (push) Blocked by required conditions
CI / test-integration (push) Blocked by required conditions
CI / test-env-virtual (push) Blocked by required conditions
CI / test-env-nix (push) Blocked by required conditions
CI / test-e2e (push) Blocked by required conditions
CI / test-virgin-user (push) Blocked by required conditions
CI / test-virgin-root (push) Blocked by required conditions
CI / lint-shell (push) Blocked by required conditions
CI / lint-python (push) Blocked by required conditions
CI / lint-docker (push) Blocked by required conditions
Docker Linter / Lint Dockerfile (push) Blocked by required conditions
CodeQL Advanced / Check security (none, actions) (push) Blocked by required conditions
CodeQL Advanced / Check security (none, python) (push) Blocked by required conditions
Test Units / test-unit (push) Blocked by required conditions
Test Code Integration / test-integration (push) Blocked by required conditions
Test OS Containers / test-env-virtual (arch) (push) Blocked by required conditions
Test OS Containers / test-env-virtual (centos) (push) Blocked by required conditions
Test OS Containers / test-env-virtual (debian) (push) Blocked by required conditions
Test OS Containers / test-env-virtual (fedora) (push) Blocked by required conditions
Test OS Containers / test-env-virtual (ubuntu) (push) Blocked by required conditions
Test Virgin Nix (flake only) / test-env-nix (arch) (push) Blocked by required conditions
Test Virgin Nix (flake only) / test-env-nix (centos) (push) Blocked by required conditions
Test Virgin Nix (flake only) / test-env-nix (debian) (push) Blocked by required conditions
Test Virgin Nix (flake only) / test-env-nix (fedora) (push) Blocked by required conditions
Test Virgin Nix (flake only) / test-env-nix (ubuntu) (push) Blocked by required conditions
Test End-To-End / test-e2e (arch) (push) Blocked by required conditions
Test End-To-End / test-e2e (centos) (push) Blocked by required conditions
Test End-To-End / test-e2e (debian) (push) Blocked by required conditions
Test End-To-End / test-e2e (fedora) (push) Blocked by required conditions
Test End-To-End / test-e2e (ubuntu) (push) Blocked by required conditions
Test Virgin User / test-virgin-user (arch) (push) Blocked by required conditions
Test Virgin User / test-virgin-user (centos) (push) Blocked by required conditions
Test Virgin User / test-virgin-user (debian) (push) Blocked by required conditions
Test Virgin User / test-virgin-user (fedora) (push) Blocked by required conditions
Test Virgin User / test-virgin-user (ubuntu) (push) Blocked by required conditions
Test Virgin Root / test-virgin-root (arch) (push) Blocked by required conditions
Test Virgin Root / test-virgin-root (centos) (push) Blocked by required conditions
Test Virgin Root / test-virgin-root (debian) (push) Blocked by required conditions
Test Virgin Root / test-virgin-root (fedora) (push) Blocked by required conditions
Test Virgin Root / test-virgin-root (ubuntu) (push) Blocked by required conditions
ShellCheck / lint-shell (push) Blocked by required conditions
Ruff (Python code sniffer) / lint-python (push) Blocked by required conditions
The release base used to be the latest v* tag only. It is now the highest version found in that tag and in every file the release writes: pyproject.toml, CHANGELOG.md, flake.nix, PKGBUILD, the RPM spec, debian/changelog and Cargo.toml. A lagging file can no longer lower the base and a file ahead of the tag raises it; when the sources disagree the release says so and names the version it starts from. Repos whose sources agree release exactly as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
d823a42edc
commit
810d27badc
4 files changed
+120
-17
No files matched your search
@@ -4,6 +4,8 @@ Version discovery and bumping helpers for the release workflow.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
from pkgmgr.core.git.queries import get_tags
|
from pkgmgr.core.git.queries import get_tags
|
||||||
from pkgmgr.core.version.semver import (
|
from pkgmgr.core.version.semver import (
|
||||||
SemVer,
|
SemVer,
|
||||||
@@ -12,26 +14,60 @@ from pkgmgr.core.version.semver import (
|
|||||||
bump_patch,
|
bump_patch,
|
||||||
find_latest_version,
|
find_latest_version,
|
||||||
)
|
)
|
||||||
|
from pkgmgr.core.version.source import (
|
||||||
|
read_cargo_version,
|
||||||
|
read_changelog_version,
|
||||||
|
read_debian_changelog_version,
|
||||||
|
read_flake_version,
|
||||||
|
read_pkgbuild_version,
|
||||||
|
read_pyproject_version,
|
||||||
|
read_spec_version,
|
||||||
|
)
|
||||||
|
|
||||||
|
FILE_SOURCES = {
|
||||||
|
"pyproject.toml": read_pyproject_version,
|
||||||
|
"CHANGELOG.md": read_changelog_version,
|
||||||
|
"flake.nix": read_flake_version,
|
||||||
|
"PKGBUILD": read_pkgbuild_version,
|
||||||
|
"RPM spec": read_spec_version,
|
||||||
|
"debian/changelog": read_debian_changelog_version,
|
||||||
|
"Cargo.toml": read_cargo_version,
|
||||||
|
}
|
||||||
|
_CORE_VERSION = re.compile(r"^v?(\d+\.\d+\.\d+)")
|
||||||
|
|
||||||
|
|
||||||
def determine_current_version() -> SemVer:
|
def _file_versions(repo_dir: str) -> dict[str, SemVer]:
|
||||||
|
versions: dict[str, SemVer] = {}
|
||||||
|
for source, read in FILE_SOURCES.items():
|
||||||
|
match = _CORE_VERSION.match(read(repo_dir) or "")
|
||||||
|
if match:
|
||||||
|
versions[source] = SemVer.parse(match.group(1))
|
||||||
|
return versions
|
||||||
|
|
||||||
|
|
||||||
|
def determine_current_version(repo_dir: str) -> SemVer:
|
||||||
"""
|
"""
|
||||||
Determine the current semantic version from Git tags.
|
Determine the highest version across the latest SemVer tag and every
|
||||||
|
version file the release writes.
|
||||||
|
|
||||||
Behaviour:
|
Args:
|
||||||
- If there are no tags or no SemVer-compatible tags, return 0.0.0.
|
repo_dir: repository root holding the version files.
|
||||||
- Otherwise, use the latest SemVer tag as current version.
|
|
||||||
|
Returns:
|
||||||
|
The highest version found, or 0.0.0 when no source has one.
|
||||||
"""
|
"""
|
||||||
tags = get_tags()
|
versions = _file_versions(repo_dir)
|
||||||
if not tags:
|
latest = find_latest_version(get_tags() or [])
|
||||||
|
if latest is not None:
|
||||||
|
versions["tag"] = latest[1]
|
||||||
|
if not versions:
|
||||||
return SemVer(0, 0, 0)
|
return SemVer(0, 0, 0)
|
||||||
|
|
||||||
latest = find_latest_version(tags)
|
highest = max(versions.values())
|
||||||
if latest is None:
|
if len(set(versions.values())) > 1:
|
||||||
return SemVer(0, 0, 0)
|
found = ", ".join(f"{source} {ver}" for source, ver in versions.items())
|
||||||
|
print(f"[INFO] Version sources disagree ({found}); starting from {highest}.")
|
||||||
_tag, ver = latest
|
return highest
|
||||||
return ver
|
|
||||||
|
|
||||||
|
|
||||||
def bump_semver(current: SemVer, release_type: str) -> SemVer:
|
def bump_semver(current: SemVer, release_type: str) -> SemVer:
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ def _release_impl(
|
|||||||
# Pull BEFORE making any modifications
|
# Pull BEFORE making any modifications
|
||||||
ensure_clean_and_synced(preview=preview)
|
ensure_clean_and_synced(preview=preview)
|
||||||
|
|
||||||
current_ver = determine_current_version()
|
repo_root = os.path.dirname(os.path.abspath(pyproject_path))
|
||||||
|
current_ver = determine_current_version(repo_root)
|
||||||
new_ver = bump_semver(current_ver, release_type)
|
new_ver = bump_semver(current_ver, release_type)
|
||||||
new_ver_str = str(new_ver)
|
new_ver_str = str(new_ver)
|
||||||
new_tag = new_ver.to_tag(with_prefix=True)
|
new_tag = new_ver.to_tag(with_prefix=True)
|
||||||
@@ -59,7 +60,6 @@ def _release_impl(
|
|||||||
print(f"Current version: {current_ver}")
|
print(f"Current version: {current_ver}")
|
||||||
print(f"New version: {new_ver_str} ({release_type})")
|
print(f"New version: {new_ver_str} ({release_type})")
|
||||||
|
|
||||||
repo_root = os.path.dirname(os.path.abspath(pyproject_path))
|
|
||||||
paths = resolve_repo_paths(repo_root)
|
paths = resolve_repo_paths(repo_root)
|
||||||
|
|
||||||
# --- Update versioned files ------------------------------------------------
|
# --- Update versioned files ------------------------------------------------
|
||||||
|
|||||||
@@ -232,3 +232,40 @@ def read_ansible_galaxy_version(repo_dir: str) -> str | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def read_cargo_version(repo_dir: str) -> str | None:
|
||||||
|
"""
|
||||||
|
Read [package].version from Cargo.toml in repo_dir, if present.
|
||||||
|
"""
|
||||||
|
path = os.path.join(repo_dir, "Cargo.toml")
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
import tomllib
|
||||||
|
except ImportError:
|
||||||
|
import tomli as tomllib # type: ignore
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
version = (tomllib.load(f).get("package") or {}).get("version")
|
||||||
|
except (OSError, UnicodeDecodeError, tomllib.TOMLDecodeError):
|
||||||
|
return None
|
||||||
|
return version.strip() if isinstance(version, str) and version.strip() else None
|
||||||
|
|
||||||
|
|
||||||
|
def read_changelog_version(repo_dir: str) -> str | None:
|
||||||
|
"""
|
||||||
|
Read the newest release from CHANGELOG.md ("## [X.Y.Z] - date").
|
||||||
|
"""
|
||||||
|
path = os.path.join(repo_dir, "CHANGELOG.md")
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(path, encoding="utf-8") as f:
|
||||||
|
match = re.search(r"(?m)^##\s+\[(v?\d+\.\d+\.\d+)\]", f.read())
|
||||||
|
except (OSError, UnicodeDecodeError):
|
||||||
|
return None
|
||||||
|
return match.group(1).strip() if match else None
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
@@ -11,12 +13,23 @@ from pkgmgr.core.version.semver import SemVer
|
|||||||
|
|
||||||
|
|
||||||
class TestDetermineCurrentVersion(unittest.TestCase):
|
class TestDetermineCurrentVersion(unittest.TestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self._tmp = tempfile.TemporaryDirectory()
|
||||||
|
self.repo = self._tmp.name
|
||||||
|
|
||||||
|
def tearDown(self) -> None:
|
||||||
|
self._tmp.cleanup()
|
||||||
|
|
||||||
|
def _write(self, name: str, content: str) -> None:
|
||||||
|
with open(os.path.join(self.repo, name), "w", encoding="utf-8") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
@patch("pkgmgr.actions.release.versioning.get_tags", return_value=[])
|
@patch("pkgmgr.actions.release.versioning.get_tags", return_value=[])
|
||||||
def test_determine_current_version_no_tags_returns_zero(
|
def test_determine_current_version_no_tags_returns_zero(
|
||||||
self,
|
self,
|
||||||
mock_get_tags,
|
mock_get_tags,
|
||||||
) -> None:
|
) -> None:
|
||||||
ver = determine_current_version()
|
ver = determine_current_version(self.repo)
|
||||||
self.assertIsInstance(ver, SemVer)
|
self.assertIsInstance(ver, SemVer)
|
||||||
self.assertEqual((ver.major, ver.minor, ver.patch), (0, 0, 0))
|
self.assertEqual((ver.major, ver.minor, ver.patch), (0, 0, 0))
|
||||||
mock_get_tags.assert_called_once()
|
mock_get_tags.assert_called_once()
|
||||||
@@ -31,12 +44,29 @@ class TestDetermineCurrentVersion(unittest.TestCase):
|
|||||||
mock_get_tags.return_value = ["v0.1.0", "v1.2.3"]
|
mock_get_tags.return_value = ["v0.1.0", "v1.2.3"]
|
||||||
mock_find_latest_version.return_value = ("v1.2.3", SemVer(1, 2, 3))
|
mock_find_latest_version.return_value = ("v1.2.3", SemVer(1, 2, 3))
|
||||||
|
|
||||||
ver = determine_current_version()
|
ver = determine_current_version(self.repo)
|
||||||
|
|
||||||
self.assertEqual((ver.major, ver.minor, ver.patch), (1, 2, 3))
|
self.assertEqual((ver.major, ver.minor, ver.patch), (1, 2, 3))
|
||||||
mock_get_tags.assert_called_once()
|
mock_get_tags.assert_called_once()
|
||||||
mock_find_latest_version.assert_called_once_with(["v0.1.0", "v1.2.3"])
|
mock_find_latest_version.assert_called_once_with(["v0.1.0", "v1.2.3"])
|
||||||
|
|
||||||
|
@patch("pkgmgr.actions.release.versioning.get_tags", return_value=["v0.3.0"])
|
||||||
|
def test_lagging_cargo_version_does_not_lower_the_base(self, _tags) -> None:
|
||||||
|
self._write("Cargo.toml", '[package]\nname = "x"\nversion = "0.1.0"\n')
|
||||||
|
self._write("CHANGELOG.md", "# Changelog\n\n## [0.3.0] - 2026-10-30\n")
|
||||||
|
|
||||||
|
self.assertEqual(determine_current_version(self.repo), SemVer(0, 3, 0))
|
||||||
|
|
||||||
|
@patch("pkgmgr.actions.release.versioning.get_tags", return_value=["v1.0.0"])
|
||||||
|
def test_file_ahead_of_the_tag_raises_the_base(self, _tags) -> None:
|
||||||
|
self._write("pyproject.toml", '[project]\nname = "x"\nversion = "1.4.0"\n')
|
||||||
|
self._write(
|
||||||
|
"CHANGELOG.md",
|
||||||
|
"# Changelog\n\n## [Unreleased]\n\n## [1.0.0] - 2026-10-01\n",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(determine_current_version(self.repo), SemVer(1, 4, 0))
|
||||||
|
|
||||||
|
|
||||||
class TestBumpSemVer(unittest.TestCase):
|
class TestBumpSemVer(unittest.TestCase):
|
||||||
def test_bump_semver_major(self) -> None:
|
def test_bump_semver_major(self) -> None:
|
||||||
|
|||||||
Reference in new issue
Block a user