Skip to content
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 Apr 23, 2026
c21db4d
chore: apply regex escaping and stderr logging improvements to releas…
meltsufin Apr 23, 2026
dffcd62
chore: add golden file tests for release note generator
meltsufin Apr 23, 2026
9580a3d
chore: remove --first-parent flag globally in release note generator
meltsufin Apr 23, 2026
678c018
chore: add advanced pom.xml history fallback to release note generator
meltsufin Apr 24, 2026
4e53193
chore: update release note formatting and golden files
meltsufin Apr 24, 2026
6223f84
docs(spanner): backfill missing release notes for v6.113.0 to v6.116.1
meltsufin Apr 24, 2026
bcdd6d3
docs(spanner): backfill missing release notes with strict lifecycle rule
meltsufin Apr 24, 2026
45c2c44
fix: suppress noisy logs and update goldens for release note generator
meltsufin Apr 24, 2026
bc7af1e
docs(spanner): update changelog with backfilled release notes
meltsufin Apr 24, 2026
ed56fd7
docs(bigquery): update changelog with backfilled release notes
meltsufin Apr 24, 2026
821bbd0
fix: suppress noisy logs in helper function for release note generator
meltsufin Apr 25, 2026
7eb4135
docs(bigquery): update changelog with backfilled release notes
meltsufin Apr 25, 2026
0ed86c9
fix: finalize release note generator and update goldens and Spanner c…
meltsufin Apr 25, 2026
a656a6c
docs(spanner-jdbc): update changelog with backfilled release notes
meltsufin Apr 25, 2026
2220fc1
fix: handle multiple tags in release note generator
meltsufin Apr 25, 2026
87ff110
Merge branch 'main' into impl/release-note-generator
meltsufin Apr 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
278 changes: 278 additions & 0 deletions .github/release-note-generation/generate_module_notes.py
Copy link
Copy Markdown
Contributor

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

Copy link
Copy Markdown
Member Author

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.

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}:",
Comment thread
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}:([^:]+):([^:]+)$")
Comment thread
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}")
Comment thread
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}")
Comment thread
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}"
)
Comment thread
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}",
Comment thread
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()
Loading