Skip to content

fix: wrap fork push sequences in withGitHubHostToken to prevent duplicate Authorization (HTTP 400) - #48595

Open
pelikhan with Copilot wants to merge 3 commits into
mainfrom
copilot/fix-duplicate-authorization-error
Open

fix: wrap fork push sequences in withGitHubHostToken to prevent duplicate Authorization (HTTP 400)#48595
pelikhan with Copilot wants to merge 3 commits into
mainfrom
copilot/fix-duplicate-authorization-error

Conversation

Copilot AI commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Fork-backed create_pull_request fails with HTTP 400 on every git operation against head-repo when head-github-app supplies a separate fork token. The checkout-persisted upstream extraheader and the fork token both reach the fork remote, causing git to send duplicate Authorization headers.

This is the create_pull_request equivalent of the push_to_pull_request_branch fix in #40280.

Changes

git_auth_helpers.cjs — promote withGitHubHostToken to shared helper

  • Moved withGitHubHostToken(token, callback, cwd) out of push_to_pull_request_branch.cjs and into git_auth_helpers.cjs as an exported function
  • The helper saves the current extraheader, replaces it with the supplied token for the duration of the callback, then restores — ensuring exactly one Authorization source is active at a time

push_to_pull_request_branch.cjs — no behavior change

  • Imports withGitHubHostToken from git_auth_helpers.cjs; removes the now-redundant local definition

create_pull_request.cjs — fix duplicate-auth on fork push

  • handleRemoteBranchCollision gains a cwd option; all git commands (ls-remote, branch -m) and the credential override now use the caller-supplied working directory
  • Simplified the inline override/restore in handleRemoteBranchCollision to use withGitHubHostToken
  • All three fork push sequences (bundle path, patch path, allow-empty path) are now wrapped in withGitHubHostToken(headGitHubToken, …, forkCwd) so only the fork token is active for the entire sequence:
const forkCwd = process.cwd();
const runPush = async () => {
  branchName = await handleRemoteBranchCollision(, { remoteToken: headGitHubToken, cwd: forkCwd });
  await pushSignedCommits({, cwd: forkCwd, pushRemoteUrl, pushToken: headGitHubToken });
};
await (headGitHubToken && pushRemoteUrl
  ? withGitHubHostToken(headGitHubToken, runPush, forkCwd)
  : runPush());

git_auth_helpers.test.cjs — unit tests for withGitHubHostToken

  • 8 new tests covering: no-op on empty token, override applied before callback, upstream header restored after callback (including on throw), unset when no prior value, cwd propagation

…TTP 400)

- Move withGitHubHostToken from push_to_pull_request_branch.cjs into
  git_auth_helpers.cjs and export it, so it can be shared
- Update push_to_pull_request_branch.cjs to import withGitHubHostToken
  from git_auth_helpers.cjs instead of defining it locally
- Add cwd support to handleRemoteBranchCollision in create_pull_request.cjs
  so git ls-remote and git branch -m use the correct working directory
- Wrap all three fork push sequences (bundle, patch, allow-empty) in
  withGitHubHostToken so only the fork token is active for the entire
  fork operation, preventing duplicate Authorization headers (HTTP 400)
- Add withGitHubHostToken unit tests to git_auth_helpers.test.cjs

Fixes #48414

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix duplicate Authorization HTTP 400 for create_pull_request fix: wrap fork push sequences in withGitHubHostToken to prevent duplicate Authorization (HTTP 400) Jul 28, 2026
Copilot AI requested a review from pelikhan July 28, 2026 12:44
@github-actions github-actions Bot mentioned this pull request Jul 28, 2026
@pelikhan
pelikhan marked this pull request as ready for review July 28, 2026 12:55
Copilot AI review requested due to automatic review settings July 28, 2026 12:55
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR #48595 does not have the 'implementation' label and has 0 new lines of code in business logic directories (threshold: 100).

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

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

Fixes #48414 by centralizing temporary GitHub credential overrides and applying them to fork-backed PR pushes.

Changes:

  • Extracts and tests withGitHubHostToken.
  • Adds working-directory propagation for collision handling.
  • Wraps bundle, patch, retry, and empty fork pushes with fork credentials.
Show a summary per file
File Description
create_pull_request.cjs Applies fork authentication to PR push paths.
git_auth_helpers.cjs Exports the shared credential override helper.
git_auth_helpers.test.cjs Tests override, restoration, errors, and cwd.
push_to_pull_request_branch.cjs Uses the shared helper.

Review details

Tip

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

Comments suppressed due to low confidence (3)

actions/setup/js/create_pull_request.cjs:1697

  • The retry has the same checkout-wide credential regression: pushSignedCommits can fetch missing objects from upstream origin, but this wrapper makes those requests use the fork-only head token. Invoke the retry directly; the fork-targeted ls-remote and push operations already apply pushToken internally.
                await (headGitHubToken && pushRemoteUrl ? withGitHubHostToken(headGitHubToken, runRetryPush, forkCwd) : runRetryPush());

actions/setup/js/create_pull_request.cjs:2054

  • Wrapping all of runPatchPush replaces the upstream checkout credential while pushSignedCommits may perform on-demand or recovery fetches against origin. Separate fork credentials commonly cannot read a private upstream repository, so partial-clone patch pushes can now fail before reaching the fork. Leave the outer credential unchanged and rely on the already-scoped fork wrappers in the collision check and pushSignedCommits.
              await (headGitHubToken && pushRemoteUrl ? withGitHubHostToken(headGitHubToken, runPatchPush, forkCwd) : runPatchPush());

actions/setup/js/create_pull_request.cjs:2229

  • This also applies the fork token to every Git operation performed by pushSignedCommits, including possible partial-clone object fetches from upstream origin. A fork-scoped token is not a substitute for the target repository's persisted credential; call the sequence directly and let the fork-specific operations apply pushToken locally.
              await (headGitHubToken && pushRemoteUrl ? withGitHubHostToken(headGitHubToken, runEmptyPush, forkCwd) : runEmptyPush());
  • Files reviewed: 4/4 changed files
  • Comments generated: 3
  • Review effort level: Medium

Comment thread actions/setup/js/create_pull_request.cjs Outdated
Comment thread actions/setup/js/push_to_pull_request_branch.cjs Outdated
Comment thread actions/setup/js/create_pull_request.cjs Outdated

@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.

Review

The refactor looks correct and well-tested. withGitHubHostToken is properly extracted into git_auth_helpers.cjs and all three fork push paths (bundle, patch, allow-empty) are wrapped to eliminate the duplicate-Authorization HTTP 400.

Minor observation (non-blocking): In the bundle, patch, and allow-empty paths, handleRemoteBranchCollision is called with remoteToken: headGitHubToken inside the runBundlePush/runPatchPush/runEmptyPush closure that is already wrapped by an outer withGitHubHostToken(headGitHubToken, ...). The inner override saves the currently-active fork header, replaces it with the same token, and restores it — functionally a no-op but slightly redundant. Not blocking.

The 8 new unit tests cover all important cases: no-op on empty/undefined token, override before callback, restore after success and on throw, return value propagation, cwd propagation.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 19.8 AIC · ⌖ 5.26 AIC · ⊞ 5K

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

⚠️ Test Quality Score: 80/100 — Acceptable

Analyzed 8 new test(s): 6 design, 2 implementation. Test inflation ratio noted.

📊 Metrics (8 tests)
Metric Value
Analyzed 8 (JavaScript: 8)
✅ Design 6 (75%)
⚠️ Implementation 2 (25%)
Edge/error coverage 8/8 (100%)
Duplicate clusters 0
Inflation ratio 3.24:1 (⚠️ threshold: 2:1)
🚨 Violations 1

Per-Test Classification

Test Name Classification Issues
empty token edge case implementation_test Happy-path, no git operations
undefined token edge case implementation_test Happy-path edge case
override before callback design_test Validates critical override contract ✓
restore after callback design_test Validates cleanup contract ✓
restore on error design_test Tests finally-block guarantee ✓
unset when no previous design_test Edge case cleanup ✓
return passthrough implementation_test Return value forwarding
cwd propagation design_test Directory scoping contract ✓
⚠️ Flagged Issue: Test Inflation

Test Inflation Ratio: 3.24:1

  • Test file: actions/setup/js/git_auth_helpers.test.cjs (+107 lines)
  • Production file: git_auth_helpers.cjs (+33 lines)
  • Ratio: 107 ÷ 33 = 3.24x (exceeds 2:1 threshold)

Assessment: The inflation is justified by the complexity of the new withGitHubHostToken function's contract:

  • Manages state (save → override → restore)
  • Requires error-handling verification (finally block guarantee)
  • Needs edge-case coverage (empty/undefined tokens, missing previous values)
  • Requires multi-call sequencing validation (override before callback, restore after)

The 8 tests provide comprehensive coverage of the async/sync state machine and cleanup guarantees that the function implements. The ratio reflects appropriate test depth for a critical infrastructure function.

Verdict

⚠️ Acceptable with note. 25% implementation tests (threshold: 30% ✓). Test inflation of 3.24:1 is noted but justified by function complexity and critical error-handling contracts. 6/8 tests are design-level and verify important behavioral guarantees (override sequencing, cleanup-on-error, cwd scoping).

Recommendation: Approve with quality note. Tests provide solid design-level coverage of the withGitHubHostToken contract and its error guarantees.

🧪 Test quality analysis by Test Quality Sentinel · haiku45 · 10.4 AIC · ⌖ 9.79 AIC · ⊞ 8.1K ·
Comment /review to run again

@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.

✅ Test Quality Sentinel: 80/100. 25% implementation tests (threshold: 30%). 75% design-level tests verify critical behavioral contracts (override sequencing, error recovery, cwd scoping). Test inflation ratio of 3.24:1 is justified by function complexity.

@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 /diagnosing-bugs — requesting changes for dead imports left behind by the refactor.

📋 Key Themes & Highlights

Issues Found

  • Dead imports (2 files): After moving withGitHubHostToken into git_auth_helpers.cjs, both create_pull_request.cjs and push_to_pull_request_branch.cjs still import overridePersistedExtraheader and restorePersistedExtraheader even though they are no longer used directly. These should be removed.

Positive Highlights

  • ✅ Clean extraction of withGitHubHostToken into the shared helper — good single-responsibility improvement
  • ✅ All three fork push paths (bundle, patch, allow-empty) are consistently wrapped — no partial coverage
  • ✅ Excellent test suite: 8 targeted tests covering no-op, override, restore-on-throw, unset, return value, and cwd propagation
  • ✅ The forkCwd = process.cwd() capture before entering the async callback prevents any potential cwd drift
  • ✅ JSDoc updated on handleRemoteBranchCollision to document the new cwd option

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

const { checkFileProtection, checkFileProtectionPostApply } = require("./manifest_file_helpers.cjs");
const { renderTemplateFromFile, renderFilesList, buildProtectedFileList, getPromptPath } = require("./messages_core.cjs");
const { overridePersistedExtraheader, restorePersistedExtraheader } = require("./git_auth_helpers.cjs");
const { overridePersistedExtraheader, restorePersistedExtraheader, withGitHubHostToken } = require("./git_auth_helpers.cjs");

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.

[/diagnosing-bugs] Same dead-import issue: overridePersistedExtraheader and restorePersistedExtraheader are imported but no longer called in this file after the refactor.

Consider trimming to:

const { withGitHubHostToken } = require('./git_auth_helpers.cjs');

@copilot please address this.

const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs");
const { renderTemplateFromFile, buildProtectedFileList, getPromptPath } = require("./messages_core.cjs");
const { overridePersistedExtraheader, restorePersistedExtraheader } = require("./git_auth_helpers.cjs");
const { overridePersistedExtraheader, restorePersistedExtraheader, withGitHubHostToken } = require("./git_auth_helpers.cjs");

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.

[/diagnosing-bugs] Dead imports: overridePersistedExtraheader and restorePersistedExtraheader are still imported but no longer used after the refactor — this will cause linter warnings and mislead future readers. Consider reducing to just withGitHubHostToken.

@copilot please address this.

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

@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.

COMMENT — non-blocking maintainability concerns

The core fix (wrapping fork push sequences in withGitHubHostToken to avoid duplicate Authorization headers) is logically sound: it saves/restores the extraheader around each push callback, cwd is correctly threaded through to scope git config changes, and item processing is strictly sequential (for...of loops), so there is no concurrent-access risk to the mutated git config state. Tests for the new shared helper look reasonable.

Themes from this review
  • Nested-wrap fragility: handleRemoteBranchCollision's internal withGitHubHostToken(options.remoteToken, ...) call is now always nested inside an outer withGitHubHostToken(headGitHubToken, ...) wrap at every call site, and both currently use the same token. This coincidence makes the nesting a safe no-op today, but it is an unenforced invariant with no test coverage — a future change that diverges the two tokens would silently misapply/restore the wrong Authorization header. See inline comment for details and suggested hardening.
  • Duplicated wrapping boilerplate: the same forkCwd + closure + conditional withGitHubHostToken pattern is copy-pasted across four call sites (bundle push, bundle retry-after-rewrite, patch push, empty-commit push). This is exactly the kind of duplication that lets fixes land inconsistently, as nearly happened with the retry path in this same PR. A small shared helper would reduce that risk going forward.

A grumpy-coder sub-agent independently mined for issues; its findings on nesting and duplication were confirmed and incorporated. Its concern about concurrent parallel processing was checked directly against the codebase and ruled out — create_pull_request.cjs processes items via sequential for loops in safe_output_handler_manager.cjs, so there is no concurrent mutation of the shared git config extraheader.

No correctness, security, or crash-level bugs were found in the diff; the two inline comments are recommendations to harden an implicit invariant and reduce duplication, not blockers.

🔎 Code quality review by PR Code Quality Reviewer · sonnet50 · 162.1 AIC · ⌖ 4.64 AIC · ⊞ 7.4K
Comment /review to run again

const checkRemoteBranch = async () => exec.getExecOutput("git", ["ls-remote", "--heads", remoteTarget, branchName]);
const checkRemoteBranch = async () => exec.getExecOutput("git", ["ls-remote", "--heads", remoteTarget, branchName], cwd ? { cwd } : {});
let checkResult;
if (options.remoteToken) {

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.

The inner withGitHubHostToken call here is nested inside the outer wrap added around runBundlePush/runPatchPush/runEmptyPush, and both currently use the identical token — but nothing enforces that invariant.

💡 Why this matters

handleRemoteBranchCollision is now always invoked from inside an outer withGitHubHostToken(headGitHubToken, ...) block (see lines ~1632-1660, 2026-2054, 2202-2229). Since options.remoteToken is always headGitHubToken — the same value the outer wrap already applied — this inner call reads the outer override as its "previous" value and then restores that same override value when it finishes. That happens to be a harmless no-op today only because the two tokens coincide.

If a future change ever passes a different token into remoteToken (e.g. a per-call scoped token) while the outer wrap still uses headGitHubToken, the inner restorePersistedExtraheader would put the outer's overridden header back rather than the true pre-override upstream header, and the subsequent push (which runs after this inner call returns, still inside the outer callback) would silently run under the wrong or already-restored auth state — reintroducing the exact duplicate/incorrect-Authorization class of bug this PR fixes.

Suggested fix: either (a) have handleRemoteBranchCollision skip its own token override entirely when called from within an already-active outer wrap (e.g. accept a flag or detect via a shared "active token" marker), or (b) add an explicit code comment + a unit test asserting nested calls with the same token are safe, so a future refactor that diverges the tokens fails a test instead of silently breaking auth.

// the catch block below.
{
try {
const forkCwd = process.cwd();

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.

This wrap-a-closure-in-withGitHubHostToken pattern is copy-pasted three times (bundle, patch, and empty-commit push paths), increasing the odds a future fix lands in only one of them.

💡 Why this matters

Each of the three push blocks (~1632-1660, 2026-2054, 2202-2229) repeats the same shape: capture forkCwd = process.cwd(), build an async closure, then conditionally call withGitHubHostToken(headGitHubToken, closure, forkCwd) vs calling the closure directly when there is no fork token/remote. The retry-after-rewrite path even needed a fourth near-duplicate of this pattern added separately (line ~1697), which is exactly the kind of drift that duplicated logic invites — it would be easy for a future auth fix or logging addition to be applied to one call site and missed in the others.

Suggested refactor: extract a small helper, e.g. runForkPush(token, remoteUrl, cwd, fn) that encapsulates the token && remoteUrl ? withGitHubHostToken(token, fn, cwd) : fn() ternary, and call it from all four sites. This removes ~4 lines of duplication per call site and centralizes the wrapping decision so any future change to the gating condition only needs to happen once.

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

Copilot AI commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

@copilot run pr-finisher skill

Implemented a pr-finisher pass in 70365c6: removed the checkout-wide fork-token wrappers around create_pull_request push sequences and trimmed stale auth-helper imports; local validation passed (make fmt, make lint, make test-unit, make test, make recompile, and make agent-report-progress).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fork-backed create_pull_request fails with duplicate Authorization HTTP 400

3 participants