Skip to content

Make push_to_pull_request_branch cross-repo #21306

@Corb3nik

Description

@Corb3nik

Summary

Today the docs state that Push to PR Branch is "same-repo only" (safe-outputs.md line 41). The handler already supports target-repo and allowed-repos in config and resolves the target repo for the GitHub API; the missing piece is patch generation and conclusion git operations both assume a single workspace root. When the workflow checks out the target repo under a path (e.g. ./proxy-frontend for caido/proxy-frontend), patch generation runs in the workspace root and finds no commits, and the conclusion job runs git fetch/checkout/apply in the wrong directory. This plan makes push_to_pull_request_branch work end-to-end for cross-repo and multi-checkout.


Analysis

Current behavior

  • Config: compiler_types.go and safe_outputs_config_generation.go already pass target-repo (and allowed-repos) into the handler config. cross-repository.md already shows push-to-pull-request-branch: target-repo: "org/target-repo".
  • MCP handler (safe_outputs_handlers.cjs pushToPullRequestBranchHandler): Resolves target repo via resolveAndValidateRepo(entry, defaultTargetRepo, allowedRepos) and gets baseBranch for that repo, but then calls getCurrentBranch() with no args and generateGitPatch(entry.branch, baseBranch, pushPatchOptions) without cwd or repoSlug. So patch generation always runs in GITHUB_WORKSPACE / process.cwd() (e.g. ai-ops root). If the agent committed in ./proxy-frontend, that repo’s branch/commits are invisible there → "No commits were found to push."
  • Conclusion handler (push_to_pull_request_branch.cjs): Uses resolveAndValidateRepo and runs git fetch, git checkout, git apply via exec.exec with no cwd. So git runs in the job’s default cwd (workspace root), not in the target repo’s checkout.

Existing pattern to reuse

  • create_pull_request in safe_outputs_handlers.cjs: When entry.repo is set, it calls findRepoCheckout(repoSlug), uses repoCwd = checkoutResult.path, passes getCurrentBranch(repoCwd) and patchOptions.cwd / patchOptions.repoSlug into generateGitPatch. So patch is generated from the correct repo directory.
  • generate_git_patch.cjs already supports options.cwd and options.repoSlug (lines 91–95).
  • get_current_branch.cjs accepts customCwd.
  • find_repo_checkout.cjs returns { success, path, repoSlug } for a given owner/repo slug.

Gap

  1. MCP handler: For push_to_pull_request_branch, after resolving repoResult (so we have itemRepo), we never call findRepoCheckout(itemRepo). We should: if we have a resolved target repo, find its checkout path; use that for getCurrentBranch(cwd) and for generateGitPatch(..., { cwd, repoSlug, ... }). If target repo is configured but not found in workspace, return a clear error (e.g. "Repository 'org/repo' not found in workspace. Check out the target repo with a path in checkout.").
  2. Conclusion handler: Before running any git commands, resolve the target repo (already done) then resolve the path to that repo’s checkout (e.g. via findRepoCheckout(itemRepo)). Run all git operations (fetch, checkout, apply, push) with that directory as the working directory (e.g. exec.exec(..., { cwd: repoPath }) or a single process.chdir(repoPath) at the start of the push logic for that message).
  3. Docs: Remove "same-repo only" for push-to-pull-request-branch and document cross-repo (target-repo, allowed-repos, checkout path requirement).
  4. Tool schema (optional): Add optional repo parameter to safe_outputs_tools.json for push_to_pull_request_branch so when allowed-repos has multiple repos the agent can specify which one; align with create_pull_request. If scope is kept minimal, this can be a follow-up.

Implementation plan

1. MCP handler – patch generation in target repo directory

File: actions/setup/js/safe_outputs_handlers.cjs

  • In pushToPullRequestBranchHandler, after resolveAndValidateRepo and getBaseBranch(repoParts):
    • Set itemRepo = repoResult.repo (the resolved slug, e.g. caido/proxy-frontend).
    • Call findRepoCheckout(itemRepo, undefined, { allowedRepos }). If !checkoutResult.success, return a JSON error with a clear message that the target repo must be checked out with a path (point to checkout path docs or cross-repository.md).
    • Set repoCwd = checkoutResult.path, repoSlug = itemRepo.
  • When branch is not provided or equals base branch: call getCurrentBranch(repoCwd) instead of getCurrentBranch().
  • When building pushPatchOptions: if repoCwd is set, add pushPatchOptions.cwd = repoCwd and pushPatchOptions.repoSlug = repoSlug.
  • Pass pushPatchOptions into generateGitPatch(entry.branch, baseBranch, pushPatchOptions) (already done; only the contents of pushPatchOptions change).

This mirrors the create_pull_request branch/patch flow (lines 253–331) for push_to_pull_request_branch.

2. Conclusion handler – run git in target repo directory

File: actions/setup/js/push_to_pull_request_branch.cjs

  • Require findRepoCheckout from ./find_repo_checkout.cjs.
  • After resolving the target repo (repoResult / itemRepo) and before running any git commands (fetch, checkout, apply, push), resolve the checkout path:
    • If itemRepo differs from process.env.GITHUB_REPOSITORY (or whenever defaultTargetRepo is set), call findRepoCheckout(itemRepo, process.env.GITHUB_WORKSPACE, { allowedRepos }). If not found, return { success: false, error: "..." } with a message that the target repo must be checked out with a path.
    • Set repoCwd = checkoutResult.path.
  • Run all git operations for this message in repoCwd: use exec.exec("git", [...], { cwd: repoCwd, env: { ...process.env, ...gitAuthEnv } }) (and equivalent for any other git calls) so that fetch, checkout, apply, and push run in the target repo. Alternatively, process.chdir(repoCwd) at the start of the push block and restore cwd in a finally block; prefer explicit cwd in exec to avoid global state.
  • Ensure getGitAuthEnv is still applied for fetch/push in the target repo.

3. Tests

Files: actions/setup/js/safe_outputs_handlers.cjs (or a dedicated test file if handlers are tested elsewhere), actions/setup/js/push_to_pull_request_branch.test.cjs

  • Add or extend tests for push_to_pull_request_branch when target-repo is set and the workspace contains the target repo at a subdirectory: mock or fixture with two checkouts (e.g. root = workflow repo, subdir = target repo); assert patch generation is called with cwd and repoSlug pointing at the target repo.
  • Add a test for conclusion handler: when target repo is different from GITHUB_REPOSITORY, assert git commands are run with cwd set to the target repo path (or that findRepoCheckout is invoked and its path is used).
  • Add a test for error path: target-repo configured but that repo is not found in workspace; assert a clear error is returned (MCP handler and, if feasible, conclusion handler).

4. Documentation

File: docs/src/content/docs/reference/safe-outputs.md

  • Remove "same-repo only" from the Push to PR Branch bullet (line 41). Replace with wording that cross-repo is supported when target-repo is set and the target repository is checked out (e.g. with a path in checkout).

File: docs/src/content/docs/reference/safe-outputs-pull-requests.md (and cross-repository.md if needed)

  • In the Push to PR Branch section, add a short "Cross-repo" subsection: when using target-repo (and optionally allowed-repos), the target repository must be checked out in the workspace (e.g. checkout: - repository: org/target-repo; path: ./target-repo). Link to cross-repository.md and the existing example that shows push-to-pull-request-branch: target-repo: "org/target-repo".

5. Go / validation (optional)

  • pkg/workflow/safe_outputs_validation_config.go: push_to_pull_request_branch currently has "branch": {Required: true}. The tool schema treats branch as optional (defaulting to current branch). If the implementing agent finds that Required: true causes validation failures when branch is omitted in cross-repo flows, relax to optional or align with create_pull_request.
  • No change to compiler or config generation is strictly required for the above; target-repo and allowed-repos are already passed through.

6. Tool schema (optional follow-up)

  • In pkg/workflow/js/safe_outputs_tools.json, add an optional repo property to the push_to_pull_request_branch tool (type string, description: target repository in owner/repo format; when omitted, use the configured target repository; must be in allowed-repos if specified). This allows the agent to disambiguate when multiple repos are allowed. Implement only if the first iteration supports multiple allowed-repos and the agent needs to pass repo explicitly.

Guidelines for the implementing agent

  • Follow scratchpad/code-organization.md and scratchpad/testing.md.
  • Use existing helpers: findRepoCheckout, getCurrentBranch(customCwd), generateGitPatch(..., { cwd, repoSlug }), resolveAndValidateRepo, resolveTargetRepoConfig.
  • Error messages: use the template [what's wrong]. [what's expected]. [example] per CONTRIBUTING.md.
  • Run make agent-finish before completing (build, test, recompile, format, lint).
  • After implementation, a workflow that runs in repo A with checkout: - repository: B; path: ./B and push-to-pull-request-branch: target-repo: "B" should generate the patch from ./B and apply/push in ./B, so commits made in ./B are pushed correctly.

Out of scope (for this issue)

  • Adding an optional repo argument to the tool (can be a follow-up issue).
  • Changing how the activation job checks out repos (no change to checkout_manager or compiler_activation_job for this issue).
  • Fork PR handling (unchanged).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions