Skip to content

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

@yskopets

🤖 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:

  1. let a user-defined step resolve that branch for the current run, and
  2. have gh-aw check out the target repo at that branch and use it as the base when generating the create_pull_request / push_to_pull_request_branch patch.

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-files and 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 ref directly in YAML (checkout: [{ repository, ref: <release-branch> }]), asking that the checked-out ref be used as the base; its suggested workaround is a static safe-outputs.create-pull-request.base-branch:.

This issue is the dynamic case, which neither that workaround nor the fix proposed in #39407 covers:

  • The branch is not known at compile time. It is computed per-run by a user step (e.g. from an issue's branch/<name> label). A static base-branch: or a static checkout.ref cannot express a per-run value.
  • SideRepoOps: create_pull_request bases PR on repo default branch, not the checked-out ref (breaks non-default/release-branch checkouts) #39407's proposed fix (have the manifest record the checked-out 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 (custom steps: are emitted after the checkout block). So at manifest time HEAD is still the default branch, and recording HEAD would record the default branch.
  • The per-call base argument is ignored by patch generation. When the agent calls create_pull_request with base: <release-branch> (permitted via allowed-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's default_branch.

In short: there is currently no mechanism — static config, static checkout ref, or per-call base argument — 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 of safe_outputs_handlers.cjs):

if (prConfig.base_branch) baseBranch = prConfig.base_branch;            // ① static config
else if (manifestEntry && manifestEntry.default_branch) baseBranch = ; // ② checkout manifest
else baseBranch = await getBaseBranch(...);                             // ③ repos.get().default_branch

The manifest's default_branch (build_checkout_manifest.cjs) is resolved as:

git -C <path> symbolic-ref --short refs/remotes/origin/HEAD       // → repo default
  └─ fallback → gh api repos/<owner>/<repo> --jq .default_branch  // → repo default

Neither path consults the per-call base argument, the DEFAULT_BRANCH env 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:

create_pull_request { base: "<release-branch>", branch: "<feature-branch>", ... }

The safe-outputs server logged:

[safeoutputs] Using checkout-manifest default_branch for <owner>/<side-repo>: <default-branch>
[safeoutputs] Generating patch ... baseBranch: <default-branch>
[generate_git_patch] Starting patch generation: mode=full, branch=<feature-branch>, defaultBranch=<default-branch>
[generate_git_patch] Strategy 1 (full): Computing merge-base with <default-branch>
[generate_git_patch] Strategy 1: Found 455 commits between <merge-base> and <tip>
[generate_git_patch] Final: SUCCESS - patchSize≈66 MB, patchLines≈1.4M

i.e. the patch was computed as diff(origin/<default-branch>, HEAD) despite base: <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), exceeding max-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:

git -C <side-repo> checkout -B "<branch>" "origin/<branch>"
git -C <side-repo> remote set-head origin "<branch>"
echo "DEFAULT_BRANCH=<branch>" >> "$GITHUB_ENV"

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_BRANCH env 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:

  • A documented hook/step that runs before the cross-repo checkout + manifest, in which a user step resolves the target branch for this run (e.g. writes it to a known step output / env var / file).
  • gh-aw then (a) checks out the target repo at that branch, and (b) records it as the manifest default_branch / patch base for create_pull_request and push_to_pull_request_branch.

Possible shapes (any one would suffice):

  1. A frontmatter field such as 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 checkout ref.
  2. A pre-checkout / resolve-base extension point that runs before the cross-repo checkout + manifest, whose output sets both the checkout ref and the manifest default_branch.
  3. Make patch generation honor the per-call base argument (and/or the existing DEFAULT_BRANCH env 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 ref and 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.

Metadata

Metadata

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions