Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions .github/scripts/generate_changelog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Generate a Keep a Changelog section from Conventional Commit subjects."""

import argparse
import datetime as dt
import re
import subprocess
from pathlib import Path

GROUPS = {
"feat": "Added",
"fix": "Fixed",
"perf": "Changed",
"refactor": "Changed",
"docs": "Documentation",
"build": "Maintenance",
"ci": "Maintenance",
"chore": "Maintenance",
"test": "Maintenance",
}


def git(*args: str) -> str:
return subprocess.check_output(["git", *args], text=True, encoding="utf-8").strip()


def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--tag", required=True)
parser.add_argument("--repository", required=True)
args = parser.parse_args()
tags = git("tag", "--sort=-version:refname").splitlines()
previous = next((tag for tag in tags if tag != args.tag), None)
revision = f"{previous}..{args.tag}" if previous else args.tag
subjects = git("log", revision, "--format=%s").splitlines()
grouped: dict[str, list[str]] = {}
for subject in subjects:
match = re.match(r"([a-z]+)(?:\([^)]*\))?(!)?:\s*(.+)", subject)
if not match:
continue
kind, breaking, text = match.groups()
heading = "Breaking changes" if breaking else GROUPS.get(kind)
if heading:
grouped.setdefault(heading, []).append(text)
version = args.tag.removeprefix("v")
date = dt.date.today().isoformat()
order = ["Breaking changes", "Added", "Fixed", "Changed", "Documentation", "Maintenance"]
body = [f"## [{version}] - {date}", ""]
for heading in order:
if heading in grouped:
body.extend([f"### {heading}", "", *[f"- {item}" for item in grouped[heading]], ""])
compare = (
f"https://github.com/{args.repository}/compare/{previous}...{args.tag}"
if previous
else f"https://github.com/{args.repository}/releases/tag/{args.tag}"
)
body.append(f"[Full diff]({compare})")
section = "\n".join(body).rstrip() + "\n"
changelog = Path("CHANGELOG.md")
existing = (
changelog.read_text(encoding="utf-8")
if changelog.exists()
else "# Changelog\n\nAll notable changes are documented here.\n"
)
if f"## [{version}]" not in existing:
marker = "All notable changes are documented here.\n"
existing = existing.replace(marker, marker + "\n" + section + "\n", 1)
changelog.write_text(existing, encoding="utf-8")
Path("release-notes.md").write_text(section, encoding="utf-8")


if __name__ == "__main__":
main()
25 changes: 25 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,28 @@ jobs:
files: dist/*
# Requires Discussions enabled in the repo with this category present.
discussion_category_name: Announcements

changelog:
needs: publish
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
- name: Generate changelog
run: python .github/scripts/generate_changelog.py --tag "$GITHUB_REF_NAME" --repository "$GITHUB_REPOSITORY"
- name: Commit changelog
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add CHANGELOG.md
git diff --cached --quiet || git commit -m "docs: update changelog for $GITHUB_REF_NAME"
git push origin main
- name: Use changelog as release notes
run: gh release edit "$GITHUB_REF_NAME" --notes-file release-notes.md
env:
GH_TOKEN: ${{ github.token }}
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Changelog

All notable changes are documented here.

New sections are generated from Conventional Commits when a version tag is published.

## [1.0.2] - 2026-09-18

### Fixed

- Removed the unsupported Yandex image puzzle mode.
- Kept reCAPTCHA v3 support proxyless-only.

## [1.0.1] - 2026-09-17

### Fixed

- Aligned the polling interval with API recommendations.

## [1.0.0] - 2026-09-17

### Added

- Published the initial Python SDK release.
Loading