Skip to content
Merged
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
71 changes: 67 additions & 4 deletions src/configure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,16 @@ pub struct AdoContext {
/// - HTTPS: `https://dev.azure.com/{org}/{project}/_git/{repo}`
/// - HTTPS (legacy): `https://{org}.visualstudio.com/{project}/_git/{repo}`
/// - SSH: `git@ssh.dev.azure.com:v3/{org}/{project}/{repo}`
/// - SSH (legacy): `git@vs-ssh.visualstudio.com:v3/{org}/{project}/{repo}`
pub fn parse_ado_remote(remote_url: &str) -> Result<AdoContext> {
let url = remote_url.trim();

// SSH format: git@ssh.dev.azure.com:v3/{org}/{project}/{repo}
if let Some(rest) = url.strip_prefix("git@ssh.dev.azure.com:v3/") {
// Also handles legacy: git@vs-ssh.visualstudio.com:v3/{org}/{project}/{repo}
if let Some(rest) = url
.strip_prefix("git@ssh.dev.azure.com:v3/")
.or_else(|| url.strip_prefix("git@vs-ssh.visualstudio.com:v3/"))
{
let parts: Vec<&str> = rest.splitn(3, '/').collect();
if parts.len() >= 3 {
let repo_name = parts[2].trim_end_matches(".git");
Expand Down Expand Up @@ -265,6 +270,15 @@ fn fuzzy_match_by_name(agent_name: &str, definitions: &[DefinitionSummary]) -> F
}
}

/// Normalize an ADO YAML filename for comparison with local paths.
///
/// ADO's Build Definitions API stores `yamlFilename` with a leading `/`
/// (e.g., `/.azdo/pipelines/agent.yml`) and may use backslashes on Windows.
/// This strips the leading `/` and normalizes separators to forward slashes.
fn normalize_ado_yaml_path(path: &str) -> String {
path.replace('\\', "/").trim_start_matches('/').to_string()
}

/// Match detected pipeline YAML files to ADO pipeline definitions.
///
/// Strategy:
Expand All @@ -290,12 +304,14 @@ async fn match_definitions(
let yaml_path_str = pipeline.yaml_path.to_string_lossy();
let yaml_path_normalized = yaml_path_str.replace('\\', "/");

// Strategy 1: Match by YAML filename in the definition
// Strategy 1: Match by YAML filename in the definition.
// ADO stores yamlFilename with a leading '/' (e.g., "/.azdo/pipelines/agent.yml"),
// so we strip it before comparing to the locally-detected relative path.
let path_match = definitions.iter().find(|d| {
d.process
.as_ref()
.and_then(|p| p.yaml_filename.as_ref())
.is_some_and(|f| f.replace('\\', "/") == yaml_path_normalized)
.is_some_and(|f| normalize_ado_yaml_path(f) == yaml_path_normalized)
});

if let Some(def) = path_match {
Expand Down Expand Up @@ -566,7 +582,7 @@ pub async fn run(
}
println!();

// Step 5: Update GITHUB_TOKEN
// Step 4: Update GITHUB_TOKEN
if dry_run {
println!("Dry run \u{2014} no changes applied.");
println!(
Expand Down Expand Up @@ -650,6 +666,15 @@ mod tests {
assert_eq!(ctx.repo_name, "myrepo");
}

#[test]
fn test_parse_ado_remote_legacy_ssh() {
let url = "git@vs-ssh.visualstudio.com:v3/myorg/myproject/myrepo";
let ctx = parse_ado_remote(url).unwrap();
assert_eq!(ctx.org_url, "https://dev.azure.com/myorg");
assert_eq!(ctx.project, "myproject");
assert_eq!(ctx.repo_name, "myrepo");
}

#[test]
fn test_parse_ado_remote_invalid() {
assert!(parse_ado_remote("https://github.com/user/repo").is_err());
Expand All @@ -666,6 +691,44 @@ mod tests {
}
}

fn make_def_with_yaml(id: u64, name: &str, yaml_filename: &str) -> DefinitionSummary {
DefinitionSummary {
id,
name: name.to_string(),
process: Some(ProcessInfo {
yaml_filename: Some(yaml_filename.to_string()),
}),
}
}

// ==================== YAML path matching ====================

#[test]
fn test_yaml_path_match_strips_leading_slash() {
// ADO stores yamlFilename with a leading '/'
assert_eq!(
normalize_ado_yaml_path("/.azdo/pipelines/agent.yml"),
".azdo/pipelines/agent.yml"
);
}

#[test]
fn test_yaml_path_match_without_leading_slash() {
// Some ADO instances may store without leading '/'
assert_eq!(
normalize_ado_yaml_path(".azdo/pipelines/agent.yml"),
".azdo/pipelines/agent.yml"
);
}

#[test]
fn test_yaml_path_match_backslash_normalization() {
assert_eq!(
normalize_ado_yaml_path("\\.azdo\\pipelines\\agent.yml"),
".azdo/pipelines/agent.yml"
);
}

#[test]
fn test_fuzzy_match_single_unambiguous() {
let defs = vec![
Expand Down
Loading