From 9cbfddf44a8602a09aecc8e0e0bd3e601eb5656d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Fri, 6 Nov 2026 08:51:51 -0800 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9A=A1=20perf(workflows):=20install=20on?= =?UTF-8?q?ly=20the=20formatter=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assembly ran `prek install-hooks`, building every hook environment under the seven-day cutoff, though it only runs mdformat or docstrfmt on the changelog. That took most of the job (32 of 45 seconds in tox-gh's dry run) and let an unrelated young hook, such as simplint's Ruff bump, fail the release. The formatter step now installs the one hook it runs. --- .github/workflows/prepare-release-creator.yaml | 8 -------- .../workflows/prepare-towncrier-release.yaml | 8 -------- README.md | 18 +++++++++--------- tests/cooled_prek/test_cooled_prek.py | 10 +++++----- 4 files changed, 14 insertions(+), 30 deletions(-) diff --git a/.github/workflows/prepare-release-creator.yaml b/.github/workflows/prepare-release-creator.yaml index 806b9a7..3ebb996 100644 --- a/.github/workflows/prepare-release-creator.yaml +++ b/.github/workflows/prepare-release-creator.yaml @@ -106,12 +106,6 @@ jobs: with: crate: cargo-edit version: 0.13.13 - - name: ๐Ÿงน Install prek hooks - id: hooks - uses: $/cooled-prek - with: - args: install-hooks - background: true - name: ๐Ÿท๏ธ Select release version id: version env: @@ -203,8 +197,6 @@ jobs: args+=(--package "$PACKAGE") fi release-creator contributors "${args[@]}" - - name: โณ Wait for prek hooks - wait: hooks - name: ๐Ÿงน Format changelog id: format continue-on-error: true diff --git a/.github/workflows/prepare-towncrier-release.yaml b/.github/workflows/prepare-towncrier-release.yaml index 1fdfe4c..deb064a 100644 --- a/.github/workflows/prepare-towncrier-release.yaml +++ b/.github/workflows/prepare-towncrier-release.yaml @@ -73,12 +73,6 @@ jobs: venv: .venv exclude-newer: ${{ inputs.exclude-newer }} exclude-newer-package: ${{ inputs.exclude-newer-package }} - - name: ๐Ÿงน Install prek hooks - id: hooks - uses: $/cooled-prek - with: - args: install-hooks - background: true - id: prepare name: ๐Ÿ“ Build changelog uses: $/prepare-towncrier @@ -87,8 +81,6 @@ jobs: tag-prefix: ${{ inputs.tag-prefix }} changelog-prefix: ${{ inputs.changelog-prefix }} phase: build - - name: โณ Wait for prek hooks - wait: hooks - name: ๐Ÿ“ Finalize release patch uses: $/prepare-towncrier with: diff --git a/README.md b/README.md index 6940a4a..6cebe3c 100644 --- a/README.md +++ b/README.md @@ -586,8 +586,8 @@ these examples means that commit. Assembly needs release history to select a tag. The writer can check out the source commit with `fetch-depth: 1`. All jobs use `persist-credentials: false`. Assembly installs the caller's locked release tools and needs `contents: read` -and `pull-requests: read` to resolve PR authors. It starts `prek install-hooks` while building the changelog, then waits -for the hooks before formatting and uploading the patch. The writer uses the `release-auth` environment with +and `pull-requests: read` to resolve PR authors. It installs only the changelog formatter's hook, when it formats, so a +hook the release never runs cannot fail it. The writer uses the `release-auth` environment with `RELEASE_APP_PRIVATE_KEY` and `RELEASE_APP_CLIENT_ID`. Restrict that environment to `main`. Install the App on this repository only, with Contents write and Metadata read, and grant the necessary branch and tag ruleset bypass. @@ -653,11 +653,11 @@ token exists. Trigger the caller on `push`, `schedule`, `workflow_dispatch`, or ### Dry run on pull requests -A release assembles under rules a pull request's own checks never apply: hooks install with packages from the last seven -days held back, fragments link to their pull requests, and the changelog builds and formats. A pull request that breaks -any of these merges green and fails the next release. `dry-run: true` runs `assemble` alone and skips `push`, so the -pull request fails instead. A dry run needs no App key and uses its own concurrency group per ref, so it never queues -behind a real release. +A release assembles under rules a pull request's own checks never apply: the changelog formatter's hook installs with +packages from the last seven days held back, fragments link to their pull requests, and the changelog builds and +formats. A pull request that breaks any of these merges green and fails the next release. `dry-run: true` runs +`assemble` alone and skips `push`, so the pull request fails instead. A dry run needs no App key and uses its own +concurrency group per ref, so it never queues behind a real release. Make the caller's prepare workflow callable and run it from the workflow that holds the required gate, so the dry run uses the same inputs as the release: @@ -747,8 +747,8 @@ formatting must all succeed. whose release headings use `v1.2.3`. - `version`: Explicit version for a caller with its own version selector. It takes precedence over `bump`. - `phase`: `all` (default) runs the complete Action. The reusable workflow uses `build` to select the version, link - authors, and build the changelog while hooks install; it then uses `finalize` to format, render, and write the patch. - Pass the version selected by `build` to `finalize`. + authors, and build the changelog; it then uses `finalize` to format, render, and write the patch. Pass the version + selected by `build` to `finalize`. The committed `tool.towncrier` configuration gives the `filename` and `directory`. `docstrfmt` formats a `.rst` changelog and `mdformat` any other; the formatter must be a configured `prek` hook. The Action writes the patch at diff --git a/tests/cooled_prek/test_cooled_prek.py b/tests/cooled_prek/test_cooled_prek.py index 0a83279..72a8dac 100644 --- a/tests/cooled_prek/test_cooled_prek.py +++ b/tests/cooled_prek/test_cooled_prek.py @@ -5,6 +5,7 @@ import subprocess from pathlib import Path +import pytest import yaml from conftest import action @@ -31,14 +32,13 @@ def test_no_hook_run_bypasses_cooled_prek() -> None: assert not commands, commands -def test_prepare_hook_install_uses_cooled_prek() -> None: +@pytest.mark.parametrize("name", ["prepare-towncrier-release.yaml", "prepare-release-creator.yaml"]) +def test_prepare_installs_no_hook_it_does_not_run(name: str) -> None: workflow = yaml.load( - (Path(__file__).resolve().parents[2] / ".github/workflows/prepare-towncrier-release.yaml").read_text(), - Loader=yaml.BaseLoader, + (Path(__file__).resolve().parents[2] / ".github/workflows" / name).read_text(), Loader=yaml.BaseLoader ) - step = next(step for step in workflow["jobs"]["assemble"]["steps"] if step.get("id") == "hooks") - assert (step["uses"], step["with"]) == ("$/cooled-prek", {"args": "install-hooks"}) + assert "install-hooks" not in str(workflow["jobs"]["assemble"]["steps"]) def test_cooled_prek_action_passes_each_argument(tmp_path: Path, action_scripts: Path) -> None: From b612f18e8d462f5a195c09d8282ed9d75a84dea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Fri, 6 Nov 2026 08:52:00 -0800 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=9D=20docs:=20add=20changelog=20fr?= =?UTF-8?q?agment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- changelog.d/+install-one-hook.performance.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog.d/+install-one-hook.performance.md diff --git a/changelog.d/+install-one-hook.performance.md b/changelog.d/+install-one-hook.performance.md new file mode 100644 index 0000000..73412b2 --- /dev/null +++ b/changelog.d/+install-one-hook.performance.md @@ -0,0 +1,3 @@ +`prepare-towncrier-release` and `prepare-release-creator` install only the changelog formatter's hook instead of every +hook, so release preparation and its pull request dry run finish faster, and a hook the release never runs cannot fail +it.