aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/dev/README.md18
-rw-r--r--xtask/src/release.rs39
-rw-r--r--xtask/src/release/changelog.rs159
3 files changed, 173 insertions, 43 deletions
diff --git a/docs/dev/README.md b/docs/dev/README.md
index 7e4488a41..699a48e63 100644
--- a/docs/dev/README.md
+++ b/docs/dev/README.md
@@ -208,20 +208,26 @@ Release process is handled by `release`, `dist` and `promote` xtasks, `release`
208 208
209Additionally, it assumes that remote for `rust-analyzer` is called `upstream` (I use `origin` to point to my fork). 209Additionally, it assumes that remote for `rust-analyzer` is called `upstream` (I use `origin` to point to my fork).
210 210
211`release` calls the GitHub API calls to scrape pull request comments and categorize them in the changelog.
212This step uses the `curl` and `jq` applications, which need to be available in `PATH`.
213Finally, you need to obtain a GitHub personal access token and set the `GITHUB_TOKEN` environment variable.
214
211Release steps: 215Release steps:
212 216
2131. Inside rust-analyzer, run `cargo xtask release`. This will: 2171. Set the `GITHUB_TOKEN` environment variable.
2182. Inside rust-analyzer, run `cargo xtask release`. This will:
214 * checkout the `release` branch 219 * checkout the `release` branch
215 * reset it to `upstream/nightly` 220 * reset it to `upstream/nightly`
216 * push it to `upstream`. This triggers GitHub Actions which: 221 * push it to `upstream`. This triggers GitHub Actions which:
217 * runs `cargo xtask dist` to package binaries and VS Code extension 222 * runs `cargo xtask dist` to package binaries and VS Code extension
218 * makes a GitHub release 223 * makes a GitHub release
219 * pushes VS Code extension to the marketplace 224 * pushes VS Code extension to the marketplace
220 * create new changelog in `rust-analyzer.github.io` 225 * call the GitHub API for PR details
2212. While the release is in progress, fill in the changelog 226 * create a new changelog in `rust-analyzer.github.io`
2223. Commit & push the changelog 2273. While the release is in progress, fill in the changelog
2234. Tweet 2284. Commit & push the changelog
2245. Inside `rust-analyzer`, run `cargo xtask promote` -- this will create a PR to rust-lang/rust updating rust-analyzer's submodule. 2295. Tweet
2306. Inside `rust-analyzer`, run `cargo xtask promote` -- this will create a PR to rust-lang/rust updating rust-analyzer's submodule.
225 Self-approve the PR. 231 Self-approve the PR.
226 232
227If the GitHub Actions release fails because of a transient problem like a timeout, you can re-run the job from the Actions console. 233If the GitHub Actions release fails because of a transient problem like a timeout, you can re-run the job from the Actions console.
diff --git a/xtask/src/release.rs b/xtask/src/release.rs
index dde5d14ee..22bb50467 100644
--- a/xtask/src/release.rs
+++ b/xtask/src/release.rs
@@ -1,4 +1,4 @@
1use std::fmt::Write; 1mod changelog;
2 2
3use xshell::{cmd, cp, pushd, read_dir, write_file}; 3use xshell::{cmd, cp, pushd, read_dir, write_file};
4 4
@@ -38,42 +38,7 @@ impl flags::Release {
38 let tags = cmd!("git tag --list").read()?; 38 let tags = cmd!("git tag --list").read()?;
39 let prev_tag = tags.lines().filter(|line| is_release_tag(line)).last().unwrap(); 39 let prev_tag = tags.lines().filter(|line| is_release_tag(line)).last().unwrap();
40 40
41 let git_log = cmd!("git log {prev_tag}..HEAD --merges --reverse").read()?; 41 let contents = changelog::get_changelog(changelog_n, &commit, prev_tag, &today)?;
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
58Commit: commit:{}[] +
59Release: release:{}[]
60
61== Sponsors
62
63**Become a sponsor:** On https://opencollective.com/rust-analyzer/[OpenCollective] or
64https://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
77 let path = changelog_dir.join(format!("{}-changelog-{}.adoc", today, changelog_n)); 42 let path = changelog_dir.join(format!("{}-changelog-{}.adoc", today, changelog_n));
78 write_file(&path, &contents)?; 43 write_file(&path, &contents)?;
79 44
diff --git a/xtask/src/release/changelog.rs b/xtask/src/release/changelog.rs
new file mode 100644
index 000000000..ffcae2cf7
--- /dev/null
+++ b/xtask/src/release/changelog.rs
@@ -0,0 +1,159 @@
1use std::fmt::Write;
2use std::{env, iter};
3
4use anyhow::{bail, Result};
5use xshell::cmd;
6
7pub(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
71Commit: commit:{}[] +
72Release: release:{}[]
73
74== Sponsors
75
76**Become a sponsor:** On https://opencollective.com/rust-analyzer/[OpenCollective] or
77https://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)]
101enum PrKind {
102 Feature,
103 Fix,
104 Internal,
105 Other,
106 Skip,
107}
108
109struct PrInfo {
110 message: Option<String>,
111 kind: PrKind,
112}
113
114fn unescape(s: &str) -> String {
115 s.replace(r#"\""#, "").replace(r#"\n"#, "\n").replace(r#"\r"#, "")
116}
117
118fn 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
139fn 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}