Skip to content

Commit

Permalink
support generating notes of merged PRs with rebase (#18458)
Browse files Browse the repository at this point in the history
## Description 

When merging a PR, we can either use `squashed` or `rebase`.
Before, our release note script supports `squashed` commit only, this
change will add support to `rebase` PR as well.
The downside of this is it might take a bit longer to run given that
we'll need to send a HTTP request to Github API if it's not a squashed
commit. But given this script won't be run more than once a week, it's a
good tradeoff.

## Test plan 

`GITHUB_TOKEN=<my_PAT> python ./scripts/release_notes.py generate
9e2be6a
1428c71`
(all the release notes from `releases/sui-v1.27.0-release` to
`releases/sui-v1.28.0-release` branch) now shows @bmwill 's
#18099

---

## Release notes

Check each box that your changes affect. If none of the boxes relate to
your changes, release notes aren't required.

For each box you select, include information after the relevant heading
that describes the impact of your changes that a user might notice and
any actions they must take to implement updates.

- [ ] Protocol: 
- [ ] Nodes (Validators and Full nodes): 
- [ ] Indexer: 
- [ ] JSON-RPC: 
- [ ] GraphQL: 
- [ ] CLI: 
- [ ] Rust SDK:
  • Loading branch information
pei-mysten authored Jun 28, 2024
1 parent b350ded commit 4044d7d
Showing 1 changed file with 55 additions and 17 deletions.
72 changes: 55 additions & 17 deletions scripts/release_notes.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import subprocess
import sys
from typing import NamedTuple
import urllib.request

RE_NUM = re.compile("[0-9_]+")

Expand Down Expand Up @@ -60,10 +61,12 @@
"Rust SDK",
]


class Note(NamedTuple):
checked: bool
note: str


def parse_args():
"""Parse command line arguments."""

Expand Down Expand Up @@ -116,7 +119,24 @@ def git(*args):
return subprocess.check_output(["git"] + list(args)).decode().strip()


def extract_notes(commit):
def extract_notes_from_rebase_commit(commit):
# we'll need to go one level deeper to find the PR number
url = f"https://api.github.com/repos/MystenLabs/sui/commits/{commit}/pulls"
headers = {
"Accept": "application/vnd.github.v3+json",
"Authorization": f"token {os.environ['GITHUB_TOKEN']}",
}
req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req) as response:
data = json.load(response)
if len(data) == 0:
return None, ""
pr_number = data[0]["number"]
pr_notes = data[0]["body"] if data[0]["body"] else ""
return pr_number, pr_notes


def extract_notes(commit, seen):
"""Get release notes from a commit message.
Find the 'Release notes' section in the commit message, and
Expand All @@ -130,19 +150,29 @@ def extract_notes(commit):
"""
message = git("show", "-s", "--format=%B", commit)

# Extract PR number
# Extract PR number from squashed commits
match = RE_PR.match(message)
pr = match.group(1) if match else None

result = {}

# Find the release notes section
match = RE_HEADING.search(message)
if not match:
notes = ""
if pr is None:
# Extract PR number from rebase commits if it's not a squashed commit
pr, notes = extract_notes_from_rebase_commit(commit)
else:
# Otherwise, find the release notes section from the squashed commit message
match = RE_HEADING.search(message)
if not match:
return pr, result
notes = match.group(1)

if pr in seen:
# a PR can be in multiple commits if it's from a rebase,
# so we only want to process it once
return pr, result

start = 0
notes = match.group(1)

while True:
# Find the next possible release note
match = RE_NOTE.search(notes, start)
Expand All @@ -158,8 +188,8 @@ def extract_notes(commit):
end = match.start() if match else len(notes)

result[impacted] = Note(
checked = checked in 'xX',
note = notes[begin:end].strip(),
checked=checked in "xX",
note=notes[begin:end].strip(),
)
start = end

Expand All @@ -170,7 +200,9 @@ def extract_protocol_version(commit):
"""Find the max protocol version at this commit.
Assumes that it is being called from the root of the sui repository."""
for line in git("show", f"{commit}:crates/sui-protocol-config/src/lib.rs").splitlines():
for line in git(
"show", f"{commit}:crates/sui-protocol-config/src/lib.rs"
).splitlines():
if "const MAX_PROTOCOL_VERSION" not in line:
continue

Expand Down Expand Up @@ -200,7 +232,7 @@ def do_check(commit):
"""

_, notes = extract_notes(commit)
_, notes = extract_notes(commit, set())
issues = []
for impacted, note in notes.items():
if impacted not in NOTE_ORDER:
Expand All @@ -210,7 +242,9 @@ def do_check(commit):
issues.append(f" - '{impacted}' is checked but has no release note.")

if not note.checked and note.note:
issues.append(f" - '{impacted}' has a release note but is not checked: {note.note}")
issues.append(
f" - '{impacted}' has a release note but is not checked: {note.note}"
)

if not issues:
return
Expand Down Expand Up @@ -243,16 +277,20 @@ def do_generate(from_, to):
protocol_version = extract_protocol_version(to) or "XX"

commits = git(
"log", "--pretty=format:%H",
"log",
"--pretty=format:%H",
f"{from_}..{to}",
"--", *INTERESTING_DIRECTORIES,
).strip();
"--",
*INTERESTING_DIRECTORIES,
).strip()

if not commits:
return

seen_prs = set()
for commit in commits.split("\n"):
pr, notes = extract_notes(commit)
pr, notes = extract_notes(commit, seen_prs)
seen_prs.add(pr)
for impacted, note in notes.items():
if note.checked:
results[impacted].append((pr, note.note))
Expand Down Expand Up @@ -282,7 +320,7 @@ def do_generate(from_, to):


args = parse_args()
if args["command"]== "generate":
if args["command"] == "generate":
do_generate(args["from"], args["to"])
elif args["command"] == "check":
do_check(args["commit"])

0 comments on commit 4044d7d

Please sign in to comment.