-
Notifications
You must be signed in to change notification settings - Fork 1.1k
chore: add script to generate release notes based on commit history #12900
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
b38cdb5
chore: add script to generate release notes based on commit history
meltsufin c21db4d
chore: apply regex escaping and stderr logging improvements to releas…
meltsufin dffcd62
chore: add golden file tests for release note generator
meltsufin 9580a3d
chore: remove --first-parent flag globally in release note generator
meltsufin 678c018
chore: add advanced pom.xml history fallback to release note generator
meltsufin 4e53193
chore: update release note formatting and golden files
meltsufin 6223f84
docs(spanner): backfill missing release notes for v6.113.0 to v6.116.1
meltsufin bcdd6d3
docs(spanner): backfill missing release notes with strict lifecycle rule
meltsufin 45c2c44
fix: suppress noisy logs and update goldens for release note generator
meltsufin bc7af1e
docs(spanner): update changelog with backfilled release notes
meltsufin ed56fd7
docs(bigquery): update changelog with backfilled release notes
meltsufin 821bbd0
fix: suppress noisy logs in helper function for release note generator
meltsufin 7eb4135
docs(bigquery): update changelog with backfilled release notes
meltsufin 0ed86c9
fix: finalize release note generator and update goldens and Spanner c…
meltsufin a656a6c
docs(spanner-jdbc): update changelog with backfilled release notes
meltsufin 2220fc1
fix: handle multiple tags in release note generator
meltsufin 87ff110
Merge branch 'main' into impl/release-note-generator
meltsufin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
278 changes: 278 additions & 0 deletions
278
.github/release-note-generation/generate_module_notes.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,278 @@ | ||
| import argparse | ||
| import re | ||
| import subprocess | ||
| import sys | ||
|
|
||
|
|
||
| def run_cmd(cmd, cwd=None): | ||
| """Runs a shell command and returns the output.""" | ||
| result = subprocess.run( | ||
| cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=cwd | ||
| ) | ||
| if result.returncode != 0: | ||
| print(f"Error running command: {' '.join(cmd)}", file=sys.stderr) | ||
| print(result.stderr, file=sys.stderr) | ||
| sys.exit(result.returncode) | ||
| return result.stdout | ||
|
|
||
|
|
||
| def main(): | ||
| parser = argparse.ArgumentParser( | ||
| description="Generate release notes based on commit history for a specific module." | ||
| ) | ||
| parser.add_argument( | ||
| "--module", required=True, help="Module name as specified in versions.txt" | ||
| ) | ||
| parser.add_argument( | ||
| "--directory", required=True, help="Path in the monorepo where the module has code" | ||
| ) | ||
| parser.add_argument("--version", required=True, help="Target version") | ||
| parser.add_argument( | ||
| "--short-name", help="Module short-name used in commit overrides (e.g., aiplatform). Omit for repo-wide generation." | ||
| ) | ||
| args = parser.parse_args() | ||
|
|
||
| module = args.module | ||
| directory = args.directory | ||
| target_version = args.version | ||
|
|
||
| # 1. Scan backwards through git history of versions.txt | ||
| # We use -G to find commits that modified lines matching the module name. | ||
| # We use --first-parent to ignore merge noise. | ||
| log_cmd = [ | ||
| "git", | ||
| "log", | ||
| "--oneline", | ||
| "--first-parent", | ||
| f"-G^{module}:", | ||
|
meltsufin marked this conversation as resolved.
Outdated
|
||
| "--", | ||
| "versions.txt", | ||
| ] | ||
| log_output = run_cmd(log_cmd) | ||
|
|
||
| commits = [line.split()[0] for line in log_output.splitlines() if line] | ||
|
|
||
| target_commit = None | ||
| prev_commit = None | ||
| prev_version = None | ||
|
|
||
| for commit in commits: | ||
| # Get content of versions.txt at this commit | ||
| show_cmd = ["git", "show", f"{commit}:versions.txt"] | ||
| try: | ||
| content = run_cmd(show_cmd) | ||
| except SystemExit: | ||
| continue # Ignore errors if file couldn't be read | ||
|
|
||
| # Find the line for the module | ||
| pattern = re.compile(rf"^{module}:([^:]+):([^:]+)$") | ||
|
meltsufin marked this conversation as resolved.
Outdated
|
||
| for line in content.splitlines(): | ||
| match = pattern.match(line) | ||
| if match: | ||
| released_ver = match.group(1) | ||
| current_ver = match.group(2) | ||
|
|
||
| # Condition for target version | ||
| if released_ver == target_version and not target_commit: | ||
| target_commit = commit | ||
| print(f"Found target version {target_version} at {commit}") | ||
|
meltsufin marked this conversation as resolved.
Outdated
|
||
|
|
||
| # Condition for previous non-snapshot version | ||
| # We ignore snapshot versions by checking both fields. | ||
| elif ( | ||
| target_commit | ||
| and released_ver != target_version | ||
| and "-SNAPSHOT" not in released_ver | ||
| and "-SNAPSHOT" not in current_ver | ||
| ): | ||
| prev_commit = commit | ||
| prev_version = released_ver | ||
| print(f"Found previous version {released_ver} at {commit}") | ||
| break | ||
| if prev_commit: | ||
| break | ||
|
|
||
| if not target_commit: | ||
| print( | ||
| f"Target version {target_version} not found in history for module {module}." | ||
| ) | ||
| sys.exit(1) | ||
|
|
||
| # Fallback for initial version if no previous version found | ||
| if not prev_commit: | ||
| print( | ||
| f"Previous version not found in history for module {module}." | ||
| ) | ||
| # Find the first commit affecting that directory | ||
| first_commit_cmd = [ | ||
| "git", | ||
| "log", | ||
| "--reverse", | ||
| "--oneline", | ||
| "--first-parent", | ||
| "--", | ||
| directory, | ||
| ] | ||
| try: | ||
| first_commit_output = run_cmd(first_commit_cmd) | ||
| if first_commit_output: | ||
| prev_commit = first_commit_output.splitlines()[0].split()[0] | ||
| print(f"Using first commit affecting directory as base: {prev_commit}") | ||
|
meltsufin marked this conversation as resolved.
Outdated
|
||
| else: | ||
| print(f"No history found for directory {directory}.") | ||
| sys.exit(1) | ||
| except SystemExit: | ||
| sys.exit(1) | ||
|
|
||
| print( | ||
| f"Generating notes between {prev_commit} and {target_commit} for directory {directory}" | ||
| ) | ||
|
meltsufin marked this conversation as resolved.
|
||
|
|
||
| # 2. Generate commit history in that range affecting that directory | ||
| # Use --first-parent to ignore merge noise. | ||
| # Use format that includes hash, subject, and body | ||
| notes_cmd = [ | ||
| "git", | ||
| "log", | ||
| "--format=%H %s%n%b%n--END_OF_COMMIT--", | ||
| "--first-parent", | ||
| f"{prev_commit}..{target_commit}", | ||
|
meltsufin marked this conversation as resolved.
Outdated
|
||
| "--", | ||
| directory, | ||
| ] | ||
| notes_output = run_cmd(notes_cmd) | ||
|
|
||
| # Filter commit titles based on allowed prefixes and categorize them | ||
| # Supports scopes in parentheses, e.g., feat(spanner): | ||
| prefix_regex = re.compile(r"^(feat|fix|deps|docs)(\([^)]+\))?(!)?:") | ||
|
|
||
| breaking_changes = [] | ||
| features = [] | ||
| bug_fixes = [] | ||
| dependency_upgrades = [] | ||
| documentation = [] | ||
|
|
||
| def categorize_and_append(commit_hash, text): | ||
| match = prefix_regex.match(text) | ||
| if not match: | ||
| return | ||
|
|
||
| prefix = match.group(1) | ||
| is_breaking = match.group(3) == "!" | ||
|
|
||
| if is_breaking: | ||
| breaking_changes.append(f"{commit_hash[:11]} {text}") | ||
| elif prefix == "feat": | ||
| features.append(f"{commit_hash[:11]} {text}") | ||
| elif prefix == "fix": | ||
| bug_fixes.append(f"{commit_hash[:11]} {text}") | ||
| elif prefix == "deps": | ||
| dependency_upgrades.append(f"{commit_hash[:11]} {text}") | ||
| elif prefix == "docs": | ||
| documentation.append(f"{commit_hash[:11]} {text}") | ||
|
|
||
| commits_data = notes_output.split("--END_OF_COMMIT--") | ||
|
|
||
| for commit_data in commits_data: | ||
| commit_data = commit_data.strip() | ||
| if not commit_data: | ||
| continue | ||
|
|
||
| lines = commit_data.splitlines() | ||
| if not lines: | ||
| continue | ||
|
|
||
| header_parts = lines[0].split(" ", 1) | ||
| commit_hash = header_parts[0] | ||
| subject = header_parts[1] if len(header_parts) > 1 else "" | ||
|
|
||
| body = "\n".join(lines[1:]) | ||
|
|
||
| # Check for override in the entire message | ||
| if "BEGIN_COMMIT_OVERRIDE" in body or "BEGIN_COMMIT_OVERRIDE" in subject: | ||
| match = re.search(r"BEGIN_COMMIT_OVERRIDE(.*?)END_COMMIT_OVERRIDE", commit_data, re.DOTALL) | ||
| if match: | ||
| override_content = match.group(1) | ||
| current_item = [] | ||
| in_module_item = False | ||
|
|
||
| for line in override_content.splitlines(): | ||
| line_stripped = line.strip() | ||
| if not line_stripped: | ||
| continue | ||
|
|
||
| # Check if it's a new item using regex | ||
| is_new_item = prefix_regex.match(line_stripped) | ||
|
|
||
| if is_new_item: | ||
| # If we were in an item, save it | ||
| if in_module_item and current_item: | ||
| categorize_and_append(commit_hash, " ".join(current_item)) | ||
| current_item = [] | ||
| in_module_item = False | ||
|
|
||
| # Check if this new item is for our module or if we want all | ||
| should_include = False | ||
| if args.short_name: | ||
| if f"[{args.short_name}]" in line_stripped: | ||
| should_include = True | ||
| else: | ||
| should_include = True | ||
|
|
||
| if should_include: | ||
| in_module_item = True | ||
| current_item.append(line_stripped) | ||
| elif in_module_item: | ||
| # Continuation line | ||
| if line_stripped.startswith(("PiperOrigin-RevId:", "Source Link:")): | ||
| continue | ||
| if line_stripped in ("END_NESTED_COMMIT", "BEGIN_NESTED_COMMIT"): | ||
| continue | ||
| current_item.append(line_stripped) | ||
|
|
||
| # Save the last item if we were in one | ||
| if in_module_item and current_item: | ||
| categorize_and_append(commit_hash, " ".join(current_item)) | ||
|
|
||
| # Ignore the title since there was an override | ||
| continue | ||
|
|
||
| # Fallback to title check if no override | ||
| if prefix_regex.match(subject): | ||
| categorize_and_append(commit_hash, subject) | ||
|
|
||
| print("\nRelease Notes:") | ||
| if breaking_changes: | ||
| print("### ⚠ BREAKING CHANGES\n") | ||
| for item in breaking_changes: | ||
| print(f"* {item}") | ||
| print() | ||
|
|
||
| if features: | ||
| print("### Features\n") | ||
| for item in features: | ||
| print(f"* {item}") | ||
| print() | ||
|
|
||
| if bug_fixes: | ||
| print("### Bug Fixes\n") | ||
| for item in bug_fixes: | ||
| print(f"* {item}") | ||
| print() | ||
|
|
||
| if dependency_upgrades: | ||
| print("### Dependencies\n") | ||
| for item in dependency_upgrades: | ||
| print(f"* {item}") | ||
| print() | ||
|
|
||
| if documentation: | ||
| print("### Documentation\n") | ||
| for item in documentation: | ||
| print(f"* {item}") | ||
| print() | ||
|
|
||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we expect this script to replace the existing script split_release_note.py and used in the automation changelog_generation.yaml
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could potentially, but for now, it's meant just for backfilling.
It has specific logic for dealing with the complex commit history resulting from split repo migrations. That won't be relevant in the future.