Skip to content

Commit e48a03e

Browse files
jamesadevineCopilot
andcommitted
feat(compile): add Tier 3 PR filters (time-window, change-count, build-reason, expression)
Add advanced pre-activation gate filters: - time-window: only run during a UTC time range (handles overnight windows) - min-changes / max-changes: gate on number of changed files - build-reason: include/exclude by Build.Reason (PullRequest, Manual, etc.) - expression: raw ADO condition expression escape hatch, ANDed into Agent job condition at compile time Types added to PrFilters: TimeWindowFilter, min_changes, max_changes, build_reason, expression. Shell escape updated to allow colon for time format. generate_agentic_depends_on now accepts optional expression. 13 new tests (40 total in pr_filters module). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 5434bc6 commit e48a03e

3 files changed

Lines changed: 423 additions & 16 deletions

File tree

src/compile/common.rs

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1287,27 +1287,49 @@ pub fn generate_finalize_steps(finalize_steps: &[serde_yaml::Value]) -> String {
12871287
///
12881288
/// When PR filters are active, adds a condition that allows non-PR builds to
12891289
/// proceed unconditionally, while PR builds require the gate to pass.
1290+
/// When `expression` is provided, it's ANDed into the condition as an escape hatch.
12901291
pub fn generate_agentic_depends_on(
12911292
setup_steps: &[serde_yaml::Value],
12921293
has_pr_filters: bool,
1294+
expression: Option<&str>,
12931295
) -> String {
12941296
let has_setup = !setup_steps.is_empty() || has_pr_filters;
12951297

1296-
if !has_setup {
1298+
if !has_setup && expression.is_none() {
12971299
return String::new();
12981300
}
12991301

1300-
if has_pr_filters {
1301-
"dependsOn: Setup\n\
1302-
\x20 condition: |\n\
1303-
\x20 and(\n\
1304-
\x20 succeeded(),\n\
1305-
\x20 or(\n\
1306-
\x20 ne(variables['Build.Reason'], 'PullRequest'),\n\
1307-
\x20 eq(dependencies.Setup.outputs['prGate.SHOULD_RUN'], 'true')\n\
1308-
\x20 )\n\
1309-
\x20 )"
1310-
.to_string()
1302+
let depends = if has_setup {
1303+
"dependsOn: Setup\n"
1304+
} else {
1305+
""
1306+
};
1307+
1308+
if has_pr_filters || expression.is_some() {
1309+
let mut parts = Vec::new();
1310+
parts.push("succeeded()".to_string());
1311+
1312+
if has_pr_filters {
1313+
parts.push(
1314+
"or(\n\
1315+
\x20 ne(variables['Build.Reason'], 'PullRequest'),\n\
1316+
\x20 eq(dependencies.Setup.outputs['prGate.SHOULD_RUN'], 'true')\n\
1317+
\x20 )"
1318+
.to_string(),
1319+
);
1320+
}
1321+
1322+
if let Some(expr) = expression {
1323+
parts.push(expr.to_string());
1324+
}
1325+
1326+
let condition_body = parts.join(",\n ");
1327+
format!(
1328+
"{depends}\x20 condition: |\n\
1329+
\x20 and(\n\
1330+
\x20 {condition_body}\n\
1331+
\x20 )"
1332+
)
13111333
} else {
13121334
"dependsOn: Setup".to_string()
13131335
}
@@ -1947,7 +1969,8 @@ pub async fn compile_shared(
19471969
let parameters_yaml = generate_parameters(&parameters)?;
19481970
let prepare_steps = generate_prepare_steps(&front_matter.steps, extensions)?;
19491971
let finalize_steps = generate_finalize_steps(&front_matter.post_steps);
1950-
let agentic_depends_on = generate_agentic_depends_on(&front_matter.setup, has_pr_filters);
1972+
let expression = pr_filters.and_then(|f| f.expression.as_deref());
1973+
let agentic_depends_on = generate_agentic_depends_on(&front_matter.setup, has_pr_filters, expression);
19511974
let job_timeout = generate_job_timeout(front_matter);
19521975

19531976
// 9. Token acquisition and env vars

0 commit comments

Comments
 (0)