mirror of
				https://github.com/actions/checkout.git
				synced 2025-10-31 10:24:20 +08:00 
			
		
		
		
	add ssh support (#163)
This commit is contained in:
		
							parent
							
								
									80602fafba
								
							
						
					
					
						commit
						b2e6b7ed13
					
				
							
								
								
									
										40
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								README.md
									
									
									
									
									
								
							| @ -45,14 +45,40 @@ Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous | |||||||
|     # Otherwise, defaults to `master`. |     # Otherwise, defaults to `master`. | ||||||
|     ref: '' |     ref: '' | ||||||
| 
 | 
 | ||||||
|     # Auth token used to fetch the repository. The token is stored in the local git |     # Personal access token (PAT) used to fetch the repository. The PAT is configured | ||||||
|     # config, which enables your scripts to run authenticated git commands. The |     # with the local git config, which enables your scripts to run authenticated git | ||||||
|     # post-job step removes the token from the git config. [Learn more about creating |     # commands. The post-job step removes the PAT. | ||||||
|     # and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets) |     # | ||||||
|  |     # We recommend creating a service account with the least permissions necessary. | ||||||
|  |     # Also when generating a new PAT, select the least scopes necessary. | ||||||
|  |     # | ||||||
|  |     # [Learn more about creating and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets) | ||||||
|  |     # | ||||||
|     # Default: ${{ github.token }} |     # Default: ${{ github.token }} | ||||||
|     token: '' |     token: '' | ||||||
| 
 | 
 | ||||||
|     # Whether to persist the token in the git config |     # SSH key used to fetch the repository. SSH key is configured with the local git | ||||||
|  |     # config, which enables your scripts to run authenticated git commands. The | ||||||
|  |     # post-job step removes the SSH key. | ||||||
|  |     # | ||||||
|  |     # We recommend creating a service account with the least permissions necessary. | ||||||
|  |     # | ||||||
|  |     # [Learn more about creating and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets) | ||||||
|  |     ssh-key: '' | ||||||
|  | 
 | ||||||
|  |     # Known hosts in addition to the user and global host key database. The public SSH | ||||||
|  |     # keys for a host may be obtained using the utility `ssh-keyscan`. For example, | ||||||
|  |     # `ssh-keyscan github.com`. The public key for github.com is always implicitly | ||||||
|  |     # added. | ||||||
|  |     ssh-known-hosts: '' | ||||||
|  | 
 | ||||||
|  |     # Whether to perform strict host key checking. When true, adds the options | ||||||
|  |     # `StrictHostKeyChecking=yes` and `CheckHostIP=no` to the SSH command line. Use | ||||||
|  |     # the input `ssh-known-hosts` to configure additional hosts. | ||||||
|  |     # Default: true | ||||||
|  |     ssh-strict: '' | ||||||
|  | 
 | ||||||
|  |     # Whether to configure the token or SSH key with the local git config | ||||||
|     # Default: true |     # Default: true | ||||||
|     persist-credentials: '' |     persist-credentials: '' | ||||||
| 
 | 
 | ||||||
| @ -73,6 +99,10 @@ Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous | |||||||
| 
 | 
 | ||||||
|     # Whether to checkout submodules: `true` to checkout submodules or `recursive` to |     # Whether to checkout submodules: `true` to checkout submodules or `recursive` to | ||||||
|     # recursively checkout submodules. |     # recursively checkout submodules. | ||||||
|  |     # | ||||||
|  |     # When the `ssh-key` input is not provided, SSH URLs beginning with | ||||||
|  |     # `git@github.com:` are converted to HTTPS. | ||||||
|  |     # | ||||||
|     # Default: false |     # Default: false | ||||||
|     submodules: '' |     submodules: '' | ||||||
| ``` | ``` | ||||||
|  | |||||||
| @ -2,10 +2,13 @@ import * as core from '@actions/core' | |||||||
| import * as fs from 'fs' | import * as fs from 'fs' | ||||||
| import * as gitAuthHelper from '../lib/git-auth-helper' | import * as gitAuthHelper from '../lib/git-auth-helper' | ||||||
| import * as io from '@actions/io' | import * as io from '@actions/io' | ||||||
|  | import * as os from 'os' | ||||||
| import * as path from 'path' | import * as path from 'path' | ||||||
|  | import * as stateHelper from '../lib/state-helper' | ||||||
| import {IGitCommandManager} from '../lib/git-command-manager' | import {IGitCommandManager} from '../lib/git-command-manager' | ||||||
| import {IGitSourceSettings} from '../lib/git-source-settings' | import {IGitSourceSettings} from '../lib/git-source-settings' | ||||||
| 
 | 
 | ||||||
|  | const isWindows = process.platform === 'win32' | ||||||
| const testWorkspace = path.join(__dirname, '_temp', 'git-auth-helper') | const testWorkspace = path.join(__dirname, '_temp', 'git-auth-helper') | ||||||
| const originalRunnerTemp = process.env['RUNNER_TEMP'] | const originalRunnerTemp = process.env['RUNNER_TEMP'] | ||||||
| const originalHome = process.env['HOME'] | const originalHome = process.env['HOME'] | ||||||
| @ -16,9 +19,13 @@ let runnerTemp: string | |||||||
| let tempHomedir: string | let tempHomedir: string | ||||||
| let git: IGitCommandManager & {env: {[key: string]: string}} | let git: IGitCommandManager & {env: {[key: string]: string}} | ||||||
| let settings: IGitSourceSettings | let settings: IGitSourceSettings | ||||||
|  | let sshPath: string | ||||||
| 
 | 
 | ||||||
| describe('git-auth-helper tests', () => { | describe('git-auth-helper tests', () => { | ||||||
|   beforeAll(async () => { |   beforeAll(async () => { | ||||||
|  |     // SSH
 | ||||||
|  |     sshPath = await io.which('ssh') | ||||||
|  | 
 | ||||||
|     // Clear test workspace
 |     // Clear test workspace
 | ||||||
|     await io.rmRF(testWorkspace) |     await io.rmRF(testWorkspace) | ||||||
|   }) |   }) | ||||||
| @ -32,6 +39,12 @@ describe('git-auth-helper tests', () => { | |||||||
|     jest.spyOn(core, 'warning').mockImplementation(jest.fn()) |     jest.spyOn(core, 'warning').mockImplementation(jest.fn()) | ||||||
|     jest.spyOn(core, 'info').mockImplementation(jest.fn()) |     jest.spyOn(core, 'info').mockImplementation(jest.fn()) | ||||||
|     jest.spyOn(core, 'debug').mockImplementation(jest.fn()) |     jest.spyOn(core, 'debug').mockImplementation(jest.fn()) | ||||||
|  | 
 | ||||||
|  |     // Mock state helper
 | ||||||
|  |     jest.spyOn(stateHelper, 'setSshKeyPath').mockImplementation(jest.fn()) | ||||||
|  |     jest | ||||||
|  |       .spyOn(stateHelper, 'setSshKnownHostsPath') | ||||||
|  |       .mockImplementation(jest.fn()) | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|   afterEach(() => { |   afterEach(() => { | ||||||
| @ -108,6 +121,52 @@ describe('git-auth-helper tests', () => { | |||||||
|     } |     } | ||||||
|   ) |   ) | ||||||
| 
 | 
 | ||||||
|  |   const configureAuth_copiesUserKnownHosts = | ||||||
|  |     'configureAuth copies user known hosts' | ||||||
|  |   it(configureAuth_copiesUserKnownHosts, async () => { | ||||||
|  |     if (!sshPath) { | ||||||
|  |       process.stdout.write( | ||||||
|  |         `Skipped test "${configureAuth_copiesUserKnownHosts}". Executable 'ssh' not found in the PATH.\n` | ||||||
|  |       ) | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Arange
 | ||||||
|  |     await setup(configureAuth_copiesUserKnownHosts) | ||||||
|  |     expect(settings.sshKey).toBeTruthy() // sanity check
 | ||||||
|  | 
 | ||||||
|  |     // Mock fs.promises.readFile
 | ||||||
|  |     const realReadFile = fs.promises.readFile | ||||||
|  |     jest.spyOn(fs.promises, 'readFile').mockImplementation( | ||||||
|  |       async (file: any, options: any): Promise<Buffer> => { | ||||||
|  |         const userKnownHostsPath = path.join( | ||||||
|  |           os.homedir(), | ||||||
|  |           '.ssh', | ||||||
|  |           'known_hosts' | ||||||
|  |         ) | ||||||
|  |         if (file === userKnownHostsPath) { | ||||||
|  |           return Buffer.from('some-domain.com ssh-rsa ABCDEF') | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return await realReadFile(file, options) | ||||||
|  |       } | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     // Act
 | ||||||
|  |     const authHelper = gitAuthHelper.createAuthHelper(git, settings) | ||||||
|  |     await authHelper.configureAuth() | ||||||
|  | 
 | ||||||
|  |     // Assert known hosts
 | ||||||
|  |     const actualSshKnownHostsPath = await getActualSshKnownHostsPath() | ||||||
|  |     const actualSshKnownHostsContent = ( | ||||||
|  |       await fs.promises.readFile(actualSshKnownHostsPath) | ||||||
|  |     ).toString() | ||||||
|  |     expect(actualSshKnownHostsContent).toMatch( | ||||||
|  |       /some-domain\.com ssh-rsa ABCDEF/ | ||||||
|  |     ) | ||||||
|  |     expect(actualSshKnownHostsContent).toMatch(/github\.com ssh-rsa AAAAB3N/) | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|   const configureAuth_registersBasicCredentialAsSecret = |   const configureAuth_registersBasicCredentialAsSecret = | ||||||
|     'configureAuth registers basic credential as secret' |     'configureAuth registers basic credential as secret' | ||||||
|   it(configureAuth_registersBasicCredentialAsSecret, async () => { |   it(configureAuth_registersBasicCredentialAsSecret, async () => { | ||||||
| @ -129,6 +188,173 @@ describe('git-auth-helper tests', () => { | |||||||
|     expect(setSecretSpy).toHaveBeenCalledWith(expectedSecret) |     expect(setSecretSpy).toHaveBeenCalledWith(expectedSecret) | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|  |   const setsSshCommandEnvVarWhenPersistCredentialsFalse = | ||||||
|  |     'sets SSH command env var when persist-credentials false' | ||||||
|  |   it(setsSshCommandEnvVarWhenPersistCredentialsFalse, async () => { | ||||||
|  |     if (!sshPath) { | ||||||
|  |       process.stdout.write( | ||||||
|  |         `Skipped test "${setsSshCommandEnvVarWhenPersistCredentialsFalse}". Executable 'ssh' not found in the PATH.\n` | ||||||
|  |       ) | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Arrange
 | ||||||
|  |     await setup(setsSshCommandEnvVarWhenPersistCredentialsFalse) | ||||||
|  |     settings.persistCredentials = false | ||||||
|  |     const authHelper = gitAuthHelper.createAuthHelper(git, settings) | ||||||
|  | 
 | ||||||
|  |     // Act
 | ||||||
|  |     await authHelper.configureAuth() | ||||||
|  | 
 | ||||||
|  |     // Assert git env var
 | ||||||
|  |     const actualKeyPath = await getActualSshKeyPath() | ||||||
|  |     const actualKnownHostsPath = await getActualSshKnownHostsPath() | ||||||
|  |     const expectedSshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename( | ||||||
|  |       actualKeyPath | ||||||
|  |     )}" -o StrictHostKeyChecking=yes -o CheckHostIP=no -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename( | ||||||
|  |       actualKnownHostsPath | ||||||
|  |     )}"` | ||||||
|  |     expect(git.setEnvironmentVariable).toHaveBeenCalledWith( | ||||||
|  |       'GIT_SSH_COMMAND', | ||||||
|  |       expectedSshCommand | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     // Asserty git config
 | ||||||
|  |     const gitConfigLines = (await fs.promises.readFile(localGitConfigPath)) | ||||||
|  |       .toString() | ||||||
|  |       .split('\n') | ||||||
|  |       .filter(x => x) | ||||||
|  |     expect(gitConfigLines).toHaveLength(1) | ||||||
|  |     expect(gitConfigLines[0]).toMatch(/^http\./) | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   const configureAuth_setsSshCommandWhenPersistCredentialsTrue = | ||||||
|  |     'sets SSH command when persist-credentials true' | ||||||
|  |   it(configureAuth_setsSshCommandWhenPersistCredentialsTrue, async () => { | ||||||
|  |     if (!sshPath) { | ||||||
|  |       process.stdout.write( | ||||||
|  |         `Skipped test "${configureAuth_setsSshCommandWhenPersistCredentialsTrue}". Executable 'ssh' not found in the PATH.\n` | ||||||
|  |       ) | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Arrange
 | ||||||
|  |     await setup(configureAuth_setsSshCommandWhenPersistCredentialsTrue) | ||||||
|  |     const authHelper = gitAuthHelper.createAuthHelper(git, settings) | ||||||
|  | 
 | ||||||
|  |     // Act
 | ||||||
|  |     await authHelper.configureAuth() | ||||||
|  | 
 | ||||||
|  |     // Assert git env var
 | ||||||
|  |     const actualKeyPath = await getActualSshKeyPath() | ||||||
|  |     const actualKnownHostsPath = await getActualSshKnownHostsPath() | ||||||
|  |     const expectedSshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename( | ||||||
|  |       actualKeyPath | ||||||
|  |     )}" -o StrictHostKeyChecking=yes -o CheckHostIP=no -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename( | ||||||
|  |       actualKnownHostsPath | ||||||
|  |     )}"` | ||||||
|  |     expect(git.setEnvironmentVariable).toHaveBeenCalledWith( | ||||||
|  |       'GIT_SSH_COMMAND', | ||||||
|  |       expectedSshCommand | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     // Asserty git config
 | ||||||
|  |     expect(git.config).toHaveBeenCalledWith( | ||||||
|  |       'core.sshCommand', | ||||||
|  |       expectedSshCommand | ||||||
|  |     ) | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   const configureAuth_writesExplicitKnownHosts = 'writes explicit known hosts' | ||||||
|  |   it(configureAuth_writesExplicitKnownHosts, async () => { | ||||||
|  |     if (!sshPath) { | ||||||
|  |       process.stdout.write( | ||||||
|  |         `Skipped test "${configureAuth_writesExplicitKnownHosts}". Executable 'ssh' not found in the PATH.\n` | ||||||
|  |       ) | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Arrange
 | ||||||
|  |     await setup(configureAuth_writesExplicitKnownHosts) | ||||||
|  |     expect(settings.sshKey).toBeTruthy() // sanity check
 | ||||||
|  |     settings.sshKnownHosts = 'my-custom-host.com ssh-rsa ABC123' | ||||||
|  |     const authHelper = gitAuthHelper.createAuthHelper(git, settings) | ||||||
|  | 
 | ||||||
|  |     // Act
 | ||||||
|  |     await authHelper.configureAuth() | ||||||
|  | 
 | ||||||
|  |     // Assert known hosts
 | ||||||
|  |     const actualSshKnownHostsPath = await getActualSshKnownHostsPath() | ||||||
|  |     const actualSshKnownHostsContent = ( | ||||||
|  |       await fs.promises.readFile(actualSshKnownHostsPath) | ||||||
|  |     ).toString() | ||||||
|  |     expect(actualSshKnownHostsContent).toMatch( | ||||||
|  |       /my-custom-host\.com ssh-rsa ABC123/ | ||||||
|  |     ) | ||||||
|  |     expect(actualSshKnownHostsContent).toMatch(/github\.com ssh-rsa AAAAB3N/) | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   const configureAuth_writesSshKeyAndImplicitKnownHosts = | ||||||
|  |     'writes SSH key and implicit known hosts' | ||||||
|  |   it(configureAuth_writesSshKeyAndImplicitKnownHosts, async () => { | ||||||
|  |     if (!sshPath) { | ||||||
|  |       process.stdout.write( | ||||||
|  |         `Skipped test "${configureAuth_writesSshKeyAndImplicitKnownHosts}". Executable 'ssh' not found in the PATH.\n` | ||||||
|  |       ) | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Arrange
 | ||||||
|  |     await setup(configureAuth_writesSshKeyAndImplicitKnownHosts) | ||||||
|  |     expect(settings.sshKey).toBeTruthy() // sanity check
 | ||||||
|  |     const authHelper = gitAuthHelper.createAuthHelper(git, settings) | ||||||
|  | 
 | ||||||
|  |     // Act
 | ||||||
|  |     await authHelper.configureAuth() | ||||||
|  | 
 | ||||||
|  |     // Assert SSH key
 | ||||||
|  |     const actualSshKeyPath = await getActualSshKeyPath() | ||||||
|  |     expect(actualSshKeyPath).toBeTruthy() | ||||||
|  |     const actualSshKeyContent = ( | ||||||
|  |       await fs.promises.readFile(actualSshKeyPath) | ||||||
|  |     ).toString() | ||||||
|  |     expect(actualSshKeyContent).toBe(settings.sshKey + '\n') | ||||||
|  |     if (!isWindows) { | ||||||
|  |       expect((await fs.promises.stat(actualSshKeyPath)).mode & 0o777).toBe( | ||||||
|  |         0o600 | ||||||
|  |       ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Assert known hosts
 | ||||||
|  |     const actualSshKnownHostsPath = await getActualSshKnownHostsPath() | ||||||
|  |     const actualSshKnownHostsContent = ( | ||||||
|  |       await fs.promises.readFile(actualSshKnownHostsPath) | ||||||
|  |     ).toString() | ||||||
|  |     expect(actualSshKnownHostsContent).toMatch(/github\.com ssh-rsa AAAAB3N/) | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   const configureGlobalAuth_configuresUrlInsteadOfWhenSshKeyNotSet = | ||||||
|  |     'configureGlobalAuth configures URL insteadOf when SSH key not set' | ||||||
|  |   it(configureGlobalAuth_configuresUrlInsteadOfWhenSshKeyNotSet, async () => { | ||||||
|  |     // Arrange
 | ||||||
|  |     await setup(configureGlobalAuth_configuresUrlInsteadOfWhenSshKeyNotSet) | ||||||
|  |     settings.sshKey = '' | ||||||
|  |     const authHelper = gitAuthHelper.createAuthHelper(git, settings) | ||||||
|  | 
 | ||||||
|  |     // Act
 | ||||||
|  |     await authHelper.configureAuth() | ||||||
|  |     await authHelper.configureGlobalAuth() | ||||||
|  | 
 | ||||||
|  |     // Assert temporary global config
 | ||||||
|  |     expect(git.env['HOME']).toBeTruthy() | ||||||
|  |     const configContent = ( | ||||||
|  |       await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig')) | ||||||
|  |     ).toString() | ||||||
|  |     expect( | ||||||
|  |       configContent.indexOf(`url.https://github.com/.insteadOf git@github.com`) | ||||||
|  |     ).toBeGreaterThanOrEqual(0) | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|   const configureGlobalAuth_copiesGlobalGitConfig = |   const configureGlobalAuth_copiesGlobalGitConfig = | ||||||
|     'configureGlobalAuth copies global git config' |     'configureGlobalAuth copies global git config' | ||||||
|   it(configureGlobalAuth_copiesGlobalGitConfig, async () => { |   it(configureGlobalAuth_copiesGlobalGitConfig, async () => { | ||||||
| @ -211,6 +437,67 @@ describe('git-auth-helper tests', () => { | |||||||
|     } |     } | ||||||
|   ) |   ) | ||||||
| 
 | 
 | ||||||
|  |   const configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrueAndSshKeyNotSet = | ||||||
|  |     'configureSubmoduleAuth configures token when persist credentials true and SSH key not set' | ||||||
|  |   it( | ||||||
|  |     configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrueAndSshKeyNotSet, | ||||||
|  |     async () => { | ||||||
|  |       // Arrange
 | ||||||
|  |       await setup( | ||||||
|  |         configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrueAndSshKeyNotSet | ||||||
|  |       ) | ||||||
|  |       settings.sshKey = '' | ||||||
|  |       const authHelper = gitAuthHelper.createAuthHelper(git, settings) | ||||||
|  |       await authHelper.configureAuth() | ||||||
|  |       const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any> | ||||||
|  |       mockSubmoduleForeach.mockClear() // reset calls
 | ||||||
|  | 
 | ||||||
|  |       // Act
 | ||||||
|  |       await authHelper.configureSubmoduleAuth() | ||||||
|  | 
 | ||||||
|  |       // Assert
 | ||||||
|  |       expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3) | ||||||
|  |       expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch( | ||||||
|  |         /unset-all.*insteadOf/ | ||||||
|  |       ) | ||||||
|  |       expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/) | ||||||
|  |       expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(/url.*insteadOf/) | ||||||
|  |     } | ||||||
|  |   ) | ||||||
|  | 
 | ||||||
|  |   const configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrueAndSshKeySet = | ||||||
|  |     'configureSubmoduleAuth configures token when persist credentials true and SSH key set' | ||||||
|  |   it( | ||||||
|  |     configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrueAndSshKeySet, | ||||||
|  |     async () => { | ||||||
|  |       if (!sshPath) { | ||||||
|  |         process.stdout.write( | ||||||
|  |           `Skipped test "${configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrueAndSshKeySet}". Executable 'ssh' not found in the PATH.\n` | ||||||
|  |         ) | ||||||
|  |         return | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       // Arrange
 | ||||||
|  |       await setup( | ||||||
|  |         configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrueAndSshKeySet | ||||||
|  |       ) | ||||||
|  |       const authHelper = gitAuthHelper.createAuthHelper(git, settings) | ||||||
|  |       await authHelper.configureAuth() | ||||||
|  |       const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any> | ||||||
|  |       mockSubmoduleForeach.mockClear() // reset calls
 | ||||||
|  | 
 | ||||||
|  |       // Act
 | ||||||
|  |       await authHelper.configureSubmoduleAuth() | ||||||
|  | 
 | ||||||
|  |       // Assert
 | ||||||
|  |       expect(mockSubmoduleForeach).toHaveBeenCalledTimes(2) | ||||||
|  |       expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch( | ||||||
|  |         /unset-all.*insteadOf/ | ||||||
|  |       ) | ||||||
|  |       expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/) | ||||||
|  |     } | ||||||
|  |   ) | ||||||
|  | 
 | ||||||
|   const configureSubmoduleAuth_doesNotConfigureTokenWhenPersistCredentialsFalse = |   const configureSubmoduleAuth_doesNotConfigureTokenWhenPersistCredentialsFalse = | ||||||
|     'configureSubmoduleAuth does not configure token when persist credentials false' |     'configureSubmoduleAuth does not configure token when persist credentials false' | ||||||
|   it( |   it( | ||||||
| @ -223,37 +510,135 @@ describe('git-auth-helper tests', () => { | |||||||
|       settings.persistCredentials = false |       settings.persistCredentials = false | ||||||
|       const authHelper = gitAuthHelper.createAuthHelper(git, settings) |       const authHelper = gitAuthHelper.createAuthHelper(git, settings) | ||||||
|       await authHelper.configureAuth() |       await authHelper.configureAuth() | ||||||
|       ;(git.submoduleForeach as jest.Mock<any, any>).mockClear() // reset calls
 |       const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any> | ||||||
|  |       mockSubmoduleForeach.mockClear() // reset calls
 | ||||||
| 
 | 
 | ||||||
|       // Act
 |       // Act
 | ||||||
|       await authHelper.configureSubmoduleAuth() |       await authHelper.configureSubmoduleAuth() | ||||||
| 
 | 
 | ||||||
|       // Assert
 |       // Assert
 | ||||||
|       expect(git.submoduleForeach).not.toHaveBeenCalled() |       expect(mockSubmoduleForeach).toBeCalledTimes(1) | ||||||
|  |       expect(mockSubmoduleForeach.mock.calls[0][0] as string).toMatch( | ||||||
|  |         /unset-all.*insteadOf/ | ||||||
|  |       ) | ||||||
|     } |     } | ||||||
|   ) |   ) | ||||||
| 
 | 
 | ||||||
|   const configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrue = |   const configureSubmoduleAuth_doesNotConfigureUrlInsteadOfWhenPersistCredentialsTrueAndSshKeySet = | ||||||
|     'configureSubmoduleAuth configures token when persist credentials true' |     'configureSubmoduleAuth does not configure URL insteadOf when persist credentials true and SSH key set' | ||||||
|   it( |   it( | ||||||
|     configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrue, |     configureSubmoduleAuth_doesNotConfigureUrlInsteadOfWhenPersistCredentialsTrueAndSshKeySet, | ||||||
|     async () => { |     async () => { | ||||||
|  |       if (!sshPath) { | ||||||
|  |         process.stdout.write( | ||||||
|  |           `Skipped test "${configureSubmoduleAuth_doesNotConfigureUrlInsteadOfWhenPersistCredentialsTrueAndSshKeySet}". Executable 'ssh' not found in the PATH.\n` | ||||||
|  |         ) | ||||||
|  |         return | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|       // Arrange
 |       // Arrange
 | ||||||
|       await setup( |       await setup( | ||||||
|         configureSubmoduleAuth_configuresTokenWhenPersistCredentialsTrue |         configureSubmoduleAuth_doesNotConfigureUrlInsteadOfWhenPersistCredentialsTrueAndSshKeySet | ||||||
|       ) |       ) | ||||||
|       const authHelper = gitAuthHelper.createAuthHelper(git, settings) |       const authHelper = gitAuthHelper.createAuthHelper(git, settings) | ||||||
|       await authHelper.configureAuth() |       await authHelper.configureAuth() | ||||||
|       ;(git.submoduleForeach as jest.Mock<any, any>).mockClear() // reset calls
 |       const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any> | ||||||
|  |       mockSubmoduleForeach.mockClear() // reset calls
 | ||||||
| 
 | 
 | ||||||
|       // Act
 |       // Act
 | ||||||
|       await authHelper.configureSubmoduleAuth() |       await authHelper.configureSubmoduleAuth() | ||||||
| 
 | 
 | ||||||
|       // Assert
 |       // Assert
 | ||||||
|       expect(git.submoduleForeach).toHaveBeenCalledTimes(1) |       expect(mockSubmoduleForeach).toHaveBeenCalledTimes(2) | ||||||
|  |       expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch( | ||||||
|  |         /unset-all.*insteadOf/ | ||||||
|  |       ) | ||||||
|  |       expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/) | ||||||
|     } |     } | ||||||
|   ) |   ) | ||||||
| 
 | 
 | ||||||
|  |   const configureSubmoduleAuth_removesUrlInsteadOfWhenPersistCredentialsFalse = | ||||||
|  |     'configureSubmoduleAuth removes URL insteadOf when persist credentials false' | ||||||
|  |   it( | ||||||
|  |     configureSubmoduleAuth_removesUrlInsteadOfWhenPersistCredentialsFalse, | ||||||
|  |     async () => { | ||||||
|  |       // Arrange
 | ||||||
|  |       await setup( | ||||||
|  |         configureSubmoduleAuth_removesUrlInsteadOfWhenPersistCredentialsFalse | ||||||
|  |       ) | ||||||
|  |       settings.persistCredentials = false | ||||||
|  |       const authHelper = gitAuthHelper.createAuthHelper(git, settings) | ||||||
|  |       await authHelper.configureAuth() | ||||||
|  |       const mockSubmoduleForeach = git.submoduleForeach as jest.Mock<any, any> | ||||||
|  |       mockSubmoduleForeach.mockClear() // reset calls
 | ||||||
|  | 
 | ||||||
|  |       // Act
 | ||||||
|  |       await authHelper.configureSubmoduleAuth() | ||||||
|  | 
 | ||||||
|  |       // Assert
 | ||||||
|  |       expect(mockSubmoduleForeach).toBeCalledTimes(1) | ||||||
|  |       expect(mockSubmoduleForeach.mock.calls[0][0] as string).toMatch( | ||||||
|  |         /unset-all.*insteadOf/ | ||||||
|  |       ) | ||||||
|  |     } | ||||||
|  |   ) | ||||||
|  | 
 | ||||||
|  |   const removeAuth_removesSshCommand = 'removeAuth removes SSH command' | ||||||
|  |   it(removeAuth_removesSshCommand, async () => { | ||||||
|  |     if (!sshPath) { | ||||||
|  |       process.stdout.write( | ||||||
|  |         `Skipped test "${removeAuth_removesSshCommand}". Executable 'ssh' not found in the PATH.\n` | ||||||
|  |       ) | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Arrange
 | ||||||
|  |     await setup(removeAuth_removesSshCommand) | ||||||
|  |     const authHelper = gitAuthHelper.createAuthHelper(git, settings) | ||||||
|  |     await authHelper.configureAuth() | ||||||
|  |     let gitConfigContent = ( | ||||||
|  |       await fs.promises.readFile(localGitConfigPath) | ||||||
|  |     ).toString() | ||||||
|  |     expect(gitConfigContent.indexOf('core.sshCommand')).toBeGreaterThanOrEqual( | ||||||
|  |       0 | ||||||
|  |     ) // sanity check
 | ||||||
|  |     const actualKeyPath = await getActualSshKeyPath() | ||||||
|  |     expect(actualKeyPath).toBeTruthy() | ||||||
|  |     await fs.promises.stat(actualKeyPath) | ||||||
|  |     const actualKnownHostsPath = await getActualSshKnownHostsPath() | ||||||
|  |     expect(actualKnownHostsPath).toBeTruthy() | ||||||
|  |     await fs.promises.stat(actualKnownHostsPath) | ||||||
|  | 
 | ||||||
|  |     // Act
 | ||||||
|  |     await authHelper.removeAuth() | ||||||
|  | 
 | ||||||
|  |     // Assert git config
 | ||||||
|  |     gitConfigContent = ( | ||||||
|  |       await fs.promises.readFile(localGitConfigPath) | ||||||
|  |     ).toString() | ||||||
|  |     expect(gitConfigContent.indexOf('core.sshCommand')).toBeLessThan(0) | ||||||
|  | 
 | ||||||
|  |     // Assert SSH key file
 | ||||||
|  |     try { | ||||||
|  |       await fs.promises.stat(actualKeyPath) | ||||||
|  |       throw new Error('SSH key should have been deleted') | ||||||
|  |     } catch (err) { | ||||||
|  |       if (err.code !== 'ENOENT') { | ||||||
|  |         throw err | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Assert known hosts file
 | ||||||
|  |     try { | ||||||
|  |       await fs.promises.stat(actualKnownHostsPath) | ||||||
|  |       throw new Error('SSH known hosts should have been deleted') | ||||||
|  |     } catch (err) { | ||||||
|  |       if (err.code !== 'ENOENT') { | ||||||
|  |         throw err | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|   const removeAuth_removesToken = 'removeAuth removes token' |   const removeAuth_removesToken = 'removeAuth removes token' | ||||||
|   it(removeAuth_removesToken, async () => { |   it(removeAuth_removesToken, async () => { | ||||||
|     // Arrange
 |     // Arrange
 | ||||||
| @ -401,6 +786,36 @@ async function setup(testName: string): Promise<void> { | |||||||
|     ref: 'refs/heads/master', |     ref: 'refs/heads/master', | ||||||
|     repositoryName: 'my-repo', |     repositoryName: 'my-repo', | ||||||
|     repositoryOwner: 'my-org', |     repositoryOwner: 'my-org', | ||||||
|     repositoryPath: '' |     repositoryPath: '', | ||||||
|  |     sshKey: sshPath ? 'some ssh private key' : '', | ||||||
|  |     sshKnownHosts: '', | ||||||
|  |     sshStrict: true | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | async function getActualSshKeyPath(): Promise<string> { | ||||||
|  |   let actualTempFiles = (await fs.promises.readdir(runnerTemp)) | ||||||
|  |     .sort() | ||||||
|  |     .map(x => path.join(runnerTemp, x)) | ||||||
|  |   if (actualTempFiles.length === 0) { | ||||||
|  |     return '' | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   expect(actualTempFiles).toHaveLength(2) | ||||||
|  |   expect(actualTempFiles[0].endsWith('_known_hosts')).toBeFalsy() | ||||||
|  |   return actualTempFiles[0] | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function getActualSshKnownHostsPath(): Promise<string> { | ||||||
|  |   let actualTempFiles = (await fs.promises.readdir(runnerTemp)) | ||||||
|  |     .sort() | ||||||
|  |     .map(x => path.join(runnerTemp, x)) | ||||||
|  |   if (actualTempFiles.length === 0) { | ||||||
|  |     return '' | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   expect(actualTempFiles).toHaveLength(2) | ||||||
|  |   expect(actualTempFiles[1].endsWith('_known_hosts')).toBeTruthy() | ||||||
|  |   expect(actualTempFiles[1].startsWith(actualTempFiles[0])).toBeTruthy() | ||||||
|  |   return actualTempFiles[1] | ||||||
|  | } | ||||||
|  | |||||||
							
								
								
									
										43
									
								
								action.yml
									
									
									
									
									
								
							
							
						
						
									
										43
									
								
								action.yml
									
									
									
									
									
								
							| @ -11,13 +11,42 @@ inputs: | |||||||
|       event.  Otherwise, defaults to `master`. |       event.  Otherwise, defaults to `master`. | ||||||
|   token: |   token: | ||||||
|     description: > |     description: > | ||||||
|       Auth token used to fetch the repository. The token is stored in the local |       Personal access token (PAT) used to fetch the repository. The PAT is configured | ||||||
|       git config, which enables your scripts to run authenticated git commands. |       with the local git config, which enables your scripts to run authenticated git | ||||||
|       The post-job step removes the token from the git config. [Learn more about |       commands. The post-job step removes the PAT. | ||||||
|       creating and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets) | 
 | ||||||
|  | 
 | ||||||
|  |       We recommend creating a service account with the least permissions necessary. | ||||||
|  |       Also when generating a new PAT, select the least scopes necessary. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |       [Learn more about creating and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets) | ||||||
|     default: ${{ github.token }} |     default: ${{ github.token }} | ||||||
|  |   ssh-key: | ||||||
|  |     description: > | ||||||
|  |       SSH key used to fetch the repository. SSH key is configured with the local | ||||||
|  |       git config, which enables your scripts to run authenticated git commands. | ||||||
|  |       The post-job step removes the SSH key. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |       We recommend creating a service account with the least permissions necessary. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |       [Learn more about creating and using | ||||||
|  |       encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets) | ||||||
|  |   ssh-known-hosts: | ||||||
|  |     description: > | ||||||
|  |       Known hosts in addition to the user and global host key database. The public | ||||||
|  |       SSH keys for a host may be obtained using the utility `ssh-keyscan`. For example, | ||||||
|  |       `ssh-keyscan github.com`. The public key for github.com is always implicitly added. | ||||||
|  |   ssh-strict: | ||||||
|  |     description: > | ||||||
|  |       Whether to perform strict host key checking. When true, adds the options `StrictHostKeyChecking=yes` | ||||||
|  |       and `CheckHostIP=no` to the SSH command line. Use the input `ssh-known-hosts` to | ||||||
|  |       configure additional hosts. | ||||||
|  |     default: true | ||||||
|   persist-credentials: |   persist-credentials: | ||||||
|     description: 'Whether to persist the token in the git config' |     description: 'Whether to configure the token or SSH key with the local git config' | ||||||
|     default: true |     default: true | ||||||
|   path: |   path: | ||||||
|     description: 'Relative path under $GITHUB_WORKSPACE to place the repository' |     description: 'Relative path under $GITHUB_WORKSPACE to place the repository' | ||||||
| @ -34,6 +63,10 @@ inputs: | |||||||
|     description: > |     description: > | ||||||
|       Whether to checkout submodules: `true` to checkout submodules or `recursive` to |       Whether to checkout submodules: `true` to checkout submodules or `recursive` to | ||||||
|       recursively checkout submodules. |       recursively checkout submodules. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |       When the `ssh-key` input is not provided, SSH URLs beginning with `git@github.com:` are | ||||||
|  |       converted to HTTPS. | ||||||
|     default: false |     default: false | ||||||
| runs: | runs: | ||||||
|   using: node12 |   using: node12 | ||||||
|  | |||||||
							
								
								
									
										152
									
								
								dist/index.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										152
									
								
								dist/index.js
									
									
									
									
										vendored
									
									
								
							| @ -2621,6 +2621,14 @@ exports.IsPost = !!process.env['STATE_isPost']; | |||||||
|  * The repository path for the POST action. The value is empty during the MAIN action. |  * The repository path for the POST action. The value is empty during the MAIN action. | ||||||
|  */ |  */ | ||||||
| exports.RepositoryPath = process.env['STATE_repositoryPath'] || ''; | exports.RepositoryPath = process.env['STATE_repositoryPath'] || ''; | ||||||
|  | /** | ||||||
|  |  * The SSH key path for the POST action. The value is empty during the MAIN action. | ||||||
|  |  */ | ||||||
|  | exports.SshKeyPath = process.env['STATE_sshKeyPath'] || ''; | ||||||
|  | /** | ||||||
|  |  * The SSH known hosts path for the POST action. The value is empty during the MAIN action. | ||||||
|  |  */ | ||||||
|  | exports.SshKnownHostsPath = process.env['STATE_sshKnownHostsPath'] || ''; | ||||||
| /** | /** | ||||||
|  * Save the repository path so the POST action can retrieve the value. |  * Save the repository path so the POST action can retrieve the value. | ||||||
|  */ |  */ | ||||||
| @ -2628,6 +2636,20 @@ function setRepositoryPath(repositoryPath) { | |||||||
|     coreCommand.issueCommand('save-state', { name: 'repositoryPath' }, repositoryPath); |     coreCommand.issueCommand('save-state', { name: 'repositoryPath' }, repositoryPath); | ||||||
| } | } | ||||||
| exports.setRepositoryPath = setRepositoryPath; | exports.setRepositoryPath = setRepositoryPath; | ||||||
|  | /** | ||||||
|  |  * Save the SSH key path so the POST action can retrieve the value. | ||||||
|  |  */ | ||||||
|  | function setSshKeyPath(sshKeyPath) { | ||||||
|  |     coreCommand.issueCommand('save-state', { name: 'sshKeyPath' }, sshKeyPath); | ||||||
|  | } | ||||||
|  | exports.setSshKeyPath = setSshKeyPath; | ||||||
|  | /** | ||||||
|  |  * Save the SSH known hosts path so the POST action can retrieve the value. | ||||||
|  |  */ | ||||||
|  | function setSshKnownHostsPath(sshKnownHostsPath) { | ||||||
|  |     coreCommand.issueCommand('save-state', { name: 'sshKnownHostsPath' }, sshKnownHostsPath); | ||||||
|  | } | ||||||
|  | exports.setSshKnownHostsPath = setSshKnownHostsPath; | ||||||
| // Publish a variable so that when the POST action runs, it can determine it should run the cleanup logic.
 | // Publish a variable so that when the POST action runs, it can determine it should run the cleanup logic.
 | ||||||
| // This is necessary since we don't have a separate entry point.
 | // This is necessary since we don't have a separate entry point.
 | ||||||
| if (!exports.IsPost) { | if (!exports.IsPost) { | ||||||
| @ -5080,14 +5102,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) { | |||||||
| Object.defineProperty(exports, "__esModule", { value: true }); | Object.defineProperty(exports, "__esModule", { value: true }); | ||||||
| const assert = __importStar(__webpack_require__(357)); | const assert = __importStar(__webpack_require__(357)); | ||||||
| const core = __importStar(__webpack_require__(470)); | const core = __importStar(__webpack_require__(470)); | ||||||
|  | const exec = __importStar(__webpack_require__(986)); | ||||||
| const fs = __importStar(__webpack_require__(747)); | const fs = __importStar(__webpack_require__(747)); | ||||||
| const io = __importStar(__webpack_require__(1)); | const io = __importStar(__webpack_require__(1)); | ||||||
| const os = __importStar(__webpack_require__(87)); | const os = __importStar(__webpack_require__(87)); | ||||||
| const path = __importStar(__webpack_require__(622)); | const path = __importStar(__webpack_require__(622)); | ||||||
| const regexpHelper = __importStar(__webpack_require__(528)); | const regexpHelper = __importStar(__webpack_require__(528)); | ||||||
|  | const stateHelper = __importStar(__webpack_require__(153)); | ||||||
| const v4_1 = __importDefault(__webpack_require__(826)); | const v4_1 = __importDefault(__webpack_require__(826)); | ||||||
| const IS_WINDOWS = process.platform === 'win32'; | const IS_WINDOWS = process.platform === 'win32'; | ||||||
| const HOSTNAME = 'github.com'; | const HOSTNAME = 'github.com'; | ||||||
|  | const SSH_COMMAND_KEY = 'core.sshCommand'; | ||||||
| function createAuthHelper(git, settings) { | function createAuthHelper(git, settings) { | ||||||
|     return new GitAuthHelper(git, settings); |     return new GitAuthHelper(git, settings); | ||||||
| } | } | ||||||
| @ -5097,6 +5122,8 @@ class GitAuthHelper { | |||||||
|         this.tokenConfigKey = `http.https://${HOSTNAME}/.extraheader`; |         this.tokenConfigKey = `http.https://${HOSTNAME}/.extraheader`; | ||||||
|         this.insteadOfKey = `url.https://${HOSTNAME}/.insteadOf`; |         this.insteadOfKey = `url.https://${HOSTNAME}/.insteadOf`; | ||||||
|         this.insteadOfValue = `git@${HOSTNAME}:`; |         this.insteadOfValue = `git@${HOSTNAME}:`; | ||||||
|  |         this.sshKeyPath = ''; | ||||||
|  |         this.sshKnownHostsPath = ''; | ||||||
|         this.temporaryHomePath = ''; |         this.temporaryHomePath = ''; | ||||||
|         this.git = gitCommandManager; |         this.git = gitCommandManager; | ||||||
|         this.settings = gitSourceSettings || {}; |         this.settings = gitSourceSettings || {}; | ||||||
| @ -5111,6 +5138,7 @@ class GitAuthHelper { | |||||||
|             // Remove possible previous values
 |             // Remove possible previous values
 | ||||||
|             yield this.removeAuth(); |             yield this.removeAuth(); | ||||||
|             // Configure new values
 |             // Configure new values
 | ||||||
|  |             yield this.configureSsh(); | ||||||
|             yield this.configureToken(); |             yield this.configureToken(); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| @ -5150,7 +5178,9 @@ class GitAuthHelper { | |||||||
|                 yield this.configureToken(newGitConfigPath, true); |                 yield this.configureToken(newGitConfigPath, true); | ||||||
|                 // Configure HTTPS instead of SSH
 |                 // Configure HTTPS instead of SSH
 | ||||||
|                 yield this.git.tryConfigUnset(this.insteadOfKey, true); |                 yield this.git.tryConfigUnset(this.insteadOfKey, true); | ||||||
|                 yield this.git.config(this.insteadOfKey, this.insteadOfValue, true); |                 if (!this.settings.sshKey) { | ||||||
|  |                     yield this.git.config(this.insteadOfKey, this.insteadOfValue, true); | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|             catch (err) { |             catch (err) { | ||||||
|                 // Unset in case somehow written to the real global config
 |                 // Unset in case somehow written to the real global config
 | ||||||
| @ -5162,27 +5192,29 @@ class GitAuthHelper { | |||||||
|     } |     } | ||||||
|     configureSubmoduleAuth() { |     configureSubmoduleAuth() { | ||||||
|         return __awaiter(this, void 0, void 0, function* () { |         return __awaiter(this, void 0, void 0, function* () { | ||||||
|  |             // Remove possible previous HTTPS instead of SSH
 | ||||||
|  |             yield this.removeGitConfig(this.insteadOfKey, true); | ||||||
|             if (this.settings.persistCredentials) { |             if (this.settings.persistCredentials) { | ||||||
|                 // Configure a placeholder value. This approach avoids the credential being captured
 |                 // Configure a placeholder value. This approach avoids the credential being captured
 | ||||||
|                 // by process creation audit events, which are commonly logged. For more information,
 |                 // by process creation audit events, which are commonly logged. For more information,
 | ||||||
|                 // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
 |                 // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
 | ||||||
|                 const commands = [ |                 const output = yield this.git.submoduleForeach(`git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url`, this.settings.nestedSubmodules); | ||||||
|                     `git config --local "${this.tokenConfigKey}" "${this.tokenPlaceholderConfigValue}"`, |  | ||||||
|                     `git config --local "${this.insteadOfKey}" "${this.insteadOfValue}"`, |  | ||||||
|                     `git config --local --show-origin --name-only --get-regexp remote.origin.url` |  | ||||||
|                 ]; |  | ||||||
|                 const output = yield this.git.submoduleForeach(commands.join(' && '), this.settings.nestedSubmodules); |  | ||||||
|                 // Replace the placeholder
 |                 // Replace the placeholder
 | ||||||
|                 const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []; |                 const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []; | ||||||
|                 for (const configPath of configPaths) { |                 for (const configPath of configPaths) { | ||||||
|                     core.debug(`Replacing token placeholder in '${configPath}'`); |                     core.debug(`Replacing token placeholder in '${configPath}'`); | ||||||
|                     this.replaceTokenPlaceholder(configPath); |                     this.replaceTokenPlaceholder(configPath); | ||||||
|                 } |                 } | ||||||
|  |                 // Configure HTTPS instead of SSH
 | ||||||
|  |                 if (!this.settings.sshKey) { | ||||||
|  |                     yield this.git.submoduleForeach(`git config --local '${this.insteadOfKey}' '${this.insteadOfValue}'`, this.settings.nestedSubmodules); | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|     removeAuth() { |     removeAuth() { | ||||||
|         return __awaiter(this, void 0, void 0, function* () { |         return __awaiter(this, void 0, void 0, function* () { | ||||||
|  |             yield this.removeSsh(); | ||||||
|             yield this.removeToken(); |             yield this.removeToken(); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| @ -5193,6 +5225,62 @@ class GitAuthHelper { | |||||||
|             yield io.rmRF(this.temporaryHomePath); |             yield io.rmRF(this.temporaryHomePath); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |     configureSsh() { | ||||||
|  |         return __awaiter(this, void 0, void 0, function* () { | ||||||
|  |             if (!this.settings.sshKey) { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             // Write key
 | ||||||
|  |             const runnerTemp = process.env['RUNNER_TEMP'] || ''; | ||||||
|  |             assert.ok(runnerTemp, 'RUNNER_TEMP is not defined'); | ||||||
|  |             const uniqueId = v4_1.default(); | ||||||
|  |             this.sshKeyPath = path.join(runnerTemp, uniqueId); | ||||||
|  |             stateHelper.setSshKeyPath(this.sshKeyPath); | ||||||
|  |             yield fs.promises.mkdir(runnerTemp, { recursive: true }); | ||||||
|  |             yield fs.promises.writeFile(this.sshKeyPath, this.settings.sshKey.trim() + '\n', { mode: 0o600 }); | ||||||
|  |             // Remove inherited permissions on Windows
 | ||||||
|  |             if (IS_WINDOWS) { | ||||||
|  |                 const icacls = yield io.which('icacls.exe'); | ||||||
|  |                 yield exec.exec(`"${icacls}" "${this.sshKeyPath}" /grant:r "${process.env['USERDOMAIN']}\\${process.env['USERNAME']}:F"`); | ||||||
|  |                 yield exec.exec(`"${icacls}" "${this.sshKeyPath}" /inheritance:r`); | ||||||
|  |             } | ||||||
|  |             // Write known hosts
 | ||||||
|  |             const userKnownHostsPath = path.join(os.homedir(), '.ssh', 'known_hosts'); | ||||||
|  |             let userKnownHosts = ''; | ||||||
|  |             try { | ||||||
|  |                 userKnownHosts = (yield fs.promises.readFile(userKnownHostsPath)).toString(); | ||||||
|  |             } | ||||||
|  |             catch (err) { | ||||||
|  |                 if (err.code !== 'ENOENT') { | ||||||
|  |                     throw err; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             let knownHosts = ''; | ||||||
|  |             if (userKnownHosts) { | ||||||
|  |                 knownHosts += `# Begin from ${userKnownHostsPath}\n${userKnownHosts}\n# End from ${userKnownHostsPath}\n`; | ||||||
|  |             } | ||||||
|  |             if (this.settings.sshKnownHosts) { | ||||||
|  |                 knownHosts += `# Begin from input known hosts\n${this.settings.sshKnownHosts}\n# end from input known hosts\n`; | ||||||
|  |             } | ||||||
|  |             knownHosts += `# Begin implicitly added github.com\ngithub.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==\n# End implicitly added github.com\n`; | ||||||
|  |             this.sshKnownHostsPath = path.join(runnerTemp, `${uniqueId}_known_hosts`); | ||||||
|  |             stateHelper.setSshKnownHostsPath(this.sshKnownHostsPath); | ||||||
|  |             yield fs.promises.writeFile(this.sshKnownHostsPath, knownHosts); | ||||||
|  |             // Configure GIT_SSH_COMMAND
 | ||||||
|  |             const sshPath = yield io.which('ssh', true); | ||||||
|  |             let sshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename(this.sshKeyPath)}"`; | ||||||
|  |             if (this.settings.sshStrict) { | ||||||
|  |                 sshCommand += ' -o StrictHostKeyChecking=yes -o CheckHostIP=no'; | ||||||
|  |             } | ||||||
|  |             sshCommand += ` -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename(this.sshKnownHostsPath)}"`; | ||||||
|  |             core.info(`Temporarily overriding GIT_SSH_COMMAND=${sshCommand}`); | ||||||
|  |             this.git.setEnvironmentVariable('GIT_SSH_COMMAND', sshCommand); | ||||||
|  |             // Configure core.sshCommand
 | ||||||
|  |             if (this.settings.persistCredentials) { | ||||||
|  |                 yield this.git.config(SSH_COMMAND_KEY, sshCommand); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|     configureToken(configPath, globalConfig) { |     configureToken(configPath, globalConfig) { | ||||||
|         return __awaiter(this, void 0, void 0, function* () { |         return __awaiter(this, void 0, void 0, function* () { | ||||||
|             // Validate args
 |             // Validate args
 | ||||||
| @ -5223,21 +5311,50 @@ class GitAuthHelper { | |||||||
|             yield fs.promises.writeFile(configPath, content); |             yield fs.promises.writeFile(configPath, content); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |     removeSsh() { | ||||||
|  |         return __awaiter(this, void 0, void 0, function* () { | ||||||
|  |             // SSH key
 | ||||||
|  |             const keyPath = this.sshKeyPath || stateHelper.SshKeyPath; | ||||||
|  |             if (keyPath) { | ||||||
|  |                 try { | ||||||
|  |                     yield io.rmRF(keyPath); | ||||||
|  |                 } | ||||||
|  |                 catch (err) { | ||||||
|  |                     core.debug(err.message); | ||||||
|  |                     core.warning(`Failed to remove SSH key '${keyPath}'`); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             // SSH known hosts
 | ||||||
|  |             const knownHostsPath = this.sshKnownHostsPath || stateHelper.SshKnownHostsPath; | ||||||
|  |             if (knownHostsPath) { | ||||||
|  |                 try { | ||||||
|  |                     yield io.rmRF(knownHostsPath); | ||||||
|  |                 } | ||||||
|  |                 catch (_a) { | ||||||
|  |                     // Intentionally empty
 | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             // SSH command
 | ||||||
|  |             yield this.removeGitConfig(SSH_COMMAND_KEY); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|     removeToken() { |     removeToken() { | ||||||
|         return __awaiter(this, void 0, void 0, function* () { |         return __awaiter(this, void 0, void 0, function* () { | ||||||
|             // HTTP extra header
 |             // HTTP extra header
 | ||||||
|             yield this.removeGitConfig(this.tokenConfigKey); |             yield this.removeGitConfig(this.tokenConfigKey); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|     removeGitConfig(configKey) { |     removeGitConfig(configKey, submoduleOnly = false) { | ||||||
|         return __awaiter(this, void 0, void 0, function* () { |         return __awaiter(this, void 0, void 0, function* () { | ||||||
|             if ((yield this.git.configExists(configKey)) && |             if (!submoduleOnly) { | ||||||
|                 !(yield this.git.tryConfigUnset(configKey))) { |                 if ((yield this.git.configExists(configKey)) && | ||||||
|                 // Load the config contents
 |                     !(yield this.git.tryConfigUnset(configKey))) { | ||||||
|                 core.warning(`Failed to remove '${configKey}' from the git config`); |                     // Load the config contents
 | ||||||
|  |                     core.warning(`Failed to remove '${configKey}' from the git config`); | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|             const pattern = regexpHelper.escape(configKey); |             const pattern = regexpHelper.escape(configKey); | ||||||
|             yield this.git.submoduleForeach(`git config --local --name-only --get-regexp ${pattern} && git config --local --unset-all ${configKey} || :`, true); |             yield this.git.submoduleForeach(`git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :`, true); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @ -5680,7 +5797,9 @@ function getSource(settings) { | |||||||
|     return __awaiter(this, void 0, void 0, function* () { |     return __awaiter(this, void 0, void 0, function* () { | ||||||
|         // Repository URL
 |         // Repository URL
 | ||||||
|         core.info(`Syncing repository: ${settings.repositoryOwner}/${settings.repositoryName}`); |         core.info(`Syncing repository: ${settings.repositoryOwner}/${settings.repositoryName}`); | ||||||
|         const repositoryUrl = `https://${hostname}/${encodeURIComponent(settings.repositoryOwner)}/${encodeURIComponent(settings.repositoryName)}`; |         const repositoryUrl = settings.sshKey | ||||||
|  |             ? `git@${hostname}:${encodeURIComponent(settings.repositoryOwner)}/${encodeURIComponent(settings.repositoryName)}.git` | ||||||
|  |             : `https://${hostname}/${encodeURIComponent(settings.repositoryOwner)}/${encodeURIComponent(settings.repositoryName)}`; | ||||||
|         // Remove conflicting file path
 |         // Remove conflicting file path
 | ||||||
|         if (fsHelper.fileExistsSync(settings.repositoryPath)) { |         if (fsHelper.fileExistsSync(settings.repositoryPath)) { | ||||||
|             yield io.rmRF(settings.repositoryPath); |             yield io.rmRF(settings.repositoryPath); | ||||||
| @ -13940,6 +14059,11 @@ function getInputs() { | |||||||
|     core.debug(`recursive submodules = ${result.nestedSubmodules}`); |     core.debug(`recursive submodules = ${result.nestedSubmodules}`); | ||||||
|     // Auth token
 |     // Auth token
 | ||||||
|     result.authToken = core.getInput('token'); |     result.authToken = core.getInput('token'); | ||||||
|  |     // SSH
 | ||||||
|  |     result.sshKey = core.getInput('ssh-key'); | ||||||
|  |     result.sshKnownHosts = core.getInput('ssh-known-hosts'); | ||||||
|  |     result.sshStrict = | ||||||
|  |         (core.getInput('ssh-strict') || 'true').toUpperCase() === 'TRUE'; | ||||||
|     // Persist credentials
 |     // Persist credentials
 | ||||||
|     result.persistCredentials = |     result.persistCredentials = | ||||||
|         (core.getInput('persist-credentials') || 'false').toUpperCase() === 'TRUE'; |         (core.getInput('persist-credentials') || 'false').toUpperCase() === 'TRUE'; | ||||||
|  | |||||||
| @ -13,6 +13,7 @@ import {IGitSourceSettings} from './git-source-settings' | |||||||
| 
 | 
 | ||||||
| const IS_WINDOWS = process.platform === 'win32' | const IS_WINDOWS = process.platform === 'win32' | ||||||
| const HOSTNAME = 'github.com' | const HOSTNAME = 'github.com' | ||||||
|  | const SSH_COMMAND_KEY = 'core.sshCommand' | ||||||
| 
 | 
 | ||||||
| export interface IGitAuthHelper { | export interface IGitAuthHelper { | ||||||
|   configureAuth(): Promise<void> |   configureAuth(): Promise<void> | ||||||
| @ -36,6 +37,8 @@ class GitAuthHelper { | |||||||
|   private readonly tokenPlaceholderConfigValue: string |   private readonly tokenPlaceholderConfigValue: string | ||||||
|   private readonly insteadOfKey: string = `url.https://${HOSTNAME}/.insteadOf` |   private readonly insteadOfKey: string = `url.https://${HOSTNAME}/.insteadOf` | ||||||
|   private readonly insteadOfValue: string = `git@${HOSTNAME}:` |   private readonly insteadOfValue: string = `git@${HOSTNAME}:` | ||||||
|  |   private sshKeyPath = '' | ||||||
|  |   private sshKnownHostsPath = '' | ||||||
|   private temporaryHomePath = '' |   private temporaryHomePath = '' | ||||||
|   private tokenConfigValue: string |   private tokenConfigValue: string | ||||||
| 
 | 
 | ||||||
| @ -61,6 +64,7 @@ class GitAuthHelper { | |||||||
|     await this.removeAuth() |     await this.removeAuth() | ||||||
| 
 | 
 | ||||||
|     // Configure new values
 |     // Configure new values
 | ||||||
|  |     await this.configureSsh() | ||||||
|     await this.configureToken() |     await this.configureToken() | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -106,7 +110,9 @@ class GitAuthHelper { | |||||||
| 
 | 
 | ||||||
|       // Configure HTTPS instead of SSH
 |       // Configure HTTPS instead of SSH
 | ||||||
|       await this.git.tryConfigUnset(this.insteadOfKey, true) |       await this.git.tryConfigUnset(this.insteadOfKey, true) | ||||||
|       await this.git.config(this.insteadOfKey, this.insteadOfValue, true) |       if (!this.settings.sshKey) { | ||||||
|  |         await this.git.config(this.insteadOfKey, this.insteadOfValue, true) | ||||||
|  |       } | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       // Unset in case somehow written to the real global config
 |       // Unset in case somehow written to the real global config
 | ||||||
|       core.info( |       core.info( | ||||||
| @ -118,17 +124,15 @@ class GitAuthHelper { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async configureSubmoduleAuth(): Promise<void> { |   async configureSubmoduleAuth(): Promise<void> { | ||||||
|  |     // Remove possible previous HTTPS instead of SSH
 | ||||||
|  |     await this.removeGitConfig(this.insteadOfKey, true) | ||||||
|  | 
 | ||||||
|     if (this.settings.persistCredentials) { |     if (this.settings.persistCredentials) { | ||||||
|       // Configure a placeholder value. This approach avoids the credential being captured
 |       // Configure a placeholder value. This approach avoids the credential being captured
 | ||||||
|       // by process creation audit events, which are commonly logged. For more information,
 |       // by process creation audit events, which are commonly logged. For more information,
 | ||||||
|       // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
 |       // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
 | ||||||
|       const commands = [ |  | ||||||
|         `git config --local "${this.tokenConfigKey}" "${this.tokenPlaceholderConfigValue}"`, |  | ||||||
|         `git config --local "${this.insteadOfKey}" "${this.insteadOfValue}"`, |  | ||||||
|         `git config --local --show-origin --name-only --get-regexp remote.origin.url` |  | ||||||
|       ] |  | ||||||
|       const output = await this.git.submoduleForeach( |       const output = await this.git.submoduleForeach( | ||||||
|         commands.join(' && '), |         `git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url`, | ||||||
|         this.settings.nestedSubmodules |         this.settings.nestedSubmodules | ||||||
|       ) |       ) | ||||||
| 
 | 
 | ||||||
| @ -139,10 +143,19 @@ class GitAuthHelper { | |||||||
|         core.debug(`Replacing token placeholder in '${configPath}'`) |         core.debug(`Replacing token placeholder in '${configPath}'`) | ||||||
|         this.replaceTokenPlaceholder(configPath) |         this.replaceTokenPlaceholder(configPath) | ||||||
|       } |       } | ||||||
|  | 
 | ||||||
|  |       // Configure HTTPS instead of SSH
 | ||||||
|  |       if (!this.settings.sshKey) { | ||||||
|  |         await this.git.submoduleForeach( | ||||||
|  |           `git config --local '${this.insteadOfKey}' '${this.insteadOfValue}'`, | ||||||
|  |           this.settings.nestedSubmodules | ||||||
|  |         ) | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async removeAuth(): Promise<void> { |   async removeAuth(): Promise<void> { | ||||||
|  |     await this.removeSsh() | ||||||
|     await this.removeToken() |     await this.removeToken() | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -152,6 +165,77 @@ class GitAuthHelper { | |||||||
|     await io.rmRF(this.temporaryHomePath) |     await io.rmRF(this.temporaryHomePath) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   private async configureSsh(): Promise<void> { | ||||||
|  |     if (!this.settings.sshKey) { | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Write key
 | ||||||
|  |     const runnerTemp = process.env['RUNNER_TEMP'] || '' | ||||||
|  |     assert.ok(runnerTemp, 'RUNNER_TEMP is not defined') | ||||||
|  |     const uniqueId = uuid() | ||||||
|  |     this.sshKeyPath = path.join(runnerTemp, uniqueId) | ||||||
|  |     stateHelper.setSshKeyPath(this.sshKeyPath) | ||||||
|  |     await fs.promises.mkdir(runnerTemp, {recursive: true}) | ||||||
|  |     await fs.promises.writeFile( | ||||||
|  |       this.sshKeyPath, | ||||||
|  |       this.settings.sshKey.trim() + '\n', | ||||||
|  |       {mode: 0o600} | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     // Remove inherited permissions on Windows
 | ||||||
|  |     if (IS_WINDOWS) { | ||||||
|  |       const icacls = await io.which('icacls.exe') | ||||||
|  |       await exec.exec( | ||||||
|  |         `"${icacls}" "${this.sshKeyPath}" /grant:r "${process.env['USERDOMAIN']}\\${process.env['USERNAME']}:F"` | ||||||
|  |       ) | ||||||
|  |       await exec.exec(`"${icacls}" "${this.sshKeyPath}" /inheritance:r`) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Write known hosts
 | ||||||
|  |     const userKnownHostsPath = path.join(os.homedir(), '.ssh', 'known_hosts') | ||||||
|  |     let userKnownHosts = '' | ||||||
|  |     try { | ||||||
|  |       userKnownHosts = ( | ||||||
|  |         await fs.promises.readFile(userKnownHostsPath) | ||||||
|  |       ).toString() | ||||||
|  |     } catch (err) { | ||||||
|  |       if (err.code !== 'ENOENT') { | ||||||
|  |         throw err | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     let knownHosts = '' | ||||||
|  |     if (userKnownHosts) { | ||||||
|  |       knownHosts += `# Begin from ${userKnownHostsPath}\n${userKnownHosts}\n# End from ${userKnownHostsPath}\n` | ||||||
|  |     } | ||||||
|  |     if (this.settings.sshKnownHosts) { | ||||||
|  |       knownHosts += `# Begin from input known hosts\n${this.settings.sshKnownHosts}\n# end from input known hosts\n` | ||||||
|  |     } | ||||||
|  |     knownHosts += `# Begin implicitly added github.com\ngithub.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==\n# End implicitly added github.com\n` | ||||||
|  |     this.sshKnownHostsPath = path.join(runnerTemp, `${uniqueId}_known_hosts`) | ||||||
|  |     stateHelper.setSshKnownHostsPath(this.sshKnownHostsPath) | ||||||
|  |     await fs.promises.writeFile(this.sshKnownHostsPath, knownHosts) | ||||||
|  | 
 | ||||||
|  |     // Configure GIT_SSH_COMMAND
 | ||||||
|  |     const sshPath = await io.which('ssh', true) | ||||||
|  |     let sshCommand = `"${sshPath}" -i "$RUNNER_TEMP/${path.basename( | ||||||
|  |       this.sshKeyPath | ||||||
|  |     )}"` | ||||||
|  |     if (this.settings.sshStrict) { | ||||||
|  |       sshCommand += ' -o StrictHostKeyChecking=yes -o CheckHostIP=no' | ||||||
|  |     } | ||||||
|  |     sshCommand += ` -o "UserKnownHostsFile=$RUNNER_TEMP/${path.basename( | ||||||
|  |       this.sshKnownHostsPath | ||||||
|  |     )}"` | ||||||
|  |     core.info(`Temporarily overriding GIT_SSH_COMMAND=${sshCommand}`) | ||||||
|  |     this.git.setEnvironmentVariable('GIT_SSH_COMMAND', sshCommand) | ||||||
|  | 
 | ||||||
|  |     // Configure core.sshCommand
 | ||||||
|  |     if (this.settings.persistCredentials) { | ||||||
|  |       await this.git.config(SSH_COMMAND_KEY, sshCommand) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   private async configureToken( |   private async configureToken( | ||||||
|     configPath?: string, |     configPath?: string, | ||||||
|     globalConfig?: boolean |     globalConfig?: boolean | ||||||
| @ -198,23 +282,55 @@ class GitAuthHelper { | |||||||
|     await fs.promises.writeFile(configPath, content) |     await fs.promises.writeFile(configPath, content) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   private async removeSsh(): Promise<void> { | ||||||
|  |     // SSH key
 | ||||||
|  |     const keyPath = this.sshKeyPath || stateHelper.SshKeyPath | ||||||
|  |     if (keyPath) { | ||||||
|  |       try { | ||||||
|  |         await io.rmRF(keyPath) | ||||||
|  |       } catch (err) { | ||||||
|  |         core.debug(err.message) | ||||||
|  |         core.warning(`Failed to remove SSH key '${keyPath}'`) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // SSH known hosts
 | ||||||
|  |     const knownHostsPath = | ||||||
|  |       this.sshKnownHostsPath || stateHelper.SshKnownHostsPath | ||||||
|  |     if (knownHostsPath) { | ||||||
|  |       try { | ||||||
|  |         await io.rmRF(knownHostsPath) | ||||||
|  |       } catch { | ||||||
|  |         // Intentionally empty
 | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // SSH command
 | ||||||
|  |     await this.removeGitConfig(SSH_COMMAND_KEY) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   private async removeToken(): Promise<void> { |   private async removeToken(): Promise<void> { | ||||||
|     // HTTP extra header
 |     // HTTP extra header
 | ||||||
|     await this.removeGitConfig(this.tokenConfigKey) |     await this.removeGitConfig(this.tokenConfigKey) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private async removeGitConfig(configKey: string): Promise<void> { |   private async removeGitConfig( | ||||||
|     if ( |     configKey: string, | ||||||
|       (await this.git.configExists(configKey)) && |     submoduleOnly: boolean = false | ||||||
|       !(await this.git.tryConfigUnset(configKey)) |   ): Promise<void> { | ||||||
|     ) { |     if (!submoduleOnly) { | ||||||
|       // Load the config contents
 |       if ( | ||||||
|       core.warning(`Failed to remove '${configKey}' from the git config`) |         (await this.git.configExists(configKey)) && | ||||||
|  |         !(await this.git.tryConfigUnset(configKey)) | ||||||
|  |       ) { | ||||||
|  |         // Load the config contents
 | ||||||
|  |         core.warning(`Failed to remove '${configKey}' from the git config`) | ||||||
|  |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const pattern = regexpHelper.escape(configKey) |     const pattern = regexpHelper.escape(configKey) | ||||||
|     await this.git.submoduleForeach( |     await this.git.submoduleForeach( | ||||||
|       `git config --local --name-only --get-regexp ${pattern} && git config --local --unset-all ${configKey} || :`, |       `git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :`, | ||||||
|       true |       true | ||||||
|     ) |     ) | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -18,9 +18,13 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> { | |||||||
|   core.info( |   core.info( | ||||||
|     `Syncing repository: ${settings.repositoryOwner}/${settings.repositoryName}` |     `Syncing repository: ${settings.repositoryOwner}/${settings.repositoryName}` | ||||||
|   ) |   ) | ||||||
|   const repositoryUrl = `https://${hostname}/${encodeURIComponent( |   const repositoryUrl = settings.sshKey | ||||||
|     settings.repositoryOwner |     ? `git@${hostname}:${encodeURIComponent( | ||||||
|   )}/${encodeURIComponent(settings.repositoryName)}` |         settings.repositoryOwner | ||||||
|  |       )}/${encodeURIComponent(settings.repositoryName)}.git` | ||||||
|  |     : `https://${hostname}/${encodeURIComponent( | ||||||
|  |         settings.repositoryOwner | ||||||
|  |       )}/${encodeURIComponent(settings.repositoryName)}` | ||||||
| 
 | 
 | ||||||
|   // Remove conflicting file path
 |   // Remove conflicting file path
 | ||||||
|   if (fsHelper.fileExistsSync(settings.repositoryPath)) { |   if (fsHelper.fileExistsSync(settings.repositoryPath)) { | ||||||
|  | |||||||
| @ -10,5 +10,8 @@ export interface IGitSourceSettings { | |||||||
|   submodules: boolean |   submodules: boolean | ||||||
|   nestedSubmodules: boolean |   nestedSubmodules: boolean | ||||||
|   authToken: string |   authToken: string | ||||||
|  |   sshKey: string | ||||||
|  |   sshKnownHosts: string | ||||||
|  |   sshStrict: boolean | ||||||
|   persistCredentials: boolean |   persistCredentials: boolean | ||||||
| } | } | ||||||
|  | |||||||
| @ -112,6 +112,12 @@ export function getInputs(): IGitSourceSettings { | |||||||
|   // Auth token
 |   // Auth token
 | ||||||
|   result.authToken = core.getInput('token') |   result.authToken = core.getInput('token') | ||||||
| 
 | 
 | ||||||
|  |   // SSH
 | ||||||
|  |   result.sshKey = core.getInput('ssh-key') | ||||||
|  |   result.sshKnownHosts = core.getInput('ssh-known-hosts') | ||||||
|  |   result.sshStrict = | ||||||
|  |     (core.getInput('ssh-strict') || 'true').toUpperCase() === 'TRUE' | ||||||
|  | 
 | ||||||
|   // Persist credentials
 |   // Persist credentials
 | ||||||
|   result.persistCredentials = |   result.persistCredentials = | ||||||
|     (core.getInput('persist-credentials') || 'false').toUpperCase() === 'TRUE' |     (core.getInput('persist-credentials') || 'false').toUpperCase() === 'TRUE' | ||||||
|  | |||||||
| @ -59,13 +59,17 @@ function updateUsage( | |||||||
| 
 | 
 | ||||||
|     // Constrain the width of the description
 |     // Constrain the width of the description
 | ||||||
|     const width = 80 |     const width = 80 | ||||||
|     let description = input.description as string |     let description = (input.description as string) | ||||||
|  |       .trimRight() | ||||||
|  |       .replace(/\r\n/g, '\n') // Convert CR to LF
 | ||||||
|  |       .replace(/ +/g, ' ') //    Squash consecutive spaces
 | ||||||
|  |       .replace(/ \n/g, '\n') //  Squash space followed by newline
 | ||||||
|     while (description) { |     while (description) { | ||||||
|       // Longer than width? Find a space to break apart
 |       // Longer than width? Find a space to break apart
 | ||||||
|       let segment: string = description |       let segment: string = description | ||||||
|       if (description.length > width) { |       if (description.length > width) { | ||||||
|         segment = description.substr(0, width + 1) |         segment = description.substr(0, width + 1) | ||||||
|         while (!segment.endsWith(' ') && segment) { |         while (!segment.endsWith(' ') && !segment.endsWith('\n') && segment) { | ||||||
|           segment = segment.substr(0, segment.length - 1) |           segment = segment.substr(0, segment.length - 1) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -77,15 +81,30 @@ function updateUsage( | |||||||
|         segment = description |         segment = description | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       description = description.substr(segment.length) // Remaining
 |       // Check for newline
 | ||||||
|       segment = segment.trimRight() // Trim the trailing space
 |       const newlineIndex = segment.indexOf('\n') | ||||||
|       newReadme.push(`    # ${segment}`) |       if (newlineIndex >= 0) { | ||||||
|  |         segment = segment.substr(0, newlineIndex + 1) | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       // Append segment
 | ||||||
|  |       newReadme.push(`    # ${segment}`.trimRight()) | ||||||
|  | 
 | ||||||
|  |       // Remaining
 | ||||||
|  |       description = description.substr(segment.length) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Input and default
 |  | ||||||
|     if (input.default !== undefined) { |     if (input.default !== undefined) { | ||||||
|  |       // Append blank line if description had paragraphs
 | ||||||
|  |       if ((input.description as string).trimRight().match(/\n[ ]*\r?\n/)) { | ||||||
|  |         newReadme.push(`    #`) | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       // Default
 | ||||||
|       newReadme.push(`    # Default: ${input.default}`) |       newReadme.push(`    # Default: ${input.default}`) | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     // Input name
 | ||||||
|     newReadme.push(`    ${key}: ''`) |     newReadme.push(`    ${key}: ''`) | ||||||
| 
 | 
 | ||||||
|     firstInput = false |     firstInput = false | ||||||
|  | |||||||
| @ -11,6 +11,17 @@ export const IsPost = !!process.env['STATE_isPost'] | |||||||
| export const RepositoryPath = | export const RepositoryPath = | ||||||
|   (process.env['STATE_repositoryPath'] as string) || '' |   (process.env['STATE_repositoryPath'] as string) || '' | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * The SSH key path for the POST action. The value is empty during the MAIN action. | ||||||
|  |  */ | ||||||
|  | export const SshKeyPath = (process.env['STATE_sshKeyPath'] as string) || '' | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * The SSH known hosts path for the POST action. The value is empty during the MAIN action. | ||||||
|  |  */ | ||||||
|  | export const SshKnownHostsPath = | ||||||
|  |   (process.env['STATE_sshKnownHostsPath'] as string) || '' | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * Save the repository path so the POST action can retrieve the value. |  * Save the repository path so the POST action can retrieve the value. | ||||||
|  */ |  */ | ||||||
| @ -22,6 +33,24 @@ export function setRepositoryPath(repositoryPath: string) { | |||||||
|   ) |   ) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * Save the SSH key path so the POST action can retrieve the value. | ||||||
|  |  */ | ||||||
|  | export function setSshKeyPath(sshKeyPath: string) { | ||||||
|  |   coreCommand.issueCommand('save-state', {name: 'sshKeyPath'}, sshKeyPath) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Save the SSH known hosts path so the POST action can retrieve the value. | ||||||
|  |  */ | ||||||
|  | export function setSshKnownHostsPath(sshKnownHostsPath: string) { | ||||||
|  |   coreCommand.issueCommand( | ||||||
|  |     'save-state', | ||||||
|  |     {name: 'sshKnownHostsPath'}, | ||||||
|  |     sshKnownHostsPath | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // Publish a variable so that when the POST action runs, it can determine it should run the cleanup logic.
 | // Publish a variable so that when the POST action runs, it can determine it should run the cleanup logic.
 | ||||||
| // This is necessary since we don't have a separate entry point.
 | // This is necessary since we don't have a separate entry point.
 | ||||||
| if (!IsPost) { | if (!IsPost) { | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 eric sciple
						eric sciple