Same bug as the swarmctl/hivectl fix, a third instance argus's review didn't name but nix/checks.nix's hive-forge-docs freshness check (same pattern as hivectl-docs/swarmctl-docs) caught in CI: the earlier Contractions/Foreign/Auto batches edited docs/tools/forge-cli.md directly instead of the clap #[arg(...)]/doc-comment strings in hive-forge/src/main.rs and hive-forge/src/verbs/*.rs. Applied the same 13 wording changes to source that the earlier commits already made to the generated .md, matched 1:1 against 'git diff origin/main HEAD -- docs/tools/forge-cli.md' rather than guessed. Several source doc comments feed two rendered sections each (e.g. reaction.rs's one Add-variant doc renders under both 'issue reaction add' and 'pr reaction add', since both subcommands share the same enum) -- one source fix, two generated-doc fixes. Regenerated from the now-fixed source and confirmed byte-identical to what's already committed (diff exit 0) -- source and generated output are back in sync, same as the swarmctl/hivectl fix. cargo clippy -p hive-forge --all-targets -- -D warnings and scripts/check-doc-refs.sh both clean.
137 lines
5.1 KiB
Rust
137 lines
5.1 KiB
Rust
//! `ci-runs [--workflow <file>] [--branch <name>] [--limit N] [--page N]` —
|
|
//! list a repo's CI Actions runs, newest first.
|
|
//!
|
|
//! A `ci-rerun` dispatch creates a `workflow_dispatch` run that publishes
|
|
//! no commit status, so there was previously no way from the CLI to see
|
|
//! what it actually created — or to discover a real run number to hand
|
|
//! `ci-log`/`artifact-get` at all, short of guessing near a known one.
|
|
//! This lists runs directly; `--workflow`/`--branch` filter server-side
|
|
//! via `ListActionRunsQuery`, not a client-side scan.
|
|
|
|
use anyhow::Result;
|
|
use clap::Args as ClapArgs;
|
|
use forgejo_api::structs::{ActionRun, ListActionRunsQuery};
|
|
|
|
use super::ci_common::run_number_of;
|
|
use crate::client::Client;
|
|
use crate::verbs::print_json;
|
|
|
|
#[derive(ClapArgs)]
|
|
pub struct Args {
|
|
/// Only runs of this workflow file (for example `ci.yml`).
|
|
#[arg(long)]
|
|
workflow: Option<String>,
|
|
/// 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/<name>`.
|
|
#[arg(long)]
|
|
branch: Option<String>,
|
|
/// How many runs to print (default 20).
|
|
#[arg(long, default_value_t = 20, value_parser = clap::value_parser!(u64).range(1..))]
|
|
limit: u64,
|
|
/// Page number (1-based, default 1). Combine with `--limit` to page
|
|
/// through further back than the default window.
|
|
#[arg(long, default_value_t = 1, value_parser = clap::value_parser!(u64).range(1..))]
|
|
page: u64,
|
|
}
|
|
|
|
/// # Errors
|
|
///
|
|
/// Returns an error if the list request fails (network, or a non-2xx).
|
|
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.as_deref().map(qualify_ref),
|
|
..Default::default()
|
|
};
|
|
let body = client
|
|
.api()
|
|
.list_action_runs(owner, name, query)
|
|
.page(u32::try_from(args.page).unwrap_or(u32::MAX))
|
|
.page_size(u32::try_from(args.limit).unwrap_or(u32::MAX))
|
|
.send()?;
|
|
let runs = body.workflow_runs.unwrap_or_default();
|
|
|
|
if client.json_mode() {
|
|
return print_json(&serde_json::to_value(&runs)?);
|
|
}
|
|
|
|
if runs.is_empty() {
|
|
println!("no matching runs in {}", client.repo()?);
|
|
return Ok(());
|
|
}
|
|
for run in &runs {
|
|
print_row(run);
|
|
}
|
|
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.
|
|
fn print_row(run: &ActionRun) {
|
|
let number = run_number_of(run).map_or_else(|| "?".to_owned(), |n| format!("#{n}"));
|
|
let status = run.status.as_deref().unwrap_or("unknown");
|
|
let workflow = run.workflow_id.as_deref().unwrap_or("?");
|
|
let run_ref = run.prettyref.as_deref().unwrap_or("?");
|
|
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/#");
|
|
}
|
|
}
|