Skip to main content

ralph/tools/
gh_tools.rs

1/// GitHub CLI (`gh`) integration.
2///
3/// All functions require `gh` to be installed and authenticated.
4/// Use `gh_available()` to gate before calling anything else.
5use std::path::Path;
6
7/// Returns true when `gh` is installed and the user is authenticated.
8pub fn gh_available() -> bool {
9    std::process::Command::new("gh")
10        .args(["auth", "status"])
11        .stdout(std::process::Stdio::null())
12        .stderr(std::process::Stdio::null())
13        .status()
14        .map(|s| s.success())
15        .unwrap_or(false)
16}
17
18/// Create a GitHub PR for the current branch.
19///
20/// `title` and `body` are required.  Pass `draft = true` for a draft PR.
21/// `base` overrides the target branch (default: repo default branch).
22pub async fn create_pr(
23    title: &str,
24    body: &str,
25    draft: bool,
26    base: Option<&str>,
27    workspace: &Path,
28) -> crate::errors::Result<String> {
29    let mut cmd = tokio::process::Command::new("gh");
30    cmd.args(["pr", "create", "--title", title, "--body", body]);
31    if draft {
32        cmd.arg("--draft");
33    }
34    if let Some(b) = base {
35        cmd.args(["--base", b]);
36    }
37    cmd.current_dir(workspace);
38
39    let out = cmd
40        .output()
41        .await
42        .map_err(|e| crate::errors::RalphError::ToolFailed {
43            tool: "create_pr".to_string(),
44            message: e.to_string(),
45        })?;
46
47    if out.status.success() {
48        Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
49    } else {
50        Err(crate::errors::RalphError::ToolFailed {
51            tool: "create_pr".to_string(),
52            message: String::from_utf8_lossy(&out.stderr).trim().to_string(),
53        })
54    }
55}
56
57/// Return a summary of recent CI runs for the current (or specified) branch.
58pub async fn get_ci_status(
59    branch: Option<&str>,
60    workspace: &Path,
61) -> crate::errors::Result<String> {
62    let mut cmd = tokio::process::Command::new("gh");
63    cmd.args(["run", "list", "--limit", "5"]);
64    if let Some(b) = branch {
65        cmd.args(["--branch", b]);
66    }
67    cmd.current_dir(workspace);
68
69    let out = cmd
70        .output()
71        .await
72        .map_err(|e| crate::errors::RalphError::ToolFailed {
73            tool: "get_ci_status".to_string(),
74            message: e.to_string(),
75        })?;
76
77    if !out.status.success() {
78        return Err(crate::errors::RalphError::ToolFailed {
79            tool: "get_ci_status".to_string(),
80            message: String::from_utf8_lossy(&out.stderr).trim().to_string(),
81        });
82    }
83
84    let text = String::from_utf8_lossy(&out.stdout).to_string();
85    if text.trim().is_empty() {
86        Ok("No recent CI runs found for this branch.".to_string())
87    } else {
88        Ok(text)
89    }
90}