Skip to content

Commit 576f506

Browse files
bors[bot]lnicola
andauthored
Merge #8605
8605: internal: Automatically categorize the changelog entries r=matklad a=lnicola Co-authored-by: Laurențiu Nicola <lnicola@dend.ro>
2 parents 0bb074a + 39ce393 commit 576f506

File tree

3 files changed

+173
-43
lines changed

3 files changed

+173
-43
lines changed

docs/dev/README.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -208,20 +208,26 @@ Release process is handled by `release`, `dist` and `promote` xtasks, `release`
208208

209209
Additionally, it assumes that remote for `rust-analyzer` is called `upstream` (I use `origin` to point to my fork).
210210

211+
`release` calls the GitHub API calls to scrape pull request comments and categorize them in the changelog.
212+
This step uses the `curl` and `jq` applications, which need to be available in `PATH`.
213+
Finally, you need to obtain a GitHub personal access token and set the `GITHUB_TOKEN` environment variable.
214+
211215
Release steps:
212216

213-
1. Inside rust-analyzer, run `cargo xtask release`. This will:
217+
1. Set the `GITHUB_TOKEN` environment variable.
218+
2. Inside rust-analyzer, run `cargo xtask release`. This will:
214219
* checkout the `release` branch
215220
* reset it to `upstream/nightly`
216221
* push it to `upstream`. This triggers GitHub Actions which:
217222
* runs `cargo xtask dist` to package binaries and VS Code extension
218223
* makes a GitHub release
219224
* pushes VS Code extension to the marketplace
220-
* create new changelog in `rust-analyzer.github.io`
221-
2. While the release is in progress, fill in the changelog
222-
3. Commit & push the changelog
223-
4. Tweet
224-
5. Inside `rust-analyzer`, run `cargo xtask promote` -- this will create a PR to rust-lang/rust updating rust-analyzer's submodule.
225+
* call the GitHub API for PR details
226+
* create a new changelog in `rust-analyzer.github.io`
227+
3. While the release is in progress, fill in the changelog
228+
4. Commit & push the changelog
229+
5. Tweet
230+
6. Inside `rust-analyzer`, run `cargo xtask promote` -- this will create a PR to rust-lang/rust updating rust-analyzer's submodule.
225231
Self-approve the PR.
226232

227233
If the GitHub Actions release fails because of a transient problem like a timeout, you can re-run the job from the Actions console.

xtask/src/release.rs

Lines changed: 2 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::fmt::Write;
1+
mod changelog;
22

33
use xshell::{cmd, cp, pushd, read_dir, write_file};
44

@@ -38,42 +38,7 @@ impl flags::Release {
3838
let tags = cmd!("git tag --list").read()?;
3939
let prev_tag = tags.lines().filter(|line| is_release_tag(line)).last().unwrap();
4040

41-
let git_log = cmd!("git log {prev_tag}..HEAD --merges --reverse").read()?;
42-
let mut git_log_summary = String::new();
43-
for line in git_log.lines() {
44-
let line = line.trim_start();
45-
if let Some(p) = line.find(':') {
46-
if let Ok(pr) = line[..p].parse::<u32>() {
47-
writeln!(git_log_summary, "* pr:{}[]{}", pr, &line[p + 1..]).unwrap();
48-
}
49-
}
50-
}
51-
52-
let contents = format!(
53-
"\
54-
= Changelog #{}
55-
:sectanchors:
56-
:page-layout: post
57-
58-
Commit: commit:{}[] +
59-
Release: release:{}[]
60-
61-
== Sponsors
62-
63-
**Become a sponsor:** On https://opencollective.com/rust-analyzer/[OpenCollective] or
64-
https://github.com/sponsors/rust-analyzer[GitHub Sponsors].
65-
66-
== New Features
67-
68-
{}
69-
70-
== Fixes
71-
72-
== Internal Improvements
73-
",
74-
changelog_n, commit, today, git_log_summary
75-
);
76-
41+
let contents = changelog::get_changelog(changelog_n, &commit, prev_tag, &today)?;
7742
let path = changelog_dir.join(format!("{}-changelog-{}.adoc", today, changelog_n));
7843
write_file(&path, &contents)?;
7944

xtask/src/release/changelog.rs

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
use std::fmt::Write;
2+
use std::{env, iter};
3+
4+
use anyhow::{bail, Result};
5+
use xshell::cmd;
6+
7+
pub(crate) fn get_changelog(
8+
changelog_n: usize,
9+
commit: &str,
10+
prev_tag: &str,
11+
today: &str,
12+
) -> Result<String> {
13+
let git_log = cmd!("git log {prev_tag}..HEAD --merges --reverse").read()?;
14+
let mut features = String::new();
15+
let mut fixes = String::new();
16+
let mut internal = String::new();
17+
let mut others = String::new();
18+
for line in git_log.lines() {
19+
let line = line.trim_start();
20+
if let Some(p) = line.find(':') {
21+
let pr = &line[..p];
22+
if let Ok(pr_num) = pr.parse::<u32>() {
23+
let accept = "Accept: application/vnd.github.v3+json";
24+
let token = match env::var("GITHUB_TOKEN") {
25+
Ok(token) => token,
26+
Err(_) => bail!("Please obtain a personal access token from https://github.com/settings/tokens and set the `GITHUB_TOKEN` environment variable."),
27+
};
28+
let authorization = format!("Authorization: token {}", token);
29+
let pr_url = "https://api.github.com/repos/rust-analyzer/rust-analyzer/issues";
30+
31+
// we don't use an HTTPS client or JSON parser to keep the build times low
32+
let pr_json =
33+
cmd!("curl -s -H {accept} -H {authorization} {pr_url}/{pr}").read()?;
34+
let pr_title = cmd!("jq .title").stdin(&pr_json).read()?;
35+
let pr_title = unescape(&pr_title[1..pr_title.len() - 1]);
36+
let pr_comment = cmd!("jq .body").stdin(pr_json).read()?;
37+
38+
let comments_json =
39+
cmd!("curl -s -H {accept} -H {authorization} {pr_url}/{pr}/comments").read()?;
40+
let pr_comments = cmd!("jq .[].body").stdin(comments_json).read()?;
41+
42+
let l = iter::once(pr_comment.as_str())
43+
.chain(pr_comments.lines())
44+
.rev()
45+
.find_map(|it| {
46+
let it = unescape(&it[1..it.len() - 1]);
47+
it.lines().find_map(parse_changelog_line)
48+
})
49+
.into_iter()
50+
.next()
51+
.unwrap_or_else(|| parse_title_line(&pr_title));
52+
let s = match l.kind {
53+
PrKind::Feature => &mut features,
54+
PrKind::Fix => &mut fixes,
55+
PrKind::Internal => &mut internal,
56+
PrKind::Other => &mut others,
57+
PrKind::Skip => continue,
58+
};
59+
writeln!(s, "* pr:{}[] {}", pr_num, l.message.as_deref().unwrap_or(&pr_title))
60+
.unwrap();
61+
}
62+
}
63+
}
64+
65+
let contents = format!(
66+
"\
67+
= Changelog #{}
68+
:sectanchors:
69+
:page-layout: post
70+
71+
Commit: commit:{}[] +
72+
Release: release:{}[]
73+
74+
== Sponsors
75+
76+
**Become a sponsor:** On https://opencollective.com/rust-analyzer/[OpenCollective] or
77+
https://github.com/sponsors/rust-analyzer[GitHub Sponsors].
78+
79+
== New Features
80+
81+
{}
82+
83+
== Fixes
84+
85+
{}
86+
87+
== Internal Improvements
88+
89+
{}
90+
91+
== Others
92+
93+
{}
94+
",
95+
changelog_n, commit, today, features, fixes, internal, others
96+
);
97+
Ok(contents)
98+
}
99+
100+
#[derive(Clone, Copy)]
101+
enum PrKind {
102+
Feature,
103+
Fix,
104+
Internal,
105+
Other,
106+
Skip,
107+
}
108+
109+
struct PrInfo {
110+
message: Option<String>,
111+
kind: PrKind,
112+
}
113+
114+
fn unescape(s: &str) -> String {
115+
s.replace(r#"\""#, "").replace(r#"\n"#, "\n").replace(r#"\r"#, "")
116+
}
117+
118+
fn parse_changelog_line(s: &str) -> Option<PrInfo> {
119+
let parts = s.splitn(3, ' ').collect::<Vec<_>>();
120+
if parts.len() < 2 || parts[0] != "changelog" {
121+
return None;
122+
}
123+
let message = parts.get(2).map(|it| it.to_string());
124+
let kind = match parts[1].trim_end_matches(':') {
125+
"feature" => PrKind::Feature,
126+
"fix" => PrKind::Fix,
127+
"internal" => PrKind::Internal,
128+
"skip" => PrKind::Skip,
129+
_ => {
130+
let kind = PrKind::Other;
131+
let message = format!("{} {}", parts[1], message.unwrap_or_default());
132+
return Some(PrInfo { kind, message: Some(message) });
133+
}
134+
};
135+
let res = PrInfo { kind, message };
136+
Some(res)
137+
}
138+
139+
fn parse_title_line(s: &str) -> PrInfo {
140+
let lower = s.to_ascii_lowercase();
141+
const PREFIXES: [(&str, PrKind); 5] = [
142+
("feat: ", PrKind::Feature),
143+
("feature: ", PrKind::Feature),
144+
("fix: ", PrKind::Fix),
145+
("internal: ", PrKind::Internal),
146+
("minor: ", PrKind::Skip),
147+
];
148+
149+
for &(prefix, kind) in &PREFIXES {
150+
if lower.starts_with(prefix) {
151+
let message = match &kind {
152+
PrKind::Skip => None,
153+
_ => Some(s[prefix.len()..].to_string()),
154+
};
155+
return PrInfo { kind, message };
156+
}
157+
}
158+
PrInfo { kind: PrKind::Other, message: Some(s.to_string()) }
159+
}

0 commit comments

Comments
 (0)