From bcc5319a0be5e0bcae92d9051883ce7420c135ff Mon Sep 17 00:00:00 2001 From: eric sciple Date: Mon, 13 Oct 2025 21:48:22 +0000 Subject: [PATCH 01/10] Persist creds to a separate file --- __test__/git-auth-helper.test.ts | 117 +++++++++++++++++++++++++------ dist/index.js | 95 ++++++++++++++++++++----- src/git-auth-helper.ts | 110 +++++++++++++++++++++++------ src/git-command-manager.ts | 13 +++- 4 files changed, 272 insertions(+), 63 deletions(-) diff --git a/__test__/git-auth-helper.test.ts b/__test__/git-auth-helper.test.ts index 7633704..c08fb2d 100644 --- a/__test__/git-auth-helper.test.ts +++ b/__test__/git-auth-helper.test.ts @@ -86,16 +86,29 @@ describe('git-auth-helper tests', () => { // Act await authHelper.configureAuth() - // Assert config - const configContent = ( + // Assert config - check that .git/config contains includeIf entries + const localConfigContent = ( await fs.promises.readFile(localGitConfigPath) ).toString() + expect( + localConfigContent.indexOf('includeIf.gitdir:') + ).toBeGreaterThanOrEqual(0) + + // Assert credentials config file contains the actual credentials + const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter( + f => f.startsWith('git-credentials-') && f.endsWith('.config') + ) + expect(credentialsFiles.length).toBe(1) + const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0]) + const credentialsContent = ( + await fs.promises.readFile(credentialsConfigPath) + ).toString() const basicCredential = Buffer.from( `x-access-token:${settings.authToken}`, 'utf8' ).toString('base64') expect( - configContent.indexOf( + credentialsContent.indexOf( `http.${expectedServerUrl}/.extraheader AUTHORIZATION: basic ${basicCredential}` ) ).toBeGreaterThanOrEqual(0) @@ -120,7 +133,7 @@ describe('git-auth-helper tests', () => { 'inject https://github.com as github server url' it(configureAuth_AcceptsGitHubServerUrlSetToGHEC, async () => { await testAuthHeader( - configureAuth_AcceptsGitHubServerUrl, + configureAuth_AcceptsGitHubServerUrlSetToGHEC, 'https://github.com' ) }) @@ -141,12 +154,17 @@ describe('git-auth-helper tests', () => { // Act await authHelper.configureAuth() - // Assert config - const configContent = ( - await fs.promises.readFile(localGitConfigPath) + // Assert config - check credentials config file (not local .git/config) + const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter( + f => f.startsWith('git-credentials-') && f.endsWith('.config') + ) + expect(credentialsFiles.length).toBe(1) + const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0]) + const credentialsContent = ( + await fs.promises.readFile(credentialsConfigPath) ).toString() expect( - configContent.indexOf( + credentialsContent.indexOf( `http.https://github.com/.extraheader AUTHORIZATION` ) ).toBeGreaterThanOrEqual(0) @@ -251,13 +269,16 @@ describe('git-auth-helper tests', () => { expectedSshCommand ) - // Asserty git config + // Assert git config const gitConfigLines = (await fs.promises.readFile(localGitConfigPath)) .toString() .split('\n') .filter(x => x) - expect(gitConfigLines).toHaveLength(1) - expect(gitConfigLines[0]).toMatch(/^http\./) + // Should have includeIf entries pointing to credentials file + expect(gitConfigLines.length).toBeGreaterThan(0) + expect( + gitConfigLines.some(line => line.indexOf('includeIf.gitdir:') >= 0) + ).toBeTruthy() }) const configureAuth_setsSshCommandWhenPersistCredentialsTrue = @@ -419,8 +440,20 @@ describe('git-auth-helper tests', () => { expect( configContent.indexOf('value-from-global-config') ).toBeGreaterThanOrEqual(0) + // Global config should have include.path pointing to credentials file + expect(configContent.indexOf('include.path')).toBeGreaterThanOrEqual(0) + + // Check credentials in the separate config file + const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter( + f => f.startsWith('git-credentials-') && f.endsWith('.config') + ) + expect(credentialsFiles.length).toBeGreaterThan(0) + const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0]) + const credentialsContent = ( + await fs.promises.readFile(credentialsConfigPath) + ).toString() expect( - configContent.indexOf( + credentialsContent.indexOf( `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}` ) ).toBeGreaterThanOrEqual(0) @@ -463,8 +496,20 @@ describe('git-auth-helper tests', () => { const configContent = ( await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig')) ).toString() + // Global config should have include.path pointing to credentials file + expect(configContent.indexOf('include.path')).toBeGreaterThanOrEqual(0) + + // Check credentials in the separate config file + const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter( + f => f.startsWith('git-credentials-') && f.endsWith('.config') + ) + expect(credentialsFiles.length).toBeGreaterThan(0) + const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0]) + const credentialsContent = ( + await fs.promises.readFile(credentialsConfigPath) + ).toString() expect( - configContent.indexOf( + credentialsContent.indexOf( `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}` ) ).toBeGreaterThanOrEqual(0) @@ -660,19 +705,35 @@ describe('git-auth-helper tests', () => { await setup(removeAuth_removesToken) const authHelper = gitAuthHelper.createAuthHelper(git, settings) await authHelper.configureAuth() - let gitConfigContent = ( + + // Sanity check - verify includeIf entries exist in local config + let localConfigContent = ( await fs.promises.readFile(localGitConfigPath) ).toString() - expect(gitConfigContent.indexOf('http.')).toBeGreaterThanOrEqual(0) // sanity check + expect( + localConfigContent.indexOf('includeIf.gitdir:') + ).toBeGreaterThanOrEqual(0) + + // Sanity check - verify credentials file exists + let credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter( + f => f.startsWith('git-credentials-') && f.endsWith('.config') + ) + expect(credentialsFiles.length).toBe(1) // Act await authHelper.removeAuth() - // Assert git config - gitConfigContent = ( + // Assert includeIf entries removed from local git config + localConfigContent = ( await fs.promises.readFile(localGitConfigPath) ).toString() - expect(gitConfigContent.indexOf('http.')).toBeLessThan(0) + expect(localConfigContent.indexOf('includeIf.gitdir:')).toBeLessThan(0) + + // Assert credentials config file deleted + credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter( + f => f.startsWith('git-credentials-') && f.endsWith('.config') + ) + expect(credentialsFiles.length).toBe(0) }) const removeGlobalConfig_removesOverride = @@ -733,10 +794,20 @@ async function setup(testName: string): Promise { checkout: jest.fn(), checkoutDetach: jest.fn(), config: jest.fn( - async (key: string, value: string, globalConfig?: boolean) => { - const configPath = globalConfig - ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig') - : localGitConfigPath + async ( + key: string, + value: string, + globalConfig?: boolean, + add?: boolean, + configFile?: string + ) => { + const configPath = + configFile || + (globalConfig + ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig') + : localGitConfigPath) + // Ensure directory exists + await fs.promises.mkdir(path.dirname(configPath), {recursive: true}) await fs.promises.appendFile(configPath, `\n${key} ${value}`) } ), @@ -830,6 +901,7 @@ async function setup(testName: string): Promise { async function getActualSshKeyPath(): Promise { let actualTempFiles = (await fs.promises.readdir(runnerTemp)) + .filter(x => !x.startsWith('git-credentials-')) // Exclude credentials config file .sort() .map(x => path.join(runnerTemp, x)) if (actualTempFiles.length === 0) { @@ -843,6 +915,7 @@ async function getActualSshKeyPath(): Promise { async function getActualSshKnownHostsPath(): Promise { let actualTempFiles = (await fs.promises.readdir(runnerTemp)) + .filter(x => !x.startsWith('git-credentials-')) // Exclude credentials config file .sort() .map(x => path.join(runnerTemp, x)) if (actualTempFiles.length === 0) { diff --git a/dist/index.js b/dist/index.js index f3ae6f3..7ea5685 100644 --- a/dist/index.js +++ b/dist/index.js @@ -162,6 +162,8 @@ class GitAuthHelper { this.sshKeyPath = ''; this.sshKnownHostsPath = ''; this.temporaryHomePath = ''; + this.credentialsConfigPath = ''; // Path to separate credentials config file in RUNNER_TEMP + this.credentialsIncludeKeys = []; // Track includeIf/include config keys for cleanup this.git = gitCommandManager; this.settings = gitSourceSettings || {}; // Token auth header @@ -187,6 +189,20 @@ class GitAuthHelper { yield this.configureToken(); }); } + getCredentialsConfigPath() { + return __awaiter(this, void 0, void 0, function* () { + if (this.credentialsConfigPath) { + return this.credentialsConfigPath; + } + const runnerTemp = process.env['RUNNER_TEMP'] || ''; + assert.ok(runnerTemp, 'RUNNER_TEMP is not defined'); + // Create a unique filename for this checkout instance + const configFileName = `git-credentials-${(0, uuid_1.v4)()}.config`; + this.credentialsConfigPath = path.join(runnerTemp, configFileName); + core.debug(`Credentials config path: ${this.credentialsConfigPath}`); + return this.credentialsConfigPath; + }); + } configureTempGlobalConfig() { return __awaiter(this, void 0, void 0, function* () { var _a; @@ -229,10 +245,10 @@ class GitAuthHelper { configureGlobalAuth() { return __awaiter(this, void 0, void 0, function* () { // 'configureTempGlobalConfig' noops if already set, just returns the path - const newGitConfigPath = yield this.configureTempGlobalConfig(); + yield this.configureTempGlobalConfig(); try { // Configure the token - yield this.configureToken(newGitConfigPath, true); + yield this.configureToken(true); // Configure HTTPS instead of SSH yield this.git.tryConfigUnset(this.insteadOfKey, true); if (!this.settings.sshKey) { @@ -351,20 +367,45 @@ class GitAuthHelper { } }); } - configureToken(configPath, globalConfig) { + configureToken(globalConfig) { return __awaiter(this, void 0, void 0, function* () { - // Validate args - assert.ok((configPath && globalConfig) || (!configPath && !globalConfig), 'Unexpected configureToken parameter combinations'); - // Default config path - if (!configPath && !globalConfig) { - configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config'); + // Get the credentials config file path in RUNNER_TEMP + const credentialsConfigPath = yield this.getCredentialsConfigPath(); + // Write placeholder to the separate credentials config file using git config. + // This approach avoids the credential being captured by process creation audit events, + // which are commonly logged. For more information, refer to + // https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing + yield this.git.config(this.tokenConfigKey, this.tokenPlaceholderConfigValue, false, false, credentialsConfigPath); + // Replace the placeholder in the credentials config file + yield this.replaceTokenPlaceholder(credentialsConfigPath); + // Add include or includeIf to reference the credentials config + if (globalConfig) { + // For global config, use unconditional include. + // No need to track for cleanup since the temp .gitconfig file (which contains + // this include.path entry) gets deleted by removeGlobalConfig(). + yield this.git.config('include.path', credentialsConfigPath, true); + } + else { + // For local config, use includeIf.gitdir to match the .git directory. + // Configure for both host and container paths to support Docker container actions. + const gitDir = path.join(this.git.getWorkingDirectory(), '.git'); + const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`; + yield this.git.config(hostIncludeKey, credentialsConfigPath); + this.credentialsIncludeKeys.push(hostIncludeKey); + // Configure for container scenario where paths are mapped to fixed locations + const githubWorkspace = process.env['GITHUB_WORKSPACE']; + if (githubWorkspace) { + // Calculate the relative path of the working directory from GITHUB_WORKSPACE + const workingDirectory = this.git.getWorkingDirectory(); + const relativePath = path.relative(githubWorkspace, workingDirectory); + // Container paths: GITHUB_WORKSPACE -> /github/workspace, RUNNER_TEMP -> /github/runner_temp + const containerGitDir = path.posix.join('/github/workspace', relativePath, '.git'); + const containerCredentialsPath = path.posix.join('/github/runner_temp', path.basename(credentialsConfigPath)); + const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`; + yield this.git.config(containerIncludeKey, containerCredentialsPath); + this.credentialsIncludeKeys.push(containerIncludeKey); + } } - // Configure a placeholder value. This approach avoids the credential being captured - // by process creation audit events, which are commonly logged. For more information, - // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing - yield this.git.config(this.tokenConfigKey, this.tokenPlaceholderConfigValue, globalConfig); - // Replace the placeholder - yield this.replaceTokenPlaceholder(configPath || ''); }); } replaceTokenPlaceholder(configPath) { @@ -411,8 +452,24 @@ class GitAuthHelper { } removeToken() { return __awaiter(this, void 0, void 0, function* () { + var _a; // HTTP extra header yield this.removeGitConfig(this.tokenConfigKey); + // Remove include/includeIf config entries + for (const includeKey of this.credentialsIncludeKeys) { + yield this.removeGitConfig(includeKey); + } + this.credentialsIncludeKeys = []; + // Remove credentials config file + if (this.credentialsConfigPath) { + try { + yield io.rmRF(this.credentialsConfigPath); + } + catch (err) { + core.debug(`${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`); + core.warning(`Failed to remove credentials config '${this.credentialsConfigPath}'`); + } + } }); } removeGitConfig(configKey_1) { @@ -627,9 +684,15 @@ class GitCommandManager { yield this.execGit(args); }); } - config(configKey, configValue, globalConfig, add) { + config(configKey, configValue, globalConfig, add, configFile) { return __awaiter(this, void 0, void 0, function* () { - const args = ['config', globalConfig ? '--global' : '--local']; + const args = ['config']; + if (configFile) { + args.push('--file', configFile); + } + else { + args.push(globalConfig ? '--global' : '--local'); + } if (add) { args.push('--add'); } diff --git a/src/git-auth-helper.ts b/src/git-auth-helper.ts index 126e8e5..216e8b1 100644 --- a/src/git-auth-helper.ts +++ b/src/git-auth-helper.ts @@ -43,6 +43,8 @@ class GitAuthHelper { private sshKeyPath = '' private sshKnownHostsPath = '' private temporaryHomePath = '' + private credentialsConfigPath = '' // Path to separate credentials config file in RUNNER_TEMP + private credentialsIncludeKeys: string[] = [] // Track includeIf/include config keys for cleanup constructor( gitCommandManager: IGitCommandManager, @@ -81,6 +83,22 @@ class GitAuthHelper { await this.configureToken() } + private async getCredentialsConfigPath(): Promise { + if (this.credentialsConfigPath) { + return this.credentialsConfigPath + } + + const runnerTemp = process.env['RUNNER_TEMP'] || '' + assert.ok(runnerTemp, 'RUNNER_TEMP is not defined') + + // Create a unique filename for this checkout instance + const configFileName = `git-credentials-${uuid()}.config` + this.credentialsConfigPath = path.join(runnerTemp, configFileName) + + core.debug(`Credentials config path: ${this.credentialsConfigPath}`) + return this.credentialsConfigPath + } + async configureTempGlobalConfig(): Promise { // Already setup global config if (this.temporaryHomePath?.length > 0) { @@ -126,10 +144,10 @@ class GitAuthHelper { async configureGlobalAuth(): Promise { // 'configureTempGlobalConfig' noops if already set, just returns the path - const newGitConfigPath = await this.configureTempGlobalConfig() + await this.configureTempGlobalConfig() try { // Configure the token - await this.configureToken(newGitConfigPath, true) + await this.configureToken(true) // Configure HTTPS instead of SSH await this.git.tryConfigUnset(this.insteadOfKey, true) @@ -272,32 +290,62 @@ class GitAuthHelper { } } - private async configureToken( - configPath?: string, - globalConfig?: boolean - ): Promise { - // Validate args - assert.ok( - (configPath && globalConfig) || (!configPath && !globalConfig), - 'Unexpected configureToken parameter combinations' - ) + private async configureToken(globalConfig?: boolean): Promise { + // Get the credentials config file path in RUNNER_TEMP + const credentialsConfigPath = await this.getCredentialsConfigPath() - // Default config path - if (!configPath && !globalConfig) { - configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config') - } - - // Configure a placeholder value. This approach avoids the credential being captured - // by process creation audit events, which are commonly logged. For more information, - // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing + // Write placeholder to the separate credentials config file using git config. + // This approach avoids the credential being captured by process creation audit events, + // which are commonly logged. For more information, refer to + // https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing await this.git.config( this.tokenConfigKey, this.tokenPlaceholderConfigValue, - globalConfig + false, + false, + credentialsConfigPath ) - // Replace the placeholder - await this.replaceTokenPlaceholder(configPath || '') + // Replace the placeholder in the credentials config file + await this.replaceTokenPlaceholder(credentialsConfigPath) + + // Add include or includeIf to reference the credentials config + if (globalConfig) { + // For global config, use unconditional include. + // No need to track for cleanup since the temp .gitconfig file (which contains + // this include.path entry) gets deleted by removeGlobalConfig(). + await this.git.config('include.path', credentialsConfigPath, true) + } else { + // For local config, use includeIf.gitdir to match the .git directory. + // Configure for both host and container paths to support Docker container actions. + const gitDir = path.join(this.git.getWorkingDirectory(), '.git') + const hostIncludeKey = `includeIf.gitdir:${gitDir}.path` + await this.git.config(hostIncludeKey, credentialsConfigPath) + this.credentialsIncludeKeys.push(hostIncludeKey) + + // Configure for container scenario where paths are mapped to fixed locations + const githubWorkspace = process.env['GITHUB_WORKSPACE'] + if (githubWorkspace) { + // Calculate the relative path of the working directory from GITHUB_WORKSPACE + const workingDirectory = this.git.getWorkingDirectory() + const relativePath = path.relative(githubWorkspace, workingDirectory) + + // Container paths: GITHUB_WORKSPACE -> /github/workspace, RUNNER_TEMP -> /github/runner_temp + const containerGitDir = path.posix.join( + '/github/workspace', + relativePath, + '.git' + ) + const containerCredentialsPath = path.posix.join( + '/github/runner_temp', + path.basename(credentialsConfigPath) + ) + + const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path` + await this.git.config(containerIncludeKey, containerCredentialsPath) + this.credentialsIncludeKeys.push(containerIncludeKey) + } + } } private async replaceTokenPlaceholder(configPath: string): Promise { @@ -348,6 +396,24 @@ class GitAuthHelper { private async removeToken(): Promise { // HTTP extra header await this.removeGitConfig(this.tokenConfigKey) + + // Remove include/includeIf config entries + for (const includeKey of this.credentialsIncludeKeys) { + await this.removeGitConfig(includeKey) + } + this.credentialsIncludeKeys = [] + + // Remove credentials config file + if (this.credentialsConfigPath) { + try { + await io.rmRF(this.credentialsConfigPath) + } catch (err) { + core.debug(`${(err as any)?.message ?? err}`) + core.warning( + `Failed to remove credentials config '${this.credentialsConfigPath}'` + ) + } + } } private async removeGitConfig( diff --git a/src/git-command-manager.ts b/src/git-command-manager.ts index 8e42a38..0dfb11c 100644 --- a/src/git-command-manager.ts +++ b/src/git-command-manager.ts @@ -28,7 +28,8 @@ export interface IGitCommandManager { configKey: string, configValue: string, globalConfig?: boolean, - add?: boolean + add?: boolean, + configFile?: string ): Promise configExists(configKey: string, globalConfig?: boolean): Promise fetch( @@ -223,9 +224,15 @@ class GitCommandManager { configKey: string, configValue: string, globalConfig?: boolean, - add?: boolean + add?: boolean, + configFile?: string ): Promise { - const args: string[] = ['config', globalConfig ? '--global' : '--local'] + const args: string[] = ['config'] + if (configFile) { + args.push('--file', configFile) + } else { + args.push(globalConfig ? '--global' : '--local') + } if (add) { args.push('--add') } From d9b320ec703c510de925b29ba1e7e0ae32e2b6d3 Mon Sep 17 00:00:00 2001 From: eric sciple Date: Tue, 14 Oct 2025 18:39:36 +0000 Subject: [PATCH 02/10] . --- src/git-auth-helper.ts | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/src/git-auth-helper.ts b/src/git-auth-helper.ts index 216e8b1..9b8131d 100644 --- a/src/git-auth-helper.ts +++ b/src/git-auth-helper.ts @@ -318,33 +318,37 @@ class GitAuthHelper { } else { // For local config, use includeIf.gitdir to match the .git directory. // Configure for both host and container paths to support Docker container actions. - const gitDir = path.join(this.git.getWorkingDirectory(), '.git') + let gitDir = path.join(this.git.getWorkingDirectory(), '.git') + // Use forward slashes for git config, even on Windows + gitDir = gitDir.replace(/\\/g, '/') const hostIncludeKey = `includeIf.gitdir:${gitDir}.path` await this.git.config(hostIncludeKey, credentialsConfigPath) this.credentialsIncludeKeys.push(hostIncludeKey) // Configure for container scenario where paths are mapped to fixed locations const githubWorkspace = process.env['GITHUB_WORKSPACE'] - if (githubWorkspace) { - // Calculate the relative path of the working directory from GITHUB_WORKSPACE - const workingDirectory = this.git.getWorkingDirectory() - const relativePath = path.relative(githubWorkspace, workingDirectory) + assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined') + + // Calculate the relative path of the working directory from GITHUB_WORKSPACE + const workingDirectory = this.git.getWorkingDirectory() + let relativePath = path.relative(githubWorkspace, workingDirectory) - // Container paths: GITHUB_WORKSPACE -> /github/workspace, RUNNER_TEMP -> /github/runner_temp - const containerGitDir = path.posix.join( - '/github/workspace', - relativePath, - '.git' - ) - const containerCredentialsPath = path.posix.join( - '/github/runner_temp', - path.basename(credentialsConfigPath) - ) + // Container paths: GITHUB_WORKSPACE -> /github/workspace, RUNNER_TEMP -> /github/runner_temp + // Use forward slashes for git config + relativePath = relativePath.replace(/\\/g, '/') + const containerGitDir = path.posix.join( + '/github/workspace', + relativePath, + '.git' + ) + const containerCredentialsPath = path.posix.join( + '/github/runner_temp', + path.basename(credentialsConfigPath) + ) - const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path` - await this.git.config(containerIncludeKey, containerCredentialsPath) - this.credentialsIncludeKeys.push(containerIncludeKey) - } + const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path` + await this.git.config(containerIncludeKey, containerCredentialsPath) + this.credentialsIncludeKeys.push(containerIncludeKey) } } From 82257b56c286941ced7ff325d288c742ebe2850a Mon Sep 17 00:00:00 2001 From: eric sciple Date: Tue, 14 Oct 2025 18:55:51 +0000 Subject: [PATCH 03/10] . --- src/git-auth-helper.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/git-auth-helper.ts b/src/git-auth-helper.ts index 9b8131d..8ebe4dc 100644 --- a/src/git-auth-helper.ts +++ b/src/git-auth-helper.ts @@ -171,11 +171,13 @@ class GitAuthHelper { await this.removeGitConfig(this.insteadOfKey, true) if (this.settings.persistCredentials) { + // TODO: UPDATE THIS + // Configure a placeholder value. This approach avoids the credential being captured // by process creation audit events, which are commonly logged. For more information, // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing const output = await this.git.submoduleForeach( - // wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline + // Wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline `sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`, this.settings.nestedSubmodules ) @@ -311,14 +313,13 @@ class GitAuthHelper { // Add include or includeIf to reference the credentials config if (globalConfig) { - // For global config, use unconditional include. - // No need to track for cleanup since the temp .gitconfig file (which contains - // this include.path entry) gets deleted by removeGlobalConfig(). + // Global config file is temporary await this.git.config('include.path', credentialsConfigPath, true) } else { // For local config, use includeIf.gitdir to match the .git directory. // Configure for both host and container paths to support Docker container actions. let gitDir = path.join(this.git.getWorkingDirectory(), '.git') + console.log(`Git dir: ${gitDir}`) // Use forward slashes for git config, even on Windows gitDir = gitDir.replace(/\\/g, '/') const hostIncludeKey = `includeIf.gitdir:${gitDir}.path` From b13eccf3512f384df6cc0a1b2cad14ec9a2a6a0b Mon Sep 17 00:00:00 2001 From: eric sciple Date: Tue, 14 Oct 2025 19:07:14 +0000 Subject: [PATCH 04/10] . --- dist/index.js | 36 ++++++++++++++++++++---------------- src/git-auth-helper.ts | 1 + 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/dist/index.js b/dist/index.js index 7ea5685..b1bd84b 100644 --- a/dist/index.js +++ b/dist/index.js @@ -270,11 +270,12 @@ class GitAuthHelper { // Remove possible previous HTTPS instead of SSH yield this.removeGitConfig(this.insteadOfKey, true); if (this.settings.persistCredentials) { + // TODO: UPDATE THIS // Configure a placeholder value. This approach avoids the credential being captured // by process creation audit events, which are commonly logged. For more information, // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing const output = yield this.git.submoduleForeach( - // wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline + // Wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline `sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`, this.settings.nestedSubmodules); // Replace the placeholder const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []; @@ -380,31 +381,34 @@ class GitAuthHelper { yield this.replaceTokenPlaceholder(credentialsConfigPath); // Add include or includeIf to reference the credentials config if (globalConfig) { - // For global config, use unconditional include. - // No need to track for cleanup since the temp .gitconfig file (which contains - // this include.path entry) gets deleted by removeGlobalConfig(). + // Global config file is temporary yield this.git.config('include.path', credentialsConfigPath, true); } else { // For local config, use includeIf.gitdir to match the .git directory. // Configure for both host and container paths to support Docker container actions. - const gitDir = path.join(this.git.getWorkingDirectory(), '.git'); + let gitDir = path.join(this.git.getWorkingDirectory(), '.git'); + console.log(`Git dir: ${gitDir}`); + core.info(`Git dir: ${gitDir}`); + // Use forward slashes for git config, even on Windows + gitDir = gitDir.replace(/\\/g, '/'); const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`; yield this.git.config(hostIncludeKey, credentialsConfigPath); this.credentialsIncludeKeys.push(hostIncludeKey); // Configure for container scenario where paths are mapped to fixed locations const githubWorkspace = process.env['GITHUB_WORKSPACE']; - if (githubWorkspace) { - // Calculate the relative path of the working directory from GITHUB_WORKSPACE - const workingDirectory = this.git.getWorkingDirectory(); - const relativePath = path.relative(githubWorkspace, workingDirectory); - // Container paths: GITHUB_WORKSPACE -> /github/workspace, RUNNER_TEMP -> /github/runner_temp - const containerGitDir = path.posix.join('/github/workspace', relativePath, '.git'); - const containerCredentialsPath = path.posix.join('/github/runner_temp', path.basename(credentialsConfigPath)); - const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`; - yield this.git.config(containerIncludeKey, containerCredentialsPath); - this.credentialsIncludeKeys.push(containerIncludeKey); - } + assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined'); + // Calculate the relative path of the working directory from GITHUB_WORKSPACE + const workingDirectory = this.git.getWorkingDirectory(); + let relativePath = path.relative(githubWorkspace, workingDirectory); + // Container paths: GITHUB_WORKSPACE -> /github/workspace, RUNNER_TEMP -> /github/runner_temp + // Use forward slashes for git config + relativePath = relativePath.replace(/\\/g, '/'); + const containerGitDir = path.posix.join('/github/workspace', relativePath, '.git'); + const containerCredentialsPath = path.posix.join('/github/runner_temp', path.basename(credentialsConfigPath)); + const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`; + yield this.git.config(containerIncludeKey, containerCredentialsPath); + this.credentialsIncludeKeys.push(containerIncludeKey); } }); } diff --git a/src/git-auth-helper.ts b/src/git-auth-helper.ts index 8ebe4dc..8b60334 100644 --- a/src/git-auth-helper.ts +++ b/src/git-auth-helper.ts @@ -320,6 +320,7 @@ class GitAuthHelper { // Configure for both host and container paths to support Docker container actions. let gitDir = path.join(this.git.getWorkingDirectory(), '.git') console.log(`Git dir: ${gitDir}`) + core.info(`Git dir: ${gitDir}`) // Use forward slashes for git config, even on Windows gitDir = gitDir.replace(/\\/g, '/') const hostIncludeKey = `includeIf.gitdir:${gitDir}.path` From 74fe54f098825466930d5ebb65ba554df116bfe5 Mon Sep 17 00:00:00 2001 From: eric sciple Date: Tue, 14 Oct 2025 21:06:49 +0000 Subject: [PATCH 05/10] . --- src/git-auth-helper.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/git-auth-helper.ts b/src/git-auth-helper.ts index 8b60334..35e0ddf 100644 --- a/src/git-auth-helper.ts +++ b/src/git-auth-helper.ts @@ -319,8 +319,6 @@ class GitAuthHelper { // For local config, use includeIf.gitdir to match the .git directory. // Configure for both host and container paths to support Docker container actions. let gitDir = path.join(this.git.getWorkingDirectory(), '.git') - console.log(`Git dir: ${gitDir}`) - core.info(`Git dir: ${gitDir}`) // Use forward slashes for git config, even on Windows gitDir = gitDir.replace(/\\/g, '/') const hostIncludeKey = `includeIf.gitdir:${gitDir}.path` From 8e4be9ae1244fa3ca78fd2d317e8b4a80ddaf6d8 Mon Sep 17 00:00:00 2001 From: eric sciple Date: Tue, 14 Oct 2025 22:10:23 +0000 Subject: [PATCH 06/10] Add container path support for submodules and improve code readability --- dist/index.js | 43 ++++++++++++++++-------- src/git-auth-helper.ts | 75 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 91 insertions(+), 27 deletions(-) diff --git a/dist/index.js b/dist/index.js index b1bd84b..dddd213 100644 --- a/dist/index.js +++ b/dist/index.js @@ -270,18 +270,33 @@ class GitAuthHelper { // Remove possible previous HTTPS instead of SSH yield this.removeGitConfig(this.insteadOfKey, true); if (this.settings.persistCredentials) { - // TODO: UPDATE THIS - // Configure a placeholder value. This approach avoids the credential being captured - // by process creation audit events, which are commonly logged. For more information, - // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing - const output = yield this.git.submoduleForeach( - // Wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline - `sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`, this.settings.nestedSubmodules); - // Replace the placeholder - const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []; - for (const configPath of configPaths) { - core.debug(`Replacing token placeholder in '${configPath}'`); - yield this.replaceTokenPlaceholder(configPath); + // Use the same credentials config file created for the main repo + const credentialsConfigPath = yield this.getCredentialsConfigPath(); + const githubWorkspace = process.env['GITHUB_WORKSPACE']; + assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined'); + const containerCredentialsPath = path.posix.join('/github/runner_temp', path.basename(credentialsConfigPath)); + // Calculate container git directory base path + const workingDirectory = this.git.getWorkingDirectory(); + let relativePath = path.relative(githubWorkspace, workingDirectory); + relativePath = relativePath.replace(/\\/g, '/'); + const containerWorkspaceBase = path.posix.join('/github/workspace', relativePath); + // Get submodule paths. + // `git rev-parse --show-toplevel` returns the absolute path of each submodule's working tree. + const submodulePaths = yield this.git.submoduleForeach(`git rev-parse --show-toplevel`, this.settings.nestedSubmodules); + // For each submodule, configure includeIf entries pointing to the shared credentials file. + // Configure both host and container paths to support Docker container actions. + for (const submodulePath of submodulePaths.split('\n').filter(x => x)) { + // Configure host path includeIf. + // Use forward slashes for git config, even on Windows. + let submoduleGitDir = path.join(submodulePath, '.git'); + submoduleGitDir = submoduleGitDir.replace(/\\/g, '/'); + yield this.git.config(`includeIf.gitdir:${submoduleGitDir}.path`, credentialsConfigPath, false, false, path.join(submodulePath, '.git', 'config')); + // Configure container path includeIf. + // Use forward slashes for git config, even on Windows. + let submoduleRelativePath = path.relative(workingDirectory, submodulePath); + submoduleRelativePath = submoduleRelativePath.replace(/\\/g, '/'); + const containerSubmoduleGitDir = path.posix.join(containerWorkspaceBase, submoduleRelativePath, '.git'); + yield this.git.config(`includeIf.gitdir:${containerSubmoduleGitDir}.path`, containerCredentialsPath, false, false, path.join(submodulePath, '.git', 'config')); } if (this.settings.sshKey) { // Configure core.sshCommand @@ -388,8 +403,6 @@ class GitAuthHelper { // For local config, use includeIf.gitdir to match the .git directory. // Configure for both host and container paths to support Docker container actions. let gitDir = path.join(this.git.getWorkingDirectory(), '.git'); - console.log(`Git dir: ${gitDir}`); - core.info(`Git dir: ${gitDir}`); // Use forward slashes for git config, even on Windows gitDir = gitDir.replace(/\\/g, '/'); const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`; @@ -464,6 +477,8 @@ class GitAuthHelper { yield this.removeGitConfig(includeKey); } this.credentialsIncludeKeys = []; + // Remove includeIf entries from submodules + yield this.git.submoduleForeach(`sh -c "git config --local --get-regexp '^includeIf\\.' && git config --local --remove-section includeIf || :"`, true); // Remove credentials config file if (this.credentialsConfigPath) { try { diff --git a/src/git-auth-helper.ts b/src/git-auth-helper.ts index 35e0ddf..b7ac196 100644 --- a/src/git-auth-helper.ts +++ b/src/git-auth-helper.ts @@ -171,23 +171,66 @@ class GitAuthHelper { await this.removeGitConfig(this.insteadOfKey, true) if (this.settings.persistCredentials) { - // TODO: UPDATE THIS + // Use the same credentials config file created for the main repo + const credentialsConfigPath = await this.getCredentialsConfigPath() + const githubWorkspace = process.env['GITHUB_WORKSPACE'] + assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined') - // Configure a placeholder value. This approach avoids the credential being captured - // by process creation audit events, which are commonly logged. For more information, - // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing - const output = await this.git.submoduleForeach( - // Wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline - `sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`, + const containerCredentialsPath = path.posix.join( + '/github/runner_temp', + path.basename(credentialsConfigPath) + ) + + // Calculate container git directory base path + const workingDirectory = this.git.getWorkingDirectory() + let relativePath = path.relative(githubWorkspace, workingDirectory) + relativePath = relativePath.replace(/\\/g, '/') + const containerWorkspaceBase = path.posix.join( + '/github/workspace', + relativePath + ) + + // Get submodule paths. + // `git rev-parse --show-toplevel` returns the absolute path of each submodule's working tree. + const submodulePaths = await this.git.submoduleForeach( + `git rev-parse --show-toplevel`, this.settings.nestedSubmodules ) - // Replace the placeholder - const configPaths: string[] = - output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || [] - for (const configPath of configPaths) { - core.debug(`Replacing token placeholder in '${configPath}'`) - await this.replaceTokenPlaceholder(configPath) + // For each submodule, configure includeIf entries pointing to the shared credentials file. + // Configure both host and container paths to support Docker container actions. + for (const submodulePath of submodulePaths.split('\n').filter(x => x)) { + // Configure host path includeIf. + // Use forward slashes for git config, even on Windows. + let submoduleGitDir = path.join(submodulePath, '.git') + submoduleGitDir = submoduleGitDir.replace(/\\/g, '/') + await this.git.config( + `includeIf.gitdir:${submoduleGitDir}.path`, + credentialsConfigPath, + false, + false, + path.join(submodulePath, '.git', 'config') + ) + + // Configure container path includeIf. + // Use forward slashes for git config, even on Windows. + let submoduleRelativePath = path.relative( + workingDirectory, + submodulePath + ) + submoduleRelativePath = submoduleRelativePath.replace(/\\/g, '/') + const containerSubmoduleGitDir = path.posix.join( + containerWorkspaceBase, + submoduleRelativePath, + '.git' + ) + await this.git.config( + `includeIf.gitdir:${containerSubmoduleGitDir}.path`, + containerCredentialsPath, + false, + false, + path.join(submodulePath, '.git', 'config') + ) } if (this.settings.sshKey) { @@ -407,6 +450,12 @@ class GitAuthHelper { } this.credentialsIncludeKeys = [] + // Remove includeIf entries from submodules + await this.git.submoduleForeach( + `sh -c "git config --local --get-regexp '^includeIf\\.' && git config --local --remove-section includeIf || :"`, + true + ) + // Remove credentials config file if (this.credentialsConfigPath) { try { From a60fb6cabeb5b7d5054ef3e79ff58ab31a034001 Mon Sep 17 00:00:00 2001 From: eric sciple Date: Tue, 14 Oct 2025 22:24:46 +0000 Subject: [PATCH 07/10] Use git config --show-origin to reliably get submodule config paths --- dist/index.js | 16 ++++++++++------ src/git-auth-helper.ts | 20 +++++++++++++------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/dist/index.js b/dist/index.js index dddd213..36fb2d2 100644 --- a/dist/index.js +++ b/dist/index.js @@ -280,23 +280,27 @@ class GitAuthHelper { let relativePath = path.relative(githubWorkspace, workingDirectory); relativePath = relativePath.replace(/\\/g, '/'); const containerWorkspaceBase = path.posix.join('/github/workspace', relativePath); - // Get submodule paths. - // `git rev-parse --show-toplevel` returns the absolute path of each submodule's working tree. - const submodulePaths = yield this.git.submoduleForeach(`git rev-parse --show-toplevel`, this.settings.nestedSubmodules); + // Get submodule config file paths. + // Use `--show-origin` to get the config file path for each submodule. + const output = yield this.git.submoduleForeach(`git config --local --show-origin --name-only --get-regexp remote.origin.url`, this.settings.nestedSubmodules); + // Extract config file paths from the output (lines starting with "file:"). + const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []; // For each submodule, configure includeIf entries pointing to the shared credentials file. // Configure both host and container paths to support Docker container actions. - for (const submodulePath of submodulePaths.split('\n').filter(x => x)) { + for (const configPath of configPaths) { + // Get the submodule path from its config file path. + const submodulePath = path.dirname(path.dirname(configPath)); // Configure host path includeIf. // Use forward slashes for git config, even on Windows. let submoduleGitDir = path.join(submodulePath, '.git'); submoduleGitDir = submoduleGitDir.replace(/\\/g, '/'); - yield this.git.config(`includeIf.gitdir:${submoduleGitDir}.path`, credentialsConfigPath, false, false, path.join(submodulePath, '.git', 'config')); + yield this.git.config(`includeIf.gitdir:${submoduleGitDir}.path`, credentialsConfigPath, false, false, configPath); // Configure container path includeIf. // Use forward slashes for git config, even on Windows. let submoduleRelativePath = path.relative(workingDirectory, submodulePath); submoduleRelativePath = submoduleRelativePath.replace(/\\/g, '/'); const containerSubmoduleGitDir = path.posix.join(containerWorkspaceBase, submoduleRelativePath, '.git'); - yield this.git.config(`includeIf.gitdir:${containerSubmoduleGitDir}.path`, containerCredentialsPath, false, false, path.join(submodulePath, '.git', 'config')); + yield this.git.config(`includeIf.gitdir:${containerSubmoduleGitDir}.path`, containerCredentialsPath, false, false, configPath); } if (this.settings.sshKey) { // Configure core.sshCommand diff --git a/src/git-auth-helper.ts b/src/git-auth-helper.ts index b7ac196..4abc64e 100644 --- a/src/git-auth-helper.ts +++ b/src/git-auth-helper.ts @@ -190,16 +190,22 @@ class GitAuthHelper { relativePath ) - // Get submodule paths. - // `git rev-parse --show-toplevel` returns the absolute path of each submodule's working tree. - const submodulePaths = await this.git.submoduleForeach( - `git rev-parse --show-toplevel`, + // Get submodule config file paths. + // Use `--show-origin` to get the config file path for each submodule. + const output = await this.git.submoduleForeach( + `git config --local --show-origin --name-only --get-regexp remote.origin.url`, this.settings.nestedSubmodules ) + // Extract config file paths from the output (lines starting with "file:"). + const configPaths = + output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || [] + // For each submodule, configure includeIf entries pointing to the shared credentials file. // Configure both host and container paths to support Docker container actions. - for (const submodulePath of submodulePaths.split('\n').filter(x => x)) { + for (const configPath of configPaths) { + // Get the submodule path from its config file path. + const submodulePath = path.dirname(path.dirname(configPath)) // Configure host path includeIf. // Use forward slashes for git config, even on Windows. let submoduleGitDir = path.join(submodulePath, '.git') @@ -209,7 +215,7 @@ class GitAuthHelper { credentialsConfigPath, false, false, - path.join(submodulePath, '.git', 'config') + configPath ) // Configure container path includeIf. @@ -229,7 +235,7 @@ class GitAuthHelper { containerCredentialsPath, false, false, - path.join(submodulePath, '.git', 'config') + configPath ) } From 0f2eb6b1463fa88ed31f347c52112235aaece31a Mon Sep 17 00:00:00 2001 From: eric sciple Date: Tue, 14 Oct 2025 23:15:53 +0000 Subject: [PATCH 08/10] Split removeGitConfig, improve comments, fix tests, and set GITHUB_WORKSPACE in tests --- __test__/git-auth-helper.test.ts | 11 ++++- dist/index.js | 73 ++++++++++++++-------------- src/git-auth-helper.ts | 81 +++++++++++++++----------------- 3 files changed, 85 insertions(+), 80 deletions(-) diff --git a/__test__/git-auth-helper.test.ts b/__test__/git-auth-helper.test.ts index c08fb2d..15c0736 100644 --- a/__test__/git-auth-helper.test.ts +++ b/__test__/git-auth-helper.test.ts @@ -595,11 +595,14 @@ describe('git-auth-helper tests', () => { await authHelper.configureSubmoduleAuth() // Assert + // Should get submodule config paths (1 call) and configure insteadOf (2 calls for two values) expect(mockSubmoduleForeach).toHaveBeenCalledTimes(4) expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch( /unset-all.*insteadOf/ ) - expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/) + expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch( + /show-origin.*remote\.origin\.url/ + ) expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch( /url.*insteadOf.*git@github.com:/ ) @@ -634,11 +637,14 @@ describe('git-auth-helper tests', () => { await authHelper.configureSubmoduleAuth() // Assert + // Should get submodule config paths (1 call) and configure sshCommand (1 call) expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3) expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch( /unset-all.*insteadOf/ ) - expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/) + expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch( + /show-origin.*remote\.origin\.url/ + ) expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(/core\.sshCommand/) } ) @@ -776,6 +782,7 @@ async function setup(testName: string): Promise { await fs.promises.mkdir(tempHomedir, {recursive: true}) process.env['RUNNER_TEMP'] = runnerTemp process.env['HOME'] = tempHomedir + process.env['GITHUB_WORKSPACE'] = workspace // Create git config globalGitConfigPath = path.join(tempHomedir, '.gitconfig') diff --git a/dist/index.js b/dist/index.js index 36fb2d2..d95cb13 100644 --- a/dist/index.js +++ b/dist/index.js @@ -163,7 +163,7 @@ class GitAuthHelper { this.sshKnownHostsPath = ''; this.temporaryHomePath = ''; this.credentialsConfigPath = ''; // Path to separate credentials config file in RUNNER_TEMP - this.credentialsIncludeKeys = []; // Track includeIf/include config keys for cleanup + this.credentialsIncludeKeys = []; // Track includeIf config keys for cleanup this.git = gitCommandManager; this.settings = gitSourceSettings || {}; // Token auth header @@ -268,18 +268,19 @@ class GitAuthHelper { configureSubmoduleAuth() { return __awaiter(this, void 0, void 0, function* () { // Remove possible previous HTTPS instead of SSH - yield this.removeGitConfig(this.insteadOfKey, true); + yield this.removeSubmoduleGitConfig(this.insteadOfKey); if (this.settings.persistCredentials) { - // Use the same credentials config file created for the main repo + // Credentials config path const credentialsConfigPath = yield this.getCredentialsConfigPath(); + // Container credentials config path + const containerCredentialsPath = path.posix.join('/github/runner_temp', path.basename(credentialsConfigPath)); + // Container repo path + const workingDirectory = this.git.getWorkingDirectory(); const githubWorkspace = process.env['GITHUB_WORKSPACE']; assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined'); - const containerCredentialsPath = path.posix.join('/github/runner_temp', path.basename(credentialsConfigPath)); - // Calculate container git directory base path - const workingDirectory = this.git.getWorkingDirectory(); let relativePath = path.relative(githubWorkspace, workingDirectory); relativePath = relativePath.replace(/\\/g, '/'); - const containerWorkspaceBase = path.posix.join('/github/workspace', relativePath); + const containerRepoPath = path.posix.join('/github/workspace', relativePath); // Get submodule config file paths. // Use `--show-origin` to get the config file path for each submodule. const output = yield this.git.submoduleForeach(`git config --local --show-origin --name-only --get-regexp remote.origin.url`, this.settings.nestedSubmodules); @@ -288,18 +289,16 @@ class GitAuthHelper { // For each submodule, configure includeIf entries pointing to the shared credentials file. // Configure both host and container paths to support Docker container actions. for (const configPath of configPaths) { - // Get the submodule path from its config file path. + // Submodule path const submodulePath = path.dirname(path.dirname(configPath)); - // Configure host path includeIf. - // Use forward slashes for git config, even on Windows. + // Configure host includeIf let submoduleGitDir = path.join(submodulePath, '.git'); - submoduleGitDir = submoduleGitDir.replace(/\\/g, '/'); + submoduleGitDir = submoduleGitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows yield this.git.config(`includeIf.gitdir:${submoduleGitDir}.path`, credentialsConfigPath, false, false, configPath); - // Configure container path includeIf. - // Use forward slashes for git config, even on Windows. + // Configure container includeIf let submoduleRelativePath = path.relative(workingDirectory, submodulePath); - submoduleRelativePath = submoduleRelativePath.replace(/\\/g, '/'); - const containerSubmoduleGitDir = path.posix.join(containerWorkspaceBase, submoduleRelativePath, '.git'); + submoduleRelativePath = submoduleRelativePath.replace(/\\/g, '/'); // Use forward slashes, even on Windows + const containerSubmoduleGitDir = path.posix.join(containerRepoPath, submoduleRelativePath, '.git'); yield this.git.config(`includeIf.gitdir:${containerSubmoduleGitDir}.path`, containerCredentialsPath, false, false, configPath); } if (this.settings.sshKey) { @@ -404,25 +403,23 @@ class GitAuthHelper { yield this.git.config('include.path', credentialsConfigPath, true); } else { - // For local config, use includeIf.gitdir to match the .git directory. - // Configure for both host and container paths to support Docker container actions. + // Host git directory let gitDir = path.join(this.git.getWorkingDirectory(), '.git'); - // Use forward slashes for git config, even on Windows - gitDir = gitDir.replace(/\\/g, '/'); + gitDir = gitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows + // Configure host includeIf const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`; yield this.git.config(hostIncludeKey, credentialsConfigPath); this.credentialsIncludeKeys.push(hostIncludeKey); - // Configure for container scenario where paths are mapped to fixed locations + // Container git directory const githubWorkspace = process.env['GITHUB_WORKSPACE']; assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined'); - // Calculate the relative path of the working directory from GITHUB_WORKSPACE const workingDirectory = this.git.getWorkingDirectory(); let relativePath = path.relative(githubWorkspace, workingDirectory); - // Container paths: GITHUB_WORKSPACE -> /github/workspace, RUNNER_TEMP -> /github/runner_temp - // Use forward slashes for git config - relativePath = relativePath.replace(/\\/g, '/'); + relativePath = relativePath.replace(/\\/g, '/'); // Use forward slashes, even on Windows const containerGitDir = path.posix.join('/github/workspace', relativePath, '.git'); + // Container credentials config path const containerCredentialsPath = path.posix.join('/github/runner_temp', path.basename(credentialsConfigPath)); + // Configure container includeIf const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`; yield this.git.config(containerIncludeKey, containerCredentialsPath); this.credentialsIncludeKeys.push(containerIncludeKey); @@ -469,19 +466,21 @@ class GitAuthHelper { } // SSH command yield this.removeGitConfig(SSH_COMMAND_KEY); + yield this.removeSubmoduleGitConfig(SSH_COMMAND_KEY); }); } removeToken() { return __awaiter(this, void 0, void 0, function* () { var _a; - // HTTP extra header + // Remove HTTP extra header yield this.removeGitConfig(this.tokenConfigKey); - // Remove include/includeIf config entries + yield this.removeSubmoduleGitConfig(this.tokenConfigKey); + // Remove includeIf for (const includeKey of this.credentialsIncludeKeys) { yield this.removeGitConfig(includeKey); } this.credentialsIncludeKeys = []; - // Remove includeIf entries from submodules + // Remove submodule includeIf yield this.git.submoduleForeach(`sh -c "git config --local --get-regexp '^includeIf\\.' && git config --local --remove-section includeIf || :"`, true); // Remove credentials config file if (this.credentialsConfigPath) { @@ -495,18 +494,20 @@ class GitAuthHelper { } }); } - removeGitConfig(configKey_1) { - return __awaiter(this, arguments, void 0, function* (configKey, submoduleOnly = false) { - if (!submoduleOnly) { - if ((yield this.git.configExists(configKey)) && - !(yield this.git.tryConfigUnset(configKey))) { - // Load the config contents - core.warning(`Failed to remove '${configKey}' from the git config`); - } + removeGitConfig(configKey) { + return __awaiter(this, void 0, void 0, function* () { + if ((yield this.git.configExists(configKey)) && + !(yield this.git.tryConfigUnset(configKey))) { + // Load the config contents + core.warning(`Failed to remove '${configKey}' from the git config`); } + }); + } + removeSubmoduleGitConfig(configKey) { + return __awaiter(this, void 0, void 0, function* () { const pattern = regexpHelper.escape(configKey); yield this.git.submoduleForeach( - // wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline + // Wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline. `sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`, true); }); } diff --git a/src/git-auth-helper.ts b/src/git-auth-helper.ts index 4abc64e..6cf4ab1 100644 --- a/src/git-auth-helper.ts +++ b/src/git-auth-helper.ts @@ -44,7 +44,7 @@ class GitAuthHelper { private sshKnownHostsPath = '' private temporaryHomePath = '' private credentialsConfigPath = '' // Path to separate credentials config file in RUNNER_TEMP - private credentialsIncludeKeys: string[] = [] // Track includeIf/include config keys for cleanup + private credentialsIncludeKeys: string[] = [] // Track includeIf config keys for cleanup constructor( gitCommandManager: IGitCommandManager, @@ -168,24 +168,25 @@ class GitAuthHelper { async configureSubmoduleAuth(): Promise { // Remove possible previous HTTPS instead of SSH - await this.removeGitConfig(this.insteadOfKey, true) + await this.removeSubmoduleGitConfig(this.insteadOfKey) if (this.settings.persistCredentials) { - // Use the same credentials config file created for the main repo + // Credentials config path const credentialsConfigPath = await this.getCredentialsConfigPath() - const githubWorkspace = process.env['GITHUB_WORKSPACE'] - assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined') + // Container credentials config path const containerCredentialsPath = path.posix.join( '/github/runner_temp', path.basename(credentialsConfigPath) ) - // Calculate container git directory base path + // Container repo path const workingDirectory = this.git.getWorkingDirectory() + const githubWorkspace = process.env['GITHUB_WORKSPACE'] + assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined') let relativePath = path.relative(githubWorkspace, workingDirectory) relativePath = relativePath.replace(/\\/g, '/') - const containerWorkspaceBase = path.posix.join( + const containerRepoPath = path.posix.join( '/github/workspace', relativePath ) @@ -204,12 +205,12 @@ class GitAuthHelper { // For each submodule, configure includeIf entries pointing to the shared credentials file. // Configure both host and container paths to support Docker container actions. for (const configPath of configPaths) { - // Get the submodule path from its config file path. + // Submodule path const submodulePath = path.dirname(path.dirname(configPath)) - // Configure host path includeIf. - // Use forward slashes for git config, even on Windows. + + // Configure host includeIf let submoduleGitDir = path.join(submodulePath, '.git') - submoduleGitDir = submoduleGitDir.replace(/\\/g, '/') + submoduleGitDir = submoduleGitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows await this.git.config( `includeIf.gitdir:${submoduleGitDir}.path`, credentialsConfigPath, @@ -218,15 +219,14 @@ class GitAuthHelper { configPath ) - // Configure container path includeIf. - // Use forward slashes for git config, even on Windows. + // Configure container includeIf let submoduleRelativePath = path.relative( workingDirectory, submodulePath ) - submoduleRelativePath = submoduleRelativePath.replace(/\\/g, '/') + submoduleRelativePath = submoduleRelativePath.replace(/\\/g, '/') // Use forward slashes, even on Windows const containerSubmoduleGitDir = path.posix.join( - containerWorkspaceBase, + containerRepoPath, submoduleRelativePath, '.git' ) @@ -365,36 +365,34 @@ class GitAuthHelper { // Global config file is temporary await this.git.config('include.path', credentialsConfigPath, true) } else { - // For local config, use includeIf.gitdir to match the .git directory. - // Configure for both host and container paths to support Docker container actions. + // Host git directory let gitDir = path.join(this.git.getWorkingDirectory(), '.git') - // Use forward slashes for git config, even on Windows - gitDir = gitDir.replace(/\\/g, '/') + gitDir = gitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows + + // Configure host includeIf const hostIncludeKey = `includeIf.gitdir:${gitDir}.path` await this.git.config(hostIncludeKey, credentialsConfigPath) this.credentialsIncludeKeys.push(hostIncludeKey) - // Configure for container scenario where paths are mapped to fixed locations + // Container git directory const githubWorkspace = process.env['GITHUB_WORKSPACE'] assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined') - - // Calculate the relative path of the working directory from GITHUB_WORKSPACE const workingDirectory = this.git.getWorkingDirectory() let relativePath = path.relative(githubWorkspace, workingDirectory) - - // Container paths: GITHUB_WORKSPACE -> /github/workspace, RUNNER_TEMP -> /github/runner_temp - // Use forward slashes for git config - relativePath = relativePath.replace(/\\/g, '/') + relativePath = relativePath.replace(/\\/g, '/') // Use forward slashes, even on Windows const containerGitDir = path.posix.join( '/github/workspace', relativePath, '.git' ) + + // Container credentials config path const containerCredentialsPath = path.posix.join( '/github/runner_temp', path.basename(credentialsConfigPath) ) + // Configure container includeIf const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path` await this.git.config(containerIncludeKey, containerCredentialsPath) this.credentialsIncludeKeys.push(containerIncludeKey) @@ -444,19 +442,21 @@ class GitAuthHelper { // SSH command await this.removeGitConfig(SSH_COMMAND_KEY) + await this.removeSubmoduleGitConfig(SSH_COMMAND_KEY) } private async removeToken(): Promise { - // HTTP extra header + // Remove HTTP extra header await this.removeGitConfig(this.tokenConfigKey) + await this.removeSubmoduleGitConfig(this.tokenConfigKey) - // Remove include/includeIf config entries + // Remove includeIf for (const includeKey of this.credentialsIncludeKeys) { await this.removeGitConfig(includeKey) } this.credentialsIncludeKeys = [] - // Remove includeIf entries from submodules + // Remove submodule includeIf await this.git.submoduleForeach( `sh -c "git config --local --get-regexp '^includeIf\\.' && git config --local --remove-section includeIf || :"`, true @@ -475,23 +475,20 @@ class GitAuthHelper { } } - private async removeGitConfig( - configKey: string, - submoduleOnly: boolean = false - ): Promise { - if (!submoduleOnly) { - if ( - (await this.git.configExists(configKey)) && - !(await this.git.tryConfigUnset(configKey)) - ) { - // Load the config contents - core.warning(`Failed to remove '${configKey}' from the git config`) - } + private async removeGitConfig(configKey: string): Promise { + if ( + (await this.git.configExists(configKey)) && + !(await this.git.tryConfigUnset(configKey)) + ) { + // Load the config contents + core.warning(`Failed to remove '${configKey}' from the git config`) } + } + private async removeSubmoduleGitConfig(configKey: string): Promise { const pattern = regexpHelper.escape(configKey) await this.git.submoduleForeach( - // wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline + // Wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline. `sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`, true ) From 96c65894942921d85edebc65cf03fafa1c057023 Mon Sep 17 00:00:00 2001 From: eric sciple Date: Tue, 14 Oct 2025 23:56:34 +0000 Subject: [PATCH 09/10] Fix submodule git directory paths for includeIf --- dist/index.js | 15 +++++++-------- src/git-auth-helper.ts | 21 ++++++++------------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/dist/index.js b/dist/index.js index d95cb13..55e003a 100644 --- a/dist/index.js +++ b/dist/index.js @@ -289,16 +289,15 @@ class GitAuthHelper { // For each submodule, configure includeIf entries pointing to the shared credentials file. // Configure both host and container paths to support Docker container actions. for (const configPath of configPaths) { - // Submodule path - const submodulePath = path.dirname(path.dirname(configPath)); + // The config file is at .git/modules/submodule-name/config + let submoduleConfigDir = path.dirname(configPath); + submoduleConfigDir = submoduleConfigDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows // Configure host includeIf - let submoduleGitDir = path.join(submodulePath, '.git'); - submoduleGitDir = submoduleGitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows - yield this.git.config(`includeIf.gitdir:${submoduleGitDir}.path`, credentialsConfigPath, false, false, configPath); + yield this.git.config(`includeIf.gitdir:${submoduleConfigDir}.path`, credentialsConfigPath, false, false, configPath); // Configure container includeIf - let submoduleRelativePath = path.relative(workingDirectory, submodulePath); - submoduleRelativePath = submoduleRelativePath.replace(/\\/g, '/'); // Use forward slashes, even on Windows - const containerSubmoduleGitDir = path.posix.join(containerRepoPath, submoduleRelativePath, '.git'); + let relativeSubmoduleConfigDir = path.relative(githubWorkspace, submoduleConfigDir); + relativeSubmoduleConfigDir = relativeSubmoduleConfigDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows + const containerSubmoduleGitDir = path.posix.join('/github/workspace', relativeSubmoduleConfigDir); yield this.git.config(`includeIf.gitdir:${containerSubmoduleGitDir}.path`, containerCredentialsPath, false, false, configPath); } if (this.settings.sshKey) { diff --git a/src/git-auth-helper.ts b/src/git-auth-helper.ts index 6cf4ab1..1b16fba 100644 --- a/src/git-auth-helper.ts +++ b/src/git-auth-helper.ts @@ -205,14 +205,13 @@ class GitAuthHelper { // For each submodule, configure includeIf entries pointing to the shared credentials file. // Configure both host and container paths to support Docker container actions. for (const configPath of configPaths) { - // Submodule path - const submodulePath = path.dirname(path.dirname(configPath)) + // The config file is at .git/modules/submodule-name/config + let submoduleConfigDir = path.dirname(configPath) + submoduleConfigDir = submoduleConfigDir.replace(/\\/g, '/') // Use forward slashes, even on Windows // Configure host includeIf - let submoduleGitDir = path.join(submodulePath, '.git') - submoduleGitDir = submoduleGitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows await this.git.config( - `includeIf.gitdir:${submoduleGitDir}.path`, + `includeIf.gitdir:${submoduleConfigDir}.path`, credentialsConfigPath, false, false, @@ -220,15 +219,11 @@ class GitAuthHelper { ) // Configure container includeIf - let submoduleRelativePath = path.relative( - workingDirectory, - submodulePath - ) - submoduleRelativePath = submoduleRelativePath.replace(/\\/g, '/') // Use forward slashes, even on Windows + let relativeSubmoduleConfigDir = path.relative(githubWorkspace, submoduleConfigDir) + relativeSubmoduleConfigDir = relativeSubmoduleConfigDir.replace(/\\/g, '/') // Use forward slashes, even on Windows const containerSubmoduleGitDir = path.posix.join( - containerRepoPath, - submoduleRelativePath, - '.git' + '/github/workspace', + relativeSubmoduleConfigDir ) await this.git.config( `includeIf.gitdir:${containerSubmoduleGitDir}.path`, From 762bf756aaed6ab2a490f59c834d0a93e47993b5 Mon Sep 17 00:00:00 2001 From: eric sciple Date: Wed, 15 Oct 2025 00:13:45 +0000 Subject: [PATCH 10/10] Run prettier format --- src/git-auth-helper.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/git-auth-helper.ts b/src/git-auth-helper.ts index 1b16fba..b2872a1 100644 --- a/src/git-auth-helper.ts +++ b/src/git-auth-helper.ts @@ -219,8 +219,14 @@ class GitAuthHelper { ) // Configure container includeIf - let relativeSubmoduleConfigDir = path.relative(githubWorkspace, submoduleConfigDir) - relativeSubmoduleConfigDir = relativeSubmoduleConfigDir.replace(/\\/g, '/') // Use forward slashes, even on Windows + let relativeSubmoduleConfigDir = path.relative( + githubWorkspace, + submoduleConfigDir + ) + relativeSubmoduleConfigDir = relativeSubmoduleConfigDir.replace( + /\\/g, + '/' + ) // Use forward slashes, even on Windows const containerSubmoduleGitDir = path.posix.join( '/github/workspace', relativeSubmoduleConfigDir