Skip to content

Cross-repo create_pull_request silently drops the PR (green run) when target-repo is a dynamic expression #47845

Description

@jhamon

Summary

When a create_pull_request (or push_to_pull_request_branch) safe output uses a dynamic target-repo — e.g. target-repo: ${{ inputs.owner }}/${{ inputs.repo }} for a reusable single-repo worker driven by workflow_dispatch inputs — the generated patch file is written by the producer (agent job, inside the firewalled sandbox) under a name containing the un-expanded literal ${GH_AW_INPUT_OWNER}/${GH_AW_INPUT_REPO}, while the consumer (safe_outputs job) looks for a name built from the Actions-expanded value. The names never match, so the consumer reports No patch file found and skips the PR.

Because a missing patch is a warning (correct for genuine no-ops), the whole run concludes green — the agent did and verified all the work, but no PR is ever opened, and nothing surfaces the loss.

Version: compiler v0.83.1 (latest release). Also present on main.

Reproduction

Reusable single-repo worker, dispatched with owner/repo inputs. Frontmatter:

on:
  workflow_dispatch:
    inputs:
      owner: { required: true }
      repo:  { required: true }

safe-outputs:
  create-pull-request:
    target-repo: ${{ inputs.owner }}/${{ inputs.repo }}
    max: 1
  push-to-pull-request-branch:
    target-repo: ${{ inputs.owner }}/${{ inputs.repo }}

The target repo is checked out to ./target; patch_workspace_path: target. The agent edits files under ./target, verifies them, and calls create_pull_request.

Observed

Producer (agent job), ingestion step:

Successfully parsed 1 valid output items → output_types: create_pull_request
Found 2 patch/bundle file(s):
  aw-${gh_aw_input_owner}-${gh_aw_input_repo}-agent-<branch>.bundle
  aw-${gh_aw_input_owner}-${gh_aw_input_repo}-agent-<branch>.patch

The patch file physically on disk is literally named aw-${gh_aw_input_owner}-${gh_aw_input_repo}-… — the template was slugified (lowercased, /-) without being expanded.

Consumer (safe_outputs job), Process Safe Outputs:

Patch file path: (not set)
Apply transport mode: patch (bundle file present: false)
##[warning]No patch file found - cannot create pull request without changes
⏭ Message 1 (create_pull_request) skipped — No patch file found

Run conclusion: success.

Root cause

The single frontmatter value is compiled into two different substitution styles:

Job Where Value in the lock Evaluated by Result
Producer (agent) Generate Safe Outputs Config step, config.json written via a single-quoted heredoc (<< 'DELIM') ${GH_AW_INPUT_OWNER}/${GH_AW_INPUT_REPO} nothing — quoted heredoc disables expansion, and the sandbox process reading the config has neither var set literal string → leaks into patch filename
Consumer (safe_outputs) GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG env ${{ inputs.owner }}/${{ inputs.repo }} GitHub Actions real slug

The producer's patch filename derives from current_checkout_repo, resolved in actions/setup/js/safe_outputs_handlers.cjs (repoSlug = repoResult.repo) from the config's target-repo / current_checkout_repo — still the literal ${GH_AW_INPUT_OWNER}/… string. pkg/workflow/safe_outputs_patch_workspace.go sets current_checkout_repo at compile time by string-comparing target-repo against the checkout's registered repo; both are the same raw expression string, so they match and the literal expression is copied through verbatim.

The two jobs disagree on the patch filename purely because one path expands the value and the other doesn't.

Impact

  • Any centralized "control repo operates on a target repo" pattern (dispatch inputs → dynamic target-repo) silently never produces PRs.
  • add_comment / create_issue / add_labels are unaffected — they route via the consumer config's expanded value and don't depend on a producer-written patch filename.
  • The green run hides it; there's no failed check to alert on.

Suggested fix

Expand ${GH_AW_INPUT_*} when the producer writes config.json, so producer and consumer agree on the resolved slug — e.g. emit the Generate Safe Outputs Config heredoc unquoted (the step already exports GH_AW_INPUT_OWNER / GH_AW_INPUT_REPO), or resolve target-repo / current_checkout_repo to the concrete slug before writing. Alternatively, have the consumer locate the patch via the aw-*.patch glob it already uses for threat-detection prep, rather than reconstructing an exact filename independently.

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions