What you can do: check auth, view the repo, the full pull-request lifecycle (list/view/create/merge/ready/close, review/comment, CI checks, feedback), issues, releases, and GitHub Actions runs (list/view/watch, plus dispatch a workflow and rerun/cancel a run). This guide is the full reference — every command by theme, with examples.
vcs-github drives the GitHub CLI (gh) from Rust. Every operation is async,
runs inside an OS job (via processkit) so a gh subprocess is never
orphaned, and returns the structured processkit::Error instead of a stringly
exit. Commands that ask for --json are deserialized into typed structs; the
crate never scrapes human-readable output.
Consumers code against the [GitHubApi] trait and substitute a fake in tests —
the real [GitHub] client only appears at the edges. See
Testing & mocking for the two seams.
Requires the gh binary on PATH, authenticated via gh auth login. An
unauthenticated gh surfaces as an ErrorReason::Exit (gh's auth-required exit), not
a silent empty result.
use vcs_github::GitHub;
let gh = GitHub::new(); // GitHub<JobRunner> — the real job-backed clientGitHub::new() builds a client over processkit's real job-backed runner. Two
knobs and one test seam:
# use vcs_github::GitHub;
use std::time::Duration;
use processkit::testing::ScriptedRunner;
// Cap every spawned `gh` — a slow/hung command becomes `ErrorReason::Timeout`.
let gh = GitHub::new().default_timeout(Duration::from_secs(30));
// Inject a fake process executor instead of spawning `gh` (tests, CI).
let gh = GitHub::with_runner(ScriptedRunner::new());The timeout matters for blocking calls — see run_watch, which
parks for the lifetime of a CI run.
Most methods take a leading dir: &Path. When you make several calls against
one repo, bind it once and drop the argument:
# use vcs_github::{GitHub, GitHubApi};
use std::path::Path;
# async fn demo(repo: &Path) -> Result<(), processkit::Error> {
let gh = GitHub::new();
let at = gh.at(repo); // GitHubAt<'_, R> — Copy, cheap to pass around
let prs = at.pr_list().await?; // == gh.pr_list(repo)
let issues = at.issue_list().await?;
# Ok(()) }gh.at(dir) returns a [GitHubAt] — a Copy view holding two references. Its
bound methods produce byte-identical argv to the dir-taking calls (the crate
guards this with a test); the only difference is ergonomics. The genuinely
dir-independent methods (version, auth_status) forward verbatim. The raw
escape hatches (run/run_raw/run_args/run_raw_args) are bound to dir
on the view — gh.at(dir).run(…) runs in the bound repo's cwd; call run on the
GitHub client itself for the process-cwd form (see Raw escape
hatches).
GitHubApi::run/run_raw take &[String] (the trait must stay object-safe and
mockall-friendly). On the concrete GitHub, two inherent methods take string
slices so you skip the Vec<String> allocation:
# use vcs_github::GitHub;
# async fn demo() -> Result<(), processkit::Error> {
let gh = GitHub::new();
let out = gh.run_args(&["pr", "list"]).await?; // String — trimmed stdout
let res = gh.run_raw_args(&["pr", "list"]).await?; // ProcessResult<String> — no error on non-zero
# Ok(()) }Both are also available on the bound handle (gh.at(dir).run_args(…)) — where,
unlike on the client, they run in the bound dir (see Raw escape
hatches).
async fn version(&self) -> Result<String>; // `gh --version`
async fn auth_status(&self) -> Result<bool>; // `gh auth status` exits 0
async fn auth_status_for(&self, host: &GitHubHost) -> Result<bool>;// `gh auth status --hostname <host>`
async fn api(&self, dir: &Path, endpoint: &str) -> Result<String>;// `gh api <endpoint>` in `dir`
async fn repo_view(&self, dir: &Path) -> Result<RepoView>;// `gh repo view --json …`auth_status reads the exit code as a bool — gh auth status exits 0 when
authenticated, non-zero when not. But that is the only thing folded into the
bool: a spawn failure, a timeout, or any unexpected exit still errors rather
than reporting a silent false.
# use vcs_github::{GitHub, GitHubApi};
# async fn demo() -> Result<(), processkit::Error> {
let gh = GitHub::new();
match gh.auth_status().await {
Ok(true) => println!("authenticated"),
Ok(false) => println!("not logged in (run `gh auth login`)"),
Err(e) if matches!(e.reason(), processkit::ErrorReason::Timeout { .. }) => {
eprintln!("gh timed out")
}
Err(e) => eprintln!("{e}"),
}
# Ok(()) }gh reads a different credential environment variable per host — GH_TOKEN
for github.com, GH_ENTERPRISE_TOKEN for a GitHub Enterprise Server (GHES) host
— and its auth status can be scoped to one host. vcs-github models the target
host as a [GitHubHost] so a supplied credential lands in the variable gh
actually reads for that host, and an auth probe checks exactly the host you care
about.
# use vcs_github::{GitHub, GitHubApi, GitHubHost};
# async fn demo() -> Result<(), processkit::Error> {
// SaaS (the default): the token is injected as GH_TOKEN.
let saas = GitHubHost::github_com();
// A GHES host: derive it from the repo's remote (an unparseable/hostless remote
// is an Err, never a silent github.com fallback), or name it directly.
let ghes = GitHubHost::from_remote_url("https://ghe.example.com/acme/app.git")?;
let ghes = GitHubHost::new("ghe.example.com")?; // equivalent, from a bare host
// Bind the host + a token: for a GHES host the token goes to GH_ENTERPRISE_TOKEN
// (never GH_TOKEN) and GH_HOST is set, so an enterprise secret can't leak into the
// github.com env. One client per host keeps hosts isolated.
let gh = GitHub::new().with_host(ghes.clone()).with_token("ghe-pat");
// Probe auth for just that host — a broken session for another host can't turn
// this into a false negative for the one you target.
if !gh.auth_status_for(&ghes).await? {
eprintln!("not logged in to {}", ghes.as_str());
}
# let _ = saas; Ok(()) }with_host selects the credential env var (GH_TOKEN for github.com,
GH_ENTERPRISE_TOKEN for a GHES host) and pins GH_HOST; combine it with
with_token/with_env_token/with_credentials in either order. GH_HOST only
steers gh's host inference for commands with no repository context — a
repo-scoped method still resolves its host from the working directory's remote —
so use a host-bound client with repositories on that host. Without with_host the
client keeps the previous behaviour: github.com semantics, credential as
GH_TOKEN.
GitHubHost::new / from_remote_url reject an empty, malformed, or
undeterminable host with a diagnosable error rather than defaulting to github.com,
so an ambiguous host never quietly authenticates against the wrong server with the
github.com token. auth_status_for runs gh auth status --hostname <host> and,
like auth_status, folds only the exit code into the bool (a spawn failure or
timeout still errors).
api returns the raw REST/GraphQL response body unparsed — your escape hatch
to any endpoint the typed methods don't cover. The endpoint is guarded
against flag-injection: a leading - or an empty string is refused before
anything spawns (gh would otherwise parse gh api -evil as a flag).
repo_view flattens gh's nested owner/defaultBranchRef objects into a flat
[RepoView] — owner is the login string, default_branch is the ref name (empty
for an empty repository).
async fn pr_list(&self, dir: &Path) -> Result<Vec<PullRequest>>;
async fn pr_list_with(&self, dir: &Path, spec: PrList) -> Result<Vec<PullRequest>>;
async fn pr_list_for_branch(&self, dir: &Path, head: &str, base: &str) -> Result<Vec<PullRequest>>;
async fn pr_view(&self, dir: &Path, number: u64) -> Result<PullRequest>;
async fn pr_create(&self, dir: &Path, spec: PrCreate) -> Result<String>;
async fn pr_add_labels(&self, dir: &Path, number: u64, labels: &[String]) -> Result<()>;
async fn pr_remove_labels(&self, dir: &Path, number: u64, labels: &[String]) -> Result<()>;pr_list is the compatibility shorthand for PrList::new() (open, limit 100).
Use pr_list_with plus PrListState::{Open, Closed, Merged, All} and .limit(n)
to query history; a zero limit is rejected before spawning. pr_list_for_branch passes
--state all, so a closed or merged PR for the head→base pair is reported
too — branch on each entry's state. Empty when none match.
pr_create returns the new PR's URL (trimmed stdout). It takes a
PrCreate spec carrying the title/body and the optional head
(None = the current branch) and base (None = the repo default) branches;
each branch is appended as --head <b> / --base <b> only when set. Chain
.labels(vec![…]) to repeat --label <name> at creation; the add/remove methods
use gh pr edit --add-label / --remove-label for existing PRs.
# use vcs_github::{GitHub, GitHubApi, PrCreate};
use std::path::Path;
# async fn demo(repo: &Path) -> Result<(), processkit::Error> {
let gh = GitHub::new();
for pr in gh.pr_list_for_branch(repo, "feat/streaming", "main").await? {
println!("#{} [{}] {} — {}", pr.number, pr.state, pr.title, pr.url);
}
let url = gh
.pr_create(repo, PrCreate::new("Add streaming", "Implements …")
.head("feat/streaming").base("main"))
.await?;
println!("opened {url}");
# Ok(()) }async fn pr_merge(&self, dir: &Path, number: u64, merge: PrMerge) -> Result<()>;
async fn pr_mark_ready(&self, dir: &Path, number: u64) -> Result<()>;
async fn pr_close(&self, dir: &Path, number: u64, spec: PrClose) -> Result<()>;pr_merge takes a [PrMerge] config (strategy + optional --auto /
--delete-branch). pr_mark_ready flips a draft to ready-for-review. pr_close
takes a [PrClose] config and closes without merging, optionally deleting the
head branch (PrClose::new().delete_branch()).
# use vcs_github::{GitHub, GitHubApi, PrClose, PrMerge};
use std::path::Path;
# async fn demo(repo: &Path) -> Result<(), processkit::Error> {
let gh = GitHub::new();
gh.pr_mark_ready(repo, 7).await?;
gh.pr_merge(repo, 7, PrMerge::squash().delete_branch()).await?;
// or bail out:
gh.pr_close(repo, 8, PrClose::new().delete_branch()).await?; // --delete-branch
# Ok(()) }async fn pr_checks(&self, dir: &Path, number: u64) -> Result<Vec<CheckRun>>;
async fn pr_review(&self, dir: &Path, number: u64, action: ReviewAction) -> Result<()>;
async fn pr_comment(&self, dir: &Path, number: u64, body: &str) -> Result<String>;
async fn pr_feedback(&self, dir: &Path, number: u64) -> Result<PrFeedback>;
async fn pr_diff(&self, dir: &Path, number: u64) -> Result<Vec<FileDiff>>;pr_checks returns the PR's checks as Vec<CheckRun>. gh encodes the overall
outcome in its exit code — 0 all passed, 8 still pending, 1 some
failed — but prints the same JSON for all three, so the crate parses the list in
every case and lets you branch on each entry's bucket. A PR with
no checks at all (gh exits 1 with a "no checks reported" message and no JSON)
yields an empty list. Any other non-zero exit — no such PR, auth required,
timeout — is a genuine error. A JSON that fails to parse surfaces as
ErrorReason::Parse, never masked by the exit code.
# use vcs_github::{CheckBucket, GitHub, GitHubApi};
use std::path::Path;
# async fn demo(repo: &Path) -> Result<(), processkit::Error> {
let gh = GitHub::new();
for c in gh.pr_checks(repo, 7).await? {
match c.bucket {
CheckBucket::Fail | CheckBucket::Cancel => println!("✗ {} ({})", c.name, c.link),
CheckBucket::Pending => println!("… {}", c.name),
_ => {}
}
}
# Ok(()) }pr_review submits a review described by [ReviewAction]; the body lives in
the variant because gh requires one for request-changes and comment reviews.
pr_comment adds a conversation comment and returns its URL (--body is
mandatory — without it gh would drop into an interactive prompt and hang a
headless run). pr_feedback fetches the PR's submitted reviews and conversation
comments into a [PrFeedback], flattening gh's nested author objects (a deleted
account's null author becomes an empty login).
# use vcs_github::{GitHub, GitHubApi, ReviewAction};
use std::path::Path;
# async fn demo(repo: &Path) -> Result<(), processkit::Error> {
let gh = GitHub::new();
gh.pr_review(repo, 7, ReviewAction::request_changes("fix the parser")).await?;
let fb = gh.pr_feedback(repo, 7).await?;
for r in &fb.reviews { println!("{} {}", r.author, r.state); }
# Ok(()) }pr_diff returns the PR's diff as one [FileDiff] per changed file
(gh pr diff <n> --color never), parsed through the same unified-diff parser
vcs-git/vcs-jj
use — gh pr diff emits the same git-format diff git diff does, so
vcs-github re-exports [FileDiff] (and [ChangeKind], [Hunk], [DiffLine])
rather than depending on vcs-diff directly for the type alone.
# use vcs_github::{GitHub, GitHubApi};
use std::path::Path;
# async fn demo(repo: &Path) -> Result<(), processkit::Error> {
let gh = GitHub::new();
for f in gh.pr_diff(repo, 7).await? {
println!("{:?} {}", f.change, f.path);
}
# Ok(()) }async fn issue_list(&self, dir: &Path) -> Result<Vec<Issue>>;
async fn issue_list_with(&self, dir: &Path, spec: IssueList) -> Result<Vec<Issue>>;
async fn issue_view(&self, dir: &Path, number: u64) -> Result<Issue>;
async fn issue_create(&self, dir: &Path, title: &str, body: &str) -> Result<String>;
async fn issue_create_with(&self, dir: &Path, spec: IssueCreate) -> Result<String>;
async fn issue_add_labels(&self, dir: &Path, number: u64, labels: &[String]) -> Result<()>;
async fn issue_remove_labels(&self, dir: &Path, number: u64, labels: &[String]) -> Result<()>;issue_list means open issues with limit 100. issue_list_with accepts
IssueListState::{Open, Closed, All} plus .limit(n). Both fetch
number,title,state,body,url, so the listed issues carry
body/url too (see Issue); issue_view returns the same fields for
a single issue. issue_create returns the new issue's URL and preserves the
original string-based compatibility surface. issue_create_with accepts
IssueCreate::new(title, body).labels(vec![…]); existing labels are changed with
issue_add_labels / issue_remove_labels.
# use vcs_github::{GitHub, GitHubApi, IssueCreate};
use std::path::Path;
# async fn demo(repo: &Path) -> Result<(), processkit::Error> {
let gh = GitHub::new();
let url = gh.issue_create_with(repo,
IssueCreate::new("Flaky test", "`pr_checks` hangs on …")
.labels(vec!["bug".into(), "ci".into()])).await?;
let full = gh.issue_view(repo, 3).await?; // a single issue, body + url populated
# let _ = (url, full);
# Ok(()) }async fn workflow_list(&self, dir: &Path) -> Result<Vec<Workflow>>;
async fn workflow_list_with(&self, dir: &Path, spec: WorkflowList) -> Result<Vec<Workflow>>;
async fn workflow_view(&self, dir: &Path, selector: &str) -> Result<Workflow>;
async fn run_list(&self, dir: &Path, limit: u64, branch: Option<String>) -> Result<Vec<WorkflowRun>>;
async fn run_view(&self, dir: &Path, id: u64) -> Result<WorkflowRun>;
async fn run_watch(&self, dir: &Path, id: u64) -> Result<WorkflowRun>;
async fn workflow_dispatch(&self, dir: &Path, spec: WorkflowDispatch) -> Result<()>;
async fn run_rerun(&self, dir: &Path, id: u64, scope: RerunScope) -> Result<()>;
async fn run_cancel(&self, dir: &Path, id: u64) -> Result<()>;workflow_list returns up to 50 active workflow definitions. Use
workflow_list_with(WorkflowList::new().all().limit(n)) to include disabled
workflows and choose the cap. workflow_view resolves a numeric database id,
display name (case-insensitive), filename, or repository-relative path. Current
gh workflow view has no JSON exporter, so the typed method resolves against a
disabled-inclusive, fully paginated gh workflow list --json id,name,path,state
inventory; it never scrapes the human-readable summary. Missing or ambiguous
selectors return a structured parse error, and an empty selector is rejected
before spawning.
run_list returns recent runs, newest first, capped at limit; branch
(owned Option<String>, again for mockall) adds --branch <b> when Some.
run_view fetches one run by its id — which is [WorkflowRun::database_id],
not the URL number.
run_watch blocks until the run finishes, then reads its final state via a
follow-up run view. It deliberately omits gh's --exit-status: that flag
would fold the run's outcome onto the process exit code, which can't distinguish
a failed run from a cancelled one — the follow-up view's
conclusion can. A client default_timeout kills the watch
when it elapses (ErrorReason::Timeout), so drive run_watch from a client with no
(or a generous) timeout.
# use vcs_github::{GitHub, GitHubApi};
use std::path::Path;
# async fn demo(repo: &Path) -> Result<(), processkit::Error> {
let gh = GitHub::new(); // no default_timeout — the watch may park for minutes
let run = gh.run_watch(repo, 27023111945).await?;
match run.conclusion.as_str() {
"success" => println!("green"),
other => println!("ended: {other}"), // "failure", "cancelled", …
}
# Ok(()) }The three control verbs close the CI automation loop (start a workflow,
rerun a failed one, cancel a running one). All three return Result<()> and
follow gh's exit-code convention (gh help exit-codes): 0 on success, 1
on failure (surfaced as ErrorReason::Exit), 4 if gh is not authenticated.
workflow_dispatch fires a workflow_dispatch event (gh workflow run) via a
WorkflowDispatch spec — the workflow (its file or display
name), an optional target ref, and any number of key=value inputs. The
workflow file must declare an on: workflow_dispatch trigger. It returns
Result<()>, not a run URL: GitHub's dispatch API replies 204 No Content
with no run id (the dispatch is asynchronous — the run may not exist yet), so
poll run_list to find the run it started. Inputs are emitted with
--raw-field (not --field, whose @value reads a file — the raw form
keeps an input value like @/etc/passwd a literal string). The bare <workflow>
positional is flag-injection guarded like release_view's tag; the ref and
each key=value ride in flag-VALUE slots (consumed verbatim, like --branch),
so an input value may safely begin with -.
run_rerun reruns a completed run (gh run rerun <id>); pass a
RerunScope — All reruns every job, FailedOnly adds
--failed (only the failed jobs and their dependencies). run_cancel requests
cancellation of an in-progress run (gh run cancel <id>). Both take the run id
([WorkflowRun::database_id], a u64 — never flag-like, so unguarded) and are
themselves asynchronous: a rerun starts a new run, and cancellation returns
before jobs wind down — read the outcome back via run_view/run_watch (a
cancelled run's conclusion is "cancelled").
# use vcs_github::{GitHub, GitHubApi, RerunScope, WorkflowDispatch};
use std::path::Path;
# async fn demo(repo: &Path) -> Result<(), processkit::Error> {
let gh = GitHub::new();
// Kick off a deploy workflow on a tag, with two inputs.
gh.workflow_dispatch(
repo,
WorkflowDispatch::new("deploy.yml")
.git_ref("v1.4.0")
.field("environment", "staging")
.field("dry_run", "true"),
).await?;
gh.run_rerun(repo, 27023111945, RerunScope::FailedOnly).await?; // retry failures
gh.run_cancel(repo, 27023111946).await?; // stop a run
# Ok(()) }async fn release_list(&self, dir: &Path) -> Result<Vec<Release>>;
async fn release_view(&self, dir: &Path, tag: &str) -> Result<Release>;
async fn release_create(&self, dir: &Path, spec: ReleaseCreate) -> Result<String>; // returns the URL
async fn release_delete(&self, dir: &Path, tag: &str) -> Result<()>;release_list returns releases newest first; it does not request
body/url (both None — use release_view), but it is the only endpoint
that reports is_latest. release_view fills body/url (as Some)
for one tag but has no isLatest field, so is_latest defaults to false there.
The tag is flag-injection guarded like api's endpoint.
release_create (gh release create <tag> [--title] [--notes] [--draft] [--prerelease]) takes the [ReleaseCreate] spec — a constructor new(tag) plus
chained title / notes / draft / prerelease setters (#[non_exhaustive],
built by the ≥2 options → builder rule) — and returns the new release's URL. gh
creates the git tag from the default branch's latest state if it doesn't yet exist,
and requires notes when run non-interactively, so set notes (or drive
--notes-file/--generate-notes through run) for a headless create. Asset
uploads are out of scope — attach files with run if you need them.
release_delete (gh release delete <tag> --yes) deletes the release only, not the
underlying git tag (--yes skips gh's confirmation prompt so a headless delete never
hangs). Both mutators flag-injection-guard the bare <tag> positional like
release_view.
async fn run(&self, args: &[String]) -> Result<String>; // trimmed stdout; errors on non-zero
async fn run_raw(&self, args: &[String]) -> Result<ProcessResult<String>>; // never errors on non-zerorun runs gh <args> and returns trimmed stdout, erroring on a non-zero exit.
run_raw captures the full [ProcessResult] and never treats a non-zero exit
as an error — inspect .code() / .stdout() / .stderr() yourself. Use these
for any gh subcommand the typed API doesn't wrap. (The inherent &[&str]
variants run_args / run_raw_args are documented under
Construction.)
cwd (T-035). On the client (gh.run(…)) these run in the process's
current directory — supply the whole argv, so target a specific repo with -R owner/repo. On the bound view (gh.at(dir).run(…)) they are instead bound to
dir: the view forwards to the client's dir-taking run_in/run_raw_in/
run_args_in/run_raw_args_in, so a raw call through the handle runs in the bound
repo's cwd, like every other GitHubAt method (and like api). Reach for the
client's run when you deliberately want the process cwd.
All result structs are #[non_exhaustive] (match with .., construct via the
crate). Fields populated by some endpoints but not others come back as empty
strings/false, never panicking — note the per-method gaps below.
From pr_list / pr_list_for_branch / pr_view. Fields:
number: u64, title: String, state: String ("OPEN", "MERGED",
"CLOSED"), head_ref_name: String, base_ref_name: String, url: String.
From issue_list and issue_view (both fetch number, title, state,
body, url). Fields: number: u64, title: String, state: String,
body: String, url: String.
From run_list / run_view / run_watch. Fields: database_id: u64 (the
<run-id> other commands take), name: String, display_title: String,
status: String ("queued", "in_progress", "completed"),
conclusion: String ("success", "failure", "cancelled", "skipped") —
gh reports an empty string until the run completes (not null),
workflow_name: String, head_branch: String, event: String, url: String,
created_at: String (ISO 8601).
From workflow_list / workflow_list_with / workflow_view. Fields:
id: u64, name: String, path: String, and state: String ("active",
"disabled_manually", or "disabled_inactivity"; future values remain strings).
From pr_checks. Fields: name: String, state: String ("SUCCESS",
"FAILURE", "IN_PROGRESS", …), bucket: CheckBucket — gh's categorisation of
state and the field to branch on: the typed enum Pass/Fail/Pending/
Skipping/Cancel (+ an Unknown catch-all for forward compatibility), with
is_failing()/is_pending()/is_passing()/is_unknown() helpers; workflow: String (empty for non-Actions checks),
link: String, started_at: String — empty until the check starts,
completed_at: String — empty until it completes.
From release_list / release_view. Fields: tag_name: String,
name: String (may be empty), body: Option<String> — None from
release_list (it doesn't request the field; Some from release_view),
url: Option<String> — None from release_list (Some from
release_view), published_at: String (ISO 8601, empty for a draft),
is_draft: bool, is_prerelease: bool, is_latest: bool — only release_list
reports this; from release_view it defaults to false.
From pr_feedback (pr view --json reviews). Fields: author: String (login;
empty for a deleted account), state: String ("APPROVED",
"CHANGES_REQUESTED", "COMMENTED", "DISMISSED", "PENDING"),
body: String (may be empty), submitted_at: String (ISO 8601).
From pr_feedback (pr view --json comments). Fields: author: String (login;
empty for a deleted account), body: String, url: String,
created_at: String (ISO 8601).
From pr_feedback. Fields: reviews: Vec<Review> and comments: Vec<Comment>,
each in gh's order (oldest first).
From repo_view, flattening gh's nested objects. Fields: name: String,
owner: String (the login), description: Option<String> (None when GitHub
returns null), url: String, is_private: bool, default_branch: String
(empty for an empty repository).
#[non_exhaustive] enum naming gh's mutually exclusive strategy flags:
pub enum MergeStrategy {
Merge, // --merge (a merge commit)
Squash, // --squash (one commit)
Rebase, // --rebase (onto the base)
}The pr_merge options. #[non_exhaustive] — build
it through the strategy constructor, then chain the optional flags, rather than
a struct literal:
# use vcs_github::PrMerge;
let _ = PrMerge::merge(); // --merge
let _ = PrMerge::squash().delete_branch(); // --squash --delete-branch
let _ = PrMerge::rebase().auto(); // --rebase --auto
let _ = PrMerge::squash().auto().delete_branch(); // --squash --auto --delete-branchmerge() / squash() / rebase() pick the strategy (all default auto: false,
delete_branch: false); auto() enables --auto (merge once requirements are
met); delete_branch() enables --delete-branch. Public fields: strategy: MergeStrategy, auto: bool, delete_branch: bool.
The pr_create options. #[non_exhaustive]
with private-by-spec ergonomics — build through PrCreate::new(title, body) and
chain the optional branch setters rather than a struct literal:
# use vcs_github::PrCreate;
let _ = PrCreate::new("Add streaming", "Implements …"); // current branch → repo default
let _ = PrCreate::new("Add streaming", "Implements …")
.head("feat/streaming").base("main"); // --head feat/streaming --base mainnew(title, body) takes impl Into<String> (source/target left to gh's
defaults); .head(b) sets --head (the source branch), .base(b) sets --base
(the target). Public fields: title: String, body: String,
head: Option<String>, base: Option<String>.
What pr_review submits. Now a
#[non_exhaustive] struct with private fields, so the invariant holds by
construction — gh requires a body for request-changes/comment reviews, so those
are only reachable through the constructors that take one, and an empty-body
request-changes is unrepresentable. The review kind is a separate
ReviewKind enum read back via .kind().
# use vcs_github::{ReviewAction, ReviewKind};
let _ = ReviewAction::approve(); // --approve (no body)
let _ = ReviewAction::approve().with_body("LGTM"); // --approve --body LGTM
let _ = ReviewAction::request_changes("fix the parser"); // --request-changes --body <body>
let _ = ReviewAction::comment("nice"); // --comment --body <body>
let a = ReviewAction::approve().with_body("LGTM");
assert_eq!(a.kind(), ReviewKind::Approve);
assert_eq!(a.body(), Some("LGTM"));approve()— approve with no body; attach one with.with_body(b).request_changes(body)/comment(body)— gh requires the body, so it is taken by construction..with_body(body)— attach or replace the body (mainly to give an approve a message)..kind() -> ReviewKind/.body() -> Option<&str>— read the parts back.
#[non_exhaustive], Copy enum naming which review ReviewAction submits, read
back via ReviewAction::kind:
pub enum ReviewKind {
Approve, // --approve
RequestChanges, // --request-changes
Comment, // --comment
}What workflow_dispatch fires.
#[non_exhaustive] — build it through WorkflowDispatch::new(workflow) (the
≥2 options → builder rule: a target ref and inputs) plus the chained
setters, rather than a struct literal:
# use vcs_github::WorkflowDispatch;
let _ = WorkflowDispatch::new("ci.yml"); // default branch, no inputs
let _ = WorkflowDispatch::new("deploy.yml")
.git_ref("v1.4.0") // --ref v1.4.0
.field("environment", "staging") // --raw-field environment=staging
.field("dry_run", "true"); // --raw-field dry_run=truenew(workflow)— the workflow's file name (ci.yml) or display name (gh's bare positional). Flag-injection guarded..git_ref(r)— the branch/tag whose workflow-file version to run (--ref); omitted, gh uses the repository's default branch. (Namedgit_refbecauserefis a Rust keyword.).field(key, value)— add oneworkflow_dispatchinput; call once per input. Emitted as--raw-field key=valuein order added — the raw form, so a value beginning with@is a literal string, not a file read (gh's--field@syntax).
Public fields: workflow: String, git_ref: Option<String>,
fields: Vec<(String, String)>.
Which jobs run_rerun reruns — a
#[non_exhaustive], Copy enum passed directly (a single toggle doesn't reach
the builder bar):
pub enum RerunScope {
All, // gh run rerun <id> (every job)
FailedOnly, // gh run rerun <id> --failed (failed jobs + their dependencies)
}-
Supported CLI versions — the gh 2.0.0 floor, explicit preflight, and runner-provided integration coverage.
-
Testing & mocking — the
mockfeature (MockGitHubApi) and theScriptedRunnerseam. -
Process model & errors — OS-job containment, timeouts, and the
Error/ProcessResultshapes. -
crate docs — quickstart and crate-level docs.