Repository navigation
SideRepoOps: API for a user step to resolve the target branch at runtime, then check it out and use it as the patch base (dynamic base; not covered by #39407) #41265
Description
Activity
- locked and limited conversation to collaborators
on Jun 24, 2026 - unlocked this conversation
on Jun 24, 2026 🤖 This comment has been generated by Claude Code.
Having now built a downstream workaround for this, I want to propose what I think is the cleanest preferred solution — and flag one implementation subtlety so it doesn't get lost, because it's the difference between "works" and "looks like it works."
Preferred solution
Let
checkout.refreference the output of apre-step(i.e. accept a runtime${{ steps.* }}expression), and have gh-aw treat the checked-out ref as the base forcreate_pull_request/push_to_pull_request_branch.From the user's perspective this is the only knob they touch: resolve the branch in a pre-step (e.g. from an issue label, a dispatch input, an API lookup), point
checkout.refat that output, and the PR/patch is naturally based on the branch they checked out. That's the intuitive contract — the PR is based on what I checked out — with nobase-branchduplication, no manifest fiddling, and no post-checkout re-anchoring. It also covers the dynamic / per-run case that a staticbase-branch:or a staticcheckout.refcan't express.The subtlety: "check out the ref" is not automatically enough today
The patch base is currently derived from the checkout manifest's
default_branch, whichbuild_checkout_manifest.cjsresolves as:git -C <path> symbolic-ref --short refs/remotes/origin/HEAD // primary └─ fallback → gh api repos/<owner>/<repo> --jq .default_branchactions/checkoutdoes not setrefs/remotes/origin/HEAD, even when given an explicitref. I verified this by simulating the exact checkout flow gh-aw emits (init +git fetch origin <ref>+ checkout + the "Fetch additional refs"git fetch origin '+refs/heads/*:refs/remotes/origin/*'):current branch: <release-branch> git symbolic-ref refs/remotes/origin/HEAD => fatal: ref refs/remotes/origin/HEAD is not a symbolic ref (NOT set)So the resolver falls through to the GitHub API default branch, and the patch is computed as
diff(origin/<default-branch>, HEAD)regardless of which ref was checked out — pulling in the entire<default-branch>→<release-branch>divergence. In other words, wiringcheckout.refto a pre-step output alone would put the working tree on the right branch but still base the patch on the default branch.What the implementation needs
For the preferred solution to "just work," gh-aw must derive the base from the checked-out ref rather than
origin/HEAD/ the API. Concretely, the checkout-manifest step (or the checkout step) could record the actually-checked-out branch — e.g.git -C <path> rev-parse --abbrev-ref HEAD, or rungit remote set-head origin <ref>so the resolver's primary lookup succeeds. Combined withcheckout.refaccepting a pre-step output, the entire user-facing surface collapses to a single, intuitive field.This is the same "base should track the checked-out ref" point from the original report; the pre-step-output
refis what makes it usable when the branch isn't known at compile time.Redaction note: placeholders as before (
<owner>/<side-repo>,<release-branch>,<default-branch>).- locked and limited conversation to collaborators
on Jun 24, 2026 - unlocked this conversation
on Jun 24, 2026 - linked a pull request that will close this issue[WIP] Add API for resolving target branch at runtime #44476
on Jul 9, 2026 Aspire now has a concrete v0.86.2 run showing the remaining cross-job half of this dynamic-base problem: https://github.com/microsoft/aspire/actions/runs/32112079288
We use an agent-job workaround that resolves the docs target at runtime and configures:
safe-outputs: create-pull-request: base-branch: ${{ steps.resolve-target.outputs.branch || 'main' }}
For this run, the resolver selected
release/13.5. That was sufficient for agent-time patch generation: gh-aw produced a valid three-file patch/bundle whose base is exactlyaa2777825624f037a77160728939e36f7c788eff(release/13.5).The preserved RPC trace is important: the model's
create_pull_requestMCP request contained onlytitleandbody. The safeoutputs server config hadcreate_pull_request.base_branch = release/13.5, and gh-aw itself enriched the rawsafeoutputs.jsonlitem with:{ "head_repo": "microsoft/aspire.dev", "base_branch": "release/13.5", "base_commit": "aa2777825624f037a77160728939e36f7c788eff", "branch": "docs/pr-17235-32112079288-1" }However, canonical
/tmp/gh-aw/agent_output.jsonretained onlytype,body,branch, andtitlefor the PR item. The resolved base metadata was not transported into the separate safe-output application job. In that job the resolver step does not exist, so the same expression falls back tomain; without a supported cross-job runtime-base value, apply-time checkout cannot reliably select the branch used to generate the patch.Our fail-closed compatibility step rejected the missing canonical target instead of applying the release-based bundle to
main, so no incorrect PR was created. We can make the repository workaround parse and validate the server-enriched raw artifact, but that is relying on internal artifact shape.This run therefore demonstrates that a step-output
base-branchexpression can fix patch generation while the runtime-resolved branch still lacks a supported path into the safe-output job. A first-class mechanism that carries one resolved value through checkout, patch generation, and application would remove this fragile split.- locked and limited conversation to collaborators
on Aug 18, 2026 - unlocked this conversation
on Aug 18, 2026 Status (2026-10-07; Contributor proposal): The August 18 Aspire report says a runtime-resolved
release/13.5base produced the intended agent-time patch, but the resolved base metadata did not reach the separate safe-output application job. The proposed next step is to carry that base through collection and application, then test that the resulting PR targets the same branch used for patch generation.
🤖 This issue was investigated and filed by Claude Code.
Summary
In a SideRepoOps workflow, the branch of the target/side repository that a given run should operate on is sometimes only knowable at runtime — e.g. it is read from a label on the triggering issue, a dispatch input, or an API lookup — and is not fixed in the workflow YAML. Today there is no supported way to:
create_pull_request/push_to_pull_request_branchpatch.Because nothing carries a runtime-resolved branch into patch generation, the patch is computed against the target repo's default branch. The merge-base lies far behind the intended branch, so the diff drags in the entire default→release divergence — blowing past
max-patch-filesand failing PR creation.Request: an API / extension point for a runtime-resolved base branch in the SideRepoOps pattern.
How this differs from #39407
#39407 covers the static case: a checkout that pins a non-default
refdirectly in YAML (checkout: [{ repository, ref: <release-branch> }]), asking that the checked-out ref be used as the base; its suggested workaround is a staticsafe-outputs.create-pull-request.base-branch:.This issue is the dynamic case, which neither that workaround nor the fix proposed in #39407 covers:
branch/<name>label). A staticbase-branch:or a staticcheckout.refcannot express a per-run value.HEAD) is insufficient here. The only place a user step can re-anchor the checkout is after the built-in "Build checkout manifest for safe-outputs handlers" step (customsteps:are emitted after the checkout block). So at manifest timeHEADis still the default branch, and recordingHEADwould record the default branch.baseargument is ignored by patch generation. When the agent callscreate_pull_requestwithbase: <release-branch>(permitted viaallowed-base-branches), that value is honored for the eventual PR API target but is not used to compute the patch base — patch generation prefers the checkout manifest'sdefault_branch.In short: there is currently no mechanism — static config, static checkout
ref, or per-callbaseargument — that lets a SideRepoOps run target a runtime-resolved branch and have the patch computed against it.Where the base comes from (for reference)
Base resolution for
create_pull_request(per #39407's citation ofsafe_outputs_handlers.cjs):The manifest's
default_branch(build_checkout_manifest.cjs) is resolved as:Neither path consults the per-call
baseargument, theDEFAULT_BRANCHenv var, or any runtime-resolved value. ① is the only override, and it is static.Observed failure (redacted)
A SideRepoOps run (gh-aw
v0.80.9) in which the agent committed a small change (5 tracked files) on a feature branch derived from<release-branch>, then called:The safe-outputs server logged:
i.e. the patch was computed as
diff(origin/<default-branch>, HEAD)despitebase: <release-branch>in the call. The resulting patch contained thousands of files (generated.gen.go/.pb.*artifacts produced by the default↔release divergence — none of them touched by the agent), exceedingmax-patch-files, so PR creation was rejected and fell back to a review issue.Current workaround and why it silently fails
A custom step placed after the manifest step re-anchors the checkout and tries to override the base:
This no longer works: the safe-outputs patch generator reads the checkout manifest (built earlier, recording the default branch), which takes precedence over the
DEFAULT_BRANCHenv override. The manifest is frozen before the re-anchor runs, so the patch base reverts to the default branch — with no error, just a huge patch.Requested API
A supported way to inject a runtime base-branch resolution into the SideRepoOps lifecycle. Sketch of the contract:
default_branch/ patch base forcreate_pull_requestandpush_to_pull_request_branch.Possible shapes (any one would suffice):
safe-outputs.create-pull-request.base-branch-from:pointing at a step output / env var that gh-aw reads at runtime, and which also drives the target-repo checkoutref.pre-checkout/resolve-baseextension point that runs before the cross-repo checkout + manifest, whose output sets both the checkoutrefand the manifestdefault_branch.baseargument (and/or the existingDEFAULT_BRANCHenv override) so a custom step that has already resolved the branch can set it.Options 1 and 2 are preferable, because they derive the checkout
refand the patch base from a single user-resolved value, eliminating the drift that comes from specifying the branch in two places.Redaction note
Private repository names, branch names, issue numbers, and run URLs are replaced with placeholders (
<owner>/<side-repo>,<release-branch>,<default-branch>,<feature-branch>). Related: #39407.