This commit is contained in:
Rick 2026-10-31 11:54:39 +08:00 • committed by GitHub
commit 988754e9e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 190 additions and 0 deletions

View file

@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""Fail when a release tag and the pyproject version at that tag disagree.
Usage
-----
python3 check_release_tag_version.py <tag> [path/to/pyproject.toml]
The tag may carry a leading `v` and a PEP 440 suffix (`rc`, `dev`, `post`, or a
vendor suffix such as `-stable`). Only the X.Y.Z release segment is compared, so
`v1.107.0-rc.2` matches a pyproject version of `1.107.0`.
Exit code 1 with a `::error::` line on stderr when the tag and the version do
not name the same release.
"""
from __future__ import annotations
import pathlib
import re
import sys
from typing import Final
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
_RELEASE: Final = re.compile(r"^(?P<release>[0-9]+\.[0-9]+\.[0-9]+)(?P<suffix>.*)$")
_ALLOWED_SUFFIX: Final = re.compile(r"^(?:$|[.\-+].*|(?:a|b|rc|dev|post)[0-9].*)$")
def _release_segment(value: str, *, leading_v: bool) -> str | None:
candidate: Final = value[1:] if leading_v and value.startswith("v") else value
match: Final = _RELEASE.fullmatch(candidate)
if match is None or _ALLOWED_SUFFIX.fullmatch(match.group("suffix")) is None:
return None
return match.group("release")
def read_version(pyproject: pathlib.Path) -> str:
with pyproject.open("rb") as f:
return tomllib.load(f)["project"]["version"]
def main(argv: list[str]) -> int:
if len(argv) < 2:
print( # noqa: T201 # the ::error:: line to stderr is the workflow's failure signal
"::error::usage: check_release_tag_version.py <tag> [path/to/pyproject.toml]",
file=sys.stderr,
)
return 1
tag: Final = argv[1]
pyproject: Final = pathlib.Path(argv[2]) if len(argv) > 2 else pathlib.Path("pyproject.toml")
tag_release: Final = _release_segment(tag, leading_v=True)
if tag_release is None:
print( # noqa: T201 # the ::error:: line to stderr is the workflow's failure signal
f"::error::{tag} is not a release tag with an X.Y.Z version", file=sys.stderr
)
return 1
version: Final = read_version(pyproject)
version_release: Final = _release_segment(version, leading_v=False)
if version_release is None:
print( # noqa: T201 # the ::error:: line to stderr is the workflow's failure signal
f"::error::pyproject.toml version {version} is not an X.Y.Z version", file=sys.stderr
)
return 1
if tag_release != version_release:
print( # noqa: T201 # the ::error:: line to stderr is the workflow's failure signal
f"::error::{tag} points at a commit whose pyproject.toml version is {version}. "
f"Tag the commit for {tag_release}, or correct the version before tagging.",
file=sys.stderr,
)
return 1
print(f"{tag} matches pyproject.toml version {version}") # noqa: T201 # the workflow shows this line
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))

View file

@ -0,0 +1,24 @@
name: Validate Release Tag
on:
push:
tags:
- "v*"
permissions:
contents: read
jobs:
validate:
name: Check the tag against pyproject.toml
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: Check the tag against pyproject.toml
env:
RELEASE_TAG: ${{ github.ref_name }}
run: python3 .github/scripts/check_release_tag_version.py "$RELEASE_TAG"

View file

@ -0,0 +1,83 @@
"""Tests for .github/scripts/check_release_tag_version.py."""
import importlib.util
import sys
from pathlib import Path
from typing import Final
import pytest
_REPO_ROOT: Final = Path(__file__).resolve().parents[2]
_MODULE_PATH: Final = _REPO_ROOT / ".github" / "scripts" / "check_release_tag_version.py"
_spec: Final = importlib.util.spec_from_file_location("check_release_tag_version", _MODULE_PATH)
check_release_tag_version: Final = importlib.util.module_from_spec(_spec)
sys.modules[_spec.name] = check_release_tag_version
_spec.loader.exec_module(check_release_tag_version)
def _run(tmp_path: Path, tag: str, version: str, capsys: pytest.CaptureFixture[str]) -> tuple[int, str, str]:
pyproject: Final = tmp_path / "pyproject.toml"
pyproject.write_text(f'[project]\nname = "litellm"\nversion = "{version}"\n', encoding="utf-8")
code: Final = check_release_tag_version.main(["check_release_tag_version.py", tag, str(pyproject)])
captured: Final = capsys.readouterr()
return code, captured.out, captured.err
@pytest.mark.parametrize(
("tag", "version"),
[
("v1.107.0", "1.107.0"),
("1.107.0", "1.107.0"),
("v1.107.0-rc.2", "1.107.0"),
("1.84.0rc1", "1.84.0"),
("v1.84.0.dev42", "1.84.0.dev42"),
("v1.107.0.post1", "1.107.0.post1"),
("v1.83.10-stable", "1.83.10"),
("v1.83.3-stable.opus-4.7", "1.83.3"),
],
)
def test_a_tag_that_matches_the_pyproject_version_passes(
tmp_path: Path, capsys: pytest.CaptureFixture[str], tag: str, version: str
) -> None:
code, out, err = _run(tmp_path, tag, version, capsys)
assert (code, err) == (0, "")
assert out == f"{tag} matches pyproject.toml version {version}\n"
@pytest.mark.parametrize(("tag", "version"), [("v1.107.0", "1.108.0"), ("v1.106.2", "1.107.0")])
def test_a_tag_that_does_not_match_the_pyproject_version_fails(
tmp_path: Path, capsys: pytest.CaptureFixture[str], tag: str, version: str
) -> None:
code, out, err = _run(tmp_path, tag, version, capsys)
assert code == 1
assert out == ""
assert err.startswith(f"::error::{tag} points at a commit whose pyproject.toml version is {version}.")
@pytest.mark.parametrize("tag", ["release-1.107.0", "v1.107", "v1.107.0foo"])
def test_a_tag_that_is_not_an_x_y_z_release_fails(tmp_path: Path, capsys: pytest.CaptureFixture[str], tag: str) -> None:
code, out, err = _run(tmp_path, tag, "1.107.0", capsys)
assert code == 1
assert out == ""
assert err == f"::error::{tag} is not a release tag with an X.Y.Z version\n"
def test_a_malformed_pyproject_version_fails(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
code, out, err = _run(tmp_path, "v1.107.0", "1.107", capsys)
assert code == 1
assert out == ""
assert err == "::error::pyproject.toml version 1.107 is not an X.Y.Z version\n"
def test_a_missing_tag_argument_fails(capsys: pytest.CaptureFixture[str]) -> None:
assert check_release_tag_version.main(["check_release_tag_version.py"]) == 1
assert capsys.readouterr().err == ("::error::usage: check_release_tag_version.py <tag> [path/to/pyproject.toml]\n")
def test_the_repo_pyproject_is_read_when_no_path_is_given(
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
monkeypatch.chdir(_REPO_ROOT)
version: Final = check_release_tag_version.read_version(_REPO_ROOT / "pyproject.toml")
assert check_release_tag_version.main(["check_release_tag_version.py", f"v{version}"]) == 0
assert capsys.readouterr().out == f"v{version} matches pyproject.toml version {version}\n"