Add changelog generator skill featuring a GenAI summary of the changes and the full changelog generated as copy-paste-able HTML from the git log. Prompt: Use the generate-changelog skill to generate a changelog from the branches 7727 to 7778 Bug: webrtc:465491622 Change-Id: I24fcf89aa4caf9a6af77d6f564c5656c2e36dc2f Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/463801 Reviewed-by: Harald Alvestrand <hta@webrtc.org> Commit-Queue: Philipp Hancke <philipp.hancke@googlemail.com> Reviewed-by: Evan Shrubsole <eshr@webrtc.org> Cr-Commit-Position: refs/heads/main@{#47614}
diff --git a/agents/skills/webrtc-changelog/SKILL.md b/agents/skills/webrtc-changelog/SKILL.md new file mode 100644 index 0000000..0c42a44 --- /dev/null +++ b/agents/skills/webrtc-changelog/SKILL.md
@@ -0,0 +1,46 @@ +--- +name: webrtc-changelog +description: Generate a professional HTML changelog from WebRTC git logs. Automatically categorizes changes, includes a high-level summary, and provides detailed commit and review links. +--- + +# WebRTC Changelog Generator + +`webrtc-changelog` automates the generation of a professional, categorized HTML +report for WebRTC changes. + +## Workflow + +1. **Fetch Branch Information:** Navigate to + `https://chromiumdash.appspot.com/branches` and extract the current and + previous branch numbers associated with the milestone from the **webrtc** + column of the first two rows in the **Branch details** table. These will be + used as `<branch2>` (current) and `<branch1>` (previous). Note the milestone + number (e.g., 148). +2. **Update Git** Execute `git fetch origin` to retrieve the remote git + branches. +3. **Fetch Git Log:** Execute + `git log branch-heads/<branch1>..branch-heads/<branch2>` to retrieve the + commit history. +4. **Generate Summary:** Analyze the retrieved git log and produce a high-level + summary of the most significant changes. The summary MUST be formatted as an + HTML unordered list (`<ul><li>...</li></ul>`) for best presentation in the + report. +5. **Generate HTML Report:** Use the `generate_changelog.py` script to create + the final report, injecting the generated HTML summary and the milestone: + ```bash + python3 tools_webrtc/generate_changelog.py <branch1> <branch2> <output_file> --summary_text "<summary_html>" --milestone M<milestone_number> + ``` + +## Features + +### AI-Powered Summary + +- High-level, one-pager overview of the release. +- Automatically includes commit and author statistics. + +### Categorized Changes + +- Automatically categorizes commits based on keywords in the commit. +- Provides 📝 (Commit) and 🔍 (Review/Gerrit) links for each entry. +- Automatically converts `Bug:` references (e.g., `webrtc:123`, `chromium:456`, + `b/789`) into hyperlinks.
diff --git a/tools_webrtc/generate_changelog.py b/tools_webrtc/generate_changelog.py new file mode 100755 index 0000000..abe1d4d --- /dev/null +++ b/tools_webrtc/generate_changelog.py
@@ -0,0 +1,337 @@ +#!/usr/bin/env vpython3 + +# Copyright (c) 2026 The WebRTC project authors. All Rights Reserved. +# +# Use of this source code is governed by a BSD-style license +# that can be found in the LICENSE file in the root of the source +# tree. An additional intellectual property rights grant can be found +# in the file PATENTS. All contributing project authors may +# be found in the AUTHORS file in the root of the source tree. +"""Script to generate a WebRTC changelog in HTML format. + +This script parses git logs between two branches and categorizes commits +to produce a user-friendly changelog. +""" + +import argparse +import collections +import re +import subprocess +import sys + +# Define categories and keyword mappings +CATEGORIES = { + 'API': ['api/'], + 'Transport': ['transport', 'dtls', 'ice', 'p2p', 'rtp', 'sctp'], + 'Audio': ['audio', 'aec3', 'voice', 'audio_processing'], + 'Video': ['video', 'h264', 'vp8', 'vp9', 'av1', 'video_coding'], + 'Peerconnection': ['pc/', 'peer_connection', 'signaling', 'jsep'], + 'Stats': ['stats', 'collector', 'rtc_stats'], + 'Security': ['security', 'bounds', 'overflow'], + 'Infrastructure': ['build', 'gn', 'ninja', 'iwyu', 'owners', 'watchlist'], +} + +CHANGELOG_CSS = """ + body { + font-family: sans-serif; + line-height: 1.4; + color: #333; + max-width: 1200px; + margin: 0 auto; + padding: 15px; + font-size: 0.9em; + } + h1 { + color: #1a73e8; + border-bottom: 2px solid #1a73e8; + padding-bottom: 5px; + font-size: 1.5em; + } + h2 { + color: #1a73e8; + font-size: 1.2em; + margin: 15px 0 5px 0; + } + table { + width: 100%; + border-collapse: collapse; + margin-top: 10px; + margin-bottom: 20px; + } + th, td { + border: 1px solid #dadce0; + padding: 6px; + text-align: left; + vertical-align: top; + } + th { + background-color: #f8f9fa; + } + a { + color: #1a73e8; + text-decoration: none; + } + .external-links { + margin-bottom: 15px; + padding: 10px; + background: #f1f3f4; + border-radius: 4px; + } + .external-links a { + font-weight: bold; + margin-right: 15px; + } + .summary-container { + display: flex; + gap: 15px; + align-items: flex-start; + } + .overall-summary { + flex: 2; + } + .category-summary { + flex: 1; + } + .category-summary table { + margin-top: 0; + } +""" + + +def get_category(subject, body): + """Categorizes a commit based on keywords in the subject and body.""" + combined = (subject + ' ' + body).lower() + # Split by whitespace or the presence of "/" but keep the "/" + # e.g. "pc/srtp_session.cc" -> ["pc/", "srtp_session.cc"] + # Then strip punctuation to allow exact matching with keywords. + words = [ + w.strip('.,:;()[]{}') for w in re.split(r'(?<=/)|\s+', combined) if w + ] + for cat, keywords in CATEGORIES.items(): + if any(kw in words for kw in keywords): + return cat + return 'General' + + +def format_single_bug(bug_str): + patterns = [ + (r'webrtc:(\d+)', r'https://issues.webrtc.org/issues/\1'), + (r'chromium:(\d+)', r'https://issues.chromium.org/issues/\1'), + (r'b/(\d+)', r'https://issues.chromium.org/issues/\1'), + (r'b:(\d+)', r'https://issues.chromium.org/issues/\1'), + ] + for pattern, url in patterns: + if re.search(pattern, bug_str, re.IGNORECASE): + url_part = re.sub(pattern, url, bug_str, flags=re.IGNORECASE) + return (f'<a href="{url_part}">' + f'{bug_str}</a>') + return bug_str + + +def format_bugs(bug_list): + """Formats a list of bug strings into HTML links, separated by commas.""" + if not bug_list: + return 'None' + formatted_bugs = [] + for bug_str in bug_list.split(','): + bug_str = bug_str.strip() + if bug_str and bug_str.lower() != 'none': + formatted_bugs.append(format_single_bug(bug_str)) + return ', '.join(formatted_bugs) if formatted_bugs else 'None' + + +def parse_git_commits(log_text): + # Split by the custom delimiter, and filter out empty strings + commit_chunks = log_text.split('--WEBRTC-COMMIT-DELIMITER--') + commits = [] + for chunk in commit_chunks: + chunk = chunk.strip() + if not chunk: + continue + + lines = chunk.splitlines() + if len(lines) < 2: + # Should have at least (hash, author, subject) and a + # (potentially empty) body line. + continue + + [commit_hash, author, subject] = lines[0].split(' ', 2) + body_lines = lines[1:] + + bugs = [] + review_url = None + parsed_body = [] + # Extract Bug and Reviewed-on from body lines and keep all body lines. + for line in body_lines: + line_stripped = line.strip() + if line_stripped.startswith('Bug:') or line_stripped.startswith( + 'Fixed:'): + # Note: this is a single string even if there are multiple + # comma-separated bug ids. + bugs.append(line.split(':', 1)[1].strip()) + elif line_stripped.startswith('Reviewed-on:'): + review_url = line.split(':', 1)[1].strip() + parsed_body.append(line) + category = get_category(subject, '\n'.join(parsed_body)) + bug = ','.join(bugs) if bugs else 'None' + + commits.append({ + 'hash': commit_hash, + 'author': author, + 'subject': subject, + 'bug': bug, + 'review_url': review_url, + 'category': category, + 'body': parsed_body + }) + + return commits + + +def parse_log(log_text): + """Parses and filter git commits.""" + raw_commits = parse_git_commits(log_text) + + filtered_commits = [] + for commit in raw_commits: + if commit['subject'] == 'Update WebRTC code version': + continue + if commit['subject'].startswith('Roll '): + continue + exclude_prefixes = ('Revert', 'Reland', 'Reapply') + if commit['subject'].startswith(exclude_prefixes): + continue + if 'webrtc-version-updater' in commit['author']: + continue + filtered_commits.append(commit) + + return filtered_commits + + +def generate_html(commits, + from_branch, + to_branch, + milestone=None, + summary_text=None): + """Generates an HTML changelog from a list of commits.""" + authors = set(commit['author'] for commit in commits) + full_log_url = (f'https://webrtc.googlesource.com/src/+log/branch-heads/' + f'{from_branch}..branch-heads/{to_branch}') + schedule_url = 'https://chromiumdash.appspot.com/schedule' + cat_counts = collections.defaultdict(int) + for commit in commits: + cat_counts[commit['category']] += 1 + + ai_summary_html = (f'<div id="ai-summary">{summary_text}</div>' + if summary_text else + '<div id="ai-summary">AI_SUMMARY_PLACEHOLDER</div>') + + formatted_milestone = f'{milestone} ' if milestone else '' + html = [ + f"""<!DOCTYPE html> +<html> +<head> +<style> +{CHANGELOG_CSS} +</style> +</head> +<body> +<h1>WebRTC Changelog {formatted_milestone} ({from_branch}..{to_branch})</h1> +<div class="external-links"> + <a href="{full_log_url}">Full list of changes</a> + <a href="{schedule_url}">Chromium Release Schedule</a> +</div> +<div class="summary-container"> + <div class="overall-summary"> + <p> + This release contains {len(commits)} commits + by {len(authors)} authors. + </p> + <h2>Summary (AI-generated)</h2> + {ai_summary_html} + </div> + <div class="category-summary"> + <h2>Categories</h2> + <table> + <thead><tr><th>Category</th><th>Changes</th></tr></thead> + <tbody> +""" + ] + for cat, count in sorted(cat_counts.items()): + html.append(f'<tr><td>{cat}</td><td>{count}</td></tr>') + html.append("""</tbody></table></div></div> +<h2>Detailed List of Changes (newest first)</h2> +<table> + <thead> + <tr> + <th>Change Description</th> + <th>Category</th> + <th>Links</th> + <th>Bug</th> + </tr> + </thead> + <tbody> +""") + for commit in commits: + commit_link = (f'<a href="https://webrtc.googlesource.com/src/+/' + f'{commit["hash"]}">📝</a>') + review_link = (f'<a href="{commit["review_url"]}">🔍</a>' + if commit['review_url'] else '') + html.append( + f'<tr><td>{commit["subject"]}</td><td>{commit["category"]}</td>' + f'<td style="white-space:nowrap;">{commit_link} {review_link}</td>' + f'<td>{format_bugs(commit["bug"])}</td></tr>') + html.append('</tbody></table></body></html>') + return ''.join(html) + + +def main(): + parser = argparse.ArgumentParser( + description='Generate WebRTC changelog with optional AI summary.') + parser.add_argument('branch1', + help='First branch for comparison (e.g., 7727).') + parser.add_argument('branch2', + help='Second branch for comparison (e.g., 7778).') + parser.add_argument('output_file', help='Path to the output HTML file.') + parser.add_argument('--summary_text', + help='Optional AI-generated summary text to inject.', + default=None) + parser.add_argument('--milestone', + help='Optional milestone name (e.g. M148).', + default=None) + + args = parser.parse_args() + + try: + log_content = subprocess.check_output([ + 'git', 'log', '--format=%h %ae %s%n%b--WEBRTC-COMMIT-DELIMITER--', + f'branch-heads/{args.branch1}..branch-heads/{args.branch2}' + ]).decode('utf-8') + except subprocess.CalledProcessError as error: + print(f'Error running git log: {error}', file=sys.stderr) + sys.exit(1) + + commits = parse_log(log_content) + + if not commits: + print('No commits found in the specified range.') + sys.exit(0) + + branch_info = f'({args.branch1} to {args.branch2})' + if args.milestone: + branch_info = f'{args.milestone} {branch_info}' + + html = generate_html(commits, + from_branch=args.branch1, + to_branch=args.branch2, + milestone=args.milestone, + summary_text=args.summary_text) + + with open(args.output_file, 'w') as file_handle: + file_handle.write(html) + + print(f'Changelog written to {args.output_file}') + + +if __name__ == '__main__': + main()