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
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ Every compiled pipeline runs as three sequential jobs:
│ ├── fuzzy_schedule.rs # Fuzzy schedule parsing
│ ├── logging.rs # File-based logging infrastructure
│ ├── mcp.rs # SafeOutputs MCP server (stdio + HTTP)
│ ├── configure.rs # `configure` CLI command — orchestration shim atop `src/ado/`
│ ├── configure.rs # `configure` CLI command (deprecated) — hidden alias forwarding to `secrets set GITHUB_TOKEN`
│ ├── secrets.rs # `secrets set/list/delete` subcommand group — manages pipeline variables (never prints values from `list`)
│ ├── enable.rs # `enable` CLI command — registers ADO build definitions for compiled pipelines and ensures they are enabled
│ ├── 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)
Expand Down
25 changes: 17 additions & 8 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,23 @@ Global flags (apply to all subcommands): `--verbose, -v` (enable info-level logg
- `--ado-project <name>` - Azure DevOps project name override
- `--dry-run` - Validate inputs but skip ADO API calls (useful for local testing and QA review)

- `configure` - Detect agentic pipelines in a local repository and update the `GITHUB_TOKEN` pipeline variable on their Azure DevOps build definitions
- `--token <token>` / `GITHUB_TOKEN` env var - The new GITHUB_TOKEN value (prompted if omitted)
- `--org <url>` - Override: Azure DevOps organization URL (e.g. `https://dev.azure.com/myorg`) or just the org name (e.g. `myorg`, auto-prefixed to the canonical URL). 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 (prompted if omitted)
- `--path <path>` - Path to the repository root (defaults to current directory)
- `--dry-run` - Preview changes without applying them
- `--definition-ids <ids>` - Explicit pipeline definition IDs to update (comma-separated, skips auto-detection)
- `configure` *(deprecated; hidden in --help)* - Alias forwarding to `secrets set GITHUB_TOKEN`. Existing scripts keep working but get a stderr warning. The alias will be removed in the next minor release.

- `secrets set <name> [<value>] [PATH]` - Set a pipeline variable (with `isSecret=true`) on every matched ADO definition. Value resolution: positional `<value>` → `--value-stdin` (one line) → interactive tty prompt with echo off.
- `--allow-override` - Mark the variable as `allowOverride=true` (default: false).
- `--value-stdin` - Read the value from a single line on stdin.
- `--dry-run` - Print the planned set without calling the ADO API.
- `--org / --project / --pat` - ADO context overrides (same semantics as the other lifecycle commands).
- `--definition-ids <ids>` - Explicit pipeline definition IDs (comma-separated; skips local-fixture auto-detection).

- `secrets list [PATH]` - List variable names and their `isSecret` / `allowOverride` flags on every matched definition. **Never prints values.**
- `--json` - Emit machine-readable JSON.
- `--org / --project / --pat / --definition-ids` - As above.

- `secrets delete <name> [PATH]` - Delete the named variable from every matched definition. No-op when the variable is absent.
- `--dry-run` - Print the planned deletion plan without calling the ADO API.
- `--org / --project / --pat / --definition-ids` - As above.


- `enable [PATH]` - Register an ADO build definition for each compiled pipeline discovered under `PATH` (or the current directory) and ensure it is `enabled`. For each fixture, matches against the existing ADO definitions by `yamlFilename` first, then by sanitized display name; creates a new definition when neither matches, flips `queueStatus` to `enabled` when an existing definition is `disabled` / `paused`, and skips when it is already `enabled`. Fail-soft per fixture; exits non-zero if any fixture failed.
- `--org <url>` - Override: Azure DevOps organization (URL or bare org name). Inferred from git remote by default.
Expand Down
147 changes: 19 additions & 128 deletions src/configure.rs
Original file line number Diff line number Diff line change
@@ -1,70 +1,16 @@
//! The `configure` CLI command.
//! The `configure` CLI command (deprecated).
//!
//! Detects agentic pipelines in a local repository and updates the
//! `GITHUB_TOKEN` pipeline variable on their corresponding Azure DevOps
//! build definitions.
//!
//! Note: this command is being renamed to `secrets set GITHUB_TOKEN` as
//! part of the Phase 1 CLI overhaul. The current entry point remains the
//! orchestration shim below; all shared ADO REST logic lives in
//! [`crate::ado`].
//! Sets `GITHUB_TOKEN` on every matched ADO definition. This command
//! is retained as a hidden deprecation alias forwarding to
//! [`crate::secrets::run_set_github_token`]; new code should use
//! `ado-aw secrets set GITHUB_TOKEN <value>` instead.

use anyhow::{Context, Result};
use anyhow::Result;
use std::path::Path;

use crate::ado::{
AdoAuth, AdoContext, MatchedDefinition, resolve_ado_context, resolve_auth,
resolve_definitions, update_pipeline_variable,
};

/// Resolves the GitHub token from the CLI flag or an interactive prompt.
fn resolve_token(token: Option<&str>) -> Result<String> {
match token {
Some(t) => Ok(t.to_string()),
None => inquire::Password::new("Enter the new GITHUB_TOKEN:")
.without_confirmation()
.prompt()
.context("Failed to read token from interactive prompt"),
}
}

/// Updates the `GITHUB_TOKEN` variable on every matched pipeline
/// definition and reports per-definition success/failure.
async fn apply_token_updates(
client: &reqwest::Client,
ado_ctx: &AdoContext,
auth: &AdoAuth,
matched: &[MatchedDefinition],
token: &str,
) -> Result<()> {
println!("Updating GITHUB_TOKEN on matched definitions...");
let mut success_count = 0;
let mut failure_count = 0;

for m in matched {
match update_pipeline_variable(client, ado_ctx, auth, m.id, "GITHUB_TOKEN", token).await {
Ok(()) => {
println!(" \u{2713} Updated '{}' (id={})", m.name, m.id);
success_count += 1;
}
Err(e) => {
eprintln!(" \u{2717} Failed to update '{}' (id={}): {}", m.name, m.id, e);
failure_count += 1;
}
}
}

println!();
println!("Done: {} updated, {} failed.", success_count, failure_count);

if failure_count > 0 {
anyhow::bail!("{} definition(s) failed to update", failure_count);
}

Ok(())
}

/// Run the configure command.
/// Forwarder for the legacy `configure --token` invocation. Emits a
/// deprecation warning to stderr and forwards to the unified
/// `secrets set GITHUB_TOKEN` code path.
pub async fn run(
token: Option<&str>,
org: Option<&str>,
Expand All @@ -74,69 +20,14 @@ pub async fn run(
dry_run: bool,
definition_ids: Option<&[u64]>,
) -> Result<()> {
let repo_path = match path {
Some(p) => tokio::fs::canonicalize(p)
.await
.with_context(|| format!("Could not resolve path: {}", p.display()))?,
None => tokio::fs::canonicalize(".")
.await
.context("Could not resolve current directory")?,
};

let token = resolve_token(token)?;
let auth = resolve_auth(pat).await?;
let ado_ctx = resolve_ado_context(&repo_path, org, project).await?;

println!(
"ADO context: org={}, project={}{}",
ado_ctx.org_url,
ado_ctx.project,
if ado_ctx.repo_name.is_empty() {
String::new()
} else {
format!(", repo={}", ado_ctx.repo_name)
}
);
println!();

let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.context("Failed to create HTTP client")?;

let Some(matched) =
resolve_definitions(&client, &ado_ctx, &auth, definition_ids, &repo_path).await?
else {
return Ok(());
};

if matched.is_empty() {
println!("No matching ADO pipeline definitions found.");
println!("Make sure your pipelines are registered in Azure DevOps and point to the detected YAML files.");
return Ok(());
}

println!("{} definition(s) to update:", matched.len());
for m in &matched {
if m.yaml_path.is_empty() {
println!(" [{}] '{}' (id={})", m.match_method, m.name, m.id);
} else {
println!(
" [{}] '{}' (id={}) \u{2190} {}",
m.match_method, m.name, m.id, m.yaml_path
);
}
}
println!();

if dry_run {
println!("Dry run \u{2014} no changes applied.");
println!(
"Would update GITHUB_TOKEN on {} definition(s).",
matched.len()
);
return Ok(());
}

apply_token_updates(&client, &ado_ctx, &auth, &matched, &token).await
crate::secrets::run_set_github_token(
token,
org,
project,
pat,
path,
dry_run,
definition_ids,
)
.await
}
137 changes: 136 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,74 @@ mod ndjson;
pub mod runtimes;
pub mod sanitize;
mod safeoutputs;
mod secrets;
mod tools;
pub mod validate;

use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use std::path::{Path, PathBuf};

#[derive(Subcommand, Debug)]
enum SecretsCmd {
/// Set a pipeline variable on every matched definition (isSecret=true).
Set {
/// Variable name to set (e.g. `GITHUB_TOKEN`).
name: String,
/// Variable value. If omitted, falls back to `--value-stdin` or an
/// interactive tty prompt with echo off.
value: Option<String>,
/// Path to the repository root (defaults to current directory).
path: Option<PathBuf>,
#[arg(long)]
org: Option<String>,
#[arg(long)]
project: Option<String>,
#[arg(long, env = "AZURE_DEVOPS_EXT_PAT")]
pat: Option<String>,
/// Mark the variable as `allowOverride=true` (default: false).
#[arg(long)]
allow_override: bool,
/// Read the value from a single line on stdin.
#[arg(long)]
value_stdin: bool,
#[arg(long)]
dry_run: bool,
/// Explicit definition IDs (skips local-fixture auto-detection).
#[arg(long, value_delimiter = ',')]
definition_ids: Option<Vec<u64>>,
},
/// List variable names + flags on every matched definition. Never prints values.
List {
path: Option<PathBuf>,
#[arg(long)]
org: Option<String>,
#[arg(long)]
project: Option<String>,
#[arg(long, env = "AZURE_DEVOPS_EXT_PAT")]
pat: Option<String>,
#[arg(long)]
json: bool,
#[arg(long, value_delimiter = ',')]
definition_ids: Option<Vec<u64>>,
},
/// Delete a named variable from every matched definition.
Delete {
name: String,
path: Option<PathBuf>,
#[arg(long)]
org: Option<String>,
#[arg(long)]
project: Option<String>,
#[arg(long, env = "AZURE_DEVOPS_EXT_PAT")]
pat: Option<String>,
#[arg(long)]
dry_run: bool,
#[arg(long, value_delimiter = ',')]
definition_ids: Option<Vec<u64>>,
},
}

#[derive(Subcommand, Debug)]
enum Commands {
/// Compile markdown to pipeline definition (or recompile all detected pipelines)
Expand Down Expand Up @@ -115,7 +176,9 @@ enum Commands {
#[arg(long)]
force: bool,
},
/// Detect agentic pipelines and update GITHUB_TOKEN on their ADO definitions
/// (Deprecated) Set GITHUB_TOKEN on every matched ADO definition.
/// Use `secrets set GITHUB_TOKEN <value>` instead.
#[command(hide = true)]
Configure {
/// The new GITHUB_TOKEN value (defaults to GITHUB_TOKEN env var; prompted if omitted)
#[arg(long, env = "GITHUB_TOKEN")]
Expand All @@ -140,6 +203,11 @@ enum Commands {
#[arg(long, value_delimiter = ',')]
definition_ids: Option<Vec<u64>>,
},
/// Manage pipeline-variable secrets on every matched ADO definition.
Secrets {
#[command(subcommand)]
action: SecretsCmd,
},
/// Register an ADO build definition for each compiled pipeline and ensure it's enabled.
Enable {
/// Path to the repository root (defaults to current directory). Used
Expand Down Expand Up @@ -523,6 +591,7 @@ async fn main() -> Result<()> {
Some(Commands::McpHttp { .. }) => "mcp-http",
Some(Commands::Init { .. }) => "init",
Some(Commands::Configure { .. }) => "configure",
Some(Commands::Secrets { .. }) => "secrets",
Some(Commands::Enable { .. }) => "enable",
None => "ado-aw",
};
Expand Down Expand Up @@ -632,6 +701,72 @@ async fn main() -> Result<()> {
)
.await?;
}
Commands::Secrets { action } => match action {
SecretsCmd::Set {
name,
value,
path,
org,
project,
pat,
allow_override,
value_stdin,
dry_run,
definition_ids,
} => {
secrets::run_set(secrets::SetOptions {
name: &name,
value: value.as_deref(),
org: org.as_deref(),
project: project.as_deref(),
pat: pat.as_deref(),
path: path.as_deref(),
allow_override,
value_stdin,
dry_run,
definition_ids: definition_ids.as_deref(),
})
.await?;
}
SecretsCmd::List {
path,
org,
project,
pat,
json,
definition_ids,
} => {
secrets::run_list(secrets::ListOptions {
org: org.as_deref(),
project: project.as_deref(),
pat: pat.as_deref(),
path: path.as_deref(),
json,
definition_ids: definition_ids.as_deref(),
})
.await?;
}
SecretsCmd::Delete {
name,
path,
org,
project,
pat,
dry_run,
definition_ids,
} => {
secrets::run_delete(secrets::DeleteOptions {
name: &name,
org: org.as_deref(),
project: project.as_deref(),
pat: pat.as_deref(),
path: path.as_deref(),
dry_run,
definition_ids: definition_ids.as_deref(),
})
.await?;
}
},
Commands::Enable {
path,
org,
Expand Down
Loading
Loading