List PRs in changelog generator

This commit is contained in:
goaaats 2025-12-07 15:52:13 +01:00
parent c50237cf66
commit 094483e5a0
2 changed files with 166 additions and 67 deletions

View file

@ -8,7 +8,8 @@ import re
import sys
import json
import argparse
from typing import List, Tuple, Optional
import os
from typing import List, Tuple, Optional, Dict, Any
def run_git_command(args: List[str]) -> str:
@ -30,14 +31,14 @@ def get_last_two_tags() -> Tuple[str, str]:
"""Get the latest two git tags."""
tags = run_git_command(["tag", "--sort=-version:refname"])
tag_list = [t for t in tags.split("\n") if t]
# Filter out old tags that start with 'v' (old versioning scheme)
tag_list = [t for t in tag_list if not t.startswith('v')]
if len(tag_list) < 2:
print("Error: Need at least 2 tags in the repository", file=sys.stderr)
sys.exit(1)
return tag_list[0], tag_list[1]
@ -55,58 +56,144 @@ def get_submodule_commit(submodule_path: str, tag: str) -> Optional[str]:
return None
def get_commits_between_tags(tag1: str, tag2: str) -> List[Tuple[str, str]]:
"""Get commits between two tags. Returns list of (message, author) tuples."""
def get_repo_info() -> Tuple[str, str]:
"""Get repository owner and name from git remote."""
try:
remote_url = run_git_command(["config", "--get", "remote.origin.url"])
# Handle both HTTPS and SSH URLs
# SSH: git@github.com:owner/repo.git
# HTTPS: https://github.com/owner/repo.git
match = re.search(r'github\.com[:/](.+?)/(.+?)(?:\.git)?$', remote_url)
if match:
owner = match.group(1)
repo = match.group(2)
return owner, repo
else:
print("Error: Could not parse GitHub repository from remote URL", file=sys.stderr)
sys.exit(1)
except:
print("Error: Could not get git remote URL", file=sys.stderr)
sys.exit(1)
def get_commits_between_tags(tag1: str, tag2: str) -> List[str]:
"""Get commit SHAs between two tags."""
log_output = run_git_command([
"log",
f"{tag2}..{tag1}",
"--format=%s|%an|%h"
"--format=%H"
])
commits = []
for line in log_output.split("\n"):
if "|" in line:
message, author, sha = line.split("|", 2)
commits.append((message.strip(), author.strip(), sha.strip()))
commits = [sha.strip() for sha in log_output.split("\n") if sha.strip()]
return commits
def filter_commits(commits: List[Tuple[str, str]], ignore_patterns: List[str]) -> List[Tuple[str, str]]:
"""Filter out commits matching any of the ignore patterns."""
def get_pr_for_commit(commit_sha: str, owner: str, repo: str, token: str) -> Optional[Dict[str, Any]]:
"""Get PR information for a commit using GitHub API."""
try:
import requests
except ImportError:
print("Error: requests library is required. Install it with: pip install requests", file=sys.stderr)
sys.exit(1)
headers = {
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28"
}
if token:
headers["Authorization"] = f"Bearer {token}"
url = f"https://api.github.com/repos/{owner}/{repo}/commits/{commit_sha}/pulls"
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
prs = response.json()
if prs and len(prs) > 0:
# Return the first PR (most relevant one)
pr = prs[0]
return {
"number": pr["number"],
"title": pr["title"],
"author": pr["user"]["login"],
"url": pr["html_url"]
}
except requests.exceptions.HTTPError as e:
if e.response.status_code == 404:
# Commit might not be associated with a PR
return None
elif e.response.status_code == 403:
print("Warning: GitHub API rate limit exceeded. Consider providing a token.", file=sys.stderr)
return None
else:
print(f"Warning: Failed to fetch PR for commit {commit_sha[:7]}: {e}", file=sys.stderr)
return None
except Exception as e:
print(f"Warning: Error fetching PR for commit {commit_sha[:7]}: {e}", file=sys.stderr)
return None
return None
def get_prs_between_tags(tag1: str, tag2: str, owner: str, repo: str, token: str) -> List[Dict[str, Any]]:
"""Get PRs between two tags using GitHub API."""
commits = get_commits_between_tags(tag1, tag2)
print(f"Found {len(commits)} commits, fetching PR information...")
prs = []
seen_pr_numbers = set()
for i, commit_sha in enumerate(commits, 1):
if i % 10 == 0:
print(f"Progress: {i}/{len(commits)} commits processed...")
pr_info = get_pr_for_commit(commit_sha, owner, repo, token)
if pr_info and pr_info["number"] not in seen_pr_numbers:
seen_pr_numbers.add(pr_info["number"])
prs.append(pr_info)
return prs
def filter_prs(prs: List[Dict[str, Any]], ignore_patterns: List[str]) -> List[Dict[str, Any]]:
"""Filter out PRs matching any of the ignore patterns."""
compiled_patterns = [re.compile(pattern) for pattern in ignore_patterns]
filtered = []
for message, author, sha in commits:
if not any(pattern.search(message) for pattern in compiled_patterns):
filtered.append((message, author, sha))
for pr in prs:
if not any(pattern.search(pr["title"]) for pattern in compiled_patterns):
filtered.append(pr)
return filtered
def generate_changelog(version: str, prev_version: str, commits: List[Tuple[str, str]],
cs_commit_new: Optional[str], cs_commit_old: Optional[str]) -> str:
def generate_changelog(version: str, prev_version: str, prs: List[Dict[str, Any]],
cs_commit_new: Optional[str], cs_commit_old: Optional[str],
owner: str, repo: str) -> str:
"""Generate markdown changelog."""
# Calculate statistics
commit_count = len(commits)
unique_authors = len(set(author for _, author, _ in commits))
pr_count = len(prs)
unique_authors = len(set(pr["author"] for pr in prs))
changelog = f"# Dalamud Release v{version}\n\n"
changelog += f"We just released Dalamud v{version}, which should be available to users within a few minutes. "
changelog += f"This release includes **{commit_count} commit{'s' if commit_count != 1 else ''} from {unique_authors} contributor{'s' if unique_authors != 1 else ''}**.\n"
changelog += f"[Click here](<https://github.com/goatcorp/Dalamud/compare/{prev_version}...{version}>) to see all Dalamud changes.\n\n"
changelog += f"This release includes **{pr_count} PR{'s' if pr_count != 1 else ''} from {unique_authors} contributor{'s' if unique_authors != 1 else ''}**.\n"
changelog += f"[Click here](<https://github.com/{owner}/{repo}/compare/{prev_version}...{version}>) to see all Dalamud changes.\n\n"
if cs_commit_new and cs_commit_old and cs_commit_new != cs_commit_old:
changelog += f"It ships with an updated **FFXIVClientStructs [`{cs_commit_new[:7]}`](<https://github.com/aers/FFXIVClientStructs/commit/{cs_commit_new}>)**.\n"
changelog += f"[Click here](<https://github.com/aers/FFXIVClientStructs/compare/{cs_commit_old}...{cs_commit_new}>) to see all CS changes.\n"
elif cs_commit_new:
changelog += f"It ships with **FFXIVClientStructs [`{cs_commit_new[:7]}`](<https://github.com/aers/FFXIVClientStructs/commit/{cs_commit_new}>)**.\n"
changelog += "## Dalamud Changes\n\n"
for message, author, sha in commits:
changelog += f"* {message} (by **{author}** as [`{sha}`](<https://github.com/goatcorp/Dalamud/commit/{sha}>))\n"
for pr in prs:
changelog += f"* {pr['title']} ([#**{pr['number']}**](<{pr['url']}>) by **{pr['author']}**)\n"
return changelog
@ -117,9 +204,9 @@ def post_to_discord(webhook_url: str, content: str, version: str) -> None:
except ImportError:
print("Error: requests library is required. Install it with: pip install requests", file=sys.stderr)
sys.exit(1)
filename = f"changelog-v{version}.md"
# Prepare the payload
data = {
"content": f"Dalamud v{version} has been released!",
@ -130,13 +217,13 @@ def post_to_discord(webhook_url: str, content: str, version: str) -> None:
}
]
}
# Prepare the files
files = {
"payload_json": (None, json.dumps(data)),
"files[0]": (filename, content.encode('utf-8'), 'text/markdown')
}
try:
result = requests.post(webhook_url, files=files)
result.raise_for_status()
@ -158,54 +245,64 @@ def main():
required=True,
help="Discord webhook URL"
)
parser.add_argument(
"--github-token",
default=os.environ.get("GITHUB_TOKEN"),
help="GitHub API token (or set GITHUB_TOKEN env var). Increases rate limit."
)
parser.add_argument(
"--ignore",
action="append",
default=[],
help="Regex patterns to ignore commits (can be specified multiple times)"
help="Regex patterns to ignore PRs (can be specified multiple times)"
)
parser.add_argument(
"--submodule-path",
default="lib/FFXIVClientStructs",
help="Path to the FFXIVClientStructs submodule (default: lib/FFXIVClientStructs)"
)
args = parser.parse_args()
# Get repository info
owner, repo = get_repo_info()
print(f"Repository: {owner}/{repo}")
# Get the last two tags
latest_tag, previous_tag = get_last_two_tags()
print(f"Generating changelog between {previous_tag} and {latest_tag}")
# Get submodule commits at both tags
cs_commit_new = get_submodule_commit(args.submodule_path, latest_tag)
cs_commit_old = get_submodule_commit(args.submodule_path, previous_tag)
if cs_commit_new:
print(f"FFXIVClientStructs commit (new): {cs_commit_new[:7]}")
if cs_commit_old:
print(f"FFXIVClientStructs commit (old): {cs_commit_old[:7]}")
# Get commits between tags
commits = get_commits_between_tags(latest_tag, previous_tag)
print(f"Found {len(commits)} commits")
# Filter commits
filtered_commits = filter_commits(commits, args.ignore)
print(f"After filtering: {len(filtered_commits)} commits")
# Get PRs between tags
prs = get_prs_between_tags(latest_tag, previous_tag, owner, repo, args.github_token)
prs.reverse()
print(f"Found {len(prs)} PRs")
# Filter PRs
filtered_prs = filter_prs(prs, args.ignore)
print(f"After filtering: {len(filtered_prs)} PRs")
# Generate changelog
changelog = generate_changelog(latest_tag, previous_tag, filtered_commits,
cs_commit_new, cs_commit_old)
changelog = generate_changelog(latest_tag, previous_tag, filtered_prs,
cs_commit_new, cs_commit_old, owner, repo)
print("\n" + "="*50)
print("Generated Changelog:")
print("="*50)
print(changelog)
print("="*50 + "\n")
# Post to Discord
post_to_discord(args.webhook_url, changelog, latest_tag)
if __name__ == "__main__":
main()
main()

View file

@ -6,41 +6,43 @@ on:
tags:
- '*'
permissions: read-all
jobs:
generate-changelog:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history and tags
submodules: true # Fetch submodules
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.14'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests
- name: Generate and post changelog
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GIT_TERMINAL_PROMPT: 0
run: |
python .github/generate_changelog.py \
--webhook-url "${{ secrets.DISCORD_CHANGELOG_WEBHOOK_URL }}" \
--ignore "^Merge" \
--ignore "^build:" \
--ignore "^docs:"
env:
GIT_TERMINAL_PROMPT: 0
--ignore "Update ClientStructs" \
--ignore "^build:"
- name: Upload changelog as artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: changelog
path: changelog-*.md
if-no-files-found: ignore
if-no-files-found: ignore