This commit is contained in:
Drew Ballance 2026-03-25 12:54:58 -05:00 committed by GitHub
commit c400e00279
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 110 additions and 12 deletions

View File

@ -238,6 +238,85 @@ describe('git-auth-helper tests', () => {
expect(setSecretSpy).toHaveBeenCalledWith(expectedSecret) 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
;(git.getWorkingDirectory as jest.Mock).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 = (
await fs.promises.realpath(path.join(symlinkPath, '.git'))
).replace(/\\/g, '/')
const symlinkGitDir = path.join(symlinkPath, '.git').replace(/\\/g, '/')
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 non-existent path so realpathSync throws ENOENT naturally,
// exercising the catch fallback in configureToken()
const nonexistentPath = path.join(runnerTemp, 'does-not-exist')
;(git.getWorkingDirectory as jest.Mock).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 = path
.join(nonexistentPath, '.git')
.replace(/\\/g, '/')
expect(
localConfigContent.indexOf(`includeIf.gitdir:${fallbackGitDir}.path`)
).toBeGreaterThanOrEqual(0)
})
const setsSshCommandEnvVarWhenPersistCredentialsFalse = const setsSshCommandEnvVarWhenPersistCredentialsFalse =
'sets SSH command env var when persist-credentials false' 'sets SSH command env var when persist-credentials false'
it(setsSshCommandEnvVarWhenPersistCredentialsFalse, async () => { it(setsSshCommandEnvVarWhenPersistCredentialsFalse, async () => {

16
dist/index.js vendored
View File

@ -406,9 +406,19 @@ class GitAuthHelper {
); );
} }
else { else {
// Host git directory // Host git directory - resolve symlinks so includeIf gitdir matching works
let gitDir = path.join(this.git.getWorkingDirectory(), '.git'); // on self-hosted runners where _work is a symlink to an external volume.
gitDir = gitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows 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 // Configure host includeIf
const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`; const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`;
yield this.git.config(hostIncludeKey, credentialsConfigPath); yield this.git.config(hostIncludeKey, credentialsConfigPath);

View File

@ -8,9 +8,9 @@ import * as path from 'path'
import * as regexpHelper from './regexp-helper' import * as regexpHelper from './regexp-helper'
import * as stateHelper from './state-helper' import * as stateHelper from './state-helper'
import * as urlHelper from './url-helper' import * as urlHelper from './url-helper'
import {v4 as uuid} from 'uuid' import { v4 as uuid } from 'uuid'
import {IGitCommandManager} from './git-command-manager' 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'
const SSH_COMMAND_KEY = 'core.sshCommand' const SSH_COMMAND_KEY = 'core.sshCommand'
@ -92,7 +92,7 @@ class GitAuthHelper {
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined') assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
const uniqueId = uuid() const uniqueId = uuid()
this.temporaryHomePath = path.join(runnerTemp, uniqueId) this.temporaryHomePath = path.join(runnerTemp, uniqueId)
await fs.promises.mkdir(this.temporaryHomePath, {recursive: true}) await fs.promises.mkdir(this.temporaryHomePath, { recursive: true })
// Copy the global git config // Copy the global git config
const gitConfigPath = path.join( const gitConfigPath = path.join(
@ -258,11 +258,11 @@ class GitAuthHelper {
const uniqueId = uuid() const uniqueId = uuid()
this.sshKeyPath = path.join(runnerTemp, uniqueId) this.sshKeyPath = path.join(runnerTemp, uniqueId)
stateHelper.setSshKeyPath(this.sshKeyPath) stateHelper.setSshKeyPath(this.sshKeyPath)
await fs.promises.mkdir(runnerTemp, {recursive: true}) await fs.promises.mkdir(runnerTemp, { recursive: true })
await fs.promises.writeFile( await fs.promises.writeFile(
this.sshKeyPath, this.sshKeyPath,
this.settings.sshKey.trim() + '\n', this.settings.sshKey.trim() + '\n',
{mode: 0o600} { mode: 0o600 }
) )
// Remove inherited permissions on Windows // Remove inherited permissions on Windows
@ -366,9 +366,18 @@ class GitAuthHelper {
true // globalConfig? true // globalConfig?
) )
} else { } else {
// Host git directory // Host git directory - resolve symlinks so includeIf gitdir matching works
let gitDir = path.join(this.git.getWorkingDirectory(), '.git') // on self-hosted runners where _work is a symlink to an external volume.
gitDir = gitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows 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 // Configure host includeIf
const hostIncludeKey = `includeIf.gitdir:${gitDir}.path` const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`