From da11b00cd9801545e4623668ef2790a23f6774a2 Mon Sep 17 00:00:00 2001 From: shuo-zhou~ Date: Thu, 17 Sep 2026 19:22:37 +0100 Subject: [PATCH 1/2] Replace the changelog action with a maintained one saadmk11/changelog-ci@v1.2.0 builds FROM python:3.12-slim-bullseye. Bullseye is end-of-life and Debian is draining its package pool, so apt-get install git fails with exit code 100 and the workflow cannot build at all. Switch to mikepenz/release-changelog-builder-action, which builds no Docker image and is actively maintained, keep the same label groups, and grant the workflow the permissions the changelog commit and the pull request comment need. Re-runs are idempotent: a section that already exists for the version is replaced in place, and identical content produces no commit. --- .github/changelog-ci-config.json | 33 ------- .github/changelog-config.json | 28 ++++++ .github/scripts/update_changelog.py | 129 ++++++++++++++++++++++++++++ .github/workflows/changelog.yml | 118 ++++++++++++++++++++++--- .gitignore | 3 +- CONTRIBUTING.md | 2 +- 6 files changed, 264 insertions(+), 49 deletions(-) delete mode 100644 .github/changelog-ci-config.json create mode 100644 .github/changelog-config.json create mode 100644 .github/scripts/update_changelog.py diff --git a/.github/changelog-ci-config.json b/.github/changelog-ci-config.json deleted file mode 100644 index 61cd892..0000000 --- a/.github/changelog-ci-config.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "changelog_type": "pull_request", - "header_prefix": "Version", - "commit_changelog": true, - "comment_changelog": true, - "include_unlabeled_changes": true, - "unlabeled_group_title": "Other Changes", - "pull_request_title_regex": "(?i:release)", - "version_regex": "v?[0-9]+[.]+[0-9]+[.]+[0-9]+([ab][0-9]+)?", - "exclude_labels": ["dependabot", "bot", "ci"], - "group_config": [ - { - "title": "New Features", - "labels": ["feature", "enhancement"] - }, - { - "title": "Bug Fixes", - "labels": ["bug", "bugfix"] - }, - { - "title": "Code Improvements", - "labels": ["improvements", "refactor", "refactoring"] - }, - { - "title": "Documentation", - "labels": ["documentation", "docs", "doc"] - }, - { - "title": "Tests", - "labels": ["tests", "testing", "test"] - } - ] -} diff --git a/.github/changelog-config.json b/.github/changelog-config.json new file mode 100644 index 0000000..90951af --- /dev/null +++ b/.github/changelog-config.json @@ -0,0 +1,28 @@ +{ + "template": "#{{CHANGELOG}}\n\n#### Other Changes\n\n#{{UNCATEGORIZED}}", + "pr_template": "- #{{TITLE}} ([##{{NUMBER}}](#{{URL}}))", + "empty_template": "- No pull requests were merged since the last release.", + "ignore_labels": ["dependabot", "bot", "ci"], + "categories": [ + { + "title": "#### New Features", + "labels": ["feature", "enhancement"] + }, + { + "title": "#### Bug Fixes", + "labels": ["bug", "bugfix"] + }, + { + "title": "#### Code Improvements", + "labels": ["improvements", "refactor", "refactoring"] + }, + { + "title": "#### Documentation", + "labels": ["documentation", "docs", "doc"] + }, + { + "title": "#### Tests", + "labels": ["tests", "testing", "test"] + } + ] +} diff --git a/.github/scripts/update_changelog.py b/.github/scripts/update_changelog.py new file mode 100644 index 0000000..4c7506f --- /dev/null +++ b/.github/scripts/update_changelog.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +"""Insert a generated release section into .github/CHANGELOG.md. + +Usage: update_changelog.py + +The generated file holds the label-grouped list of pull requests produced by +release-changelog-builder-action. It is turned into a section + + # Version + + #### + + - + +which is written to the top of the changelog. Group headings without any +entries below them are dropped, so an empty "Other Changes" group does not +leave a dangling heading behind. + +The generated file is rewritten to hold the final section, so the same text +can be reused as the pull request comment body. + +Running the workflow more than once for the same version is safe: a section +that is already present is replaced in place instead of being duplicated, and +identical content leaves the file untouched. +""" + +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path + +SECTION_RE = re.compile(r"^#\s+Version\s+(?P\S+)\s*$") +GROUP_RE = re.compile(r"^#{2,6}\s") + + +def normalise_version(version: str) -> str: + """Drop a leading ``v`` so tag names and headers compare equal.""" + return version.strip().lstrip("vV") + + +def tidy(text: str) -> str: + """Trim trailing whitespace and collapse runs of blank lines.""" + lines = [line.rstrip() for line in text.splitlines()] + collapsed: list[str] = [] + for line in lines: + if not line and collapsed and not collapsed[-1]: + continue + collapsed.append(line) + while collapsed and not collapsed[0]: + collapsed.pop(0) + while collapsed and not collapsed[-1]: + collapsed.pop() + return "\n".join(collapsed) + + +def drop_empty_groups(text: str) -> str: + """Remove group headings that have no entries below them.""" + lines = text.splitlines() + kept: list[str] = [] + index = 0 + while index < len(lines): + if GROUP_RE.match(lines[index]): + end = index + 1 + while end < len(lines) and not GROUP_RE.match(lines[end]): + end += 1 + if any(line.strip() for line in lines[index + 1 : end]): + kept.extend(lines[index:end]) + index = end + else: + kept.append(lines[index]) + index += 1 + return tidy("\n".join(kept)) + + +def replace_section(changelog: str, version: str, section: str) -> str: + """Replace the section of ``version``, or prepend it when absent.""" + lines = changelog.splitlines() + start = None + for index, line in enumerate(lines): + match = SECTION_RE.match(line) + if match and normalise_version(match.group("version")) == version: + start = index + break + + if start is None: + return tidy(f"{section}\n\n{changelog}") + "\n" + + end = len(lines) + for index in range(start + 1, len(lines)): + if SECTION_RE.match(lines[index]): + end = index + break + + merged = "\n\n".join(["\n".join(lines[:start]), section, "\n".join(lines[end:])]) + return tidy(merged) + "\n" + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("version", help="release version, e.g. 0.2.0b1") + parser.add_argument("generated", type=Path, help="changelog built by the action") + parser.add_argument("changelog", type=Path, help="changelog file to update") + args = parser.parse_args() + + if not args.generated.is_file(): + sys.stderr.write(f"error: {args.generated} was not generated\n") + return 1 + + body = drop_empty_groups(args.generated.read_text(encoding="utf-8")) + if not body: + sys.stderr.write(f"error: {args.generated} is empty\n") + return 1 + + version = normalise_version(args.version) + section = f"# Version {version}\n\n{body}" + + existing = args.changelog.read_text(encoding="utf-8") if args.changelog.is_file() else "" + args.changelog.write_text(replace_section(existing, version, section), encoding="utf-8") + # Reuse the section as the pull request comment body. + args.generated.write_text(section + "\n", encoding="utf-8") + + sys.stdout.write(f"updated {args.changelog} for version {version}\n") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index e006387..2a09662 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -1,31 +1,121 @@ -# This workflow will generate a log of changes automatically upon a new release. -# See https://github.com/marketplace/actions/changelog-ci +# Generates the changelog for release pull requests. +# +# A pull request whose title mentions a release and carries a version, e.g. +# "Prepare the 0.2.0b1 release of kalelinear", gets a label-grouped changelog +# of every pull request merged since the previous tag. The section is written +# to .github/CHANGELOG.md, committed to the pull request branch, and posted as +# a pull request comment. +# +# See https://github.com/mikepenz/release-changelog-builder-action name: changelog on: pull_request: - types: [opened] + # "opened"/"reopened" cover the usual flow, and "edited" picks up a pull + # request that is retitled into a release. "synchronize" is deliberately + # left out: the changelog commit below would trigger it again. + types: [opened, reopened, edited] + +# Writing the changelog commit and the pull request comment needs more than +# the default read-only token. Without these permissions both operations fail +# and the action only reports a warning, leaving a green but empty run. +permissions: + contents: write + pull-requests: write + +# A newer run of the same pull request supersedes the one in flight. +concurrency: + group: changelog-${{ github.event.pull_request.number }} + cancel-in-progress: true jobs: log-changes: name: Log changes runs-on: ubuntu-latest + # Pull requests from forks receive a read-only token, which can neither + # push nor comment. Only branches of this repository are supported. + if: github.event.pull_request.head.repo.full_name == github.repository steps: - - name: Checkout code + - name: Checkout pull request branch uses: actions/checkout@v4 with: - repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.event.pull_request.head.ref }} - # Keep checkout shallow; changelog-ci handles unshallow internally. - # Using fetch-depth: 0 causes changelog-ci to fail with: - # "fatal: --unshallow on a complete repository does not make sense" - fetch-depth: 1 + # The full history is needed to locate the previous release tag. + fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - - name: Run changelog - uses: saadmk11/changelog-ci@v1.2.0 + + - name: Get release version from pull request title + id: version + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + version="" + if printf '%s' "$PR_TITLE" | grep -qi 'release'; then + version=$(printf '%s' "$PR_TITLE" \ + | grep -oEi 'v?[0-9]+\.[0-9]+\.[0-9]+([ab][0-9]+)?' \ + | head -n1) + fi + if [ -z "$version" ]; then + echo "::notice::Not a release pull request, skipping changelog generation." + fi + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: Find previous release tag + id: previous + if: steps.version.outputs.version != '' + run: | + tag=$(git describe --tags --abbrev=0 2>/dev/null || true) + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "Previous release tag: ${tag:-none}" + + - name: Build changelog + if: steps.version.outputs.version != '' + # v6.3.0, pinned to a commit so an upstream tag move cannot change the + # code that runs here. Dependabot can bump this. + uses: mikepenz/release-changelog-builder-action@cb021f9b36a51a7c6f18e4679b6fa2cb77a1260c with: - changelog_filename: .github/CHANGELOG.md - config_file: .github/changelog-ci-config.json + configuration: .github/changelog-config.json + fromTag: ${{ steps.previous.outputs.tag }} + toTag: ${{ github.event.pull_request.head.sha }} + outputFile: .github/changelog.generated.md + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Update changelog + if: steps.version.outputs.version != '' + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + python3 .github/scripts/update_changelog.py \ + "$VERSION" \ + .github/changelog.generated.md \ + .github/CHANGELOG.md + + - name: Commit changelog + id: commit + if: steps.version.outputs.version != '' env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.version.outputs.version }} + run: | + if git diff --quiet -- .github/CHANGELOG.md; then + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "::notice::CHANGELOG.md already lists this release, nothing to commit." + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add .github/CHANGELOG.md + git commit -m "Update CHANGELOG.md for version ${VERSION#v}" + git push + echo "changed=true" >> "$GITHUB_OUTPUT" + + - name: Comment changelog on pull request + if: steps.version.outputs.version != '' && steps.commit.outputs.changed == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: gh pr comment "$PR_NUMBER" --body-file .github/changelog.generated.md + + - name: Remove generated file + if: always() + run: rm -f .github/changelog.generated.md diff --git a/.gitignore b/.gitignore index 24191a3..50ace6a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,8 @@ venv/ *.zip *.ckpt *.json -!.github/changelog-ci-config.json +!.github/changelog-config.json +.github/changelog.generated.md # Distribution / packaging build/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0c7167e..0005e17 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -223,7 +223,7 @@ Because the package is distributed on PyPI, the actual version strings must be v #### Release checklist - Bump the version in `setup.py` and `kalelinear/__init__.py`. -- Update [`.github/CHANGELOG.md`](.github/CHANGELOG.md) with a summary of changes since the last release. +- Update [`.github/CHANGELOG.md`](.github/CHANGELOG.md) with a summary of changes since the last release. The [changelog workflow](.github/workflows/changelog.yml) opens a grouped list of the merged pull requests at the top of the file when a pull request is titled with the version (e.g. `Prepare the 0.2.0b1 release of kalelinear`); reword those entries as needed before merging. - Create a GitHub release for the new version (e.g. `0.1.0b1`), marking prereleases appropriately. - The [release workflow](.github/workflows/release.yml) builds the wheel and source distribution and publishes them to Test PyPI (prerelease) or PyPI (final release). From 222c0fba723e15cac3cba418719cfd54317d1543 Mon Sep 17 00:00:00 2001 From: shuo-zhou~ Date: Mon, 21 Sep 2026 22:15:30 +0100 Subject: [PATCH 2/2] update changelog --- .github/workflows/changelog.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 2a09662..aca93e5 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -54,7 +54,7 @@ jobs: if printf '%s' "$PR_TITLE" | grep -qi 'release'; then version=$(printf '%s' "$PR_TITLE" \ | grep -oEi 'v?[0-9]+\.[0-9]+\.[0-9]+([ab][0-9]+)?' \ - | head -n1) + | head -n1 || true) fi if [ -z "$version" ]; then echo "::notice::Not a release pull request, skipping changelog generation." @@ -86,7 +86,8 @@ jobs: env: VERSION: ${{ steps.version.outputs.version }} run: | - python3 .github/scripts/update_changelog.py \ + git show "${{ github.event.pull_request.base.sha }}:.github/scripts/update_changelog.py" > "$RUNNER_TEMP/update_changelog.py" + python3 "$RUNNER_TEMP/update_changelog.py" \ "$VERSION" \ .github/changelog.generated.md \ .github/CHANGELOG.md