Skip to content

Commit f85dd39

Browse files
fix: YAML path matching and legacy SSH URL support (#95)
* fix: YAML path matching and legacy SSH URL support - Strip leading '/' from ADO yamlFilename before comparing to local relative paths (ADO stores e.g. '/.azdo/pipelines/agent.yml') - Add legacy SSH URL format: git@vs-ssh.visualstudio.com:v3/... - Fix step numbering in run() comments (3→4, was skipping 4) - Add tests for YAML path matching with/without leading slash - Add test for legacy SSH URL parsing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: extract normalize_ado_yaml_path helper and update docs - Extract YAML path normalization into a named helper function so tests exercise the same code path as production (not duplicated logic) - Normalize separators before trimming leading slash (handles backslash paths) - Add backslash normalization test - Add legacy SSH format to parse_ado_remote doc comment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent fb1de50 commit f85dd39

1 file changed

Lines changed: 67 additions & 4 deletions

File tree

src/configure.rs

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,16 @@ pub struct AdoContext {
3232
/// - HTTPS: `https://dev.azure.com/{org}/{project}/_git/{repo}`
3333
/// - HTTPS (legacy): `https://{org}.visualstudio.com/{project}/_git/{repo}`
3434
/// - SSH: `git@ssh.dev.azure.com:v3/{org}/{project}/{repo}`
35+
/// - SSH (legacy): `git@vs-ssh.visualstudio.com:v3/{org}/{project}/{repo}`
3536
pub fn parse_ado_remote(remote_url: &str) -> Result<AdoContext> {
3637
let url = remote_url.trim();
3738

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

273+
/// Normalize an ADO YAML filename for comparison with local paths.
274+
///
275+
/// ADO's Build Definitions API stores `yamlFilename` with a leading `/`
276+
/// (e.g., `/.azdo/pipelines/agent.yml`) and may use backslashes on Windows.
277+
/// This strips the leading `/` and normalizes separators to forward slashes.
278+
fn normalize_ado_yaml_path(path: &str) -> String {
279+
path.replace('\\', "/").trim_start_matches('/').to_string()
280+
}
281+
268282
/// Match detected pipeline YAML files to ADO pipeline definitions.
269283
///
270284
/// Strategy:
@@ -290,12 +304,14 @@ async fn match_definitions(
290304
let yaml_path_str = pipeline.yaml_path.to_string_lossy();
291305
let yaml_path_normalized = yaml_path_str.replace('\\', "/");
292306

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

301317
if let Some(def) = path_match {
@@ -566,7 +582,7 @@ pub async fn run(
566582
}
567583
println!();
568584

569-
// Step 5: Update GITHUB_TOKEN
585+
// Step 4: Update GITHUB_TOKEN
570586
if dry_run {
571587
println!("Dry run \u{2014} no changes applied.");
572588
println!(
@@ -650,6 +666,15 @@ mod tests {
650666
assert_eq!(ctx.repo_name, "myrepo");
651667
}
652668

669+
#[test]
670+
fn test_parse_ado_remote_legacy_ssh() {
671+
let url = "git@vs-ssh.visualstudio.com:v3/myorg/myproject/myrepo";
672+
let ctx = parse_ado_remote(url).unwrap();
673+
assert_eq!(ctx.org_url, "https://dev.azure.com/myorg");
674+
assert_eq!(ctx.project, "myproject");
675+
assert_eq!(ctx.repo_name, "myrepo");
676+
}
677+
653678
#[test]
654679
fn test_parse_ado_remote_invalid() {
655680
assert!(parse_ado_remote("https://github.com/user/repo").is_err());
@@ -666,6 +691,44 @@ mod tests {
666691
}
667692
}
668693

694+
fn make_def_with_yaml(id: u64, name: &str, yaml_filename: &str) -> DefinitionSummary {
695+
DefinitionSummary {
696+
id,
697+
name: name.to_string(),
698+
process: Some(ProcessInfo {
699+
yaml_filename: Some(yaml_filename.to_string()),
700+
}),
701+
}
702+
}
703+
704+
// ==================== YAML path matching ====================
705+
706+
#[test]
707+
fn test_yaml_path_match_strips_leading_slash() {
708+
// ADO stores yamlFilename with a leading '/'
709+
assert_eq!(
710+
normalize_ado_yaml_path("/.azdo/pipelines/agent.yml"),
711+
".azdo/pipelines/agent.yml"
712+
);
713+
}
714+
715+
#[test]
716+
fn test_yaml_path_match_without_leading_slash() {
717+
// Some ADO instances may store without leading '/'
718+
assert_eq!(
719+
normalize_ado_yaml_path(".azdo/pipelines/agent.yml"),
720+
".azdo/pipelines/agent.yml"
721+
);
722+
}
723+
724+
#[test]
725+
fn test_yaml_path_match_backslash_normalization() {
726+
assert_eq!(
727+
normalize_ado_yaml_path("\\.azdo\\pipelines\\agent.yml"),
728+
".azdo/pipelines/agent.yml"
729+
);
730+
}
731+
669732
#[test]
670733
fn test_fuzzy_match_single_unambiguous() {
671734
let defs = vec![

0 commit comments

Comments
 (0)