Skip to content

Commit 788250d

Browse files
fix(compile): reject unknown front-matter fields with deny_unknown_fields (#409)
Add #[serde(deny_unknown_fields)] to FrontMatter so typos like 'safeoutputs:' (instead of 'safe-outputs:') or top-level 'schedule:' (instead of 'on: schedule:') produce a clear compile error rather than being silently ignored. Previously, unknown YAML keys were silently dropped by serde, which meant write-requiring safe-outputs could bypass the validate_write_permissions check entirely, leading to confusing runtime errors (e.g. 'AZURE_DEVOPS_ORG_URL not set'). Also fixes all example files and docs that incorrectly used top-level 'schedule:' instead of 'on: schedule:'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent de911dc commit 788250d

6 files changed

Lines changed: 75 additions & 11 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ description: "Checks for outdated dependencies and opens PRs"
8181
engine:
8282
id: copilot
8383
model: claude-sonnet-4.5
84-
schedule: weekly on monday around 9:00
84+
on:
85+
schedule: weekly on monday around 9:00
8586
pool: AZS-1ES-L-MMS-ubuntu-22.04
8687
tools:
8788
azure-devops: true

docs/front-matter.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@ engine: copilot # Engine identifier. Defaults to copilot. Currently only 'copilo
1616
# id: copilot
1717
# model: claude-opus-4.7
1818
# timeout-minutes: 30
19-
schedule: daily around 14:00 # Fuzzy schedule syntax - see docs/schedule-syntax.md
20-
# schedule: # Alternative object format (with branch filtering)
21-
# run: daily around 14:00
22-
# branches:
23-
# - main
24-
# - release/*
25-
workspace: repo # Optional: "root", "repo" (alias: "self"), or a checked-out repository alias. If not specified, defaults to "root" when no additional repositories are listed in `checkout:`, and to "repo" when one or more additional repos are checked out. See "Workspace Defaults" below.
19+
on:
20+
schedule: daily around 14:00 # Fuzzy schedule syntax - see docs/schedule-syntax.md
21+
# schedule: # Alternative object format (with branch filtering)
22+
# run: daily around 14:00
23+
# branches:
24+
# - main
25+
# - release/*
26+
workspace: repo# Optional: "root", "repo" (alias: "self"), or a checked-out repository alias. If not specified, defaults to "root" when no additional repositories are listed in `checkout:`, and to "repo" when one or more additional repos are checked out. See "Workspace Defaults" below.
2627
pool: AZS-1ES-L-MMS-ubuntu-22.04 # Agent pool name (string format). Defaults to AZS-1ES-L-MMS-ubuntu-22.04.
2728
# pool: # Alternative object format (required for 1ES if specifying os)
2829
# name: AZS-1ES-L-MMS-ubuntu-22.04

examples/azure-devops-mcp.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
---
22
name: "ADO Work Item Triage"
33
description: "Triages work items using the Azure DevOps MCP"
4-
schedule: daily around 9:00
4+
on:
5+
schedule: daily around 9:00
56
tools:
67
azure-devops:
78
toolsets: [core, work, work-items]

examples/lean-verifier.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
---
22
name: "Lean Formal Verifier"
33
description: "Analyzes code and builds formal Lean 4 proofs of critical invariants"
4-
schedule: weekly on friday around 17:00
4+
on:
5+
schedule: weekly on friday around 17:00
56
tools:
67
cache-memory: true
78
runtimes:

examples/sample-agent.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
---
22
name: "Daily Code Review"
33
description: "Reviews code changes and creates summary reports"
4-
schedule: daily
4+
on:
5+
schedule: daily
56
repositories:
67
- repository: azure-devops-agentic-pipelines
78
type: git

src/compile/types.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,7 @@ pub struct PipelineParameter {
566566

567567
/// Front matter configuration from the input markdown file
568568
#[derive(Debug, Deserialize)]
569+
#[serde(deny_unknown_fields)]
569570
pub struct FrontMatter {
570571
/// Agent name (required)
571572
pub name: String,
@@ -1712,6 +1713,64 @@ Body
17121713
assert!(result.is_err(), "unknown fields in network should be rejected");
17131714
}
17141715

1716+
// ─── FrontMatter deny_unknown_fields ─────────────────────────────────────
1717+
1718+
#[test]
1719+
fn test_front_matter_rejects_unknown_top_level_field() {
1720+
let content = r#"---
1721+
name: "Test"
1722+
description: "Test"
1723+
safeoutputs:
1724+
upload-pipeline-artifact: {}
1725+
---
1726+
1727+
Body
1728+
"#;
1729+
let result = super::super::common::parse_markdown(content);
1730+
assert!(result.is_err(), "unknown top-level field 'safeoutputs' should be rejected");
1731+
let err = format!("{:#}", result.unwrap_err());
1732+
assert!(
1733+
err.contains("unknown field `safeoutputs`"),
1734+
"error should mention unknown field `safeoutputs`, got: {}",
1735+
err
1736+
);
1737+
}
1738+
1739+
#[test]
1740+
fn test_front_matter_rejects_top_level_schedule() {
1741+
let content = r#"---
1742+
name: "Test"
1743+
description: "Test"
1744+
schedule: daily around 14:00
1745+
---
1746+
1747+
Body
1748+
"#;
1749+
let result = super::super::common::parse_markdown(content);
1750+
assert!(result.is_err(), "top-level 'schedule' should be rejected (use on.schedule)");
1751+
let err = format!("{:#}", result.unwrap_err());
1752+
assert!(
1753+
err.contains("unknown field `schedule`"),
1754+
"error should mention unknown field `schedule`, got: {}",
1755+
err
1756+
);
1757+
}
1758+
1759+
#[test]
1760+
fn test_front_matter_accepts_safe_outputs_with_hyphen() {
1761+
let content = r#"---
1762+
name: "Test"
1763+
description: "Test"
1764+
safe-outputs:
1765+
upload-pipeline-artifact: {}
1766+
---
1767+
1768+
Body
1769+
"#;
1770+
let (fm, _) = super::super::common::parse_markdown(content).unwrap();
1771+
assert!(fm.safe_outputs.contains_key("upload-pipeline-artifact"));
1772+
}
1773+
17151774
// ─── PrTriggerConfig deserialization ─────────────────────────────────────
17161775
// NOTE: These tests use `triggers:` as a wrapper key and deserialize
17171776
// OnConfig directly (not through FrontMatter). They test struct

0 commit comments

Comments
 (0)