Skip to content

Commit 28293cb

Browse files
Utilize new tracking issues for release notes
1 parent 0cd16fe commit 28293cb

File tree

2 files changed

+233
-53
lines changed

2 files changed

+233
-53
lines changed

src/main.rs

+227-53
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::collections::BTreeMap;
2+
use std::collections::HashMap;
23
use std::env;
34

45
use askama::Template;
@@ -8,8 +9,6 @@ use chrono::Duration;
89
use reqwest::header::HeaderMap;
910
use serde_json as json;
1011

11-
type JsonRefArray<'a> = Vec<&'a json::Value>;
12-
1312
const SKIP_LABELS: &[&str] = &[
1413
"beta-nominated",
1514
"beta-accepted",
@@ -18,6 +17,13 @@ const SKIP_LABELS: &[&str] = &[
1817
"rollup",
1918
];
2019

20+
const RELNOTES_LABELS: &[&str] = &[
21+
"relnotes",
22+
"relnotes-perf",
23+
"finished-final-comment-period",
24+
"needs-fcp",
25+
];
26+
2127
#[derive(Clone, Template)]
2228
#[template(path = "relnotes.md", escape = "none")]
2329
struct ReleaseNotes {
@@ -35,6 +41,8 @@ struct ReleaseNotes {
3541
unsorted: String,
3642
unsorted_relnotes: String,
3743
version: String,
44+
internal_changes_relnotes: String,
45+
internal_changes_unsorted: String,
3846
}
3947

4048
fn main() {
@@ -54,8 +62,8 @@ fn main() {
5462
end = end + six_weeks;
5563
}
5664

57-
let mut issues = get_issues_by_milestone(&version, "rust");
58-
issues.sort_by_cached_key(|issue| issue["number"].as_u64().unwrap());
65+
let issues = get_issues_by_milestone(&version, "rust");
66+
let mut tracking_rust = TrackingIssues::collect(&issues);
5967

6068
// Skips `beta-accepted` as those PRs were backported onto the
6169
// previous stable.
@@ -67,30 +75,75 @@ fn main() {
6775
.any(|o| SKIP_LABELS.contains(&o["name"].as_str().unwrap()))
6876
});
6977

70-
let relnotes_tags = &["relnotes", "finished-final-comment-period", "needs-fcp"];
71-
72-
let (relnotes, rest) = partition_by_tag(in_release, relnotes_tags);
78+
let (relnotes, rest) = in_release
79+
.into_iter()
80+
.partition::<Vec<_>, _>(|o| has_tags(o, RELNOTES_LABELS));
7381

7482
let (
7583
compat_relnotes,
7684
libraries_relnotes,
7785
language_relnotes,
7886
compiler_relnotes,
87+
internal_changes_relnotes,
7988
unsorted_relnotes,
80-
) = partition_prs(relnotes);
89+
) = to_sections(relnotes, &mut tracking_rust);
8190

82-
let (compat_unsorted, libraries_unsorted, language_unsorted, compiler_unsorted, unsorted) =
83-
partition_prs(rest);
91+
let (
92+
compat_unsorted,
93+
libraries_unsorted,
94+
language_unsorted,
95+
compiler_unsorted,
96+
internal_changes_unsorted,
97+
unsorted,
98+
) = to_sections(rest, &mut tracking_rust);
8499

85-
let mut cargo_issues = get_issues_by_milestone(&version, "cargo");
86-
cargo_issues.sort_by_cached_key(|issue| issue["number"].as_u64().unwrap());
100+
let cargo_issues = get_issues_by_milestone(&version, "cargo");
87101

88102
let (cargo_relnotes, cargo_unsorted) = {
89-
let (relnotes, rest) = partition_by_tag(cargo_issues.iter(), relnotes_tags);
103+
let (relnotes, rest) = cargo_issues
104+
.iter()
105+
.partition::<Vec<_>, _>(|o| has_tags(o, RELNOTES_LABELS));
90106

91-
(map_to_line_items(relnotes), map_to_line_items(rest))
107+
(
108+
relnotes
109+
.iter()
110+
.map(|o| {
111+
format!(
112+
"- [{title}]({url}/)",
113+
title = o["title"].as_str().unwrap(),
114+
url = o["url"].as_str().unwrap(),
115+
)
116+
})
117+
.collect::<Vec<_>>()
118+
.join("\n"),
119+
rest.iter()
120+
.map(|o| {
121+
format!(
122+
"- [{title}]({url}/)",
123+
title = o["title"].as_str().unwrap(),
124+
url = o["url"].as_str().unwrap(),
125+
)
126+
})
127+
.collect::<Vec<_>>()
128+
.join("\n"),
129+
)
92130
};
93131

132+
for issue in tracking_rust.issues.values() {
133+
for (section, (used, _)) in issue.sections.iter() {
134+
if *used {
135+
continue;
136+
}
137+
138+
eprintln!(
139+
"Did not use {:?} from {} <{}>",
140+
section,
141+
issue.raw["title"].as_str().unwrap(),
142+
issue.raw["url"].as_str().unwrap()
143+
);
144+
}
145+
}
146+
94147
let relnotes = ReleaseNotes {
95148
version,
96149
date: (end + six_weeks).naive_utc(),
@@ -104,6 +157,8 @@ fn main() {
104157
compiler_unsorted,
105158
cargo_relnotes,
106159
cargo_unsorted,
160+
internal_changes_relnotes,
161+
internal_changes_unsorted,
107162
unsorted_relnotes,
108163
unsorted,
109164
};
@@ -112,11 +167,29 @@ fn main() {
112167
}
113168

114169
fn get_issues_by_milestone(version: &str, repo_name: &'static str) -> Vec<json::Value> {
170+
let mut out = get_issues_by_milestone_inner(version, repo_name, "issues");
171+
out.extend(get_issues_by_milestone_inner(
172+
version,
173+
repo_name,
174+
"pullRequests",
175+
));
176+
out.sort_unstable_by_key(|v| v["number"].as_u64().unwrap());
177+
out.dedup_by_key(|v| v["number"].as_u64().unwrap());
178+
out
179+
}
180+
181+
fn get_issues_by_milestone_inner(
182+
version: &str,
183+
repo_name: &'static str,
184+
ty: &str,
185+
) -> Vec<json::Value> {
115186
use reqwest::blocking::Client;
116187

117188
let headers = request_header();
118189
let mut args = BTreeMap::new();
119-
args.insert("states", String::from("[MERGED]"));
190+
if ty == "pullRequests" {
191+
args.insert("states", String::from("[MERGED]"));
192+
}
120193
args.insert("last", String::from("100"));
121194
let mut issues = Vec::new();
122195

@@ -128,11 +201,12 @@ fn get_issues_by_milestone(version: &str, repo_name: &'static str) -> Vec<json::
128201
milestones(query: "{version}", first: 1) {{
129202
totalCount
130203
nodes {{
131-
pullRequests({args}) {{
204+
{ty}({args}) {{
132205
nodes {{
133206
number
134207
title
135208
url
209+
body
136210
labels(last: 100) {{
137211
nodes {{
138212
name
@@ -149,6 +223,7 @@ fn get_issues_by_milestone(version: &str, repo_name: &'static str) -> Vec<json::
149223
}}"#,
150224
repo_name = repo_name,
151225
version = version,
226+
ty = ty,
152227
args = args
153228
.iter()
154229
.map(|(k, v)| format!("{}: {}", k, v))
@@ -170,9 +245,7 @@ fn get_issues_by_milestone(version: &str, repo_name: &'static str) -> Vec<json::
170245
.send()
171246
.unwrap();
172247
let status = response.status();
173-
let json = response
174-
.json::<json::Value>()
175-
.unwrap();
248+
let json = response.json::<json::Value>().unwrap();
176249
if !status.is_success() {
177250
panic!("API Error {}: {}", status, json);
178251
}
@@ -184,7 +257,7 @@ fn get_issues_by_milestone(version: &str, repo_name: &'static str) -> Vec<json::
184257
"More than one milestone matched the query \"{version}\". Please be more specific.",
185258
version = version
186259
);
187-
let pull_requests_data = milestones_data["nodes"][0]["pullRequests"].clone();
260+
let pull_requests_data = milestones_data["nodes"][0][ty].clone();
188261

189262
let mut pull_requests = pull_requests_data["nodes"].as_array().unwrap().clone();
190263
issues.append(&mut pull_requests);
@@ -212,45 +285,146 @@ fn request_header() -> HeaderMap {
212285
headers
213286
}
214287

215-
fn map_to_line_items<'a>(iter: impl IntoIterator<Item = &'a json::Value>) -> String {
216-
iter.into_iter()
217-
.map(|o| {
218-
format!(
219-
"- [{title}]({url}/)",
220-
title = o["title"].as_str().unwrap(),
221-
url = o["url"].as_str().unwrap(),
222-
)
223-
})
224-
.collect::<Vec<_>>()
225-
.join("\n")
288+
struct TrackingIssues {
289+
// Maps the issue/PR number *tracked* by the issue in `json::Value`.
290+
//
291+
// bool is tracking whether we've used that issue already.
292+
issues: HashMap<u64, TrackingIssue>,
226293
}
227294

228-
fn partition_by_tag<'a>(
229-
iter: impl IntoIterator<Item = &'a json::Value>,
230-
tags: &[&str],
231-
) -> (JsonRefArray<'a>, JsonRefArray<'a>) {
232-
iter.into_iter().partition(|o| {
233-
o["labels"]["nodes"]
234-
.as_array()
235-
.unwrap()
236-
.iter()
237-
.any(|o| tags.iter().any(|tag| o["name"] == *tag))
238-
})
295+
#[derive(Debug)]
296+
struct TrackingIssue {
297+
raw: json::Value,
298+
// Section name -> (used, lines)
299+
sections: HashMap<String, (bool, Vec<String>)>,
300+
}
301+
302+
impl TrackingIssues {
303+
fn collect(all: &[json::Value]) -> Self {
304+
let prefix = "Tracking issue for release notes of #";
305+
let mut tracking_issues = HashMap::new();
306+
for o in all.iter() {
307+
let title = o["title"].as_str().unwrap();
308+
if let Some(tail) = title.strip_prefix(prefix) {
309+
let for_number = tail[..tail.find(':').unwrap()].parse::<u64>().unwrap();
310+
let mut sections = HashMap::new();
311+
let body = o["body"].as_str().unwrap();
312+
let relnotes = body
313+
.split("```")
314+
.nth(1)
315+
.unwrap()
316+
.strip_prefix("markdown")
317+
.unwrap();
318+
let mut in_section = None;
319+
for line in relnotes.lines() {
320+
if line.trim().is_empty() {
321+
continue;
322+
}
323+
324+
if let Some(header) = line.strip_prefix("# ") {
325+
in_section = Some(header);
326+
continue;
327+
}
328+
329+
if let Some(section) = in_section {
330+
sections
331+
.entry(section.to_owned())
332+
.or_insert_with(|| (false, vec![]))
333+
.1
334+
.push(line.to_owned());
335+
}
336+
}
337+
tracking_issues.insert(
338+
for_number,
339+
TrackingIssue {
340+
raw: o.clone(),
341+
sections,
342+
},
343+
);
344+
}
345+
}
346+
Self {
347+
issues: tracking_issues,
348+
}
349+
}
239350
}
240351

241-
fn partition_prs<'a>(
352+
fn map_to_line_items<'a>(
242353
iter: impl IntoIterator<Item = &'a json::Value>,
243-
) -> (String, String, String, String, String) {
244-
let (compat_notes, rest) = partition_by_tag(iter, &["C-future-compatibility"]);
245-
let (libs, rest) = partition_by_tag(rest, &["T-libs", "T-libs-api"]);
246-
let (lang, rest) = partition_by_tag(rest, &["T-lang"]);
247-
let (compiler, rest) = partition_by_tag(rest, &["T-compiler"]);
354+
tracking_issues: &mut TrackingIssues,
355+
by_section: &mut HashMap<&'static str, String>,
356+
) {
357+
for o in iter {
358+
let title = o["title"].as_str().unwrap();
359+
if title.starts_with("Tracking issue for release notes of #") {
360+
continue;
361+
}
362+
let number = o["number"].as_u64().unwrap();
363+
364+
if let Some(issue) = tracking_issues.issues.get_mut(&number) {
365+
for (section, (used, lines)) in issue.sections.iter_mut() {
366+
if let Some(contents) = by_section.get_mut(section.as_str()) {
367+
*used = true;
368+
for line in lines.iter() {
369+
contents.push_str(line);
370+
contents.push('\n');
371+
}
372+
}
373+
}
248374

375+
// If we have a dedicated tracking issue, don't use our default rules.
376+
continue;
377+
}
378+
379+
// In the future we expect to have increasingly few things fall into this category, as
380+
// things are added to the dedicated tracking issue category in triagebot (today mostly
381+
// FCPs are missing).
382+
383+
let section = if has_tags(o, &["C-future-compatibility"]) {
384+
"Compatibility Notes"
385+
} else if has_tags(o, &["T-libs", "T-libs-api"]) {
386+
"Library"
387+
} else if has_tags(o, &["T-lang"]) {
388+
"Language"
389+
} else if has_tags(o, &["T-compiler"]) {
390+
"Compiler"
391+
} else {
392+
"Other"
393+
};
394+
by_section.get_mut(section).unwrap().push_str(&format!(
395+
"- [{title}]({url}/)\n",
396+
title = o["title"].as_str().unwrap(),
397+
url = o["url"].as_str().unwrap(),
398+
));
399+
}
400+
}
401+
402+
fn has_tags<'a>(o: &'a json::Value, tags: &[&str]) -> bool {
403+
o["labels"]["nodes"]
404+
.as_array()
405+
.unwrap()
406+
.iter()
407+
.any(|o| tags.iter().any(|tag| o["name"] == *tag))
408+
}
409+
410+
fn to_sections<'a>(
411+
iter: impl IntoIterator<Item = &'a json::Value>,
412+
mut tracking: &mut TrackingIssues,
413+
) -> (String, String, String, String, String, String) {
414+
let mut by_section = HashMap::new();
415+
by_section.insert("Compatibility Notes", String::new());
416+
by_section.insert("Library", String::new());
417+
by_section.insert("Language", String::new());
418+
by_section.insert("Compiler", String::new());
419+
by_section.insert("Internal Changes", String::new());
420+
by_section.insert("Other", String::new());
421+
map_to_line_items(iter, &mut tracking, &mut by_section);
249422
(
250-
map_to_line_items(compat_notes),
251-
map_to_line_items(libs),
252-
map_to_line_items(lang),
253-
map_to_line_items(compiler),
254-
map_to_line_items(rest),
423+
by_section.remove("Compatibility Notes").unwrap(),
424+
by_section.remove("Library").unwrap(),
425+
by_section.remove("Language").unwrap(),
426+
by_section.remove("Compiler").unwrap(),
427+
by_section.remove("Internal Changes").unwrap(),
428+
by_section.remove("Other").unwrap(),
255429
)
256430
}

0 commit comments

Comments
 (0)