blob: 838d1f2931fe3778c0ce298d6948fcab890d6f81 [file] [log] [blame]
# Copyright 2022 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Script to generate release notes."""
import re
import subprocess
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):
"""Extracts relnotes from a commit message (passed in as a list of lines)."""
relnote_lines = []
in_relnote = False
for line in commit_message_lines:
if not line or line.startswith("PiperOrigin-RevId:"):
in_relnote = False
m = re.match(r"^RELNOTES(?:\[(INC|NEW)\])?:", line)
if m is not None:
in_relnote = True
line = line[len(m[0]):]
if m[1] == "INC":
line = "**[Incompatible]** " + line.strip()
line = line.strip()
if in_relnote and line:
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
return relnote
def get_relnotes_between(base, head):
"""Gets all relnotes for commits between `base` and `head`."""
commits = git("rev-list", f"{base}..{head}", "--grep=RELNOTES")
relnotes = []
rolled_back_commits = set()
# We go in reverse-chronological order, so that we can identify rollback
# commits and ignore the rolled-back commits.
for commit in commits:
if commit in rolled_back_commits:
continue
lines = git("show", "-s", commit, "--pretty=format:%B")
m = re.match(r"^Automated rollback of commit ([\dA-Fa-f]+)", lines[0])
if m is not None:
rolled_back_commits.add(m[1])
# The rollback commit itself is also skipped.
continue
relnote = extract_relnotes(lines)
if relnote is not None:
relnotes.append(relnote)
return relnotes
def get_external_authors_between(base, head):
"""Gets all external authors for commits between `base` and `head`."""
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))
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}")
# 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)
# 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))
filtered_relnotes = [
note for note in cur_release_relnotes if note not in last_release_relnotes
]
# Reverse so that the notes are in chronological order.
filtered_relnotes.reverse()
print()
print()
for note in filtered_relnotes:
print("*", note)
print()
print()
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}.")