From 0a59575dfdc64420fd8ad4e682091081eff1e9b1 Mon Sep 17 00:00:00 2001 From: Nathan Yee Date: Tue, 12 Aug 2025 14:44:28 -0700 Subject: [PATCH 1/3] Add deployment notes to github utils script --- utils/github_utils.py | 99 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 96 insertions(+), 3 deletions(-) diff --git a/utils/github_utils.py b/utils/github_utils.py index e5885539df84..1f869b97e91a 100755 --- a/utils/github_utils.py +++ b/utils/github_utils.py @@ -8,8 +8,9 @@ import click import json import subprocess +from datetime import datetime from time import sleep -from typing import NoReturn +from typing import Dict, List, NoReturn @click.group() @@ -89,7 +90,6 @@ def release_notes(pull_request_number: int) -> NoReturn: dispatch_pr_url = "https://github.com/Netflix/dispatch/pull/" exclude_bot_authors = True exclude_labels = ["skip-changelog", "UI/UX", "javascript"] - gh_pr_list_merged_command = 'gh pr list -s merged --json "title,author,number,labels" -L 2000' sections = { "bug": "", "dependencies": "", @@ -105,7 +105,9 @@ def release_notes(pull_request_number: int) -> NoReturn: } click.echo(f"Fetching list of merged PRs since #{pull_request_number}...") - pull_requests = json.loads(run_command(gh_pr_list_merged_command)) + # Fetch only merged PRs for release notes + gh_command = 'gh pr list -s merged --json "title,author,number,labels" -L 2000' + pull_requests = json.loads(run_command(gh_command)) if not pull_requests: click.echo(f"No PRs merged since #{pull_request_number}.") @@ -157,5 +159,96 @@ def release_notes(pull_request_number: int) -> NoReturn: ) +def fetch_pull_requests(repo: str = "Netflix/dispatch", limit: int = 2000) -> List[Dict]: + """Fetch pull requests from repository using gh CLI.""" + gh_command = f'gh pr list --repo {repo} --state all --limit {limit} --json number,title,state,createdAt,author,url,headRefName,labels' + output = run_command(gh_command) + return json.loads(output) + + +def categorize_pull_requests(prs: List[Dict]) -> Dict[str, List[Dict]]: + """Categorize pull requests by their type.""" + categories = { + 'Fix': [], + 'Chore': [], + 'Feature': [], + 'Refactor': [] + } + + for pr in prs: + title = pr['title'] + first_word = title.split('(')[0].split(':')[0].lower() + + if first_word in ['fix', 'fixes']: + categories['Fix'].append(pr) + elif first_word in ['chore', 'deps', 'deps-dev']: + categories['Chore'].append(pr) + elif first_word in ['refactor', 'refactoring']: + categories['Refactor'].append(pr) + else: + categories['Feature'].append(pr) + + return categories + + +def clean_title(title: str) -> str: + """Clean PR title by removing prefix and capitalizing.""" + if ':' in title: + title = title.split(':', 1)[1].strip() + return title[0].upper() + title[1:] if title else title + + +def format_deployment_prs(prs: List[Dict]) -> None: + """Format and print pull requests for deployment announcements.""" + if not prs: + print("No pull requests found") + return + + today = datetime.now().strftime("%b %d") + print(f":announcement-2549: *Dispatch deployment to production today* ({today}) at [TIME] PT. Expect brief downtime.") + print() + + categories = categorize_pull_requests(prs) + + for category, pr_list in categories.items(): + if pr_list: + print(f"*{category}*") + for pr in pr_list: + title = clean_title(pr['title']) + print(f"• {title} ([#{pr['number']}]({pr['url']}))") + print() + + +@cli.command() +@click.option( + "--pr-number", + "-n", + required=True, + type=int, + help="PR number to start from (inclusive)" +) +@click.option( + "--repo", + default="Netflix/dispatch", + help="Repository in format owner/repo (default: Netflix/dispatch)" +) +def deployment_notes(pr_number: int, repo: str) -> NoReturn: + """Generate deployment notes for PRs starting from a specified PR number.""" + try: + # Fetch all PRs + prs = fetch_pull_requests(repo, limit=100) + + # Filter PRs with number >= pr_number + filtered_prs = [pr for pr in prs if pr["number"] >= pr_number] + + # Sort by PR number (descending) + filtered_prs.sort(key=lambda x: x["number"], reverse=True) + + format_deployment_prs(filtered_prs) + + except Exception as e: + click.echo(f"Error: {e}", err=True) + + if __name__ == "__main__": cli() From e920221a30d8c11a106522a76d5c00fe7d8e36d9 Mon Sep 17 00:00:00 2001 From: Nathan Yee Date: Tue, 12 Aug 2025 16:40:33 -0700 Subject: [PATCH 2/3] Update template based on feedback --- utils/github_utils.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/utils/github_utils.py b/utils/github_utils.py index 1f869b97e91a..5ee2a6536b6a 100755 --- a/utils/github_utils.py +++ b/utils/github_utils.py @@ -169,10 +169,9 @@ def fetch_pull_requests(repo: str = "Netflix/dispatch", limit: int = 2000) -> Li def categorize_pull_requests(prs: List[Dict]) -> Dict[str, List[Dict]]: """Categorize pull requests by their type.""" categories = { - 'Fix': [], - 'Chore': [], - 'Feature': [], - 'Refactor': [] + 'Fixes': [], + 'Features': [], + 'Refactors': [] } for pr in prs: @@ -180,13 +179,11 @@ def categorize_pull_requests(prs: List[Dict]) -> Dict[str, List[Dict]]: first_word = title.split('(')[0].split(':')[0].lower() if first_word in ['fix', 'fixes']: - categories['Fix'].append(pr) - elif first_word in ['chore', 'deps', 'deps-dev']: - categories['Chore'].append(pr) + categories['Fixes'].append(pr) elif first_word in ['refactor', 'refactoring']: - categories['Refactor'].append(pr) + categories['Refactors'].append(pr) else: - categories['Feature'].append(pr) + categories['Features'].append(pr) return categories @@ -208,7 +205,15 @@ def format_deployment_prs(prs: List[Dict]) -> None: print(f":announcement-2549: *Dispatch deployment to production today* ({today}) at [TIME] PT. Expect brief downtime.") print() - categories = categorize_pull_requests(prs) + # Filter out chore PRs before categorizing + non_chore_prs = [] + for pr in prs: + title = pr['title'] + first_word = title.split('(')[0].split(':')[0].lower() + if first_word not in ['chore', 'deps', 'deps-dev']: + non_chore_prs.append(pr) + + categories = categorize_pull_requests(non_chore_prs) for category, pr_list in categories.items(): if pr_list: From c4078c48b3698f3003ccf1274e45f84f205264fb Mon Sep 17 00:00:00 2001 From: Nathan Yee Date: Wed, 13 Aug 2025 16:45:56 -0700 Subject: [PATCH 3/3] Address feedback --- utils/github_utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/utils/github_utils.py b/utils/github_utils.py index 5ee2a6536b6a..6ef1d9a921e0 100755 --- a/utils/github_utils.py +++ b/utils/github_utils.py @@ -10,7 +10,7 @@ import subprocess from datetime import datetime from time import sleep -from typing import Dict, List, NoReturn +from typing import NoReturn @click.group() @@ -159,14 +159,14 @@ def release_notes(pull_request_number: int) -> NoReturn: ) -def fetch_pull_requests(repo: str = "Netflix/dispatch", limit: int = 2000) -> List[Dict]: +def fetch_pull_requests(repo: str = "Netflix/dispatch", limit: int = 2000) -> list[dict]: """Fetch pull requests from repository using gh CLI.""" gh_command = f'gh pr list --repo {repo} --state all --limit {limit} --json number,title,state,createdAt,author,url,headRefName,labels' output = run_command(gh_command) return json.loads(output) -def categorize_pull_requests(prs: List[Dict]) -> Dict[str, List[Dict]]: +def categorize_pull_requests(prs: list[dict]) -> dict[str, list[dict]]: """Categorize pull requests by their type.""" categories = { 'Fixes': [], @@ -195,7 +195,7 @@ def clean_title(title: str) -> str: return title[0].upper() + title[1:] if title else title -def format_deployment_prs(prs: List[Dict]) -> None: +def format_deployment_prs(prs: list[dict]) -> None: """Format and print pull requests for deployment announcements.""" if not prs: print("No pull requests found")