From 2e0c94b44958478f8c6c8bd5115fb36bb023cd6e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 14:32:46 +0000 Subject: [PATCH 1/3] feat(cli): exempt githubnext/ado-aw repo from GitHub remote guard Agent-Logs-Url: https://github.com/githubnext/ado-aw/sessions/e8a46b21-15ab-4b8c-abe5-215eff7bd7fe Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com> --- src/main.rs | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 1b2985e3..9326f7a7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -189,6 +189,39 @@ fn is_github_remote(remote_url: &str) -> bool { .is_some_and(|host| host.eq_ignore_ascii_case("github.com")) } +/// Returns true if the remote URL points at the `githubnext/ado-aw` +/// repository itself. This is the canonical home of `ado-aw`, and it is +/// expected that maintainers run `ado-aw compile`/`init` against it (for +/// example, to regenerate fixtures or examples) even though the origin is on +/// GitHub rather than Azure DevOps. +fn is_ado_aw_repo_remote(remote_url: &str) -> bool { + let url = remote_url.trim(); + + // Strip an optional trailing `.git` and split off the owner/repo path. + fn matches_path(path: &str) -> bool { + let trimmed = path.trim_matches('/'); + let trimmed = trimmed.strip_suffix(".git").unwrap_or(trimmed); + trimmed.eq_ignore_ascii_case("githubnext/ado-aw") + } + + if let Some(rest) = url.strip_prefix("git@github.com:") { + return matches_path(rest); + } + if let Some(rest) = url.strip_prefix("ssh://git@github.com/") { + return matches_path(rest); + } + + if let Ok(parsed) = url::Url::parse(url) + && parsed + .host_str() + .is_some_and(|host| host.eq_ignore_ascii_case("github.com")) + { + return matches_path(parsed.path()); + } + + false +} + async fn ensure_non_github_remote_for_ado_aw(command_name: &str, repo_path: &Path) -> Result<()> { // Integration tests invoke this binary from the ado-aw repository itself, // which is intentionally hosted on GitHub. @@ -203,6 +236,13 @@ async fn ensure_non_github_remote_for_ado_aw(command_name: &str, repo_path: &Pat }; if is_github_remote(&remote_url) { + if is_ado_aw_repo_remote(&remote_url) { + // The `githubnext/ado-aw` repository is the home of this tool. + // Allow `ado-aw` commands to run there so maintainers can exercise + // the CLI against examples and fixtures in-tree. + return Ok(()); + } + anyhow::bail!( "Cannot run `ado-aw {}` in a GitHub repository (origin: {}). \ `ado-aw` is for Azure DevOps repositories. \ @@ -478,7 +518,7 @@ async fn main() -> Result<()> { #[cfg(test)] mod tests { - use super::is_github_remote; + use super::{is_ado_aw_repo_remote, is_github_remote}; #[test] fn detects_github_https_remote() { @@ -508,4 +548,34 @@ mod tests { fn does_not_flag_non_github_remote() { assert!(!is_github_remote("https://gitlab.com/owner/repo.git")); } + + #[test] + fn detects_ado_aw_repo_https_remote() { + assert!(is_ado_aw_repo_remote( + "https://github.com/githubnext/ado-aw.git" + )); + assert!(is_ado_aw_repo_remote("https://github.com/githubnext/ado-aw")); + assert!(is_ado_aw_repo_remote( + "https://github.com/GitHubNext/ADO-AW.git" + )); + } + + #[test] + fn detects_ado_aw_repo_ssh_remote() { + assert!(is_ado_aw_repo_remote("git@github.com:githubnext/ado-aw.git")); + assert!(is_ado_aw_repo_remote( + "ssh://git@github.com/githubnext/ado-aw.git" + )); + } + + #[test] + fn does_not_flag_other_github_repo_as_ado_aw() { + assert!(!is_ado_aw_repo_remote( + "https://github.com/owner/other-repo.git" + )); + assert!(!is_ado_aw_repo_remote("git@github.com:owner/ado-aw.git")); + assert!(!is_ado_aw_repo_remote( + "https://dev.azure.com/myorg/myproject/_git/ado-aw" + )); + } } From 7efc689cbcc673ec74cc2839b93d055d7986d9b8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 14:47:39 +0000 Subject: [PATCH 2/3] feat(cli): allow `init --force` to bypass GitHub remote guard Agent-Logs-Url: https://github.com/githubnext/ado-aw/sessions/894f7a19-183c-4f52-abe7-3fff415b43ed Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com> --- src/main.rs | 79 +++++------------------------------------------------ 1 file changed, 7 insertions(+), 72 deletions(-) diff --git a/src/main.rs b/src/main.rs index 9326f7a7..a744f9dc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -189,39 +189,6 @@ fn is_github_remote(remote_url: &str) -> bool { .is_some_and(|host| host.eq_ignore_ascii_case("github.com")) } -/// Returns true if the remote URL points at the `githubnext/ado-aw` -/// repository itself. This is the canonical home of `ado-aw`, and it is -/// expected that maintainers run `ado-aw compile`/`init` against it (for -/// example, to regenerate fixtures or examples) even though the origin is on -/// GitHub rather than Azure DevOps. -fn is_ado_aw_repo_remote(remote_url: &str) -> bool { - let url = remote_url.trim(); - - // Strip an optional trailing `.git` and split off the owner/repo path. - fn matches_path(path: &str) -> bool { - let trimmed = path.trim_matches('/'); - let trimmed = trimmed.strip_suffix(".git").unwrap_or(trimmed); - trimmed.eq_ignore_ascii_case("githubnext/ado-aw") - } - - if let Some(rest) = url.strip_prefix("git@github.com:") { - return matches_path(rest); - } - if let Some(rest) = url.strip_prefix("ssh://git@github.com/") { - return matches_path(rest); - } - - if let Ok(parsed) = url::Url::parse(url) - && parsed - .host_str() - .is_some_and(|host| host.eq_ignore_ascii_case("github.com")) - { - return matches_path(parsed.path()); - } - - false -} - async fn ensure_non_github_remote_for_ado_aw(command_name: &str, repo_path: &Path) -> Result<()> { // Integration tests invoke this binary from the ado-aw repository itself, // which is intentionally hosted on GitHub. @@ -236,13 +203,6 @@ async fn ensure_non_github_remote_for_ado_aw(command_name: &str, repo_path: &Pat }; if is_github_remote(&remote_url) { - if is_ado_aw_repo_remote(&remote_url) { - // The `githubnext/ado-aw` repository is the home of this tool. - // Allow `ado-aw` commands to run there so maintainers can exercise - // the CLI against examples and fixtures in-tree. - return Ok(()); - } - anyhow::bail!( "Cannot run `ado-aw {}` in a GitHub repository (origin: {}). \ `ado-aw` is for Azure DevOps repositories. \ @@ -489,7 +449,12 @@ async fn main() -> Result<()> { } Commands::Init { path, force } => { let init_path = path.as_deref().unwrap_or(Path::new(".")); - ensure_non_github_remote_for_ado_aw("init", init_path).await?; + // `--force` bypasses the GitHub-remote guard so maintainers can + // run `ado-aw init` inside this repository (or other GitHub-hosted + // forks) for development and example regeneration. + if !force { + ensure_non_github_remote_for_ado_aw("init", init_path).await?; + } init::run(path.as_deref(), force).await?; } Commands::Configure { @@ -518,7 +483,7 @@ async fn main() -> Result<()> { #[cfg(test)] mod tests { - use super::{is_ado_aw_repo_remote, is_github_remote}; + use super::is_github_remote; #[test] fn detects_github_https_remote() { @@ -548,34 +513,4 @@ mod tests { fn does_not_flag_non_github_remote() { assert!(!is_github_remote("https://gitlab.com/owner/repo.git")); } - - #[test] - fn detects_ado_aw_repo_https_remote() { - assert!(is_ado_aw_repo_remote( - "https://github.com/githubnext/ado-aw.git" - )); - assert!(is_ado_aw_repo_remote("https://github.com/githubnext/ado-aw")); - assert!(is_ado_aw_repo_remote( - "https://github.com/GitHubNext/ADO-AW.git" - )); - } - - #[test] - fn detects_ado_aw_repo_ssh_remote() { - assert!(is_ado_aw_repo_remote("git@github.com:githubnext/ado-aw.git")); - assert!(is_ado_aw_repo_remote( - "ssh://git@github.com/githubnext/ado-aw.git" - )); - } - - #[test] - fn does_not_flag_other_github_repo_as_ado_aw() { - assert!(!is_ado_aw_repo_remote( - "https://github.com/owner/other-repo.git" - )); - assert!(!is_ado_aw_repo_remote("git@github.com:owner/ado-aw.git")); - assert!(!is_ado_aw_repo_remote( - "https://dev.azure.com/myorg/myproject/_git/ado-aw" - )); - } } From 16a283ce62fdcd8f13d0072e8e2550a8c057017e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 14:53:18 +0000 Subject: [PATCH 3/3] feat(cli): make init always overwrite; --force only bypasses GitHub guard Agent-Logs-Url: https://github.com/githubnext/ado-aw/sessions/0ad2fb85-cc7d-49fe-aae4-e43a542f5e08 Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com> --- docs/cli.md | 2 +- src/init.rs | 11 +++-------- src/main.rs | 5 +++-- tests/init_tests.rs | 29 +++++++++++++++++++++-------- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index bb272c69..b3de7970 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -8,7 +8,7 @@ Global flags (apply to all subcommands): `--verbose, -v` (enable info-level logg - `init` - Initialize a repository for AI-first agentic pipeline authoring - `--path ` - Target directory (defaults to current directory) - - `--force` - Overwrite existing agent file + - `--force` - Bypass the GitHub-remote guard (use when running inside a GitHub-hosted repository like `githubnext/ado-aw` itself) - Creates `.github/agents/ado-aw.agent.md` — a Copilot dispatcher agent that routes to specialized prompts for creating, updating, and debugging agentic pipelines - The agent auto-downloads the ado-aw compiler and handles the full lifecycle (create → compile → check) - `compile []` - Compile a markdown file to Azure DevOps pipeline YAML. If no path is given, auto-discovers and recompiles all detected agentic pipelines in the current directory. diff --git a/src/init.rs b/src/init.rs index 3037c70c..49f0ff1e 100644 --- a/src/init.rs +++ b/src/init.rs @@ -7,18 +7,13 @@ const AGENT_TEMPLATE: &str = include_str!("data/init-agent.md"); const AGENT_DIR: &str = ".github/agents"; const AGENT_FILENAME: &str = "ado-aw.agent.md"; -pub async fn run(path: Option<&std::path::Path>, force: bool) -> Result<()> { +pub async fn run(path: Option<&std::path::Path>) -> Result<()> { let base = path.map(PathBuf::from).unwrap_or_else(|| PathBuf::from(".")); let agent_dir = base.join(AGENT_DIR); let agent_path = agent_dir.join(AGENT_FILENAME); - // Check if file already exists - if agent_path.exists() && !force { - anyhow::bail!( - "{} already exists. Use --force to overwrite.", - agent_path.display() - ); - } + // `init` always (re)writes the agent file so it stays in sync with the + // currently installed compiler version. // Create directory structure tokio::fs::create_dir_all(&agent_dir) diff --git a/src/main.rs b/src/main.rs index a744f9dc..583ecbc5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -104,7 +104,8 @@ enum Commands { /// Target directory (defaults to current directory) #[arg(long)] path: Option, - /// Overwrite existing agent file + /// Bypass the GitHub-remote guard (use when running inside a + /// GitHub-hosted repository like `githubnext/ado-aw` itself) #[arg(long)] force: bool, }, @@ -455,7 +456,7 @@ async fn main() -> Result<()> { if !force { ensure_non_github_remote_for_ado_aw("init", init_path).await?; } - init::run(path.as_deref(), force).await?; + init::run(path.as_deref()).await?; } Commands::Configure { token, diff --git a/tests/init_tests.rs b/tests/init_tests.rs index 7c2c7c4d..a55f5d4b 100644 --- a/tests/init_tests.rs +++ b/tests/init_tests.rs @@ -32,9 +32,9 @@ fn test_init_creates_agent_file() { ); } -/// Test that `init` refuses to overwrite without --force +/// Test that `init` always overwrites an existing agent file (no --force needed) #[test] -fn test_init_refuses_overwrite_without_force() { +fn test_init_overwrites_by_default() { let temp_dir = tempfile::tempdir().expect("Failed to create temp directory"); // First run should succeed @@ -44,21 +44,34 @@ fn test_init_refuses_overwrite_without_force() { .expect("Failed to run ado-aw init"); assert!(output.status.success(), "First init should succeed"); - // Second run without --force should fail + let agent_path = temp_dir.path().join(".github/agents/ado-aw.agent.md"); + + // Tamper with the file + fs::write(&agent_path, "tampered content").expect("Should write tampered content"); + + // Second run without --force should still succeed and restore the template let output = ado_aw_bin() .args(["init", "--path", temp_dir.path().to_str().unwrap()]) .output() .expect("Failed to run ado-aw init"); - assert!(!output.status.success(), "Second init without --force should fail"); + assert!( + output.status.success(), + "Second init should succeed and overwrite: {}", + String::from_utf8_lossy(&output.stderr) + ); - let stderr = String::from_utf8_lossy(&output.stderr); + let content = fs::read_to_string(&agent_path).expect("Should read agent file"); + assert!( + content.contains("ADO Agentic Pipelines Agent"), + "Default init should restore the template content" + ); assert!( - stderr.contains("already exists"), - "Error should mention file already exists: {stderr}" + !content.contains("tampered"), + "Tampered content should be overwritten" ); } -/// Test that `init --force` overwrites an existing agent file +/// Test that `init --force` also overwrites an existing agent file #[test] fn test_init_force_overwrites() { let temp_dir = tempfile::tempdir().expect("Failed to create temp directory");