gh_workflow_parser/
gh.rs

1//! Functions for interacting with GitHub via the `gh` CLI
2use std::error::Error;
3use std::ffi::{OsStr, OsString};
4use std::path::PathBuf;
5use std::sync::OnceLock;
6
7pub mod gh_cli;
8pub mod gh_cli_fake;
9pub mod util;
10
11/// Get the GitHub CLI and initialize it with a default repository
12/// If `fake` is true, a fake GitHub CLI is returned.
13/// The fake GitHub CLI is used for testing and does not interact with GitHub
14///
15/// # Arguments
16///
17/// * `repo` - The default repository to use
18/// * `fake` - If true, a fake GitHub CLI is returned
19///
20/// # Returns
21///
22/// [`Box<dyn GitHub>`](GitHub) - The GitHub CLI interface
23///
24/// # Example
25///
26/// ```
27/// # use gh_workflow_parser::gh::init_github_cli;
28/// let github_cli = init_github_cli("https://example.com/repo".to_string(), false);
29/// ```
30pub fn init_github_cli(repo: String, fake: bool) -> Box<dyn GitHub> {
31    if fake {
32        Box::new(gh_cli_fake::GitHubCliFake::new(repo))
33    } else {
34        Box::new(gh_cli::GitHubCli::new(repo))
35    }
36}
37
38/// Trait describing the methods that the GitHub CLI should implement
39pub trait GitHub {
40    /// Get the summary of a run in a GitHub repository, if `repo` is `None` the default repository is used
41    /// Returns the summary as a [String]
42    fn run_summary(&self, repo: Option<&str>, run_id: &str) -> Result<String, Box<dyn Error>>;
43
44    /// Get the log of a failed job in a GitHub repository, if `repo` is `None` the default repository is used
45    /// Returns the log as a [String]
46    fn failed_job_log(&self, repo: Option<&str>, job_id: &str) -> Result<String, Box<dyn Error>>;
47
48    /// Create an issue in a GitHub repository, if `repo` is `None` the default repository is used
49    fn create_issue(
50        &self,
51        repo: Option<&str>,
52        title: &str,
53        body: &str,
54        labels: &[String],
55    ) -> Result<(), Box<dyn Error>>;
56
57    /// Get the bodies of open issues with a specific label in a GitHub repository, if `repo` is `None` the default repository is used
58    /// Returns [`Vec<String>`](Vec) of issue bodies
59    fn issue_bodies_open_with_label(
60        &self,
61        repo: Option<&str>,
62        label: &str,
63    ) -> Result<Vec<String>, Box<dyn Error>>;
64
65    /// Get all labels in a GitHub repository, if `repo` is `None` the default repository is used
66    /// Returns [`Vec<String>`](Vec) of GitHub labels
67    fn all_labels(&self, repo: Option<&str>) -> Result<Vec<String>, Box<dyn Error>>;
68
69    /// Create a label in a GitHub repository, if `repo` is `None` the default repository is used
70    /// The color should be a 6 character hex code (e.g. "FF0000")
71    /// if `force` is true and the label already exists, it will be overwritten
72    fn create_label(
73        &self,
74        repo: Option<&str>,
75        name: &str,
76        color: &str,
77        description: &str,
78        force: bool,
79    ) -> Result<(), Box<dyn Error>>;
80
81    /// Get the default repository for the GitHub CLI
82    fn default_repo(&self) -> &str;
83}
84
85include!(concat!(env!("OUT_DIR"), "/include_gh_cli.rs"));
86pub static GITHUB_CLI: OnceLock<OsString> = OnceLock::new();
87pub fn gh_cli() -> &'static OsStr {
88    GITHUB_CLI.get_or_init(|| {
89        let gh_cli_path = gh_cli_first_time_setup().unwrap();
90        OsString::from(gh_cli_path)
91    })
92}
93
94pub fn gh_cli_first_time_setup() -> Result<PathBuf, Box<dyn Error>> {
95    let mut path = std::env::current_exe()?;
96    path.pop();
97    path.push("gh-workflow-parser-deps");
98
99    if !path.exists() {
100        std::fs::create_dir(&path)?;
101    }
102
103    let gh_cli_path = path.join("gh_cli");
104
105    if !gh_cli_path.exists() {
106        log::debug!("the gh_cli file at {gh_cli_path:?} doesn't exist. Creating it...");
107        // first decompress the gh-cli binary blob
108        let gh_cli_bytes = GH_CLI_BYTES;
109        log::trace!("gh_cli_bytes size: {}", gh_cli_bytes.len());
110
111        let decompressed_gh_cli = crate::util::bzip2_decompress(gh_cli_bytes)?;
112        log::trace!("decompressed_gh_cli size: {}", decompressed_gh_cli.len());
113
114        // Write the gh_cli file to the gh_cli_path
115        std::fs::write(&gh_cli_path, decompressed_gh_cli)?;
116        #[cfg(target_os = "linux")]
117        crate::util::set_linux_file_permissions(&gh_cli_path, 0o755)?;
118        log::debug!("gh_cli file written to {gh_cli_path:?}");
119    } else {
120        log::debug!(
121            "the gh_cli file at {gh_cli_path:?} already exists. Skipping first time setup..."
122        );
123    }
124
125    Ok(gh_cli_path)
126}