Skip to content

Commit a0f6042

Browse files
Add script to draft release notes from GitHub API
Add scripts/draft_release_notes.py, which calls the GitHub release-notes generation endpoint (via `gh`) to produce a PR list in the same format as the existing CHANGELOG file. It also prepends a heuristic draft summary paragraph that highlights user-visible features and counts bug fixes. The summary is marked with an HTML comment reminding the release manager to review and edit it before committing. Usage: scripts/draft_release_notes.py cbmc-<version> Also add a link to the CHANGELOG file in the release body generated by the regular-release workflow, and update doc/ADR/release_process.md to document the new script. Fixes: #7907 Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
1 parent 54feda7 commit a0f6042

File tree

3 files changed

+192
-2
lines changed

3 files changed

+192
-2
lines changed

.github/workflows/regular-release.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ jobs:
4040
draft: false
4141
prerelease: false
4242
body: |
43-
This is CBMC version ${{ env.CBMC_VERSION }}.
43+
This is CBMC version ${{ env.CBMC_VERSION }}. See [CHANGELOG](CHANGELOG) for what changed in this release.
4444
4545
## MacOS
4646

doc/ADR/release_process.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
\page release-process Release Process
22

33
**Date**: 2020-10-08
4-
**Updated**: 2023-03-29
4+
**Updated**: 2026-03-30
55
**Author**: Fotis Koutoulakis, fotis.koutoulakis@diffblue.com
66
**Domain**: Release & Packaging
77

@@ -47,6 +47,18 @@ anything more, but the process is described below for reference:
4747
described in the tag pushed (so, tag `cbmc-5.15.20` is going to
4848
create the release titled `cbmc-5.15.20` on the release page).
4949

50+
### Changelog / Release Notes
51+
52+
Before pushing the release tag, the release manager should update the
53+
`CHANGELOG` file at the repository root. A draft can be generated using:
54+
55+
scripts/draft_release_notes.py cbmc-<version>
56+
57+
This calls the GitHub release-notes API to produce a PR list in the same
58+
format already used in `CHANGELOG`, and prepends a draft summary paragraph.
59+
The summary is heuristic and must be reviewed and edited before committing.
60+
See `scripts/draft_release_notes.py --help` for options.
61+
5062
4. `.github/workflows/release-packages.yaml` gets triggered automatically
5163
at the creation of the release, and its job is to build packages for
5264
Windows, Ubuntu 18.04 and Ubuntu 20.04 (for now, we may support more

scripts/draft_release_notes.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Generate draft release notes for a CBMC release.
4+
5+
Calls the GitHub release-notes generation endpoint (the same one behind
6+
the "Generate release notes" button in the GitHub UI) and prepends a
7+
draft summary paragraph derived from the PR titles.
8+
9+
Usage:
10+
scripts/draft_release_notes.py cbmc-6.8.0
11+
scripts/draft_release_notes.py cbmc-6.8.0 --previous cbmc-6.7.1
12+
"""
13+
14+
import json
15+
import re
16+
import subprocess
17+
import sys
18+
import textwrap
19+
20+
21+
def gh_generate_notes(tag: str, previous: str, repo: str) -> str:
22+
"""Call the GitHub generate-notes API via `gh`."""
23+
cmd = [
24+
"gh", "api", f"repos/{repo}/releases/generate-notes",
25+
"-f", f"tag_name={tag}",
26+
"-f", f"previous_tag_name={previous}",
27+
]
28+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
29+
return json.loads(result.stdout)["body"]
30+
31+
32+
def previous_tag(tag: str, repo: str) -> str:
33+
"""Find the tag immediately before *tag* using `gh`."""
34+
cmd = [
35+
"gh", "api", f"repos/{repo}/tags",
36+
"--paginate", "-q", ".[].name",
37+
]
38+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
39+
tags = [t for t in result.stdout.splitlines() if t.startswith("cbmc-")]
40+
41+
def version_key(t):
42+
parts = t.split("-", 1)[1].split(".")
43+
nums = []
44+
for p in parts:
45+
m = re.match(r"(\d+)", p)
46+
nums.append(int(m.group(1)) if m else 0)
47+
return nums
48+
49+
tags.sort(key=version_key, reverse=True)
50+
for i, t in enumerate(tags):
51+
if t == tag and i + 1 < len(tags):
52+
return tags[i + 1]
53+
sys.exit(f"Cannot find a tag before {tag}")
54+
55+
56+
def version_from_tag(tag: str) -> str:
57+
return tag.split("-", 1)[1]
58+
59+
60+
# Patterns for changes that are NOT user-facing
61+
_SKIP = re.compile(
62+
r"(?i)"
63+
r"bump |dependabot|"
64+
r"\bCI\b|ci:|ci job|GitHub Action|runner|"
65+
r"Compile Java regression|"
66+
r"CODEOWNERS|"
67+
r"clang-format|"
68+
r"Release CBMC|"
69+
r"Merge pull request"
70+
)
71+
72+
# Patterns that suggest a user-visible feature (not just a fix/refactor)
73+
_FEATURE = re.compile(
74+
r"(?i)"
75+
r"\badd\b|"
76+
r"\bimplement\b|"
77+
r"\bintroduce\b|"
78+
r"\bsupport\b|"
79+
r"\bnew\b|"
80+
r"\benable\b"
81+
)
82+
83+
84+
def draft_summary(notes: str, version: str) -> str:
85+
"""Build a one-paragraph draft summary from the generated notes.
86+
87+
Strategy: pick the top user-visible feature PRs and mention them.
88+
This is a *draft* — the release manager should edit it.
89+
"""
90+
# Extract PR lines: "* <title> by @author in <url>"
91+
pr_lines = [
92+
line.strip()
93+
for line in notes.splitlines()
94+
if line.strip().startswith("* ")
95+
]
96+
97+
# Filter to user-facing changes
98+
visible = [l for l in pr_lines if not _SKIP.search(l)]
99+
features = [l for l in visible if _FEATURE.search(l)]
100+
fixes = [l for l in visible if l not in features]
101+
102+
# Extract short title + PR number for highlights
103+
def extract(line: str):
104+
m = re.match(r"\*\s+(.+?)\s+by\s+@", line)
105+
title = m.group(1) if m else line.lstrip("* ")
106+
m2 = re.search(r"/pull/(\d+)", line)
107+
pr = m2.group(1) if m2 else None
108+
return title, pr
109+
110+
highlights = []
111+
for line in (features or visible)[:3]:
112+
title, pr = extract(line)
113+
ref = f" (via #{pr})" if pr else ""
114+
highlights.append(f"{title}{ref}")
115+
116+
n_fixes = len(fixes)
117+
fix_note = ""
118+
if n_fixes:
119+
fix_note = (
120+
f" The release also includes {n_fixes} bug fix"
121+
f"{'es' if n_fixes != 1 else ''}."
122+
)
123+
124+
if not highlights:
125+
return f"<!-- TODO: write a summary for CBMC {version} -->\n"
126+
127+
if len(highlights) == 1:
128+
body = highlights[0]
129+
elif len(highlights) == 2:
130+
body = f"{highlights[0]} and {highlights[1]}"
131+
else:
132+
body = f"{highlights[0]}, {highlights[1]}, and {highlights[2]}"
133+
134+
return (
135+
f"<!-- DRAFT — please review and edit this summary -->\n"
136+
f"This release includes {body}.{fix_note}\n"
137+
)
138+
139+
140+
def format_release_notes(notes: str, version: str) -> str:
141+
"""Combine a header, draft summary, and the GitHub-generated body."""
142+
summary = textwrap.fill(draft_summary(notes, version), width=80)
143+
return f"# CBMC {version}\n\n{summary}\n\n{notes}\n"
144+
145+
146+
def main():
147+
import argparse
148+
p = argparse.ArgumentParser(
149+
description="Generate draft CHANGELOG entry for a CBMC release"
150+
)
151+
p.add_argument("tag", help="Release tag, e.g. cbmc-6.8.0")
152+
p.add_argument("--previous", help="Previous release tag (auto-detected)")
153+
p.add_argument("--repo", default="diffblue/cbmc")
154+
p.add_argument(
155+
"-o", "--output",
156+
help="Write to file instead of stdout",
157+
)
158+
args = p.parse_args()
159+
160+
prev = args.previous or previous_tag(args.tag, args.repo)
161+
ver = version_from_tag(args.tag)
162+
163+
print(f"Generating notes for {args.tag} (since {prev})...",
164+
file=sys.stderr)
165+
166+
notes = gh_generate_notes(args.tag, prev, args.repo)
167+
output = format_release_notes(notes, ver)
168+
169+
if args.output:
170+
with open(args.output, "w") as f:
171+
f.write(output)
172+
print(f"Written to {args.output}", file=sys.stderr)
173+
else:
174+
print(output)
175+
176+
177+
if __name__ == "__main__":
178+
main()

0 commit comments

Comments
 (0)