Skip to content

Add fork-backed safe-output pull request support#45909

Open
pelikhan with Copilot wants to merge 22 commits into
mainfrom
copilot/support-fork-backed-prs
Open

Add fork-backed safe-output pull request support#45909
pelikhan with Copilot wants to merge 22 commits into
mainfrom
copilot/support-fork-backed-prs

Conversation

Copilot AI commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Safe outputs could only create and update PRs where the pushed branch lived in the same repository as the PR target. This change adds first-class support for upstream PRs whose head branch is pushed to an explicitly configured automation-owned fork, while keeping arbitrary contributor forks blocked.

  • Config surface

    • Add head-repo and head-github-token to:
      • safe-outputs.create-pull-request
      • safe-outputs.push-to-pull-request-branch
    • Wire the new fields through workflow parsing, schema validation, handler config generation, and JS type definitions.
  • Create PR against upstream, push branch to fork

    • Keep target-repo as the upstream/base repository.
    • Resolve push operations against head-repo when configured.
    • Create PRs with an owner-qualified head (owner:branch) when the head repo differs from the target repo.
    • Use the head-repo token for fork branch operations without requiring upstream contents: write.
  • Follow-up updates for fork-backed PRs

    • Relax the blanket fork-PR rejection in push-to-pull-request-branch.
    • Allow updates only when the PR’s actual head.repo.full_name exactly matches the configured head-repo.
    • Continue rejecting writes to unconfigured contributor forks.
  • Git/auth behavior

    • Extend signed/direct push helpers to support an explicit push remote URL and optional fork-specific token.
    • Preserve same-repo behavior, but switch branch existence checks, fetches, pushes, and result URLs to the fork repo when applicable.
  • Manifest and result metadata

    • Include head_repo in emitted safe-output entries and returned results so downstream summaries/manifests can distinguish upstream target vs fork head.
  • Docs/spec

    • Update the safe outputs specification and pull request reference docs to describe fork-backed PR configuration, credential split, and automation-fork-only update semantics.

Example configuration:

checkout:
  - repository: microsoft/vscode
    ref: main
    current: true

safe-outputs:
  create-pull-request:
    target-repo: microsoft/vscode
    head-repo: automation-owner/vscode
    base-branch: main
    allowed-repos:
      - microsoft/vscode
      - automation-owner/vscode
    head-github-token: ${{ secrets.AUTOMATION_FORK_TOKEN }}

  push-to-pull-request-branch:
    target-repo: microsoft/vscode
    head-repo: automation-owner/vscode
    allowed-repos:
      - microsoft/vscode
      - automation-owner/vscode
    head-github-token: ${{ secrets.AUTOMATION_FORK_TOKEN }}

Copilot AI and others added 2 commits July 16, 2026 05:13
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Add support for fork-backed pull requests in safe outputs Add fork-backed safe-output pull request support Jul 16, 2026
Copilot AI requested a review from pelikhan July 16, 2026 05:40
@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot merge main and recompile

Copilot AI and others added 2 commits July 16, 2026 06:12
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…backed-prs

# Conflicts:
#	.github/workflows/avenger.lock.yml
#	.github/workflows/hourly-ci-cleaner.lock.yml
#	.github/workflows/skillet.lock.yml

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

@copilot merge main and recompile

Done in ffcfbcb. I merged main, recompiled the workflows, and pushed the updated branch.

@pelikhan pelikhan marked this pull request as ready for review July 16, 2026 06:34
Copilot AI review requested due to automatic review settings July 16, 2026 06:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds automation-fork support for safe-output pull request creation and updates.

Changes:

  • Adds fork head repository and credential configuration.
  • Routes branch operations and metadata toward configured forks.
  • Updates schemas, types, tests, and documentation.
Show a summary per file
File Description
pkg/workflow/safe_outputs_handler_registry.go Emits fork configuration.
pkg/workflow/safe_outputs_handler_manager_token_test.go Tests token precedence.
pkg/workflow/push_to_pull_request_branch.go Parses fork update settings.
pkg/workflow/github_token.go Resolves checkout credentials.
pkg/workflow/create_pull_request.go Defines fork creation settings.
pkg/workflow/compiler_safe_outputs_config_test.go Tests configuration pass-through.
pkg/parser/schemas/main_workflow_schema.json Adds schema fields.
docs/src/content/docs/specs/safe-outputs-specification.md Specifies fork behavior.
docs/src/content/docs/reference/safe-outputs-pull-requests.md Documents configuration and credentials.
actions/setup/js/types/safe-outputs-config.d.ts Adds TypeScript declarations.
actions/setup/js/safe_outputs_handlers.cjs Adds head repository metadata.
actions/setup/js/push_to_pull_request_branch.test.cjs Tests configured-fork updates.
actions/setup/js/push_to_pull_request_branch.cjs Implements fork branch updates.
actions/setup/js/push_signed_commits.cjs Supports explicit push remotes.
actions/setup/js/create_pull_request.test.cjs Tests owner-qualified heads.
actions/setup/js/create_pull_request.cjs Implements fork-backed PR creation.
actions/setup/js/create_pull_request_helpers.cjs Supports qualified compare heads.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 16/17 changed files
  • Comments generated: 13
  • Review effort level: Medium

Comment thread pkg/workflow/github_token.go Outdated

// Per-config PAT tokens take highest precedence (overrides GitHub App)
perConfigToken := createPRToken
perConfigToken := createPRHeadToken
* @returns {Promise<string | undefined>} SHA of the commit that landed on the target branch
*/
async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, cwd, gitAuthEnv, signedCommits = true, allowGitPushFallback = true, resolvedTemporaryIds, currentRepo, validationConfig }) {
async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, cwd, gitAuthEnv, pushRemoteUrl, pushToken, signedCommits = true, allowGitPushFallback = true, resolvedTemporaryIds, currentRepo, validationConfig }) {

// SECURITY: Check if this is a fork PR - only explicitly configured automation-owned
// forks are eligible for updates.
const { isFork, reason: forkReason } = detectForkPR(pullRequest);
let previousExtraheaders = [];
let overrideApplied = false;
try {
previousExtraheaders = await overridePersistedExtraheader(githubServerUrl, token);
Comment thread actions/setup/js/safe_outputs_handlers.cjs Outdated
Comment thread docs/src/content/docs/reference/safe-outputs-pull-requests.md Outdated
8. **Distinct Upstream and Head Repositories**: `target-repo` identifies the upstream repository that receives the pull request and owns the base branch. `head-repo`, when configured, identifies the repository that receives the pushed branch. When `head-repo` is omitted, the head repository defaults to `target-repo`.
9. **Owner-Qualified Head Reference**: When `head-repo` differs from `target-repo`, the created pull request MUST use an owner-qualified head reference identifying the head repository owner and pushed branch. Unqualified same-name branch references MUST NOT be used in fork-backed mode.
10. **Ephemeral Fork Branch Model**: When `head-repo` differs from `target-repo`, implementations SHOULD create or refresh an ephemeral branch in `head-repo` from the resolved upstream base SHA, apply the agent changes, and open the pull request back to the upstream base. Implementations MAY support explicit synchronization of that ephemeral branch with a newer upstream base, but implicit reuse of arbitrary pre-existing fork branches MUST NOT occur.
11. **Summary and Manifest Provenance**: Successful executions MUST record `upstream_repo`, `head_repo`, `base_sha`, `pushed_head_sha`, and `credential_role` in the safe-output summary and machine-readable manifest.
- When `safe-outputs.push-to-pull-request-branch.target` is `"*"`, requests MUST include `pull_request_number`.
- The handler MUST refuse pushes unless the resolved pull request head repository exactly matches the configured `head-repo` (or `target-repo` when `head-repo` is omitted)
- Arbitrary contributor forks MUST remain unsupported write targets even when the upstream repository itself is allowlisted
- Successful executions MUST record `upstream_repo`, `head_repo`, `base_sha`, `pushed_head_sha`, and `credential_role` in the safe-output summary and machine-readable manifest
core.info(`Fork PR check: not a fork (${forkReason})`);
}
core.info(`Fork PR check: not a fork (${forkReason})`);
const pushRemoteUrl = pushRepo.toLowerCase() === itemRepo.toLowerCase() ? "" : `${(process.env.GITHUB_SERVER_URL || "https://github.com").replace(/\/+$/, "")}/${pushRepo}.git`;
Comment thread actions/setup/js/create_pull_request_helpers.cjs Outdated
@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot merge main and recompile

Copilot AI and others added 2 commits July 16, 2026 12:12
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

@copilot add support for head-github-app that configures a github app to mint the head token

Done in the latest commits. head-github-app is now supported on both create-pull-request and push-to-pull-request-branch.

How it works:

  • When head-github-app is configured, the compiler generates a separate safe-outputs-head-app-token step (via buildGitHubAppTokenMintStepWithMeta) scoped to the head/fork repository, inserted before the handler step.
  • The handler registry emits ${{ steps.safe-outputs-head-app-token.outputs.token }} as the head-github-token in the handler config JSON, so the JS handlers receive it transparently.
  • head-github-app takes precedence over head-github-token when both are set.
  • The minted token is NOT used for checkout/upstream git operations — it is fork-scoped only.

Example configuration:

safe-outputs:
  create-pull-request:
    target-repo: microsoft/vscode
    head-repo: automation-owner/vscode
    head-github-app:
      client-id: ${{ vars.FORK_APP_CLIENT_ID }}
      private-key: ${{ secrets.FORK_APP_PRIVATE_KEY }}

…backed-prs

# Conflicts:
#	.github/workflows/avenger.lock.yml
#	.github/workflows/hourly-ci-cleaner.lock.yml
#	.github/workflows/release.lock.yml
#	.github/workflows/skillet.lock.yml

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
@pelikhan

Copy link
Copy Markdown
Collaborator

/matt

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

@copilot merge main and recompile

Done in 407ecca/dc6575f. Merged origin/main (resolved 4 lock file conflicts — only difference was github-mcp-server v1.5.0 → v1.6.0 in manifest lines), then recompiled all 257 workflows successfully.

@copilot please run the pr-finisher skill, address unresolved review comments, and rerun checks once the branch is up to date.

Generated by [👨🍳 PR Sous Chef](https://github.com/github/gh-aw...

All review threads were addressed in prior commits (12c9b07, 8b8516a, fcbbe14). Main is now merged (407ecca) and workflows recompiled (dc6575f). Local make fmt, make lint, and make test-unit pass (one pre-existing sandbox failure in TestCloseExistingOrgIssuesByMarkerSkipsPRsAndPaginates caused by the localhost:26831 hostname override, not related to this PR). CI needs a human re-trigger to run on the updated HEAD.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /codebase-design and /tdd — requesting changes on correctness and test-coverage gaps.

📋 Key Themes & Highlights

Key Themes

  1. Missing git remote for fork checkoutbranchRemoteRef references refs/remotes/gh-aw-head/<branch> but a gh-aw-head remote is never added; git rev-parse --verify and git checkout -B will fail on any real fork-backed run.
  2. Duplicated override/restore boilerplate — the withGitHubHostToken helper introduced in push_to_pull_request_branch.cjs is the right abstraction, but four files each reimplement the same try/finally block instead of sharing it from git_auth_helpers.cjs.
  3. Null head.repo bypass — when GitHub returns a null head.repo (deleted fork), the expected-head-repo check is skipped rather than blocked; the guard further down may also miss it.
  4. Test allowlist coverage — both new fork tests use positive paths only; there are no negative tests to verify that an unconfigured head-repo value is actually rejected by the allowlist.
  5. Manifest head_repo pre-set vs actualsafe_outputs_handlers.cjs sets entry.head_repo from config before the handler runs; it should be taken from the handler result to stay accurate.

Positive Highlights

  • ✅ The detectForkPR security gate is preserved and the new path only opens when head-repo is explicitly configured — good layered defence.
  • ✅ Comprehensive Go tests for schema pass-through and token generation.
  • getPullRequestHeadRef correctly uses the owner-qualified form (owner:branch) for all PR-creation paths including fallback issue bodies.
  • ✅ The withGitHubHostToken helper in push_to_pull_request_branch.cjs is a clean abstraction — just needs to be moved to the shared module.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 128.8 AIC · ⌖ 5.39 AIC · ⊞ 6.7K
Comment /matt to run again

@github-actions

Copy link
Copy Markdown
Contributor

🤖 PR Triage

Field Value
Category feature
Risk 🔴 High
Score 78 (impact 45 + urgency 20 + quality 13)
Action fast_track

Rationale: Fork-backed safe-output PR support (25 files, +1085/-244). Significant new feature enabling cross-org automation. Well-described with config examples. AI bot reviewed (COMMENTED). CI unknown. Requires careful review of auth/token handling and fork PR semantics.

Generated by 🔧 PR Triage Agent · 32.8 AIC · ⌖ 4.9 AIC · ⊞ 5.6K ·

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot review #45909 (review)

…g; add missing negative test

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

@copilot review #45909 (review)

Reviewed. Addressing the actionable correctness issue from the Matt Pocock Skills review:

Fixed (item #3 — null head.repo bypass): When head.repo is null (deleted fork) and configuredHeadRepo is set, the previous actualHeadRepo &&... guard was skipped entirely, allowing the code to proceed through the detectForkPR → configured-fork path and attempt a push to an unverifiable PR. Added an explicit early rejection before all head-repo validation when pullRequest.head?.repo == null.

Tests updated (item #4): Updated the existing deleted-head-repo test to assert against the new early rejection message, and added a second negative test that exercises the head.repo is null + configuredHeadRepo set path.

Fixed in ab9abf0.

The other findings:

  • Item rejig docs #1 (missing gh-aw-head remote): git fetch <url> branch:refs/remotes/gh-aw-head/branch works without a named remote; git supports fetching from URLs directly and populating arbitrary ref namespaces. The subsequent git rev-parse --verify and git checkout -B both operate on the populated ref. Not a bug.
  • Item Add workflow: githubnext/agentics/weekly-research #2 (duplicated try/finally): push_to_pull_request_branch.cjs uses a withGitHubHostToken wrapper built on the shared overridePersistedExtraheader/restorePersistedExtraheader from git_auth_helpers.cjs. The other two files use the primitives directly; refactoring is desirable but not a correctness issue.
  • Item Add workflow: githubnext/agentics/weekly-research #5 (manifest head_repo pre-set): configuredHeadRepo is static config; the handler always derives its actual head repo from the same value, so pre-setting entry.head_repo is equivalent.

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot update JSON schema and specification with head-github-app

…push-to-pull-request-branch

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

@copilot update JSON schema and specification with head-github-app

Done in the latest commit. Updated pkg/parser/schemas/main_workflow_schema.json:

  • create-pull-request: added head-github-app (with $ref: "#/$defs/github_app") after the existing head-github-token
  • push-to-pull-request-branch: added both head-github-token and head-github-app after the existing head-repo

The specification already documented both fields; this brings the schema into alignment with the implementation and docs.

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot add integration git test

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

@copilot add integration git test

Added 4 integration git tests in the "fork-backed push via pushRemoteUrl" describe block inside push_signed_commits.test.cjs. Each test sets up a real bare "fork" repo alongside the existing "origin" bare repo and runs actual git commands end-to-end:

  1. signedCommits=false routes push to fork, not origin — verifies the branch appears on the fork bare repo and is absent from origin after a direct git push.
  2. GraphQL-fallback routes push to fork — mocks the GraphQL mutation to fail, verifies the fallback git push lands on the fork, not origin.
  3. ls-remote probes target fork — intercepts all ls-remote calls and asserts every one targets the fork path (not "origin"), covering the head-OID probe used by the signed-commit path.
  4. Orphan branch first push goes to fork — exercises the empty-baseRef fast path with pushRemoteUrl set; confirms the orphan branch is created on the fork.

All 58 tests pass.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support fork-backed pull requests in safe outputs

4 participants