feat(compile): make on.pr a first-class trigger via synthetic CI-derived PR context#922
feat(compile): make on.pr a first-class trigger via synthetic CI-derived PR context#922jamesadevine wants to merge 16 commits into
Conversation
Default true. Plumbs the schema through PrTriggerConfig with a serde-renamed field; existing struct-literal call sites use ..Default::default() for forward-compat. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Serializes on.pr branches/paths to base64-encoded JSON consumed by the upcoming exec-context-pr-synth.js bundle. Mirrors GATE_SPEC encoding and adds an 8 KiB defence-in-depth cap. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds skeleton index.ts (no-op main) and match.ts (picomatch-based branch/path glob helpers with refs/heads/ and leading-slash normalisation). Wires the bundle into the npm build/clean/test:smoke chain and the release.yml ado-script.zip packager. Adds picomatch as a direct dependency so ncc bundles it deterministically. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Decodes PR_SYNTH_SPEC, no-ops on real PR builds and GitHub-typed repos, queries ADO REST for active PRs by sourceRefName, enforces exactly-one-match + target-branch filter + path filter, emits AW_SYNTHETIC_PR* outputs consumed by gate.js and exec-context-pr.js. Adds listActivePullRequestsBySourceRef to shared/ado-client.ts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
26 new tests across match.ts (glob normalisation + include/exclude semantics) and index.ts (real-PR no-op, GitHub no-op, spec decode errors, zero/multi-match skips, path filter rejection, happy path with all five AW_SYNTHETIC_PR* outputs). Adds an ESM entry-point guard to index.ts so importing main() does not trigger top-level process.exit. Removes the source-branch pre-filter; pr.branches filters the TARGET ref per the issue spec, not the build source. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When the upstream synthPr Setup-job step has elected a CI build for PR treatment, the gate must run the full PR-spec predicates instead of auto-passing via the 'not a PullRequest build' bypass. Only PullRequest specs honour the override; PipelineCompletion specs are unaffected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds EXEC_CONTEXT_PR_SYNTH_PATH constant, synthetic_pr_active + pr_trigger_for_synth fields to AdoScriptExtension, synthetic_pr_step() generator, and wires collect_extensions to populate the flags from on.pr.synthetic-from-ci. Setup-job emits the synthPr step BEFORE prGate so downstream env coalescing can read the dependencies.Setup.outputs['synthPr.*'] values. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…-context-pr Extends compile_gate_step_external with a synthetic_pr_active flag. When true and ctx == PullRequest, ADO_PR_ID / ADO_SOURCE_BRANCH / ADO_TARGET_BRANCH are emitted as coalesce(variables.System.PullRequest.*, dependencies.Setup.outputs.synthPr.*) so the gate evaluator picks up either the real PR variables or the synthPr Setup-job outputs. Also exports AW_SYNTHETIC_PR so gate/bypass.ts can detect the synthetic-promotion case. PrContextContributor gains the same synthetic_pr_active flag and switches SYSTEM_PULLREQUEST_PULLREQUESTID + SYSTEM_PULLREQUEST_TARGETBRANCH to the coalesced form, with the step condition broadened to accept either real or synthetic PR. This also closes compile-exec-context-cond. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extends generate_agentic_depends_on with a synthetic_pr_active flag. When true the condition gains a leading ne(synthPr.AW_SYNTHETIC_PR_SKIP, true) guard plus a broadened PR clause accepting real PR builds, synthPr promotion, or gate-passed. Threads the flag from compile_shared via front_matter.pr_trigger().synthetic_from_ci. Existing callers default to false (no behavioural change). Adds two unit tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…s on When on.pr.synthetic-from-ci is on (default) and on.pr.branches.include is non-empty, emit a top-level trigger: block mirroring those branches so CI fires only on the configured set. Without this, ADO would queue a build for every push and most would be wasted compute (synthPr would skip them). Pipeline/schedule suppression still wins, synthetic-from-ci: false preserves the previous default, and an empty include list disables the narrowing. Five new unit tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… opt-out Adds two fixtures and two integration tests. Fixture A asserts the full synth wiring is emitted (synthPr step, PR_SYNTH_SPEC env, broadened exec-context-pr.js condition, AW_SYNTHETIC_PR_SKIP guard, narrowed trigger block). Fixture B asserts ALL synth artefacts are absent under synthetic-from-ci: false (substring-negation back-compat guard, no stored baseline needed). The GitHub-resource case planned as fixture C is omitted because it produces the same YAML as A; the runtime no-op is covered by the bundle's vitest suite. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… files front-matter.md gains the new knob in the example block and a 'PR Triggering in Azure Repos' section walking through why the feature exists, the 7-step runtime contract, the auto-narrowed CI trigger, and how to opt out. The three prompt files (create/update/debug) each cross-link to the new section with a one-paragraph note tailored to their context. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: The design and implementation are solid, but there is one logic bug in the CI trigger narrowing that would silently break the feature for the typical PR workflow (feature branch → PR → main), plus a minor injection surface to verify. Findings🐛 Bugs / Logic Issues
🔒 Security Concerns
✅ What Looks Good
|
The narrowed trigger emitted by branch 2 of generate_ci_trigger used pr.branches.include as the trigger include list. Those are PR target branches (e.g. 'main'), but ADO trigger: fires on pushes TO listed branches, so narrowing to [main] suppressed CI on the feature branches synthPr actually needs to react to: a push to feature/x with an open PR feature/x -> main would never queue a build, defeating the entire synthetic-from-ci feature. Remove branch 2 of generate_ci_trigger, the four narrowing-shape unit tests, and the narrowed-trigger assertion in test_synthetic_pr_default_emits_full_synth_wiring. Add a positive 'does not narrow' unit test, a negative integration assertion, and keep the pipeline-trigger priority test. Update docs/front-matter.md and prompts/create-ado-agentic-workflow.md to explain why narrowing is intentionally absent. Cost concern from the original commit (a217df1) is addressed by the synthPr Setup step's existing fast-exit: a single listActivePullRequestsBySourceRef call returns [] for branches without a matching PR and the Agent job self-skips via AW_SYNTHETIC_PR_SKIP. Addresses Rust PR Reviewer feedback on #922. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Needs changes — good architecture overall, but one logic regression and one misleading doc fragment. Findings🐛 Bugs / Logic Issues
📄 Documentation Bug
|
Replaces the unshipped on.pr.synthetic-from-ci: true|false boolean with a two-value enum on.pr.mode: synthetic|policy (default synthetic). The two modes give the agent author a single coherent choice between the no-policy-required path and the operator-installed-branch-policy path.
Mode semantics:
* synthetic (default) — emit synthPr Setup-job step + downstream env coalescing + broadened conditions. CI trigger left at ADO default (all branches). Synth promotes CI builds with a matching open PR; non-matching CI builds self-skip cleanly via AW_SYNTHETIC_PR_SKIP. No Build Validation branch policy required.
* policy — omit all synth wiring AND emit rigger: none. Branch-policy-driven PR builds are the sole source of pipeline runs; feature-branch pushes no longer queue duplicate CI builds. Choose this when an operator has explicitly installed a Build Validation branch policy.
Previously, synthetic-from-ci: false omitted the synth wiring but did NOT suppress the CI trigger, so feature-branch pushes still queued CI builds that immediately bypassed the gate as 'not a PR build'. The new mode: policy closes that gap by emitting rigger: none, so every PR update fires exactly one PR-typed build.
Implementation:
* New PrMode { Synthetic, Policy } enum (Default = Synthetic) in src/compile/types.rs, replacing PrTriggerConfig.synthetic_from_ci: bool. PrTriggerConfig now derives Default.
* generate_ci_trigger gains a mode: policy → trigger: none branch after the existing pipeline/schedule suppression branch.
* All internal call sites (extensions/mod.rs, extensions/exec_context/mod.rs, common.rs::generate_agentic_depends_on derivation) replace p.synthetic_from_ci with matches!(p.mode, PrMode::Synthetic). The internal synthetic_pr_active: bool flag is preserved — it remains the right semantic abstraction.
* Fixtures: synthetic-pr-opt-out.md renamed to pr-mode-policy.md with mode: policy; synthetic-pr-default.md description cleaned (no longer references the removed narrowing). The policy fixture's integration test now asserts rigger: none is emitted.
* Unit tests rewritten around the two-mode contract: synth mode keeps the ADO default, policy mode emits trigger:none, pipeline-completion trigger still wins on priority. Schema tests cover all four cases (omitted, synthetic, policy, invalid value).
* Docs (front-matter.md), and prompts (create/update/debug) rewritten to present the two modes as a table and explain the policy mode's rigger: none emission.
No back-compat alias for synthetic-from-ci since the knob never shipped (still on the feature branch).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Ambitious and well-designed feature — architecture is clean, test coverage is strong, but there are two runtime correctness concerns about how ADO evaluates the synthesised env-block expressions that should be verified before merge. Findings🐛 Bugs / Logic Issues1. Gate step uses a cross-job output reference within the same job ( The gate step is emitted into the Setup job (returned from AW_SYNTHETIC_PR: $[ coalesce(dependencies.Setup.outputs['synthPr.AW_SYNTHETIC_PR'], '') ]
ADO_PR_ID: $[ coalesce(variables['System.PullRequest.PullRequestId'], dependencies.Setup.outputs['synthPr.AW_SYNTHETIC_PR_ID']) ]
Practical consequence: 2. ADO documents The standard pattern for accessing cross-job outputs in a step env is to map the output to a job-level variable first: # Agent job
variables:
synthPrId: $[ dependencies.Setup.outputs['synthPr.AW_SYNTHETIC_PR_ID'] ]
steps:
- bash: ...
env:
ADO_PR_ID: $(synthPrId) # macro, not $[]This would require a compiler-generated 3.
setOutput("AW_SYNTHETIC_PR_TARGETBRANCH", (pr.targetRefName ?? "").replace(/^refs\/heads\//, ""));
|
… enforcement Addresses two real correctness bugs and one stale comment from the Rust PR Reviewer feedback on #922. (1) Gate-step same-job synthPr reference (filter_ir.rs::compile_gate_step_external): The gate step is emitted into the Setup job (same job as `synthPr`), so the env-block references `dependencies.Setup.outputs['synthPr.X']` were silently wrong — that is cross-job syntax and resolves to null inside the producing job, making `coalesce(...)` always return the empty string. On synth-promoted CI builds this left `AW_SYNTHETIC_PR=''`, so `gate/bypass.ts` took the "not a PR build" auto-pass and the agent ran without `pr.filters` being evaluated at all. Fixed by switching the gate-step env coalesce to the same-job runtime expression `variables['synthPr.X']`, which resolves step output variables added to the producing job's variable scope. The Agent-job env (in `exec_context/pr.rs`) keeps `dependencies.Setup.outputs[...]` — that step runs cross-job where the dependencies form is the correct one. (2) Agent-job condition gate enforcement (common.rs::generate_agentic_depends_on): With `mode: synthetic` + `pr.filters`, the synth branch emitted `or(eq(Build.Reason, 'PullRequest'), eq(synthPr.AW_SYNTHETIC_PR, 'true'), eq(prGate.SHOULD_RUN, 'true'))`. The first two arms make any PR build (real or synth) run the agent UNCONDITIONALLY — silently bypassing the gate that `pr.filters` exists to enforce. Replaced with `or(and(ne(Build.Reason, 'PullRequest'), ne(synthPr.AW_SYNTHETIC_PR, 'true')), eq(prGate.SHOULD_RUN, 'true'))` — non-PR / non-synth builds run unconditionally; real-PR and synth-PR builds must pass the gate. (3) Removed stale `compile-coalesce-env todo` comment in `exec_context/pr.rs`; the work referenced is now implemented in the same function. Findings 2 and 3 from the same review were false alarms: `$[ coalesce(...) ]` IS documented as valid in step-level `env:` blocks, and `System.PullRequest.TargetBranch` is documented as the full `refs/heads/<name>` form, matching `pr.targetRefName`. Both clarified inline with MS-docs cross-references. Test changes: * `test_agentic_depends_on_synthetic_pr_active_emits_skip_guard_and_gate_enforced_pr_clause` (renamed from `_and_broader_pr_clause`) — pins the new AND-NOT shape and asserts the old permissive bypass arms are gone. * `test_pr_filter_synth_mode_agent_condition_enforces_gate` — integration test that exercises the `pr-filter-tier1-agent.md` fixture (mode: synthetic + pr.filters) and asserts the Agent-job dependsOn condition contains both AND-NOT arms and none of the buggy bypass arms. * `test_pr_filter_synth_mode_gate_step_uses_same_job_synth_ref` — integration test asserting the gate-step env uses `variables['synthPr.X']` (same-job) and contains no `dependencies.Setup.outputs['synthPr.X']` cross-job references inside the producing job. * `test_synthetic_pr_default_emits_full_synth_wiring` — dropped the misleading `eq(synthPr.AW_SYNTHETIC_PR, 'true')` assertion (the fixture has no filters, so that string came from the exec-context-pr step, not the Agent-job condition). All `cargo test` (1811 lib + 131 compiler-integration + others) and `cargo clippy --all-targets --all-features` pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good overall — well-structured feature with strong test coverage and clear security reasoning. A few items worth fixing. Findings🐛 Bugs / Logic Issues
|
Summary
Closes #916.
Makes
on: pra first-class trigger in Azure Repos by deriving PR context from the ADO REST API on CI-triggered builds. No Build Validation branch policy required.Why
Azure DevOps Services ignores the YAML
pr:block unless a per-branch Build Validation policy is registered server-side. Without that policy, agit pushto a feature branch fires the compiled pipeline asBuild.Reason = IndividualCIeven when an open PR exists, so:scripts/ado-script/src/gate/bypass.tstakes the "Not a Pull Request build — gate passes automatically" path and never gets PR identifiers.scripts/ado-script/src/exec-context-pr/index.tsis conditioned oneq(Build.Reason, 'PullRequest')and is skipped entirely, soaw-context/pr/{base.sha,head.sha}and the agent-prompt PR-context fragment are never staged.This defeated the "one markdown file is the whole workflow" promise — registering an
ado-awagent shouldn't require a separate ADO-UI click for every target branch.What
Adds a new front-matter knob
on.pr.mode(defaultsynthetic). The two modes give the author a single choice between "no policy required" and "policy installed":on.pr.modetrigger:synthetic(default)policytrigger: noneWhen
mode: synthetic(the default):Setup-job script
exec-context-pr-synth.jsqueries the ADO REST API at build time:BUILD_REASON == PullRequest→ no-op (real PR build owns the path).BUILD_REPOSITORY_PROVIDER == GitHub→ no-op (GitHub repos already get correctpr:semantics).PR_SYNTH_SPEC(base64 JSON ofpr.branches+pr.paths).sourceRefName == Build.SourceBranch, filter bytargetRefNamematchingpr.branches.pr.pathsagainst the iteration-API change list. Otherwise → emitAW_SYNTHETIC_PR_SKIP=truewith reason, agent self-skips cleanly.AW_SYNTHETIC_PR=true+AW_SYNTHETIC_PR_ID/SOURCEBRANCH/TARGETBRANCH/IS_DRAFTas Setup-job outputs.Downstream env coalescing:
gate.jsandexec-context-pr.jsenv blocks switchADO_PR_ID,ADO_SOURCE_BRANCH,ADO_TARGET_BRANCH,SYSTEM_PULLREQUEST_*to$[ coalesce(variables['System.PullRequest.*'], dependencies.Setup.outputs['synthPr.*']) ]so real PR builds remain bit-identical while CI-triggered builds pick up the synthesised identifiers.Gate bypass update:
gate/bypass.tschecksAW_SYNTHETIC_PR === 'true'and skips the "not a PR build" auto-pass when the synthPr step promoted the run.Agent-job condition update:
dependsOncondition gains anAW_SYNTHETIC_PR_SKIPguard and a broadened PR clause accepting real-PR / synthetic-promotion / gate-passed.When
mode: policy: none of the synth wiring above is emitted; the compiler additionally emitstrigger: noneso feature-branch pushes do not queue duplicate CI builds alongside the policy-driven PR build. Every PR update fires exactly one PR-typed build.Why the CI trigger is intentionally not auto-narrowed in
mode: syntheticAn earlier iteration auto-emitted a top-level
trigger:block mirroringpr.branches.includeto spare unrelated branches from queueing self-skipping builds. That was a logic bug:pr.branches.includelists PR target branches (e.g.main), but ADOtrigger:fires on pushes to the listed branches. Narrowing to[main]suppressed CI on the feature branches synthPr actually needs to react to — pushing tofeature/xwith an open PRfeature/x → mainwould never queue a build, defeating the entire synthetic-from-ci feature. The compiler therefore leaves the top-leveltrigger:at the ADO default ("trigger on every branch") in synth mode, and relies on the synthPr Setup step's existing fast-exit for cost control: a singlelistActivePullRequestsBySourceRefcall returns[]on branches without a matching PR and the Agent job self-skips viaAW_SYNTHETIC_PR_SKIP=true.Decisions frozen in design
on.pr.mode, defaultsynthetic(alternative:policy)sourceRefNamewhosetargetRefNamematchespr.branches.include/excludepr.pathson CI)BUILD_REPOSITORY_PROVIDER=GitHub)logInfoline +AW_SYNTHETIC_PR_SKIP=true. Never red, never noisy.Hard prerequisite
#915 (the
addBuildTag("pr-gate:passed")colon bug) had to land first — every synthetic-PR gate path would otherwise die on the build-tag REST call. Already merged via #917 (4122a640).Test plan
Automated (all green)
Highlights:
cargo test --bins— 1811 pass, 0 failcargo test --test compiler_tests— 129 pass, 0 fail (includes 2 new snapshot fixtures:synthetic-pr-default.mdasserts full synth wiring with no narrowed CI trigger;pr-mode-policy.mdasserts the policy mode emits zero synth artefacts ANDtrigger: none)scripts/ado-script/src/exec-context-pr-synth/__tests__/(match.ts glob semantics + index.ts runtime contract with mocked ADO client: real-PR no-op, GitHub no-op, spec decode errors, zero/multi-match skips, path-filter rejection, happy path, draft PR)AW_SYNTHETIC_PR=truesuppresses bypass for PR specs only)on.pr.mode(default→synthetic, explicit synthetic, explicit policy, invalid value rejected)build_pr_synth_spectests (round-trip, empty arrays, 8 KiB size cap)generate_agentic_depends_ontests for the synth-skip + broader PR clausegenerate_ci_triggertests covering the two-mode contract (synth mode keeps ADO default, policy mode emitstrigger: none, pipeline-completion trigger still wins)AdoScriptExtension::setup_stepstests for synth-step emission (without and with a gate)node …underset -euo pipefail)Regression sweep
Recompiled 29 of 33 existing fixtures (the other 4 —
runtime_imports_author_marker_*— expectedly need sibling files only the test harness copies). Confirmed:on.prcorrectly carry the new synth wiringon.prshow zero synth artefactson.pragentsManual end-to-end (deferred to a separate task)
Per the implementation plan, the end-to-end demo against
msazuresphere/4x4/azure-devops-agentic-pipelinesPR #38551 (reviewer pipeline runs without a Build Validation branch policy, producing review comments +ado-aw audit <buildId>showing a cleanaw-context/pr/{base.sha,head.sha}stage and no missing-PR-context warnings) will be performed after merge. The 12-commit history is deliberately reviewable in isolation.Files changed
src/compile/types.rs—PrTriggerConfig.synthetic_from_cisrc/compile/{filter_ir,common,pr_filters,extensions/ado_script,extensions/exec_context/{mod,pr},extensions/mod}.rsscripts/ado-script/src/exec-context-pr-synth/{index,match,spec}.ts+ tests + harness;scripts/ado-script/src/{gate/bypass,shared/ado-client}.tsscripts/ado-script/package.json+.gitignore;.github/workflows/release.yml(pack the new bundle intoado-script.zip)tests/compiler_tests.rs+tests/fixtures/synthetic-pr-{default,opt-out}.mddocs/front-matter.md(new "PR Triggering in Azure Repos" section);prompts/{create,update,debug}-ado-agentic-workflow.mdcross-links