Skip to content

Commit 596f025

Browse files
jamesadevineCopilot
andcommitted
feat(compile)!: unify schedule and triggers under on: key
BREAKING CHANGE: top-level schedule: and triggers: keys are replaced by a single on: key, aligning with gh-aw's on: syntax. Migration: schedule: daily → on: { schedule: daily } triggers: → on: pipeline: ... pipeline: ... pr: ... pr: ... Changes: - OnConfig struct replaces TriggerConfig, absorbs ScheduleConfig - FrontMatter convenience methods: schedule(), has_schedule(), pipeline_trigger(), pr_trigger(), pr_filters() - PipelineTrigger gains filters: Option<PipelineFilters> for pipeline-specific gate filters (time-window, source-pipeline, branch, build-reason, expression) - PrFilters gains commit-message filter (Build.SourceVersionMessage) - All fixtures, tests, and reference sites updated - 1042 tests pass Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent af6e06f commit 596f025

7 files changed

Lines changed: 224 additions & 94 deletions

File tree

src/compile/common.rs

Lines changed: 68 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use anyhow::{Context, Result};
44
use std::collections::{HashMap, HashSet};
55
use std::path::Path;
66

7-
use super::types::{FrontMatter, PipelineParameter, Repository, TriggerConfig};
7+
use super::types::{FrontMatter, OnConfig, PipelineParameter, Repository};
88
use super::extensions::{CompilerExtension, Extension, McpgServerConfig, McpgGatewayConfig, McpgConfig, CompileContext};
99
use crate::compile::types::McpConfig;
1010
use crate::fuzzy_schedule;
@@ -146,14 +146,14 @@ pub fn validate_front_matter_identity(front_matter: &FrontMatter) -> Result<()>
146146
}
147147

148148
// Validate trigger.pipeline fields for newlines and ADO expressions
149-
if let Some(trigger_config) = &front_matter.triggers {
149+
if let Some(trigger_config) = &front_matter.on_config {
150150
if let Some(pipeline) = &trigger_config.pipeline {
151-
validate::reject_pipeline_injection(&pipeline.name, "triggers.pipeline.name")?;
151+
validate::reject_pipeline_injection(&pipeline.name, "on.pipeline.name")?;
152152
if let Some(project) = &pipeline.project {
153-
validate::reject_pipeline_injection(project, "triggers.pipeline.project")?;
153+
validate::reject_pipeline_injection(project, "on.pipeline.project")?;
154154
}
155155
for branch in &pipeline.branches {
156-
validate::reject_pipeline_injection(branch, &format!("triggers.pipeline.branches entry {:?}", branch))?;
156+
validate::reject_pipeline_injection(branch, &format!("on.pipeline.branches entry {:?}", branch))?;
157157
}
158158
}
159159
}
@@ -202,20 +202,20 @@ pub fn generate_schedule(name: &str, config: &super::types::ScheduleConfig) -> R
202202
/// When `triggers.pr` is explicitly configured, PR triggers stay enabled regardless
203203
/// of schedule or pipeline triggers (overrides suppression). Native ADO branch/path
204204
/// filters are emitted if configured.
205-
pub fn generate_pr_trigger(triggers: &Option<TriggerConfig>, has_schedule: bool) -> String {
206-
let has_pipeline_trigger = triggers
205+
pub fn generate_pr_trigger(on_config: &Option<OnConfig>, has_schedule: bool) -> String {
206+
let has_pipeline_trigger = on_config
207207
.as_ref()
208208
.and_then(|t| t.pipeline.as_ref())
209209
.is_some();
210210

211-
let has_pr_trigger = triggers
211+
let has_pr_trigger = on_config
212212
.as_ref()
213213
.and_then(|t| t.pr.as_ref())
214214
.is_some();
215215

216216
// Explicit triggers.pr overrides schedule/pipeline suppression
217217
if has_pr_trigger {
218-
return super::pr_filters::generate_native_pr_trigger(triggers.as_ref().unwrap().pr.as_ref().unwrap());
218+
return super::pr_filters::generate_native_pr_trigger(on_config.as_ref().unwrap().pr.as_ref().unwrap());
219219
}
220220

221221
match (has_pipeline_trigger, has_schedule) {
@@ -227,8 +227,8 @@ pub fn generate_pr_trigger(triggers: &Option<TriggerConfig>, has_schedule: bool)
227227
}
228228

229229
/// Generate CI trigger configuration
230-
pub fn generate_ci_trigger(triggers: &Option<TriggerConfig>, has_schedule: bool) -> String {
231-
let has_pipeline_trigger = triggers
230+
pub fn generate_ci_trigger(on_config: &Option<OnConfig>, has_schedule: bool) -> String {
231+
let has_pipeline_trigger = on_config
232232
.as_ref()
233233
.and_then(|t| t.pipeline.as_ref())
234234
.is_some();
@@ -241,8 +241,8 @@ pub fn generate_ci_trigger(triggers: &Option<TriggerConfig>, has_schedule: bool)
241241
}
242242

243243
/// Generate pipeline resource YAML for pipeline completion triggers
244-
pub fn generate_pipeline_resources(triggers: &Option<TriggerConfig>) -> Result<String> {
245-
let Some(trigger_config) = triggers else {
244+
pub fn generate_pipeline_resources(on_config: &Option<OnConfig>) -> Result<String> {
245+
let Some(trigger_config) = on_config else {
246246
return Ok(String::new());
247247
};
248248

@@ -1798,7 +1798,7 @@ pub async fn compile_shared(
17981798
validate_front_matter_identity(front_matter)?;
17991799

18001800
// 2. Generate schedule
1801-
let schedule = match &front_matter.schedule {
1801+
let schedule = match front_matter.schedule() {
18021802
Some(s) => generate_schedule(&front_matter.name, s)
18031803
.with_context(|| format!("Failed to parse schedule '{}'", s.expression()))?,
18041804
None => String::new(),
@@ -1839,10 +1839,10 @@ pub async fn compile_shared(
18391839
)?;
18401840
let working_directory = generate_working_directory(&effective_workspace);
18411841
let trigger_repo_directory = generate_trigger_repo_directory(&front_matter.checkout);
1842-
let pipeline_resources = generate_pipeline_resources(&front_matter.triggers)?;
1843-
let has_schedule = front_matter.schedule.is_some();
1844-
let pr_trigger = generate_pr_trigger(&front_matter.triggers, has_schedule);
1845-
let ci_trigger = generate_ci_trigger(&front_matter.triggers, has_schedule);
1842+
let pipeline_resources = generate_pipeline_resources(&front_matter.on_config)?;
1843+
let has_schedule = front_matter.has_schedule();
1844+
let pr_trigger = generate_pr_trigger(&front_matter.on_config, has_schedule);
1845+
let ci_trigger = generate_ci_trigger(&front_matter.on_config, has_schedule);
18461846

18471847
// 6. Generate source path and pipeline path
18481848
let source_path = generate_source_path(input_path);
@@ -1856,11 +1856,7 @@ pub async fn compile_shared(
18561856
.unwrap_or_else(|| DEFAULT_POOL.to_string());
18571857

18581858
// 8. Setup/teardown jobs, parameters, prepare/finalize steps
1859-
let pr_filters = front_matter
1860-
.triggers
1861-
.as_ref()
1862-
.and_then(|t| t.pr.as_ref())
1863-
.and_then(|pr| pr.filters.as_ref());
1859+
let pr_filters = front_matter.pr_filters();
18641860
let has_pr_filters = pr_filters.is_some();
18651861
let setup_job = generate_setup_job(&front_matter.setup, &pool, pr_filters);
18661862
let teardown_job = generate_teardown_job(&front_matter.teardown, &pool);
@@ -2522,13 +2518,15 @@ mod tests {
25222518

25232519
#[test]
25242520
fn test_generate_pr_trigger_pipeline_only() {
2525-
let triggers = Some(crate::compile::types::TriggerConfig {
2521+
let triggers = Some(crate::compile::types::OnConfig {
25262522
pipeline: Some(crate::compile::types::PipelineTrigger {
25272523
name: "Build".into(),
25282524
project: None,
25292525
branches: vec![],
2526+
filters: None,
25302527
}),
25312528
pr: None,
2529+
schedule: None,
25322530
});
25332531
let result = generate_pr_trigger(&triggers, false);
25342532
assert!(result.contains("pr: none"));
@@ -2537,13 +2535,15 @@ mod tests {
25372535

25382536
#[test]
25392537
fn test_generate_pr_trigger_both_pipeline_and_schedule() {
2540-
let triggers = Some(crate::compile::types::TriggerConfig {
2538+
let triggers = Some(crate::compile::types::OnConfig {
25412539
pipeline: Some(crate::compile::types::PipelineTrigger {
25422540
name: "Build".into(),
25432541
project: None,
25442542
branches: vec![],
2543+
filters: None,
25452544
}),
25462545
pr: None,
2546+
schedule: None,
25472547
});
25482548
let result = generate_pr_trigger(&triggers, true);
25492549
assert!(result.contains("pr: none"));
@@ -2570,27 +2570,31 @@ mod tests {
25702570

25712571
#[test]
25722572
fn test_generate_ci_trigger_pipeline_only() {
2573-
let triggers = Some(crate::compile::types::TriggerConfig {
2573+
let triggers = Some(crate::compile::types::OnConfig {
25742574
pipeline: Some(crate::compile::types::PipelineTrigger {
25752575
name: "Build".into(),
25762576
project: None,
25772577
branches: vec![],
2578+
filters: None,
25782579
}),
25792580
pr: None,
2581+
schedule: None,
25802582
});
25812583
let result = generate_ci_trigger(&triggers, false);
25822584
assert_eq!(result, "trigger: none");
25832585
}
25842586

25852587
#[test]
25862588
fn test_generate_ci_trigger_both_pipeline_and_schedule() {
2587-
let triggers = Some(crate::compile::types::TriggerConfig {
2589+
let triggers = Some(crate::compile::types::OnConfig {
25882590
pipeline: Some(crate::compile::types::PipelineTrigger {
25892591
name: "Build".into(),
25902592
project: None,
25912593
branches: vec![],
2594+
filters: None,
25922595
}),
25932596
pr: None,
2597+
schedule: None,
25942598
});
25952599
let result = generate_ci_trigger(&triggers, true);
25962600
assert_eq!(result, "trigger: none");
@@ -2606,20 +2610,22 @@ mod tests {
26062610

26072611
#[test]
26082612
fn test_generate_pipeline_resources_empty_trigger_config() {
2609-
let triggers = Some(crate::compile::types::TriggerConfig { pipeline: None, pr: None });
2613+
let triggers = Some(crate::compile::types::OnConfig { schedule: None, pipeline: None, pr: None });
26102614
let result = generate_pipeline_resources(&triggers).unwrap();
26112615
assert!(result.is_empty());
26122616
}
26132617

26142618
#[test]
26152619
fn test_generate_pipeline_resources_with_branches() {
2616-
let triggers = Some(crate::compile::types::TriggerConfig {
2620+
let triggers = Some(crate::compile::types::OnConfig {
26172621
pipeline: Some(crate::compile::types::PipelineTrigger {
26182622
name: "Build Pipeline".into(),
26192623
project: Some("OtherProject".into()),
26202624
branches: vec!["main".into(), "release/*".into()],
2625+
filters: None,
26212626
}),
26222627
pr: None,
2628+
schedule: None,
26232629
});
26242630
let result = generate_pipeline_resources(&triggers).unwrap();
26252631
assert!(result.contains("source: 'Build Pipeline'"));
@@ -2633,13 +2639,15 @@ mod tests {
26332639

26342640
#[test]
26352641
fn test_generate_pipeline_resources_without_branches_triggers_on_any() {
2636-
let triggers = Some(crate::compile::types::TriggerConfig {
2642+
let triggers = Some(crate::compile::types::OnConfig {
26372643
pipeline: Some(crate::compile::types::PipelineTrigger {
26382644
name: "My Pipeline".into(),
26392645
project: None,
26402646
branches: vec![],
2647+
filters: None,
26412648
}),
26422649
pr: None,
2650+
schedule: None,
26432651
});
26442652
let result = generate_pipeline_resources(&triggers).unwrap();
26452653
assert!(result.contains("source: 'My Pipeline'"));
@@ -2650,13 +2658,15 @@ mod tests {
26502658

26512659
#[test]
26522660
fn test_generate_pipeline_resources_resource_id_is_snake_case() {
2653-
let triggers = Some(crate::compile::types::TriggerConfig {
2661+
let triggers = Some(crate::compile::types::OnConfig {
26542662
pipeline: Some(crate::compile::types::PipelineTrigger {
26552663
name: "My Build Pipeline".into(),
26562664
project: None,
26572665
branches: vec![],
2666+
filters: None,
26582667
}),
26592668
pr: None,
2669+
schedule: None,
26602670
});
26612671
let result = generate_pipeline_resources(&triggers).unwrap();
26622672
// The pipeline resource ID should be snake_case derived from the name
@@ -3562,49 +3572,55 @@ mod tests {
35623572
#[test]
35633573
fn test_validate_front_matter_identity_rejects_newline_in_trigger_pipeline_name() {
35643574
let mut fm = minimal_front_matter();
3565-
fm.triggers = Some(TriggerConfig {
3575+
fm.on_config = Some(OnConfig {
35663576
pipeline: Some(crate::compile::types::PipelineTrigger {
35673577
name: "Build\ninjected: true".to_string(),
35683578
project: None,
35693579
branches: vec![],
3580+
filters: None,
35703581
}),
35713582
pr: None,
3583+
schedule: None,
35723584
});
35733585
let result = validate_front_matter_identity(&fm);
35743586
assert!(result.is_err());
3575-
assert!(result.unwrap_err().to_string().contains("triggers.pipeline.name"));
3587+
assert!(result.unwrap_err().to_string().contains("on.pipeline.name"));
35763588
}
35773589

35783590
#[test]
35793591
fn test_validate_front_matter_identity_rejects_newline_in_trigger_pipeline_project() {
35803592
let mut fm = minimal_front_matter();
3581-
fm.triggers = Some(TriggerConfig {
3593+
fm.on_config = Some(OnConfig {
35823594
pipeline: Some(crate::compile::types::PipelineTrigger {
35833595
name: "Build Pipeline".to_string(),
35843596
project: Some("OtherProject\ninjected: true".to_string()),
35853597
branches: vec![],
3598+
filters: None,
35863599
}),
35873600
pr: None,
3601+
schedule: None,
35883602
});
35893603
let result = validate_front_matter_identity(&fm);
35903604
assert!(result.is_err());
3591-
assert!(result.unwrap_err().to_string().contains("triggers.pipeline.project"));
3605+
assert!(result.unwrap_err().to_string().contains("on.pipeline.project"));
35923606
}
35933607

35943608
#[test]
35953609
fn test_validate_front_matter_identity_rejects_newline_in_trigger_pipeline_branch() {
35963610
let mut fm = minimal_front_matter();
3597-
fm.triggers = Some(TriggerConfig {
3611+
fm.on_config = Some(OnConfig {
35983612
pipeline: Some(crate::compile::types::PipelineTrigger {
35993613
name: "Build Pipeline".to_string(),
36003614
project: None,
36013615
branches: vec!["main\ninjected: true".to_string()],
3616+
filters: None,
36023617
}),
36033618
pr: None,
3619+
schedule: None,
36043620
});
36053621
let result = validate_front_matter_identity(&fm);
36063622
assert!(result.is_err());
3607-
assert!(result.unwrap_err().to_string().contains("triggers.pipeline.branches"));
3623+
assert!(result.unwrap_err().to_string().contains("on.pipeline.branches"));
36083624
}
36093625

36103626
#[test]
@@ -3619,13 +3635,15 @@ mod tests {
36193635
#[test]
36203636
fn test_validate_front_matter_identity_allows_valid_trigger_pipeline_fields() {
36213637
let mut fm = minimal_front_matter();
3622-
fm.triggers = Some(TriggerConfig {
3638+
fm.on_config = Some(OnConfig {
36233639
pipeline: Some(crate::compile::types::PipelineTrigger {
36243640
name: "Build Pipeline".to_string(),
36253641
project: Some("OtherProject".to_string()),
36263642
branches: vec!["main".to_string(), "release/*".to_string()],
3643+
filters: None,
36273644
}),
36283645
pr: None,
3646+
schedule: None,
36293647
});
36303648
let result = validate_front_matter_identity(&fm);
36313649
assert!(result.is_ok());
@@ -3643,13 +3661,15 @@ mod tests {
36433661
#[test]
36443662
fn test_validate_front_matter_identity_rejects_ado_expression_in_trigger_pipeline_name() {
36453663
let mut fm = minimal_front_matter();
3646-
fm.triggers = Some(TriggerConfig {
3664+
fm.on_config = Some(OnConfig {
36473665
pipeline: Some(crate::compile::types::PipelineTrigger {
36483666
name: "Build $(System.AccessToken)".to_string(),
36493667
project: None,
36503668
branches: vec![],
3669+
filters: None,
36513670
}),
36523671
pr: None,
3672+
schedule: None,
36533673
});
36543674
let result = validate_front_matter_identity(&fm);
36553675
assert!(result.is_err());
@@ -3659,13 +3679,15 @@ mod tests {
36593679
#[test]
36603680
fn test_validate_front_matter_identity_rejects_ado_expression_in_trigger_pipeline_project() {
36613681
let mut fm = minimal_front_matter();
3662-
fm.triggers = Some(TriggerConfig {
3682+
fm.on_config = Some(OnConfig {
36633683
pipeline: Some(crate::compile::types::PipelineTrigger {
36643684
name: "Build Pipeline".to_string(),
36653685
project: Some("$(System.AccessToken)".to_string()),
36663686
branches: vec![],
3687+
filters: None,
36673688
}),
36683689
pr: None,
3690+
schedule: None,
36693691
});
36703692
let result = validate_front_matter_identity(&fm);
36713693
assert!(result.is_err());
@@ -3675,13 +3697,15 @@ mod tests {
36753697
#[test]
36763698
fn test_validate_front_matter_identity_rejects_ado_expression_in_trigger_pipeline_branch() {
36773699
let mut fm = minimal_front_matter();
3678-
fm.triggers = Some(TriggerConfig {
3700+
fm.on_config = Some(OnConfig {
36793701
pipeline: Some(crate::compile::types::PipelineTrigger {
36803702
name: "Build Pipeline".to_string(),
36813703
project: None,
36823704
branches: vec!["$[variables['token']]".to_string()],
3705+
filters: None,
36833706
}),
36843707
pr: None,
3708+
schedule: None,
36853709
});
36863710
let result = validate_front_matter_identity(&fm);
36873711
assert!(result.is_err());
@@ -3690,13 +3714,15 @@ mod tests {
36903714

36913715
#[test]
36923716
fn test_pipeline_resources_escapes_single_quotes() {
3693-
let triggers = Some(TriggerConfig {
3717+
let triggers = Some(OnConfig {
36943718
pipeline: Some(crate::compile::types::PipelineTrigger {
36953719
name: "Build's Pipeline".to_string(),
36963720
project: Some("My'Project".to_string()),
36973721
branches: vec!["main".to_string(), "it's-branch".to_string()],
3722+
filters: None,
36983723
}),
36993724
pr: None,
3725+
schedule: None,
37003726
});
37013727
let result = generate_pipeline_resources(&triggers).unwrap();
37023728
assert!(result.contains("source: 'Build''s Pipeline'"));

0 commit comments

Comments
 (0)