feat(compile): decouple create-pull-request diff base from checkout depth#1425
Conversation
|
@copilot resolve conflicts |
Done. Merged |
|
/rust-review |
|
✅ Rust PR Reviewer completed successfully! |
🔍 Rust PR ReviewSummary: Solid implementation — the approach is correct and well-tested. Two minor findings worth addressing. Findings
|
…drift Address Rust PR Reviewer feedback on #1425: - Make create_pr_target_branch() fall back to CreatePrConfig's default target branch instead of a hard-coded "main", so the compile-time prepare-step default and the Stage 3 executor default cannot drift. - Add a types.rs unit test covering explicit/implicit(default)/absent create-pull-request, with a guard asserting the shared default is "main". - Add a compile-integration test that a bare `create-pull-request: {}` emits the prepare step targeting 'main' (exercises the fallback e2e). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good — solid implementation with thorough test coverage; one minor doc formatting bug and one edge case worth noting. Findings
|
🔍 Rust PR ReviewSummary: Two findings from the prior review run remain unaddressed; everything else is solid. Findings🐛 Bugs / Logic Issues
|
…epth On shallow-default ADO agent pools a bare `checkout: self` leaves no `origin/<target>` ref, so the host-side SafeOutputs MCP server (`src/mcp.rs::find_merge_base`) cannot compute a create-pull-request diff base and the PR fails to open. Previously the only workaround was hand-editing the compiled lock (forcing `fetchDepth: 0`), which then tripped the runtime integrity check. Following gh-aw's decoupled model, add a credentialed Agent-job prepare step (runs on the host before the AWF-wrapped agent, using `$(System.AccessToken)`) that fetches and progressively deepens the configured `target-branch` and points `origin/HEAD` at it, so the MCP server resolves a base with no forced full-history checkout and no lock hand-edit. Automatic when create-pull-request is configured; no new front-matter. - New `prepare-pr-base` ado-script bundle reusing an extracted `shared/merge-base.ts::ensureTargetRefFetched` helper. - `Bundle::PreparePrBase`, `prepare_pr_base_active`, `prepare_pr_base_step_typed`, `FrontMatter::create_pr_target_branch()`, activation + Agent-job emission. - Unit + compile-integration + mcp symbolic-ref regression tests; docs. Closes #1413 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…drift Address Rust PR Reviewer feedback on #1425: - Make create_pr_target_branch() fall back to CreatePrConfig's default target branch instead of a hard-coded "main", so the compile-time prepare-step default and the Stage 3 executor default cannot drift. - Add a types.rs unit test covering explicit/implicit(default)/absent create-pull-request, with a guard asserting the shared default is "main". - Add a compile-integration test that a bare `create-pull-request: {}` emits the prepare step targeting 'main' (exercises the fallback e2e). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…repo) The prepare-pr-base bundle chdir'd to $(Build.SourcesDirectory), but the host-side SafeOutputs MCP server operates on the resolved `self` working directory, which in multi-repo layouts is a $(Build.Repository.Name) subdirectory of $(Build.SourcesDirectory). In that layout the bundle would deepen the wrong directory and the diff base would still fail. Thread the resolved working_directory into the step as a `--repo-dir` argv flag (identical to the MCP server's bounding_directory) and chdir to it, falling back to BUILD_SOURCESDIRECTORY then cwd. Add unit + parse-arg tests covering the multi-repo repo-dir. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t repos An agent can call create-pull-request with `repository: <alias>` for any checked-out repo, but the prepare step only deepened the self clone — so a PR to an alias repo still hit the shallow-clone diff-base failure. Deepen the target branch in every git dir mcp.rs::resolve_git_dir_for_patch would read: working_directory (self) + working_directory/<alias> per checkout alias. The bundle now loops over repeated `--repo-dir` flags, isolating per-dir failures. Repo-dir args use double quotes (ADO-macro path convention, shellcheck SC2016-clean); target-branch stays single-quoted. Branch semantics: the single create-pull-request target-branch (the PR destination/base) is deepened uniformly in every repo dir, not the per-repo checkout ref. Per-repo target branches + the pre-existing MCP path-resolution mismatches are tracked as a follow-up. Also fixes a latent shellcheck SC2016 on the single-repo --repo-dir arg introduced in the prior commit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A meta repo that checks out N repos can now open a PR into each repo's own target branch, instead of a single target-branch applied to all. The base-ref deepening (prepare-pr-base) uses the per-repo target so the diff base resolves for each repo, and Stage 3 opens each PR into its resolved target. New create-pull-request config (collision-free — target-branch stays a literal): - target-branch: literal fallback (default main), unchanged. - target-branches: per-alias literal overrides (highest precedence). - infer-target-from-checkout-ref: opt-in bool; a checkout repo with no explicit override targets its own repos: ref. self never infers. Resolution (shared by the compiler's deepening and Stage 3's PR creation, so the deepened branch always matches the PR base): target-branches[R] > inferred ref > target-branch > main. - CreatePrConfig gains target_branches + infer_target_from_checkout_ref + resolve_target_branch(); ExecutionContext gains repo_refs (populated at Stage 3 and used to resolve per-repo targets). - prepare-pr-base emits/consumes repeated `--repo-dir <dir> --target-branch <branch>` pairs (per-dir target); double-quoted ADO-macro dir, single-quoted literal branch (shellcheck-clean). - Resolver unit matrix + compile-integration (meta repo, diverse refs) + bundle multi-target tests; docs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
92a879b to
3182e85
Compare
🔍 Rust PR ReviewSummary: Solid, well-designed implementation — the approach is sound and test coverage is thorough. Two minor issues worth addressing; everything else is done correctly. Findings🐛 Bugs / Logic Issues
|
…eedback) Address the Rust PR Reviewer findings on #1425: - short_branch: guard the degenerate "refs/heads/" (empty after strip) to return the original ref instead of "", so a malformed repos: ref never yields an empty target_ref. Align the TS shortBranch counterpart so both sides resolve identically (no drift). - lower_repos: reject repository aliases containing shell-unsafe characters via is_safe_path_segment (the same allowlist the runtime MCP server uses). The alias is embedded in a double-quoted bash path in prepare-pr-base and used unquoted as an ADO checkout/resource name. - CreatePrConfig.target_branches: add #[sanitize_config(sanitize_keys)] so alias keys are sanitized like values, consistent with project discipline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good overall — the core security invariants are sound and the architecture is clean. Two findings worth addressing before merge. Findings🐛 Bugs / Logic Issues
|
…r config Address the Rust PR Reviewer findings on #1425: - main.rs build_execution_context: call front_matter.checkout_repo_refs() instead of duplicating its filter/map inline, so the Stage 3 repo_refs can never drift from the compile-time helper. - types.rs create_pr_config(): warn (not silently default) when the create-pull-request config fails to deserialize. Kept as a warning rather than bail! because Stage 3's get_tool_config also swallows the error and uses Default (.ok().unwrap_or_default()); bailing only at compile time would create the very compile-vs-runtime asymmetry the reviewer flagged. - prepare-pr-base shortBranch: align with Rust short_branch for empty input ("" -> "") so the lock-step comment is accurate for every input. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Well-designed, thoroughly-tested. Two findings worth addressing — one minor inconsistency and one edge-case UX issue. Findings
|
Address remaining review feedback on #1425: - create-pull-request: null (the "enable with defaults" idiom) no longer emits a spurious malformed-config warning; create_pr_config() maps null to defaults silently while still enabling the tool + prepare step. - Warn at compile time when infer-target-from-checkout-ref is set and a checkout repo is at a non-branch ref (e.g. refs/tags/*), which would make the PR target an invalid refs/heads/refs/tags/... ref. Advisory (not fatal): the repo may be a dependency checkout the agent never PRs against. - Lock the target_branches sanitization contract with a test: the #[sanitize_config(sanitize_keys)] derive sanitizes BOTH keys and values. - Fix the docs/ado-script.md ASCII tree connectors (conclusion/ + github-app-token/ were last-child └── despite prepare-pr-base/ following). - Remove the now-orphaned create_pr_target_branch() helper; prepare_pr_base activation and emission share the single create_pr_config() predicate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good overall — the approach is sound, security is solid, and the tests are comprehensive. One minor UX issue worth noting. Findings
|
Summary
Closes #1413.
On an ADO agent pool whose default git fetch is shallow (
fetchDepth: 1), a barecheckoutleaves norefs/remotes/origin/<target>ref. Thecreate-pull-requestdiff base is computed at agent time by the host-side SafeOutputs MCP server (src/mcp.rs::find_merge_base), which only inspects existing local refs and never fetches — so it cannot compute a merge base and the PR can't be opened:The only prior workaround was hand-editing the compiled
.lock.ymlto forcefetchDepth: 0, which then failed the runtime "Verify pipeline integrity" step and forcedado-aw-debug.skip-integrity: true.Approach (gh-aw style)
github/gh-awkeeps checkout shallow and does the heavy git deepening in a credentialed step, not the sandboxed agent. This PR adapts that model to ADO: whenevercreate-pull-requestis configured, the compiler emits a credentialed Agent-job prepare step that runs on the host (before the AWF-wrapped agent, using$(System.AccessToken)) and, for each allowed create-PR repo dir, fetches + progressively deepens that repo's target branch intorefs/remotes/origin/<target>and pointsrefs/remotes/origin/HEADat it.mcp.rs::find_merge_basethen resolves a base with no in-sandbox network, no forced full-history checkout, and no lock hand-edit (integrity check stays on). It reuses the exact fetch/deepen logic already used by the PR execution-context precompute (shared/merge-base.ts::ensureTargetRefFetched). No new front-matter is required for the base case — automatic whencreate-pull-requestis present.Multi-repo coverage
An agent can call
create-pull-requestwithrepository: <alias>for any checked-out repo. The prepare step deepens the target branch in every git dirmcp.rs::resolve_git_dir_for_patchwould read —working_directory(self) +working_directory/<alias>for eachcheckout:alias — so a PR to any allowed repo works on shallow pools. The bundle consumes repeated--repo-dir <dir> --target-branch <branch>pairs with isolated per-dir failures.Per-repo target branches (meta-checkout model)
A meta repo that checks out N repos can open a PR into each repo's own target branch.
create-pull-requestgains (collision-free —target-branchstays a plain literal):target-branch— literal fallback (defaultmain), unchanged.target-branches— optional per-alias literal overrides (highest precedence).infer-target-from-checkout-ref— opt-in bool: a checkout repo with no explicit override targets its ownrepos: ref.selfnever infers.Resolution per repo
R:target-branches[R]→ (if the bool)R's checkout ref →target-branch→main. The same resolver drives the compiler's per-dir deepening and Stage 3's PR creation, so the branch that is fetched/deepened always matches the branch the PR targets.target-branchis a plain literal (never a directive), so no branch name can be misread.Branch semantics: the deepened branch is each repo's destination/base (
target-branch), not the per-reporepos:checkoutref(the source side).Key changes
prepare-pr-baseado-script bundle — loops over(dir, target)pairs, deepening each via the sharedensureTargetRefFetchedhelper (extracted fromresolveMergeBase); per-dir failures isolated.ado_bundle.rs—Bundle::PreparePrBase(BundleAuth::Bearer).ado_script.rs—PREPARE_PR_BASE_PATH,prepare_pr_base_active,prepare_pr_base_step_typed(&[(dir, target)])(double-quoted ADO-macro dir + single-quoted literal branch, shellcheck-clean;SYSTEM_ACCESSTOKENviaapply_bundle_auth).create_pull_request.rs—CreatePrConfiggainstarget_branches+infer_target_from_checkout_ref+resolve_target_branch(); Stage 3 resolves perself.repository.result.rs/main.rs—ExecutionContext.repo_refs(alias → checkout ref), populated at Stage 3 fromfront_matter.repositories.types.rs/agentic_pipeline.rs—create_pr_config()(sanitized) +checkout_repo_refs(); the compiler builds the per-dir(dir, target)list via the shared resolver.mcp.rs— regression test provingfind_merge_baseresolves via theorigin/HEADsymbolic ref.safe-outputs.md,ado-script.md,execution-context.md,AGENTS.md+ site mdx mirrors.Test plan
cargo clippy --all-targets --all-features— clean.cargo test— 2513 unit + all integration suites green (0 failed), incl. the resolver precedence matrix, per-repo-target compile-integration (meta repo, diverse refs), multi-repo coverage,types.rsdefault/drift-guard,mcpsymbolic-ref regression, andbash_lint_tests(shellcheck).npm test— prepare-pr-base + merge-base suites green (155 TS tests); bundle builds to theado-script.ziproot.create-pull-requestcompile with exactly the expected step (self-only ⇒ one--repo-dir/--target-branchpair).