-
Notifications
You must be signed in to change notification settings - Fork 3
Replace the changelog action with a maintained one #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| { | ||
| "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"], | ||
| "consume": true | ||
| }, | ||
| { | ||
| "title": "#### Bug Fixes", | ||
| "labels": ["bug", "bugfix"], | ||
| "consume": true | ||
| }, | ||
| { | ||
| "title": "#### Code Improvements", | ||
| "labels": ["improvements", "refactor", "refactoring"], | ||
| "consume": true | ||
| }, | ||
| { | ||
| "title": "#### Documentation", | ||
| "labels": ["documentation", "docs", "doc"], | ||
| "consume": true | ||
| }, | ||
| { | ||
| "title": "#### Tests", | ||
| "labels": ["tests", "testing", "test"], | ||
| "consume": true | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| #!/usr/bin/env python3 | ||
| """Insert a generated release section into .github/CHANGELOG.md. | ||
|
|
||
| Usage: update_changelog.py <version> <generated-file> <changelog-file> | ||
|
|
||
| The generated file holds the label-grouped list of pull requests produced by | ||
| release-changelog-builder-action. It is turned into a section | ||
|
|
||
| # Version <version> | ||
|
|
||
| #### <group> | ||
|
|
||
| - <pull request> | ||
|
|
||
| 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. If no group has any entries, which happens | ||
| when every pull request carries an ignored label, a placeholder line is | ||
| written instead. | ||
|
|
||
| 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<version>\S+)\s*$") | ||
| GROUP_RE = re.compile(r"^#{2,6}\s") | ||
| NO_ENTRIES_PLACEHOLDER = "- No changelog entries: all pull requests merged since the last release have ignored labels." | ||
|
|
||
|
|
||
| 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: | ||
| # The action falls back to its empty_template only when no pull | ||
| # requests were fetched at all. When every fetched pull request | ||
| # carries an ignored label, it renders just the empty group headings, | ||
| # which drop_empty_groups removes. The release still gets a section. | ||
| body = NO_ENTRIES_PLACEHOLDER | ||
|
|
||
| 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()) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,31 +1,148 @@ | ||
| # 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: | ||
| # Limited to "opened": "reopened" and "edited" would re-run this job on | ||
| # every subsequent edit (including a maintainer rewording the generated | ||
| # changelog section), and update_changelog.py always replaces that | ||
| # section wholesale, silently discarding the reword. The trade-off is | ||
| # that a pull request retitled into a release after opening won't get a | ||
| # changelog. "synchronize" is deliberately left out too: the changelog | ||
| # commit below would trigger it again. | ||
| types: [opened] | ||
|
|
||
| # 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 | ||
| # \b anchors both ends so "0.4.0rc1" isn't truncated into the | ||
| # unrelated version "0.4.0": only "a"/"b" pre-releases are | ||
| # supported, and anything else must fail loudly below rather | ||
| # than being silently misfiled under the wrong version. | ||
| version=$(printf '%s' "$PR_TITLE" \ | ||
| | grep -oEi '\bv?[0-9]+\.[0-9]+\.[0-9]+([ab][0-9]+)?\b' \ | ||
| | head -n1 || true) | ||
| version=${version,,} | ||
| if [ -z "$version" ] && printf '%s' "$PR_TITLE" | grep -qE '[0-9]+\.[0-9]+\.[0-9]+'; then | ||
| echo "::error::Pull request title mentions a release and a version, but the version isn't a plain release or an a/b pre-release (e.g. 0.4.0 or 0.4.0b1): $PR_TITLE" | ||
| exit 1 | ||
| fi | ||
|
Comment on lines
58
to
+70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, this fixes the |
||
| 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 | ||
| # Without this, a failed commit-comparison request (e.g. a bad tag | ||
| # or an API error) is treated as an empty diff: the action emits | ||
| # the "no pull requests were merged" placeholder as if it were a | ||
| # real result, and the steps below would commit that placeholder | ||
| # over a valid changelog section. | ||
| failOnError: true | ||
| token: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
|
shuo-zhou marked this conversation as resolved.
|
||
| - name: Update changelog | ||
| if: steps.version.outputs.version != '' | ||
| env: | ||
| VERSION: ${{ steps.version.outputs.version }} | ||
| run: | | ||
| 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 | ||
|
|
||
| - 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 | ||
| # Independent of steps.commit.outputs.changed: Update changelog | ||
| # above always rewrites changelog.generated.md with the current | ||
| # section, whether or not this run pushed a commit. Gating on | ||
| # "changed" would mean a run that pushes successfully but then | ||
| # fails (or is cancelled) before commenting could never comment on | ||
| # a later re-run, since that re-run would find the content already | ||
| # committed and skip the push, permanently skipping the comment too. | ||
| if: steps.version.outputs.version != '' | ||
| 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 | ||
|
|
||
|
shuo-zhou marked this conversation as resolved.
|
||
| - name: Remove generated file | ||
| if: always() | ||
| run: rm -f .github/changelog.generated.md | ||
Uh oh!
There was an error while loading. Please reload this page.