Fix includeIf case sensitivity on Windows self-hosted runners

Switch to includeIf.gitdir/i: on Windows so path matching is
case-insensitive, matching the filesystem behavior. This fixes
auth failures on self-hosted Windows runners where the workspace
folder casing doesn't match between the runner config and disk.

Also update the cleanup regex to handle both gitdir: and gitdir/i:
variants.

Fixes #2345
This commit is contained in:
Salman Muin Kayser Chishti
2026-05-06 12:17:20 +00:00
committed by GitHub
parent 900f2210b1
commit be385d8fff
3 changed files with 66 additions and 14 deletions
+40
View File
@@ -974,6 +974,46 @@ describe('git-auth-helper tests', () => {
).toBe(false) ).toBe(false)
expect((authHelper as any).testCredentialsConfigPath('')).toBe(false) expect((authHelper as any).testCredentialsConfigPath('')).toBe(false)
}) })
const includeIfCleanupRegex_matchesBothVariants =
'includeIf cleanup regex matches both gitdir: and gitdir/i: keys'
it(includeIfCleanupRegex_matchesBothVariants, async () => {
// The cleanup regex must match both variants so credential
// removal works regardless of which was written
const regex = /^includeIf\.gitdir(\/i)?:/
expect(regex.test('includeIf.gitdir:D:/workspaces/repo/.git.path')).toBe(
true
)
expect(regex.test('includeIf.gitdir/i:D:/Workspaces/repo/.git.path')).toBe(
true
)
expect(regex.test('includeIf.gitdir/i:/github/workspace/.git.path')).toBe(
true
)
expect(regex.test('includeIf.gitdir:~/projects/foo/.git.path')).toBe(true)
expect(regex.test('includeIf.onbranch:main.path')).toBe(false)
expect(regex.test('include.path')).toBe(false)
})
const includeIfDirective_usesCorrectVariantForPlatform =
'includeIf directive uses gitdir/i on Windows and gitdir on other platforms'
it(includeIfDirective_usesCorrectVariantForPlatform, async () => {
await setup(includeIfDirective_usesCorrectVariantForPlatform)
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
await authHelper.configureAuth()
const localConfigContent = (
await fs.promises.readFile(localGitConfigPath)
).toString()
if (isWindows) {
expect(localConfigContent).toContain('includeIf.gitdir/i:')
expect(localConfigContent).not.toContain('includeIf.gitdir:')
} else {
expect(localConfigContent).toContain('includeIf.gitdir:')
expect(localConfigContent).not.toContain('includeIf.gitdir/i:')
}
})
}) })
async function setup(testName: string): Promise<void> { async function setup(testName: string): Promise<void> {
+13 -7
View File
@@ -151,6 +151,12 @@ const stateHelper = __importStar(__nccwpck_require__(4866));
const urlHelper = __importStar(__nccwpck_require__(9437)); const urlHelper = __importStar(__nccwpck_require__(9437));
const uuid_1 = __nccwpck_require__(5840); const uuid_1 = __nccwpck_require__(5840);
const IS_WINDOWS = process.platform === 'win32'; const IS_WINDOWS = process.platform === 'win32';
// Use case-insensitive gitdir matching on Windows to handle path casing mismatches
// between the runner's GITHUB_WORKSPACE and the actual filesystem casing.
// See: https://github.com/actions/checkout/issues/2345
const INCLUDE_IF_GITDIR = IS_WINDOWS
? 'includeIf.gitdir/i:'
: 'includeIf.gitdir:';
const SSH_COMMAND_KEY = 'core.sshCommand'; const SSH_COMMAND_KEY = 'core.sshCommand';
function createAuthHelper(git, settings) { function createAuthHelper(git, settings) {
return new GitAuthHelper(git, settings); return new GitAuthHelper(git, settings);
@@ -270,7 +276,7 @@ class GitAuthHelper {
let submoduleGitDir = path.dirname(configPath); // The config file is at .git/modules/submodule-name/config let submoduleGitDir = path.dirname(configPath); // The config file is at .git/modules/submodule-name/config
submoduleGitDir = submoduleGitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows submoduleGitDir = submoduleGitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
// Configure host includeIf // Configure host includeIf
yield this.git.config(`includeIf.gitdir:${submoduleGitDir}.path`, credentialsConfigPath, false, // globalConfig? yield this.git.config(`${INCLUDE_IF_GITDIR}${submoduleGitDir}.path`, credentialsConfigPath, false, // globalConfig?
false, // add? false, // add?
configPath); configPath);
// Container submodule git directory // Container submodule git directory
@@ -280,7 +286,7 @@ class GitAuthHelper {
relativeSubmoduleGitDir = relativeSubmoduleGitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows relativeSubmoduleGitDir = relativeSubmoduleGitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
const containerSubmoduleGitDir = path.posix.join('/github/workspace', relativeSubmoduleGitDir); const containerSubmoduleGitDir = path.posix.join('/github/workspace', relativeSubmoduleGitDir);
// Configure container includeIf // Configure container includeIf
yield this.git.config(`includeIf.gitdir:${containerSubmoduleGitDir}.path`, containerCredentialsPath, false, // globalConfig? yield this.git.config(`${INCLUDE_IF_GITDIR}${containerSubmoduleGitDir}.path`, containerCredentialsPath, false, // globalConfig?
false, // add? false, // add?
configPath); configPath);
} }
@@ -410,10 +416,10 @@ class GitAuthHelper {
let gitDir = path.join(this.git.getWorkingDirectory(), '.git'); let gitDir = path.join(this.git.getWorkingDirectory(), '.git');
gitDir = gitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows gitDir = gitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
// Configure host includeIf // Configure host includeIf
const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`; const hostIncludeKey = `${INCLUDE_IF_GITDIR}${gitDir}.path`;
yield this.git.config(hostIncludeKey, credentialsConfigPath); yield this.git.config(hostIncludeKey, credentialsConfigPath);
// Configure host includeIf for worktrees // Configure host includeIf for worktrees
const hostWorktreeIncludeKey = `includeIf.gitdir:${gitDir}/worktrees/*.path`; const hostWorktreeIncludeKey = `${INCLUDE_IF_GITDIR}${gitDir}/worktrees/*.path`;
yield this.git.config(hostWorktreeIncludeKey, credentialsConfigPath); yield this.git.config(hostWorktreeIncludeKey, credentialsConfigPath);
// Container git directory // Container git directory
const workingDirectory = this.git.getWorkingDirectory(); const workingDirectory = this.git.getWorkingDirectory();
@@ -425,10 +431,10 @@ class GitAuthHelper {
// Container credentials config path // Container credentials config path
const containerCredentialsPath = path.posix.join('/github/runner_temp', path.basename(credentialsConfigPath)); const containerCredentialsPath = path.posix.join('/github/runner_temp', path.basename(credentialsConfigPath));
// Configure container includeIf // Configure container includeIf
const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`; const containerIncludeKey = `${INCLUDE_IF_GITDIR}${containerGitDir}.path`;
yield this.git.config(containerIncludeKey, containerCredentialsPath); yield this.git.config(containerIncludeKey, containerCredentialsPath);
// Configure container includeIf for worktrees // Configure container includeIf for worktrees
const containerWorktreeIncludeKey = `includeIf.gitdir:${containerGitDir}/worktrees/*.path`; const containerWorktreeIncludeKey = `${INCLUDE_IF_GITDIR}${containerGitDir}/worktrees/*.path`;
yield this.git.config(containerWorktreeIncludeKey, containerCredentialsPath); yield this.git.config(containerWorktreeIncludeKey, containerCredentialsPath);
} }
}); });
@@ -565,7 +571,7 @@ class GitAuthHelper {
const credentialsPaths = new Set(); const credentialsPaths = new Set();
try { try {
// Get all includeIf.gitdir keys // Get all includeIf.gitdir keys
const keys = yield this.git.tryGetConfigKeys('^includeIf\\.gitdir:', false, // globalConfig? const keys = yield this.git.tryGetConfigKeys('^includeIf\\.gitdir(/i)?:', false, // globalConfig?
configPath); configPath);
for (const key of keys) { for (const key of keys) {
// Get all values for this key // Get all values for this key
+13 -7
View File
@@ -13,6 +13,12 @@ import {IGitCommandManager} from './git-command-manager'
import {IGitSourceSettings} from './git-source-settings' import {IGitSourceSettings} from './git-source-settings'
const IS_WINDOWS = process.platform === 'win32' const IS_WINDOWS = process.platform === 'win32'
// Use case-insensitive gitdir matching on Windows to handle path casing mismatches
// between the runner's GITHUB_WORKSPACE and the actual filesystem casing.
// See: https://github.com/actions/checkout/issues/2345
const INCLUDE_IF_GITDIR = IS_WINDOWS
? 'includeIf.gitdir/i:'
: 'includeIf.gitdir:'
const SSH_COMMAND_KEY = 'core.sshCommand' const SSH_COMMAND_KEY = 'core.sshCommand'
export interface IGitAuthHelper { export interface IGitAuthHelper {
@@ -182,7 +188,7 @@ class GitAuthHelper {
// Configure host includeIf // Configure host includeIf
await this.git.config( await this.git.config(
`includeIf.gitdir:${submoduleGitDir}.path`, `${INCLUDE_IF_GITDIR}${submoduleGitDir}.path`,
credentialsConfigPath, credentialsConfigPath,
false, // globalConfig? false, // globalConfig?
false, // add? false, // add?
@@ -204,7 +210,7 @@ class GitAuthHelper {
// Configure container includeIf // Configure container includeIf
await this.git.config( await this.git.config(
`includeIf.gitdir:${containerSubmoduleGitDir}.path`, `${INCLUDE_IF_GITDIR}${containerSubmoduleGitDir}.path`,
containerCredentialsPath, containerCredentialsPath,
false, // globalConfig? false, // globalConfig?
false, // add? false, // add?
@@ -371,11 +377,11 @@ class GitAuthHelper {
gitDir = gitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows gitDir = gitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
// Configure host includeIf // Configure host includeIf
const hostIncludeKey = `includeIf.gitdir:${gitDir}.path` const hostIncludeKey = `${INCLUDE_IF_GITDIR}${gitDir}.path`
await this.git.config(hostIncludeKey, credentialsConfigPath) await this.git.config(hostIncludeKey, credentialsConfigPath)
// Configure host includeIf for worktrees // Configure host includeIf for worktrees
const hostWorktreeIncludeKey = `includeIf.gitdir:${gitDir}/worktrees/*.path` const hostWorktreeIncludeKey = `${INCLUDE_IF_GITDIR}${gitDir}/worktrees/*.path`
await this.git.config(hostWorktreeIncludeKey, credentialsConfigPath) await this.git.config(hostWorktreeIncludeKey, credentialsConfigPath)
// Container git directory // Container git directory
@@ -397,11 +403,11 @@ class GitAuthHelper {
) )
// Configure container includeIf // Configure container includeIf
const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path` const containerIncludeKey = `${INCLUDE_IF_GITDIR}${containerGitDir}.path`
await this.git.config(containerIncludeKey, containerCredentialsPath) await this.git.config(containerIncludeKey, containerCredentialsPath)
// Configure container includeIf for worktrees // Configure container includeIf for worktrees
const containerWorktreeIncludeKey = `includeIf.gitdir:${containerGitDir}/worktrees/*.path` const containerWorktreeIncludeKey = `${INCLUDE_IF_GITDIR}${containerGitDir}/worktrees/*.path`
await this.git.config( await this.git.config(
containerWorktreeIncludeKey, containerWorktreeIncludeKey,
containerCredentialsPath containerCredentialsPath
@@ -554,7 +560,7 @@ class GitAuthHelper {
try { try {
// Get all includeIf.gitdir keys // Get all includeIf.gitdir keys
const keys = await this.git.tryGetConfigKeys( const keys = await this.git.tryGetConfigKeys(
'^includeIf\\.gitdir:', '^includeIf\\.gitdir(/i)?:',
false, // globalConfig? false, // globalConfig?
configPath configPath
) )