From 917952c5c049d40a03698c08bc9cddeb57978bf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Sun, 18 Oct 2026 01:00:35 -0700 Subject: [PATCH 1/8] =?UTF-8?q?=F0=9F=91=B7=20ci(release):=20require=20cha?= =?UTF-8?q?ngelog=20entries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/check.yaml | 30 ++++++++++++++++++++++++++ .github/workflows/prepare-release.yaml | 6 ++++++ .github/workflows/release.yaml | 12 +++++++++++ docs/changelog/151.contrib.rst | 1 + 4 files changed, 49 insertions(+) create mode 100644 docs/changelog/151.contrib.rst diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index ad7df4f..a1095f1 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -5,6 +5,7 @@ on: branches: ["main"] tags-ignore: ["**"] pull_request: + types: [opened, synchronize, reopened, labeled, unlabeled] schedule: - cron: "0 8 * * *" concurrency: @@ -80,3 +81,32 @@ jobs: run: tasks/old_target_check.sh 3.5 rejected - name: Reject Python 2.7 run: tasks/old_target_check.sh 2.7 rejected + changelog: + if: github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'skip news') + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 + with: + python-version: "3.15" + enable-cache: false + - run: uvx towncrier check --compare-with "origin/${GITHUB_BASE_REF}" + gate: + name: Required checks pass + if: always() + needs: [test, old-target, changelog] + runs-on: ubuntu-24.04 + steps: + - env: + NEEDS_JSON: ${{ toJSON(needs) }} + run: | + jq -e ' + all( + to_entries[]; + .value.result == "success" or + (.key == "changelog" and .value.result == "skipped") + ) + ' <<< "$NEEDS_JSON" diff --git a/.github/workflows/prepare-release.yaml b/.github/workflows/prepare-release.yaml index adfa2e3..45b8d46 100644 --- a/.github/workflows/prepare-release.yaml +++ b/.github/workflows/prepare-release.yaml @@ -81,6 +81,12 @@ jobs: echo "version=$next" >> $GITHUB_OUTPUT - name: Install tox run: uv tool install --python-preference only-managed --python 3.15 tox@latest --with tox-uv + - name: Require a news fragment + run: | + if ! compgen -G 'docs/changelog/*.*.rst' > /dev/null; then + echo '::error::add a docs/changelog/*.rst fragment before preparing a release' + exit 1 + fi - name: Run release process run: tox run -e release -- ${STEPS_VERSION_OUTPUTS_VERSION} env: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 259993a..86ddf93 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -14,6 +14,18 @@ jobs: with: fetch-depth: 0 persist-credentials: false + - name: Require a changelog section for the tag + env: + TAG: ${{ github.ref_name }} + run: | + if [[ ! "$TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::unexpected release tag $TAG" + exit 1 + fi + if ! grep -qE "^ v${TAG//./\\.} \\(" docs/changelog.rst; then + echo "::error::docs/changelog.rst has no section for $TAG" + exit 1 + fi - name: Install the latest version of uv uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 with: diff --git a/docs/changelog/151.contrib.rst b/docs/changelog/151.contrib.rst new file mode 100644 index 0000000..e52aaad --- /dev/null +++ b/docs/changelog/151.contrib.rst @@ -0,0 +1 @@ +Require a pull request news fragment and a versioned changelog section before publishing. From f88ad95aa6aceb84f6cfe26061918873a4b0149b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Sun, 18 Oct 2026 01:02:44 -0700 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=90=9B=20fix(release):=20stop=20force?= =?UTF-8?q?-pushing=20refs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tasks/release.py | 127 +++++++---------------------------------------- 1 file changed, 18 insertions(+), 109 deletions(-) diff --git a/tasks/release.py b/tasks/release.py index 9306ae9..b8e0d7b 100644 --- a/tasks/release.py +++ b/tasks/release.py @@ -1,55 +1,33 @@ -"""Handles creating a release commit and tag, then publishes the GitHub release.""" - from __future__ import annotations from pathlib import Path from subprocess import CalledProcessError, check_call, run +from typing import Final -from git import Commit, Head, Remote, Repo, TagReference +from git import Commit, Remote, Repo from packaging.version import Version -ROOT_SRC_DIR = Path(__file__).parents[1] +_ROOT_SRC_DIR: Final[Path] = Path(__file__).parents[1] def main(version_str: str) -> None: version = Version(version_str) - repo = Repo(str(ROOT_SRC_DIR)) + repo = Repo(str(_ROOT_SRC_DIR)) if repo.is_dirty(): msg = "Current repository is dirty. Please commit any changes and try again." raise RuntimeError(msg) - upstream, release_branch = create_release_branch(repo, version) - original_main_sha = upstream.refs.main.commit.hexsha - main_pushed = False - tag_pushed = False - release_created = False - try: - main_pushed, tag_pushed, release_created = push_release(repo, upstream, release_branch, version) - finalize_release(repo, upstream, release_branch) - except Exception: - cleanup_failed_release( - repo, - upstream, - version, - release_branch, - original_main_sha, - release_created=release_created, - tag_pushed=tag_pushed, - main_pushed=main_pushed, - ) - raise - - -def create_release_branch(repo: Repo, version: Version) -> tuple[Remote, Head]: - print("create release branch from upstream main") # ruff:ignore[print] upstream = get_upstream(repo) upstream.fetch() - branch_name = f"release-{version}" - release_branch = repo.create_head(branch_name, upstream.refs.main, force=True) - upstream.push(refspec=f"{branch_name}:{branch_name}", force=True) - release_branch.set_tracking_branch(upstream.refs[branch_name]) - release_branch.checkout() - return upstream, release_branch + if str(version) in {tag.name for tag in repo.tags}: + msg = f"release tag {version} already exists" + raise RuntimeError(msg) + repo.git.checkout("-B", f"release-{version}", upstream.refs.main) + release_commit = release_changelog(repo, version) + repo.create_tag(str(version), ref=release_commit.hexsha) + repo.git.push(upstream.name, "HEAD:main") + repo.git.push(upstream.name, f"refs/tags/{version}") + create_github_release(version) def get_upstream(repo: Repo) -> Remote: @@ -60,47 +38,26 @@ def get_upstream(repo: Repo) -> Remote: raise RuntimeError(msg) -def push_release(repo: Repo, upstream: Remote, release_branch: Head, version: Version) -> tuple[bool, bool, bool]: - release_commit = release_changelog(repo, version) - tag = tag_release_commit(release_commit, repo, version) - print("push release commit") # ruff:ignore[print] - repo.git.push(upstream.name, f"{release_branch}:main", "-f") - print("push release tag") # ruff:ignore[print] - repo.git.push(upstream.name, tag, "-f") - create_github_release(version) - return True, True, True - - def release_changelog(repo: Repo, version: Version) -> Commit: print("generate release commit") # ruff:ignore[print] - check_call(["towncrier", "build", "--yes", "--version", version.public], cwd=str(ROOT_SRC_DIR)) # ruff:ignore[start-process-with-partial-path] + check_call(["towncrier", "build", "--yes", "--version", version.public], cwd=str(_ROOT_SRC_DIR)) # ruff:ignore[start-process-with-partial-path] print("format changelog with pre-commit") # ruff:ignore[print] - changelog_path = ROOT_SRC_DIR / "docs" / "changelog.rst" + changelog_path = _ROOT_SRC_DIR / "docs" / "changelog.rst" try: - check_call(["pre-commit", "run", "--files", str(changelog_path)], cwd=str(ROOT_SRC_DIR)) # ruff:ignore[start-process-with-partial-path] + check_call(["pre-commit", "run", "--files", str(changelog_path)], cwd=str(_ROOT_SRC_DIR)) # ruff:ignore[start-process-with-partial-path] except CalledProcessError: print("pre-commit made formatting changes, staging them") # ruff:ignore[print] - repo.index.add([str(changelog_path)]) + repo.git.add("-A", "--", "docs/changelog.rst", "docs/changelog") return repo.index.commit(f"release {version}") -def tag_release_commit(release_commit: Commit, repo: Repo, version: Version) -> TagReference: - print("tag release commit") # ruff:ignore[print] - version_str = str(version) - if version_str in {tag.name for tag in repo.tags}: - print(f"delete existing tag {version_str}") # ruff:ignore[print] - repo.delete_tag(repo.tags[version_str]) - print(f"create tag {version_str}") # ruff:ignore[print] - return repo.create_tag(version_str, ref=release_commit.hexsha, force=True) - - def create_github_release(version: Version) -> None: print("create github release") # ruff:ignore[print] version_str = str(version) try: result = run( ["gh", "release", "create", version_str, "--title", f"v{version_str}", "--generate-notes"], # ruff:ignore[start-process-with-partial-path] - cwd=str(ROOT_SRC_DIR), + cwd=str(_ROOT_SRC_DIR), capture_output=True, text=True, check=True, @@ -116,54 +73,6 @@ def create_github_release(version: Version) -> None: raise -def finalize_release(repo: Repo, upstream: Remote, release_branch: Head) -> None: - print("checkout main to new release and delete release branch") # ruff:ignore[print] - repo.heads.main.checkout() - repo.delete_head(release_branch, force=True) - print("delete remote release branch") # ruff:ignore[print] - repo.git.push(upstream.name, f":{release_branch}", "--no-verify") - upstream.fetch() - repo.git.reset("--hard", f"{upstream.name}/main") - print("All done!") # ruff:ignore[print] - - -def cleanup_failed_release( # ruff:ignore[too-many-arguments] - repo: Repo, - upstream: Remote, - version: Version, - release_branch: Head, - original_main_sha: str, - *, - release_created: bool, - tag_pushed: bool, - main_pushed: bool, -) -> None: - print("Release failed! Cleaning up...") # ruff:ignore[print] - if release_created: - print(f"Deleting GitHub release {version}") # ruff:ignore[print] - try: - check_call(["gh", "release", "delete", str(version), "--yes"], cwd=str(ROOT_SRC_DIR)) # ruff:ignore[start-process-with-partial-path] - except Exception as cleanup_error: # ruff:ignore[blind-except] - print(f"Warning: Failed to delete GitHub release: {cleanup_error}") # ruff:ignore[print] - if tag_pushed: - print(f"Deleting remote tag {version}") # ruff:ignore[print] - try: - repo.git.push(upstream.name, f":refs/tags/{version}", "--no-verify") - except Exception as cleanup_error: # ruff:ignore[blind-except] - print(f"Warning: Failed to delete remote tag: {cleanup_error}") # ruff:ignore[print] - if main_pushed: - print(f"Reverting main to {original_main_sha[:8]}") # ruff:ignore[print] - try: - repo.git.push(upstream.name, f"{original_main_sha}:main", "-f", "--no-verify") - except Exception as cleanup_error: # ruff:ignore[blind-except] - print(f"Warning: Failed to revert main: {cleanup_error}") # ruff:ignore[print] - print("Deleting remote release branch") # ruff:ignore[print] - try: - repo.git.push(upstream.name, f":{release_branch}", "--no-verify") - except Exception as cleanup_error: # ruff:ignore[blind-except] - print(f"Warning: Failed to delete remote branch: {cleanup_error}") # ruff:ignore[print] - - if __name__ == "__main__": import argparse From e2bba78ff9910882785938c5322f486956df33f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Sun, 18 Oct 2026 01:28:52 -0700 Subject: [PATCH 3/8] fix: exclude internal news fragments --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8b504da..9475c43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -228,5 +228,4 @@ type = [ { directory = "doc", name = "Improved documentation", showcontent = true }, { directory = "packaging", name = "Packaging updates and notes for downstreams", showcontent = true }, { directory = "contrib", name = "Contributor-facing changes", showcontent = true }, - { directory = "misc", name = "Miscellaneous internal changes", showcontent = true }, ] From a9fd8638927e4d1753e7ccbd2ffadc770490fd96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Sun, 18 Oct 2026 01:44:09 -0700 Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=91=B7=20ci(release):=20use=20a=20rep?= =?UTF-8?q?ository=20app?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/prepare-release.yaml | 28 ++++++++++++++++++-------- docs/changelog/151.contrib.rst | 1 - tasks/release.py | 4 ++-- 3 files changed, 22 insertions(+), 11 deletions(-) delete mode 100644 docs/changelog/151.contrib.rst diff --git a/.github/workflows/prepare-release.yaml b/.github/workflows/prepare-release.yaml index 45b8d46..d09943e 100644 --- a/.github/workflows/prepare-release.yaml +++ b/.github/workflows/prepare-release.yaml @@ -18,27 +18,39 @@ env: FORCE_COLOR: 1 jobs: prepare-release: + if: github.ref == 'refs/heads/main' runs-on: ubuntu-24.04 environment: release-auth permissions: - contents: write + contents: read steps: + - id: token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.RELEASE_APP_CLIENT_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + repositories: ${{ github.event.repository.name }} + permission-contents: write - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - token: ${{ secrets.RELEASE_PAT }} - persist-credentials: true + token: ${{ steps.token.outputs.token }} + persist-credentials: true # zizmor: ignore[artipacked] the release script pushes main and its tag - name: Install the latest version of uv uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 with: python-version: "3.15" - enable-cache: true + enable-cache: false cache-dependency-glob: "pyproject.toml" - github-token: ${{ secrets.GITHUB_TOKEN }} - name: Configure git + env: + GH_TOKEN: ${{ steps.token.outputs.token }} + APP_SLUG: ${{ steps.token.outputs.app-slug }} run: | - git config user.name "${GITHUB_ACTOR}" - git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" + app_user_id=$(gh api "/users/${APP_SLUG}[bot]" --jq .id) + git config user.name "${APP_SLUG}[bot]" + git config user.email "${app_user_id}+${APP_SLUG}[bot]@users.noreply.github.com" - name: Calculate next version id: version env: @@ -90,7 +102,7 @@ jobs: - name: Run release process run: tox run -e release -- ${STEPS_VERSION_OUTPUTS_VERSION} env: - GH_TOKEN: ${{ secrets.RELEASE_PAT }} + GH_TOKEN: ${{ steps.token.outputs.token }} STEPS_VERSION_OUTPUTS_VERSION: ${{ steps.version.outputs.version }} - name: Display completion message run: echo "Release ${STEPS_VERSION_OUTPUTS_VERSION} prepared and pushed successfully!" diff --git a/docs/changelog/151.contrib.rst b/docs/changelog/151.contrib.rst deleted file mode 100644 index e52aaad..0000000 --- a/docs/changelog/151.contrib.rst +++ /dev/null @@ -1 +0,0 @@ -Require a pull request news fragment and a versioned changelog section before publishing. diff --git a/tasks/release.py b/tasks/release.py index b8e0d7b..5249ffb 100644 --- a/tasks/release.py +++ b/tasks/release.py @@ -24,7 +24,7 @@ def main(version_str: str) -> None: raise RuntimeError(msg) repo.git.checkout("-B", f"release-{version}", upstream.refs.main) release_commit = release_changelog(repo, version) - repo.create_tag(str(version), ref=release_commit.hexsha) + repo.create_tag(str(version), ref=release_commit.hexsha, message=f"release {version}") repo.git.push(upstream.name, "HEAD:main") repo.git.push(upstream.name, f"refs/tags/{version}") create_github_release(version) @@ -46,7 +46,7 @@ def release_changelog(repo: Repo, version: Version) -> Commit: try: check_call(["pre-commit", "run", "--files", str(changelog_path)], cwd=str(_ROOT_SRC_DIR)) # ruff:ignore[start-process-with-partial-path] except CalledProcessError: - print("pre-commit made formatting changes, staging them") # ruff:ignore[print] + check_call(["pre-commit", "run", "--files", str(changelog_path)], cwd=str(_ROOT_SRC_DIR)) # ruff:ignore[start-process-with-partial-path] repo.git.add("-A", "--", "docs/changelog.rst", "docs/changelog") return repo.index.commit(f"release {version}") From 7fb63f5f302892c5e2c6794d268713acf738a01d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Sun, 18 Oct 2026 02:46:45 -0700 Subject: [PATCH 5/8] =?UTF-8?q?=F0=9F=90=9B=20fix(release):=20isolate=20pr?= =?UTF-8?q?eparation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/prepare-release.yaml | 120 +++++++++++++++++-------- 1 file changed, 83 insertions(+), 37 deletions(-) diff --git a/.github/workflows/prepare-release.yaml b/.github/workflows/prepare-release.yaml index d09943e..4a87f25 100644 --- a/.github/workflows/prepare-release.yaml +++ b/.github/workflows/prepare-release.yaml @@ -17,40 +17,34 @@ permissions: env: FORCE_COLOR: 1 jobs: - prepare-release: + assemble: if: github.ref == 'refs/heads/main' runs-on: ubuntu-24.04 - environment: release-auth permissions: contents: read + outputs: + version: ${{ steps.version.outputs.version }} steps: - - id: token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - with: - client-id: ${{ vars.RELEASE_APP_CLIENT_ID }} - private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} - owner: ${{ github.repository_owner }} - repositories: ${{ github.event.repository.name }} - permission-contents: write - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - fetch-depth: 0 - token: ${{ steps.token.outputs.token }} - persist-credentials: true # zizmor: ignore[artipacked] the release script pushes main and its tag - - name: Install the latest version of uv + fetch-depth: 0 # The next version comes from the tags. + persist-credentials: false + - name: Install uv uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 with: python-version: "3.15" enable-cache: false - cache-dependency-glob: "pyproject.toml" - - name: Configure git - env: - GH_TOKEN: ${{ steps.token.outputs.token }} - APP_SLUG: ${{ steps.token.outputs.app-slug }} + - name: Require a news fragment run: | - app_user_id=$(gh api "/users/${APP_SLUG}[bot]" --jq .id) - git config user.name "${APP_SLUG}[bot]" - git config user.email "${app_user_id}+${APP_SLUG}[bot]@users.noreply.github.com" + if ! compgen -G 'docs/changelog/*.*.rst' > /dev/null; then + echo '::error::add a docs/changelog/*.rst fragment before preparing a release' + exit 1 + fi + - name: Set up remote tracking + run: | + git remote -v + git fetch origin + git branch -a - name: Calculate next version id: version env: @@ -90,21 +84,73 @@ jobs: esac echo "Next version: $next" - echo "version=$next" >> $GITHUB_OUTPUT - - name: Install tox - run: uv tool install --python-preference only-managed --python 3.15 tox@latest --with tox-uv - - name: Require a news fragment + echo "version=$next" >> "$GITHUB_OUTPUT" + - name: Build release patch + env: + VERSION: ${{ steps.version.outputs.version }} run: | - if ! compgen -G 'docs/changelog/*.*.rst' > /dev/null; then - echo '::error::add a docs/changelog/*.rst fragment before preparing a release' - exit 1 - fi - - name: Run release process - run: tox run -e release -- ${STEPS_VERSION_OUTPUTS_VERSION} + set -euo pipefail + uvx towncrier build --yes --version "$VERSION" + uvx --with pre-commit-uv pre-commit run --files docs/changelog.rst || + uvx --with pre-commit-uv pre-commit run --files docs/changelog.rst + git diff HEAD --binary > "$RUNNER_TEMP/release.patch" + test -s "$RUNNER_TEMP/release.patch" + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: release-patch + path: ${{ runner.temp }}/release.patch + if-no-files-found: error + push: + needs: assemble + runs-on: ubuntu-24.04 + environment: release-auth + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: release-patch + path: ${{ runner.temp }}/release-patch + - name: Validate release patch env: - GH_TOKEN: ${{ steps.token.outputs.token }} - STEPS_VERSION_OUTPUTS_VERSION: ${{ steps.version.outputs.version }} - - name: Display completion message - run: echo "Release ${STEPS_VERSION_OUTPUTS_VERSION} prepared and pushed successfully!" + VERSION: ${{ needs.assemble.outputs.version }} + run: | + set -euo pipefail + git apply --check "$RUNNER_TEMP/release-patch/release.patch" + git apply --index "$RUNNER_TEMP/release-patch/release.patch" + while IFS= read -r path; do + case "$path" in + docs/changelog.rst|docs/changelog/*.rst) ;; + *) echo "::error::unexpected release path: $path"; exit 1 ;; + esac + done < <(git diff --cached --name-only) + grep -qE "^ v${VERSION//./\\.} \(" docs/changelog.rst + git diff --cached --check + - id: token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.RELEASE_APP_CLIENT_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + repositories: ${{ github.event.repository.name }} + permission-contents: write + - name: Commit and tag the release env: - STEPS_VERSION_OUTPUTS_VERSION: ${{ steps.version.outputs.version }} + VERSION: ${{ needs.assemble.outputs.version }} + GH_TOKEN: ${{ steps.token.outputs.token }} + APP_SLUG: ${{ steps.token.outputs.app-slug }} + run: | + set -euo pipefail + app_user_id=$(gh api "/users/${APP_SLUG}[bot]" --jq .id) + git config user.name "${APP_SLUG}[bot]" + git config user.email "${app_user_id}+${APP_SLUG}[bot]@users.noreply.github.com" + git commit -m "release $VERSION" + git tag -a "$VERSION" -m "release $VERSION" + gh auth setup-git --hostname github.com --force + git push origin HEAD:main + git push origin "refs/tags/$VERSION" + gh release create "$VERSION" --title "v$VERSION" --generate-notes --verify-tag From fd2db59d8a4b0495c76a6e20dea2ea2cc5d783c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Sun, 18 Oct 2026 03:44:49 -0700 Subject: [PATCH 6/8] =?UTF-8?q?=F0=9F=94=92=20Push=20release=20refs=20atom?= =?UTF-8?q?ically?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/prepare-release.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/prepare-release.yaml b/.github/workflows/prepare-release.yaml index 4a87f25..c36fc2b 100644 --- a/.github/workflows/prepare-release.yaml +++ b/.github/workflows/prepare-release.yaml @@ -151,6 +151,5 @@ jobs: git commit -m "release $VERSION" git tag -a "$VERSION" -m "release $VERSION" gh auth setup-git --hostname github.com --force - git push origin HEAD:main - git push origin "refs/tags/$VERSION" + git push --atomic origin HEAD:main "refs/tags/$VERSION" gh release create "$VERSION" --title "v$VERSION" --generate-notes --verify-tag From 1630e9ab6c41f50bf1d0831a14460c98d23909ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Sun, 18 Oct 2026 09:21:57 -0700 Subject: [PATCH 7/8] =?UTF-8?q?=F0=9F=90=9B=20fix(release):=20refresh=20ma?= =?UTF-8?q?in=20before=20push?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/prepare-release.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/prepare-release.yaml b/.github/workflows/prepare-release.yaml index c36fc2b..a1c1d9c 100644 --- a/.github/workflows/prepare-release.yaml +++ b/.github/workflows/prepare-release.yaml @@ -120,6 +120,8 @@ jobs: VERSION: ${{ needs.assemble.outputs.version }} run: | set -euo pipefail + git fetch origin main + git reset --hard origin/main git apply --check "$RUNNER_TEMP/release-patch/release.patch" git apply --index "$RUNNER_TEMP/release-patch/release.patch" while IFS= read -r path; do From 895c5a1a7f38902f7fecd31da360df578befff40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Sun, 18 Oct 2026 09:21:57 -0700 Subject: [PATCH 8/8] =?UTF-8?q?=F0=9F=91=B7=20ci:=20label=20automation=20P?= =?UTF-8?q?Rs=20skip=20news?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/dependabot.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 69d0cfd..fccacae 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -2,6 +2,10 @@ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" + labels: + - dependencies + - github_actions + - skip news schedule: interval: "weekly" day: "tuesday"