xtask/
publish.rs

1mod notes;
2
3use crate::flags;
4use anyhow::bail;
5use std::env;
6use xshell::{Shell, cmd};
7
8impl flags::PublishReleaseNotes {
9    pub(crate) fn run(self, sh: &Shell) -> anyhow::Result<()> {
10        let asciidoc = sh.read_file(&self.changelog)?;
11        let mut markdown = notes::convert_asciidoc_to_markdown(std::io::Cursor::new(&asciidoc))?;
12        if !markdown.starts_with("# Changelog") {
13            bail!("changelog Markdown should start with `# Changelog`");
14        }
15        const NEWLINES: &str = "\n\n";
16        let Some(idx) = markdown.find(NEWLINES) else {
17            bail!("missing newlines after changelog title");
18        };
19        markdown.replace_range(0..idx + NEWLINES.len(), "");
20
21        let file_name = check_file_name(self.changelog)?;
22        let tag_name = &file_name[0..10];
23        let original_changelog_url = create_original_changelog_url(&file_name);
24        let additional_paragraph =
25            format!("\nSee also the [changelog post]({original_changelog_url}).");
26        markdown.push_str(&additional_paragraph);
27        if self.dry_run {
28            println!("{markdown}");
29        } else {
30            update_release(sh, tag_name, &markdown)?;
31        }
32        Ok(())
33    }
34}
35
36fn check_file_name<P: AsRef<std::path::Path>>(path: P) -> anyhow::Result<String> {
37    let file_name = path
38        .as_ref()
39        .file_name()
40        .ok_or_else(|| anyhow::format_err!("file name is not specified as `changelog`"))?
41        .to_string_lossy();
42
43    let mut chars = file_name.chars();
44    if file_name.len() >= 10
45        && chars.next().unwrap().is_ascii_digit()
46        && chars.next().unwrap().is_ascii_digit()
47        && chars.next().unwrap().is_ascii_digit()
48        && chars.next().unwrap().is_ascii_digit()
49        && chars.next().unwrap() == '-'
50        && chars.next().unwrap().is_ascii_digit()
51        && chars.next().unwrap().is_ascii_digit()
52        && chars.next().unwrap() == '-'
53        && chars.next().unwrap().is_ascii_digit()
54        && chars.next().unwrap().is_ascii_digit()
55    {
56        Ok(file_name.to_string())
57    } else {
58        bail!("unexpected file name format; no date information prefixed")
59    }
60}
61
62fn create_original_changelog_url(file_name: &str) -> String {
63    let year = &file_name[0..4];
64    let month = &file_name[5..7];
65    let day = &file_name[8..10];
66    let mut stem = &file_name[11..];
67    if let Some(stripped) = stem.strip_suffix(".adoc") {
68        stem = stripped;
69    }
70    format!("https://rust-analyzer.github.io/thisweek/{year}/{month}/{day}/{stem}.html")
71}
72
73fn update_release(sh: &Shell, tag_name: &str, release_notes: &str) -> anyhow::Result<()> {
74    let token = match env::var("GITHUB_TOKEN") {
75        Ok(token) => token,
76        Err(_) => bail!(
77            "Please obtain a personal access token from https://github.com/settings/tokens and set the `GITHUB_TOKEN` environment variable."
78        ),
79    };
80    let accept = "Accept: application/vnd.github+json";
81    let authorization = format!("Authorization: Bearer {token}");
82    let api_version = "X-GitHub-Api-Version: 2022-11-28";
83    let release_url = "https://api.github.com/repos/rust-lang/rust-analyzer/releases";
84
85    let release_json = cmd!(
86        sh,
87        "curl -sf -H {accept} -H {authorization} -H {api_version} {release_url}/tags/{tag_name}"
88    )
89    .read()?;
90    let release_id = cmd!(sh, "jq .id").stdin(release_json).read()?;
91
92    let mut patch = String::new();
93    // note: the GitHub API doesn't update the target commit if the tag already exists
94    write_json::object(&mut patch)
95        .string("tag_name", tag_name)
96        .string("target_commitish", "master")
97        .string("name", tag_name)
98        .string("body", release_notes)
99        .bool("draft", false)
100        .bool("prerelease", false);
101    let _ = cmd!(
102        sh,
103        "curl -sf -X PATCH -H {accept} -H {authorization} -H {api_version} {release_url}/{release_id} -d {patch}"
104    )
105    .read()?;
106
107    Ok(())
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn original_changelog_url_creation() {
116        let input = "2019-07-24-changelog-0.adoc";
117        let actual = create_original_changelog_url(input);
118        let expected = "https://rust-analyzer.github.io/thisweek/2019/07/24/changelog-0.html";
119        assert_eq!(actual, expected);
120    }
121}