Skip to content

Commit 9d779cc

Browse files
Add a script to automatically generate expanded release notes using an LLM (#3080)
* Prototype release notes generation using llm * feat: add argparse for dynamic release tag input in release_notes.py Co-authored-by: aider (bedrock/us.anthropic.claude-sonnet-4-20250514-v1:0) <aider@aider.chat> * Expand script description --------- Co-authored-by: aider (bedrock/us.anthropic.claude-sonnet-4-20250514-v1:0) <aider@aider.chat>
1 parent e46a12e commit 9d779cc

File tree

2 files changed

+225
-0
lines changed

2 files changed

+225
-0
lines changed

scripts/release_notes.py

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

scripts/release_notes_examples.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
## Run configurations
2+
3+
### Repo directory
4+
5+
It's now possible to specify the directory in the container where the repo is mounted:
6+
7+
```yaml
8+
type: dev-environment
9+
10+
ide: vscode
11+
12+
repos:
13+
- local_path: .
14+
path: my_repo
15+
16+
# or using short syntax:
17+
# - .:my_repo
18+
```
19+
20+
The `path` property can be an absolute path or a relative path (with respect to `working_dir`). It's available inside run as the `$DSTACK_REPO_DIR` environment variable. If `path` is not set, the `/workflow` path is used.
21+
22+
### Working directory
23+
24+
Previously, the `working_dir` property had complicated semantics: it defaulted to the repo path (`/workflow`), but for tasks and services without `commands`, the image working directory was used. You could also specify custom `working_dir` relative to the repo directory. This is now reversed: you specify `working_dir` as absolute path, and the repo path can be specified relative to it.
25+
26+
> [!NOTE]
27+
> During transitioning period, the legacy behavior of using `/workflow` is preserved if `working_dir` is not set. In future releases, this will be simplified, and `working_dir` will always default to the image working directory.
28+
29+
## Fleet configuration
30+
31+
### Nodes, retry, and target
32+
33+
`dstack` now indefinitely maintains `nodes.min` specified for cloud fleets. If instances get terminated for any reason and there are fewer instances than `nodes.min`, `dstack` will provision new fleet instances in the background.
34+
35+
There is also a new `nodes.target` property that specifies the number of instances to provision on fleet apply. Since now `nodes.min` is always maintained, you may specify `nodes.target` different from `nodes.min` to provision more instances than needs to be maintained.
36+
37+
Example:
38+
39+
```yaml
40+
type: fleet
41+
name: default-fleet
42+
nodes:
43+
min: 1 # Maintain one instance
44+
target: 2 # Provision two instances initially
45+
max: 3
46+
```
47+
48+
`dstack` will provision two instances. After deleting one instance, there will be one instances left. Deleting the last instance will trigger `dstack` to re-create the instance.
49+
50+
## Offers
51+
52+
The UI now has a dedicated page showing GPU offers available across all configured backends.
53+
54+
<img width="750" src="https://github.com/user-attachments/assets/827b56d9-2b92-43d0-bb27-a7e6926b1b80" />
55+
56+
## Digital Ocean and AMD Developer Cloud
57+
58+
The release adds native integration with [DigitalOcean](https://www.digitalocean.com/products/gradient/gpu-droplets) and
59+
[AMD Developer Cloud](https://www.amd.com/en/developer/resources/cloud-access/amd-developer-cloud.html).
60+
61+
A backend configuration example:
62+
63+
```yaml
64+
projects:
65+
- name: main
66+
backends:
67+
- type: amddevcloud
68+
project_name: TestProject
69+
creds:
70+
type: api_key
71+
api_key: ...
72+
```
73+
74+
For DigitalOcean, set `type` to `digitalocean`.
75+
76+
The `digitalocean` and `amddevcloud` backends support NVIDIA and AMD GPU VMs, respectively, and allow you to run
77+
[dev environments](../../docs/concepts/dev-environments.md) (interactive development), [tasks](../../docs/concepts/tasks.md)
78+
(training, fine-tuning, or other batch jobs), and [services](../../docs/concepts/services.md) (inference).
79+
80+
## Security
81+
82+
> [!IMPORTANT]
83+
> This update fixes a vulnerability in the `cloudrift`, `cudo`, and `datacrunch` backends. Instances created with earlier `dstack` versions lack proper firewall rules, potentially exposing internal APIs and allowing unauthorized access.
84+
>
85+
> Users of these backends are advised to update to the latest version and re-create any running instances.
86+
87+
## What's changed
88+
89+
* Minor Hot Aisle Cleanup by @Bihan in https://github.com/dstackai/dstack/pull/2978
90+
* UI for offers #3004 by @olgenn in https://github.com/dstackai/dstack/pull/3042
91+
* Add `repos[].path` property by @un-def in https://github.com/dstackai/dstack/pull/3041
92+
* style(frontend): Add missing final newline by @un-def in https://github.com/dstackai/dstack/pull/3044
93+
* Implement fleet state-spec consolidation to maintain `nodes.min` by @r4victor in https://github.com/dstackai/dstack/pull/3047
94+
* Add digital ocean and amd dev backend by @Bihan in https://github.com/dstackai/dstack/pull/3030
95+
* test: include amddevcloud and digitalocean in backend types by @Bihan in https://github.com/dstackai/dstack/pull/3053
96+
* Fix missing digitaloceanbase configurator methods by @Bihan in https://github.com/dstackai/dstack/pull/3055
97+
* Expose job working dir via environment variable by @un-def in https://github.com/dstackai/dstack/pull/3049
98+
* [runner] Ensure `working_dir` exists by @un-def in https://github.com/dstackai/dstack/pull/3052
99+
* Fix server compatibility with pre-0.19.27 runners by @un-def in https://github.com/dstackai/dstack/pull/3054
100+
* Bind shim and exposed container ports to localhost by @jvstme in https://github.com/dstackai/dstack/pull/3057
101+
* Fix client compatibility with pre-0.19.27 servers by @un-def in https://github.com/dstackai/dstack/pull/3063
102+
* [Docs] Reflect the repo and working directory changes (#3041) by @peterschmidt85 in https://github.com/dstackai/dstack/pull/3064
103+
* Show a CLI warning when using autocreated fleets by @r4victor in https://github.com/dstackai/dstack/pull/3060
104+
* Improve UX with private repos by @un-def in https://github.com/dstackai/dstack/pull/3065
105+
* Set up instance-level firewall on all backends by @jvstme in https://github.com/dstackai/dstack/pull/3058
106+
* Exclude target when equal to min for responses by @r4victor in https://github.com/dstackai/dstack/pull/3070
107+
* [Docs] Shorten the default `working_dir` warning by @peterschmidt85 in https://github.com/dstackai/dstack/pull/3072
108+
* Do not issue empty update for deleted_fleets_placement_groups by @r4victor in https://github.com/dstackai/dstack/pull/3071
109+
* Exclude target when equal to min for responses (attempt 2) by @r4victor in https://github.com/dstackai/dstack/pull/3074
110+
111+
112+
**Full changelog**: https://github.com/dstackai/dstack/compare/0.19.26...0.19.27

0 commit comments

Comments
 (0)