github / github/gh-aw

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)

Open
#41,265 2 comments 1 reaction 2 assignees Claimed by @pelikhan View on GitHub
ai-generated community
Dominant language
Go
Stars
5.1k
Forks
541
Avg merge
5h 48m
Merged PRs (30d)
773

Description

🤖 _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: }]`), 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/` label). A static `base-branch:` or a static `checkout.ref` cannot express a per-run value.
- **#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: ` (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`):

```js
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 symbolic-ref --short refs/remotes/origin/HEAD // → repo default
└─ fallback → gh api repos// --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 ``, then called:

```
create_pull_request { base: "", branch: "", ... }
```

The safe-outputs server logged:

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

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

```bash
git -C checkout -B "" "origin/"
git -C remote set-head origin ""
echo "DEFAULT_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 (`/`, ``, ``, ``). Related: #39407.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.