Skip to content

[WIP] Fix duplicate Authorization headers from checkout@v7 - #50377

Merged
pelikhan merged 5 commits into
mainfrom
copilot/v0844-fix-duplicate-authorization-headers
Aug 4, 2026
Merged

[WIP] Fix duplicate Authorization headers from checkout@v7#50377
pelikhan merged 5 commits into
mainfrom
copilot/v0844-fix-duplicate-authorization-headers

Conversation

Copilot AI commented Aug 4, 2026

Copy link
Copy Markdown
Contributor
  • Add findIncludedExtraheaderConfigFiles to detect config files referenced via includeIf.gitdir*.path (as written by actions/checkout@v7)
  • Extend unsetExtraheaderAllScopes to also unset the extraheader key in those included files, with a safety check restricting file targets to RUNNER_TEMP/OS tmp dir
  • Add unit tests covering the new function and its safety validation
  • Add a real-git integration test reproducing the checkout v7 includeIf.gitdir scenario and confirming the value is cleared
  • All targeted tests pass (50/50)

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 33.5 AIC · ⌖ 6.08 AIC · ⊞ 8.3K ·
Comment /souschef to run again


Generated by 👨‍🍳 PR Sous Chef · gpt54 · 8.54 AIC · ⌖ 6.21 AIC · ⊞ 8.3K ·
Comment /souschef to run again

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI requested a review from pelikhan August 4, 2026 19:57
@pelikhan
pelikhan marked this pull request as ready for review August 4, 2026 20:06
Copilot AI balanced review requested due to automatic review settings August 4, 2026 20:06
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

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

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

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

Addresses duplicate Authorization headers caused by checkout v7’s included credential files.

Changes:

  • Detects temp-directory includeIf.gitdir config files.
  • Clears extraheaders from included files.
  • Adds unit and real-git integration tests.
Show a summary per file
File Description
actions/setup/js/git_auth_helpers.cjs Adds included-config discovery and cleanup.
actions/setup/js/git_auth_helpers.test.cjs Adds unit coverage for discovery and safety checks.
actions/setup/js/git_auth_helpers.integration.test.cjs Tests checkout v7-style Git configuration.

Review details

Tip

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

Suppressed comments (1)

actions/setup/js/git_auth_helpers.cjs:110

  • An unexpected write failure here leaves the included checkout Authorization header active, but the override continues and adds the fork header locally, recreating the duplicate-header failure. Treat this like the global/local unsets and abort before running any authenticated operation.
    if (result.exitCode !== 0 && result.exitCode !== 5) {
      core.warning(`git_auth_helpers: git config --file ${file} --unset-all ${key} failed (exit ${result.exitCode}): ${result.stderr.trim()}`);
    }
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment on lines +106 to +107
for (const file of includedFiles) {
const result = await exec.getExecOutput("git", ["config", "--file", file, "--unset-all", key], { silent: true, ignoreReturnCode: true });
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 80/100 — Excellent

Analyzed 50 test(s): 47 design, 3 implementation, 0 violation(s).

📊 Metrics (50 tests)
Metric Value
Analyzed 50 (Go: 0, JS: 50)
✅ Design 47 (94%)
⚠️ Implementation 3 (6%)
Edge/error coverage 40 (80%)
Duplicate clusters 0
Inflation YES — 938 test lines vs 284 prod lines (≈3.3:1)
🚨 Violations 0
Test File Classification Issues
checkoutHasPersistedExtraheader — false/true git_auth_helpers.test.cjs design_test
unsetExtraheaderAllScopes — throw on unexpected exit code git_auth_helpers.test.cjs design_test
unsetExtraheaderAllScopes — includeIf/checkout v7 git_auth_helpers.test.cjs design_test
findIncludedExtraheaderConfigFiles — security: skip outside RUNNER_TEMP git_auth_helpers.test.cjs design_test
overridePersistedExtraheader — clear scopes before write git_auth_helpers.test.cjs design_test
overridePersistedExtraheader — warn and fallback on read failure git_auth_helpers.test.cjs design_test
restorePersistedExtraheader — partial restore cleanup + re-throw git_auth_helpers.test.cjs design_test
withGitHubHostToken — restore after callback throws git_auth_helpers.test.cjs design_test
withGitHubHostToken — no accumulation across cycles (regression) git_auth_helpers.test.cjs design_test
overridePersistedExtraheader — strip trailing slash git_auth_helpers.test.cjs implementation_test URL normalization detail
restorePersistedExtraheader — strip trailing slash git_auth_helpers.test.cjs implementation_test URL normalization detail
checkoutHasPersistedExtraheader — strip trailing slash git_auth_helpers.test.cjs implementation_test URL normalization detail
Integration: unsetExtraheaderAllScopes — checkout v7 includeIf file git_auth_helpers.integration.test.cjs design_test
Integration: withGitHubHostToken — no accumulation across cycles git_auth_helpers.integration.test.cjs design_test
Integration: withGitHubHostToken — exactly one header inside callback git_auth_helpers.integration.test.cjs design_test
Integration: withGitHubHostToken — restore after callback throws git_auth_helpers.integration.test.cjs design_test

(Representative sample — 50 tests analyzed total)

⚠️ Flagged (1 — test inflation)

Inflation flag — combined test files add 938 lines against 284 production lines (≈3.3:1, threshold 2:1). This does not constitute a violation here: the PR fixes a subtle multi-scope git config credential bug, and the test volume is justified for a security-sensitive module. The integration test suite (real git repos, GIT_CONFIG_GLOBAL isolation) is particularly valuable.

URL-normalization tests (3 tests: strip trailing slash for checkoutHasPersistedExtraheader, overridePersistedExtraheader, restorePersistedExtraheader) verify internal argument construction rather than user-visible behavior. Borderline implementation tests — consider collapsing into a table-driven form.

Verdict

passed. 6% implementation tests (threshold: 30%). No violations. The integration tests exercising real git config semantics (multi-scope unset, checkout@v7 includeIf files, no-accumulation regression across retry cycles) are a strong quality signal for this fix.

🧪 Test quality analysis by Test Quality Sentinel · sonnet46 · 58.8 AIC · ⌖ 11.4 AIC · ⊞ 8.6K ·
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 — Excellent. 6% implementation tests (threshold: 30%). No violations. Strong behavioral coverage with real-git integration tests.

@github-actions github-actions Bot mentioned this pull request Aug 4, 2026

@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: Fix duplicate Authorization headers from checkout@v7

The approach is correct and well-reasoned. The includeIf.gitdir-referenced credential file path is identified by querying the local config, validated against known-safe temp roots (RUNNER_TEMP / OS tmpdir), and then cleared via git config --file ... --unset-all. Integration and unit test coverage is solid.

Two non-blocking suggestions (see inline comments):

  1. require('path') / require('os') inside function body – move these to module-level.
  2. Dangling includeIf.gitdir directive – after clearing the credential file, consider also unsetting the referencing includeIf.gitdir*.path key in .git/config to leave the local config clean.

Neither blocks merging; the security-critical path (safe-root guard + exit-code 5 tolerance) is correct.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 28.8 AIC · ⌖ 13.4 AIC · ⊞ 5.4K

Comment thread actions/setup/js/git_auth_helpers.cjs Outdated
}

const path = require("path");
const os = require("os");

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.

Non-blocking: require('path') and require('os') are called inside the function body on every invocation. These should be module-level requires at the top of the file alongside the existing core and exec references.

const path = require('path');
const os = require('os');

While Node.js caches require calls, putting them inside the function obscures the module's dependencies and is inconsistent with the rest of the file.

@copilot please address this.

}
for (const file of includedFiles) {
const result = await exec.getExecOutput("git", ["config", "--file", file, "--unset-all", key], { silent: true, ignoreReturnCode: true });
if (result.exitCode !== 0 && result.exitCode !== 5) {

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.

Non-blocking – missing cleanup: After unsetting the extraheader key from the included credential file, the includeIf.gitdir:<path>.path entry in .git/config itself is not removed. The dangling include directive is harmless once the file's extraheader is cleared, but leaving it adds noise and could confuse future reads. Consider also running:

await exec.getExecOutput('git', ['config', '--local', '--unset-all', matchingKey], opts);

...for each matched includeIf.gitdir*.path key so the local config is left in a clean state.

@copilot please address this.

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

Blocking issues found in the new includeIf.gitdir credential-clearing logic — see inline comments.

Themes
  • Error-handling asymmetry (high): the scope-unset loop throws on unexpected git exit codes, but the new includeIf-file-unset loop only warns for the same failure class — this can silently leave the checkout@v7 credentials file with a stale extraheader, reproducing the exact bug this PR fixes.
  • Safe-root check bypass via symlinks (medium): path.resolve does not dereference symlinks, so a symlink under RUNNER_TEMP pointing outside it defeats the safety check meant to restrict which files get mutated.
  • Fragile key/value parsing (medium): splitting --get-regexp output on the first space breaks if the gitdir path (embedded in the key) contains a space, silently dropping or mis-locating the credentials file.
  • Test coverage gap (low-medium): the new failure-warning path for file unsets has no test, unlike its scope-unset counterpart.

Given this is marked WIP, these are all fixable before merge — none require redesigning the approach, but the error-handling asymmetry in particular could quietly undermine the bug fix this PR is meant to deliver.

🔎 Code quality review by PR Code Quality Reviewer · auto · 72 AIC · ⌖ 4.33 AIC · ⊞ 7.9K
Comment /review to run again

Comment thread actions/setup/js/git_auth_helpers.cjs Outdated
for (const file of includedFiles) {
const result = await exec.getExecOutput("git", ["config", "--file", file, "--unset-all", key], { silent: true, ignoreReturnCode: true });
if (result.exitCode !== 0 && result.exitCode !== 5) {
core.warning(`git_auth_helpers: git config --file ${file} --unset-all ${key} failed (exit ${result.exitCode}): ${result.stderr.trim()}`);

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.

Failures here are only warned about, while the identical failure class in the scope-unset loop above throws — this asymmetry can silently leave the checkout@v7 credentials file with a stale extraheader, reproducing the exact duplicate-header bug this PR is meant to fix.

Details

The --global/--local loop throws on any exit code other than 0 or 5, telling the caller the credential may still be effective. The new includeIf-file loop only logs a warning for the same failure class (permission denied, lock contention, corrupt file, etc.) and continues silently. Since this is exactly the file actions/checkout@v7 uses to persist the credential, a failed unset here is at least as consequential as a failed scope unset, yet the caller gets no signal cleanup did not succeed.

Suggested fix: throw (or aggregate and re-throw) on unexpected exit codes here too, or surface failure via a return value so callers can detect the credential may still be active.

Comment thread actions/setup/js/git_auth_helpers.cjs Outdated
const filePath = (spaceIdx === -1 ? "" : trimmed.slice(spaceIdx + 1)).trim();
if (!filePath) continue;
const resolved = path.resolve(cwd || process.cwd(), filePath);
if (safeRoots.some(root => resolved === root || resolved.startsWith(root + path.sep))) {

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 safe-root check uses path.resolve, which never dereferences symlinks, so a symlink placed inside RUNNER_TEMP that points outside it will pass this check and then have git config --file <path> --unset-all run against it.

Details

path.resolve only normalizes ./.. segments and joins with cwd; it does not call fs.realpath. If an attacker (or a stray leftover from a prior job on a shared/self-hosted runner) can place a symlink under $RUNNER_TEMP pointing at an arbitrary file, resolved.startsWith(root + path.sep) still passes because the symlink's own path is inside the safe root, even though it dereferences elsewhere. The subsequent git config --file <resolved> --unset-all call would then operate on the symlink target, not the safe temp file — exactly the class of write the comment above this function claims to prevent.

Fix: resolve with fs.realpathSync(resolved) (wrapped in try/catch for missing files) before the safe-root comparison, and re-validate the real path against safeRoots.

Comment thread actions/setup/js/git_auth_helpers.cjs Outdated
for (const line of result.stdout.split("\n")) {
const trimmed = line.trim();
if (!trimmed) continue;
const spaceIdx = trimmed.indexOf(" ");

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.

Splitting on the first space to separate the includeIf key from its file-path value will silently misparse if the gitdir path (embedded in the key itself) contains a space.

Details

git config --get-regexp prints lines as <key> <value>, and here the key is includeif.gitdir:<gitdir-path>.path. If <gitdir-path> contains a space (plausible for checkout paths under custom working directories, or on Windows runners), trimmed.indexOf(" ") finds a space inside the key rather than the true key/value separator, producing a garbage filePath. That garbage path then either fails the safe-root check (silently dropped, leaving the credentials file uncleared) or, worse, resolves to something unexpected.

Consider using git config --get-regexp --null (NUL-separated key/value pairs) to avoid ambiguity, or explicitly locating the last space rather than the first, given .path keys generally have simple values (but not always, if the value itself contains a space).

});

it("should also unset the key from includeIf.gitdir-referenced config files (checkout v7 case)", async () => {
process.env.RUNNER_TEMP = "/home/runner/work/_temp";

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.

No test covers the file-unset failure path (non-0/non-5 exit code) for the new includeIf-referenced config file loop, even though the equivalent scope-unset failure path is tested just above.

Details

The test at line ~100 (should throw when a scope unset fails...) verifies the throw behavior for --global/--local unsets. There is no equivalent test asserting what happens when git config --file <path> --unset-all returns an exit code other than 0 or 5 (e.g. 4, permission denied) — currently that path only calls core.warning and swallows the error (see the related comment on line 109). Add a test that mocks such a failure and asserts the current (or fixed) behavior, so any future change to this error-handling asymmetry is caught by the suite.

@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, /tdd, and /codebase-design — requesting changes on two small but real issues.

📋 Key Themes & Highlights

Issues Found

  • RUNNER_TEMP env var leak — the unsetExtraheaderAllScopes unit test mutates process.env.RUNNER_TEMP without try/finally; a mid-test throw would corrupt the environment for subsequent tests.
  • Inline require callspath and os are required inside findIncludedExtraheaderConfigFiles on every call instead of at module top level, contrary to the existing file style.
  • Regex coverage note — minor: the --get-regexp pattern is fine as-is, but a clarifying comment and an extra unit test for the gitdir/ slash variant would guard against unintentional future changes.

Positive Highlights

  • ✅ Excellent root-cause diagnosis: the bug is the includeIf indirection that bypasses scope-targeted unsets — not a superficial symptom fix.
  • ✅ Safety guard (RUNNER_TEMP / OS tmp dir allowlist) is the right defence-in-depth approach.
  • ✅ Both unit and integration test coverage, including a real-git reproduction of the checkout v7 scenario.
  • ✅ Exit-code 5 ("key not found") is correctly treated as a non-error in --unset-all calls.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 40.3 AIC · ⌖ 7.49 AIC · ⊞ 7.1K
Comment /matt to run again

});

it("should also unset the key from includeIf.gitdir-referenced config files (checkout v7 case)", async () => {
process.env.RUNNER_TEMP = "/home/runner/work/_temp";

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.

[/tdd] RUNNER_TEMP is mutated without try/finally — if the test throws before the delete on line 120, it leaks into subsequent tests.

💡 Suggested fix

Save and restore RUNNER_TEMP with the same try/finally pattern used in the findIncludedExtraheaderConfigFiles describe block:

const orig = process.env.RUNNER_TEMP;
try {
  process.env.RUNNER_TEMP = "/home/runner/work/_temp";
  // ... rest of test body ...
} finally {
  if (orig === undefined) delete process.env.RUNNER_TEMP;
  else process.env.RUNNER_TEMP = orig;
}

@copilot please address this.

Comment thread actions/setup/js/git_auth_helpers.cjs Outdated
return [];
}

const path = require("path");

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.

[/codebase-design] path and os are required inside the function on every call. These are core Node.js built-ins that should be required at the top of the file alongside the existing core and exec references.

💡 Suggested fix

Move to the top of the file with the other requires:

const path = require("path");
const os = require("os");

Then remove the inline require calls from findIncludedExtraheaderConfigFiles.

@copilot please address this.

Comment thread actions/setup/js/git_auth_helpers.cjs Outdated
*/
async function findIncludedExtraheaderConfigFiles(cwd) {
const opts = { silent: true, ignoreReturnCode: true, ...(cwd ? { cwd } : {}) };
const result = await exec.getExecOutput("git", ["config", "--local", "--get-regexp", "^includeif\\.gitdir.*\\.path$"], opts);

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] The regex ^includeif\.gitdir.*\.path$ is case-insensitive in git config output (git lowercases all section names in --get-regexp output), but the pattern itself uses a literal lowercase match — which is fine for the current output format. However, it is worth a brief comment explaining why lowercase is sufficient so a future reader does not "fix" it unnecessarily.

Also consider whether includeIf.gitdir/ (forward-slash variant, which git also supports) is covered by the .* wildcard — it is, but a unit test for that path value would make this explicit.

@copilot please address this.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot Please address the remaining blocking review feedback on this PR, rerun the failing checks, refresh the branch if needed, then run the pr-finisher skill before handing it back.

Open review feedback to close out:

  • github-actions changes requested: error-handling asymmetry in the new includeIf credential-file unset loop can silently leave stale extraheaders; align failure handling with the scope-unset path.
  • github-actions changes requested: harden the RUNNER_TEMP safety check against symlink escapes and make the --get-regexp parsing robust for paths containing spaces.
  • github-actions changes requested: fix the RUNNER_TEMP test env leak with try/finally, and move inline require('path') / require('os') calls to module scope.

Failed checks:

Please resolve addressed review threads once the reviewer feedback has been handled.

Generated by 👨🍳 PR Sous Chef

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 33.5 AIC · ⌖ 6.08 AIC · ⊞ 8.3K ·
Comment /souschef to run again

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Please address the remaining blocking review feedback on this PR, rerun the failing checks, refresh the branch if needed, then run the pr-finisher skill before handing it back.

Open review...

Addressed all review feedback in commit Fix TypeScript error, harden includeIf parsing, align error handling:

  • TypeScript error fixed: moved require('path'), require('os'), require('fs') to module scope and replaced .filter(Boolean) with an explicit type-guard filter (p): p is string => p !== undefined && p !== "".
  • Robust --get-regexp parsing for paths with spaces: replaced the single-pass --get-regexp split-on-first-space approach with a two-step --name-only --get-regexp (to get key names) + --get-all <key> (to get values). This correctly handles spaces in gitdir paths embedded in the config key.
  • Symlink escape hardening: added fs.realpathSync resolution before the RUNNER_TEMP/tmp containment check. Falls back to the lexical path if the file doesn't exist yet.
  • Error-handling alignment: the --file unset loop now throws on unexpected failures (matching the behavior of the scope-unset loop), rather than issuing a warning.
  • Test env leak fix: the unsetExtraheaderAllScopes test that sets RUNNER_TEMP now uses try/finally to guarantee cleanup.
  • All 36 unit tests pass; TypeScript typecheck passes cleanly.

Copilot AI requested a review from gh-aw-bot August 4, 2026 20:52
@pelikhan
pelikhan merged commit 599b34e into main Aug 4, 2026
8 checks passed
@pelikhan
pelikhan deleted the copilot/v0844-fix-duplicate-authorization-headers branch August 4, 2026 21:26
@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot Please address the remaining review feedback on this PR, refresh the branch if GitHub allows it, and then run the pr-finisher skill before handing it back.

Open review feedback to close out (newest first):

  • github-actions changes requested: ensure any unexpected git config --file ... --unset-all failure aborts instead of only warning, so stale checkout credentials cannot survive.
  • github-actions comment: now that you replied with the fixes, please resolve any addressed review threads.

Generated by 👨🍳 PR Sous Chef

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 8.54 AIC · ⌖ 6.21 AIC · ⊞ 8.3K ·
Comment /souschef to run again

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.

v0.84.4: checkout@v7 includeIf credential still causes duplicate Authorization headers

4 participants