From bcc5319a0be5e0bcae92d9051883ce7420c135ff Mon Sep 17 00:00:00 2001 From: eric sciple Date: Mon, 13 Oct 2025 21:48:22 +0000 Subject: [PATCH 01/22] 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/22] . --- 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/22] . --- 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/22] . --- 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/22] . --- 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/22] 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/22] 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/22] 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/22] 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/22] 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 From 6397f22a4f1d0ba0fd19fe6cde1d0f296199ee93 Mon Sep 17 00:00:00 2001 From: eric sciple Date: Wed, 15 Oct 2025 14:09:45 +0000 Subject: [PATCH 11/22] . --- src/git-auth-helper.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/git-auth-helper.ts b/src/git-auth-helper.ts index b2872a1..60cc187 100644 --- a/src/git-auth-helper.ts +++ b/src/git-auth-helper.ts @@ -205,13 +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) { - // 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 + // Submodule Git directory + 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 // Configure host includeIf await this.git.config( - `includeIf.gitdir:${submoduleConfigDir}.path`, + `includeIf.gitdir:${submoduleGitDir}.path`, credentialsConfigPath, false, false, @@ -219,17 +219,14 @@ class GitAuthHelper { ) // Configure container includeIf - let relativeSubmoduleConfigDir = path.relative( + let relativeSubmoduleGitDir = path.relative( githubWorkspace, - submoduleConfigDir + submoduleGitDir ) - relativeSubmoduleConfigDir = relativeSubmoduleConfigDir.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', - relativeSubmoduleConfigDir + relativeSubmoduleGitDir ) await this.git.config( `includeIf.gitdir:${containerSubmoduleGitDir}.path`, From aa7e6581cb27c8023e62b8f592b75a9de28e337a Mon Sep 17 00:00:00 2001 From: eric sciple Date: Wed, 15 Oct 2025 14:13:56 +0000 Subject: [PATCH 12/22] . --- __test__/verify-submodules-recursive.sh | 2 +- __test__/verify-submodules-true.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/__test__/verify-submodules-recursive.sh b/__test__/verify-submodules-recursive.sh index 1b68f9b..bdb803c 100755 --- a/__test__/verify-submodules-recursive.sh +++ b/__test__/verify-submodules-recursive.sh @@ -17,7 +17,7 @@ fi echo "Testing persisted credential" pushd ./submodules-recursive/submodule-level-1/submodule-level-2 -git config --local --name-only --get-regexp http.+extraheader && git fetch +git config --local --name-only --get-regexp '^includeIf\.' && git fetch if [ "$?" != "0" ]; then echo "Failed to validate persisted credential" popd diff --git a/__test__/verify-submodules-true.sh b/__test__/verify-submodules-true.sh index 43769fe..d2cd831 100755 --- a/__test__/verify-submodules-true.sh +++ b/__test__/verify-submodules-true.sh @@ -17,7 +17,7 @@ fi echo "Testing persisted credential" pushd ./submodules-true/submodule-level-1 -git config --local --name-only --get-regexp http.+extraheader && git fetch +git config --local --name-only --get-regexp '^includeIf\.' && git fetch if [ "$?" != "0" ]; then echo "Failed to validate persisted credential" popd From ff9f98e487023d637f21fe322c3fd28c34a8db52 Mon Sep 17 00:00:00 2001 From: eric sciple Date: Wed, 15 Oct 2025 15:20:00 +0000 Subject: [PATCH 13/22] . --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e62ac3b..0b1ad24 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -326,6 +326,6 @@ jobs: exit 1 fi - # needed to make checkout post cleanup succeed - - name: Fix Checkout - uses: actions/checkout@v4.1.6 + # # needed to make checkout post cleanup succeed + # - name: Fix Checkout + # uses: actions/checkout@v4.1.6 From 857facff5cb7a998974f1b9ac5c48f9440c33276 Mon Sep 17 00:00:00 2001 From: eric sciple Date: Wed, 15 Oct 2025 15:36:09 +0000 Subject: [PATCH 14/22] . --- .github/workflows/test.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0b1ad24..974e9a7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -299,15 +299,18 @@ jobs: test-output: runs-on: ubuntu-latest steps: - # Clone this repo + # Download the action at the current ref - name: Checkout uses: actions/checkout@v4.1.6 + with: + path: actions-checkout # Basic checkout using git - name: Checkout basic id: checkout - uses: ./ + uses: ./actions-checkout with: + path: cloned-using-local-action ref: test-data/v2/basic # Verify output From 2bcd7c6585690a2dd416c53b94b33565113e7081 Mon Sep 17 00:00:00 2001 From: eric sciple Date: Wed, 15 Oct 2025 15:38:31 +0000 Subject: [PATCH 15/22] . --- .github/workflows/test.yml | 4 ---- dist/index.js | 14 +++++++------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 974e9a7..daeacc9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -328,7 +328,3 @@ jobs: echo "Expected commit to be 82f71901cf8c021332310dcc8cdba84c4193ff5d" exit 1 fi - - # # needed to make checkout post cleanup succeed - # - name: Fix Checkout - # uses: actions/checkout@v4.1.6 diff --git a/dist/index.js b/dist/index.js index 55e003a..7c0a591 100644 --- a/dist/index.js +++ b/dist/index.js @@ -289,15 +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) { - // 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 + // Submodule Git directory + 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 // Configure host includeIf - yield this.git.config(`includeIf.gitdir:${submoduleConfigDir}.path`, credentialsConfigPath, false, false, configPath); + yield this.git.config(`includeIf.gitdir:${submoduleGitDir}.path`, credentialsConfigPath, false, false, configPath); // Configure container includeIf - let relativeSubmoduleConfigDir = path.relative(githubWorkspace, submoduleConfigDir); - relativeSubmoduleConfigDir = relativeSubmoduleConfigDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows - const containerSubmoduleGitDir = path.posix.join('/github/workspace', relativeSubmoduleConfigDir); + let relativeSubmoduleGitDir = path.relative(githubWorkspace, submoduleGitDir); + relativeSubmoduleGitDir = relativeSubmoduleGitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows + const containerSubmoduleGitDir = path.posix.join('/github/workspace', relativeSubmoduleGitDir); yield this.git.config(`includeIf.gitdir:${containerSubmoduleGitDir}.path`, containerCredentialsPath, false, false, configPath); } if (this.settings.sshKey) { From e4894fca20d13f959e14bd70dcb88446d867a802 Mon Sep 17 00:00:00 2001 From: eric sciple Date: Thu, 16 Oct 2025 21:58:52 +0000 Subject: [PATCH 16/22] . --- __test__/git-auth-helper.test.ts | 45 +++++++++++++++++ __test__/git-directory-helper.test.ts | 3 ++ __test__/verify-submodules-recursive.sh | 2 +- __test__/verify-submodules-true.sh | 2 +- dist/index.js | 67 +++++++++++++++++++++++-- src/git-auth-helper.ts | 26 ++++++++-- src/git-command-manager.ts | 64 +++++++++++++++++++++++ 7 files changed, 199 insertions(+), 10 deletions(-) diff --git a/__test__/git-auth-helper.test.ts b/__test__/git-auth-helper.test.ts index 15c0736..f8700ee 100644 --- a/__test__/git-auth-helper.test.ts +++ b/__test__/git-auth-helper.test.ts @@ -872,8 +872,53 @@ async function setup(testName: string): Promise { return true } ), + tryConfigUnsetValue: jest.fn( + async (key: string, value: string, globalConfig?: boolean): Promise => { + const configPath = globalConfig + ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig') + : localGitConfigPath + let content = await fs.promises.readFile(configPath) + let lines = content + .toString() + .split('\n') + .filter(x => x) + .filter(x => !(x.startsWith(key) && x.includes(value))) + await fs.promises.writeFile(configPath, lines.join('\n')) + return true + } + ), tryDisableAutomaticGarbageCollection: jest.fn(), tryGetFetchUrl: jest.fn(), + tryGetConfigValues: jest.fn( + async (key: string, globalConfig?: boolean): Promise => { + const configPath = globalConfig + ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig') + : localGitConfigPath + const content = await fs.promises.readFile(configPath) + const lines = content + .toString() + .split('\n') + .filter(x => x && x.startsWith(key)) + .map(x => x.substring(key.length).trim()) + return lines + } + ), + tryGetConfigKeys: jest.fn( + async (pattern: string, globalConfig?: boolean): Promise => { + const configPath = globalConfig + ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig') + : localGitConfigPath + const content = await fs.promises.readFile(configPath) + const lines = content + .toString() + .split('\n') + .filter(x => x) + const keys = lines + .filter(x => new RegExp(pattern).test(x.split(' ')[0])) + .map(x => x.split(' ')[0]) + return [...new Set(keys)] // Remove duplicates + } + ), tryReset: jest.fn(), version: jest.fn() } diff --git a/__test__/git-directory-helper.test.ts b/__test__/git-directory-helper.test.ts index 22e9ae6..d728e28 100644 --- a/__test__/git-directory-helper.test.ts +++ b/__test__/git-directory-helper.test.ts @@ -493,12 +493,15 @@ async function setup(testName: string): Promise { return true }), tryConfigUnset: jest.fn(), + tryConfigUnsetValue: jest.fn(), tryDisableAutomaticGarbageCollection: jest.fn(), tryGetFetchUrl: jest.fn(async () => { // Sanity check - this function shouldn't be called when the .git directory doesn't exist await fs.promises.stat(path.join(repositoryPath, '.git')) return repositoryUrl }), + tryGetConfigValues: jest.fn(), + tryGetConfigKeys: jest.fn(), tryReset: jest.fn(async () => { return true }), diff --git a/__test__/verify-submodules-recursive.sh b/__test__/verify-submodules-recursive.sh index bdb803c..1b68f9b 100755 --- a/__test__/verify-submodules-recursive.sh +++ b/__test__/verify-submodules-recursive.sh @@ -17,7 +17,7 @@ fi echo "Testing persisted credential" pushd ./submodules-recursive/submodule-level-1/submodule-level-2 -git config --local --name-only --get-regexp '^includeIf\.' && git fetch +git config --local --name-only --get-regexp http.+extraheader && git fetch if [ "$?" != "0" ]; then echo "Failed to validate persisted credential" popd diff --git a/__test__/verify-submodules-true.sh b/__test__/verify-submodules-true.sh index d2cd831..43769fe 100755 --- a/__test__/verify-submodules-true.sh +++ b/__test__/verify-submodules-true.sh @@ -17,7 +17,7 @@ fi echo "Testing persisted credential" pushd ./submodules-true/submodule-level-1 -git config --local --name-only --get-regexp '^includeIf\.' && git fetch +git config --local --name-only --get-regexp http.+extraheader && git fetch if [ "$?" != "0" ]; then echo "Failed to validate persisted credential" popd diff --git a/dist/index.js b/dist/index.js index 7c0a591..df77d86 100644 --- a/dist/index.js +++ b/dist/index.js @@ -474,11 +474,29 @@ class GitAuthHelper { // Remove HTTP extra header yield this.removeGitConfig(this.tokenConfigKey); yield this.removeSubmoduleGitConfig(this.tokenConfigKey); - // Remove includeIf - for (const includeKey of this.credentialsIncludeKeys) { - yield this.removeGitConfig(includeKey); + // Remove includeIf entries that point to git-credentials-*.config files + // This is more aggressive than tracking keys, but necessary since cleanup + // runs in a post-step where this.credentialsIncludeKeys is empty + try { + // Get all includeIf.gitdir keys + const keys = yield this.git.tryGetConfigKeys('^includeIf\\.gitdir:'); + for (const key of keys) { + // Get all values for this key + const values = yield this.git.tryGetConfigValues(key); + if (values.length > 0) { + // Remove only values that match git-credentials-.config pattern + for (const value of values) { + if (/git-credentials-[0-9a-f-]+\.config$/i.test(value)) { + yield this.git.tryConfigUnsetValue(key, value); + } + } + } + } + } + catch (err) { + // Ignore errors - this is cleanup code + core.debug(`Error during includeIf cleanup: ${err}`); } - this.credentialsIncludeKeys = []; // 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 @@ -922,6 +940,18 @@ class GitCommandManager { return output.exitCode === 0; }); } + tryConfigUnsetValue(configKey, configValue, globalConfig) { + return __awaiter(this, void 0, void 0, function* () { + const output = yield this.execGit([ + 'config', + globalConfig ? '--global' : '--local', + '--unset', + configKey, + configValue + ], true); + return output.exitCode === 0; + }); + } tryDisableAutomaticGarbageCollection() { return __awaiter(this, void 0, void 0, function* () { const output = yield this.execGit(['config', '--local', 'gc.auto', '0'], true); @@ -941,6 +971,35 @@ class GitCommandManager { return stdout; }); } + tryGetConfigValues(configKey, globalConfig) { + return __awaiter(this, void 0, void 0, function* () { + const output = yield this.execGit([ + 'config', + globalConfig ? '--global' : '--local', + '--get-all', + configKey + ], true); + if (output.exitCode !== 0) { + return []; + } + return output.stdout.trim().split('\n').filter(value => value.trim()); + }); + } + tryGetConfigKeys(pattern, globalConfig) { + return __awaiter(this, void 0, void 0, function* () { + const output = yield this.execGit([ + 'config', + globalConfig ? '--global' : '--local', + '--name-only', + '--get-regexp', + pattern + ], true); + if (output.exitCode !== 0) { + return []; + } + return output.stdout.trim().split('\n').filter(key => key.trim()); + }); + } tryReset() { return __awaiter(this, void 0, void 0, function* () { const output = yield this.execGit(['reset', '--hard', 'HEAD'], true); diff --git a/src/git-auth-helper.ts b/src/git-auth-helper.ts index 60cc187..a529041 100644 --- a/src/git-auth-helper.ts +++ b/src/git-auth-helper.ts @@ -448,11 +448,29 @@ class GitAuthHelper { await this.removeGitConfig(this.tokenConfigKey) await this.removeSubmoduleGitConfig(this.tokenConfigKey) - // Remove includeIf - for (const includeKey of this.credentialsIncludeKeys) { - await this.removeGitConfig(includeKey) + // Remove includeIf entries that point to git-credentials-*.config files + // This is more aggressive than tracking keys, but necessary since cleanup + // runs in a post-step where this.credentialsIncludeKeys is empty + try { + // Get all includeIf.gitdir keys + const keys = await this.git.tryGetConfigKeys('^includeIf\\.gitdir:') + + for (const key of keys) { + // Get all values for this key + const values = await this.git.tryGetConfigValues(key) + if (values.length > 0) { + // Remove only values that match git-credentials-.config pattern + for (const value of values) { + if (/git-credentials-[0-9a-f-]+\.config$/i.test(value)) { + await this.git.tryConfigUnsetValue(key, value) + } + } + } + } + } catch (err) { + // Ignore errors - this is cleanup code + core.debug(`Error during includeIf cleanup: ${err}`) } - this.credentialsIncludeKeys = [] // Remove submodule includeIf await this.git.submoduleForeach( diff --git a/src/git-command-manager.ts b/src/git-command-manager.ts index 0dfb11c..ed3220b 100644 --- a/src/git-command-manager.ts +++ b/src/git-command-manager.ts @@ -60,8 +60,11 @@ export interface IGitCommandManager { tagExists(pattern: string): Promise tryClean(): Promise tryConfigUnset(configKey: string, globalConfig?: boolean): Promise + tryConfigUnsetValue(configKey: string, configValue: string, globalConfig?: boolean): Promise tryDisableAutomaticGarbageCollection(): Promise tryGetFetchUrl(): Promise + tryGetConfigValues(configKey: string, globalConfig?: boolean): Promise + tryGetConfigKeys(pattern: string, globalConfig?: boolean): Promise tryReset(): Promise version(): Promise } @@ -462,6 +465,24 @@ class GitCommandManager { return output.exitCode === 0 } + async tryConfigUnsetValue( + configKey: string, + configValue: string, + globalConfig?: boolean + ): Promise { + const output = await this.execGit( + [ + 'config', + globalConfig ? '--global' : '--local', + '--unset', + configKey, + configValue + ], + true + ) + return output.exitCode === 0 + } + async tryDisableAutomaticGarbageCollection(): Promise { const output = await this.execGit( ['config', '--local', 'gc.auto', '0'], @@ -488,6 +509,49 @@ class GitCommandManager { return stdout } + async tryGetConfigValues( + configKey: string, + globalConfig?: boolean + ): Promise { + const output = await this.execGit( + [ + 'config', + globalConfig ? '--global' : '--local', + '--get-all', + configKey + ], + true + ) + + if (output.exitCode !== 0) { + return [] + } + + return output.stdout.trim().split('\n').filter(value => value.trim()) + } + + async tryGetConfigKeys( + pattern: string, + globalConfig?: boolean + ): Promise { + const output = await this.execGit( + [ + 'config', + globalConfig ? '--global' : '--local', + '--name-only', + '--get-regexp', + pattern + ], + true + ) + + if (output.exitCode !== 0) { + return [] + } + + return output.stdout.trim().split('\n').filter(key => key.trim()) + } + async tryReset(): Promise { const output = await this.execGit(['reset', '--hard', 'HEAD'], true) return output.exitCode === 0 From 3292e202f30df413bfb70907fa70c7d34bd69382 Mon Sep 17 00:00:00 2001 From: eric sciple Date: Thu, 16 Oct 2025 22:58:25 +0000 Subject: [PATCH 17/22] . --- dist/index.js | 2 +- src/git-auth-helper.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dist/index.js b/dist/index.js index df77d86..e865a0d 100644 --- a/dist/index.js +++ b/dist/index.js @@ -498,7 +498,7 @@ class GitAuthHelper { core.debug(`Error during includeIf cleanup: ${err}`); } // Remove submodule includeIf - yield this.git.submoduleForeach(`sh -c "git config --local --get-regexp '^includeIf\\.' && git config --local --remove-section includeIf || :"`, true); + 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 a529041..3576ba9 100644 --- a/src/git-auth-helper.ts +++ b/src/git-auth-helper.ts @@ -474,7 +474,7 @@ class GitAuthHelper { // Remove submodule includeIf await this.git.submoduleForeach( - `sh -c "git config --local --get-regexp '^includeIf\\.' && git config --local --remove-section includeIf || :"`, + `sh -c "git config --local --get-regexp '^includeif\\.' && git config --local --remove-section includeif || :"`, true ) From f8060825ea6d5d4e1f8f8464ed9d9d0380dc1e7b Mon Sep 17 00:00:00 2001 From: eric sciple Date: Fri, 17 Oct 2025 00:02:33 +0000 Subject: [PATCH 18/22] . --- __test__/git-auth-helper.test.ts | 41 +++-- __test__/git-directory-helper.test.ts | 1 + dist/index.js | 213 ++++++++++++++++---------- src/git-auth-helper.ts | 172 ++++++++++++--------- src/git-command-manager.ts | 87 ++++++----- 5 files changed, 320 insertions(+), 194 deletions(-) diff --git a/__test__/git-auth-helper.test.ts b/__test__/git-auth-helper.test.ts index f8700ee..1bcc140 100644 --- a/__test__/git-auth-helper.test.ts +++ b/__test__/git-auth-helper.test.ts @@ -595,18 +595,15 @@ 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) + // Should configure insteadOf (2 calls for two values) + expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3) expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch( /unset-all.*insteadOf/ ) expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch( - /show-origin.*remote\.origin\.url/ - ) - expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch( /url.*insteadOf.*git@github.com:/ ) - expect(mockSubmoduleForeach.mock.calls[3][0]).toMatch( + expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch( /url.*insteadOf.*org-123456@github.com:/ ) } @@ -637,15 +634,12 @@ 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) + // Should configure sshCommand (1 call) + expect(mockSubmoduleForeach).toHaveBeenCalledTimes(2) expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch( /unset-all.*insteadOf/ ) - expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch( - /show-origin.*remote\.origin\.url/ - ) - expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(/core\.sshCommand/) + expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/core\.sshCommand/) } ) @@ -768,6 +762,28 @@ describe('git-auth-helper tests', () => { } } }) + + const testCredentialsConfigPath_matchesCredentialsConfigPaths = + 'testCredentialsConfigPath matches credentials config paths' + it(testCredentialsConfigPath_matchesCredentialsConfigPaths, async () => { + // Arrange + await setup(testCredentialsConfigPath_matchesCredentialsConfigPaths) + const authHelper = gitAuthHelper.createAuthHelper(git, settings) + + // Get a real credentials config path + const credentialsConfigPath = await (authHelper as any).getCredentialsConfigPath() + + // Act & Assert + expect((authHelper as any).testCredentialsConfigPath(credentialsConfigPath)).toBe(true) + expect((authHelper as any).testCredentialsConfigPath('/some/path/git-credentials-12345678-abcd-1234-5678-123456789012.config')).toBe(true) + expect((authHelper as any).testCredentialsConfigPath('/some/path/git-credentials-abcdef12-3456-7890-abcd-ef1234567890.config')).toBe(true) + + // Test invalid paths + expect((authHelper as any).testCredentialsConfigPath('/some/path/other-config.config')).toBe(false) + expect((authHelper as any).testCredentialsConfigPath('/some/path/git-credentials-invalid.config')).toBe(false) + expect((authHelper as any).testCredentialsConfigPath('/some/path/git-credentials-.config')).toBe(false) + expect((authHelper as any).testCredentialsConfigPath('')).toBe(false) + }) }) async function setup(testName: string): Promise { @@ -834,6 +850,7 @@ async function setup(testName: string): Promise { env: {}, fetch: jest.fn(), getDefaultBranch: jest.fn(), + getSubmoduleConfigPaths: jest.fn(async () => []), getWorkingDirectory: jest.fn(() => workspace), init: jest.fn(), isDetached: jest.fn(), diff --git a/__test__/git-directory-helper.test.ts b/__test__/git-directory-helper.test.ts index d728e28..de79dc8 100644 --- a/__test__/git-directory-helper.test.ts +++ b/__test__/git-directory-helper.test.ts @@ -471,6 +471,7 @@ async function setup(testName: string): Promise { configExists: jest.fn(), fetch: jest.fn(), getDefaultBranch: jest.fn(), + getSubmoduleConfigPaths: jest.fn(async () => []), getWorkingDirectory: jest.fn(() => repositoryPath), init: jest.fn(), isDetached: jest.fn(), diff --git a/dist/index.js b/dist/index.js index e865a0d..3d2b9ca 100644 --- a/dist/index.js +++ b/dist/index.js @@ -163,7 +163,6 @@ class GitAuthHelper { this.sshKnownHostsPath = ''; this.temporaryHomePath = ''; this.credentialsConfigPath = ''; // Path to separate credentials config file in RUNNER_TEMP - this.credentialsIncludeKeys = []; // Track includeIf config keys for cleanup this.git = gitCommandManager; this.settings = gitSourceSettings || {}; // Token auth header @@ -189,20 +188,6 @@ 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; @@ -282,10 +267,7 @@ class GitAuthHelper { relativePath = relativePath.replace(/\\/g, '/'); 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); - // Extract config file paths from the output (lines starting with "file:"). - const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []; + const configPaths = yield this.git.getSubmoduleConfigPaths(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 configPath of configPaths) { @@ -329,6 +311,10 @@ class GitAuthHelper { } }); } + /** + * Configures SSH authentication by writing the SSH key and known hosts, + * and setting up the GIT_SSH_COMMAND environment variable. + */ configureSsh() { return __awaiter(this, void 0, void 0, function* () { if (!this.settings.sshKey) { @@ -385,6 +371,11 @@ class GitAuthHelper { } }); } + /** + * Configures token-based authentication by creating a credentials config file + * and setting up includeIf entries to reference it. + * @param globalConfig Whether to configure global config instead of local + */ configureToken(globalConfig) { return __awaiter(this, void 0, void 0, function* () { // Get the credentials config file path in RUNNER_TEMP @@ -395,7 +386,15 @@ class GitAuthHelper { // 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); + let content = (yield fs.promises.readFile(credentialsConfigPath)).toString(); + const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue); + if (placeholderIndex < 0 || + placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)) { + throw new Error(`Unable to replace auth placeholder in ${credentialsConfigPath}`); + } + assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined'); + content = content.replace(this.tokenPlaceholderConfigValue, this.tokenConfigValue); + yield fs.promises.writeFile(credentialsConfigPath, content); // Add include or includeIf to reference the credentials config if (globalConfig) { // Global config file is temporary @@ -408,7 +407,6 @@ class GitAuthHelper { // Configure host includeIf const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`; yield this.git.config(hostIncludeKey, credentialsConfigPath); - this.credentialsIncludeKeys.push(hostIncludeKey); // Container git directory const githubWorkspace = process.env['GITHUB_WORKSPACE']; assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined'); @@ -421,24 +419,31 @@ class GitAuthHelper { // Configure container includeIf const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`; yield this.git.config(containerIncludeKey, containerCredentialsPath); - this.credentialsIncludeKeys.push(containerIncludeKey); } }); } - replaceTokenPlaceholder(configPath) { + /** + * Gets or creates the path to the credentials config file in RUNNER_TEMP. + * @returns The absolute path to the credentials config file + */ + getCredentialsConfigPath() { return __awaiter(this, void 0, void 0, function* () { - assert.ok(configPath, 'configPath is not defined'); - let content = (yield fs.promises.readFile(configPath)).toString(); - const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue); - if (placeholderIndex < 0 || - placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)) { - throw new Error(`Unable to replace auth placeholder in ${configPath}`); + if (this.credentialsConfigPath) { + return this.credentialsConfigPath; } - assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined'); - content = content.replace(this.tokenPlaceholderConfigValue, this.tokenConfigValue); - yield fs.promises.writeFile(configPath, content); + 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; }); } + /** + * Removes SSH authentication configuration by cleaning up SSH keys, + * known hosts files, and SSH command configurations. + */ removeSsh() { return __awaiter(this, void 0, void 0, function* () { var _a; @@ -468,6 +473,10 @@ class GitAuthHelper { yield this.removeSubmoduleGitConfig(SSH_COMMAND_KEY); }); } + /** + * Removes token-based authentication by cleaning up HTTP headers, + * includeIf entries, and credentials config files. + */ removeToken() { return __awaiter(this, void 0, void 0, function* () { var _a; @@ -475,30 +484,12 @@ class GitAuthHelper { yield this.removeGitConfig(this.tokenConfigKey); yield this.removeSubmoduleGitConfig(this.tokenConfigKey); // Remove includeIf entries that point to git-credentials-*.config files - // This is more aggressive than tracking keys, but necessary since cleanup - // runs in a post-step where this.credentialsIncludeKeys is empty - try { - // Get all includeIf.gitdir keys - const keys = yield this.git.tryGetConfigKeys('^includeIf\\.gitdir:'); - for (const key of keys) { - // Get all values for this key - const values = yield this.git.tryGetConfigValues(key); - if (values.length > 0) { - // Remove only values that match git-credentials-.config pattern - for (const value of values) { - if (/git-credentials-[0-9a-f-]+\.config$/i.test(value)) { - yield this.git.tryConfigUnsetValue(key, value); - } - } - } - } + yield this.removeIncludeIfCredentials(); + // Remove submodule includeIf entries that point to git-credentials-*.config files + const submoduleConfigPaths = yield this.git.getSubmoduleConfigPaths(true); + for (const configPath of submoduleConfigPaths) { + yield this.removeIncludeIfCredentials(configPath); } - catch (err) { - // Ignore errors - this is cleanup code - core.debug(`Error during includeIf cleanup: ${err}`); - } - // 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) { try { @@ -511,6 +502,10 @@ class GitAuthHelper { } }); } + /** + * Removes a git config key from the local repository config. + * @param configKey The git config key to remove + */ removeGitConfig(configKey) { return __awaiter(this, void 0, void 0, function* () { if ((yield this.git.configExists(configKey)) && @@ -520,6 +515,10 @@ class GitAuthHelper { } }); } + /** + * Removes a git config key from all submodule configs. + * @param configKey The git config key to remove + */ removeSubmoduleGitConfig(configKey) { return __awaiter(this, void 0, void 0, function* () { const pattern = regexpHelper.escape(configKey); @@ -528,6 +527,47 @@ class GitAuthHelper { `sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`, true); }); } + /** + * Removes includeIf entries that point to git-credentials-*.config files. + * @param configPath Optional path to a specific git config file to operate on + */ + removeIncludeIfCredentials(configPath) { + return __awaiter(this, void 0, void 0, function* () { + try { + // Get all includeIf.gitdir keys + const keys = yield this.git.tryGetConfigKeys('^includeIf\\.gitdir:', false, configPath); + for (const key of keys) { + // Get all values for this key + const values = yield this.git.tryGetConfigValues(key, false, configPath); + if (values.length > 0) { + // Remove only values that match git-credentials-.config pattern + for (const value of values) { + if (this.testCredentialsConfigPath(value)) { + yield this.git.tryConfigUnsetValue(key, value, false, configPath); + } + } + } + } + } + catch (err) { + // Ignore errors - this is cleanup code + if (configPath) { + core.debug(`Error during includeIf cleanup for ${configPath}: ${err}`); + } + else { + core.debug(`Error during includeIf cleanup: ${err}`); + } + } + }); + } + /** + * Tests if a path matches the git-credentials-*.config pattern. + * @param path The path to test + * @returns True if the path matches the credentials config pattern + */ + testCredentialsConfigPath(path) { + return /git-credentials-[0-9a-f-]+\.config$/i.test(path); + } } @@ -810,6 +850,16 @@ class GitCommandManager { throw new Error('Unexpected output when retrieving default branch'); }); } + getSubmoduleConfigPaths(recursive) { + return __awaiter(this, void 0, void 0, function* () { + // Get submodule config file paths. + // Use `--show-origin` to get the config file path for each submodule. + const output = yield this.submoduleForeach(`git config --local --show-origin --name-only --get-regexp remote.origin.url`, recursive); + // Extract config file paths from the output (lines starting with "file:"). + const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []; + return configPaths; + }); + } getWorkingDirectory() { return this.workingDirectory; } @@ -940,15 +990,17 @@ class GitCommandManager { return output.exitCode === 0; }); } - tryConfigUnsetValue(configKey, configValue, globalConfig) { + tryConfigUnsetValue(configKey, configValue, globalConfig, configFile) { return __awaiter(this, void 0, void 0, function* () { - const output = yield this.execGit([ - 'config', - globalConfig ? '--global' : '--local', - '--unset', - configKey, - configValue - ], true); + const args = ['config']; + if (configFile) { + args.push('--file', configFile); + } + else { + args.push(globalConfig ? '--global' : '--local'); + } + args.push('--unset', configKey, configValue); + const output = yield this.execGit(args, true); return output.exitCode === 0; }); } @@ -971,29 +1023,34 @@ class GitCommandManager { return stdout; }); } - tryGetConfigValues(configKey, globalConfig) { + tryGetConfigValues(configKey, globalConfig, configFile) { return __awaiter(this, void 0, void 0, function* () { - const output = yield this.execGit([ - 'config', - globalConfig ? '--global' : '--local', - '--get-all', - configKey - ], true); + const args = ['config']; + if (configFile) { + args.push('--file', configFile); + } + else { + args.push(globalConfig ? '--global' : '--local'); + } + args.push('--get-all', configKey); + const output = yield this.execGit(args, true); if (output.exitCode !== 0) { return []; } return output.stdout.trim().split('\n').filter(value => value.trim()); }); } - tryGetConfigKeys(pattern, globalConfig) { + tryGetConfigKeys(pattern, globalConfig, configFile) { return __awaiter(this, void 0, void 0, function* () { - const output = yield this.execGit([ - 'config', - globalConfig ? '--global' : '--local', - '--name-only', - '--get-regexp', - pattern - ], true); + const args = ['config']; + if (configFile) { + args.push('--file', configFile); + } + else { + args.push(globalConfig ? '--global' : '--local'); + } + args.push('--name-only', '--get-regexp', pattern); + const output = yield this.execGit(args, true); if (output.exitCode !== 0) { return []; } diff --git a/src/git-auth-helper.ts b/src/git-auth-helper.ts index 3576ba9..245962e 100644 --- a/src/git-auth-helper.ts +++ b/src/git-auth-helper.ts @@ -44,7 +44,6 @@ class GitAuthHelper { private sshKnownHostsPath = '' private temporaryHomePath = '' private credentialsConfigPath = '' // Path to separate credentials config file in RUNNER_TEMP - private credentialsIncludeKeys: string[] = [] // Track includeIf config keys for cleanup constructor( gitCommandManager: IGitCommandManager, @@ -83,22 +82,6 @@ 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) { @@ -192,16 +175,10 @@ class GitAuthHelper { ) // 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`, + const configPaths = await this.git.getSubmoduleConfigPaths( 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 configPath of configPaths) { @@ -268,6 +245,10 @@ class GitAuthHelper { } } + /** + * Configures SSH authentication by writing the SSH key and known hosts, + * and setting up the GIT_SSH_COMMAND environment variable. + */ private async configureSsh(): Promise { if (!this.settings.sshKey) { return @@ -339,6 +320,11 @@ class GitAuthHelper { } } + /** + * Configures token-based authentication by creating a credentials config file + * and setting up includeIf entries to reference it. + * @param globalConfig Whether to configure global config instead of local + */ private async configureToken(globalConfig?: boolean): Promise { // Get the credentials config file path in RUNNER_TEMP const credentialsConfigPath = await this.getCredentialsConfigPath() @@ -356,7 +342,20 @@ class GitAuthHelper { ) // Replace the placeholder in the credentials config file - await this.replaceTokenPlaceholder(credentialsConfigPath) + let content = (await fs.promises.readFile(credentialsConfigPath)).toString() + const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue) + if ( + placeholderIndex < 0 || + placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue) + ) { + throw new Error(`Unable to replace auth placeholder in ${credentialsConfigPath}`) + } + assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined') + content = content.replace( + this.tokenPlaceholderConfigValue, + this.tokenConfigValue + ) + await fs.promises.writeFile(credentialsConfigPath, content) // Add include or includeIf to reference the credentials config if (globalConfig) { @@ -370,7 +369,6 @@ class GitAuthHelper { // Configure host includeIf const hostIncludeKey = `includeIf.gitdir:${gitDir}.path` await this.git.config(hostIncludeKey, credentialsConfigPath) - this.credentialsIncludeKeys.push(hostIncludeKey) // Container git directory const githubWorkspace = process.env['GITHUB_WORKSPACE'] @@ -393,28 +391,33 @@ class GitAuthHelper { // Configure container includeIf const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path` await this.git.config(containerIncludeKey, containerCredentialsPath) - this.credentialsIncludeKeys.push(containerIncludeKey) } } - private async replaceTokenPlaceholder(configPath: string): Promise { - assert.ok(configPath, 'configPath is not defined') - let content = (await fs.promises.readFile(configPath)).toString() - const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue) - if ( - placeholderIndex < 0 || - placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue) - ) { - throw new Error(`Unable to replace auth placeholder in ${configPath}`) + /** + * Gets or creates the path to the credentials config file in RUNNER_TEMP. + * @returns The absolute path to the credentials config file + */ + private async getCredentialsConfigPath(): Promise { + if (this.credentialsConfigPath) { + return this.credentialsConfigPath } - assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined') - content = content.replace( - this.tokenPlaceholderConfigValue, - this.tokenConfigValue - ) - await fs.promises.writeFile(configPath, content) + + 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 } + /** + * Removes SSH authentication configuration by cleaning up SSH keys, + * known hosts files, and SSH command configurations. + */ private async removeSsh(): Promise { // SSH key const keyPath = this.sshKeyPath || stateHelper.SshKeyPath @@ -443,40 +446,23 @@ class GitAuthHelper { await this.removeSubmoduleGitConfig(SSH_COMMAND_KEY) } + /** + * Removes token-based authentication by cleaning up HTTP headers, + * includeIf entries, and credentials config files. + */ private async removeToken(): Promise { // Remove HTTP extra header await this.removeGitConfig(this.tokenConfigKey) await this.removeSubmoduleGitConfig(this.tokenConfigKey) // Remove includeIf entries that point to git-credentials-*.config files - // This is more aggressive than tracking keys, but necessary since cleanup - // runs in a post-step where this.credentialsIncludeKeys is empty - try { - // Get all includeIf.gitdir keys - const keys = await this.git.tryGetConfigKeys('^includeIf\\.gitdir:') - - for (const key of keys) { - // Get all values for this key - const values = await this.git.tryGetConfigValues(key) - if (values.length > 0) { - // Remove only values that match git-credentials-.config pattern - for (const value of values) { - if (/git-credentials-[0-9a-f-]+\.config$/i.test(value)) { - await this.git.tryConfigUnsetValue(key, value) - } - } - } - } - } catch (err) { - // Ignore errors - this is cleanup code - core.debug(`Error during includeIf cleanup: ${err}`) - } + await this.removeIncludeIfCredentials() - // Remove submodule includeIf - await this.git.submoduleForeach( - `sh -c "git config --local --get-regexp '^includeif\\.' && git config --local --remove-section includeif || :"`, - true - ) + // Remove submodule includeIf entries that point to git-credentials-*.config files + const submoduleConfigPaths = await this.git.getSubmoduleConfigPaths(true) + for (const configPath of submoduleConfigPaths) { + await this.removeIncludeIfCredentials(configPath) + } // Remove credentials config file if (this.credentialsConfigPath) { @@ -491,6 +477,10 @@ class GitAuthHelper { } } + /** + * Removes a git config key from the local repository config. + * @param configKey The git config key to remove + */ private async removeGitConfig(configKey: string): Promise { if ( (await this.git.configExists(configKey)) && @@ -501,6 +491,10 @@ class GitAuthHelper { } } + /** + * Removes a git config key from all submodule configs. + * @param configKey The git config key to remove + */ private async removeSubmoduleGitConfig(configKey: string): Promise { const pattern = regexpHelper.escape(configKey) await this.git.submoduleForeach( @@ -509,4 +503,44 @@ class GitAuthHelper { true ) } + + /** + * Removes includeIf entries that point to git-credentials-*.config files. + * @param configPath Optional path to a specific git config file to operate on + */ + private async removeIncludeIfCredentials(configPath?: string): Promise { + try { + // Get all includeIf.gitdir keys + const keys = await this.git.tryGetConfigKeys('^includeIf\\.gitdir:', false, configPath) + + for (const key of keys) { + // Get all values for this key + const values = await this.git.tryGetConfigValues(key, false, configPath) + if (values.length > 0) { + // Remove only values that match git-credentials-.config pattern + for (const value of values) { + if (this.testCredentialsConfigPath(value)) { + await this.git.tryConfigUnsetValue(key, value, false, configPath) + } + } + } + } + } catch (err) { + // Ignore errors - this is cleanup code + if (configPath) { + core.debug(`Error during includeIf cleanup for ${configPath}: ${err}`) + } else { + core.debug(`Error during includeIf cleanup: ${err}`) + } + } + } + + /** + * Tests if a path matches the git-credentials-*.config pattern. + * @param path The path to test + * @returns True if the path matches the credentials config pattern + */ + private testCredentialsConfigPath(path: string): boolean { + return /git-credentials-[0-9a-f-]+\.config$/i.test(path) + } } diff --git a/src/git-command-manager.ts b/src/git-command-manager.ts index ed3220b..71a5e9e 100644 --- a/src/git-command-manager.ts +++ b/src/git-command-manager.ts @@ -42,6 +42,7 @@ export interface IGitCommandManager { } ): Promise getDefaultBranch(repositoryUrl: string): Promise + getSubmoduleConfigPaths(recursive: boolean): Promise getWorkingDirectory(): string init(): Promise isDetached(): Promise @@ -60,11 +61,11 @@ export interface IGitCommandManager { tagExists(pattern: string): Promise tryClean(): Promise tryConfigUnset(configKey: string, globalConfig?: boolean): Promise - tryConfigUnsetValue(configKey: string, configValue: string, globalConfig?: boolean): Promise + tryConfigUnsetValue(configKey: string, configValue: string, globalConfig?: boolean, configFile?: string): Promise tryDisableAutomaticGarbageCollection(): Promise tryGetFetchUrl(): Promise - tryGetConfigValues(configKey: string, globalConfig?: boolean): Promise - tryGetConfigKeys(pattern: string, globalConfig?: boolean): Promise + tryGetConfigValues(configKey: string, globalConfig?: boolean, configFile?: string): Promise + tryGetConfigKeys(pattern: string, globalConfig?: boolean, configFile?: string): Promise tryReset(): Promise version(): Promise } @@ -333,6 +334,21 @@ class GitCommandManager { throw new Error('Unexpected output when retrieving default branch') } + async getSubmoduleConfigPaths(recursive: boolean): Promise { + // Get submodule config file paths. + // Use `--show-origin` to get the config file path for each submodule. + const output = await this.submoduleForeach( + `git config --local --show-origin --name-only --get-regexp remote.origin.url`, + recursive + ) + + // Extract config file paths from the output (lines starting with "file:"). + const configPaths = + output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || [] + + return configPaths + } + getWorkingDirectory(): string { return this.workingDirectory } @@ -468,18 +484,18 @@ class GitCommandManager { async tryConfigUnsetValue( configKey: string, configValue: string, - globalConfig?: boolean + globalConfig?: boolean, + configFile?: string ): Promise { - const output = await this.execGit( - [ - 'config', - globalConfig ? '--global' : '--local', - '--unset', - configKey, - configValue - ], - true - ) + const args = ['config'] + if (configFile) { + args.push('--file', configFile) + } else { + args.push(globalConfig ? '--global' : '--local') + } + args.push('--unset', configKey, configValue) + + const output = await this.execGit(args, true) return output.exitCode === 0 } @@ -511,17 +527,18 @@ class GitCommandManager { async tryGetConfigValues( configKey: string, - globalConfig?: boolean + globalConfig?: boolean, + configFile?: string ): Promise { - const output = await this.execGit( - [ - 'config', - globalConfig ? '--global' : '--local', - '--get-all', - configKey - ], - true - ) + const args = ['config'] + if (configFile) { + args.push('--file', configFile) + } else { + args.push(globalConfig ? '--global' : '--local') + } + args.push('--get-all', configKey) + + const output = await this.execGit(args, true) if (output.exitCode !== 0) { return [] @@ -532,18 +549,18 @@ class GitCommandManager { async tryGetConfigKeys( pattern: string, - globalConfig?: boolean + globalConfig?: boolean, + configFile?: string ): Promise { - const output = await this.execGit( - [ - 'config', - globalConfig ? '--global' : '--local', - '--name-only', - '--get-regexp', - pattern - ], - true - ) + const args = ['config'] + if (configFile) { + args.push('--file', configFile) + } else { + args.push(globalConfig ? '--global' : '--local') + } + args.push('--name-only', '--get-regexp', pattern) + + const output = await this.execGit(args, true) if (output.exitCode !== 0) { return [] From c9518fb4085560fb070febbe36aa2b70cba22e0b Mon Sep 17 00:00:00 2001 From: eric sciple Date: Fri, 17 Oct 2025 00:18:04 +0000 Subject: [PATCH 19/22] . --- dist/index.js | 20 ++++++++++++++------ src/git-auth-helper.ts | 25 ++++++++++++++++++------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/dist/index.js b/dist/index.js index 3d2b9ca..5725651 100644 --- a/dist/index.js +++ b/dist/index.js @@ -483,21 +483,25 @@ class GitAuthHelper { // Remove HTTP extra header yield this.removeGitConfig(this.tokenConfigKey); yield this.removeSubmoduleGitConfig(this.tokenConfigKey); + // Collect credentials config paths that need to be removed + const credentialsPaths = new Set(); // Remove includeIf entries that point to git-credentials-*.config files - yield this.removeIncludeIfCredentials(); + const mainCredentialsPaths = yield this.removeIncludeIfCredentials(); + mainCredentialsPaths.forEach(path => credentialsPaths.add(path)); // Remove submodule includeIf entries that point to git-credentials-*.config files const submoduleConfigPaths = yield this.git.getSubmoduleConfigPaths(true); for (const configPath of submoduleConfigPaths) { - yield this.removeIncludeIfCredentials(configPath); + const submoduleCredentialsPaths = yield this.removeIncludeIfCredentials(configPath); + submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path)); } - // Remove credentials config file - if (this.credentialsConfigPath) { + // Remove credentials config files + for (const credentialsPath of credentialsPaths) { try { - yield io.rmRF(this.credentialsConfigPath); + yield io.rmRF(credentialsPath); } 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}'`); + core.warning(`Failed to remove credentials config '${credentialsPath}'`); } } }); @@ -530,9 +534,11 @@ class GitAuthHelper { /** * Removes includeIf entries that point to git-credentials-*.config files. * @param configPath Optional path to a specific git config file to operate on + * @returns Array of unique credentials config file paths that were found and removed */ removeIncludeIfCredentials(configPath) { return __awaiter(this, void 0, void 0, function* () { + const credentialsPaths = new Set(); try { // Get all includeIf.gitdir keys const keys = yield this.git.tryGetConfigKeys('^includeIf\\.gitdir:', false, configPath); @@ -543,6 +549,7 @@ class GitAuthHelper { // Remove only values that match git-credentials-.config pattern for (const value of values) { if (this.testCredentialsConfigPath(value)) { + credentialsPaths.add(value); yield this.git.tryConfigUnsetValue(key, value, false, configPath); } } @@ -558,6 +565,7 @@ class GitAuthHelper { core.debug(`Error during includeIf cleanup: ${err}`); } } + return Array.from(credentialsPaths); }); } /** diff --git a/src/git-auth-helper.ts b/src/git-auth-helper.ts index 245962e..f7c8d8a 100644 --- a/src/git-auth-helper.ts +++ b/src/git-auth-helper.ts @@ -455,23 +455,28 @@ class GitAuthHelper { await this.removeGitConfig(this.tokenConfigKey) await this.removeSubmoduleGitConfig(this.tokenConfigKey) + // Collect credentials config paths that need to be removed + const credentialsPaths = new Set() + // Remove includeIf entries that point to git-credentials-*.config files - await this.removeIncludeIfCredentials() + const mainCredentialsPaths = await this.removeIncludeIfCredentials() + mainCredentialsPaths.forEach(path => credentialsPaths.add(path)) // Remove submodule includeIf entries that point to git-credentials-*.config files const submoduleConfigPaths = await this.git.getSubmoduleConfigPaths(true) for (const configPath of submoduleConfigPaths) { - await this.removeIncludeIfCredentials(configPath) + const submoduleCredentialsPaths = await this.removeIncludeIfCredentials(configPath) + submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path)) } - // Remove credentials config file - if (this.credentialsConfigPath) { + // Remove credentials config files + for (const credentialsPath of credentialsPaths) { try { - await io.rmRF(this.credentialsConfigPath) + await io.rmRF(credentialsPath) } catch (err) { core.debug(`${(err as any)?.message ?? err}`) core.warning( - `Failed to remove credentials config '${this.credentialsConfigPath}'` + `Failed to remove credentials config '${credentialsPath}'` ) } } @@ -507,8 +512,11 @@ class GitAuthHelper { /** * Removes includeIf entries that point to git-credentials-*.config files. * @param configPath Optional path to a specific git config file to operate on + * @returns Array of unique credentials config file paths that were found and removed */ - private async removeIncludeIfCredentials(configPath?: string): Promise { + private async removeIncludeIfCredentials(configPath?: string): Promise { + const credentialsPaths = new Set() + try { // Get all includeIf.gitdir keys const keys = await this.git.tryGetConfigKeys('^includeIf\\.gitdir:', false, configPath) @@ -520,6 +528,7 @@ class GitAuthHelper { // Remove only values that match git-credentials-.config pattern for (const value of values) { if (this.testCredentialsConfigPath(value)) { + credentialsPaths.add(value) await this.git.tryConfigUnsetValue(key, value, false, configPath) } } @@ -533,6 +542,8 @@ class GitAuthHelper { core.debug(`Error during includeIf cleanup: ${err}`) } } + + return Array.from(credentialsPaths) } /** From dc519229d3bf63fba02c3d2082153bd4ed299ca6 Mon Sep 17 00:00:00 2001 From: eric sciple Date: Fri, 17 Oct 2025 00:24:10 +0000 Subject: [PATCH 20/22] . --- dist/index.js | 3 +++ src/git-auth-helper.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/dist/index.js b/dist/index.js index 5725651..00553d6 100644 --- a/dist/index.js +++ b/dist/index.js @@ -481,11 +481,13 @@ class GitAuthHelper { return __awaiter(this, void 0, void 0, function* () { var _a; // Remove HTTP extra header + core.info("Removing HTTP extra header"); yield this.removeGitConfig(this.tokenConfigKey); yield this.removeSubmoduleGitConfig(this.tokenConfigKey); // Collect credentials config paths that need to be removed const credentialsPaths = new Set(); // Remove includeIf entries that point to git-credentials-*.config files + core.info("Removing includeIf entries pointing to credentials config files"); const mainCredentialsPaths = yield this.removeIncludeIfCredentials(); mainCredentialsPaths.forEach(path => credentialsPaths.add(path)); // Remove submodule includeIf entries that point to git-credentials-*.config files @@ -497,6 +499,7 @@ class GitAuthHelper { // Remove credentials config files for (const credentialsPath of credentialsPaths) { try { + core.info(`Removing credentials config '${credentialsPath}'`); yield io.rmRF(credentialsPath); } catch (err) { diff --git a/src/git-auth-helper.ts b/src/git-auth-helper.ts index f7c8d8a..5a3d388 100644 --- a/src/git-auth-helper.ts +++ b/src/git-auth-helper.ts @@ -452,6 +452,7 @@ class GitAuthHelper { */ private async removeToken(): Promise { // Remove HTTP extra header + core.info("Removing HTTP extra header") await this.removeGitConfig(this.tokenConfigKey) await this.removeSubmoduleGitConfig(this.tokenConfigKey) @@ -459,6 +460,7 @@ class GitAuthHelper { const credentialsPaths = new Set() // Remove includeIf entries that point to git-credentials-*.config files + core.info("Removing includeIf entries pointing to credentials config files") const mainCredentialsPaths = await this.removeIncludeIfCredentials() mainCredentialsPaths.forEach(path => credentialsPaths.add(path)) @@ -472,6 +474,7 @@ class GitAuthHelper { // Remove credentials config files for (const credentialsPath of credentialsPaths) { try { + core.info(`Removing credentials config '${credentialsPath}'`) await io.rmRF(credentialsPath) } catch (err) { core.debug(`${(err as any)?.message ?? err}`) From 1929737c8ebde1680feb5786e41f0fdfc2f4ff52 Mon Sep 17 00:00:00 2001 From: eric sciple Date: Fri, 17 Oct 2025 00:32:56 +0000 Subject: [PATCH 21/22] . --- dist/index.js | 20 ++++++++++++++------ src/git-auth-helper.ts | 23 +++++++++++++++-------- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/dist/index.js b/dist/index.js index 00553d6..bdd7187 100644 --- a/dist/index.js +++ b/dist/index.js @@ -498,13 +498,21 @@ class GitAuthHelper { } // Remove credentials config files for (const credentialsPath of credentialsPaths) { - try { - core.info(`Removing credentials config '${credentialsPath}'`); - yield io.rmRF(credentialsPath); + // Only remove credentials config files if they are under RUNNER_TEMP + const runnerTemp = process.env['RUNNER_TEMP']; + assert.ok(runnerTemp, 'RUNNER_TEMP is not defined'); + if (credentialsPath.startsWith(runnerTemp)) { + try { + core.info(`Removing credentials config '${credentialsPath}'`); + yield io.rmRF(credentialsPath); + } + 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 '${credentialsPath}'`); + } } - 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 '${credentialsPath}'`); + else { + core.debug(`Skipping removal of credentials config '${credentialsPath}' - not under RUNNER_TEMP`); } } }); diff --git a/src/git-auth-helper.ts b/src/git-auth-helper.ts index 5a3d388..32510a8 100644 --- a/src/git-auth-helper.ts +++ b/src/git-auth-helper.ts @@ -473,14 +473,21 @@ class GitAuthHelper { // Remove credentials config files for (const credentialsPath of credentialsPaths) { - try { - core.info(`Removing credentials config '${credentialsPath}'`) - await io.rmRF(credentialsPath) - } catch (err) { - core.debug(`${(err as any)?.message ?? err}`) - core.warning( - `Failed to remove credentials config '${credentialsPath}'` - ) + // Only remove credentials config files if they are under RUNNER_TEMP + const runnerTemp = process.env['RUNNER_TEMP'] + assert.ok(runnerTemp, 'RUNNER_TEMP is not defined') + if (credentialsPath.startsWith(runnerTemp)) { + try { + core.info(`Removing credentials config '${credentialsPath}'`) + await io.rmRF(credentialsPath) + } catch (err) { + core.debug(`${(err as any)?.message ?? err}`) + core.warning( + `Failed to remove credentials config '${credentialsPath}'` + ) + } + } else { + core.debug(`Skipping removal of credentials config '${credentialsPath}' - not under RUNNER_TEMP`) } } } From 317d15a1e84a529dde6ac884f6481334b549431d Mon Sep 17 00:00:00 2001 From: eric sciple Date: Fri, 17 Oct 2025 00:34:58 +0000 Subject: [PATCH 22/22] . --- dist/index.js | 10 +++++++--- src/git-auth-helper.ts | 8 ++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/dist/index.js b/dist/index.js index bdd7187..10eb642 100644 --- a/dist/index.js +++ b/dist/index.js @@ -446,11 +446,12 @@ class GitAuthHelper { */ removeSsh() { return __awaiter(this, void 0, void 0, function* () { - var _a; + var _a, _b; // SSH key const keyPath = this.sshKeyPath || stateHelper.SshKeyPath; if (keyPath) { try { + core.info(`Removing SSH key '${keyPath}'`); yield io.rmRF(keyPath); } catch (err) { @@ -462,13 +463,16 @@ class GitAuthHelper { const knownHostsPath = this.sshKnownHostsPath || stateHelper.SshKnownHostsPath; if (knownHostsPath) { try { + core.info(`Removing SSH known hosts '${knownHostsPath}'`); yield io.rmRF(knownHostsPath); } - catch (_b) { - // Intentionally empty + catch (err) { + core.debug(`${(_b = err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err}`); + core.warning(`Failed to remove SSH known hosts '${knownHostsPath}'`); } } // SSH command + core.info("Removing SSH command configuration"); yield this.removeGitConfig(SSH_COMMAND_KEY); yield this.removeSubmoduleGitConfig(SSH_COMMAND_KEY); }); diff --git a/src/git-auth-helper.ts b/src/git-auth-helper.ts index 32510a8..8bec327 100644 --- a/src/git-auth-helper.ts +++ b/src/git-auth-helper.ts @@ -423,6 +423,7 @@ class GitAuthHelper { const keyPath = this.sshKeyPath || stateHelper.SshKeyPath if (keyPath) { try { + core.info(`Removing SSH key '${keyPath}'`) await io.rmRF(keyPath) } catch (err) { core.debug(`${(err as any)?.message ?? err}`) @@ -435,13 +436,16 @@ class GitAuthHelper { this.sshKnownHostsPath || stateHelper.SshKnownHostsPath if (knownHostsPath) { try { + core.info(`Removing SSH known hosts '${knownHostsPath}'`) await io.rmRF(knownHostsPath) - } catch { - // Intentionally empty + } catch (err) { + core.debug(`${(err as any)?.message ?? err}`) + core.warning(`Failed to remove SSH known hosts '${knownHostsPath}'`) } } // SSH command + core.info("Removing SSH command configuration") await this.removeGitConfig(SSH_COMMAND_KEY) await this.removeSubmoduleGitConfig(SSH_COMMAND_KEY) }