diff --git a/hive-forge/src/verbs/ci_runs.rs b/hive-forge/src/verbs/ci_runs.rs index 6f36e0d7..ab023438 100644 --- a/hive-forge/src/verbs/ci_runs.rs +++ b/hive-forge/src/verbs/ci_runs.rs @@ -21,7 +21,10 @@ pub struct Args { /// Only runs of this workflow file (e.g. `ci.yml`). #[arg(long)] workflow: Option, - /// Only runs on this branch or ref (e.g. `main`, `damocles/foo`). + /// Only runs on this ref. A branch name (`main`, `damocles/foo`) or a + /// PR (`#N`) is qualified for you; a `refs/…` value is used as + /// given. An all-digit value is read as a PR number — to filter a + /// branch literally named that, pass `refs/heads/`. #[arg(long)] branch: Option, /// How many runs to print (default 20). @@ -40,7 +43,7 @@ pub fn run(client: &Client, args: Args) -> Result<()> { let (owner, name) = client.owner_repo()?; let query = ListActionRunsQuery { workflow_id: args.workflow.clone(), - r#ref: args.branch.clone(), + r#ref: args.branch.as_deref().map(qualify_ref), ..Default::default() }; let body = client @@ -65,6 +68,30 @@ pub fn run(client: &Client, args: Args) -> Result<()> { Ok(()) } +/// Qualify a `--branch` value into the full ref the runs-list filter +/// matches on. It only matches a complete ref, while the listing prints +/// `prettyref` (`main`, `#N`) — so without this every value a caller +/// can read off the output comes back as an empty result set, which is +/// indistinguishable from a branch that has never been built. +/// +/// A slash cannot be used to detect an already-qualified ref: branch +/// names contain slashes (`damocles/3932-merge-collisions-lint`), so +/// `refs/` is the only reliable marker and anything else is a branch. +/// +/// Deliberately not shared with `ci-rerun`, whose `ref` is a +/// `workflow_dispatch` body field taking a **bare** branch name. Same +/// field name, different domain; qualifying there would break it. +fn qualify_ref(value: &str) -> String { + if value.starts_with("refs/") { + return value.to_owned(); + } + let digits = value.strip_prefix('#').unwrap_or(value); + if !digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit()) { + return format!("refs/pull/{digits}/head"); + } + format!("refs/heads/{value}") +} + /// One human-readable line per run: number, status, workflow file, ref, /// title. Widths are cosmetic alignment, not a fixed schema — a longer /// value just pushes the next column over rather than truncating. @@ -76,3 +103,35 @@ fn print_row(run: &ActionRun) { let title = run.title.as_deref().unwrap_or(""); println!("{number:<7} {status:<10} {workflow:<20} {run_ref:<28} {title}"); } + +#[cfg(test)] +mod tests { + use super::qualify_ref; + + #[test] + fn qualifies_a_bare_branch_name() { + assert_eq!(qualify_ref("main"), "refs/heads/main"); + } + + #[test] + fn a_slash_does_not_imply_an_already_qualified_ref() { + assert_eq!(qualify_ref("damocles/foo"), "refs/heads/damocles/foo"); + } + + #[test] + fn reads_a_pr_number_as_its_pull_head_ref() { + assert_eq!(qualify_ref("#3967"), "refs/pull/3967/head"); // lint:allow test input + assert_eq!(qualify_ref("3967"), "refs/pull/3967/head"); + } + + #[test] + fn leaves_a_qualified_ref_alone() { + assert_eq!(qualify_ref("refs/heads/main"), "refs/heads/main"); + assert_eq!(qualify_ref("refs/pull/3967/merge"), "refs/pull/3967/merge"); + } + + #[test] + fn a_lone_hash_is_a_branch_name_not_an_empty_pr() { + assert_eq!(qualify_ref("#"), "refs/heads/#"); + } +}