Update relnotes scripts.
1. Update relnotes.sh to use relnotes.py to generate release notes if not a rolling release
2. Update relnotes.py:
- Include co-authors in acknowledgements
- Make sure older releases work with the correct "last" release
- For minor/patch release, use commit message if no RELNOTES found
- Add option to sort relnotes by category
- Other minor fixes/updates
PiperOrigin-RevId: 531628752
Change-Id: Ifdcf55d319e7221b7ed4eb7e510a3c4c9a88b41d
diff --git a/scripts/release/relnotes.py b/scripts/release/relnotes.py
index 838d1f2..15feff2 100644
--- a/scripts/release/relnotes.py
+++ b/scripts/release/relnotes.py
@@ -14,25 +14,20 @@
"""Script to generate release notes."""
+import os
import re
import subprocess
-
+import sys
import requests
-def get_last_release():
- """Discovers the last stable release name from GitHub."""
- response = requests.get("https://github.com/bazelbuild/bazel/releases/latest")
- return response.url.split("/")[-1]
-
-
def git(*args):
"""Runs git as a subprocess, and returns its stdout as a list of lines."""
return subprocess.check_output(["git"] +
list(args)).decode("utf-8").strip().split("\n")
-def extract_relnotes(commit_message_lines):
+def extract_relnotes(commit_message_lines, is_major_release):
"""Extracts relnotes from a commit message (passed in as a list of lines)."""
relnote_lines = []
in_relnote = False
@@ -50,14 +45,27 @@
relnote_lines.append(line)
relnote = " ".join(relnote_lines)
relnote_lower = relnote.strip().lower().rstrip(".")
- if relnote_lower == "n/a" or relnote_lower == "none":
- return None
+ if relnote_lower == "n/a" or relnote_lower == "none" or not relnote_lower:
+ if is_major_release:
+ return None
+ relnote = re.sub(
+ r"\[\d+\.\d+\.\d\]\s?", "", commit_message_lines[0].strip()
+ )
+ else:
+ issue_id = re.search(
+ r"\(\#[0-9]+\)$", commit_message_lines[0].strip().split()[-1]
+ )
+ if issue_id:
+ relnote = relnote + " " + issue_id.group(0).strip()
+
return relnote
-def get_relnotes_between(base, head):
+def get_relnotes_between(base, head, is_major_release):
"""Gets all relnotes for commits between `base` and `head`."""
- commits = git("rev-list", f"{base}..{head}", "--grep=RELNOTES")
+ commits = git("rev-list", f"{base}..{head}")
+ if commits == [""]:
+ return []
relnotes = []
rolled_back_commits = set()
# We go in reverse-chronological order, so that we can identify rollback
@@ -71,36 +79,117 @@
rolled_back_commits.add(m[1])
# The rollback commit itself is also skipped.
continue
- relnote = extract_relnotes(lines)
+ relnote = extract_relnotes(lines, is_major_release)
if relnote is not None:
relnotes.append(relnote)
return relnotes
+def get_label(issue_id):
+ """Get team-X label added to issue."""
+ auth = os.system(
+ "gsutil cat"
+ " gs://bazel-trusted-encrypted-secrets/github-trusted-token.enc |"
+ " gcloud kms decrypt --project bazel-public --location global"
+ " --keyring buildkite --key github-trusted-token --ciphertext-file"
+ " - --plaintext-file -"
+ )
+ headers = {
+ "Authorization": "Bearer " + auth,
+ "Accept": "application/vnd.github+json",
+ }
+ response = requests.get(
+ "https://api.github.com/repos/bazelbuild/bazel/issues/"
+ + issue_id + "/labels", headers=headers,
+ )
+ for item in response.json():
+ for key, value in item.items():
+ if key == "name" and "team-" in value:
+ return value.strip()
+ return None
+
+
+def get_categorized_relnotes(filtered_notes):
+ """Sort release notes by category."""
+ categorized_relnotes = {}
+ for relnote in filtered_notes:
+ issue_id = re.search(r"\(\#[0-9]+\)$", relnote.strip().split()[-1])
+ category = None
+ if issue_id:
+ category = get_label(re.sub(r"\(|\#|\)", "", issue_id.group(0).strip()))
+
+ if category is None:
+ category = "General"
+ else:
+ category = re.sub("team-", "", category)
+
+ try:
+ categorized_relnotes[category].append(relnote)
+ except KeyError:
+ categorized_relnotes[category] = [relnote]
+
+ return dict(sorted(categorized_relnotes.items()))
+
+
def get_external_authors_between(base, head):
"""Gets all external authors for commits between `base` and `head`."""
+
+ # Get all authors
authors = git("log", f"{base}..{head}", "--format=%aN|%aE")
- authors = set(author.partition("|")[0].rstrip() for author in authors
- if not author.endswith("@google.com"))
- return ", ".join(sorted(authors, key=str.casefold))
+ authors = set(
+ author.partition("|")[0].rstrip()
+ for author in authors
+ if not (author.endswith(("@google.com", "@users.noreply.github.com")))
+ )
+
+ # Get all co-authors
+ contributors = git(
+ "log", f"{base}..{head}", "--format=%(trailers:key=Co-authored-by)"
+ )
+
+ coauthors = []
+ for coauthor in contributors:
+ if coauthor and not re.search(
+ "@google.com|@users.noreply.github.com", coauthor
+ ):
+ coauthors.append(
+ " ".join(re.sub(r"Co-authored-by: |<.*?>", "", coauthor).split())
+ )
+ return ", ".join(sorted(authors.union(coauthors), key=str.casefold))
if __name__ == "__main__":
- # Get the last stable release.
- last_release = get_last_release()
- print("last_release is", last_release)
- git("fetch", "origin", f"refs/tags/{last_release}:refs/tags/{last_release}")
+ # Get last release and make sure it's consistent with current X.Y.Z release
+ # e.g. if current_release is 5.3.3, last_release should be 5.3.2 even if
+ # latest release is 6.1.1
+ current_release = git("rev-parse", "--abbrev-ref", "HEAD")
+ current_release = re.sub(
+ r"rc\d", "", current_release[0].removeprefix("release-")
+ )
+
+ is_major = bool(re.fullmatch(r"\d+.0.0", current_release))
+
+ tags = [tag for tag in git("tag", "--sort=refname") if "pre" not in tag]
+ if current_release not in tags:
+ tags.append(current_release)
+ tags.sort()
+ last_release = tags[tags.index(current_release) - 1]
+ else:
+ print("Error: release tag already exists")
+ sys.exit(1)
# Assuming HEAD is on the current (to-be-released) release, find the merge
# base with the last release so that we know which commits to generate notes
# for.
merge_base = git("merge-base", "HEAD", last_release)[0]
- print("merge base with", last_release, "is", merge_base)
+ print("Baseline: ", merge_base)
# Generate notes for all commits from last branch cut to HEAD, but filter out
# any identical notes from the previous release branch.
- cur_release_relnotes = get_relnotes_between(merge_base, "HEAD")
- last_release_relnotes = set(get_relnotes_between(merge_base, last_release))
+ cur_release_relnotes = get_relnotes_between(merge_base, "HEAD", is_major)
+ last_release_relnotes = set(
+ get_relnotes_between(merge_base, last_release, is_major)
+ )
filtered_relnotes = [
note for note in cur_release_relnotes if note not in last_release_relnotes
]
@@ -108,13 +197,22 @@
# Reverse so that the notes are in chronological order.
filtered_relnotes.reverse()
print()
- print()
- for note in filtered_relnotes:
- print("*", note)
+ print("Release Notes:")
+
+ if len(sys.argv) >= 2 and sys.argv[1] == "sort":
+ print()
+ categorized_release_notes = get_categorized_relnotes(filtered_relnotes)
+ for label in categorized_release_notes:
+ print(label + ":")
+ for note in categorized_release_notes[label]:
+ print("+", note)
+ print()
+ else:
+ for note in filtered_relnotes:
+ print("+", note)
print()
- print()
+ print("Acknowledgements:")
external_authors = get_external_authors_between(merge_base, "HEAD")
- print(
- "This release contains contributions from many people at Google, "
- f"as well as {external_authors}.")
+ print("This release contains contributions from many people at Google" +
+ ("." if not external_authors else f", as well as {external_authors}."))