Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,10 @@ manual `Unreleased` changelog section into the release, and adds a fresh
`Unreleased` section. It never tags or publishes.

`release-check` validates that version-changing pull requests use the expected
release branch, contain matching changelog headings, and modify only files
listed by the version configuration. After merge, it exposes the version and
tag to the caller-owned trusted-publishing workflow. This flow reads repository
release branch, contain matching changelog headings, and modify only files the
version configuration owns, named either by `filename` or by `glob`. After
merge, it exposes the version and tag to the caller-owned trusted-publishing
workflow. This flow reads repository
state rather than commit messages, so merge, squash, and rebase strategies are
all supported.

Expand Down
2 changes: 1 addition & 1 deletion release-check/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ runs:

- uses: actions/setup-python@v7.0.0
with:
python-version: "3.12"
python-version: "3.13"

- name: Detect comparison revisions
id: revisions
Expand Down
24 changes: 17 additions & 7 deletions release-check/check_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import re
import subprocess
import tomllib
from pathlib import Path
from pathlib import Path, PurePath


SEMVER = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$")
Expand All @@ -29,11 +29,17 @@ def version(config: dict) -> str:
raise SystemExit("Missing tool.bumpversion.current_version") from error


def configured_files(config: dict, config_path: str) -> set[str]:
files = {config_path}
def configured_files(config: dict, config_path: str) -> tuple[set[str], list[str]]:
names = {config_path}
globs = []
for item in config["tool"]["bumpversion"].get("files", []):
files.add(str(item["filename"]))
return files
if "filename" in item:
names.add(str(item["filename"]))
elif "glob" in item:
globs.append(str(item["glob"]))
else:
raise SystemExit("Every tool.bumpversion.files entry needs a filename or a glob")
return names, globs


def write_output(path: Path, name: str, value: str) -> None:
Expand Down Expand Up @@ -89,9 +95,13 @@ def main() -> None:
if len(release_heading.findall(changelog)) != 1:
raise SystemExit(f"The changelog must contain exactly one release heading for {new_version}")

allowed = configured_files(head_config, args.config)
names, globs = configured_files(head_config, args.config)
changed = set(git("diff", "--name-only", args.base, args.head).splitlines())
unexpected = sorted(changed - allowed)
unexpected = sorted(
path
for path in changed
if path not in names and not any(PurePath(path).full_match(glob) for glob in globs)
)
if unexpected:
raise SystemExit("Release changes files outside the version configuration: " + ", ".join(unexpected))

Expand Down
55 changes: 55 additions & 0 deletions tests/test_release_changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,58 @@ def test_release_check_accepts_both_keep_a_changelog_heading_styles(self) -> Non
["is-release=true", "version=1.2.3", "tag=v1.2.3"],
)

def test_release_check_accepts_files_matched_by_a_glob(self) -> None:
with tempfile.TemporaryDirectory() as directory:
repository = Path(directory)
self._git(repository, "init")
self._git(repository, "config", "user.name", "Tests")
self._git(repository, "config", "user.email", "tests@example.com")
self._write_project(repository, "1.2.2")
(repository / "CHANGELOG.md").write_text("# Changelog\n\n## Unreleased\n", encoding="utf-8")
component = repository / "src" / "components" / "Widget" / "code.py"
component.parent.mkdir(parents=True)
component.write_text("# r: package>=1.2.2\n", encoding="utf-8")
self._git(repository, "add", ".")
self._git(repository, "commit", "-m", "Base")
base = self._git(repository, "rev-parse", "HEAD").stdout.strip()

self._write_project(repository, "1.2.3")
(repository / "CHANGELOG.md").write_text(
"# Changelog\n\n## Unreleased\n\n## [1.2.3] 2026-08-28\n",
encoding="utf-8",
)
component.write_text("# r: package>=1.2.3\n", encoding="utf-8")
self._git(repository, "add", ".")
self._git(repository, "commit", "-m", "Release")
head = self._git(repository, "rev-parse", "HEAD").stdout.strip()
output = repository / "output"

subprocess.run(
(
sys.executable,
ROOT / "release-check" / "check_release.py",
"--base",
base,
"--head",
head,
"--config",
"pyproject.toml",
"--changelog",
"CHANGELOG.md",
"--pull-request-branch",
"release/v1.2.3",
"--output",
output,
),
cwd=repository,
check=True,
)

self.assertEqual(
output.read_text(encoding="utf-8").splitlines(),
["is-release=true", "version=1.2.3", "tag=v1.2.3"],
)

@staticmethod
def _git(repository: Path, *args: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
Expand All @@ -98,6 +150,9 @@ def _write_project(repository: Path, version: str) -> None:
"[[tool.bumpversion.files]]",
'filename = "CHANGELOG.md"',
"",
"[[tool.bumpversion.files]]",
'glob = "src/components/**/code.py"',
"",
)
),
encoding="utf-8",
Expand Down