diff --git a/docs/cli.md b/docs/cli.md index ef07ed50..f0ddee39 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -37,7 +37,7 @@ Global flags (apply to all subcommands): `--verbose, -v` (enable info-level logg - `configure` - Detect agentic pipelines in a local repository and update the `GITHUB_TOKEN` pipeline variable on their Azure DevOps build definitions - `--token ` / `GITHUB_TOKEN` env var - The new GITHUB_TOKEN value (prompted if omitted) - - `--org ` - Override: Azure DevOps organization URL (inferred from git remote by default) + - `--org ` - 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 ` - Override: Azure DevOps project name (inferred from git remote by default) - `--pat ` / `AZURE_DEVOPS_EXT_PAT` env var - PAT for ADO API authentication (prompted if omitted) - `--path ` - Path to the repository root (defaults to current directory) diff --git a/src/configure.rs b/src/configure.rs index 253f9788..c70e2834 100644 --- a/src/configure.rs +++ b/src/configure.rs @@ -652,6 +652,33 @@ async fn resolve_auth(pat: Option<&str>) -> Result { } } +/// Normalize a `--org` value to a full ADO organization URL. +/// +/// Users commonly pass just the org name (e.g. `myorg`) instead of the full +/// URL (`https://dev.azure.com/myorg`). Accept both forms by prefixing the +/// canonical `https://dev.azure.com/` host when the input has no scheme. +/// +/// Also accepts the legacy `{org}.visualstudio.com` form and rewrites it to +/// the modern `dev.azure.com/{org}` form for consistency with `parse_ado_remote`. +pub fn normalize_org_url(org: &str) -> String { + let trimmed = org.trim().trim_end_matches('/'); + + // Bare org name: no scheme, no dots — assume it's just the org. + if !trimmed.contains("://") && !trimmed.contains('/') && !trimmed.contains('.') { + return format!("https://dev.azure.com/{}", trimmed); + } + + // Legacy `https://{org}.visualstudio.com` → `https://dev.azure.com/{org}`. + if let Ok(url) = url::Url::parse(trimmed) + && let Some(host) = url.host_str() + && let Some(org) = host.strip_suffix(".visualstudio.com") + { + return format!("https://dev.azure.com/{}", org); + } + + trimmed.to_string() +} + /// Resolves the ADO context from the git remote (best-effort) with CLI overrides. /// Falls back to explicit `--org`/`--project` when the remote is absent or non-ADO. async fn resolve_ado_context( @@ -677,7 +704,7 @@ async fn resolve_ado_context( // Git remote parsed — apply overrides (Some(mut ctx), org, project) => { if let Some(org) = org { - ctx.org_url = org.to_string(); + ctx.org_url = normalize_org_url(org); } if let Some(project) = project { ctx.project = project.to_string(); @@ -688,7 +715,7 @@ async fn resolve_ado_context( (None, Some(org), Some(project)) => { info!("No ADO git remote; using --org and --project"); Ok(AdoContext { - org_url: org.to_string(), + org_url: normalize_org_url(org), project: project.to_string(), repo_name: String::new(), }) @@ -922,6 +949,52 @@ mod tests { assert!(parse_ado_remote("not-a-url").is_err()); } + // ==================== Org URL normalization ==================== + + #[test] + fn normalize_org_url_accepts_bare_name() { + assert_eq!( + normalize_org_url("myorg"), + "https://dev.azure.com/myorg" + ); + } + + #[test] + fn normalize_org_url_preserves_full_url() { + assert_eq!( + normalize_org_url("https://dev.azure.com/myorg"), + "https://dev.azure.com/myorg" + ); + } + + #[test] + fn normalize_org_url_strips_trailing_slash() { + assert_eq!( + normalize_org_url("https://dev.azure.com/myorg/"), + "https://dev.azure.com/myorg" + ); + } + + #[test] + fn normalize_org_url_rewrites_legacy_visualstudio() { + assert_eq!( + normalize_org_url("https://myorg.visualstudio.com"), + "https://dev.azure.com/myorg" + ); + assert_eq!( + normalize_org_url("https://myorg.visualstudio.com/"), + "https://dev.azure.com/myorg" + ); + } + + #[test] + fn normalize_org_url_trims_whitespace() { + assert_eq!( + normalize_org_url(" myorg "), + "https://dev.azure.com/myorg" + ); + } + // ==================== Fuzzy name matching ==================== fn make_def(id: u64, name: &str) -> DefinitionSummary { diff --git a/src/main.rs b/src/main.rs index 676a86a3..2cc3f75a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -118,7 +118,8 @@ enum Commands { /// The new GITHUB_TOKEN value (defaults to GITHUB_TOKEN env var; prompted if omitted) #[arg(long, env = "GITHUB_TOKEN")] token: Option, - /// Override: Azure DevOps organization URL (inferred from git remote by default) + /// 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, /// Override: Azure DevOps project name (inferred from git remote by default)