xtask_toolkit/
gh_cli.rs

1use std::path::Path;
2
3use xshell::{cmd, Shell};
4
5#[derive(serde::Deserialize)]
6#[serde(rename_all = "camelCase")]
7struct GhResponse {
8    pub tag_name: String,
9    pub is_draft: bool,
10    pub is_prelease: bool,
11}
12
13pub struct Release {
14    pub name: String,
15    pub version: semver::Version,
16    pub draft: bool,
17    pub prelease: bool,
18}
19
20impl TryFrom<GhResponse> for Release {
21    type Error = ();
22    fn try_from(value: GhResponse) -> Result<Self, Self::Error> {
23        let (name, version) = value.tag_name.rsplit_once('-').ok_or(())?;
24
25        let version = semver::Version::parse(version).map_err(|_| ())?;
26
27        Ok(Release {
28            name: name.to_string(),
29            version,
30            draft: value.is_draft,
31            prelease: value.is_prelease,
32        })
33    }
34}
35
36#[derive(Debug, thiserror::Error)]
37pub enum GetFromGHError {
38    #[error(transparent)]
39    XShellError(#[from] xshell::Error),
40
41    #[error(transparent)]
42    SerdeError(#[from] serde_json::Error),
43}
44
45#[derive(Default)]
46pub enum ReleaseMode {
47    #[default]
48    Normal,
49
50    Draft,
51    Prerelease,
52}
53
54impl Release {
55    pub fn new(name: &str, version: &str) -> Result<Self, semver::Error> {
56        let version = semver::Version::parse(version)?;
57        Ok(Release {
58            name: name.to_string(),
59            version,
60            draft: false,
61            prelease: false,
62        })
63    }
64
65    pub fn with_release_mode(&mut self, mode: ReleaseMode) -> &mut Self {
66        self.draft = matches!(mode, ReleaseMode::Draft);
67        self.prelease = matches!(mode, ReleaseMode::Prerelease);
68        self
69    }
70
71    pub fn get_from_gh() -> Result<Vec<Self>, GetFromGHError> {
72        let sh = Shell::new()?;
73        let previous_releases: Vec<GhResponse> =
74            serde_json::from_str(&cmd!(sh, "gh release list --json tagName").read()?)?;
75
76        Ok(previous_releases
77            .into_iter()
78            .filter_map(|x| Self::try_from(x).ok())
79            .collect())
80    }
81
82    pub fn release<T, I>(&self, files: T) -> Result<String, xshell::Error>
83    where
84        T: IntoIterator<Item = I>,
85        I: AsRef<Path> + Sized,
86    {
87        let sh = Shell::new()?;
88        let files = files
89            .into_iter()
90            .map(|x| x.as_ref().display().to_string())
91            .collect::<Vec<_>>();
92
93        let name = self.name.clone();
94
95        let draft = if self.draft { " --draft" } else { "" };
96
97        sh.cmd("gh")
98            .args(["release", "create", "generate-notes", draft, &name])
99            .args(files)
100            .read()
101    }
102}