Skip to content

Commit a59eda0

Browse files
authored
fix(ado-script): recover shallow Azure checkout merge bases (#1563)
* fix(compile): emit explicit zero fetch depth Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3a226a4f-e4b2-44cf-a957-9a5731b6ae13 * test(ado-script): add Azure shallow checkout baseline Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3a226a4f-e4b2-44cf-a957-9a5731b6ae13 * test(ado-script): use hosted pool for bundle e2e Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3a226a4f-e4b2-44cf-a957-9a5731b6ae13 * fix(ado-script): recover shallow PR merge bases Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3a226a4f-e4b2-44cf-a957-9a5731b6ae13 * test(ado-script): verify Azure checkout behavior Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3a226a4f-e4b2-44cf-a957-9a5731b6ae13 * docs(ado-script): document bounded shallow recovery Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3a226a4f-e4b2-44cf-a957-9a5731b6ae13 * fix(ado-script): harden shallow fetch boundaries Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3a226a4f-e4b2-44cf-a957-9a5731b6ae13 * fix(ado-script): scope authenticated shallow fetches Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3a226a4f-e4b2-44cf-a957-9a5731b6ae13
1 parent 5001f4a commit a59eda0

28 files changed

Lines changed: 1931 additions & 562 deletions

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ fail-closed and only pauses when the agent actually proposed a reviewed output.
268268
│ ├── approval-summary/ # Safe-outputs summary renderer (bundled to approval-summary.js; end-of-Agent-job summary tab)
269269
│ ├── github-app-token/ # GitHub App token minter (bundled to github-app-token.js; mints installation token in Agent + Detection when engine.github-app-token is set)
270270
│ ├── executor-e2e/ # Stage 3 safe-output E2E test harness (not a bundle; runs deterministic scenarios against a real ADO project and files a GitHub issue on failure)
271-
│ ├── prepare-pr-base/ # create-pull-request base-ref preparer (bundled to prepare-pr-base.js; fetches/deepens target branch so mcp.rs finds a diff base on shallow-default pools — issue #1413; emitted in BOTH the Agent job and the SafeOutputs job before the executor's worktree add — issue #1453)
271+
│ ├── prepare-pr-base/ # create-pull-request preparer (bundled to prepare-pr-base.js): Agent mode uses ADO diff metadata + bounded dual-ref fallback to make the merge-base reachable; SafeOutputs mode fetches only the target worktree tip
272272
│ └── shared/ # Shared modules across bundles (auth, ado-client, env-facts, types.gen.ts)
273273
├── tests/ # Integration tests and fixtures
274274
├── docs/ # Per-concept reference documentation (see index below)

docs/ado-script.md

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -74,21 +74,16 @@ pipeline** as runtime helpers. Today it produces thirteen bundles:
7474
base — issue #1413) **and** the SafeOutputs job (before `ado-aw execute`, so
7575
the Stage 3 executor's `git worktree add` resolves `origin/<target>`; each ADO
7676
job has an isolated checkout, so the ref must be re-fetched in the job that
77-
builds the worktree — issue #1453). For each allowed create-PR repo dir
78-
(`self` + every `checkout:` alias, passed as repeated `--repo-dir <dir>
79-
--target-branch <branch>` pairs in the same dir form
80-
`mcp.rs::resolve_git_dir_for_patch` resolves), it fetches and progressively
81-
deepens THAT repo's resolved target branch into `refs/remotes/origin/<target>`
82-
and points `refs/remotes/origin/HEAD` at it, so a PR can be computed/opened on
83-
shallow-default agent pools without a full-history `checkout: self`. In a
84-
multi-checkout ("meta repo") setup each dir may carry a
85-
different target (see `create-pull-request`'s `target-branches` /
86-
`infer-target-from-checkout-ref`). Reuses
87-
`shared/merge-base.ts::ensureTargetRefFetched` (the same fetch/deepen logic as
88-
the PR execution-context precompute). Each `--repo-dir` is a double-quoted
89-
ADO-macro path; each `--target-branch` is a single-quoted literal; the ADO
90-
bearer (`SYSTEM_ACCESSTOKEN`) rides in masked env for the authenticated git
91-
fetch. Per-dir failures are isolated (logged + skipped). Runs outside AWF. See
77+
builds the worktree — issue #1453). Agent `patch-base` mode uses ADO
78+
`getCommitDiffs` metadata (`commonCommit`, ahead, behind) to fetch the exact
79+
shallow source/target ranges and verifies the base locally. Ineligible or
80+
unavailable REST falls back to bounded dual-ref depths 200/500/2000, never an
81+
automatic full-history fetch. SafeOutputs `target-worktree` mode fetches only
82+
the target tip at depth 1. Each allowed repo is passed as a typed
83+
`--repo-dir` / `--source-ref` / `--target-branch` tuple; per-dir failures are
84+
isolated and surfaced as ADO warnings. The bearer
85+
(`SYSTEM_ACCESSTOKEN`) remains in masked env and spawned-git
86+
`GIT_CONFIG_*`, never argv or `.git/config`. Runs outside AWF. See
9287
[`safe-outputs.md`](safe-outputs.md#create-pull-request).
9388

9489
> **Internal-only.** `ado-script` is not a user-facing front-matter
@@ -216,10 +211,10 @@ inside `src/compile/extensions/exec_context/pr.rs`:
216211
merge-commit (parent count ≥ 3 per ADO's PR-validation flow),
217212
`merge-base.ts::resolveMergeBase` computes `git merge-base` over
218213
the two parents. If that cannot resolve in a shallow checkout, it
219-
fetches both target and source refs with progressive deepening
220-
(`--depth=200/500/2000/--unshallow`) and retries the parent
221-
merge-base. Otherwise it fetches the target branch with progressive
222-
deepening and then runs `git merge-base` against `HEAD`. Same
214+
fetches both target and source refs with bounded deepening
215+
(`--depth=200/500/2000`) and retries the parent merge-base. Otherwise
216+
it fetches both source and target at the same bounded depths and then
217+
runs `git merge-base` against `HEAD`. Same
223218
`BASE_SHA` semantics in both paths (git's true common ancestor).
224219
3. **Stage artefacts** — writes `aw-context/pr/base.sha` and
225220
`aw-context/pr/head.sha` so the agent can `git diff $(cat

docs/execution-context.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ historically rebuilt the same ~120 lines of bash to work around this.
2929
The execution-context plugin owns that step centrally — but does
3030
*only* the part the agent cannot do for itself:
3131

32-
- Fetches the PR target branch with progressive deepening until
32+
- Fetches the PR source and target refs with bounded deepening until
3333
`git merge-base` resolves (requires the bearer; cannot happen
3434
inside the agent's sandbox).
3535
- Writes the resolved `base.sha` and `head.sha` so the agent can
@@ -484,11 +484,11 @@ under `scripts/ado-script/src/exec-context-pr/`. The bundle:
484484
computes `git merge-base HEAD^1 HEAD^2` as the base — same
485485
semantics as the deepening path. If a shallow checkout lacks
486486
sufficient ancestry, it fetches both the target and source refs
487-
with progressive deepening and retries; unresolved ancestry fails
487+
with bounded deepening and retries; unresolved ancestry fails
488488
closed into `error.txt`.
489489
Otherwise:
490-
4. **Fetches the PR target branch with progressive deepening** —
491-
`--depth=200`, then `500`, then `2000`, then finally `--unshallow`.
490+
4. **Fetches source and target with bounded deepening** —
491+
`--depth=200`, then `500`, then `2000`.
492492
After each successful fetch, attempts `git merge-base
493493
origin/<target> HEAD` and continues to the next depth if it
494494
cannot resolve yet. See `merge-base.ts`.
@@ -540,7 +540,7 @@ preserves the Stage 1 read-only invariant with these design choices:
540540
| Mechanism | Decision |
541541
|-----------------------------------------------------------|----------|
542542
| Override `checkout: self` with `persistCredentials: true` | **Rejected.** It would write the build identity's bearer into `.git/config` inside the workspace, which is then mounted into the AWF sandbox where the agent could read and exfiltrate it. |
543-
| Override `checkout: self` with `fetchDepth: 0` | **Rejected.** Unnecessary — the precompute fetches exactly the refs it needs. |
543+
| Override `checkout: self` with `fetchDepth: 0` | **Not automatic.** Authors may opt into explicit full history, but it can be very expensive; the precompute normally uses bounded source/target fetches. |
544544
| In-step `SYSTEM_ACCESSTOKEN` + `GIT_CONFIG_*` bearer env | **Adopted.** `SYSTEM_ACCESSTOKEN` is mapped from `$(System.AccessToken)` only into the `node exec-context-pr.js` step's process env. The bundle's `git.ts::bearerEnv` then injects `GIT_CONFIG_COUNT` / `GIT_CONFIG_KEY_0` / `GIT_CONFIG_VALUE_0` into the *spawned `git` child process's* env only — not into the Node process's own env, and never via `git -c` on argv. The token never appears in process listings and is never written to disk. After the Node process exits, the bearer is gone from the runtime environment the agent inherits. |
545545
546546
After the precompute step exits, the bearer is gone from the runtime

docs/front-matter.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -408,10 +408,11 @@ Object fields:
408408

409409
### Tuning checkout fetch behavior (`fetch-depth` / `fetch-tags`)
410410

411-
On large monorepos the checkout step can dominate the run: ADO's default
412-
`checkout` performs a full-history clone **and** `git fetch --tags`, which may
413-
take tens of minutes. `fetch-depth` and `fetch-tags` let you tune this
414-
per-repository:
411+
On large monorepos the checkout step can dominate the run. Azure DevOps can
412+
apply a pipeline-level shallow-fetch setting (newer pipelines commonly use
413+
depth 1), while tag syncing can also add substantial transfer. `fetch-depth`
414+
and `fetch-tags` let source-controlled YAML override those settings per
415+
repository:
415416

416417
```yaml
417418
repos:
@@ -420,7 +421,9 @@ repos:
420421
fetch-tags: false # skip the (often huge) tag fetch
421422
```
422423

423-
- `fetch-depth: 0` means **full history** (no `fetchDepth` is emitted).
424+
- `fetch-depth: 0` explicitly emits `fetchDepth: 0`, disabling shallow fetch
425+
even when the pipeline UI is configured for depth 1. Full history can be
426+
very expensive in a large or old repository.
424427
- When a field is omitted the ADO default applies, so agents that don't set
425428
these compile **unchanged**.
426429
- Setting `fetch-depth`/`fetch-tags` on an entry with `checkout: false` has no

docs/safe-outputs.md

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -272,31 +272,32 @@ Creates a pull request with code changes made by the agent. When invoked:
272272

273273
During Stage 3 execution, the repository is validated against the allowed list (from `checkout:` + "self"), then the patch is applied and a PR is created in Azure DevOps.
274274

275-
**Shallow-clone agent pools (automatic):** The diff base for the patch is
276-
computed at agent time from the checked-out repository. On agent pools whose
277-
default git fetch is shallow (`fetchDepth: 1`), a bare `checkout` leaves no
278-
`origin/<target-branch>` ref, which would otherwise prevent the diff base from
279-
being computed. To handle this transparently, whenever `create-pull-request` is
280-
configured the compiler emits a credentialed **prepare step** that fetches and
281-
progressively deepens the configured `target-branch` and points `origin/HEAD` at
282-
it — in the `self` checkout **and in each additional `checkout:` repo dir**, so a
283-
PR to *any* allowed repository works. The prepare step runs in **both** the Agent
284-
job (before the agent runs, so the host-side SafeOutputs MCP server can compute
285-
the diff base) **and** the SafeOutputs job (before `ado-aw execute`, so the
286-
Stage 3 executor's `git worktree add` resolves `origin/<target>` — each ADO job
287-
has an isolated checkout, so the ref must be re-fetched in the job that builds
288-
the worktree; issue #1453). This means create-pull-request works on
289-
shallow-default pools **without** forcing a full-history checkout and **without**
290-
hand-editing the compiled lock (so the runtime integrity check keeps passing). No
291-
configuration is required. See [`docs/ado-script.md`](ado-script.md)
275+
**Shallow-clone agent pools (automatic):** The diff base is computed at agent
276+
time from the checked-out repository. For same-organization Azure Repos,
277+
`prepare-pr-base.js` asks the ADO Diffs API for the exact `commonCommit`,
278+
`aheadCount`, and `behindCount`, then fetches only the source and target ranges
279+
needed to make that base locally reachable. It verifies the server result with
280+
`git merge-base --all` before the host-side SafeOutputs MCP server can generate
281+
a patch. Non-Azure/cross-organization/unavailable-REST cases use bounded
282+
dual-ref depths 200/500/2000 and fail clearly rather than silently fetching full
283+
history.
284+
285+
The compiler emits separate modes in the two isolated ADO jobs:
286+
287+
- **Agent — `patch-base`:** prepares and verifies both sides of the merge-base.
288+
- **SafeOutputs — `target-worktree`:** fetches only `origin/<target>` at depth 1
289+
for the executor's `git worktree add` (issue #1453).
290+
291+
No full-history checkout or `--unshallow` fallback is forced. Authors can
292+
explicitly set `repos: [{ name: self, fetch-depth: 0 }]` when they accept that
293+
potentially large cost. The generated lock remains source-controlled and the
294+
runtime integrity check stays enabled. See [`docs/ado-script.md`](ado-script.md)
292295
(`prepare-pr-base.js`).
293296

294-
> **Branch semantics.** The step deepens each repo's resolved `target-branch`
295-
> (the PR's **destination/base**) — not the per-repo `repos:` checkout `ref` (the
296-
> source side). By default every repo targets the single `target-branch`; enable
297-
> `infer-target-from-checkout-ref` (and/or `target-branches`) to give each repo
298-
> its own base branch in a multi-checkout setup. The deepened branch always
299-
> matches the branch the PR targets (shared resolution).
297+
> **Branch semantics.** Each repo carries its checkout source ref and its
298+
> resolved PR destination. By default every repo targets the single
299+
> `target-branch`; enable `infer-target-from-checkout-ref` (and/or
300+
> `target-branches`) to give each repo its own base branch.
300301

301302
**Stage 3 Execution Architecture (Hybrid Git + ADO API):**
302303

prompts/create-ado-agentic-workflow.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ When `on.pr` is set: the native ADO `pr:` trigger block is generated from `branc
496496

497497
**`on.pr` triggering works without a Build Validation branch policy.** By default (`mode: synthetic`), the compiler emits a Setup-job script that, on CI-triggered builds, looks up the open PR for `Build.SourceBranch` via the ADO REST API and promotes the build to PR semantics if exactly one matches `pr.branches` (and `pr.paths` if configured). Zero or multiple matches → the Agent job self-skips cleanly. Set `on.pr.mode: policy` when an operator-installed Build Validation branch policy is in place — that mode omits all synth wiring AND emits `trigger: none` so feature-branch pushes do not queue duplicate CI builds alongside the policy-driven PR build. Note that in `mode: synthetic` the top-level CI `trigger:` is **not** auto-narrowed to `pr.branches.include`: those are PR target branches, and ADO `trigger:` fires on pushes *to* listed branches, so narrowing would suppress CI on the feature branches synthPr must react to. Full reference: ["PR Triggering in Azure Repos" in `docs/front-matter.md`](../docs/front-matter.md#pr-triggering-in-azure-repos).
498498

499-
**PR-reviewer agents — DO NOT write your own precompute step.** When `on.pr` is set, the compiler automatically (1) fetches the PR target branch with progressive deepening, (2) resolves and stages `aw-context/pr/base.sha` + `aw-context/pr/head.sha`, (3) appends a prompt fragment listing common `git diff`/`git show`/`git log` commands and example Azure DevOps MCP tool calls (`repo_get_pull_request_by_id`, `repo_list_pull_request_threads`, `repo_create_pull_request_thread`) with the PR id / project / repo pre-filled, and (4) adds `git`, `git diff`, `git log`, `git show`, `git status`, `git rev-parse`, `git symbolic-ref` to the agent's bash allow-list. The agent runs `git diff $BASE..$HEAD` itself inside the AWF sandbox (objects are already fetched into the workspace). On failure (e.g. merge-base could not be resolved), the failure fragment tells the agent to surface the error rather than produce an empty review. Opt out via `execution-context.pr.enabled: false`. Full reference: [`docs/execution-context.md`](../docs/execution-context.md).
499+
**PR-reviewer agents — DO NOT write your own precompute step.** When `on.pr` is set, the compiler automatically (1) fetches the PR source and target refs with bounded deepening, (2) resolves and stages `aw-context/pr/base.sha` + `aw-context/pr/head.sha`, (3) appends a prompt fragment listing common `git diff`/`git show`/`git log` commands and example Azure DevOps MCP tool calls (`repo_get_pull_request_by_id`, `repo_list_pull_request_threads`, `repo_create_pull_request_thread`) with the PR id / project / repo pre-filled, and (4) adds `git`, `git diff`, `git log`, `git show`, `git status`, `git rev-parse`, `git symbolic-ref` to the agent's bash allow-list. The agent runs `git diff $BASE..$HEAD` itself inside the AWF sandbox (objects are already fetched into the workspace). On failure (e.g. merge-base could not be resolved), the failure fragment tells the agent to surface the error rather than produce an empty review. Opt out via `execution-context.pr.enabled: false`. Full reference: [`docs/execution-context.md`](../docs/execution-context.md).
500500

501501
#### Pipeline Triggers (`on.pipeline`)
502502

0 commit comments

Comments
 (0)