`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 re
import sys
from collections import defaultdict
from datetime import date
from dataclasses import dataclass
from typing import Any, List, Optional
@ -23,15 +25,13 @@ from tqdm import tqdm
OWNER = "emilk"
REPO = "egui"
INCLUDE_LABELS = False # It adds quite a bit of visual noise
OFFICIAL_DEVS = [
"emilk",
]
@dataclass
class PrInfo:
pr_number: int
gh_user_name: str
pr_title: str
title: str
labels: List[str]
@ -85,7 +85,7 @@ def fetch_pr_info(pr_number: int) -> Optional[PrInfo]:
if response.status_code == 200:
labels = [label["name"] for label in json["labels"]]
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:
print(f"ERROR {url}: {response.status_code} - {json['message']}")
return None
@ -101,18 +101,83 @@ def get_commit_info(commit: Any) -> CommitInfo:
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):
if text.startswith(prefix):
return text[len(prefix) :]
return text # or whatever
def print_section(crate: str, items: List[str]) -> None:
if 0 < len(items):
print(f"#### {crate}")
for line in items:
print(f"* {line}")
print()
def print_section(heading: str, content: str) -> None:
if content != "":
print(f"## {heading}")
print(content)
print()
def changelog_filepath(crate: str) -> str:
@ -124,10 +189,9 @@ def changelog_filepath(crate: str) -> str:
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"
for item in items:
insert_text += f"* {item}\n"
insert_text += content
insert_text += "\n"
file_path = changelog_filepath(crate)
@ -193,7 +257,7 @@ def main() -> None:
ignore_labels = ["CI", "dependencies"]
sections = {}
crate_sections = defaultdict(list)
unsorted_prs = []
unsorted_commits = []
@ -209,66 +273,42 @@ def main() -> None:
unsorted_commits.append(summary)
else:
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
# We prefer the PR title if available
title = pr_info.pr_title if pr_info else title
labels = pr_info.labels if pr_info else []
assert pr_info is not None
if "exclude from changelog" in labels:
if "exclude from changelog" in pr_info.labels:
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.
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
for crate in crate_names:
if crate in labels:
sections.setdefault(crate, []).append(summary)
if crate in pr_info.labels:
crate_sections[crate].append(pr_info)
added = True
if not added:
if not any(label in labels for label in ignore_labels):
unsorted_prs.append(summary)
# 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
if not any(label in pr_info.labels for label in ignore_labels):
unsorted_prs.append(pr_summary(pr_info))
print()
print(f"Full diff at https://github.com/emilk/egui/compare/{args.commit_range}")
print()
for crate in crate_names:
if crate in sections:
items = sections[crate]
print_section(crate, items)
print_section("Unsorted PRs", unsorted_prs)
print_section("Unsorted commits", unsorted_commits)
if crate in crate_sections:
prs = crate_sections[crate]
print_section(crate, changelog_from_prs(prs, crate))
print_section("Unsorted PRs", "\n".join([f"* {item}" for item in unsorted_prs]))
print_section("Unsorted commits", "\n".join([f"* {item}" for item in unsorted_commits]))
if args.write:
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)