-
-
Notifications
You must be signed in to change notification settings - Fork 222
Expand file tree
/
Copy pathrelease_notes.py
More file actions
113 lines (92 loc) · 3.73 KB
/
release_notes.py
File metadata and controls
113 lines (92 loc) · 3.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# /// script
# dependencies = [
# "requests",
# "litellm",
# "boto3", # for AWS Bedrock
# ]
# ///
import argparse
import os
import re
from pathlib import Path
import requests
from litellm import completion
REPO = "dstackai/dstack"
BRANCH = "master"
# GITHUB_TOKEN to avoid rate limiting
GITHUB_TOKEN = os.environ["GITHUB_TOKEN"]
# Model can be any supported by LiteLLM: https://docs.litellm.ai/docs/providers
# Prover-specific credentials are picked up from env
# e.g. AWS_REGION=us-east-1 AWS_PROFILE=... for AWS Bedrock
MODEL = os.getenv("LLM_MODEL", "bedrock/us.anthropic.claude-sonnet-4-20250514-v1:0")
def get_draft_release_by_tag(tag: str) -> dict:
r = requests.get(
f"https://api.github.com/repos/{REPO}/releases",
headers={"Authorization": f"token {GITHUB_TOKEN}"},
timeout=10,
)
for release in r.json():
if release["tag_name"] == tag and release["draft"]:
return release
# May error if the draft not on the first page - we assume draft was created recently
raise ValueError(f"Release for tag {tag} not found")
def get_prs_from_draft(draft_body: str) -> list[dict]:
prs = []
pr_numbers = extract_pr_numbers_from_draft(draft_body)
for pr_number in pr_numbers:
r = requests.get(
f"https://api.github.com/repos/{REPO}/pulls/{pr_number}",
headers={"Authorization": f"token {GITHUB_TOKEN}"},
timeout=10,
)
prs.append(r.json())
return prs
def extract_pr_numbers_from_draft(notes: str) -> list[int]:
return [int(num) for num in re.findall(r"/pull/(\d+)", notes)]
def generate_release_notes(
draft_body: str,
prs: list[dict],
examples: str,
) -> str:
pr_summaries = "\n\n".join(f"PR #{pr['number']}: {pr['title']}\n{pr['body']}" for pr in prs)
prompt = f"""
You are a release notes generator.
Here are the draft GitHub release notes:
{draft_body}
Here are the PR details (titles + descriptions):
{pr_summaries}
Task:
* Keep the 'What's Changed' and 'Contributors' sections as they are.
* Add expanded sections in the beginning for major features and changes. Do not mention minor fixes.
* Use clear, user-friendly prose. Avoid emojis.
* Use the PR descriptions to enrich the expanded sections.
* Include examples of how to use the new features when they are available in the PR descriptions.
* Do not group sections based on functionality (like "New features"). Instead, group by domain (e.g. "Runs", "Backends", "Examples") or do not group at all.
* Include "Deprecations" and "Breaking changes" sections if there are any.
Examples of good release notes:
{examples}
"""
response = completion(
model=MODEL,
messages=[{"role": "user", "content": prompt}],
)
return response["choices"][0]["message"]["content"]
if __name__ == "__main__":
# TODO: When the script is sufficiently polished, we may automate draft release generation and its update,
# and integrate the script into the CI.
parser = argparse.ArgumentParser(
description=(
"Generate expanded `dstack` release notes from a release draft using LLM."
" The script accepts a release tag for which you must generate automatic release notes beforehand."
" The script does not publish or change anything on GitHub and only outputs the generated release notes."
)
)
parser.add_argument("tag", help="Release tag (e.g., 0.19.25)")
args = parser.parse_args()
with open(Path(__file__).parent / "release_notes_examples.md") as f:
examples = f.read()
draft_release = get_draft_release_by_tag(args.tag)
draft_body = draft_release["body"]
prs = get_prs_from_draft(draft_body)
notes = generate_release_notes(draft_body, prs, examples)
print(notes)