Skip to content

Commit a032b4e

Browse files
feat: add \configure\ command to detect pipelines and update GITHUB_TOKEN (#92)
* feat: add \configure\ command to detect pipelines and update GITHUB_TOKEN Add a new \�do-aw configure\ CLI command that: - Detects agentic pipelines by scanning YAML files for a new \# @ado-aw\ header - Infers ADO org/project from the git remote URL - Matches detected pipelines to ADO build definitions (by YAML path, then name) - Updates the GITHUB_TOKEN pipeline variable via the Build Definitions API The compiler now prepends a header comment to all compiled YAML output: # This file is auto-generated by ado-aw. Do not edit manually. # @ado-aw source=<input-path> version=<compiler-version> New modules: src/detect.rs (scanning/parsing), src/configure.rs (orchestration + ADO API). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address PR review feedback for configure command - Security: read --token and --pat from env vars (GITHUB_TOKEN, AZURE_DEVOPS_EXT_PAT) via clap's env attribute, avoiding exposure in process listings - Performance: use BufReader to read only first 5 lines in pipeline detection instead of loading entire files - Correctness: paginate ADO Build Definitions API using x-ms-continuationtoken header to handle projects with >100 pipelines - Correctness: tighten name-based fallback matching — warn on fuzzy matches, skip ambiguous ones (multiple candidates) - Correctness: replace std::process::exit(1) with anyhow::bail! to preserve Rust error handling and cleanup - Cleanup: remove hardcoded version string in user-facing message - Docs: add race condition comment on GET→PUT update cycle Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: prompt for GITHUB_TOKEN interactively when not provided The --token flag now falls back to an interactive password prompt (via inquire) when neither the CLI flag nor the GITHUB_TOKEN env var is set. This avoids exposing the token in shell history or process listings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address second round of PR review feedback - Fix header marker prefix matching: require exact '# @ado-aw ' with trailing space so '# @ado-aw-v2' is not falsely detected - Quote source path in header comment to support paths with spaces; parser handles both quoted and unquoted for backward compatibility - Strip newlines from input_path in header generation to prevent injection - Guard index access in integration test with lines.len() >= 2 assertion Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address third round of PR review feedback - URL-encode continuation token in ADO pagination (use reqwest .query()) - Escape double quotes in source path in header comment - Handle escaped quotes in header parser for correct round-trip - Extract fuzzy name matching into testable pure function - Add unit tests for fuzzy matching (single, ambiguous, none, empty, case) - Add tests for header comment quote escaping and round-trip Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address fourth round of PR review feedback - Preserve existing allowOverride when updating pipeline variable instead of silently hardcoding true (security posture fix) - Add interactive prompt fallback for --pat (matching --token behavior) - Add 30s timeout to reqwest HTTP client for better UX when ADO is slow - Canonicalize repo_path early for clearer error messages - Update --pat help text to recommend env var over CLI flag Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 3395b74 commit a032b4e

10 files changed

Lines changed: 1260 additions & 9 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ version = "0.4.0"
44
edition = "2024"
55

66
[dependencies]
7-
clap = { features = ["derive"], version = "4.5.40" }
7+
clap = { features = ["derive", "env"], version = "4.5.40" }
88
anyhow = "1.0.100"
99
async-trait = "0.1"
1010
chrono = "0.4"
@@ -23,3 +23,4 @@ env_logger = "0.11"
2323
regex-lite = "0.1"
2424
inquire = { version = "0.9.2", features = ["editor"] }
2525
terminal_size = "0.4.3"
26+
url = "2.5.8"

src/compile/common.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,34 @@ pub const AWF_VERSION: &str = "0.25.4";
462462
/// See: https://pkgs.dev.azure.com/msazuresphere/_packaging/Guardian1ESPTUpstreamOrgFeed/nuget/v3/index.json
463463
pub const COPILOT_CLI_VERSION: &str = "1.0.6";
464464

465+
/// Prefix used to identify agentic pipeline YAML files generated by ado-aw.
466+
pub const HEADER_MARKER: &str = "# @ado-aw";
467+
468+
/// Generate the header comment block prepended to all compiled pipeline YAML.
469+
///
470+
/// The header includes:
471+
/// - A human-readable "do not edit" warning
472+
/// - A machine-readable `@ado-aw` marker with source path and compiler version
473+
///
474+
/// The source path is the input path as provided to the compiler (e.g., `agents/my-agent.md`,
475+
/// `.azdo/pipelines/review.md`, or any other location the user chose). Path separators
476+
/// are normalized to forward slashes for cross-platform consistency.
477+
pub fn generate_header_comment(input_path: &std::path::Path) -> String {
478+
let version = env!("CARGO_PKG_VERSION");
479+
let source_path = input_path
480+
.to_string_lossy()
481+
.replace('\\', "/")
482+
.replace('\n', "")
483+
.replace('\r', "")
484+
.replace('"', "\\\"");
485+
486+
format!(
487+
"# This file is auto-generated by ado-aw. Do not edit manually.\n\
488+
# @ado-aw source=\"{}\" version={}\n",
489+
source_path, version
490+
)
491+
}
492+
465493
/// Generate source path for the execute command.
466494
///
467495
/// Returns a path using `{{ workspace }}` as the base, which gets resolved
@@ -988,4 +1016,27 @@ mod tests {
9881016
assert!(result.contains("SYSTEM_ACCESSTOKEN"));
9891017
assert!(result.contains("cancelling"));
9901018
}
1019+
1020+
// ─── generate_header_comment ────────────────────────────────────────────
1021+
1022+
#[test]
1023+
fn test_generate_header_comment_escapes_quotes() {
1024+
let path = std::path::Path::new("agents/my \"agent\".md");
1025+
let header = generate_header_comment(path);
1026+
assert!(
1027+
header.contains(r#"source="agents/my \"agent\".md""#),
1028+
"Quotes in path should be escaped: {}",
1029+
header
1030+
);
1031+
}
1032+
1033+
#[test]
1034+
fn test_generate_header_comment_round_trip_with_quotes() {
1035+
let path = std::path::Path::new("agents/my \"agent\".md");
1036+
let header = generate_header_comment(path);
1037+
let marker_line = header.lines().nth(1).expect("Should have second line");
1038+
let meta = crate::detect::parse_header_line(marker_line)
1039+
.expect("Should parse header with escaped quotes");
1040+
assert_eq!(meta.source, r#"agents/my "agent".md"#);
1041+
}
9911042
}

src/compile/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use std::path::{Path, PathBuf};
1818

1919
pub use common::parse_markdown;
2020
pub use common::sanitize_filename;
21+
pub use common::HEADER_MARKER;
2122
pub use types::{CompileTarget, FrontMatter, PermissionsConfig};
2223

2324
/// Trait for pipeline compilers.

src/compile/onees.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ use super::common::{
2020
self, AWF_VERSION, COPILOT_CLI_VERSION, DEFAULT_POOL, compute_effective_workspace, generate_copilot_params,
2121
generate_acquire_ado_token, generate_checkout_self, generate_checkout_steps,
2222
generate_ci_trigger, generate_copilot_ado_env, generate_executor_ado_env,
23-
generate_pipeline_path, generate_pipeline_resources, generate_pr_trigger,
24-
generate_repositories, generate_schedule, generate_source_path,
23+
generate_header_comment, generate_pipeline_path, generate_pipeline_resources,
24+
generate_pr_trigger, generate_repositories, generate_schedule, generate_source_path,
2525
generate_working_directory, replace_with_indent, validate_comment_target,
2626
validate_update_work_item_target, validate_write_permissions,
2727
};
@@ -194,6 +194,10 @@ displayName: "Finalize""#,
194194
);
195195
}
196196

197+
// Prepend header comment for pipeline detection
198+
let header = generate_header_comment(input_path);
199+
let pipeline_yaml = format!("{}{}", header, pipeline_yaml);
200+
197201
Ok(pipeline_yaml)
198202
}
199203
}

src/compile/standalone.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,11 @@ use super::common::{
1717
self, AWF_VERSION, COPILOT_CLI_VERSION, DEFAULT_POOL, compute_effective_workspace, generate_copilot_params,
1818
generate_acquire_ado_token, generate_cancel_previous_builds, generate_checkout_self,
1919
generate_checkout_steps, generate_ci_trigger, generate_copilot_ado_env,
20-
generate_executor_ado_env, generate_pipeline_path, generate_pipeline_resources,
21-
generate_pr_trigger, generate_repositories, generate_schedule, generate_source_path,
22-
generate_working_directory, replace_with_indent, sanitize_filename,
23-
validate_write_permissions,
24-
validate_comment_target,
25-
validate_update_work_item_target,
20+
generate_executor_ado_env, generate_header_comment, generate_pipeline_path,
21+
generate_pipeline_resources, generate_pr_trigger, generate_repositories,
22+
generate_schedule, generate_source_path, generate_working_directory,
23+
replace_with_indent, sanitize_filename, validate_write_permissions,
24+
validate_comment_target, validate_update_work_item_target,
2625
};
2726
use super::types::{FrontMatter, McpConfig};
2827
use crate::allowed_hosts::{CORE_ALLOWED_HOSTS, mcp_required_hosts};
@@ -198,6 +197,10 @@ impl Compiler for StandaloneCompiler {
198197
&firewall_config_json,
199198
);
200199

200+
// Prepend header comment for pipeline detection
201+
let header = generate_header_comment(input_path);
202+
let pipeline_yaml = format!("{}{}", header, pipeline_yaml);
203+
201204
Ok(pipeline_yaml)
202205
}
203206
}

0 commit comments

Comments
 (0)