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:
- Add an optional
base field to the agent-side create_pull_request tool schema (next to branch, title, body, draft, labels, repo), or
- 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
- Agent emits
base in the safe output — rejected (field not in schema).
- 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.
- 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).
- 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.
Summary
Allow the target base branch of a PR created by the
create_pull_requestsafe 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-timebase-branchstring in workflow frontmatter.Concretely, either:
basefield to the agent-sidecreate_pull_requesttool schema (next tobranch,title,body,draft,labels,repo), orbase-branchfrom a value the agent job (or asafe-outputs.stepspre-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-checkagentic workflow inmicrosoft/aspirethat fires when a PR merges and opens a draft documentation PR in a separate repo (microsoft/aspire.dev). Aspire ships release versions fromrelease/X.Ybranches, whileaspire.dev'smaintracks the latest unreleased bits. So a docs PR must usually target a release branch onaspire.dev, notmain.The correct target branch is driven by the source PR's milestone (or the milestone of the issue it closes). For example:
main13.3release/13.3main13.2.1release/13.2.1main13.3 - Preview 1release/13.3release/13.3release/13.3mainmainThis requires:
^v?(\d+)\.(\d+)(?:\.(\d+))?(both13.3and13.2.1forms, with optional prefixes/suffixes).release/X.Y[.Z]branch already exists on the target repo and creating it frommainif not.main).None of that is expressible in a GitHub Actions expression (no regex, no API calls from
${{ }}), sobase-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.godefinesBaseBranch 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.create_pull_requestaccepts onlybody,branch,draft,labels,repo,title— see the validation block in compiled lock files (pr-docs-check.lock.ymllines ~509–544 in our case). Nobasefield. If the agent emits one it's silently dropped.pkg/workflow/compiler_safe_outputs_steps.gofalls back to${{ github.base_ref || github.event.pull_request.base.ref || github.ref_name || github.event.repository.default_branch }}whenbase-branchis unset. That covers the "aspire PR merged into a release branch" row of our table but not the "merged intomainwith a release milestone" rows, which are the majority in our repo.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
basein the safe output — rejected (field not in schema).base-branchexpression — shipped as an interim step. Works for PRs already onrelease/*, does nothing for milestone-driven cases. See the interim commit.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).$RUNNER_TEMP/gh-aw/safeoutputs/config.jsonfrom 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
baseto thecreate_pull_requesttool schema. When present, the handler uses it; otherwise fall back to the workflow-levelbase-branch. The agent is already trusted to pickbranch,title,body— pickingbaseis the same class of decision and is already bounded bytarget-repoand token scope.Option B — Runtime input from the agent job (more constrained):
Allow
base-branchat the workflow level to referenceneeds.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_requesthandler:Document/support a mechanism where a step in
safe-outputs.stepscan export a value consumed by the handler'sbase_branch. Could be as simple as: "if file$RUNNER_TEMP/gh-aw/safeoutputs/base_branch.txtexists when the handler runs, its contents override the compiledbase-branch."Any of the three unblocks milestone/issue/policy-driven base selection without sacrificing the safe-outputs sandbox guarantees.
Security considerations
target-repoand the app-token scopes. A bad base branch string just means the PR can't be created (invalid ref) — not a privilege escalation.basewith a regex (e.g.^[A-Za-z0-9._/-]+$) to match existing field validation patterns.Environment
ubuntu-latestvia GitHub-hosted runnersHappy to help prototype / review / test. Thanks for gh-aw — it's been a great tool to build on.