Skip to content
Closed
Show file tree
Hide file tree
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
101 changes: 100 additions & 1 deletion src/compile/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,20 @@ pub fn generate_copilot_params(
crate::engine::GITHUB_COPILOT_CLI_ENGINE.generate_cli_params(front_matter, extensions)
}

/// Generate engine install steps for the pipeline template.
/// Delegates to the engine implementation which handles `engine.version` and `engine.command`.
pub fn generate_engine_install_steps(front_matter: &FrontMatter) -> Result<String> {
use crate::engine::Engine as _;
crate::engine::GITHUB_COPILOT_CLI_ENGINE.generate_install_steps(front_matter)
}

/// Generate the engine command path for the AWF invocation.
/// Delegates to the engine implementation which handles `engine.command`.
pub fn generate_copilot_command(front_matter: &FrontMatter) -> Result<String> {
use crate::engine::Engine as _;
crate::engine::GITHUB_COPILOT_CLI_ENGINE.generate_command_path(front_matter)
}

/// Compute the effective workspace based on explicit setting and checkout configuration.
pub fn compute_effective_workspace(
explicit_workspace: &Option<String>,
Expand Down Expand Up @@ -1969,6 +1983,10 @@ pub async fn compile_shared(
// 4. Generate copilot params
let copilot_params = generate_copilot_params(front_matter, extensions)?;

// 4b. Generate engine install steps and command path
let engine_install_steps = generate_engine_install_steps(front_matter)?;
let copilot_command = generate_copilot_command(front_matter)?;

// 5. Compute workspace, working directory, triggers
let effective_workspace = compute_effective_workspace(
&front_matter.workspace,
Expand Down Expand Up @@ -2073,7 +2091,8 @@ pub async fn compile_shared(
let replacements: Vec<(&str, &str)> = vec![
("{{ parameters }}", &parameters_yaml),
("{{ compiler_version }}", compiler_version),
("{{ copilot_version }}", COPILOT_CLI_VERSION),
("{{ engine_install_steps }}", &engine_install_steps),
("{{ copilot_command }}", &copilot_command),
("{{ pool }}", &pool),
("{{ setup_job }}", &setup_job),
("{{ teardown_job }}", &teardown_job),
Expand Down Expand Up @@ -2516,6 +2535,86 @@ mod tests {
assert_eq!(generate_job_timeout(&fm), "timeoutInMinutes: 0");
}

// ─── generate_engine_install_steps ─────────────────────────────────────────

#[test]
fn test_engine_install_steps_default() {
let fm = minimal_front_matter();
let steps = generate_engine_install_steps(&fm).unwrap();
assert!(steps.contains("NuGetAuthenticate@1"));
assert!(steps.contains(&format!("-Version {}", COPILOT_CLI_VERSION)));
}

#[test]
fn test_engine_install_steps_custom_version() {
let (fm, _) = parse_markdown(
"---\nname: test\ndescription: test\nengine:\n version: \"2.0.0\"\n---\n",
)
.unwrap();
let steps = generate_engine_install_steps(&fm).unwrap();
assert!(steps.contains("-Version 2.0.0"));
assert!(!steps.contains(COPILOT_CLI_VERSION));
}

#[test]
fn test_engine_install_steps_latest_omits_version() {
let (fm, _) = parse_markdown(
"---\nname: test\ndescription: test\nengine:\n version: latest\n---\n",
)
.unwrap();
let steps = generate_engine_install_steps(&fm).unwrap();
assert!(!steps.contains("-Version"));
assert!(steps.contains("NuGetAuthenticate@1"));
}

#[test]
fn test_engine_install_steps_skipped_with_command() {
let (fm, _) = parse_markdown(
"---\nname: test\ndescription: test\nengine:\n command: /usr/bin/copilot\n---\n",
)
.unwrap();
let steps = generate_engine_install_steps(&fm).unwrap();
assert!(steps.is_empty());
}

// ─── generate_copilot_command ─────────────────────────────────────────────

#[test]
fn test_copilot_command_default() {
let fm = minimal_front_matter();
let cmd = generate_copilot_command(&fm).unwrap();
assert_eq!(cmd, "/tmp/awf-tools/copilot");
}

#[test]
fn test_copilot_command_custom() {
let (fm, _) = parse_markdown(
"---\nname: test\ndescription: test\nengine:\n command: /usr/local/bin/my-engine\n---\n",
)
.unwrap();
let cmd = generate_copilot_command(&fm).unwrap();
assert_eq!(cmd, "/usr/local/bin/my-engine");
}

// ─── generate_copilot_params with agent ───────────────────────────────────

#[test]
fn test_copilot_params_with_agent() {
let (fm, _) = parse_markdown(
"---\nname: test\ndescription: test\nengine:\n agent: my-agent\n---\n",
)
.unwrap();
let params = generate_copilot_params(&fm, &crate::compile::extensions::collect_extensions(&fm)).unwrap();
assert!(params.contains("--agent my-agent"));
}

#[test]
fn test_copilot_params_no_agent_by_default() {
let fm = minimal_front_matter();
let params = generate_copilot_params(&fm, &crate::compile::extensions::collect_extensions(&fm)).unwrap();
assert!(!params.contains("--agent"));
}

// ─── sanitize_filename ────────────────────────────────────────────────────

#[test]
Expand Down
1 change: 1 addition & 0 deletions src/compile/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use std::path::{Path, PathBuf};

pub use common::parse_markdown;
pub use common::HEADER_MARKER;
pub use common::COPILOT_CLI_VERSION;
pub use common::generate_copilot_params;
pub use common::generate_mcpg_config;
pub use common::MCPG_IMAGE;
Expand Down
95 changes: 95 additions & 0 deletions src/compile/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,30 @@ impl EngineConfig {
EngineConfig::Full(opts) => opts.timeout_minutes,
}
}

/// Get the engine CLI version override
pub fn version(&self) -> Option<&str> {
match self {
EngineConfig::Simple(_) => None,
EngineConfig::Full(opts) => opts.version.as_deref(),
}
}

/// Get the custom engine command path override
pub fn command(&self) -> Option<&str> {
match self {
EngineConfig::Simple(_) => None,
EngineConfig::Full(opts) => opts.command.as_deref(),
}
}

/// Get the custom Copilot agent identifier
pub fn agent(&self) -> Option<&str> {
match self {
EngineConfig::Simple(_) => None,
EngineConfig::Full(opts) => opts.agent.as_deref(),
}
}
}

impl SanitizeConfigTrait for EngineConfig {
Expand All @@ -213,6 +237,18 @@ pub struct EngineOptions {
/// Workflow timeout in minutes
#[serde(default, rename = "timeout-minutes")]
pub timeout_minutes: Option<u32>,
/// Pin the engine CLI to a specific release version (e.g., "0.0.422").
/// When set to "latest", omits the -Version flag from the NuGet install step.
#[serde(default)]
pub version: Option<String>,
/// Override the default engine executable path. Skips the default installation
/// step when set. Must be an absolute path or a bare binary name.
#[serde(default)]
pub command: Option<String>,
/// Reference a custom Copilot agent file in .github/agents/
/// (e.g., "technical-doc-writer" → .github/agents/technical-doc-writer.agent.md).
#[serde(default)]
pub agent: Option<String>,
}

/// Tools configuration for the agent
Expand Down Expand Up @@ -901,6 +937,65 @@ mod tests {
assert_eq!(ec.timeout_minutes(), Some(30));
}

#[test]
fn test_engine_config_version_accessor() {
let yaml = "version: \"1.2.3\"";
let opts: EngineOptions = serde_yaml::from_str(yaml).unwrap();
let ec = EngineConfig::Full(opts);
assert_eq!(ec.version(), Some("1.2.3"));
}

#[test]
fn test_engine_config_version_none_by_default() {
let ec = EngineConfig::default();
assert_eq!(ec.version(), None);
}

#[test]
fn test_engine_config_version_none_for_simple() {
let ec = EngineConfig::Simple("claude-opus-4.5".to_string());
assert_eq!(ec.version(), None);
}

#[test]
fn test_engine_config_command_accessor() {
let yaml = "command: /usr/local/bin/my-copilot";
let opts: EngineOptions = serde_yaml::from_str(yaml).unwrap();
let ec = EngineConfig::Full(opts);
assert_eq!(ec.command(), Some("/usr/local/bin/my-copilot"));
}

#[test]
fn test_engine_config_command_none_by_default() {
let ec = EngineConfig::default();
assert_eq!(ec.command(), None);
}

#[test]
fn test_engine_config_agent_accessor() {
let yaml = "agent: technical-doc-writer";
let opts: EngineOptions = serde_yaml::from_str(yaml).unwrap();
let ec = EngineConfig::Full(opts);
assert_eq!(ec.agent(), Some("technical-doc-writer"));
}

#[test]
fn test_engine_config_agent_none_by_default() {
let ec = EngineConfig::default();
assert_eq!(ec.agent(), None);
}

#[test]
fn test_engine_config_full_with_all_new_fields() {
let yaml = "model: claude-opus-4.5\nversion: \"0.0.422\"\ncommand: /usr/bin/copilot\nagent: my-agent";
let opts: EngineOptions = serde_yaml::from_str(yaml).unwrap();
let ec = EngineConfig::Full(opts);
assert_eq!(ec.model(), "claude-opus-4.5");
assert_eq!(ec.version(), Some("0.0.422"));
assert_eq!(ec.command(), Some("/usr/bin/copilot"));
assert_eq!(ec.agent(), Some("my-agent"));
}

// ─── PermissionsConfig deserialization ───────────────────────────────

#[test]
Expand Down
53 changes: 4 additions & 49 deletions src/data/1es-base.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,30 +58,7 @@ extends:

{{ cancel_previous_builds }}

- task: NuGetAuthenticate@1
displayName: "Authenticate NuGet Feed"

- task: NuGetCommand@2
displayName: "Install Copilot CLI"
inputs:
command: 'custom'
arguments: 'install Microsoft.Copilot.CLI.linux-x64 -Source "https://pkgs.dev.azure.com/msazuresphere/_packaging/Guardian1ESPTUpstreamOrgFeed/nuget/v3/index.json" -Version {{ copilot_version }} -OutputDirectory $(Agent.TempDirectory)/tools -ExcludeVersion -NonInteractive'

- bash: |
ls -la "$(Agent.TempDirectory)/tools"
echo "##vso[task.prependpath]$(Agent.TempDirectory)/tools/Microsoft.Copilot.CLI.linux-x64"

# Copy copilot binary to /tmp so it's accessible inside AWF container
# (AWF auto-mounts /tmp:/tmp:rw but not Agent.TempDirectory)
mkdir -p /tmp/awf-tools
cp "$(Agent.TempDirectory)/tools/Microsoft.Copilot.CLI.linux-x64/copilot" /tmp/awf-tools/copilot
chmod +x /tmp/awf-tools/copilot
displayName: "Add copilot to PATH"

- bash: |
copilot --version
copilot -h
displayName: "Output copilot version"
{{ engine_install_steps }}

- bash: |
COMPILER_VERSION="{{ compiler_version }}"
Expand Down Expand Up @@ -370,7 +347,7 @@ extends:
--container-workdir "{{ working_directory }}" \
--log-level info \
--proxy-logs-dir "$(Agent.TempDirectory)/staging/logs/firewall" \
-- '/tmp/awf-tools/copilot --prompt "$(cat /tmp/awf-tools/agent-prompt.md)" --additional-mcp-config @/tmp/awf-tools/mcp-config.json {{ copilot_params }}' \
-- '{{ copilot_command }} --prompt "$(cat /tmp/awf-tools/agent-prompt.md)" --additional-mcp-config @/tmp/awf-tools/mcp-config.json {{ copilot_params }}' \
2>&1 \
| sed -u 's/##vso\[/[VSO-FILTERED] vso[/g; s/##\[/[VSO-FILTERED] [/g' \
| tee "$AGENT_OUTPUT_FILE" \
Expand Down Expand Up @@ -455,29 +432,7 @@ extends:
- download: current
artifact: agent_outputs_$(Build.BuildId)

- task: NuGetAuthenticate@1
displayName: "Authenticate NuGet Feed"

- task: NuGetCommand@2
displayName: "Install Copilot CLI"
inputs:
command: 'custom'
arguments: 'install Microsoft.Copilot.CLI.linux-x64 -Source "https://pkgs.dev.azure.com/msazuresphere/_packaging/Guardian1ESPTUpstreamOrgFeed/nuget/v3/index.json" -Version {{ copilot_version }} -OutputDirectory $(Agent.TempDirectory)/tools -ExcludeVersion -NonInteractive'

- bash: |
ls -la "$(Agent.TempDirectory)/tools"
echo "##vso[task.prependpath]$(Agent.TempDirectory)/tools/Microsoft.Copilot.CLI.linux-x64"

# Copy copilot binary to /tmp so it's accessible inside AWF container
mkdir -p /tmp/awf-tools
cp "$(Agent.TempDirectory)/tools/Microsoft.Copilot.CLI.linux-x64/copilot" /tmp/awf-tools/copilot
chmod +x /tmp/awf-tools/copilot
displayName: "Add copilot to PATH"

- bash: |
copilot --version
copilot -h
displayName: "Output copilot version"
{{ engine_install_steps }}

- bash: |
COMPILER_VERSION="{{ compiler_version }}"
Expand Down Expand Up @@ -563,7 +518,7 @@ extends:
--container-workdir "{{ working_directory }}" \
--log-level info \
--proxy-logs-dir "$(Agent.TempDirectory)/threat-analysis-logs/firewall" \
-- '/tmp/awf-tools/copilot --prompt "$(cat /tmp/awf-tools/threat-analysis-prompt.md)" {{ copilot_params }}' \
-- '{{ copilot_command }} --prompt "$(cat /tmp/awf-tools/threat-analysis-prompt.md)" {{ copilot_params }}' \
2>&1 \
| sed -u 's/##vso\[/[VSO-FILTERED] vso[/g; s/##\[/[VSO-FILTERED] [/g' \
| tee "$THREAT_OUTPUT_FILE" \
Expand Down
Loading