diff --git a/AGENTS.md b/AGENTS.md index de48e47..6c866fd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -807,13 +807,9 @@ Used by the execute command's --source parameter. The agent markdown only ever l ## {{ pipeline_path }} -Should be replaced with the path to the compiled pipeline YAML file for runtime integrity checking. The path is derived from the output path (preserving any directory structure) and is anchored at the **trigger ("self") repository** via `{{ trigger_repo_directory }}` (see below), independent of the user's `workspace:` setting: -- No additional checkouts: `$(Build.SourcesDirectory)/.yml` -- Additional checkouts present: `$(Build.SourcesDirectory)/$(Build.Repository.Name)/.yml` +Should be replaced with the path to the compiled pipeline YAML file for runtime integrity checking. The path is **relative** to the trigger repository root (e.g. `agents/ctf.yml`, `pipelines/production/review.lock.yml`). The integrity check step itself sets `workingDirectory: {{ trigger_repo_directory }}` so the relative path resolves correctly regardless of whether additional repositories are checked out, and so that `ado-aw check`'s recompile step has access to the trigger repo's `.git` directory (required to infer the ADO org for `tools.azure-devops`). -For example, an output path of `pipelines/production/review.lock.yml` resolves to `$(Build.SourcesDirectory)/pipelines/production/review.lock.yml` when no additional repositories are checked out. - -Used by the pipeline's integrity check step to verify the pipeline hasn't been modified outside the compilation process. The compiled yaml only ever lives in the trigger repo, so this is intentionally not affected by `workspace:` pointing at a non-self alias. +Used by the pipeline's integrity check step to verify the pipeline hasn't been modified outside the compilation process. ## {{ trigger_repo_directory }} @@ -821,12 +817,14 @@ Should be replaced with the directory where the trigger ("self") repository is c - No additional checkouts → `$(Build.SourcesDirectory)` (ADO checks `self` into the root) - One or more additional checkouts → `$(Build.SourcesDirectory)/$(Build.Repository.Name)` (ADO puts each checked-out repo, including `self`, into a subfolder named after the repository) -Use this marker (rather than `{{ working_directory }}` / `{{ workspace }}`) for any path that refers to a file shipped in the trigger repo (e.g. the agent markdown source and the compiled pipeline yaml itself). +Use this marker (rather than `{{ working_directory }}` / `{{ workspace }}`) for any path that refers to a file shipped in the trigger repo (e.g. the agent markdown source) or as a `workingDirectory:` for steps that need access to the trigger repo's `.git` (e.g. the integrity check step). ## {{ integrity_check }} Generates the "Verify pipeline integrity" pipeline step that downloads the released ado-aw compiler and runs `ado-aw check` against the compiled pipeline YAML. This step ensures the pipeline file hasn't been modified outside the compilation process. +The step sets `workingDirectory: {{ trigger_repo_directory }}` so that the relative `{{ pipeline_path }}` argument resolves correctly when `checkout:` produces a multi-repo `$(Build.SourcesDirectory)` layout, and so `ado-aw check`'s internal recompile can infer the ADO org from the trigger repo's git remote. + When the compiler is built with `--skip-integrity` (debug builds only), this placeholder is replaced with an empty string and the integrity step is omitted from the generated pipeline. ## {{ mcpg_debug_flags }} diff --git a/src/compile/common.rs b/src/compile/common.rs index 97357fd..7cb8f0a 100644 --- a/src/compile/common.rs +++ b/src/compile/common.rs @@ -648,6 +648,13 @@ pub fn generate_source_path(input_path: &std::path::Path) -> String { /// downloads the ado-aw compiler and runs `ado-aw check` against the /// pipeline path. /// +/// The step sets `workingDirectory: {{ trigger_repo_directory }}` so that: +/// 1. The relative `{{ pipeline_path }}` argument resolves correctly when +/// `checkout:` produces a multi-repo `$(Build.SourcesDirectory)` layout. +/// 2. `ado-aw check`'s recompile step has access to the trigger repo's +/// `.git` directory, which is required to infer the ADO org from the +/// git remote (used by `tools.azure-devops`). +/// /// When `skip` is `true` (developer builds with `--skip-integrity`), /// returns an empty string and the step is omitted from the pipeline. pub fn generate_integrity_check(skip: bool) -> String { @@ -660,6 +667,7 @@ pub fn generate_integrity_check(skip: bool) -> String { AGENTIC_PIPELINES_PATH="$(Pipeline.Workspace)/agentic-pipeline-compiler/ado-aw" chmod +x "$AGENTIC_PIPELINES_PATH" $AGENTIC_PIPELINES_PATH check "{{ pipeline_path }}" + workingDirectory: {{ trigger_repo_directory }} displayName: "Verify pipeline integrity""# .to_string() } @@ -755,9 +763,11 @@ pub fn generate_debug_pipeline_replacements(debug: bool) -> Vec<(String, String) /// Generate the pipeline YAML path for integrity checking at ADO runtime. /// -/// Returns a path using `{{ trigger_repo_directory }}` as the base. The -/// compiled pipeline yaml ships in the trigger ("self") repo, so this anchor -/// is independent of the user's `workspace:` setting. +/// Returns the path **relative** to the trigger repository root. The integrity +/// check step itself sets `workingDirectory: {{ trigger_repo_directory }}` so +/// that the path resolves correctly and so that `ado-aw check`'s recompile +/// step has access to the trigger repo's `.git` directory (needed to infer +/// the ADO org for `tools.azure-devops`). /// /// The full relative path is preserved so that pipelines compiled into /// subdirectories (e.g. `agents/ctf.yml`) produce a correct runtime path @@ -766,15 +776,13 @@ pub fn generate_debug_pipeline_replacements(debug: bool) -> Vec<(String, String) /// Absolute paths fall back to using only the filename to avoid embedding /// machine-specific paths in the generated pipeline. pub fn generate_pipeline_path(output_path: &std::path::Path) -> String { - let relative = normalize_relative_path(output_path).unwrap_or_else(|| { + normalize_relative_path(output_path).unwrap_or_else(|| { output_path .file_name() .and_then(|n| n.to_str()) .unwrap_or("pipeline.yml") .to_string() - }); - - format!("{{{{ trigger_repo_directory }}}}/{}", relative) + }) } /// Normalize a path for embedding in a generated pipeline. @@ -2646,33 +2654,32 @@ mod tests { fn test_generate_pipeline_path_preserves_directory() { // The original bug: compiling agents/ctf.md produced agents/ctf.yml as // output, but the embedded path was only ctf.yml (missing agents/). + // Pipeline path is relative to the integrity check's workingDirectory + // ({{ trigger_repo_directory }}), so no prefix is embedded here. let path = std::path::Path::new("agents/ctf.yml"); let result = generate_pipeline_path(path); - assert_eq!(result, "{{ trigger_repo_directory }}/agents/ctf.yml"); + assert_eq!(result, "agents/ctf.yml"); } #[test] fn test_generate_pipeline_path_nested_directory() { let path = std::path::Path::new("pipelines/production/review.yml"); let result = generate_pipeline_path(path); - assert_eq!( - result, - "{{ trigger_repo_directory }}/pipelines/production/review.yml" - ); + assert_eq!(result, "pipelines/production/review.yml"); } #[test] fn test_generate_pipeline_path_strips_dot_slash() { let path = std::path::Path::new("./agents/my-agent.yml"); let result = generate_pipeline_path(path); - assert_eq!(result, "{{ trigger_repo_directory }}/agents/my-agent.yml"); + assert_eq!(result, "agents/my-agent.yml"); } #[test] fn test_generate_pipeline_path_filename_only() { let path = std::path::Path::new("pipeline.yml"); let result = generate_pipeline_path(path); - assert_eq!(result, "{{ trigger_repo_directory }}/pipeline.yml"); + assert_eq!(result, "pipeline.yml"); } #[test] @@ -2693,7 +2700,7 @@ mod tests { let tmp = tempfile::TempDir::new().unwrap(); let abs_path = tmp.path().join("agents").join("ctf.yml"); let result = generate_pipeline_path(&abs_path); - assert_eq!(result, "{{ trigger_repo_directory }}/ctf.yml"); + assert_eq!(result, "ctf.yml"); } #[test] @@ -2721,7 +2728,7 @@ mod tests { fs::write(tmp.path().join(".git"), "gitdir: fake").unwrap(); let abs_path = agents_dir.join("ctf.yml"); let result = generate_pipeline_path(&abs_path); - assert_eq!(result, "{{ trigger_repo_directory }}/agents/ctf.yml"); + assert_eq!(result, "agents/ctf.yml"); } // ─── generate_trigger_repo_directory ───────────────────────────────────── @@ -2791,6 +2798,12 @@ mod tests { result.contains("{{ pipeline_path }}"), "Should contain the pipeline_path placeholder for later resolution" ); + assert!( + result.contains("workingDirectory: {{ trigger_repo_directory }}"), + "Should set workingDirectory to the trigger repo so `ado-aw check` \ + can recompile from a directory that contains .git (needed for \ + ADO org inference when tools.azure-devops is enabled)" + ); } #[test]