Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ Every compiled pipeline runs as three sequential jobs:
│ ├── configure.rs # `configure` CLI command — orchestration shim atop `src/ado/`
│ ├── enable.rs # `enable` CLI command — registers ADO build definitions for compiled pipelines and ensures they are enabled
│ ├── list.rs # `list` CLI command — renders matched ADO definitions with their latest-run state (text or JSON)
│ ├── status.rs # `status` CLI command — denser per-pipeline status block (thin renderer over `list`'s data path)
│ ├── ado/ # Shared Azure DevOps REST helpers (auth, list/match/PATCH/POST)
│ │ └── mod.rs # Used by `configure` and the lifecycle commands (enable, disable, remove, list, run, status, secrets)
│ ├── detect.rs # Agentic pipeline detection (helper for `configure`)
Expand Down
6 changes: 6 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,9 @@ Global flags (apply to all subcommands): `--verbose, -v` (enable info-level logg
- `--pat <pat>` / `AZURE_DEVOPS_EXT_PAT` env var - PAT for ADO API authentication (Azure CLI fallback if omitted).
- `--all` - Include ADO definitions that do not match any local fixture.
- `--json` - Emit machine-readable JSON.

- `status [PATH]` - Per-pipeline status: name, id, folder, `queueStatus`, latest-run summary, and a deep link — one block per matched definition. Read-only. `--json` emits the same shape as `list --json` so scripts can use either.
- `--org <url>` - Override: Azure DevOps organization (URL or bare org name). Inferred from git remote by default.
- `--project <name>` - Override: Azure DevOps project name (inferred from git remote by default).
- `--pat <pat>` / `AZURE_DEVOPS_EXT_PAT` env var - PAT for ADO API authentication (Azure CLI fallback if omitted).
- `--json` - Emit machine-readable JSON (same shape as `list --json`).
44 changes: 28 additions & 16 deletions src/ado/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -370,24 +370,14 @@ pub fn normalize_ado_yaml_path(path: &str) -> String {
/// Strategy:
/// 1. Try to match by the `yamlFilename` field in the definition's process config
/// 2. Fall back to matching by pipeline name containing the agent name
pub async fn match_definitions(
client: &reqwest::Client,
ctx: &AdoContext,
auth: &AdoAuth,
pub fn match_definitions_in(
definitions: &[DefinitionSummary],
detected: &[detect::DetectedPipeline],
) -> Result<Vec<MatchedDefinition>> {
let definitions = list_definitions(client, ctx, auth).await?;
info!(
"Found {} pipeline definitions in {}/{}",
definitions.len(),
ctx.org_url,
ctx.project
);

) -> Vec<MatchedDefinition> {
let mut matched = Vec::new();

// Log all definition yaml paths for debugging
for def in &definitions {
for def in definitions {
let yaml_path = def
.process
.as_ref()
Expand Down Expand Up @@ -440,7 +430,7 @@ pub async fn match_definitions(
.and_then(|s| s.to_str())
.unwrap_or("");

match fuzzy_match_by_name(agent_name, &definitions) {
match fuzzy_match_by_name(agent_name, definitions) {
FuzzyMatchResult::Single(idx) => {
let def = &definitions[idx];
eprintln!(
Expand Down Expand Up @@ -473,7 +463,29 @@ pub async fn match_definitions(
);
}

Ok(matched)
matched
}

/// Match detected pipeline YAML files to ADO pipeline definitions.
///
/// Strategy:
/// 1. Try to match by the `yamlFilename` field in the definition's process config
/// 2. Fall back to matching by pipeline name containing the agent name
pub async fn match_definitions(
client: &reqwest::Client,
ctx: &AdoContext,
auth: &AdoAuth,
detected: &[detect::DetectedPipeline],
) -> Result<Vec<MatchedDefinition>> {
let definitions = list_definitions(client, ctx, auth).await?;
info!(
"Found {} pipeline definitions in {}/{}",
definitions.len(),
ctx.org_url,
ctx.project
);

Ok(match_definitions_in(&definitions, detected))
}

/// Fetch the human-readable name of a pipeline definition by ID.
Expand Down
32 changes: 17 additions & 15 deletions src/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ use std::collections::HashSet;
use std::path::{Path, PathBuf};

use crate::ado::{
AdoAuth, AdoContext, DefinitionSummary, MatchedDefinition, get_latest_build,
list_definitions, match_definitions, resolve_ado_context, resolve_auth,
DefinitionSummary, MatchedDefinition, get_latest_build, list_definitions,
match_definitions_in, resolve_ado_context, resolve_auth,
};
use crate::detect;

Expand Down Expand Up @@ -217,20 +217,17 @@ pub fn render_json(rows: &[ListRow]) -> Result<String> {
let array: Vec<serde_json::Value> = rows
.iter()
.map(|r| {
// Keep this as a raw pass-through for scripting stability.
// Text output trims the leading slash for readability; JSON
// intentionally retains ADO/local-matcher path shape.
serde_json::json!({
"name": r.name,
"id": r.id,
"folder": r.folder,
"queueStatus": r.queue_status,
"yamlFilename": r.yaml_filename,
"matched": r.matched,
"lastRun": r.last_run.as_ref().map(|lr| serde_json::json!({
"id": lr.id,
"result": lr.result,
"status": lr.status,
"finishTime": lr.finish_time,
"url": lr.url,
})),
"lastRun": r.last_run.as_ref().map(|lr| &lr.raw),
})
})
.collect();
Expand Down Expand Up @@ -258,9 +255,7 @@ pub async fn run(opts: ListOptions<'_>) -> Result<()> {

let definitions = list_definitions(&client, &ado_ctx, &auth).await?;
let detected = detect::detect_pipelines(&repo_path).await.unwrap_or_default();
let matched = match_definitions(&client, &ado_ctx, &auth, &detected)
.await
.unwrap_or_default();
let matched = match_definitions_in(&definitions, &detected);

// Decide which IDs need a last-build fetch.
let target_ids: HashSet<u64> = if opts.all {
Expand Down Expand Up @@ -478,6 +473,14 @@ mod tests {

#[test]
fn render_json_emits_expected_shape() {
let raw = serde_json::json!({
"id": 999,
"result": "succeeded",
"status": "completed",
"finishTime": "2026-05-17T08:00:00Z",
"requestedFor": { "displayName": "A User" },
"triggerInfo": { "ci.sourceSha": "abc123" },
});
let rows = vec![ListRow {
id: 123,
name: "Daily noop".to_string(),
Expand All @@ -491,7 +494,7 @@ mod tests {
status: Some("completed".to_string()),
finish_time: Some("2026-05-17T08:00:00Z".to_string()),
url: Some("https://dev.azure.com/.../999".to_string()),
raw: serde_json::Value::Null,
raw: raw.clone(),
}),
}];
let out = render_json(&rows).unwrap();
Expand All @@ -502,8 +505,7 @@ mod tests {
assert_eq!(parsed[0]["queueStatus"], "enabled");
assert_eq!(parsed[0]["yamlFilename"], "/tests/noop.lock.yml");
assert_eq!(parsed[0]["matched"], true);
assert_eq!(parsed[0]["lastRun"]["id"], 999);
assert_eq!(parsed[0]["lastRun"]["result"], "succeeded");
assert_eq!(parsed[0]["lastRun"], raw);
}

#[test]
Expand Down
38 changes: 38 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ mod ndjson;
pub mod runtimes;
pub mod sanitize;
mod safeoutputs;
mod status;
mod tools;
pub mod validate;

Expand Down Expand Up @@ -198,6 +199,26 @@ enum Commands {
#[arg(long)]
json: bool,
},
/// Per-pipeline status: queueStatus + latest-run summary, for every matched definition.
Status {
/// Path to the repository root (defaults to current directory). Used
/// to auto-discover compiled pipelines, same as `compile`.
path: Option<PathBuf>,
/// Override: Azure DevOps organization (URL like `https://dev.azure.com/myorg`,
/// or just the org name `myorg`). Inferred from git remote by default.
#[arg(long)]
org: Option<String>,
/// Override: Azure DevOps project name (inferred from git remote by default).
#[arg(long)]
project: Option<String>,
/// PAT for ADO API authentication (prefer setting AZURE_DEVOPS_EXT_PAT env var;
/// Azure CLI fallback if omitted).
#[arg(long, env = "AZURE_DEVOPS_EXT_PAT")]
pat: Option<String>,
/// Emit machine-readable JSON (same shape as `list --json`).
#[arg(long)]
json: bool,
},
}

#[derive(Parser, Debug)]
Expand Down Expand Up @@ -549,6 +570,7 @@ async fn main() -> Result<()> {
Some(Commands::Configure { .. }) => "configure",
Some(Commands::Enable { .. }) => "enable",
Some(Commands::List { .. }) => "list",
Some(Commands::Status { .. }) => "status",
None => "ado-aw",
};

Expand Down Expand Up @@ -699,6 +721,22 @@ async fn main() -> Result<()> {
})
.await?;
}
Commands::Status {
path,
org,
project,
pat,
json,
} => {
status::run(status::StatusOptions {
org: org.as_deref(),
project: project.as_deref(),
pat: pat.as_deref(),
path: path.as_deref(),
json,
})
.await?;
}
}
Ok(())
}
Expand Down
Loading