Skip to content

Feature request: dynamic/per-run base branch for create_pull_request safe output #26908

@IEvangelist

Description

@IEvangelist

Summary

Allow the target base branch of a PR created by the create_pull_request safe output to be resolved per workflow run, driven by data the agent (or a pre-handler step) computes at runtime — not only by the compile-time base-branch string in workflow frontmatter.

Concretely, either:

  1. Add an optional base field to the agent-side create_pull_request tool schema (next to branch, title, body, draft, labels, repo), or
  2. Officially support reading base-branch from a value the agent job (or a safe-outputs.steps pre-step) writes — e.g. needs.agent.outputs.<name>, a file under $RUNNER_TEMP/gh-aw/safeoutputs/, or a step output — so it can be computed from data not reducible to a GitHub Actions expression.

Happy to submit a PR if the maintainers agree on the shape.

Motivation / Use case

We're building a pr-docs-check agentic workflow in microsoft/aspire that fires when a PR merges and opens a draft documentation PR in a separate repo (microsoft/aspire.dev). Aspire ships release versions from release/X.Y branches, while aspire.dev's main tracks the latest unreleased bits. So a docs PR must usually target a release branch on aspire.dev, not main.

The correct target branch is driven by the source PR's milestone (or the milestone of the issue it closes). For example:

Source aspire PR Milestone Desired aspire.dev base
Merged into main 13.3 release/13.3
Merged into main 13.2.1 release/13.2.1
Merged into main 13.3 - Preview 1 release/13.3
Merged into release/13.3 (any) release/13.3
Merged into main (no milestone) main

This requires:

  • Normalizing milestone titles with a regex like ^v?(\d+)\.(\d+)(?:\.(\d+))? (both 13.3 and 13.2.1 forms, with optional prefixes/suffixes).
  • Checking whether the resulting release/X.Y[.Z] branch already exists on the target repo and creating it from main if not.
  • Falling back through a priority chain (PR milestone → linked-issue milestone → source PR base → main).

None of that is expressible in a GitHub Actions expression (no regex, no API calls from ${{ }}), so base-branch: "${{ ... }}" — the only dynamic hook today — can't do the job.

Current limitation

From reading the source (v0.67.2 / v0.68.3):

  • pkg/workflow/create_pull_request.go defines BaseBranch string \yaml:"base-branch,omitempty"`as a **workflow-level** config. It's compiled into the safe-outputs handler's static config JSON and into theref:` of the auto-injected "Checkout repository" step.
  • The agent-side tool schema for create_pull_request accepts only body, branch, draft, labels, repo, title — see the validation block in compiled lock files (pr-docs-check.lock.yml lines ~509–544 in our case). No base field. If the agent emits one it's silently dropped.
  • pkg/workflow/compiler_safe_outputs_steps.go falls back to ${{ github.base_ref || github.event.pull_request.base.ref || github.ref_name || github.event.repository.default_branch }} when base-branch is unset. That covers the "aspire PR merged into a release branch" row of our table but not the "merged into main with a release milestone" rows, which are the majority in our repo.
  • There is a comment in the gh-aw source acknowledging that per-PR base selection is a future direction, not currently supported.

A Copilot reviewer on our draft workflow PR correctly flagged that instructing the agent to emit base: is invalid — which is what sent us down this path.

What we've tried / considered

  1. Agent emits base in the safe output — rejected (field not in schema).
  2. Compile-time base-branch expression — shipped as an interim step. Works for PRs already on release/*, does nothing for milestone-driven cases. See the interim commit.
  3. Post-handler gh pr edit --base <branch> retarget — fallback we may implement. Works but means the PR exists briefly on the wrong base, so notifications/CI fire twice, and it relies on the handler surfacing the created PR number to later steps (currently not a first-class output as far as we can tell).
  4. Rewriting $RUNNER_TEMP/gh-aw/safeoutputs/config.json from a pre-step — would work in principle but feels hacky and brittle to internal format changes.

Proposed solution (sketch)

Option A — Agent-side field (most flexible):

Add base to the create_pull_request tool schema. When present, the handler uses it; otherwise fall back to the workflow-level base-branch. The agent is already trusted to pick branch, title, body — picking base is the same class of decision and is already bounded by target-repo and token scope.

Option B — Runtime input from the agent job (more constrained):

Allow base-branch at the workflow level to reference needs.agent.outputs.<name> or read from a well-known file written by the agent or a pre-step. Semantically equivalent to Option A for our case but keeps the decision outside the tool schema.

Option C — Pre-step hook before the create_pull_request handler:

Document/support a mechanism where a step in safe-outputs.steps can export a value consumed by the handler's base_branch. Could be as simple as: "if file $RUNNER_TEMP/gh-aw/safeoutputs/base_branch.txt exists when the handler runs, its contents override the compiled base-branch."

Any of the three unblocks milestone/issue/policy-driven base selection without sacrificing the safe-outputs sandbox guarantees.

Security considerations

  • Whatever the value source, the target repo is still bounded by target-repo and the app-token scopes. A bad base branch string just means the PR can't be created (invalid ref) — not a privilege escalation.
  • For Option A, the schema can optionally constrain base with a regex (e.g. ^[A-Za-z0-9._/-]+$) to match existing field validation patterns.

Environment

  • gh-aw versions tested: v0.67.2 and v0.68.3
  • Runner: ubuntu-latest via GitHub-hosted runners
  • Auth: GitHub App token (org requirement) — this constraint doesn't change regardless of which option is chosen.

Happy to help prototype / review / test. Thanks for gh-aw — it's been a great tool to build on.

Metadata

Metadata

Labels

Type

No type
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