diff --git a/.github/workflows/copilot-cli-safeoutputs.yml b/.github/workflows/copilot-cli-safeoutputs.yml index 0dc65b54..6a6609ed 100644 --- a/.github/workflows/copilot-cli-safeoutputs.yml +++ b/.github/workflows/copilot-cli-safeoutputs.yml @@ -40,18 +40,8 @@ jobs: id: copilot-version run: | set -euo pipefail - version="$( - python3 - <<'PY' - import pathlib - import re - - text = pathlib.Path("src/engine.rs").read_text() - match = re.search(r'pub const COPILOT_CLI_VERSION: &str = "([^"]+)"', text) - if not match: - raise SystemExit("COPILOT_CLI_VERSION not found") - print(match.group(1)) - PY - )" + version="$(cargo run --quiet -- catalog --kind versions --json | jq -r '.versions.copilot_cli')" + test -n "$version" && test "$version" != "null" echo "version=${version}" >> "$GITHUB_OUTPUT" - name: Install compiler-pinned GitHub Copilot CLI diff --git a/README.md b/README.md index be5eafd6..36fcac53 100644 --- a/README.md +++ b/README.md @@ -564,7 +564,7 @@ Commands: graph Query the resolved dependency graph for an agent source file whatif Static reachability: classify jobs skipped if a step or job fails lint Run structural lint checks over an agent source file - catalog List safe-outputs, runtimes, tools, engines, and models + catalog List safe-outputs, runtimes, tools, engines, models, and versions Options: -v, --verbose Enable info-level logging diff --git a/docs/cli.md b/docs/cli.md index 57517fe0..8f743a73 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -173,8 +173,9 @@ Both `--all-repos` and `--source` route through `ado-aw`'s `discover_ado_aw_pipe - `` - Path to the agent markdown file. - `--json` - Emit lint findings as structured JSON instead of the human-readable report. -- `catalog [--kind ] [--json]` - List the compiler's in-tree registries for scripting or discovery. +- `catalog [--kind ] [--json]` - List the compiler's in-tree registries for scripting or discovery. - `--kind <...>` - Restrict output to one category. When omitted, emits every category. + - `--kind versions` - Emit the compiler's pinned **semver** versions (`copilot_cli`, `awf`, `mcpg`) as a single source of truth. CI reads these deterministically instead of scraping the Rust source, e.g. `ado-aw catalog --kind versions --json | jq -r '.versions.copilot_cli'`. - `--json` - Emit the catalog as structured JSON instead of the human-readable report. ### Hidden Build-Time Tools diff --git a/site/src/content/docs/setup/cli.mdx b/site/src/content/docs/setup/cli.mdx index a18e52aa..ac1ad501 100644 --- a/site/src/content/docs/setup/cli.mdx +++ b/site/src/content/docs/setup/cli.mdx @@ -140,7 +140,7 @@ Options: ### `catalog` -List the in-tree registries of safe-output tools, runtimes, first-class tools, engines, and models. +List the in-tree registries of safe-output tools, runtimes, first-class tools, engines, models, and pinned versions. ```bash ado-aw catalog [--kind ] [--json] @@ -148,7 +148,7 @@ ado-aw catalog [--kind ] [--json] Options: -- `--kind safe-outputs|runtimes|tools|engines|models` -- restrict output to one category; omit to see all +- `--kind safe-outputs|runtimes|tools|engines|models|versions` -- restrict output to one category; omit to see all. `--kind versions` emits the pinned semver versions (`copilot_cli`, `awf`, `mcpg`) as a deterministic source of truth for CI. - `--json` -- emit the catalog as JSON ### `trace ` diff --git a/src/compile/mod.rs b/src/compile/mod.rs index 747847bb..bc522ee8 100644 --- a/src/compile/mod.rs +++ b/src/compile/mod.rs @@ -39,7 +39,9 @@ pub use common::ADO_MCP_ENTRYPOINT; pub use common::ADO_MCP_IMAGE; pub use common::ADO_MCP_PACKAGE; pub use common::ADO_MCP_SERVER_NAME; +pub use common::AWF_VERSION; pub use common::HEADER_MARKER; +pub use common::MCPG_VERSION; pub use common::normalize_source_path; #[allow(unused_imports)] pub use common::parse_markdown; diff --git a/src/inspect/catalog.rs b/src/inspect/catalog.rs index 5d0448f1..5a34c866 100644 --- a/src/inspect/catalog.rs +++ b/src/inspect/catalog.rs @@ -5,7 +5,8 @@ use std::fmt; use serde::Serialize; -use crate::engine::DEFAULT_COPILOT_MODEL; +use crate::compile::{AWF_VERSION, MCPG_VERSION}; +use crate::engine::{COPILOT_CLI_VERSION, DEFAULT_COPILOT_MODEL}; use crate::safe_outputs::{ALL_KNOWN_SAFE_OUTPUTS, ALWAYS_ON_TOOLS, DEBUG_ONLY_TOOLS}; #[derive(Debug, Clone, PartialEq, Eq, Serialize)] @@ -28,6 +29,21 @@ pub struct ToolCatalogEntry { pub description: String, } +/// Pinned semver versions the compiler embeds, surfaced so CI and tooling can +/// read them deterministically instead of scraping the Rust source. Only +/// genuine semver version constants are included — container image references +/// and the default model name are intentionally excluded. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct VersionCatalog { + /// Pinned GitHub Copilot CLI version (`engine::COPILOT_CLI_VERSION`). + pub copilot_cli: String, + /// Pinned AWF (Agentic Workflow Firewall) binary version + /// (`compile::common::AWF_VERSION`). + pub awf: String, + /// Pinned MCP Gateway version (`compile::common::MCPG_VERSION`). + pub mcpg: String, +} + #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] pub struct Catalog { #[serde(skip_serializing_if = "Vec::is_empty")] @@ -40,6 +56,8 @@ pub struct Catalog { pub engines: Vec, #[serde(skip_serializing_if = "Vec::is_empty")] pub models: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub versions: Option, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -51,7 +69,7 @@ impl fmt::Display for UnknownCatalogKind { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "unknown --kind '{}' (expected one of: safe-outputs, runtimes, tools, engines, models)", + "unknown --kind '{}' (expected one of: safe-outputs, runtimes, tools, engines, models, versions)", self.kind ) } @@ -66,6 +84,7 @@ pub enum CatalogKind { Tools, Engines, Models, + Versions, } impl CatalogKind { @@ -76,6 +95,7 @@ impl CatalogKind { "tools" => Ok(Self::Tools), "engines" => Ok(Self::Engines), "models" => Ok(Self::Models), + "versions" => Ok(Self::Versions), other => Err(UnknownCatalogKind { kind: other.to_string(), }), @@ -90,6 +110,7 @@ pub fn catalog() -> Catalog { tools: tools(), engines: engines(), models: models(), + versions: Some(versions()), } } @@ -116,6 +137,10 @@ pub fn catalog_kind(kind: &str) -> Result { models: models(), ..Catalog::default() }, + CatalogKind::Versions => Catalog { + versions: Some(versions()), + ..Catalog::default() + }, }) } @@ -161,10 +186,25 @@ pub fn render_text(catalog: &Catalog) -> String { for model in &catalog.models { out.push_str(&format!(" {model}\n")); } + out.push('\n'); + } + if let Some(versions) = &catalog.versions { + out.push_str("Versions\n"); + out.push_str(&format!(" copilot-cli {}\n", versions.copilot_cli)); + out.push_str(&format!(" awf {}\n", versions.awf)); + out.push_str(&format!(" mcpg {}\n", versions.mcpg)); } out.trim_end().to_string() } +fn versions() -> VersionCatalog { + VersionCatalog { + copilot_cli: COPILOT_CLI_VERSION.to_string(), + awf: AWF_VERSION.to_string(), + mcpg: MCPG_VERSION.to_string(), + } +} + fn safe_outputs() -> Vec { ALL_KNOWN_SAFE_OUTPUTS .iter() @@ -319,4 +359,31 @@ mod tests { let err = catalog_kind("widgets").unwrap_err(); assert_eq!(err.kind, "widgets"); } + + #[test] + fn versions_inspect_catalog_kind_returns_pinned_semver_constants() { + let catalog = catalog_kind("versions").unwrap(); + let versions = catalog.versions.expect("versions populated"); + assert_eq!(versions.copilot_cli, COPILOT_CLI_VERSION); + assert_eq!(versions.awf, AWF_VERSION); + assert_eq!(versions.mcpg, MCPG_VERSION); + // Only the versions category is populated for --kind versions. + assert!(catalog.safe_outputs.is_empty()); + assert!(catalog.models.is_empty()); + } + + #[test] + fn full_catalog_includes_versions() { + let catalog = catalog(); + assert!(catalog.versions.is_some()); + } + + #[test] + fn versions_catalog_serializes_semver_fields_as_json() { + let catalog = catalog_kind("versions").unwrap(); + let value = serde_json::to_value(&catalog).unwrap(); + assert_eq!(value["versions"]["copilot_cli"], COPILOT_CLI_VERSION); + assert_eq!(value["versions"]["awf"], AWF_VERSION); + assert_eq!(value["versions"]["mcpg"], MCPG_VERSION); + } } diff --git a/src/main.rs b/src/main.rs index a2b541ca..42a47144 100644 --- a/src/main.rs +++ b/src/main.rs @@ -606,9 +606,9 @@ enum Commands { #[arg(long)] json: bool, }, - /// List safe-outputs, runtimes, tools, engines, and models. + /// List safe-outputs, runtimes, tools, engines, models, and versions. Catalog { - /// Category to emit: safe-outputs, runtimes, tools, engines, or models. + /// Category to emit: safe-outputs, runtimes, tools, engines, models, or versions. #[arg(long)] kind: Option, /// Emit the catalog as JSON. diff --git a/src/mcp_author/mod.rs b/src/mcp_author/mod.rs index 711ccd8d..8ffd8582 100644 --- a/src/mcp_author/mod.rs +++ b/src/mcp_author/mod.rs @@ -101,7 +101,7 @@ struct WhatIfParams { #[derive(Debug, Deserialize, JsonSchema)] struct CatalogParams { - /// Optional category: safe-outputs, runtimes, tools, engines, or models. + /// Optional category: safe-outputs, runtimes, tools, engines, models, or versions. kind: Option, } @@ -301,7 +301,7 @@ impl AuthorMcp { #[tool( name = "catalog", - description = "List supported safe-outputs, runtimes, tools, engines, and models." + description = "List supported safe-outputs, runtimes, tools, engines, models, and pinned versions." )] async fn catalog(&self, params: Parameters) -> Result { let catalog = inspect::build_catalog(params.0.kind.as_deref()).map_err(to_mcp_error)?;