Skip to content
Draft
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
1 change: 1 addition & 0 deletions .github/workflows/rust-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
paths:
- "src/**"
- "tests/**"
- "docs/template-markers.md"
- "Cargo.toml"
- "Cargo.lock"

Expand Down
19 changes: 0 additions & 19 deletions docs/template-markers.md
Original file line number Diff line number Diff line change
Expand Up @@ -455,21 +455,6 @@ Generates an `env:` block for the "Start MCP Gateway (MCPG)" pipeline step, forw

When no extensions require pipeline variables, this marker is replaced with an empty string and the MCPG step has no `env:` block.

## {{ mcp_client_config }} *(obsolete)*

**Removed in recent versions.** The Copilot CLI `mcp-config.json` is no longer generated at compile time. Instead, it is derived at **pipeline runtime** from MCPG's actual gateway output, matching gh-aw's `convert_gateway_config_copilot.cjs` pattern.

The "Start MCP Gateway (MCPG)" pipeline step:
1. Redirects MCPG's stdout to `gateway-output.json`
2. Waits for the health check and for valid JSON output
3. Transforms the output with a Python script that:
- Rewrites URLs from `127.0.0.1` → `host.docker.internal` (AWF container loopback vs host)
- Ensures `tools: ["*"]` on each server entry (Copilot CLI requirement)
- Preserves all other fields (headers, type, etc.)
4. Writes the result to `/tmp/awf-tools/mcp-config.json` and `$HOME/.copilot/mcp-config.json`

This ensures the Copilot CLI config reflects MCPG's actual runtime state rather than a compile-time prediction.

## {{ allowed_domains }}

Should be replaced with the comma-separated domain list for AWF's `--allow-domains` flag. The list includes:
Expand Down Expand Up @@ -607,10 +592,6 @@ Should be replaced with the MCPG listening port (defined as `MCPG_PORT` constant

Should be replaced with the domain the AWF-sandboxed agent uses to reach MCPG on the host (defined as `MCPG_DOMAIN` constant in `src/compile/common.rs`, currently `host.docker.internal`). Used in the pipeline to set the `MCP_GATEWAY_DOMAIN` ADO variable. Docker's `host.docker.internal` resolves to the host loopback from inside containers.

## {{ copilot_version }}

**Removed.** This marker has been absorbed into `{{ engine_install_steps }}`. The `COPILOT_CLI_VERSION` constant now lives in `src/engine.rs` and is used internally by `Engine::install_steps()`. The version can be overridden per-agent via `engine: { id: copilot, version: "..." }` in front matter.

## 1ES-Specific Template Markers

The 1ES target uses the same template markers as standalone, plus the 1ES-specific `extends:` / `stages:` / `templateContext` wrapping. The 1ES template includes `templateContext.type: buildJob` for all jobs, and the pool is specified at the top-level `parameters.pool` rather than per-job.
Expand Down
83 changes: 83 additions & 0 deletions tests/compiler_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4810,3 +4810,86 @@ fn test_example_dogfood_failure_reporter_structure() {
"Example should target githubnext/ado-aw"
);
}

/// Test that every `{{ marker }}` used in `src/data/*.yml` has a corresponding
/// `## {{ marker }}` heading in `docs/template-markers.md`.
///
/// This is the CI/docs marker-drift guard: if a marker is added to a template
/// without updating the docs, this test fails.
#[test]
fn test_template_marker_docs_coverage() {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let data_dir = manifest_dir.join("src").join("data");
let docs_file = manifest_dir.join("docs").join("template-markers.md");

// --- collect markers from src/data/*.yml ---
let yml_entries = fs::read_dir(&data_dir)
.unwrap_or_else(|e| panic!("Cannot read {}: {e}", data_dir.display()));

let mut yml_markers: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for entry in yml_entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("yml") {
continue;
}
let content = fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("Cannot read {}: {e}", path.display()));
for cap in regex_captures_markers(&content) {
yml_markers.insert(cap);
}
}

// --- collect documented marker headings from docs/template-markers.md ---
let docs = fs::read_to_string(&docs_file)
.unwrap_or_else(|e| panic!("Cannot read {}: {e}", docs_file.display()));

let mut documented: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for line in docs.lines() {
// Match lines like: ## {{ marker_name }}
if let Some(rest) = line.strip_prefix("## {{ ")
&& let Some(name) = rest.split("}}").next()
{
documented.insert(name.trim().to_string());
}
}

// Every marker that appears in the yml files must have a docs heading.
let mut missing: Vec<String> = Vec::new();
for marker in &yml_markers {
if !documented.contains(marker.as_str()) {
missing.push(format!("{{{{ {marker} }}}}"));
}
}

assert!(
missing.is_empty(),
"The following template markers appear in src/data/*.yml but have no \
'## {{{{ marker }}}}' heading in docs/template-markers.md — add docs or \
update the marker name:\n {}",
missing.join("\n ")
);
}

/// Extract all `{{ name }}` marker names from `content` (excluding `${{ }}` ADO expressions).
fn regex_captures_markers(content: &str) -> Vec<String> {
let mut results = Vec::new();
let mut s: &str = content;
while let Some(start) = s.find("{{ ") {
// Skip ADO ${{ }} expressions
if start > 0 && s.as_bytes().get(start - 1) == Some(&b'$') {
s = &s[start + 3..];
continue;
}
let after = &s[start + 3..];
if let Some(end) = after.find("}}") {
let name = after[..end].trim().to_string();
if !name.is_empty() {
results.push(name);
}
s = &after[end + 2..];
} else {
break;
}
}
results
}
Loading