`generate_changelog.py`: divide into "fixed/added/changed" sections (#4712)

Make each release a little bit easier to do
This commit is contained in:
Emil Ernerfeldt 2024-06-27 09:54:01 +02:00 committed by GitHub
parent 5051e945e4
commit 7121a49e4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 93 additions and 53 deletions

View File

@ -12,6 +12,8 @@ import multiprocessing
import os import os
import re import re
import sys import sys
from collections import defaultdict
from datetime import date from datetime import date
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, List, Optional from typing import Any, List, Optional
@ -23,15 +25,13 @@ from tqdm import tqdm
OWNER = "emilk" OWNER = "emilk"
REPO = "egui" REPO = "egui"
INCLUDE_LABELS = False # It adds quite a bit of visual noise INCLUDE_LABELS = False # It adds quite a bit of visual noise
OFFICIAL_DEVS = [
"emilk",
]
@dataclass @dataclass
class PrInfo: class PrInfo:
pr_number: int
gh_user_name: str gh_user_name: str
pr_title: str title: str
labels: List[str] labels: List[str]
@ -85,7 +85,7 @@ def fetch_pr_info(pr_number: int) -> Optional[PrInfo]:
if response.status_code == 200: if response.status_code == 200:
labels = [label["name"] for label in json["labels"]] labels = [label["name"] for label in json["labels"]]
gh_user_name = json["user"]["login"] gh_user_name = json["user"]["login"]
return PrInfo(gh_user_name=gh_user_name, pr_title=json["title"], labels=labels) return PrInfo(pr_number=pr_number, gh_user_name=gh_user_name, title=json["title"], labels=labels)
else: else:
print(f"ERROR {url}: {response.status_code} - {json['message']}") print(f"ERROR {url}: {response.status_code} - {json['message']}")
return None return None
@ -101,18 +101,83 @@ def get_commit_info(commit: Any) -> CommitInfo:
return CommitInfo(hexsha=commit.hexsha, title=commit.summary, pr_number=None) return CommitInfo(hexsha=commit.hexsha, title=commit.summary, pr_number=None)
def pr_summary(pr: PrInfo, crate_name: Optional[str] = None) -> str:
title = pr.title
if crate_name is not None:
# Remove crate name prefix (common in PR titles):
title = remove_prefix(title, f"[{crate_name}] ")
title = remove_prefix(title, f"{crate_name}: ")
title = remove_prefix(title, f"`{crate_name}`: ")
# Upper-case first letter:
title = title[0].upper() + title[1:]
# Remove trailing periods:
title = title.rstrip(".")
summary = f"{title} [#{pr.pr_number}](https://github.com/{OWNER}/{REPO}/pull/{pr.pr_number})"
if INCLUDE_LABELS and 0 < len(pr.labels):
summary += f" ({', '.join(pr.labels)})"
summary += f" by [@{pr.gh_user_name}](https://github.com/{pr.gh_user_name})"
return summary
def pr_info_section(prs: List[PrInfo], *, crate_name: str, heading: Optional[str] = None) -> str:
result = ""
if 0 < len(prs):
if heading is not None:
result += f"### {heading}\n"
for pr in prs:
result += f"* {pr_summary(pr, crate_name)}\n"
result += "\n"
return result
def changelog_from_prs(pr_infos: List[PrInfo], crate_name: str) -> str:
if len(pr_infos) == 0:
return "Nothing new"
if len(pr_infos) <= 5:
# For small crates, or small releases
return pr_info_section(pr_infos, crate_name=crate_name)
fixed = []
added = []
rest = []
for pr in pr_infos:
summary = pr_summary(pr, crate_name)
if "bug" in pr.labels:
fixed.append(pr)
elif summary.startswith("Add") or "feature" in pr.labels:
added.append(pr)
else:
rest.append(pr)
result = ""
result += pr_info_section(added, crate_name=crate_name, heading="⭐ Added")
result += pr_info_section(rest, crate_name=crate_name, heading="🔧 Changed")
result += pr_info_section(fixed, crate_name=crate_name, heading="🐛 Fixed")
return result.rstrip()
def remove_prefix(text, prefix): def remove_prefix(text, prefix):
if text.startswith(prefix): if text.startswith(prefix):
return text[len(prefix) :] return text[len(prefix) :]
return text # or whatever return text # or whatever
def print_section(crate: str, items: List[str]) -> None: def print_section(heading: str, content: str) -> None:
if 0 < len(items): if content != "":
print(f"#### {crate}") print(f"## {heading}")
for line in items: print(content)
print(f"* {line}") print()
print()
def changelog_filepath(crate: str) -> str: def changelog_filepath(crate: str) -> str:
@ -124,10 +189,9 @@ def changelog_filepath(crate: str) -> str:
return os.path.normpath(file_path) return os.path.normpath(file_path)
def add_to_changelog_file(crate: str, items: List[str], version: str) -> None: def add_to_changelog_file(crate: str, content: str, version: str) -> None:
insert_text = f"\n## {version} - {date.today()}\n" insert_text = f"\n## {version} - {date.today()}\n"
for item in items: insert_text += content
insert_text += f"* {item}\n"
insert_text += "\n" insert_text += "\n"
file_path = changelog_filepath(crate) file_path = changelog_filepath(crate)
@ -193,7 +257,7 @@ def main() -> None:
ignore_labels = ["CI", "dependencies"] ignore_labels = ["CI", "dependencies"]
sections = {} crate_sections = defaultdict(list)
unsorted_prs = [] unsorted_prs = []
unsorted_commits = [] unsorted_commits = []
@ -209,66 +273,42 @@ def main() -> None:
unsorted_commits.append(summary) unsorted_commits.append(summary)
else: else:
if f"[#{pr_number}]" in all_changelogs: if f"[#{pr_number}]" in all_changelogs:
print(f"Ignoring PR that is already in the changelog: #{pr_number}") print(f"* Ignoring PR that is already in the changelog: [#{pr_number}](https://github.com/{OWNER}/{REPO}/pull/{pr_number})")
continue continue
# We prefer the PR title if available assert pr_info is not None
title = pr_info.pr_title if pr_info else title
labels = pr_info.labels if pr_info else []
if "exclude from changelog" in labels: if "exclude from changelog" in pr_info.labels:
continue continue
if "typo" in labels: if "typo" in pr_info.labels:
# We get so many typo PRs. Let's not flood the changelog with them. # We get so many typo PRs. Let's not flood the changelog with them.
continue continue
summary = f"{title} [#{pr_number}](https://github.com/{OWNER}/{REPO}/pull/{pr_number})"
if INCLUDE_LABELS and 0 < len(labels):
summary += f" ({', '.join(labels)})"
if pr_info is not None:
gh_user_name = pr_info.gh_user_name
if gh_user_name not in OFFICIAL_DEVS:
summary += f" (thanks [@{gh_user_name}](https://github.com/{gh_user_name})!)"
added = False added = False
for crate in crate_names: for crate in crate_names:
if crate in labels: if crate in pr_info.labels:
sections.setdefault(crate, []).append(summary) crate_sections[crate].append(pr_info)
added = True added = True
if not added: if not added:
if not any(label in labels for label in ignore_labels): if not any(label in pr_info.labels for label in ignore_labels):
unsorted_prs.append(summary) unsorted_prs.append(pr_summary(pr_info))
# Clean up:
for crate in crate_names:
if crate in sections:
items = sections[crate]
for i in range(len(items)):
line = items[i]
line = remove_prefix(line, f"[{crate}] ")
line = remove_prefix(line, f"{crate}: ")
line = remove_prefix(line, f"`{crate}`: ")
line = line[0].upper() + line[1:] # Upper-case first letter
items[i] = line
print() print()
print(f"Full diff at https://github.com/emilk/egui/compare/{args.commit_range}") print(f"Full diff at https://github.com/emilk/egui/compare/{args.commit_range}")
print() print()
for crate in crate_names: for crate in crate_names:
if crate in sections: if crate in crate_sections:
items = sections[crate] prs = crate_sections[crate]
print_section(crate, items) print_section(crate, changelog_from_prs(prs, crate))
print_section("Unsorted PRs", unsorted_prs) print_section("Unsorted PRs", "\n".join([f"* {item}" for item in unsorted_prs]))
print_section("Unsorted commits", unsorted_commits) print_section("Unsorted commits", "\n".join([f"* {item}" for item in unsorted_commits]))
if args.write: if args.write:
for crate in crate_names: for crate in crate_names:
items = sections[crate] if crate in sections else ["Nothing new"] items = changelog_from_prs(crate_sections[crate], crate)
add_to_changelog_file(crate, items, args.version) add_to_changelog_file(crate, items, args.version)