Skip to content
Merged
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
2 changes: 1 addition & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <token>` / `GITHUB_TOKEN` env var - The new GITHUB_TOKEN value (prompted if omitted)
- `--org <url>` - Override: Azure DevOps organization URL (inferred from git remote by default)
- `--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)
Expand Down
77 changes: 75 additions & 2 deletions src/configure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,33 @@ async fn resolve_auth(pat: Option<&str>) -> Result<AdoAuth> {
}
}

/// 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(
Expand All @@ -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();
Expand All @@ -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(),
})
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
/// 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<String>,
/// Override: Azure DevOps project name (inferred from git remote by default)
Expand Down
Loading