feat: Add version tag bump script
This commit is contained in:
1 parent
ba23a185be
commit
06fcd7a10f
2 files changed
+137
No files matched your search
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Create a tag at HEAD by bumping the highest local X.Y.Z or vX.Y.Z tag. "
|
||||
"Preserves the latest tag's prefix; starts from v0.0.0 when no version tags exist.",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
for part in ("major", "minor", "patch"):
|
||||
group.add_argument(f"--{part}", dest="part", action="store_const", const=part,
|
||||
help=f"Bump the {part} version and reset lower components")
|
||||
parser.add_argument("--cwd", type=Path, default=Path.cwd(), help="Git repository directory")
|
||||
parser.add_argument("--push", action="store_true", help="Push only the new tag to origin")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def git(cwd: Path, arguments: list[str]) -> str:
|
||||
result = subprocess.run(
|
||||
args=["git", *arguments], cwd=cwd, check=True, capture_output=True, text=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def next_tag(tags: list[str], part: str) -> str:
|
||||
versions = []
|
||||
for tag in tags:
|
||||
match = re.fullmatch(r"(v?)(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)", tag)
|
||||
if match:
|
||||
versions.append((tuple(int(value) for value in match.groups()[1:]), match[1]))
|
||||
version, prefix = max(versions, default=((0, 0, 0), "v"))
|
||||
components = list(version)
|
||||
index = ("major", "minor", "patch").index(part)
|
||||
components[index] += 1
|
||||
components[index + 1:] = [0] * (2 - index)
|
||||
return f"{prefix}{'.'.join(str(value) for value in components)}"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s: %(message)s")
|
||||
arguments = parse_args()
|
||||
try:
|
||||
tags = git(cwd=arguments.cwd, arguments=["tag", "--list"]).splitlines()
|
||||
tag = next_tag(tags=tags, part=arguments.part)
|
||||
git(cwd=arguments.cwd, arguments=["tag", "--", tag, "HEAD"])
|
||||
logging.info(f"Created {tag} at HEAD")
|
||||
if arguments.push:
|
||||
git(cwd=arguments.cwd, arguments=["push", "origin", f"refs/tags/{tag}:refs/tags/{tag}"])
|
||||
logging.info(f"Pushed {tag} to origin")
|
||||
except subprocess.CalledProcessError as error:
|
||||
logging.error(f"Git failed: {(error.stderr or str(error)).strip()}")
|
||||
return 1
|
||||
except OSError as error:
|
||||
logging.error(f"{error}")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Executable
+69
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# dependencies = ["pytest"]
|
||||
# ///
|
||||
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
import bump_version as script
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tags, part, expected", [
|
||||
([], "patch", "v0.0.1"),
|
||||
([], "minor", "v0.1.0"),
|
||||
([], "major", "v1.0.0"),
|
||||
(["v1.9.9", "v1.10.2", "v1.2.3"], "patch", "v1.10.3"),
|
||||
(["v1.9.9", "v1.10.2"], "minor", "v1.11.0"),
|
||||
(["v1.9.9", "v1.10.2"], "major", "v2.0.0"),
|
||||
(["1.2.3", "v1.0.0"], "patch", "1.2.4"),
|
||||
(["release", "v9.0.0-rc.1", "v01.0.0", "v1.2.3+build", "v1.2.3"], "patch", "v1.2.4"),
|
||||
])
|
||||
def test_next_tag(tags: list[str], part: str, expected: str) -> None:
|
||||
assert script.next_tag(tags=tags, part=part) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("push", [False, True])
|
||||
def test_create_and_push_tag(tmp_path: Path, push: bool) -> None:
|
||||
repository = tmp_path / "repository"
|
||||
remote = tmp_path / "remote.git"
|
||||
subprocess.run(args=["git", "init", "--initial-branch=master", str(repository)], check=True, capture_output=True)
|
||||
subprocess.run(args=["git", "init", "--bare", str(remote)], check=True, capture_output=True)
|
||||
script.git(cwd=repository, arguments=["-c", "user.name=Test", "-c", "user.email=test@example.com",
|
||||
"-c", "commit.gpgsign=false", "commit", "--allow-empty", "--message=Initial"])
|
||||
script.git(cwd=repository, arguments=["remote", "add", "origin", str(remote)])
|
||||
script.git(cwd=repository, arguments=["tag", "v1.9.9"])
|
||||
script.git(cwd=repository, arguments=["tag", "v1.10.2"])
|
||||
command = [sys.executable, str(Path(script.__file__).resolve()), "--minor", "--cwd", str(repository)]
|
||||
if push:
|
||||
command.append("--push")
|
||||
subprocess.run(args=command, check=True, capture_output=True, text=True)
|
||||
assert script.git(cwd=repository, arguments=["rev-parse", "v1.11.0"]) == script.git(
|
||||
cwd=repository, arguments=["rev-parse", "HEAD"],
|
||||
)
|
||||
assert script.git(cwd=remote, arguments=["tag", "--list"]) == ("v1.11.0" if push else "")
|
||||
assert script.git(cwd=remote, arguments=["for-each-ref", "refs/heads"]) == ""
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arguments", [[], ["--major", "--patch"]])
|
||||
def test_requires_exactly_one_bump(arguments: list[str]) -> None:
|
||||
result = subprocess.run(
|
||||
args=[sys.executable, str(Path(script.__file__).resolve()), *arguments], capture_output=True,
|
||||
)
|
||||
assert result.returncode == 2
|
||||
|
||||
|
||||
def test_missing_repository(tmp_path: Path) -> None:
|
||||
result = subprocess.run(
|
||||
args=[sys.executable, str(Path(script.__file__).resolve()), "--patch", "--cwd", str(tmp_path)],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
assert result.returncode == 1
|
||||
assert "Git failed:" in result.stderr
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(args=[str(Path(__file__).resolve()), *sys.argv[1:]]))
|
||||
Reference in new issue
Block a user