mirror of
				https://github.com/actions/checkout.git
				synced 2025-10-31 22:08:09 +08:00 
			
		
		
		
	Compare commits
	
		
			10 Commits
		
	
	
		
			users/eric
			...
			762bf756aa
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 762bf756aa | ||
|  | 96c6589494 | ||
|  | 0f2eb6b146 | ||
|  | a60fb6cabe | ||
|  | 8e4be9ae12 | ||
|  | 74fe54f098 | ||
|  | b13eccf351 | ||
|  | 82257b56c2 | ||
|  | d9b320ec70 | ||
|  | bcc5319a0b | 
							
								
								
									
										9
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							| @@ -302,15 +302,12 @@ jobs: | |||||||
|       # Clone this repo |       # Clone this repo | ||||||
|       - name: Checkout |       - name: Checkout | ||||||
|         uses: actions/checkout@v4.1.6 |         uses: actions/checkout@v4.1.6 | ||||||
|         with: |  | ||||||
|           path: actions-checkout |  | ||||||
|  |  | ||||||
|       # Basic checkout using git |       # Basic checkout using git | ||||||
|       - name: Checkout basic |       - name: Checkout basic | ||||||
|         id: checkout |         id: checkout | ||||||
|         uses: ./actions-checkout |         uses: ./ | ||||||
|         with: |         with: | ||||||
|           path: cloned-using-local-action |  | ||||||
|           ref: test-data/v2/basic |           ref: test-data/v2/basic | ||||||
|  |  | ||||||
|       # Verify output |       # Verify output | ||||||
| @@ -328,3 +325,7 @@ jobs: | |||||||
|             echo "Expected commit to be 82f71901cf8c021332310dcc8cdba84c4193ff5d" |             echo "Expected commit to be 82f71901cf8c021332310dcc8cdba84c4193ff5d" | ||||||
|             exit 1 |             exit 1 | ||||||
|           fi |           fi | ||||||
|  |  | ||||||
|  |       # needed to make checkout post cleanup succeed | ||||||
|  |       - name: Fix Checkout | ||||||
|  |         uses: actions/checkout@v4.1.6 | ||||||
|   | |||||||
| @@ -595,15 +595,18 @@ describe('git-auth-helper tests', () => { | |||||||
|       await authHelper.configureSubmoduleAuth() |       await authHelper.configureSubmoduleAuth() | ||||||
|  |  | ||||||
|       // Assert |       // Assert | ||||||
|       // Should configure insteadOf (2 calls for two values) |       // Should get submodule config paths (1 call) and configure insteadOf (2 calls for two values) | ||||||
|       expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3) |       expect(mockSubmoduleForeach).toHaveBeenCalledTimes(4) | ||||||
|       expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch( |       expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch( | ||||||
|         /unset-all.*insteadOf/ |         /unset-all.*insteadOf/ | ||||||
|       ) |       ) | ||||||
|       expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch( |       expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch( | ||||||
|         /url.*insteadOf.*git@github.com:/ |         /show-origin.*remote\.origin\.url/ | ||||||
|       ) |       ) | ||||||
|       expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch( |       expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch( | ||||||
|  |         /url.*insteadOf.*git@github.com:/ | ||||||
|  |       ) | ||||||
|  |       expect(mockSubmoduleForeach.mock.calls[3][0]).toMatch( | ||||||
|         /url.*insteadOf.*org-123456@github.com:/ |         /url.*insteadOf.*org-123456@github.com:/ | ||||||
|       ) |       ) | ||||||
|     } |     } | ||||||
| @@ -634,12 +637,15 @@ describe('git-auth-helper tests', () => { | |||||||
|       await authHelper.configureSubmoduleAuth() |       await authHelper.configureSubmoduleAuth() | ||||||
|  |  | ||||||
|       // Assert |       // Assert | ||||||
|       // Should configure sshCommand (1 call) |       // Should get submodule config paths (1 call) and configure sshCommand (1 call) | ||||||
|       expect(mockSubmoduleForeach).toHaveBeenCalledTimes(2) |       expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3) | ||||||
|       expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch( |       expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch( | ||||||
|         /unset-all.*insteadOf/ |         /unset-all.*insteadOf/ | ||||||
|       ) |       ) | ||||||
|       expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/core\.sshCommand/) |       expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch( | ||||||
|  |         /show-origin.*remote\.origin\.url/ | ||||||
|  |       ) | ||||||
|  |       expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(/core\.sshCommand/) | ||||||
|     } |     } | ||||||
|   ) |   ) | ||||||
|  |  | ||||||
| @@ -706,7 +712,7 @@ describe('git-auth-helper tests', () => { | |||||||
|     const authHelper = gitAuthHelper.createAuthHelper(git, settings) |     const authHelper = gitAuthHelper.createAuthHelper(git, settings) | ||||||
|     await authHelper.configureAuth() |     await authHelper.configureAuth() | ||||||
|  |  | ||||||
|     // Verify includeIf entries exist in local config |     // Sanity check - verify includeIf entries exist in local config | ||||||
|     let localConfigContent = ( |     let localConfigContent = ( | ||||||
|       await fs.promises.readFile(localGitConfigPath) |       await fs.promises.readFile(localGitConfigPath) | ||||||
|     ).toString() |     ).toString() | ||||||
| @@ -714,192 +720,26 @@ describe('git-auth-helper tests', () => { | |||||||
|       localConfigContent.indexOf('includeIf.gitdir:') |       localConfigContent.indexOf('includeIf.gitdir:') | ||||||
|     ).toBeGreaterThanOrEqual(0) |     ).toBeGreaterThanOrEqual(0) | ||||||
|  |  | ||||||
|     // Verify both host and container includeIf entries are present |     // Sanity check - verify credentials file exists | ||||||
|     const hostGitDir = path.join(workspace, '.git').replace(/\\/g, '/') |  | ||||||
|     expect( |  | ||||||
|       localConfigContent.indexOf(`includeIf.gitdir:${hostGitDir}.path`) |  | ||||||
|     ).toBeGreaterThanOrEqual(0) |  | ||||||
|     expect( |  | ||||||
|       localConfigContent.indexOf('includeIf.gitdir:/github/workspace/.git.path') |  | ||||||
|     ).toBeGreaterThanOrEqual(0) |  | ||||||
|  |  | ||||||
|     // Verify credentials file exists |  | ||||||
|     let credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter( |     let credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter( | ||||||
|       f => f.startsWith('git-credentials-') && f.endsWith('.config') |       f => f.startsWith('git-credentials-') && f.endsWith('.config') | ||||||
|     ) |     ) | ||||||
|     expect(credentialsFiles.length).toBe(1) |     expect(credentialsFiles.length).toBe(1) | ||||||
|     const credentialsFilePath = path.join(runnerTemp, credentialsFiles[0]) |  | ||||||
|  |  | ||||||
|     // Verify credentials file contains the auth token |  | ||||||
|     let credentialsContent = ( |  | ||||||
|       await fs.promises.readFile(credentialsFilePath) |  | ||||||
|     ).toString() |  | ||||||
|     const basicCredential = Buffer.from( |  | ||||||
|       `x-access-token:${settings.authToken}`, |  | ||||||
|       'utf8' |  | ||||||
|     ).toString('base64') |  | ||||||
|     expect( |  | ||||||
|       credentialsContent.indexOf( |  | ||||||
|         `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}` |  | ||||||
|       ) |  | ||||||
|     ).toBeGreaterThanOrEqual(0) |  | ||||||
|  |  | ||||||
|     // Verify the includeIf entries point to the credentials file |  | ||||||
|     const containerCredentialsPath = path.posix.join( |  | ||||||
|       '/github/runner_temp', |  | ||||||
|       path.basename(credentialsFilePath) |  | ||||||
|     ) |  | ||||||
|     expect( |  | ||||||
|       localConfigContent.indexOf(credentialsFilePath) |  | ||||||
|     ).toBeGreaterThanOrEqual(0) |  | ||||||
|     expect( |  | ||||||
|       localConfigContent.indexOf(containerCredentialsPath) |  | ||||||
|     ).toBeGreaterThanOrEqual(0) |  | ||||||
|  |  | ||||||
|     // Act |     // Act | ||||||
|     await authHelper.removeAuth() |     await authHelper.removeAuth() | ||||||
|  |  | ||||||
|     // Assert all includeIf entries removed from local git config |     // Assert includeIf entries removed from local git config | ||||||
|     localConfigContent = ( |     localConfigContent = ( | ||||||
|       await fs.promises.readFile(localGitConfigPath) |       await fs.promises.readFile(localGitConfigPath) | ||||||
|     ).toString() |     ).toString() | ||||||
|     expect(localConfigContent.indexOf('includeIf.gitdir:')).toBeLessThan(0) |     expect(localConfigContent.indexOf('includeIf.gitdir:')).toBeLessThan(0) | ||||||
|     expect( |  | ||||||
|       localConfigContent.indexOf(`includeIf.gitdir:${hostGitDir}.path`) |  | ||||||
|     ).toBeLessThan(0) |  | ||||||
|     expect( |  | ||||||
|       localConfigContent.indexOf('includeIf.gitdir:/github/workspace/.git.path') |  | ||||||
|     ).toBeLessThan(0) |  | ||||||
|     expect(localConfigContent.indexOf(credentialsFilePath)).toBeLessThan(0) |  | ||||||
|     expect(localConfigContent.indexOf(containerCredentialsPath)).toBeLessThan(0) |  | ||||||
|  |  | ||||||
|     // Assert credentials config file deleted |     // Assert credentials config file deleted | ||||||
|     credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter( |     credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter( | ||||||
|       f => f.startsWith('git-credentials-') && f.endsWith('.config') |       f => f.startsWith('git-credentials-') && f.endsWith('.config') | ||||||
|     ) |     ) | ||||||
|     expect(credentialsFiles.length).toBe(0) |     expect(credentialsFiles.length).toBe(0) | ||||||
|  |  | ||||||
|     // Verify credentials file no longer exists on disk |  | ||||||
|     try { |  | ||||||
|       await fs.promises.stat(credentialsFilePath) |  | ||||||
|       throw new Error('Credentials file should have been deleted') |  | ||||||
|     } catch (err) { |  | ||||||
|       if ((err as any)?.code !== 'ENOENT') { |  | ||||||
|         throw err |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   }) |  | ||||||
|  |  | ||||||
|   const removeAuth_removesTokenFromSubmodules = |  | ||||||
|     'removeAuth removes token from submodules' |  | ||||||
|   it(removeAuth_removesTokenFromSubmodules, async () => { |  | ||||||
|     // Arrange |  | ||||||
|     await setup(removeAuth_removesTokenFromSubmodules) |  | ||||||
|  |  | ||||||
|     // Create fake submodule config paths |  | ||||||
|     const submodule1Dir = path.join(workspace, '.git', 'modules', 'submodule-1') |  | ||||||
|     const submodule2Dir = path.join(workspace, '.git', 'modules', 'submodule-2') |  | ||||||
|     const submodule1ConfigPath = path.join(submodule1Dir, 'config') |  | ||||||
|     const submodule2ConfigPath = path.join(submodule2Dir, 'config') |  | ||||||
|  |  | ||||||
|     await fs.promises.mkdir(submodule1Dir, {recursive: true}) |  | ||||||
|     await fs.promises.mkdir(submodule2Dir, {recursive: true}) |  | ||||||
|     await fs.promises.writeFile(submodule1ConfigPath, '') |  | ||||||
|     await fs.promises.writeFile(submodule2ConfigPath, '') |  | ||||||
|  |  | ||||||
|     // Mock getSubmoduleConfigPaths to return our fake submodules (for both configure and remove) |  | ||||||
|     const mockGetSubmoduleConfigPaths = |  | ||||||
|       git.getSubmoduleConfigPaths as jest.Mock<any, any> |  | ||||||
|     mockGetSubmoduleConfigPaths.mockResolvedValue([ |  | ||||||
|       submodule1ConfigPath, |  | ||||||
|       submodule2ConfigPath |  | ||||||
|     ]) |  | ||||||
|  |  | ||||||
|     const authHelper = gitAuthHelper.createAuthHelper(git, settings) |  | ||||||
|     await authHelper.configureAuth() |  | ||||||
|     await authHelper.configureSubmoduleAuth() |  | ||||||
|  |  | ||||||
|     // 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) |  | ||||||
|     const credentialsFilePath = path.join(runnerTemp, credentialsFiles[0]) |  | ||||||
|  |  | ||||||
|     // Verify submodule 1 config has includeIf entries |  | ||||||
|     let submodule1Content = ( |  | ||||||
|       await fs.promises.readFile(submodule1ConfigPath) |  | ||||||
|     ).toString() |  | ||||||
|     const submodule1GitDir = submodule1Dir.replace(/\\/g, '/') |  | ||||||
|     expect( |  | ||||||
|       submodule1Content.indexOf(`includeIf.gitdir:${submodule1GitDir}.path`) |  | ||||||
|     ).toBeGreaterThanOrEqual(0) |  | ||||||
|     expect( |  | ||||||
|       submodule1Content.indexOf(credentialsFilePath) |  | ||||||
|     ).toBeGreaterThanOrEqual(0) |  | ||||||
|  |  | ||||||
|     // Verify submodule 2 config has includeIf entries |  | ||||||
|     let submodule2Content = ( |  | ||||||
|       await fs.promises.readFile(submodule2ConfigPath) |  | ||||||
|     ).toString() |  | ||||||
|     const submodule2GitDir = submodule2Dir.replace(/\\/g, '/') |  | ||||||
|     expect( |  | ||||||
|       submodule2Content.indexOf(`includeIf.gitdir:${submodule2GitDir}.path`) |  | ||||||
|     ).toBeGreaterThanOrEqual(0) |  | ||||||
|     expect( |  | ||||||
|       submodule2Content.indexOf(credentialsFilePath) |  | ||||||
|     ).toBeGreaterThanOrEqual(0) |  | ||||||
|  |  | ||||||
|     // Verify both host and container paths are in each submodule config |  | ||||||
|     const containerCredentialsPath = path.posix.join( |  | ||||||
|       '/github/runner_temp', |  | ||||||
|       path.basename(credentialsFilePath) |  | ||||||
|     ) |  | ||||||
|     expect( |  | ||||||
|       submodule1Content.indexOf(containerCredentialsPath) |  | ||||||
|     ).toBeGreaterThanOrEqual(0) |  | ||||||
|     expect( |  | ||||||
|       submodule2Content.indexOf(containerCredentialsPath) |  | ||||||
|     ).toBeGreaterThanOrEqual(0) |  | ||||||
|  |  | ||||||
|     // Act - ensure mock persists for removeAuth |  | ||||||
|     mockGetSubmoduleConfigPaths.mockResolvedValue([ |  | ||||||
|       submodule1ConfigPath, |  | ||||||
|       submodule2ConfigPath |  | ||||||
|     ]) |  | ||||||
|     await authHelper.removeAuth() |  | ||||||
|  |  | ||||||
|     // Assert submodule 1 includeIf entries removed |  | ||||||
|     submodule1Content = ( |  | ||||||
|       await fs.promises.readFile(submodule1ConfigPath) |  | ||||||
|     ).toString() |  | ||||||
|     expect(submodule1Content.indexOf('includeIf.gitdir:')).toBeLessThan(0) |  | ||||||
|     expect(submodule1Content.indexOf(credentialsFilePath)).toBeLessThan(0) |  | ||||||
|     expect(submodule1Content.indexOf(containerCredentialsPath)).toBeLessThan(0) |  | ||||||
|  |  | ||||||
|     // Assert submodule 2 includeIf entries removed |  | ||||||
|     submodule2Content = ( |  | ||||||
|       await fs.promises.readFile(submodule2ConfigPath) |  | ||||||
|     ).toString() |  | ||||||
|     expect(submodule2Content.indexOf('includeIf.gitdir:')).toBeLessThan(0) |  | ||||||
|     expect(submodule2Content.indexOf(credentialsFilePath)).toBeLessThan(0) |  | ||||||
|     expect(submodule2Content.indexOf(containerCredentialsPath)).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) |  | ||||||
|  |  | ||||||
|     // Verify credentials file no longer exists on disk |  | ||||||
|     try { |  | ||||||
|       await fs.promises.stat(credentialsFilePath) |  | ||||||
|       throw new Error('Credentials file should have been deleted') |  | ||||||
|     } catch (err) { |  | ||||||
|       if ((err as any)?.code !== 'ENOENT') { |  | ||||||
|         throw err |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   const removeGlobalConfig_removesOverride = |   const removeGlobalConfig_removesOverride = | ||||||
| @@ -928,52 +768,6 @@ 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<void> { | async function setup(testName: string): Promise<void> { | ||||||
| @@ -1040,7 +834,6 @@ async function setup(testName: string): Promise<void> { | |||||||
|     env: {}, |     env: {}, | ||||||
|     fetch: jest.fn(), |     fetch: jest.fn(), | ||||||
|     getDefaultBranch: jest.fn(), |     getDefaultBranch: jest.fn(), | ||||||
|     getSubmoduleConfigPaths: jest.fn(async () => []), |  | ||||||
|     getWorkingDirectory: jest.fn(() => workspace), |     getWorkingDirectory: jest.fn(() => workspace), | ||||||
|     init: jest.fn(), |     init: jest.fn(), | ||||||
|     isDetached: jest.fn(), |     isDetached: jest.fn(), | ||||||
| @@ -1079,72 +872,8 @@ async function setup(testName: string): Promise<void> { | |||||||
|         return true |         return true | ||||||
|       } |       } | ||||||
|     ), |     ), | ||||||
|     tryConfigUnsetValue: jest.fn( |  | ||||||
|       async ( |  | ||||||
|         key: string, |  | ||||||
|         value: string, |  | ||||||
|         globalConfig?: boolean, |  | ||||||
|         configPath?: string |  | ||||||
|       ): Promise<boolean> => { |  | ||||||
|         const targetConfigPath = |  | ||||||
|           configPath || |  | ||||||
|           (globalConfig |  | ||||||
|             ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig') |  | ||||||
|             : localGitConfigPath) |  | ||||||
|         let content = await fs.promises.readFile(targetConfigPath) |  | ||||||
|         let lines = content |  | ||||||
|           .toString() |  | ||||||
|           .split('\n') |  | ||||||
|           .filter(x => x) |  | ||||||
|           .filter(x => !(x.startsWith(key) && x.includes(value))) |  | ||||||
|         await fs.promises.writeFile(targetConfigPath, lines.join('\n')) |  | ||||||
|         return true |  | ||||||
|       } |  | ||||||
|     ), |  | ||||||
|     tryDisableAutomaticGarbageCollection: jest.fn(), |     tryDisableAutomaticGarbageCollection: jest.fn(), | ||||||
|     tryGetFetchUrl: jest.fn(), |     tryGetFetchUrl: jest.fn(), | ||||||
|     tryGetConfigValues: jest.fn( |  | ||||||
|       async ( |  | ||||||
|         key: string, |  | ||||||
|         globalConfig?: boolean, |  | ||||||
|         configPath?: string |  | ||||||
|       ): Promise<string[]> => { |  | ||||||
|         const targetConfigPath = |  | ||||||
|           configPath || |  | ||||||
|           (globalConfig |  | ||||||
|             ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig') |  | ||||||
|             : localGitConfigPath) |  | ||||||
|         const content = await fs.promises.readFile(targetConfigPath) |  | ||||||
|         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, |  | ||||||
|         configPath?: string |  | ||||||
|       ): Promise<string[]> => { |  | ||||||
|         const targetConfigPath = |  | ||||||
|           configPath || |  | ||||||
|           (globalConfig |  | ||||||
|             ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig') |  | ||||||
|             : localGitConfigPath) |  | ||||||
|         const content = await fs.promises.readFile(targetConfigPath) |  | ||||||
|         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(), |     tryReset: jest.fn(), | ||||||
|     version: jest.fn() |     version: jest.fn() | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -471,7 +471,6 @@ async function setup(testName: string): Promise<void> { | |||||||
|     configExists: jest.fn(), |     configExists: jest.fn(), | ||||||
|     fetch: jest.fn(), |     fetch: jest.fn(), | ||||||
|     getDefaultBranch: jest.fn(), |     getDefaultBranch: jest.fn(), | ||||||
|     getSubmoduleConfigPaths: jest.fn(async () => []), |  | ||||||
|     getWorkingDirectory: jest.fn(() => repositoryPath), |     getWorkingDirectory: jest.fn(() => repositoryPath), | ||||||
|     init: jest.fn(), |     init: jest.fn(), | ||||||
|     isDetached: jest.fn(), |     isDetached: jest.fn(), | ||||||
| @@ -494,15 +493,12 @@ async function setup(testName: string): Promise<void> { | |||||||
|       return true |       return true | ||||||
|     }), |     }), | ||||||
|     tryConfigUnset: jest.fn(), |     tryConfigUnset: jest.fn(), | ||||||
|     tryConfigUnsetValue: jest.fn(), |  | ||||||
|     tryDisableAutomaticGarbageCollection: jest.fn(), |     tryDisableAutomaticGarbageCollection: jest.fn(), | ||||||
|     tryGetFetchUrl: jest.fn(async () => { |     tryGetFetchUrl: jest.fn(async () => { | ||||||
|       // Sanity check - this function shouldn't be called when the .git directory doesn't exist |       // Sanity check - this function shouldn't be called when the .git directory doesn't exist | ||||||
|       await fs.promises.stat(path.join(repositoryPath, '.git')) |       await fs.promises.stat(path.join(repositoryPath, '.git')) | ||||||
|       return repositoryUrl |       return repositoryUrl | ||||||
|     }), |     }), | ||||||
|     tryGetConfigValues: jest.fn(), |  | ||||||
|     tryGetConfigKeys: jest.fn(), |  | ||||||
|     tryReset: jest.fn(async () => { |     tryReset: jest.fn(async () => { | ||||||
|       return true |       return true | ||||||
|     }), |     }), | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ fi | |||||||
|  |  | ||||||
| echo "Testing persisted credential" | echo "Testing persisted credential" | ||||||
| pushd ./submodules-recursive/submodule-level-1/submodule-level-2 | pushd ./submodules-recursive/submodule-level-1/submodule-level-2 | ||||||
| git config --local --includes --name-only --get-regexp http.+extraheader && git fetch | git config --local --name-only --get-regexp http.+extraheader && git fetch | ||||||
| if [ "$?" != "0" ]; then | if [ "$?" != "0" ]; then | ||||||
|     echo "Failed to validate persisted credential" |     echo "Failed to validate persisted credential" | ||||||
|     popd |     popd | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ fi | |||||||
|  |  | ||||||
| echo "Testing persisted credential" | echo "Testing persisted credential" | ||||||
| pushd ./submodules-true/submodule-level-1 | pushd ./submodules-true/submodule-level-1 | ||||||
| git config --local --includes --name-only --get-regexp http.+extraheader && git fetch | git config --local --name-only --get-regexp http.+extraheader && git fetch | ||||||
| if [ "$?" != "0" ]; then | if [ "$?" != "0" ]; then | ||||||
|     echo "Failed to validate persisted credential" |     echo "Failed to validate persisted credential" | ||||||
|     popd |     popd | ||||||
|   | |||||||
							
								
								
									
										296
									
								
								dist/index.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										296
									
								
								dist/index.js
									
									
									
									
										vendored
									
									
								
							| @@ -163,6 +163,7 @@ class GitAuthHelper { | |||||||
|         this.sshKnownHostsPath = ''; |         this.sshKnownHostsPath = ''; | ||||||
|         this.temporaryHomePath = ''; |         this.temporaryHomePath = ''; | ||||||
|         this.credentialsConfigPath = ''; // Path to separate credentials config file in RUNNER_TEMP
 |         this.credentialsConfigPath = ''; // Path to separate credentials config file in RUNNER_TEMP
 | ||||||
|  |         this.credentialsIncludeKeys = []; // Track includeIf config keys for cleanup
 | ||||||
|         this.git = gitCommandManager; |         this.git = gitCommandManager; | ||||||
|         this.settings = gitSourceSettings || {}; |         this.settings = gitSourceSettings || {}; | ||||||
|         // Token auth header
 |         // Token auth header
 | ||||||
| @@ -188,6 +189,20 @@ class GitAuthHelper { | |||||||
|             yield this.configureToken(); |             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() { |     configureTempGlobalConfig() { | ||||||
|         return __awaiter(this, void 0, void 0, function* () { |         return __awaiter(this, void 0, void 0, function* () { | ||||||
|             var _a; |             var _a; | ||||||
| @@ -238,9 +253,7 @@ class GitAuthHelper { | |||||||
|                 yield this.git.tryConfigUnset(this.insteadOfKey, true); |                 yield this.git.tryConfigUnset(this.insteadOfKey, true); | ||||||
|                 if (!this.settings.sshKey) { |                 if (!this.settings.sshKey) { | ||||||
|                     for (const insteadOfValue of this.insteadOfValues) { |                     for (const insteadOfValue of this.insteadOfValues) { | ||||||
|                         yield this.git.config(this.insteadOfKey, insteadOfValue, true, // globalConfig?
 |                         yield this.git.config(this.insteadOfKey, insteadOfValue, true, true); | ||||||
|                         true // add?
 |  | ||||||
|                         ); |  | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| @@ -257,32 +270,35 @@ class GitAuthHelper { | |||||||
|             // Remove possible previous HTTPS instead of SSH
 |             // Remove possible previous HTTPS instead of SSH
 | ||||||
|             yield this.removeSubmoduleGitConfig(this.insteadOfKey); |             yield this.removeSubmoduleGitConfig(this.insteadOfKey); | ||||||
|             if (this.settings.persistCredentials) { |             if (this.settings.persistCredentials) { | ||||||
|                 // Get the credentials config file path in RUNNER_TEMP
 |                 // Credentials config path
 | ||||||
|                 const credentialsConfigPath = this.getCredentialsConfigPath(); |                 const credentialsConfigPath = yield this.getCredentialsConfigPath(); | ||||||
|                 // Container credentials config path
 |                 // Container credentials config path
 | ||||||
|                 const containerCredentialsPath = path.posix.join('/github/runner_temp', path.basename(credentialsConfigPath)); |                 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.
 |                 // Get submodule config file paths.
 | ||||||
|                 const configPaths = yield this.git.getSubmoduleConfigPaths(this.settings.nestedSubmodules); |                 // 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.
 |                 // For each submodule, configure includeIf entries pointing to the shared credentials file.
 | ||||||
|                 // Configure both host and container paths to support Docker container actions.
 |                 // Configure both host and container paths to support Docker container actions.
 | ||||||
|                 for (const configPath of configPaths) { |                 for (const configPath of configPaths) { | ||||||
|                     // Submodule Git directory
 |                     // The config file is at .git/modules/submodule-name/config
 | ||||||
|                     let submoduleGitDir = path.dirname(configPath); // The config file is at .git/modules/submodule-name/config
 |                     let submoduleConfigDir = path.dirname(configPath); | ||||||
|                     submoduleGitDir = submoduleGitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
 |                     submoduleConfigDir = submoduleConfigDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
 | ||||||
|                     // Configure host includeIf
 |                     // Configure host includeIf
 | ||||||
|                     yield this.git.config(`includeIf.gitdir:${submoduleGitDir}.path`, credentialsConfigPath, false, // globalConfig?
 |                     yield this.git.config(`includeIf.gitdir:${submoduleConfigDir}.path`, credentialsConfigPath, false, false, configPath); | ||||||
|                     false, // add?
 |  | ||||||
|                     configPath); |  | ||||||
|                     // Container submodule git directory
 |  | ||||||
|                     const githubWorkspace = process.env['GITHUB_WORKSPACE']; |  | ||||||
|                     assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined'); |  | ||||||
|                     let relativeSubmoduleGitDir = path.relative(githubWorkspace, submoduleGitDir); |  | ||||||
|                     relativeSubmoduleGitDir = relativeSubmoduleGitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
 |  | ||||||
|                     const containerSubmoduleGitDir = path.posix.join('/github/workspace', relativeSubmoduleGitDir); |  | ||||||
|                     // Configure container includeIf
 |                     // Configure container includeIf
 | ||||||
|                     yield this.git.config(`includeIf.gitdir:${containerSubmoduleGitDir}.path`, containerCredentialsPath, false, // globalConfig?
 |                     let relativeSubmoduleConfigDir = path.relative(githubWorkspace, submoduleConfigDir); | ||||||
|                     false, // add?
 |                     relativeSubmoduleConfigDir = relativeSubmoduleConfigDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
 | ||||||
|                     configPath); |                     const containerSubmoduleGitDir = path.posix.join('/github/workspace', relativeSubmoduleConfigDir); | ||||||
|  |                     yield this.git.config(`includeIf.gitdir:${containerSubmoduleGitDir}.path`, containerCredentialsPath, false, false, configPath); | ||||||
|                 } |                 } | ||||||
|                 if (this.settings.sshKey) { |                 if (this.settings.sshKey) { | ||||||
|                     // Configure core.sshCommand
 |                     // Configure core.sshCommand
 | ||||||
| @@ -313,10 +329,6 @@ class GitAuthHelper { | |||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|     /** |  | ||||||
|      * Configures SSH authentication by writing the SSH key and known hosts, |  | ||||||
|      * and setting up the GIT_SSH_COMMAND environment variable. |  | ||||||
|      */ |  | ||||||
|     configureSsh() { |     configureSsh() { | ||||||
|         return __awaiter(this, void 0, void 0, function* () { |         return __awaiter(this, void 0, void 0, function* () { | ||||||
|             if (!this.settings.sshKey) { |             if (!this.settings.sshKey) { | ||||||
| @@ -373,37 +385,21 @@ 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) { |     configureToken(globalConfig) { | ||||||
|         return __awaiter(this, void 0, void 0, function* () { |         return __awaiter(this, void 0, void 0, function* () { | ||||||
|             // Get the credentials config file path in RUNNER_TEMP
 |             // Get the credentials config file path in RUNNER_TEMP
 | ||||||
|             const credentialsConfigPath = this.getCredentialsConfigPath(); |             const credentialsConfigPath = yield this.getCredentialsConfigPath(); | ||||||
|             // Write placeholder to the separate credentials config file using git config.
 |             // Write placeholder to the separate credentials config file using git config.
 | ||||||
|             // This approach avoids the credential being captured by process creation audit events,
 |             // This approach avoids the credential being captured by process creation audit events,
 | ||||||
|             // which are commonly logged. For more information, refer to
 |             // 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
 |             // 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, // globalConfig?
 |             yield this.git.config(this.tokenConfigKey, this.tokenPlaceholderConfigValue, false, false, credentialsConfigPath); | ||||||
|             false, // add?
 |  | ||||||
|             credentialsConfigPath); |  | ||||||
|             // Replace the placeholder in the credentials config file
 |             // Replace the placeholder in the credentials config file
 | ||||||
|             let content = (yield fs.promises.readFile(credentialsConfigPath)).toString(); |             yield this.replaceTokenPlaceholder(credentialsConfigPath); | ||||||
|             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
 |             // Add include or includeIf to reference the credentials config
 | ||||||
|             if (globalConfig) { |             if (globalConfig) { | ||||||
|                 // Global config file is temporary
 |                 // Global config file is temporary
 | ||||||
|                 yield this.git.config('include.path', credentialsConfigPath, true // globalConfig?
 |                 yield this.git.config('include.path', credentialsConfigPath, true); | ||||||
|                 ); |  | ||||||
|             } |             } | ||||||
|             else { |             else { | ||||||
|                 // Host git directory
 |                 // Host git directory
 | ||||||
| @@ -412,10 +408,11 @@ class GitAuthHelper { | |||||||
|                 // Configure host includeIf
 |                 // Configure host includeIf
 | ||||||
|                 const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`; |                 const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`; | ||||||
|                 yield this.git.config(hostIncludeKey, credentialsConfigPath); |                 yield this.git.config(hostIncludeKey, credentialsConfigPath); | ||||||
|  |                 this.credentialsIncludeKeys.push(hostIncludeKey); | ||||||
|                 // Container git directory
 |                 // Container git directory
 | ||||||
|                 const workingDirectory = this.git.getWorkingDirectory(); |  | ||||||
|                 const githubWorkspace = process.env['GITHUB_WORKSPACE']; |                 const githubWorkspace = process.env['GITHUB_WORKSPACE']; | ||||||
|                 assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined'); |                 assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined'); | ||||||
|  |                 const workingDirectory = this.git.getWorkingDirectory(); | ||||||
|                 let relativePath = path.relative(githubWorkspace, workingDirectory); |                 let relativePath = path.relative(githubWorkspace, workingDirectory); | ||||||
|                 relativePath = relativePath.replace(/\\/g, '/'); // Use forward slashes, even on Windows
 |                 relativePath = relativePath.replace(/\\/g, '/'); // Use forward slashes, even on Windows
 | ||||||
|                 const containerGitDir = path.posix.join('/github/workspace', relativePath, '.git'); |                 const containerGitDir = path.posix.join('/github/workspace', relativePath, '.git'); | ||||||
| @@ -424,37 +421,31 @@ class GitAuthHelper { | |||||||
|                 // Configure container includeIf
 |                 // Configure container includeIf
 | ||||||
|                 const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`; |                 const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`; | ||||||
|                 yield this.git.config(containerIncludeKey, containerCredentialsPath); |                 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. |         return __awaiter(this, void 0, void 0, function* () { | ||||||
|      * @returns The absolute path to the credentials config file |             assert.ok(configPath, 'configPath is not defined'); | ||||||
|      */ |             let content = (yield fs.promises.readFile(configPath)).toString(); | ||||||
|     getCredentialsConfigPath() { |             const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue); | ||||||
|         if (this.credentialsConfigPath) { |             if (placeholderIndex < 0 || | ||||||
|             return this.credentialsConfigPath; |                 placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)) { | ||||||
|         } |                 throw new Error(`Unable to replace auth placeholder in ${configPath}`); | ||||||
|         const runnerTemp = process.env['RUNNER_TEMP'] || ''; |             } | ||||||
|         assert.ok(runnerTemp, 'RUNNER_TEMP is not defined'); |             assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined'); | ||||||
|         // Create a unique filename for this checkout instance
 |             content = content.replace(this.tokenPlaceholderConfigValue, this.tokenConfigValue); | ||||||
|         const configFileName = `git-credentials-${(0, uuid_1.v4)()}.config`; |             yield fs.promises.writeFile(configPath, content); | ||||||
|         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() { |     removeSsh() { | ||||||
|         return __awaiter(this, void 0, void 0, function* () { |         return __awaiter(this, void 0, void 0, function* () { | ||||||
|             var _a, _b; |             var _a; | ||||||
|             // SSH key
 |             // SSH key
 | ||||||
|             const keyPath = this.sshKeyPath || stateHelper.SshKeyPath; |             const keyPath = this.sshKeyPath || stateHelper.SshKeyPath; | ||||||
|             if (keyPath) { |             if (keyPath) { | ||||||
|                 try { |                 try { | ||||||
|                     core.info(`Removing SSH key '${keyPath}'`); |  | ||||||
|                     yield io.rmRF(keyPath); |                     yield io.rmRF(keyPath); | ||||||
|                 } |                 } | ||||||
|                 catch (err) { |                 catch (err) { | ||||||
| @@ -466,68 +457,42 @@ class GitAuthHelper { | |||||||
|             const knownHostsPath = this.sshKnownHostsPath || stateHelper.SshKnownHostsPath; |             const knownHostsPath = this.sshKnownHostsPath || stateHelper.SshKnownHostsPath; | ||||||
|             if (knownHostsPath) { |             if (knownHostsPath) { | ||||||
|                 try { |                 try { | ||||||
|                     core.info(`Removing SSH known hosts '${knownHostsPath}'`); |  | ||||||
|                     yield io.rmRF(knownHostsPath); |                     yield io.rmRF(knownHostsPath); | ||||||
|                 } |                 } | ||||||
|                 catch (err) { |                 catch (_b) { | ||||||
|                     core.debug(`${(_b = err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err}`); |                     // Intentionally empty
 | ||||||
|                     core.warning(`Failed to remove SSH known hosts '${knownHostsPath}'`); |  | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             // SSH command
 |             // SSH command
 | ||||||
|             core.info('Removing SSH command configuration'); |  | ||||||
|             yield this.removeGitConfig(SSH_COMMAND_KEY); |             yield this.removeGitConfig(SSH_COMMAND_KEY); | ||||||
|             yield this.removeSubmoduleGitConfig(SSH_COMMAND_KEY); |             yield this.removeSubmoduleGitConfig(SSH_COMMAND_KEY); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|     /** |  | ||||||
|      * Removes token-based authentication by cleaning up HTTP headers, |  | ||||||
|      * includeIf entries, and credentials config files. |  | ||||||
|      */ |  | ||||||
|     removeToken() { |     removeToken() { | ||||||
|         return __awaiter(this, void 0, void 0, function* () { |         return __awaiter(this, void 0, void 0, function* () { | ||||||
|             var _a; |             var _a; | ||||||
|             // Remove HTTP extra header
 |             // Remove HTTP extra header
 | ||||||
|             core.info('Removing HTTP extra header'); |  | ||||||
|             yield this.removeGitConfig(this.tokenConfigKey); |             yield this.removeGitConfig(this.tokenConfigKey); | ||||||
|             yield this.removeSubmoduleGitConfig(this.tokenConfigKey); |             yield this.removeSubmoduleGitConfig(this.tokenConfigKey); | ||||||
|             // Collect credentials config paths that need to be removed
 |             // Remove includeIf
 | ||||||
|             const credentialsPaths = new Set(); |             for (const includeKey of this.credentialsIncludeKeys) { | ||||||
|             // Remove includeIf entries that point to git-credentials-*.config files
 |                 yield this.removeGitConfig(includeKey); | ||||||
|             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
 |  | ||||||
|             const submoduleConfigPaths = yield this.git.getSubmoduleConfigPaths(true); |  | ||||||
|             for (const configPath of submoduleConfigPaths) { |  | ||||||
|                 const submoduleCredentialsPaths = yield this.removeIncludeIfCredentials(configPath); |  | ||||||
|                 submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path)); |  | ||||||
|             } |             } | ||||||
|             // Remove credentials config files
 |             this.credentialsIncludeKeys = []; | ||||||
|             for (const credentialsPath of credentialsPaths) { |             // Remove submodule includeIf
 | ||||||
|                 // Only remove credentials config files if they are under RUNNER_TEMP
 |             yield this.git.submoduleForeach(`sh -c "git config --local --get-regexp '^includeIf\\.' && git config --local --remove-section includeIf || :"`, true); | ||||||
|                 const runnerTemp = process.env['RUNNER_TEMP']; |             // Remove credentials config file
 | ||||||
|                 assert.ok(runnerTemp, 'RUNNER_TEMP is not defined'); |             if (this.credentialsConfigPath) { | ||||||
|                 if (credentialsPath.startsWith(runnerTemp)) { |                 try { | ||||||
|                     try { |                     yield io.rmRF(this.credentialsConfigPath); | ||||||
|                         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}'`); |  | ||||||
|                     } |  | ||||||
|                 } |                 } | ||||||
|                 else { |                 catch (err) { | ||||||
|                     core.debug(`Skipping removal of credentials config '${credentialsPath}' - not under RUNNER_TEMP`); |                     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}'`); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|     /** |  | ||||||
|      * Removes a git config key from the local repository config. |  | ||||||
|      * @param configKey The git config key to remove |  | ||||||
|      */ |  | ||||||
|     removeGitConfig(configKey) { |     removeGitConfig(configKey) { | ||||||
|         return __awaiter(this, void 0, void 0, function* () { |         return __awaiter(this, void 0, void 0, function* () { | ||||||
|             if ((yield this.git.configExists(configKey)) && |             if ((yield this.git.configExists(configKey)) && | ||||||
| @@ -537,10 +502,6 @@ class GitAuthHelper { | |||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|     /** |  | ||||||
|      * Removes a git config key from all submodule configs. |  | ||||||
|      * @param configKey The git config key to remove |  | ||||||
|      */ |  | ||||||
|     removeSubmoduleGitConfig(configKey) { |     removeSubmoduleGitConfig(configKey) { | ||||||
|         return __awaiter(this, void 0, void 0, function* () { |         return __awaiter(this, void 0, void 0, function* () { | ||||||
|             const pattern = regexpHelper.escape(configKey); |             const pattern = regexpHelper.escape(configKey); | ||||||
| @@ -549,53 +510,6 @@ class GitAuthHelper { | |||||||
|             `sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`, true); |             `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 |  | ||||||
|      * @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, // globalConfig?
 |  | ||||||
|                 configPath); |  | ||||||
|                 for (const key of keys) { |  | ||||||
|                     // Get all values for this key
 |  | ||||||
|                     const values = yield this.git.tryGetConfigValues(key, false, // globalConfig?
 |  | ||||||
|                     configPath); |  | ||||||
|                     if (values.length > 0) { |  | ||||||
|                         // Remove only values that match git-credentials-<uuid>.config pattern
 |  | ||||||
|                         for (const value of values) { |  | ||||||
|                             if (this.testCredentialsConfigPath(value)) { |  | ||||||
|                                 credentialsPaths.add(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}`); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             return Array.from(credentialsPaths); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|     /** |  | ||||||
|      * 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); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @@ -878,16 +792,6 @@ class GitCommandManager { | |||||||
|             throw new Error('Unexpected output when retrieving default branch'); |             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() { |     getWorkingDirectory() { | ||||||
|         return this.workingDirectory; |         return this.workingDirectory; | ||||||
|     } |     } | ||||||
| @@ -1018,20 +922,6 @@ class GitCommandManager { | |||||||
|             return output.exitCode === 0; |             return output.exitCode === 0; | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|     tryConfigUnsetValue(configKey, configValue, globalConfig, configFile) { |  | ||||||
|         return __awaiter(this, void 0, void 0, function* () { |  | ||||||
|             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; |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|     tryDisableAutomaticGarbageCollection() { |     tryDisableAutomaticGarbageCollection() { | ||||||
|         return __awaiter(this, void 0, void 0, function* () { |         return __awaiter(this, void 0, void 0, function* () { | ||||||
|             const output = yield this.execGit(['config', '--local', 'gc.auto', '0'], true); |             const output = yield this.execGit(['config', '--local', 'gc.auto', '0'], true); | ||||||
| @@ -1051,46 +941,6 @@ class GitCommandManager { | |||||||
|             return stdout; |             return stdout; | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|     tryGetConfigValues(configKey, globalConfig, configFile) { |  | ||||||
|         return __awaiter(this, void 0, void 0, function* () { |  | ||||||
|             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, configFile) { |  | ||||||
|         return __awaiter(this, void 0, void 0, function* () { |  | ||||||
|             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 []; |  | ||||||
|             } |  | ||||||
|             return output.stdout |  | ||||||
|                 .trim() |  | ||||||
|                 .split('\n') |  | ||||||
|                 .filter(key => key.trim()); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|     tryReset() { |     tryReset() { | ||||||
|         return __awaiter(this, void 0, void 0, function* () { |         return __awaiter(this, void 0, void 0, function* () { | ||||||
|             const output = yield this.execGit(['reset', '--hard', 'HEAD'], true); |             const output = yield this.execGit(['reset', '--hard', 'HEAD'], true); | ||||||
|   | |||||||
| @@ -44,6 +44,7 @@ class GitAuthHelper { | |||||||
|   private sshKnownHostsPath = '' |   private sshKnownHostsPath = '' | ||||||
|   private temporaryHomePath = '' |   private temporaryHomePath = '' | ||||||
|   private credentialsConfigPath = '' // Path to separate credentials config file in RUNNER_TEMP |   private credentialsConfigPath = '' // Path to separate credentials config file in RUNNER_TEMP | ||||||
|  |   private credentialsIncludeKeys: string[] = [] // Track includeIf config keys for cleanup | ||||||
|  |  | ||||||
|   constructor( |   constructor( | ||||||
|     gitCommandManager: IGitCommandManager, |     gitCommandManager: IGitCommandManager, | ||||||
| @@ -82,6 +83,22 @@ class GitAuthHelper { | |||||||
|     await this.configureToken() |     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> { |   async configureTempGlobalConfig(): Promise<string> { | ||||||
|     // Already setup global config |     // Already setup global config | ||||||
|     if (this.temporaryHomePath?.length > 0) { |     if (this.temporaryHomePath?.length > 0) { | ||||||
| @@ -136,12 +153,7 @@ class GitAuthHelper { | |||||||
|       await this.git.tryConfigUnset(this.insteadOfKey, true) |       await this.git.tryConfigUnset(this.insteadOfKey, true) | ||||||
|       if (!this.settings.sshKey) { |       if (!this.settings.sshKey) { | ||||||
|         for (const insteadOfValue of this.insteadOfValues) { |         for (const insteadOfValue of this.insteadOfValues) { | ||||||
|           await this.git.config( |           await this.git.config(this.insteadOfKey, insteadOfValue, true, true) | ||||||
|             this.insteadOfKey, |  | ||||||
|             insteadOfValue, |  | ||||||
|             true, // globalConfig? |  | ||||||
|             true // add? |  | ||||||
|           ) |  | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
| @@ -159,8 +171,8 @@ class GitAuthHelper { | |||||||
|     await this.removeSubmoduleGitConfig(this.insteadOfKey) |     await this.removeSubmoduleGitConfig(this.insteadOfKey) | ||||||
|  |  | ||||||
|     if (this.settings.persistCredentials) { |     if (this.settings.persistCredentials) { | ||||||
|       // Get the credentials config file path in RUNNER_TEMP |       // Credentials config path | ||||||
|       const credentialsConfigPath = this.getCredentialsConfigPath() |       const credentialsConfigPath = await this.getCredentialsConfigPath() | ||||||
|  |  | ||||||
|       // Container credentials config path |       // Container credentials config path | ||||||
|       const containerCredentialsPath = path.posix.join( |       const containerCredentialsPath = path.posix.join( | ||||||
| @@ -168,46 +180,62 @@ class GitAuthHelper { | |||||||
|         path.basename(credentialsConfigPath) |         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. |       // Get submodule config file paths. | ||||||
|       const configPaths = await this.git.getSubmoduleConfigPaths( |       // 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 |         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. |       // For each submodule, configure includeIf entries pointing to the shared credentials file. | ||||||
|       // Configure both host and container paths to support Docker container actions. |       // Configure both host and container paths to support Docker container actions. | ||||||
|       for (const configPath of configPaths) { |       for (const configPath of configPaths) { | ||||||
|         // Submodule Git directory |         // The config file is at .git/modules/submodule-name/config | ||||||
|         let submoduleGitDir = path.dirname(configPath) // The config file is at .git/modules/submodule-name/config |         let submoduleConfigDir = path.dirname(configPath) | ||||||
|         submoduleGitDir = submoduleGitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows |         submoduleConfigDir = submoduleConfigDir.replace(/\\/g, '/') // Use forward slashes, even on Windows | ||||||
|  |  | ||||||
|         // Configure host includeIf |         // Configure host includeIf | ||||||
|         await this.git.config( |         await this.git.config( | ||||||
|           `includeIf.gitdir:${submoduleGitDir}.path`, |           `includeIf.gitdir:${submoduleConfigDir}.path`, | ||||||
|           credentialsConfigPath, |           credentialsConfigPath, | ||||||
|           false, // globalConfig? |           false, | ||||||
|           false, // add? |           false, | ||||||
|           configPath |           configPath | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         // Container submodule git directory |         // Configure container includeIf | ||||||
|         const githubWorkspace = process.env['GITHUB_WORKSPACE'] |         let relativeSubmoduleConfigDir = path.relative( | ||||||
|         assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined') |  | ||||||
|         let relativeSubmoduleGitDir = path.relative( |  | ||||||
|           githubWorkspace, |           githubWorkspace, | ||||||
|           submoduleGitDir |           submoduleConfigDir | ||||||
|         ) |         ) | ||||||
|         relativeSubmoduleGitDir = relativeSubmoduleGitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows |         relativeSubmoduleConfigDir = relativeSubmoduleConfigDir.replace( | ||||||
|  |           /\\/g, | ||||||
|  |           '/' | ||||||
|  |         ) // Use forward slashes, even on Windows | ||||||
|         const containerSubmoduleGitDir = path.posix.join( |         const containerSubmoduleGitDir = path.posix.join( | ||||||
|           '/github/workspace', |           '/github/workspace', | ||||||
|           relativeSubmoduleGitDir |           relativeSubmoduleConfigDir | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         // Configure container includeIf |  | ||||||
|         await this.git.config( |         await this.git.config( | ||||||
|           `includeIf.gitdir:${containerSubmoduleGitDir}.path`, |           `includeIf.gitdir:${containerSubmoduleGitDir}.path`, | ||||||
|           containerCredentialsPath, |           containerCredentialsPath, | ||||||
|           false, // globalConfig? |           false, | ||||||
|           false, // add? |           false, | ||||||
|           configPath |           configPath | ||||||
|         ) |         ) | ||||||
|       } |       } | ||||||
| @@ -243,10 +271,6 @@ 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<void> { |   private async configureSsh(): Promise<void> { | ||||||
|     if (!this.settings.sshKey) { |     if (!this.settings.sshKey) { | ||||||
|       return |       return | ||||||
| @@ -318,14 +342,9 @@ 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<void> { |   private async configureToken(globalConfig?: boolean): Promise<void> { | ||||||
|     // Get the credentials config file path in RUNNER_TEMP |     // Get the credentials config file path in RUNNER_TEMP | ||||||
|     const credentialsConfigPath = this.getCredentialsConfigPath() |     const credentialsConfigPath = await this.getCredentialsConfigPath() | ||||||
|  |  | ||||||
|     // Write placeholder to the separate credentials config file using git config. |     // Write placeholder to the separate credentials config file using git config. | ||||||
|     // This approach avoids the credential being captured by process creation audit events, |     // This approach avoids the credential being captured by process creation audit events, | ||||||
| @@ -334,37 +353,18 @@ class GitAuthHelper { | |||||||
|     await this.git.config( |     await this.git.config( | ||||||
|       this.tokenConfigKey, |       this.tokenConfigKey, | ||||||
|       this.tokenPlaceholderConfigValue, |       this.tokenPlaceholderConfigValue, | ||||||
|       false, // globalConfig? |       false, | ||||||
|       false, // add? |       false, | ||||||
|       credentialsConfigPath |       credentialsConfigPath | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     // Replace the placeholder in the credentials config file |     // Replace the placeholder in the credentials config file | ||||||
|     let content = (await fs.promises.readFile(credentialsConfigPath)).toString() |     await this.replaceTokenPlaceholder(credentialsConfigPath) | ||||||
|     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 |     // Add include or includeIf to reference the credentials config | ||||||
|     if (globalConfig) { |     if (globalConfig) { | ||||||
|       // Global config file is temporary |       // Global config file is temporary | ||||||
|       await this.git.config( |       await this.git.config('include.path', credentialsConfigPath, true) | ||||||
|         'include.path', |  | ||||||
|         credentialsConfigPath, |  | ||||||
|         true // globalConfig? |  | ||||||
|       ) |  | ||||||
|     } else { |     } else { | ||||||
|       // Host git directory |       // Host git directory | ||||||
|       let gitDir = path.join(this.git.getWorkingDirectory(), '.git') |       let gitDir = path.join(this.git.getWorkingDirectory(), '.git') | ||||||
| @@ -373,11 +373,12 @@ class GitAuthHelper { | |||||||
|       // Configure host includeIf |       // Configure host includeIf | ||||||
|       const hostIncludeKey = `includeIf.gitdir:${gitDir}.path` |       const hostIncludeKey = `includeIf.gitdir:${gitDir}.path` | ||||||
|       await this.git.config(hostIncludeKey, credentialsConfigPath) |       await this.git.config(hostIncludeKey, credentialsConfigPath) | ||||||
|  |       this.credentialsIncludeKeys.push(hostIncludeKey) | ||||||
|  |  | ||||||
|       // Container git directory |       // Container git directory | ||||||
|       const workingDirectory = this.git.getWorkingDirectory() |  | ||||||
|       const githubWorkspace = process.env['GITHUB_WORKSPACE'] |       const githubWorkspace = process.env['GITHUB_WORKSPACE'] | ||||||
|       assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined') |       assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined') | ||||||
|  |       const workingDirectory = this.git.getWorkingDirectory() | ||||||
|       let relativePath = path.relative(githubWorkspace, workingDirectory) |       let relativePath = path.relative(githubWorkspace, workingDirectory) | ||||||
|       relativePath = relativePath.replace(/\\/g, '/') // Use forward slashes, even on Windows |       relativePath = relativePath.replace(/\\/g, '/') // Use forward slashes, even on Windows | ||||||
|       const containerGitDir = path.posix.join( |       const containerGitDir = path.posix.join( | ||||||
| @@ -395,39 +396,33 @@ class GitAuthHelper { | |||||||
|       // Configure container includeIf |       // Configure container includeIf | ||||||
|       const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path` |       const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path` | ||||||
|       await this.git.config(containerIncludeKey, containerCredentialsPath) |       await this.git.config(containerIncludeKey, containerCredentialsPath) | ||||||
|  |       this.credentialsIncludeKeys.push(containerIncludeKey) | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   private async replaceTokenPlaceholder(configPath: string): Promise<void> { | ||||||
|    * Gets or creates the path to the credentials config file in RUNNER_TEMP. |     assert.ok(configPath, 'configPath is not defined') | ||||||
|    * @returns The absolute path to the credentials config file |     let content = (await fs.promises.readFile(configPath)).toString() | ||||||
|    */ |     const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue) | ||||||
|   private getCredentialsConfigPath(): string { |     if ( | ||||||
|     if (this.credentialsConfigPath) { |       placeholderIndex < 0 || | ||||||
|       return this.credentialsConfigPath |       placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue) | ||||||
|  |     ) { | ||||||
|  |       throw new Error(`Unable to replace auth placeholder in ${configPath}`) | ||||||
|     } |     } | ||||||
|  |     assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined') | ||||||
|     const runnerTemp = process.env['RUNNER_TEMP'] || '' |     content = content.replace( | ||||||
|     assert.ok(runnerTemp, 'RUNNER_TEMP is not defined') |       this.tokenPlaceholderConfigValue, | ||||||
|  |       this.tokenConfigValue | ||||||
|     // Create a unique filename for this checkout instance |     ) | ||||||
|     const configFileName = `git-credentials-${uuid()}.config` |     await fs.promises.writeFile(configPath, content) | ||||||
|     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<void> { |   private async removeSsh(): Promise<void> { | ||||||
|     // SSH key |     // SSH key | ||||||
|     const keyPath = this.sshKeyPath || stateHelper.SshKeyPath |     const keyPath = this.sshKeyPath || stateHelper.SshKeyPath | ||||||
|     if (keyPath) { |     if (keyPath) { | ||||||
|       try { |       try { | ||||||
|         core.info(`Removing SSH key '${keyPath}'`) |  | ||||||
|         await io.rmRF(keyPath) |         await io.rmRF(keyPath) | ||||||
|       } catch (err) { |       } catch (err) { | ||||||
|         core.debug(`${(err as any)?.message ?? err}`) |         core.debug(`${(err as any)?.message ?? err}`) | ||||||
| @@ -440,73 +435,47 @@ class GitAuthHelper { | |||||||
|       this.sshKnownHostsPath || stateHelper.SshKnownHostsPath |       this.sshKnownHostsPath || stateHelper.SshKnownHostsPath | ||||||
|     if (knownHostsPath) { |     if (knownHostsPath) { | ||||||
|       try { |       try { | ||||||
|         core.info(`Removing SSH known hosts '${knownHostsPath}'`) |  | ||||||
|         await io.rmRF(knownHostsPath) |         await io.rmRF(knownHostsPath) | ||||||
|       } catch (err) { |       } catch { | ||||||
|         core.debug(`${(err as any)?.message ?? err}`) |         // Intentionally empty | ||||||
|         core.warning(`Failed to remove SSH known hosts '${knownHostsPath}'`) |  | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // SSH command |     // SSH command | ||||||
|     core.info('Removing SSH command configuration') |  | ||||||
|     await this.removeGitConfig(SSH_COMMAND_KEY) |     await this.removeGitConfig(SSH_COMMAND_KEY) | ||||||
|     await this.removeSubmoduleGitConfig(SSH_COMMAND_KEY) |     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<void> { |   private async removeToken(): Promise<void> { | ||||||
|     // Remove HTTP extra header |     // Remove HTTP extra header | ||||||
|     core.info('Removing HTTP extra header') |  | ||||||
|     await this.removeGitConfig(this.tokenConfigKey) |     await this.removeGitConfig(this.tokenConfigKey) | ||||||
|     await this.removeSubmoduleGitConfig(this.tokenConfigKey) |     await this.removeSubmoduleGitConfig(this.tokenConfigKey) | ||||||
|  |  | ||||||
|     // Collect credentials config paths that need to be removed |     // Remove includeIf | ||||||
|     const credentialsPaths = new Set<string>() |     for (const includeKey of this.credentialsIncludeKeys) { | ||||||
|  |       await this.removeGitConfig(includeKey) | ||||||
|     // 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)) |  | ||||||
|  |  | ||||||
|     // Remove submodule includeIf entries that point to git-credentials-*.config files |  | ||||||
|     const submoduleConfigPaths = await this.git.getSubmoduleConfigPaths(true) |  | ||||||
|     for (const configPath of submoduleConfigPaths) { |  | ||||||
|       const submoduleCredentialsPaths = |  | ||||||
|         await this.removeIncludeIfCredentials(configPath) |  | ||||||
|       submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path)) |  | ||||||
|     } |     } | ||||||
|  |     this.credentialsIncludeKeys = [] | ||||||
|  |  | ||||||
|     // Remove credentials config files |     // Remove submodule includeIf | ||||||
|     for (const credentialsPath of credentialsPaths) { |     await this.git.submoduleForeach( | ||||||
|       // Only remove credentials config files if they are under RUNNER_TEMP |       `sh -c "git config --local --get-regexp '^includeIf\\.' && git config --local --remove-section includeIf || :"`, | ||||||
|       const runnerTemp = process.env['RUNNER_TEMP'] |       true | ||||||
|       assert.ok(runnerTemp, 'RUNNER_TEMP is not defined') |     ) | ||||||
|       if (credentialsPath.startsWith(runnerTemp)) { |  | ||||||
|         try { |     // Remove credentials config file | ||||||
|           core.info(`Removing credentials config '${credentialsPath}'`) |     if (this.credentialsConfigPath) { | ||||||
|           await io.rmRF(credentialsPath) |       try { | ||||||
|         } catch (err) { |         await io.rmRF(this.credentialsConfigPath) | ||||||
|           core.debug(`${(err as any)?.message ?? err}`) |       } catch (err) { | ||||||
|           core.warning( |         core.debug(`${(err as any)?.message ?? err}`) | ||||||
|             `Failed to remove credentials config '${credentialsPath}'` |         core.warning( | ||||||
|           ) |           `Failed to remove credentials config '${this.credentialsConfigPath}'` | ||||||
|         } |  | ||||||
|       } else { |  | ||||||
|         core.debug( |  | ||||||
|           `Skipping removal of credentials config '${credentialsPath}' - not under RUNNER_TEMP` |  | ||||||
|         ) |         ) | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Removes a git config key from the local repository config. |  | ||||||
|    * @param configKey The git config key to remove |  | ||||||
|    */ |  | ||||||
|   private async removeGitConfig(configKey: string): Promise<void> { |   private async removeGitConfig(configKey: string): Promise<void> { | ||||||
|     if ( |     if ( | ||||||
|       (await this.git.configExists(configKey)) && |       (await this.git.configExists(configKey)) && | ||||||
| @@ -517,10 +486,6 @@ 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<void> { |   private async removeSubmoduleGitConfig(configKey: string): Promise<void> { | ||||||
|     const pattern = regexpHelper.escape(configKey) |     const pattern = regexpHelper.escape(configKey) | ||||||
|     await this.git.submoduleForeach( |     await this.git.submoduleForeach( | ||||||
| @@ -529,60 +494,4 @@ class GitAuthHelper { | |||||||
|       true |       true | ||||||
|     ) |     ) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * 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<string[]> { |  | ||||||
|     const credentialsPaths = new Set<string>() |  | ||||||
|  |  | ||||||
|     try { |  | ||||||
|       // Get all includeIf.gitdir keys |  | ||||||
|       const keys = await this.git.tryGetConfigKeys( |  | ||||||
|         '^includeIf\\.gitdir:', |  | ||||||
|         false, // globalConfig? |  | ||||||
|         configPath |  | ||||||
|       ) |  | ||||||
|  |  | ||||||
|       for (const key of keys) { |  | ||||||
|         // Get all values for this key |  | ||||||
|         const values = await this.git.tryGetConfigValues( |  | ||||||
|           key, |  | ||||||
|           false, // globalConfig? |  | ||||||
|           configPath |  | ||||||
|         ) |  | ||||||
|         if (values.length > 0) { |  | ||||||
|           // Remove only values that match git-credentials-<uuid>.config pattern |  | ||||||
|           for (const value of values) { |  | ||||||
|             if (this.testCredentialsConfigPath(value)) { |  | ||||||
|               credentialsPaths.add(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}`) |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return Array.from(credentialsPaths) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * 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) |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -42,7 +42,6 @@ export interface IGitCommandManager { | |||||||
|     } |     } | ||||||
|   ): Promise<void> |   ): Promise<void> | ||||||
|   getDefaultBranch(repositoryUrl: string): Promise<string> |   getDefaultBranch(repositoryUrl: string): Promise<string> | ||||||
|   getSubmoduleConfigPaths(recursive: boolean): Promise<string[]> |  | ||||||
|   getWorkingDirectory(): string |   getWorkingDirectory(): string | ||||||
|   init(): Promise<void> |   init(): Promise<void> | ||||||
|   isDetached(): Promise<boolean> |   isDetached(): Promise<boolean> | ||||||
| @@ -61,24 +60,8 @@ export interface IGitCommandManager { | |||||||
|   tagExists(pattern: string): Promise<boolean> |   tagExists(pattern: string): Promise<boolean> | ||||||
|   tryClean(): Promise<boolean> |   tryClean(): Promise<boolean> | ||||||
|   tryConfigUnset(configKey: string, globalConfig?: boolean): Promise<boolean> |   tryConfigUnset(configKey: string, globalConfig?: boolean): Promise<boolean> | ||||||
|   tryConfigUnsetValue( |  | ||||||
|     configKey: string, |  | ||||||
|     configValue: string, |  | ||||||
|     globalConfig?: boolean, |  | ||||||
|     configFile?: string |  | ||||||
|   ): Promise<boolean> |  | ||||||
|   tryDisableAutomaticGarbageCollection(): Promise<boolean> |   tryDisableAutomaticGarbageCollection(): Promise<boolean> | ||||||
|   tryGetFetchUrl(): Promise<string> |   tryGetFetchUrl(): Promise<string> | ||||||
|   tryGetConfigValues( |  | ||||||
|     configKey: string, |  | ||||||
|     globalConfig?: boolean, |  | ||||||
|     configFile?: string |  | ||||||
|   ): Promise<string[]> |  | ||||||
|   tryGetConfigKeys( |  | ||||||
|     pattern: string, |  | ||||||
|     globalConfig?: boolean, |  | ||||||
|     configFile?: string |  | ||||||
|   ): Promise<string[]> |  | ||||||
|   tryReset(): Promise<boolean> |   tryReset(): Promise<boolean> | ||||||
|   version(): Promise<GitVersion> |   version(): Promise<GitVersion> | ||||||
| } | } | ||||||
| @@ -347,21 +330,6 @@ class GitCommandManager { | |||||||
|     throw new Error('Unexpected output when retrieving default branch') |     throw new Error('Unexpected output when retrieving default branch') | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async getSubmoduleConfigPaths(recursive: boolean): Promise<string[]> { |  | ||||||
|     // 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 { |   getWorkingDirectory(): string { | ||||||
|     return this.workingDirectory |     return this.workingDirectory | ||||||
|   } |   } | ||||||
| @@ -494,24 +462,6 @@ class GitCommandManager { | |||||||
|     return output.exitCode === 0 |     return output.exitCode === 0 | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async tryConfigUnsetValue( |  | ||||||
|     configKey: string, |  | ||||||
|     configValue: string, |  | ||||||
|     globalConfig?: boolean, |  | ||||||
|     configFile?: string |  | ||||||
|   ): Promise<boolean> { |  | ||||||
|     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 |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   async tryDisableAutomaticGarbageCollection(): Promise<boolean> { |   async tryDisableAutomaticGarbageCollection(): Promise<boolean> { | ||||||
|     const output = await this.execGit( |     const output = await this.execGit( | ||||||
|       ['config', '--local', 'gc.auto', '0'], |       ['config', '--local', 'gc.auto', '0'], | ||||||
| @@ -538,56 +488,6 @@ class GitCommandManager { | |||||||
|     return stdout |     return stdout | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async tryGetConfigValues( |  | ||||||
|     configKey: string, |  | ||||||
|     globalConfig?: boolean, |  | ||||||
|     configFile?: string |  | ||||||
|   ): Promise<string[]> { |  | ||||||
|     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 [] |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return output.stdout |  | ||||||
|       .trim() |  | ||||||
|       .split('\n') |  | ||||||
|       .filter(value => value.trim()) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   async tryGetConfigKeys( |  | ||||||
|     pattern: string, |  | ||||||
|     globalConfig?: boolean, |  | ||||||
|     configFile?: string |  | ||||||
|   ): Promise<string[]> { |  | ||||||
|     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 [] |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return output.stdout |  | ||||||
|       .trim() |  | ||||||
|       .split('\n') |  | ||||||
|       .filter(key => key.trim()) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   async tryReset(): Promise<boolean> { |   async tryReset(): Promise<boolean> { | ||||||
|     const output = await this.execGit(['reset', '--hard', 'HEAD'], true) |     const output = await this.execGit(['reset', '--hard', 'HEAD'], true) | ||||||
|     return output.exitCode === 0 |     return output.exitCode === 0 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user