Skip to content

Commit 0c8e5e9

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 de4c9c3 commit 0c8e5e9

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
@@ -1257,27 +1257,49 @@ pub fn generate_finalize_steps(finalize_steps: &[serde_yaml::Value]) -> String {
12571257
///
12581258
/// When PR filters are active, adds a condition that allows non-PR builds to
12591259
/// proceed unconditionally, while PR builds require the gate to pass.
1260+
/// When `expression` is provided, it's ANDed into the condition as an escape hatch.
12601261
pub fn generate_agentic_depends_on(
12611262
setup_steps: &[serde_yaml::Value],
12621263
has_pr_filters: bool,
1264+
expression: Option<&str>,
12631265
) -> String {
12641266
let has_setup = !setup_steps.is_empty() || has_pr_filters;
12651267

1266-
if !has_setup {
1268+
if !has_setup && expression.is_none() {
12671269
return String::new();
12681270
}
12691271

1270-
if has_pr_filters {
1271-
"dependsOn: Setup\n\
1272-
\x20 condition: |\n\
1273-
\x20 and(\n\
1274-
\x20 succeeded(),\n\
1275-
\x20 or(\n\
1276-
\x20 ne(variables['Build.Reason'], 'PullRequest'),\n\
1277-
\x20 eq(dependencies.Setup.outputs['prGate.SHOULD_RUN'], 'true')\n\
1278-
\x20 )\n\
1279-
\x20 )"
1280-
.to_string()
1272+
let depends = if has_setup {
1273+
"dependsOn: Setup\n"
1274+
} else {
1275+
""
1276+
};
1277+
1278+
if has_pr_filters || expression.is_some() {
1279+
let mut parts = Vec::new();
1280+
parts.push("succeeded()".to_string());
1281+
1282+
if has_pr_filters {
1283+
parts.push(
1284+
"or(\n\
1285+
\x20 ne(variables['Build.Reason'], 'PullRequest'),\n\
1286+
\x20 eq(dependencies.Setup.outputs['prGate.SHOULD_RUN'], 'true')\n\
1287+
\x20 )"
1288+
.to_string(),
1289+
);
1290+
}
1291+
1292+
if let Some(expr) = expression {
1293+
parts.push(expr.to_string());
1294+
}
1295+
1296+
let condition_body = parts.join(",\n ");
1297+
format!(
1298+
"{depends}\x20 condition: |\n\
1299+
\x20 and(\n\
1300+
\x20 {condition_body}\n\
1301+
\x20 )"
1302+
)
12811303
} else {
12821304
"dependsOn: Setup".to_string()
12831305
}
@@ -1820,7 +1842,8 @@ pub async fn compile_shared(
18201842
let parameters_yaml = generate_parameters(&parameters)?;
18211843
let prepare_steps = generate_prepare_steps(&front_matter.steps, extensions)?;
18221844
let finalize_steps = generate_finalize_steps(&front_matter.post_steps);
1823-
let agentic_depends_on = generate_agentic_depends_on(&front_matter.setup, has_pr_filters);
1845+
let expression = pr_filters.and_then(|f| f.expression.as_deref());
1846+
let agentic_depends_on = generate_agentic_depends_on(&front_matter.setup, has_pr_filters, expression);
18241847
let job_timeout = generate_job_timeout(front_matter);
18251848

18261849
// 9. Token acquisition and env vars

0 commit comments

Comments
 (0)