vcs-github 0.3.0

Automate the GitHub CLI (gh) from Rust through process execution.
Documentation

vcs-github

Automate GitHub from Rust through the gh CLI and process execution. Part of the vcs-toolkit-rs workspace.

Typed, async commands over the GitHub CLI (gh) that deserialize gh … --json output into structs, behind a mockable interface. Commands run inside an OS job (via processkit) so no gh subprocess is ever orphaned, return the structured Error, and honour an optional timeout.

Inside an async context (every method is async):

use std::path::Path;
use vcs_github::{GitHub, GitHubApi};

let gh = GitHub::new();
let prs = gh.pr_list(Path::new(".")).await?; // Vec<PullRequest>
let authed = gh.auth_status().await?; // bool — true when `gh auth status` exits 0

Inspect the repo and open a PR

use std::path::Path;
use vcs_github::{GitHub, GitHubApi};

# async fn demo(repo: &Path) -> Result<(), processkit::Error> {
    let gh = GitHub::new();

    let r = gh.repo_view(repo).await?; // Repo { owner, name, default_branch, is_private, … }
    println!("{}/{} (default: {})", r.owner, r.name, r.default_branch);

    // Any PRs (open/closed/merged) merging this branch into main? (title + URL)
    for pr in gh.pr_list_for_branch(repo, "feat/streaming", "main").await? {
        println!("#{} [{}] {}{}", pr.number, pr.state, pr.title, pr.url);
    }

    // Open a PR from an explicit head into an explicit base (both optional —
    // `None` head = current branch, `None` base = repo default). Returns the URL.
    let url = gh
        .pr_create(
            repo,
            "Add streaming",
            "Implements …",
            Some("feat/streaming".to_string()),
            Some("main".to_string()),
        )
        .await?;
    println!("opened {url}");

    for issue in gh.issue_list(repo).await? {
        println!("#{} [{}] {}", issue.number, issue.state, issue.title);
    }
# Ok(()) }

auth_status and timeouts

auth_status reports the bool from gh auth status's exit code, but a spawn failure or a timeout still surfaces as a processkit::Error rather than a silent false:

# use vcs_github::{GitHub, GitHubApi};
use std::time::Duration;
# async fn demo() -> Result<(), processkit::Error> {
    let gh = GitHub::new().default_timeout(Duration::from_secs(5));
    match gh.auth_status().await {
        Ok(true) => println!("authenticated"),
        Ok(false) => println!("not logged in (run `gh auth login`)"),
        Err(processkit::Error::Timeout { .. }) => eprintln!("gh timed out"),
        Err(e) => eprintln!("{e}"),
    }
# Ok(()) }

Consumers depend on the GitHubApi trait and substitute a fake in tests — enable the mock feature for a mockall-generated MockGitHubApi, or inject a fake process runner with GitHub::with_runner(processkit::ScriptedRunner::new()…):

use processkit::{Reply, ScriptedRunner};
use std::path::Path;
use vcs_github::{GitHub, GitHubApi};

# async fn demo() {
    let json = r#"[{"number":7,"title":"Add X","state":"OPEN"}]"#;
    let gh = GitHub::with_runner(ScriptedRunner::new().on(["pr", "list"], Reply::ok(json)));
    assert_eq!(gh.pr_list(Path::new(".")).await.unwrap()[0].number, 7);
# }

Requires the gh binary on PATH (authenticated via gh auth login).

License

MIT