From 0e3964a239c7c040dfbb965a6cb7a871f5072cbc Mon Sep 17 00:00:00 2001 From: Sebastiano Date: Wed, 5 Aug 2026 15:20:35 +0200 Subject: [PATCH 1/3] feat(release): run pre-commit hooks before committing changes --- scripts/release.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/release.py b/scripts/release.py index 685bd0c..f638215 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -294,7 +294,14 @@ def main() -> int: sync_lockfile() generate_changelog(tag, prev) - run(["git", "add", "pyproject.toml", "uv.lock", "CHANGELOG.md"]) + # Run pre-commit hooks on the modified files before committing, to ensure + # that the commit passes all checks. + updated_files = ["pyproject.toml", "uv.lock", "CHANGELOG.md"] + run(["pre-commit", "run", "--files", *updated_files]) + run(["pre-commit", "run", "--files", *updated_files]) + + # Commit the changes and create an annotated tag for the release. + run(["git", "add", *updated_files]) run(["git", "commit", "-m", f"chore(release): {tag}"]) if not args.no_tag: From de14a9291e93129dd18853aa3887cf2230b82133 Mon Sep 17 00:00:00 2001 From: Sebastiano Date: Wed, 5 Aug 2026 15:21:30 +0200 Subject: [PATCH 2/3] feat(release): update pre-commit hook command to use 'uv' prefix --- scripts/release.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index f638215..0739b33 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -297,8 +297,8 @@ def main() -> int: # Run pre-commit hooks on the modified files before committing, to ensure # that the commit passes all checks. updated_files = ["pyproject.toml", "uv.lock", "CHANGELOG.md"] - run(["pre-commit", "run", "--files", *updated_files]) - run(["pre-commit", "run", "--files", *updated_files]) + run(["uv", "run", "pre-commit", "run", "--files", *updated_files]) + run(["uv", "run", "pre-commit", "run", "--files", *updated_files]) # Commit the changes and create an annotated tag for the release. run(["git", "add", *updated_files]) From 71e61eb526e50c19a46f918a8462389c80cf4614 Mon Sep 17 00:00:00 2001 From: Sebastiano Date: Wed, 5 Aug 2026 15:28:43 +0200 Subject: [PATCH 3/3] feat(release): add run_hooks function to execute pre-commit hooks on release files --- scripts/release.py | 19 +++++++++++++++++-- tests/test_release.py | 21 +++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index 0739b33..671a907 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -245,6 +245,22 @@ def generate_changelog( ) +def run_hooks(files: list[str], *, runner: Runner = run) -> None: + """Run the pre-commit hooks over the release files. + + The first pass may rewrite files (formatters, end-of-file-fixer), which + pre-commit reports as a non-zero exit even though nothing is wrong, so + its status is ignored. The second pass must come back clean. + + Args: + files: Paths to check, passed to ``pre-commit run --files``. + runner: Command runner, injectable for tests. + """ + command = ["uv", "run", "pre-commit", "run", "--files", *files] + runner(command, check=False) + runner(command) + + def main() -> int: """Run the release preparation process.""" args = parse_args() @@ -297,8 +313,7 @@ def main() -> int: # Run pre-commit hooks on the modified files before committing, to ensure # that the commit passes all checks. updated_files = ["pyproject.toml", "uv.lock", "CHANGELOG.md"] - run(["uv", "run", "pre-commit", "run", "--files", *updated_files]) - run(["uv", "run", "pre-commit", "run", "--files", *updated_files]) + run_hooks(updated_files) # Commit the changes and create an annotated tag for the release. run(["git", "add", *updated_files]) diff --git a/tests/test_release.py b/tests/test_release.py index c096601..acac7a7 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -17,6 +17,7 @@ generate_changelog, main, previous_tag, + run_hooks, sync_lockfile, tag_exists, validate_version, @@ -38,6 +39,7 @@ def __init__( results: dict[str, subprocess.CompletedProcess[str]] | None = None, ) -> None: self.commands: list[list[str]] = [] + self.checks: list[bool] = [] self.results = results or {} def __call__( @@ -48,6 +50,7 @@ def __call__( capture: bool = False, ) -> subprocess.CompletedProcess[str]: self.commands.append(command) + self.checks.append(check) key = " ".join(command) result = self.results.get(key) if result is None: @@ -199,6 +202,24 @@ def test_sync_lockfile_locks_then_verifies() -> None: assert runner.commands == [["uv", "lock"], ["uv", "lock", "--check"]] +def test_run_hooks_checks_the_given_files_twice() -> None: + runner = FakeRunner() + + run_hooks(["CHANGELOG.md"], runner=runner) + + expected = ["uv", "run", "pre-commit", "run", "--files", "CHANGELOG.md"] + assert runner.commands == [expected, expected] + + +def test_run_hooks_tolerates_files_rewritten_by_the_first_pass() -> None: + """A hook that fixes a file exits non-zero; only the retry must pass.""" + runner = FakeRunner() + + run_hooks(["CHANGELOG.md"], runner=runner) + + assert runner.checks == [False, True] + + def test_changelog_range_is_empty_without_a_previous_tag() -> None: assert changelog_range(None) == []