mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 18:27:23 +01:00
List PRs in changelog generator
This commit is contained in:
parent
c50237cf66
commit
094483e5a0
2 changed files with 166 additions and 67 deletions
161
.github/generate_changelog.py
vendored
161
.github/generate_changelog.py
vendored
|
|
@ -8,7 +8,8 @@ import re
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
import argparse
|
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:
|
def run_git_command(args: List[str]) -> str:
|
||||||
|
|
@ -55,46 +56,132 @@ def get_submodule_commit(submodule_path: str, tag: str) -> Optional[str]:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_commits_between_tags(tag1: str, tag2: str) -> List[Tuple[str, str]]:
|
def get_repo_info() -> Tuple[str, str]:
|
||||||
"""Get commits between two tags. Returns list of (message, author) tuples."""
|
"""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_output = run_git_command([
|
||||||
"log",
|
"log",
|
||||||
f"{tag2}..{tag1}",
|
f"{tag2}..{tag1}",
|
||||||
"--format=%s|%an|%h"
|
"--format=%H"
|
||||||
])
|
])
|
||||||
|
|
||||||
commits = []
|
commits = [sha.strip() for sha in log_output.split("\n") if sha.strip()]
|
||||||
for line in log_output.split("\n"):
|
|
||||||
if "|" in line:
|
|
||||||
message, author, sha = line.split("|", 2)
|
|
||||||
commits.append((message.strip(), author.strip(), sha.strip()))
|
|
||||||
|
|
||||||
return commits
|
return commits
|
||||||
|
|
||||||
|
|
||||||
def filter_commits(commits: List[Tuple[str, str]], ignore_patterns: List[str]) -> List[Tuple[str, str]]:
|
def get_pr_for_commit(commit_sha: str, owner: str, repo: str, token: str) -> Optional[Dict[str, Any]]:
|
||||||
"""Filter out commits matching any of the ignore patterns."""
|
"""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]
|
compiled_patterns = [re.compile(pattern) for pattern in ignore_patterns]
|
||||||
|
|
||||||
filtered = []
|
filtered = []
|
||||||
for message, author, sha in commits:
|
for pr in prs:
|
||||||
if not any(pattern.search(message) for pattern in compiled_patterns):
|
if not any(pattern.search(pr["title"]) for pattern in compiled_patterns):
|
||||||
filtered.append((message, author, sha))
|
filtered.append(pr)
|
||||||
|
|
||||||
return filtered
|
return filtered
|
||||||
|
|
||||||
|
|
||||||
def generate_changelog(version: str, prev_version: str, commits: List[Tuple[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]) -> str:
|
cs_commit_new: Optional[str], cs_commit_old: Optional[str],
|
||||||
|
owner: str, repo: str) -> str:
|
||||||
"""Generate markdown changelog."""
|
"""Generate markdown changelog."""
|
||||||
# Calculate statistics
|
# Calculate statistics
|
||||||
commit_count = len(commits)
|
pr_count = len(prs)
|
||||||
unique_authors = len(set(author for _, author, _ in commits))
|
unique_authors = len(set(pr["author"] for pr in prs))
|
||||||
|
|
||||||
changelog = f"# Dalamud Release v{version}\n\n"
|
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"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"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/goatcorp/Dalamud/compare/{prev_version}...{version}>) to see all Dalamud changes.\n\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:
|
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"It ships with an updated **FFXIVClientStructs [`{cs_commit_new[:7]}`](<https://github.com/aers/FFXIVClientStructs/commit/{cs_commit_new}>)**.\n"
|
||||||
|
|
@ -104,8 +191,8 @@ def generate_changelog(version: str, prev_version: str, commits: List[Tuple[str,
|
||||||
|
|
||||||
changelog += "## Dalamud Changes\n\n"
|
changelog += "## Dalamud Changes\n\n"
|
||||||
|
|
||||||
for message, author, sha in commits:
|
for pr in prs:
|
||||||
changelog += f"* {message} (by **{author}** as [`{sha}`](<https://github.com/goatcorp/Dalamud/commit/{sha}>))\n"
|
changelog += f"* {pr['title']} ([#**{pr['number']}**](<{pr['url']}>) by **{pr['author']}**)\n"
|
||||||
|
|
||||||
return changelog
|
return changelog
|
||||||
|
|
||||||
|
|
@ -158,11 +245,16 @@ def main():
|
||||||
required=True,
|
required=True,
|
||||||
help="Discord webhook URL"
|
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(
|
parser.add_argument(
|
||||||
"--ignore",
|
"--ignore",
|
||||||
action="append",
|
action="append",
|
||||||
default=[],
|
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(
|
parser.add_argument(
|
||||||
"--submodule-path",
|
"--submodule-path",
|
||||||
|
|
@ -172,6 +264,10 @@ def main():
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Get repository info
|
||||||
|
owner, repo = get_repo_info()
|
||||||
|
print(f"Repository: {owner}/{repo}")
|
||||||
|
|
||||||
# Get the last two tags
|
# Get the last two tags
|
||||||
latest_tag, previous_tag = get_last_two_tags()
|
latest_tag, previous_tag = get_last_two_tags()
|
||||||
print(f"Generating changelog between {previous_tag} and {latest_tag}")
|
print(f"Generating changelog between {previous_tag} and {latest_tag}")
|
||||||
|
|
@ -185,17 +281,18 @@ def main():
|
||||||
if cs_commit_old:
|
if cs_commit_old:
|
||||||
print(f"FFXIVClientStructs commit (old): {cs_commit_old[:7]}")
|
print(f"FFXIVClientStructs commit (old): {cs_commit_old[:7]}")
|
||||||
|
|
||||||
# Get commits between tags
|
# Get PRs between tags
|
||||||
commits = get_commits_between_tags(latest_tag, previous_tag)
|
prs = get_prs_between_tags(latest_tag, previous_tag, owner, repo, args.github_token)
|
||||||
print(f"Found {len(commits)} commits")
|
prs.reverse()
|
||||||
|
print(f"Found {len(prs)} PRs")
|
||||||
|
|
||||||
# Filter commits
|
# Filter PRs
|
||||||
filtered_commits = filter_commits(commits, args.ignore)
|
filtered_prs = filter_prs(prs, args.ignore)
|
||||||
print(f"After filtering: {len(filtered_commits)} commits")
|
print(f"After filtering: {len(filtered_prs)} PRs")
|
||||||
|
|
||||||
# Generate changelog
|
# Generate changelog
|
||||||
changelog = generate_changelog(latest_tag, previous_tag, filtered_commits,
|
changelog = generate_changelog(latest_tag, previous_tag, filtered_prs,
|
||||||
cs_commit_new, cs_commit_old)
|
cs_commit_new, cs_commit_old, owner, repo)
|
||||||
|
|
||||||
print("\n" + "="*50)
|
print("\n" + "="*50)
|
||||||
print("Generated Changelog:")
|
print("Generated Changelog:")
|
||||||
|
|
|
||||||
12
.github/workflows/generate-changelog.yml
vendored
12
.github/workflows/generate-changelog.yml
vendored
|
|
@ -6,6 +6,8 @@ on:
|
||||||
tags:
|
tags:
|
||||||
- '*'
|
- '*'
|
||||||
|
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
generate-changelog:
|
generate-changelog:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
@ -28,14 +30,14 @@ jobs:
|
||||||
pip install requests
|
pip install requests
|
||||||
|
|
||||||
- name: Generate and post changelog
|
- name: Generate and post changelog
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GIT_TERMINAL_PROMPT: 0
|
||||||
run: |
|
run: |
|
||||||
python .github/generate_changelog.py \
|
python .github/generate_changelog.py \
|
||||||
--webhook-url "${{ secrets.DISCORD_CHANGELOG_WEBHOOK_URL }}" \
|
--webhook-url "${{ secrets.DISCORD_CHANGELOG_WEBHOOK_URL }}" \
|
||||||
--ignore "^Merge" \
|
--ignore "Update ClientStructs" \
|
||||||
--ignore "^build:" \
|
--ignore "^build:"
|
||||||
--ignore "^docs:"
|
|
||||||
env:
|
|
||||||
GIT_TERMINAL_PROMPT: 0
|
|
||||||
|
|
||||||
- name: Upload changelog as artifact
|
- name: Upload changelog as artifact
|
||||||
if: always()
|
if: always()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue