diff --git a/README.md b/README.md index 27590f3..75735b4 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/release-check/action.yml b/release-check/action.yml index c4cd3a3..e592ef3 100644 --- a/release-check/action.yml +++ b/release-check/action.yml @@ -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 diff --git a/release-check/check_release.py b/release-check/check_release.py index a16b872..7b864bc 100644 --- a/release-check/check_release.py +++ b/release-check/check_release.py @@ -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*)$") @@ -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: @@ -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)) diff --git a/tests/test_release_changelog.py b/tests/test_release_changelog.py index 9f69158..a17d666 100644 --- a/tests/test_release_changelog.py +++ b/tests/test_release_changelog.py @@ -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( @@ -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",