Skip to content

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

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions