Skip to main content

sr_github/
lib.rs

1use std::process::Command;
2
3use sr_core::error::ReleaseError;
4use sr_core::release::VcsProvider;
5
6/// GitHub implementation of the VcsProvider trait using the `gh` CLI.
7pub struct GitHubProvider {
8    owner: String,
9    repo: String,
10    hostname: String,
11}
12
13impl GitHubProvider {
14    pub fn new(owner: String, repo: String, hostname: String) -> Self {
15        Self {
16            owner,
17            repo,
18            hostname,
19        }
20    }
21
22    fn base_url(&self) -> String {
23        format!("https://{}/{}/{}", self.hostname, self.owner, self.repo)
24    }
25}
26
27impl VcsProvider for GitHubProvider {
28    fn create_release(
29        &self,
30        tag: &str,
31        name: &str,
32        body: &str,
33        prerelease: bool,
34    ) -> Result<String, ReleaseError> {
35        let repo_slug = format!("{}/{}", self.owner, self.repo);
36
37        let mut args = vec![
38            "release", "create", tag, "--repo", &repo_slug, "--title", name, "--notes", body,
39        ];
40
41        if prerelease {
42            args.push("--prerelease");
43        }
44
45        let output = Command::new("gh")
46            .args(&args)
47            .output()
48            .map_err(|e| ReleaseError::Vcs(format!("failed to run gh: {e}")))?;
49
50        if !output.status.success() {
51            let stderr = String::from_utf8_lossy(&output.stderr);
52            return Err(ReleaseError::Vcs(format!(
53                "gh release create failed: {stderr}"
54            )));
55        }
56
57        let url = String::from_utf8_lossy(&output.stdout).trim().to_string();
58        Ok(url)
59    }
60
61    fn compare_url(&self, base: &str, head: &str) -> Result<String, ReleaseError> {
62        Ok(format!("{}/compare/{base}...{head}", self.base_url()))
63    }
64
65    fn release_exists(&self, tag: &str) -> Result<bool, ReleaseError> {
66        let repo_slug = format!("{}/{}", self.owner, self.repo);
67        let output = Command::new("gh")
68            .args(["release", "view", tag, "--repo", &repo_slug])
69            .output()
70            .map_err(|e| ReleaseError::Vcs(format!("failed to run gh: {e}")))?;
71        Ok(output.status.success())
72    }
73
74    fn repo_url(&self) -> Option<String> {
75        Some(self.base_url())
76    }
77
78    fn delete_release(&self, tag: &str) -> Result<(), ReleaseError> {
79        let repo_slug = format!("{}/{}", self.owner, self.repo);
80        let output = Command::new("gh")
81            .args(["release", "delete", tag, "--repo", &repo_slug, "--yes"])
82            .output()
83            .map_err(|e| ReleaseError::Vcs(format!("failed to run gh: {e}")))?;
84        if !output.status.success() {
85            let stderr = String::from_utf8_lossy(&output.stderr);
86            return Err(ReleaseError::Vcs(format!(
87                "gh release delete failed: {stderr}"
88            )));
89        }
90        Ok(())
91    }
92
93    fn upload_assets(&self, tag: &str, files: &[&str]) -> Result<(), ReleaseError> {
94        let repo_slug = format!("{}/{}", self.owner, self.repo);
95        let mut args = vec!["release", "upload", tag, "--repo", &repo_slug, "--clobber"];
96        args.extend(files);
97
98        let output = Command::new("gh")
99            .args(&args)
100            .output()
101            .map_err(|e| ReleaseError::Vcs(format!("failed to run gh: {e}")))?;
102
103        if !output.status.success() {
104            let stderr = String::from_utf8_lossy(&output.stderr);
105            return Err(ReleaseError::Vcs(format!(
106                "gh release upload failed: {stderr}"
107            )));
108        }
109
110        Ok(())
111    }
112}