mirror of
				https://github.com/actions/checkout.git
				synced 2025-10-29 05:40:31 +00:00 
			
		
		
		
	Compare commits
	
		
			18 Commits
		
	
	
		
			v5.0.0
			...
			3292e202f3
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 3292e202f3 | ||
|   | e4894fca20 | ||
|   | 2bcd7c6585 | ||
|   | 857facff5c | ||
|   | ff9f98e487 | ||
|   | aa7e6581cb | ||
|   | 6397f22a4f | ||
|   | 762bf756aa | ||
|   | 96c6589494 | ||
|   | 0f2eb6b146 | ||
|   | a60fb6cabe | ||
|   | 8e4be9ae12 | ||
|   | 74fe54f098 | ||
|   | b13eccf351 | ||
|   | 82257b56c2 | ||
|   | d9b320ec70 | ||
|   | bcc5319a0b | ||
|   | ff7abcd0c3 | 
							
								
								
									
										11
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							| @@ -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 | ||||
| @@ -325,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 | ||||
|   | ||||
| @@ -2,7 +2,11 @@ | ||||
|  | ||||
| # Checkout V5 | ||||
|  | ||||
| Checkout v5 now supports Node.js 24 | ||||
| ## What's new | ||||
|  | ||||
| - Updated to the node24 runtime | ||||
|   - This requires a minimum Actions Runner version of [v2.327.1](https://github.com/actions/runner/releases/tag/v2.327.1) to run. | ||||
|  | ||||
|  | ||||
| # Checkout V4 | ||||
|  | ||||
| @@ -154,9 +158,10 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/ | ||||
| # Scenarios | ||||
|  | ||||
| - [Checkout V5](#checkout-v5) | ||||
|   - [What's new](#whats-new) | ||||
| - [Checkout V4](#checkout-v4) | ||||
|     - [Note](#note) | ||||
| - [What's new](#whats-new) | ||||
| - [What's new](#whats-new-1) | ||||
| - [Usage](#usage) | ||||
| - [Scenarios](#scenarios) | ||||
|   - [Fetch only the root files](#fetch-only-the-root-files) | ||||
|   | ||||
| @@ -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) | ||||
| @@ -550,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:/ | ||||
|       ) | ||||
| @@ -589,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/) | ||||
|     } | ||||
|   ) | ||||
| @@ -660,19 +711,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 = | ||||
| @@ -715,6 +782,7 @@ async function setup(testName: string): Promise<void> { | ||||
|   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') | ||||
| @@ -733,10 +801,20 @@ async function setup(testName: string): Promise<void> { | ||||
|     checkout: jest.fn(), | ||||
|     checkoutDetach: jest.fn(), | ||||
|     config: jest.fn( | ||||
|       async (key: string, value: string, globalConfig?: boolean) => { | ||||
|         const configPath = globalConfig | ||||
|       async ( | ||||
|         key: string, | ||||
|         value: string, | ||||
|         globalConfig?: boolean, | ||||
|         add?: boolean, | ||||
|         configFile?: string | ||||
|       ) => { | ||||
|         const configPath = | ||||
|           configFile || | ||||
|           (globalConfig | ||||
|             ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig') | ||||
|           : localGitConfigPath | ||||
|             : localGitConfigPath) | ||||
|         // Ensure directory exists | ||||
|         await fs.promises.mkdir(path.dirname(configPath), {recursive: true}) | ||||
|         await fs.promises.appendFile(configPath, `\n${key} ${value}`) | ||||
|       } | ||||
|     ), | ||||
| @@ -794,8 +872,53 @@ async function setup(testName: string): Promise<void> { | ||||
|         return true | ||||
|       } | ||||
|     ), | ||||
|     tryConfigUnsetValue: jest.fn( | ||||
|       async (key: string, value: string, globalConfig?: boolean): Promise<boolean> => { | ||||
|         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<string[]> => { | ||||
|         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<string[]> => { | ||||
|         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() | ||||
|   } | ||||
| @@ -830,6 +953,7 @@ async function setup(testName: string): Promise<void> { | ||||
|  | ||||
| async function getActualSshKeyPath(): Promise<string> { | ||||
|   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 +967,7 @@ async function getActualSshKeyPath(): Promise<string> { | ||||
|  | ||||
| async function getActualSshKnownHostsPath(): Promise<string> { | ||||
|   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) { | ||||
|   | ||||
| @@ -493,12 +493,15 @@ async function setup(testName: string): Promise<void> { | ||||
|       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 | ||||
|     }), | ||||
|   | ||||
							
								
								
									
										207
									
								
								dist/index.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										207
									
								
								dist/index.js
									
									
									
									
										vendored
									
									
								
							| @@ -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 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) { | ||||
| @@ -252,19 +268,37 @@ 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) { | ||||
|                 // 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
 | ||||
|                 // 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'); | ||||
|                 let relativePath = path.relative(githubWorkspace, workingDirectory); | ||||
|                 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) || []; | ||||
|                 // 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) { | ||||
|                     core.debug(`Replacing token placeholder in '${configPath}'`); | ||||
|                     yield this.replaceTokenPlaceholder(configPath); | ||||
|                     // 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:${submoduleGitDir}.path`, credentialsConfigPath, false, false, configPath); | ||||
|                     // Configure container includeIf
 | ||||
|                     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) { | ||||
|                     // Configure core.sshCommand
 | ||||
| @@ -351,20 +385,44 @@ 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) { | ||||
|                 // Global config file is temporary
 | ||||
|                 yield this.git.config('include.path', credentialsConfigPath, true); | ||||
|             } | ||||
|             else { | ||||
|                 // Host git directory
 | ||||
|                 let gitDir = path.join(this.git.getWorkingDirectory(), '.git'); | ||||
|                 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); | ||||
|                 // Container git directory
 | ||||
|                 const githubWorkspace = process.env['GITHUB_WORKSPACE']; | ||||
|                 assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined'); | ||||
|                 const workingDirectory = this.git.getWorkingDirectory(); | ||||
|                 let relativePath = path.relative(githubWorkspace, workingDirectory); | ||||
|                 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); | ||||
|             } | ||||
|             // 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) { | ||||
| @@ -407,26 +465,66 @@ 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* () { | ||||
|             // HTTP extra header
 | ||||
|             var _a; | ||||
|             // Remove HTTP extra header
 | ||||
|             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-<uuid>.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}`); | ||||
|             } | ||||
|             // 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 { | ||||
|                     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) { | ||||
|         return __awaiter(this, arguments, void 0, function* (configKey, submoduleOnly = false) { | ||||
|             if (!submoduleOnly) { | ||||
|     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); | ||||
|         }); | ||||
|     } | ||||
| @@ -627,9 +725,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'); | ||||
|             } | ||||
| @@ -836,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); | ||||
| @@ -855,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); | ||||
|   | ||||
| @@ -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 config keys for cleanup | ||||
|  | ||||
|   constructor( | ||||
|     gitCommandManager: IGitCommandManager, | ||||
| @@ -81,6 +83,22 @@ class GitAuthHelper { | ||||
|     await this.configureToken() | ||||
|   } | ||||
|  | ||||
|   private async getCredentialsConfigPath(): Promise<string> { | ||||
|     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<string> { | ||||
|     // Already setup global config | ||||
|     if (this.temporaryHomePath?.length > 0) { | ||||
| @@ -126,10 +144,10 @@ class GitAuthHelper { | ||||
|  | ||||
|   async configureGlobalAuth(): Promise<void> { | ||||
|     // '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) | ||||
| @@ -150,24 +168,73 @@ class GitAuthHelper { | ||||
|  | ||||
|   async configureSubmoduleAuth(): Promise<void> { | ||||
|     // Remove possible previous HTTPS instead of SSH | ||||
|     await this.removeGitConfig(this.insteadOfKey, true) | ||||
|     await this.removeSubmoduleGitConfig(this.insteadOfKey) | ||||
|  | ||||
|     if (this.settings.persistCredentials) { | ||||
|       // 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 | ||||
|       // Credentials config path | ||||
|       const credentialsConfigPath = await 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') | ||||
|       let relativePath = path.relative(githubWorkspace, workingDirectory) | ||||
|       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 = 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"`, | ||||
|         `git config --local --show-origin --name-only --get-regexp remote.origin.url`, | ||||
|         this.settings.nestedSubmodules | ||||
|       ) | ||||
|  | ||||
|       // Replace the placeholder | ||||
|       const configPaths: string[] = | ||||
|       // 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) { | ||||
|         core.debug(`Replacing token placeholder in '${configPath}'`) | ||||
|         await this.replaceTokenPlaceholder(configPath) | ||||
|         // 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:${submoduleGitDir}.path`, | ||||
|           credentialsConfigPath, | ||||
|           false, | ||||
|           false, | ||||
|           configPath | ||||
|         ) | ||||
|  | ||||
|         // Configure container includeIf | ||||
|         let relativeSubmoduleGitDir = path.relative( | ||||
|           githubWorkspace, | ||||
|           submoduleGitDir | ||||
|         ) | ||||
|         relativeSubmoduleGitDir = relativeSubmoduleGitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows | ||||
|         const containerSubmoduleGitDir = path.posix.join( | ||||
|           '/github/workspace', | ||||
|           relativeSubmoduleGitDir | ||||
|         ) | ||||
|         await this.git.config( | ||||
|           `includeIf.gitdir:${containerSubmoduleGitDir}.path`, | ||||
|           containerCredentialsPath, | ||||
|           false, | ||||
|           false, | ||||
|           configPath | ||||
|         ) | ||||
|       } | ||||
|  | ||||
|       if (this.settings.sshKey) { | ||||
| @@ -272,32 +339,62 @@ class GitAuthHelper { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async configureToken( | ||||
|     configPath?: string, | ||||
|     globalConfig?: boolean | ||||
|   ): Promise<void> { | ||||
|     // Validate args | ||||
|     assert.ok( | ||||
|       (configPath && globalConfig) || (!configPath && !globalConfig), | ||||
|       'Unexpected configureToken parameter combinations' | ||||
|     ) | ||||
|   private async configureToken(globalConfig?: boolean): Promise<void> { | ||||
|     // 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) { | ||||
|       // Global config file is temporary | ||||
|       await this.git.config('include.path', credentialsConfigPath, true) | ||||
|     } else { | ||||
|       // Host git directory | ||||
|       let gitDir = path.join(this.git.getWorkingDirectory(), '.git') | ||||
|       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) | ||||
|  | ||||
|       // Container git directory | ||||
|       const githubWorkspace = process.env['GITHUB_WORKSPACE'] | ||||
|       assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined') | ||||
|       const workingDirectory = this.git.getWorkingDirectory() | ||||
|       let relativePath = path.relative(githubWorkspace, workingDirectory) | ||||
|       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) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async replaceTokenPlaceholder(configPath: string): Promise<void> { | ||||
| @@ -343,18 +440,58 @@ class GitAuthHelper { | ||||
|  | ||||
|     // SSH command | ||||
|     await this.removeGitConfig(SSH_COMMAND_KEY) | ||||
|     await this.removeSubmoduleGitConfig(SSH_COMMAND_KEY) | ||||
|   } | ||||
|  | ||||
|   private async removeToken(): Promise<void> { | ||||
|     // HTTP extra header | ||||
|     // 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-<uuid>.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}`) | ||||
|     } | ||||
|  | ||||
|   private async removeGitConfig( | ||||
|     configKey: string, | ||||
|     submoduleOnly: boolean = false | ||||
|   ): Promise<void> { | ||||
|     if (!submoduleOnly) { | ||||
|     // Remove submodule includeIf | ||||
|     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 { | ||||
|         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(configKey: string): Promise<void> { | ||||
|     if ( | ||||
|       (await this.git.configExists(configKey)) && | ||||
|       !(await this.git.tryConfigUnset(configKey)) | ||||
| @@ -364,9 +501,10 @@ class GitAuthHelper { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async removeSubmoduleGitConfig(configKey: string): Promise<void> { | ||||
|     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 | ||||
|     ) | ||||
|   | ||||
| @@ -28,7 +28,8 @@ export interface IGitCommandManager { | ||||
|     configKey: string, | ||||
|     configValue: string, | ||||
|     globalConfig?: boolean, | ||||
|     add?: boolean | ||||
|     add?: boolean, | ||||
|     configFile?: string | ||||
|   ): Promise<void> | ||||
|   configExists(configKey: string, globalConfig?: boolean): Promise<boolean> | ||||
|   fetch( | ||||
| @@ -59,8 +60,11 @@ export interface IGitCommandManager { | ||||
|   tagExists(pattern: string): Promise<boolean> | ||||
|   tryClean(): Promise<boolean> | ||||
|   tryConfigUnset(configKey: string, globalConfig?: boolean): Promise<boolean> | ||||
|   tryConfigUnsetValue(configKey: string, configValue: string, globalConfig?: boolean): Promise<boolean> | ||||
|   tryDisableAutomaticGarbageCollection(): Promise<boolean> | ||||
|   tryGetFetchUrl(): Promise<string> | ||||
|   tryGetConfigValues(configKey: string, globalConfig?: boolean): Promise<string[]> | ||||
|   tryGetConfigKeys(pattern: string, globalConfig?: boolean): Promise<string[]> | ||||
|   tryReset(): Promise<boolean> | ||||
|   version(): Promise<GitVersion> | ||||
| } | ||||
| @@ -223,9 +227,15 @@ class GitCommandManager { | ||||
|     configKey: string, | ||||
|     configValue: string, | ||||
|     globalConfig?: boolean, | ||||
|     add?: boolean | ||||
|     add?: boolean, | ||||
|     configFile?: string | ||||
|   ): Promise<void> { | ||||
|     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') | ||||
|     } | ||||
| @@ -455,6 +465,24 @@ class GitCommandManager { | ||||
|     return output.exitCode === 0 | ||||
|   } | ||||
|  | ||||
|   async tryConfigUnsetValue( | ||||
|     configKey: string, | ||||
|     configValue: string, | ||||
|     globalConfig?: boolean | ||||
|   ): Promise<boolean> { | ||||
|     const output = await this.execGit( | ||||
|       [ | ||||
|         'config', | ||||
|         globalConfig ? '--global' : '--local', | ||||
|         '--unset', | ||||
|         configKey, | ||||
|         configValue | ||||
|       ], | ||||
|       true | ||||
|     ) | ||||
|     return output.exitCode === 0 | ||||
|   } | ||||
|  | ||||
|   async tryDisableAutomaticGarbageCollection(): Promise<boolean> { | ||||
|     const output = await this.execGit( | ||||
|       ['config', '--local', 'gc.auto', '0'], | ||||
| @@ -481,6 +509,49 @@ class GitCommandManager { | ||||
|     return stdout | ||||
|   } | ||||
|  | ||||
|   async tryGetConfigValues( | ||||
|     configKey: string, | ||||
|     globalConfig?: boolean | ||||
|   ): Promise<string[]> { | ||||
|     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<string[]> { | ||||
|     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<boolean> { | ||||
|     const output = await this.execGit(['reset', '--hard', 'HEAD'], true) | ||||
|     return output.exitCode === 0 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user