Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions __test__/git-auth-helper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,79 @@ 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)

// Create a symlink pointing to the real workspace directory
const symlinkPath = path.join(path.dirname(workspace), 'workspace-symlink')
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 = fs
.realpathSync(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)

// Clean up symlink
await fs.promises.unlink(symlinkPath)
})

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 =
'sets SSH command env var when persist-credentials false'
it(setsSshCommandEnvVarWhenPersistCredentialsFalse, async () => {
Expand Down
15 changes: 12 additions & 3 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -406,9 +406,18 @@ 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');
gitDir = fs.realpathSync(constructed).replace(/\\/g, '/');
}
catch (_a) {
// Fall back to constructed path if realpathSync 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);
Expand Down
26 changes: 17 additions & 9 deletions src/git-auth-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import * as path from 'path'
import * as regexpHelper from './regexp-helper'
import * as stateHelper from './state-helper'
import * as urlHelper from './url-helper'
import {v4 as uuid} from 'uuid'
import {IGitCommandManager} from './git-command-manager'
import {IGitSourceSettings} from './git-source-settings'
import { v4 as uuid } from 'uuid'
import { IGitCommandManager } from './git-command-manager'
import { IGitSourceSettings } from './git-source-settings'

const IS_WINDOWS = process.platform === 'win32'
const SSH_COMMAND_KEY = 'core.sshCommand'
Expand Down Expand Up @@ -92,7 +92,7 @@ class GitAuthHelper {
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
const uniqueId = uuid()
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
const gitConfigPath = path.join(
Expand Down Expand Up @@ -258,11 +258,11 @@ class GitAuthHelper {
const uniqueId = uuid()
this.sshKeyPath = path.join(runnerTemp, uniqueId)
stateHelper.setSshKeyPath(this.sshKeyPath)
await fs.promises.mkdir(runnerTemp, {recursive: true})
await fs.promises.mkdir(runnerTemp, { recursive: true })
await fs.promises.writeFile(
this.sshKeyPath,
this.settings.sshKey.trim() + '\n',
{mode: 0o600}
{ mode: 0o600 }
)

// Remove inherited permissions on Windows
Expand Down Expand Up @@ -366,9 +366,17 @@ 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')
gitDir = fs.realpathSync(constructed).replace(/\\/g, '/')
} catch {
// Fall back to constructed path if realpathSync fails
gitDir = path.join(this.git.getWorkingDirectory(), '.git')
gitDir = gitDir.replace(/\\/g, '/')
}

// Configure host includeIf
const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`
Expand Down
Loading