diff --git a/__test__/git-auth-helper.test.ts b/__test__/git-auth-helper.test.ts index ad3566a..774d1eb 100644 --- a/__test__/git-auth-helper.test.ts +++ b/__test__/git-auth-helper.test.ts @@ -22,6 +22,11 @@ let settings: IGitSourceSettings let sshPath: string let githubServerUrl: string +// Helper function to normalize path separators to forward slashes +function convertBackslashes(file: string): string { + return file.replace(/\\/g, '/') +} + describe('git-auth-helper tests', () => { beforeAll(async () => { // SSH @@ -238,6 +243,87 @@ describe('git-auth-helper tests', () => { expect(setSecretSpy).toHaveBeenCalledWith(expectedSecret) }) + const configureAuth_resolvesSymlinksInIncludeIfGitdir = + 'configureAuth resolves symlinks in includeIf gitdir' + it(configureAuth_resolvesSymlinksInIncludeIfGitdir, async () => { + if (isWindows) { + process.stdout.write( + `Skipped test "${configureAuth_resolvesSymlinksInIncludeIfGitdir}". Symlink creation requires admin privileges on Windows.\n` + ) + return + } + + // Arrange + await setup(configureAuth_resolvesSymlinksInIncludeIfGitdir) + + const symlinkPath = path.join(path.dirname(workspace), 'workspace-symlink') + + try { + // Ensure no pre-existing symlink or file remains at this path + await fs.promises.rm(symlinkPath, {force: true}) + + // Create a symlink pointing to the real workspace directory + await fs.promises.symlink(workspace, symlinkPath) + + // Make git appear to be operating from the symlink path + const mockGetWorkingDirectory = git.getWorkingDirectory as jest.Mock + mockGetWorkingDirectory.mockReturnValue(symlinkPath) + process.env['GITHUB_WORKSPACE'] = symlinkPath + + const authHelper = gitAuthHelper.createAuthHelper(git, settings) + + // Act + await authHelper.configureAuth() + + // Assert the host includeIf uses the real resolved path, not the symlink path + const localConfigContent = ( + await fs.promises.readFile(localGitConfigPath) + ).toString() + const realGitDir = convertBackslashes( + await fs.promises.realpath(path.join(symlinkPath, '.git')) + ) + const symlinkGitDir = convertBackslashes(path.join(symlinkPath, '.git')) + + expect(realGitDir).not.toBe(symlinkGitDir) // sanity check: paths differ + expect( + localConfigContent.indexOf(`includeIf.gitdir:${realGitDir}.path`) + ).toBeGreaterThanOrEqual(0) + expect(localConfigContent.indexOf(symlinkGitDir)).toBeLessThan(0) + } finally { + // Clean up symlink (or any file) at the symlink path + await fs.promises.rm(symlinkPath, {force: true}) + } + }) + + const configureAuth_fallsBackWhenRealpathSyncFails = + 'configureAuth falls back to constructed path when realpathSync fails' + it(configureAuth_fallsBackWhenRealpathSyncFails, async () => { + // Arrange + await setup(configureAuth_fallsBackWhenRealpathSyncFails) + + // Use a nonexistent path so realpathSync throws ENOENT naturally, + // exercising the catch fallback in configureToken() + const nonexistentPath = path.join(runnerTemp, 'does-not-exist') + const mockGetWorkingDirectory = git.getWorkingDirectory as jest.Mock + mockGetWorkingDirectory.mockReturnValue(nonexistentPath) + + const authHelper = gitAuthHelper.createAuthHelper(git, settings) + + // Act - should not throw despite realpathSync failure + await authHelper.configureAuth() + + // Assert the fallback constructed path is used in the includeIf entry + const localConfigContent = ( + await fs.promises.readFile(localGitConfigPath) + ).toString() + const fallbackGitDir = convertBackslashes( + path.join(nonexistentPath, '.git') + ) + expect( + localConfigContent.indexOf(`includeIf.gitdir:${fallbackGitDir}.path`) + ).toBeGreaterThanOrEqual(0) + }) + const setsSshCommandEnvVarWhenPersistCredentialsFalse = 'sets SSH command env var when persist-credentials false' it(setsSshCommandEnvVarWhenPersistCredentialsFalse, async () => { diff --git a/dist/index.js b/dist/index.js index fe3f317..fd253d3 100644 --- a/dist/index.js +++ b/dist/index.js @@ -406,9 +406,19 @@ class GitAuthHelper { ); } else { - // Host git directory - let gitDir = path.join(this.git.getWorkingDirectory(), '.git'); - gitDir = gitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows + // Host git directory - resolve symlinks so includeIf gitdir matching works + // on self-hosted runners where _work is a symlink to an external volume. + let gitDir; + try { + const constructed = path.join(this.git.getWorkingDirectory(), '.git'); + const resolved = yield fs.promises.realpath(constructed); + gitDir = resolved.replace(/\\/g, '/'); + } + catch (_a) { + // Fall back to constructed path if realpath fails + gitDir = path.join(this.git.getWorkingDirectory(), '.git'); + gitDir = gitDir.replace(/\\/g, '/'); + } // Configure host includeIf const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`; yield this.git.config(hostIncludeKey, credentialsConfigPath); diff --git a/src/git-auth-helper.ts b/src/git-auth-helper.ts index e67db14..0ff743b 100644 --- a/src/git-auth-helper.ts +++ b/src/git-auth-helper.ts @@ -366,9 +366,18 @@ class GitAuthHelper { true // globalConfig? ) } else { - // Host git directory - let gitDir = path.join(this.git.getWorkingDirectory(), '.git') - gitDir = gitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows + // Host git directory - resolve symlinks so includeIf gitdir matching works + // on self-hosted runners where _work is a symlink to an external volume. + let gitDir: string + try { + const constructed = path.join(this.git.getWorkingDirectory(), '.git') + const resolved = await fs.promises.realpath(constructed) + gitDir = resolved.replace(/\\/g, '/') + } catch { + // Fall back to constructed path if realpath fails + gitDir = path.join(this.git.getWorkingDirectory(), '.git') + gitDir = gitDir.replace(/\\/g, '/') + } // Configure host includeIf const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`