Summary
ado-aw agents that use on.pr cannot actually be triggered by pull-request activity in Azure Repos today, because Azure DevOps Services ignores the YAML pr: block unless a Build Validation branch policy is registered server-side for the target branch. Without that policy, a git push to a feature branch fires the compiled pipeline as Build.Reason = IndividualCI even when an open PR exists, so:
scripts/ado-script/src/gate/bypass.ts takes the "Not a {bypass_label} build — gate passes automatically" path and never gets PR identifiers.
scripts/ado-script/src/exec-context-pr/index.ts is conditioned eq(variables['Build.Reason'], 'PullRequest') and is skipped entirely, so aw-context/pr/{base.sha,head.sha} and the agent-prompt PR-context fragment are never staged.
- Any agent that depends on PR context (e.g. a PR Reviewer) cannot run unless an operator manually adds a per-pipeline, per-target-branch Build Validation policy in the ADO UI.
This defeats the "one markdown file is the whole workflow" model ado-aw is built around: registering an ado-aw agent should never require a separate ADO-UI click for every target branch.
Goal
When an ado-aw-compiled pipeline is triggered by CI on a branch that has an open PR matching the pr: filters in the agent markdown, the runtime should behave as if Build.Reason = PullRequest — discover the PR via the ADO REST API, expose its identifiers to gate.js and exec-context-pr.js, gate normally, and stage aw-context/pr/. No branch policy required.
When no matching PR exists (or path filters reject the diff), the build self-cancels cleanly with a single info log line — never noisy, never red.
Design
Locked decisions
| Decision |
Value |
| Match rule |
Exactly one open PR whose sourceRefName == Build.SourceBranch and whose targetRefName matches the agent's pr.branches.include (respecting exclude). Zero or >1 → clean skip. |
| Path filtering |
When a single PR matches, also enforce pr.paths (include/exclude) against the merge-base..HEAD diff. ADO's CI trigger ignores these. |
| Default |
On whenever on.pr is configured. Opt-out via a new front-matter knob (proposed: on.pr.synthetic-from-ci: false — final name TBD in review). |
| Prerequisite |
#915 (colon-in-build-tag fix) must land first; every gate path currently dies on addBuildTag("pr-gate:passed"). |
Out of scope
- Real PR builds (
Build.Reason == PullRequest) when a branch policy is configured — that path must remain bit-identical.
- Multi-PR fanout (one CI build → N synthetic PR runs).
- GitHub-typed repo resources in ADO — those already have working
pr: semantics; the new step must detect and no-op.
Implementation playbook
A coding agent can pick this up turn-by-turn. The steps below are ordered and each is independently testable.
Step 1 — Schema knob
src/compile/types.rs + src/compile/pr_filters.rs: add synthetic-from-ci: bool (default true) to the on.pr front-matter schema. Thread the flag through so it can suppress all wiring changes when set to false.
Step 2 — PR_SYNTH_SPEC serialiser
src/compile/filter_ir.rs: build a base64-encoded JSON spec carrying:
{
"branches": { "include": [...], "exclude": [...] },
"paths": { "include": [...], "exclude": [...] }
}
Mirror GATE_SPEC mechanics — same MAX_SPEC_DECODED_BYTES cap, same decoding helper pattern.
Step 3 — New runtime script
Step 4 — Gate bypass update
scripts/ado-script/src/gate/bypass.ts: change the bypass check from buildReason !== spec.context.build_reason to buildReason !== spec.context.build_reason && process.env.AW_SYNTHETIC_PR !== "true". Update bypass.test.ts to cover the new path. ADO_PR_ID is already read from env, and the compiler will route the synthetic value into it (next step).
Step 5 — Compile-side YAML wiring
src/compile/common.rs:
- Insert the new step into the Setup job before
prGate:
- bash: node '/tmp/ado-aw-scripts/ado-script/exec-context-pr-synth.js'
name: synthPr
displayName: "Resolve synthetic PR context"
condition: ne(variables['Build.Reason'], 'PullRequest')
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
ADO_COLLECTION_URI: $(System.CollectionUri)
ADO_PROJECT: $(System.TeamProject)
ADO_REPO_ID: $(Build.Repository.ID)
BUILD_REASON: $(Build.Reason)
BUILD_REPOSITORY_PROVIDER: $(Build.Repository.Provider)
BUILD_SOURCEBRANCH: $(Build.SourceBranch)
PR_SYNTH_SPEC: "<base64>"
- Route
coalesce() of real + synthetic values into both gate.js and exec-context-pr.js env blocks:
ADO_PR_ID: $[ coalesce(variables['System.PullRequest.PullRequestId'], dependencies.Setup.outputs['synthPr.AW_SYNTHETIC_PR_ID']) ]
SYSTEM_PULLREQUEST_PULLREQUESTID: $[ coalesce(variables['System.PullRequest.PullRequestId'], dependencies.Setup.outputs['synthPr.AW_SYNTHETIC_PR_ID']) ]
SYSTEM_PULLREQUEST_TARGETBRANCH: $[ coalesce(variables['System.PullRequest.TargetBranch'], dependencies.Setup.outputs['synthPr.AW_SYNTHETIC_PR_TARGETBRANCH']) ]
SYSTEM_PULLREQUEST_SOURCEBRANCH: $[ coalesce(variables['System.PullRequest.SourceBranch'], dependencies.Setup.outputs['synthPr.AW_SYNTHETIC_PR_SOURCEBRANCH']) ]
- Update the
exec-context-pr.js step condition from eq(variables['Build.Reason'], 'PullRequest') to:
condition: or(
eq(variables['Build.Reason'], 'PullRequest'),
eq(dependencies.Setup.outputs['synthPr.AW_SYNTHETIC_PR'], 'true')
)
- Update the Agent-job condition (currently around
common.rs line 2228) from:
or(ne(variables['Build.Reason'], 'PullRequest'), eq(prGate.SHOULD_RUN, 'true'))
to additionally skip when AW_SYNTHETIC_PR_SKIP == 'true':
and(
ne(dependencies.Setup.outputs['synthPr.AW_SYNTHETIC_PR_SKIP'], 'true'),
or(
eq(variables['Build.Reason'], 'PullRequest'),
eq(dependencies.Setup.outputs['synthPr.AW_SYNTHETIC_PR'], 'true'),
eq(dependencies.Setup.outputs['prGate.SHOULD_RUN'], 'true')
)
)
- When
synthetic-from-ci is on, emit a default trigger: block mirroring pr.branches.include for source-branch CI. Today no trigger: is emitted so ADO falls back to all-branches CI; narrowing this keeps unrelated branches from queuing builds. When the agent declares its own trigger: block, defer to it.
Step 6 — Compile-side snapshot tests
src/compile/tests.rs:
- Fixture A —
on.pr with default synthetic-from-ci: true: snapshot the compiled YAML proving the new Setup step, env coalescing, exec-context-pr.js condition, Agent-job condition, and trigger: block all appear.
- Fixture B —
on.pr with synthetic-from-ci: false: snapshot proves the YAML is byte-identical to the pre-change output for back-compat.
- Fixture C — same agent with a
type: GitHub repo resource: snapshot proves the new step is omitted (or that its condition is augmented to no-op on GitHub).
Step 7 — Docs
prompts/create-ado-agentic-workflow.md — add a "PR triggering in Azure Repos" section explaining the no-branch-policy promise, the match rule, the path-filter enforcement, and how to opt out.
docs/ page on on: pr — add "How it works under the hood": the synthetic-CI path, exactly-one-PR rule, what happens on zero/multi-match, GitHub-resource no-op.
CHANGELOG.md.
Acceptance criteria
cargo test + pnpm test (script suite) all green.
- Fixture A produces a pipeline that, when triggered by CI on a branch with one matching open PR, reaches the Agent job and stages
aw-context/pr/{base.sha,head.sha} with the merge-base.
- Fixture B is byte-identical to the pre-change output.
- End-to-end demo: against the test PR in
msazuresphere/4x4/azure-devops-agentic-pipelines (PR #38551, fires the ado-aw reviewer pipeline), the reviewer must produce review comments with no Build Validation branch policy on the target repo.
Hard prerequisite
#915 (addBuildTag colon bug) must land in a release first. Without it, even the successful synthetic-PR gate path dies on addBuildTag("pr-gate:passed").
Summary
ado-awagents that useon.prcannot actually be triggered by pull-request activity in Azure Repos today, because Azure DevOps Services ignores the YAMLpr:block unless a Build Validation branch policy is registered server-side for the target branch. 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 {bypass_label} build — gate passes automatically" path and never gets PR identifiers.scripts/ado-script/src/exec-context-pr/index.tsis conditionedeq(variables['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 defeats the "one markdown file is the whole workflow" model
ado-awis built around: registering anado-awagent should never require a separate ADO-UI click for every target branch.Goal
When an
ado-aw-compiled pipeline is triggered by CI on a branch that has an open PR matching thepr:filters in the agent markdown, the runtime should behave as ifBuild.Reason = PullRequest— discover the PR via the ADO REST API, expose its identifiers togate.jsandexec-context-pr.js, gate normally, and stageaw-context/pr/. No branch policy required.When no matching PR exists (or path filters reject the diff), the build self-cancels cleanly with a single info log line — never noisy, never red.
Design
Locked decisions
sourceRefName == Build.SourceBranchand whosetargetRefNamematches the agent'spr.branches.include(respectingexclude). Zero or >1 → clean skip.pr.paths(include/exclude) against the merge-base..HEAD diff. ADO's CI trigger ignores these.on.pris configured. Opt-out via a new front-matter knob (proposed:on.pr.synthetic-from-ci: false— final name TBD in review).addBuildTag("pr-gate:passed").Out of scope
Build.Reason == PullRequest) when a branch policy is configured — that path must remain bit-identical.pr:semantics; the new step must detect and no-op.Implementation playbook
A coding agent can pick this up turn-by-turn. The steps below are ordered and each is independently testable.
Step 1 — Schema knob
src/compile/types.rs+src/compile/pr_filters.rs: addsynthetic-from-ci: bool(defaulttrue) to theon.prfront-matter schema. Thread the flag through so it can suppress all wiring changes when set tofalse.Step 2 —
PR_SYNTH_SPECserialisersrc/compile/filter_ir.rs: build a base64-encoded JSON spec carrying:{ "branches": { "include": [...], "exclude": [...] }, "paths": { "include": [...], "exclude": [...] } }GATE_SPECmechanics — sameMAX_SPEC_DECODED_BYTEScap, same decoding helper pattern.Step 3 — New runtime script
New module
scripts/ado-script/src/exec-context-pr-synth/:index.tsentry point.match.ts— glob matching for branches and paths (reusepicomatchif already in the bundle, otherwise add minimal glob helper).__tests__/covering: exactly-one-match happy path, zero matches, >1 matches, branch include/exclude filtering, path include/exclude filtering,Build.Reason == PullRequestno-op,Build.Repository.Provider == GitHubno-op.Runtime behaviour:
process.env.BUILD_REASON == "PullRequest"→ emit nothing, exit 0 (realSYSTEM_PULLREQUEST_*vars are authoritative).process.env.BUILD_REPOSITORY_PROVIDER == "GitHub"→ emit nothing, exit 0 (GitHub-typed repos already work).PR_SYNTH_SPEC. FilterBuild.SourceBranchagainstbranches.include/exclude; if it doesn't match, emit##vso[task.setvariable variable=AW_SYNTHETIC_PR_SKIP;isOutput=true]trueand onelogInfoline, exit 0.GET /{project}/_apis/git/repositories/{repoId}/pullrequests?searchCriteria.sourceRefName=$(Build.SourceBranch)&searchCriteria.status=activeviashared/ado-client.ts. ReusewithRetry+ the existing auth path.targetRefNamematchesbranches.include/exclude. Ifcount != 1→ setAW_SYNTHETIC_PR_SKIP=truewith reason in the log, exit 0.getPullRequestIterations+getIterationChanges(already inado-client.ts— no new SDK surface). Applypaths.include/excludeto the changed-file list. If empty →AW_SYNTHETIC_PR_SKIP=true, exit 0.isOutput=trueso dependent jobs can read them):AW_SYNTHETIC_PR=trueAW_SYNTHETIC_PR_ID=<id>AW_SYNTHETIC_PR_TARGETBRANCH=refs/heads/<target>AW_SYNTHETIC_PR_SOURCEBRANCH=refs/heads/<source>AW_SYNTHETIC_PR_IS_DRAFT=<bool>Step 4 — Gate bypass update
scripts/ado-script/src/gate/bypass.ts: change the bypass check frombuildReason !== spec.context.build_reasontobuildReason !== spec.context.build_reason && process.env.AW_SYNTHETIC_PR !== "true". Updatebypass.test.tsto cover the new path.ADO_PR_IDis already read from env, and the compiler will route the synthetic value into it (next step).Step 5 — Compile-side YAML wiring
src/compile/common.rs:prGate:coalesce()of real + synthetic values into bothgate.jsandexec-context-pr.jsenv blocks:exec-context-pr.jsstep condition fromeq(variables['Build.Reason'], 'PullRequest')to:common.rsline 2228) from:AW_SYNTHETIC_PR_SKIP == 'true':synthetic-from-ciis on, emit a defaulttrigger:block mirroringpr.branches.includefor source-branch CI. Today notrigger:is emitted so ADO falls back to all-branches CI; narrowing this keeps unrelated branches from queuing builds. When the agent declares its owntrigger:block, defer to it.Step 6 — Compile-side snapshot tests
src/compile/tests.rs:on.prwith defaultsynthetic-from-ci: true: snapshot the compiled YAML proving the new Setup step, env coalescing,exec-context-pr.jscondition, Agent-job condition, andtrigger:block all appear.on.prwithsynthetic-from-ci: false: snapshot proves the YAML is byte-identical to the pre-change output for back-compat.type: GitHubrepo resource: snapshot proves the new step is omitted (or that its condition is augmented to no-op on GitHub).Step 7 — Docs
prompts/create-ado-agentic-workflow.md— add a "PR triggering in Azure Repos" section explaining the no-branch-policy promise, the match rule, the path-filter enforcement, and how to opt out.docs/page onon: pr— add "How it works under the hood": the synthetic-CI path, exactly-one-PR rule, what happens on zero/multi-match, GitHub-resource no-op.CHANGELOG.md.Acceptance criteria
cargo test+pnpm test(script suite) all green.aw-context/pr/{base.sha,head.sha}with the merge-base.msazuresphere/4x4/azure-devops-agentic-pipelines(PR #38551, fires theado-aw reviewerpipeline), the reviewer must produce review comments with no Build Validation branch policy on the target repo.Hard prerequisite
#915 (
addBuildTagcolon bug) must land in a release first. Without it, even the successful synthetic-PR gate path dies onaddBuildTag("pr-gate:passed").