mirror of
				https://github.com/actions/checkout.git
				synced 2025-10-31 19:23:59 +08:00 
			
		
		
		
	more unit tests and corresponding refactoring (#174)
This commit is contained in:
		
							
								
								
									
										2
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							| @@ -19,8 +19,6 @@ jobs: | |||||||
|       - run: npm run build |       - run: npm run build | ||||||
|       - run: npm run format-check |       - run: npm run format-check | ||||||
|       - run: npm run lint |       - run: npm run lint | ||||||
|       - run: npm run pack |  | ||||||
|       - run: npm run gendocs |  | ||||||
|       - run: npm test |       - run: npm test | ||||||
|       - name: Verify no unstaged changes |       - name: Verify no unstaged changes | ||||||
|         run: __test__/verify-no-unstaged-changes.sh |         run: __test__/verify-no-unstaged-changes.sh | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,2 +1,3 @@ | |||||||
|  | __test__/_temp | ||||||
| lib/ | lib/ | ||||||
| node_modules/ | node_modules/ | ||||||
							
								
								
									
										200
									
								
								__test__/git-auth-helper.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										200
									
								
								__test__/git-auth-helper.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,200 @@ | |||||||
|  | import * as core from '@actions/core' | ||||||
|  | import * as fs from 'fs' | ||||||
|  | import * as gitAuthHelper from '../lib/git-auth-helper' | ||||||
|  | import * as io from '@actions/io' | ||||||
|  | import * as path from 'path' | ||||||
|  | import {IGitCommandManager} from '../lib/git-command-manager' | ||||||
|  | import {IGitSourceSettings} from '../lib/git-source-settings' | ||||||
|  |  | ||||||
|  | const testWorkspace = path.join(__dirname, '_temp', 'git-auth-helper') | ||||||
|  | const originalRunnerTemp = process.env['RUNNER_TEMP'] | ||||||
|  | let workspace: string | ||||||
|  | let gitConfigPath: string | ||||||
|  | let runnerTemp: string | ||||||
|  | let git: IGitCommandManager | ||||||
|  | let settings: IGitSourceSettings | ||||||
|  |  | ||||||
|  | describe('git-auth-helper tests', () => { | ||||||
|  |   beforeAll(async () => { | ||||||
|  |     // Clear test workspace | ||||||
|  |     await io.rmRF(testWorkspace) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   beforeEach(() => { | ||||||
|  |     // Mock setSecret | ||||||
|  |     jest.spyOn(core, 'setSecret').mockImplementation((secret: string) => {}) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   afterEach(() => { | ||||||
|  |     // Unregister mocks | ||||||
|  |     jest.restoreAllMocks() | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   afterAll(() => { | ||||||
|  |     // Restore RUNNER_TEMP | ||||||
|  |     delete process.env['RUNNER_TEMP'] | ||||||
|  |     if (originalRunnerTemp) { | ||||||
|  |       process.env['RUNNER_TEMP'] = originalRunnerTemp | ||||||
|  |     } | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   const configuresAuthHeader = 'configures auth header' | ||||||
|  |   it(configuresAuthHeader, async () => { | ||||||
|  |     // Arrange | ||||||
|  |     await setup(configuresAuthHeader) | ||||||
|  |     expect(settings.authToken).toBeTruthy() // sanity check | ||||||
|  |     const authHelper = gitAuthHelper.createAuthHelper(git, settings) | ||||||
|  |  | ||||||
|  |     // Act | ||||||
|  |     await authHelper.configureAuth() | ||||||
|  |  | ||||||
|  |     // Assert config | ||||||
|  |     const configContent = (await fs.promises.readFile(gitConfigPath)).toString() | ||||||
|  |     const basicCredential = Buffer.from( | ||||||
|  |       `x-access-token:${settings.authToken}`, | ||||||
|  |       'utf8' | ||||||
|  |     ).toString('base64') | ||||||
|  |     expect( | ||||||
|  |       configContent.indexOf( | ||||||
|  |         `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}` | ||||||
|  |       ) | ||||||
|  |     ).toBeGreaterThanOrEqual(0) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   const configuresAuthHeaderEvenWhenPersistCredentialsFalse = | ||||||
|  |     'configures auth header even when persist credentials false' | ||||||
|  |   it(configuresAuthHeaderEvenWhenPersistCredentialsFalse, async () => { | ||||||
|  |     // Arrange | ||||||
|  |     await setup(configuresAuthHeaderEvenWhenPersistCredentialsFalse) | ||||||
|  |     expect(settings.authToken).toBeTruthy() // sanity check | ||||||
|  |     settings.persistCredentials = false | ||||||
|  |     const authHelper = gitAuthHelper.createAuthHelper(git, settings) | ||||||
|  |  | ||||||
|  |     // Act | ||||||
|  |     await authHelper.configureAuth() | ||||||
|  |  | ||||||
|  |     // Assert config | ||||||
|  |     const configContent = (await fs.promises.readFile(gitConfigPath)).toString() | ||||||
|  |     expect( | ||||||
|  |       configContent.indexOf( | ||||||
|  |         `http.https://github.com/.extraheader AUTHORIZATION` | ||||||
|  |       ) | ||||||
|  |     ).toBeGreaterThanOrEqual(0) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   const registersBasicCredentialAsSecret = | ||||||
|  |     'registers basic credential as secret' | ||||||
|  |   it(registersBasicCredentialAsSecret, async () => { | ||||||
|  |     // Arrange | ||||||
|  |     await setup(registersBasicCredentialAsSecret) | ||||||
|  |     expect(settings.authToken).toBeTruthy() // sanity check | ||||||
|  |     const authHelper = gitAuthHelper.createAuthHelper(git, settings) | ||||||
|  |  | ||||||
|  |     // Act | ||||||
|  |     await authHelper.configureAuth() | ||||||
|  |  | ||||||
|  |     // Assert secret | ||||||
|  |     const setSecretSpy = core.setSecret as jest.Mock<any, any> | ||||||
|  |     expect(setSecretSpy).toHaveBeenCalledTimes(1) | ||||||
|  |     const expectedSecret = Buffer.from( | ||||||
|  |       `x-access-token:${settings.authToken}`, | ||||||
|  |       'utf8' | ||||||
|  |     ).toString('base64') | ||||||
|  |     expect(setSecretSpy).toHaveBeenCalledWith(expectedSecret) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   const removesToken = 'removes token' | ||||||
|  |   it(removesToken, async () => { | ||||||
|  |     // Arrange | ||||||
|  |     await setup(removesToken) | ||||||
|  |     const authHelper = gitAuthHelper.createAuthHelper(git, settings) | ||||||
|  |     await authHelper.configureAuth() | ||||||
|  |     let gitConfigContent = ( | ||||||
|  |       await fs.promises.readFile(gitConfigPath) | ||||||
|  |     ).toString() | ||||||
|  |     expect(gitConfigContent.indexOf('http.')).toBeGreaterThanOrEqual(0) // sanity check | ||||||
|  |  | ||||||
|  |     // Act | ||||||
|  |     await authHelper.removeAuth() | ||||||
|  |  | ||||||
|  |     // Assert git config | ||||||
|  |     gitConfigContent = (await fs.promises.readFile(gitConfigPath)).toString() | ||||||
|  |     expect(gitConfigContent.indexOf('http.')).toBeLessThan(0) | ||||||
|  |   }) | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | async function setup(testName: string): Promise<void> { | ||||||
|  |   testName = testName.replace(/[^a-zA-Z0-9_]+/g, '-') | ||||||
|  |  | ||||||
|  |   // Directories | ||||||
|  |   workspace = path.join(testWorkspace, testName, 'workspace') | ||||||
|  |   runnerTemp = path.join(testWorkspace, testName, 'runner-temp') | ||||||
|  |   await fs.promises.mkdir(workspace, {recursive: true}) | ||||||
|  |   await fs.promises.mkdir(runnerTemp, {recursive: true}) | ||||||
|  |   process.env['RUNNER_TEMP'] = runnerTemp | ||||||
|  |  | ||||||
|  |   // Create git config | ||||||
|  |   gitConfigPath = path.join(workspace, '.git', 'config') | ||||||
|  |   await fs.promises.mkdir(path.join(workspace, '.git'), {recursive: true}) | ||||||
|  |   await fs.promises.writeFile(path.join(workspace, '.git', 'config'), '') | ||||||
|  |  | ||||||
|  |   git = { | ||||||
|  |     branchDelete: jest.fn(), | ||||||
|  |     branchExists: jest.fn(), | ||||||
|  |     branchList: jest.fn(), | ||||||
|  |     checkout: jest.fn(), | ||||||
|  |     checkoutDetach: jest.fn(), | ||||||
|  |     config: jest.fn(async (key: string, value: string) => { | ||||||
|  |       await fs.promises.appendFile(gitConfigPath, `\n${key} ${value}`) | ||||||
|  |     }), | ||||||
|  |     configExists: jest.fn( | ||||||
|  |       async (key: string): Promise<boolean> => { | ||||||
|  |         const content = await fs.promises.readFile(gitConfigPath) | ||||||
|  |         const lines = content | ||||||
|  |           .toString() | ||||||
|  |           .split('\n') | ||||||
|  |           .filter(x => x) | ||||||
|  |         return lines.some(x => x.startsWith(key)) | ||||||
|  |       } | ||||||
|  |     ), | ||||||
|  |     fetch: jest.fn(), | ||||||
|  |     getWorkingDirectory: jest.fn(() => workspace), | ||||||
|  |     init: jest.fn(), | ||||||
|  |     isDetached: jest.fn(), | ||||||
|  |     lfsFetch: jest.fn(), | ||||||
|  |     lfsInstall: jest.fn(), | ||||||
|  |     log1: jest.fn(), | ||||||
|  |     remoteAdd: jest.fn(), | ||||||
|  |     setEnvironmentVariable: jest.fn(), | ||||||
|  |     tagExists: jest.fn(), | ||||||
|  |     tryClean: jest.fn(), | ||||||
|  |     tryConfigUnset: jest.fn( | ||||||
|  |       async (key: string): Promise<boolean> => { | ||||||
|  |         let content = await fs.promises.readFile(gitConfigPath) | ||||||
|  |         let lines = content | ||||||
|  |           .toString() | ||||||
|  |           .split('\n') | ||||||
|  |           .filter(x => x) | ||||||
|  |           .filter(x => !x.startsWith(key)) | ||||||
|  |         await fs.promises.writeFile(gitConfigPath, lines.join('\n')) | ||||||
|  |         return true | ||||||
|  |       } | ||||||
|  |     ), | ||||||
|  |     tryDisableAutomaticGarbageCollection: jest.fn(), | ||||||
|  |     tryGetFetchUrl: jest.fn(), | ||||||
|  |     tryReset: jest.fn() | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   settings = { | ||||||
|  |     authToken: 'some auth token', | ||||||
|  |     clean: true, | ||||||
|  |     commit: '', | ||||||
|  |     fetchDepth: 1, | ||||||
|  |     lfs: false, | ||||||
|  |     persistCredentials: true, | ||||||
|  |     ref: 'refs/heads/master', | ||||||
|  |     repositoryName: 'my-repo', | ||||||
|  |     repositoryOwner: 'my-org', | ||||||
|  |     repositoryPath: '' | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										382
									
								
								__test__/git-directory-helper.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										382
									
								
								__test__/git-directory-helper.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,382 @@ | |||||||
|  | import * as core from '@actions/core' | ||||||
|  | import * as fs from 'fs' | ||||||
|  | import * as gitDirectoryHelper from '../lib/git-directory-helper' | ||||||
|  | import * as io from '@actions/io' | ||||||
|  | import * as path from 'path' | ||||||
|  | import {IGitCommandManager} from '../lib/git-command-manager' | ||||||
|  |  | ||||||
|  | const testWorkspace = path.join(__dirname, '_temp', 'git-directory-helper') | ||||||
|  | let repositoryPath: string | ||||||
|  | let repositoryUrl: string | ||||||
|  | let clean: boolean | ||||||
|  | let git: IGitCommandManager | ||||||
|  |  | ||||||
|  | describe('git-directory-helper tests', () => { | ||||||
|  |   beforeAll(async () => { | ||||||
|  |     // Clear test workspace | ||||||
|  |     await io.rmRF(testWorkspace) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   beforeEach(() => { | ||||||
|  |     // Mock error/warning/info/debug | ||||||
|  |     jest.spyOn(core, 'error').mockImplementation(jest.fn()) | ||||||
|  |     jest.spyOn(core, 'warning').mockImplementation(jest.fn()) | ||||||
|  |     jest.spyOn(core, 'info').mockImplementation(jest.fn()) | ||||||
|  |     jest.spyOn(core, 'debug').mockImplementation(jest.fn()) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   afterEach(() => { | ||||||
|  |     // Unregister mocks | ||||||
|  |     jest.restoreAllMocks() | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   const cleansWhenCleanTrue = 'cleans when clean true' | ||||||
|  |   it(cleansWhenCleanTrue, async () => { | ||||||
|  |     // Arrange | ||||||
|  |     await setup(cleansWhenCleanTrue) | ||||||
|  |     await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '') | ||||||
|  |  | ||||||
|  |     // Act | ||||||
|  |     await gitDirectoryHelper.prepareExistingDirectory( | ||||||
|  |       git, | ||||||
|  |       repositoryPath, | ||||||
|  |       repositoryUrl, | ||||||
|  |       clean | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     // Assert | ||||||
|  |     const files = await fs.promises.readdir(repositoryPath) | ||||||
|  |     expect(files.sort()).toEqual(['.git', 'my-file']) | ||||||
|  |     expect(git.tryClean).toHaveBeenCalled() | ||||||
|  |     expect(git.tryReset).toHaveBeenCalled() | ||||||
|  |     expect(core.warning).not.toHaveBeenCalled() | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   const checkoutDetachWhenNotDetached = 'checkout detach when not detached' | ||||||
|  |   it(checkoutDetachWhenNotDetached, async () => { | ||||||
|  |     // Arrange | ||||||
|  |     await setup(checkoutDetachWhenNotDetached) | ||||||
|  |     await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '') | ||||||
|  |  | ||||||
|  |     // Act | ||||||
|  |     await gitDirectoryHelper.prepareExistingDirectory( | ||||||
|  |       git, | ||||||
|  |       repositoryPath, | ||||||
|  |       repositoryUrl, | ||||||
|  |       clean | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     // Assert | ||||||
|  |     const files = await fs.promises.readdir(repositoryPath) | ||||||
|  |     expect(files.sort()).toEqual(['.git', 'my-file']) | ||||||
|  |     expect(git.checkoutDetach).toHaveBeenCalled() | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   const doesNotCheckoutDetachWhenNotAlreadyDetached = | ||||||
|  |     'does not checkout detach when already detached' | ||||||
|  |   it(doesNotCheckoutDetachWhenNotAlreadyDetached, async () => { | ||||||
|  |     // Arrange | ||||||
|  |     await setup(doesNotCheckoutDetachWhenNotAlreadyDetached) | ||||||
|  |     await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '') | ||||||
|  |     const mockIsDetached = git.isDetached as jest.Mock<any, any> | ||||||
|  |     mockIsDetached.mockImplementation(async () => { | ||||||
|  |       return true | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     // Act | ||||||
|  |     await gitDirectoryHelper.prepareExistingDirectory( | ||||||
|  |       git, | ||||||
|  |       repositoryPath, | ||||||
|  |       repositoryUrl, | ||||||
|  |       clean | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     // Assert | ||||||
|  |     const files = await fs.promises.readdir(repositoryPath) | ||||||
|  |     expect(files.sort()).toEqual(['.git', 'my-file']) | ||||||
|  |     expect(git.checkoutDetach).not.toHaveBeenCalled() | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   const doesNotCleanWhenCleanFalse = 'does not clean when clean false' | ||||||
|  |   it(doesNotCleanWhenCleanFalse, async () => { | ||||||
|  |     // Arrange | ||||||
|  |     await setup(doesNotCleanWhenCleanFalse) | ||||||
|  |     clean = false | ||||||
|  |     await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '') | ||||||
|  |  | ||||||
|  |     // Act | ||||||
|  |     await gitDirectoryHelper.prepareExistingDirectory( | ||||||
|  |       git, | ||||||
|  |       repositoryPath, | ||||||
|  |       repositoryUrl, | ||||||
|  |       clean | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     // Assert | ||||||
|  |     const files = await fs.promises.readdir(repositoryPath) | ||||||
|  |     expect(files.sort()).toEqual(['.git', 'my-file']) | ||||||
|  |     expect(git.isDetached).toHaveBeenCalled() | ||||||
|  |     expect(git.branchList).toHaveBeenCalled() | ||||||
|  |     expect(core.warning).not.toHaveBeenCalled() | ||||||
|  |     expect(git.tryClean).not.toHaveBeenCalled() | ||||||
|  |     expect(git.tryReset).not.toHaveBeenCalled() | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   const removesContentsWhenCleanFails = 'removes contents when clean fails' | ||||||
|  |   it(removesContentsWhenCleanFails, async () => { | ||||||
|  |     // Arrange | ||||||
|  |     await setup(removesContentsWhenCleanFails) | ||||||
|  |     await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '') | ||||||
|  |     let mockTryClean = git.tryClean as jest.Mock<any, any> | ||||||
|  |     mockTryClean.mockImplementation(async () => { | ||||||
|  |       return false | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     // Act | ||||||
|  |     await gitDirectoryHelper.prepareExistingDirectory( | ||||||
|  |       git, | ||||||
|  |       repositoryPath, | ||||||
|  |       repositoryUrl, | ||||||
|  |       clean | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     // Assert | ||||||
|  |     const files = await fs.promises.readdir(repositoryPath) | ||||||
|  |     expect(files).toHaveLength(0) | ||||||
|  |     expect(git.tryClean).toHaveBeenCalled() | ||||||
|  |     expect(core.warning).toHaveBeenCalled() | ||||||
|  |     expect(git.tryReset).not.toHaveBeenCalled() | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   const removesContentsWhenDifferentRepositoryUrl = | ||||||
|  |     'removes contents when different repository url' | ||||||
|  |   it(removesContentsWhenDifferentRepositoryUrl, async () => { | ||||||
|  |     // Arrange | ||||||
|  |     await setup(removesContentsWhenDifferentRepositoryUrl) | ||||||
|  |     clean = false | ||||||
|  |     await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '') | ||||||
|  |     const differentRepositoryUrl = | ||||||
|  |       'https://github.com/my-different-org/my-different-repo' | ||||||
|  |  | ||||||
|  |     // Act | ||||||
|  |     await gitDirectoryHelper.prepareExistingDirectory( | ||||||
|  |       git, | ||||||
|  |       repositoryPath, | ||||||
|  |       differentRepositoryUrl, | ||||||
|  |       clean | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     // Assert | ||||||
|  |     const files = await fs.promises.readdir(repositoryPath) | ||||||
|  |     expect(files).toHaveLength(0) | ||||||
|  |     expect(core.warning).not.toHaveBeenCalled() | ||||||
|  |     expect(git.isDetached).not.toHaveBeenCalled() | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   const removesContentsWhenNoGitDirectory = | ||||||
|  |     'removes contents when no git directory' | ||||||
|  |   it(removesContentsWhenNoGitDirectory, async () => { | ||||||
|  |     // Arrange | ||||||
|  |     await setup(removesContentsWhenNoGitDirectory) | ||||||
|  |     clean = false | ||||||
|  |     await io.rmRF(path.join(repositoryPath, '.git')) | ||||||
|  |     await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '') | ||||||
|  |  | ||||||
|  |     // Act | ||||||
|  |     await gitDirectoryHelper.prepareExistingDirectory( | ||||||
|  |       git, | ||||||
|  |       repositoryPath, | ||||||
|  |       repositoryUrl, | ||||||
|  |       clean | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     // Assert | ||||||
|  |     const files = await fs.promises.readdir(repositoryPath) | ||||||
|  |     expect(files).toHaveLength(0) | ||||||
|  |     expect(core.warning).not.toHaveBeenCalled() | ||||||
|  |     expect(git.isDetached).not.toHaveBeenCalled() | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   const removesContentsWhenResetFails = 'removes contents when reset fails' | ||||||
|  |   it(removesContentsWhenResetFails, async () => { | ||||||
|  |     // Arrange | ||||||
|  |     await setup(removesContentsWhenResetFails) | ||||||
|  |     await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '') | ||||||
|  |     let mockTryReset = git.tryReset as jest.Mock<any, any> | ||||||
|  |     mockTryReset.mockImplementation(async () => { | ||||||
|  |       return false | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     // Act | ||||||
|  |     await gitDirectoryHelper.prepareExistingDirectory( | ||||||
|  |       git, | ||||||
|  |       repositoryPath, | ||||||
|  |       repositoryUrl, | ||||||
|  |       clean | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     // Assert | ||||||
|  |     const files = await fs.promises.readdir(repositoryPath) | ||||||
|  |     expect(files).toHaveLength(0) | ||||||
|  |     expect(git.tryClean).toHaveBeenCalled() | ||||||
|  |     expect(git.tryReset).toHaveBeenCalled() | ||||||
|  |     expect(core.warning).toHaveBeenCalled() | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   const removesContentsWhenUndefinedGitCommandManager = | ||||||
|  |     'removes contents when undefined git command manager' | ||||||
|  |   it(removesContentsWhenUndefinedGitCommandManager, async () => { | ||||||
|  |     // Arrange | ||||||
|  |     await setup(removesContentsWhenUndefinedGitCommandManager) | ||||||
|  |     clean = false | ||||||
|  |     await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '') | ||||||
|  |  | ||||||
|  |     // Act | ||||||
|  |     await gitDirectoryHelper.prepareExistingDirectory( | ||||||
|  |       undefined, | ||||||
|  |       repositoryPath, | ||||||
|  |       repositoryUrl, | ||||||
|  |       clean | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     // Assert | ||||||
|  |     const files = await fs.promises.readdir(repositoryPath) | ||||||
|  |     expect(files).toHaveLength(0) | ||||||
|  |     expect(core.warning).not.toHaveBeenCalled() | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   const removesLocalBranches = 'removes local branches' | ||||||
|  |   it(removesLocalBranches, async () => { | ||||||
|  |     // Arrange | ||||||
|  |     await setup(removesLocalBranches) | ||||||
|  |     await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '') | ||||||
|  |     const mockBranchList = git.branchList as jest.Mock<any, any> | ||||||
|  |     mockBranchList.mockImplementation(async (remote: boolean) => { | ||||||
|  |       return remote ? [] : ['local-branch-1', 'local-branch-2'] | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     // Act | ||||||
|  |     await gitDirectoryHelper.prepareExistingDirectory( | ||||||
|  |       git, | ||||||
|  |       repositoryPath, | ||||||
|  |       repositoryUrl, | ||||||
|  |       clean | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     // Assert | ||||||
|  |     const files = await fs.promises.readdir(repositoryPath) | ||||||
|  |     expect(files.sort()).toEqual(['.git', 'my-file']) | ||||||
|  |     expect(git.branchDelete).toHaveBeenCalledWith(false, 'local-branch-1') | ||||||
|  |     expect(git.branchDelete).toHaveBeenCalledWith(false, 'local-branch-2') | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   const removesLockFiles = 'removes lock files' | ||||||
|  |   it(removesLockFiles, async () => { | ||||||
|  |     // Arrange | ||||||
|  |     await setup(removesLockFiles) | ||||||
|  |     clean = false | ||||||
|  |     await fs.promises.writeFile( | ||||||
|  |       path.join(repositoryPath, '.git', 'index.lock'), | ||||||
|  |       '' | ||||||
|  |     ) | ||||||
|  |     await fs.promises.writeFile( | ||||||
|  |       path.join(repositoryPath, '.git', 'shallow.lock'), | ||||||
|  |       '' | ||||||
|  |     ) | ||||||
|  |     await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '') | ||||||
|  |  | ||||||
|  |     // Act | ||||||
|  |     await gitDirectoryHelper.prepareExistingDirectory( | ||||||
|  |       git, | ||||||
|  |       repositoryPath, | ||||||
|  |       repositoryUrl, | ||||||
|  |       clean | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     // Assert | ||||||
|  |     let files = await fs.promises.readdir(path.join(repositoryPath, '.git')) | ||||||
|  |     expect(files).toHaveLength(0) | ||||||
|  |     files = await fs.promises.readdir(repositoryPath) | ||||||
|  |     expect(files.sort()).toEqual(['.git', 'my-file']) | ||||||
|  |     expect(git.isDetached).toHaveBeenCalled() | ||||||
|  |     expect(git.branchList).toHaveBeenCalled() | ||||||
|  |     expect(core.warning).not.toHaveBeenCalled() | ||||||
|  |     expect(git.tryClean).not.toHaveBeenCalled() | ||||||
|  |     expect(git.tryReset).not.toHaveBeenCalled() | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   const removesRemoteBranches = 'removes local branches' | ||||||
|  |   it(removesRemoteBranches, async () => { | ||||||
|  |     // Arrange | ||||||
|  |     await setup(removesRemoteBranches) | ||||||
|  |     await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '') | ||||||
|  |     const mockBranchList = git.branchList as jest.Mock<any, any> | ||||||
|  |     mockBranchList.mockImplementation(async (remote: boolean) => { | ||||||
|  |       return remote ? ['remote-branch-1', 'remote-branch-2'] : [] | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     // Act | ||||||
|  |     await gitDirectoryHelper.prepareExistingDirectory( | ||||||
|  |       git, | ||||||
|  |       repositoryPath, | ||||||
|  |       repositoryUrl, | ||||||
|  |       clean | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     // Assert | ||||||
|  |     const files = await fs.promises.readdir(repositoryPath) | ||||||
|  |     expect(files.sort()).toEqual(['.git', 'my-file']) | ||||||
|  |     expect(git.branchDelete).toHaveBeenCalledWith(true, 'remote-branch-1') | ||||||
|  |     expect(git.branchDelete).toHaveBeenCalledWith(true, 'remote-branch-2') | ||||||
|  |   }) | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | async function setup(testName: string): Promise<void> { | ||||||
|  |   testName = testName.replace(/[^a-zA-Z0-9_]+/g, '-') | ||||||
|  |  | ||||||
|  |   // Repository directory | ||||||
|  |   repositoryPath = path.join(testWorkspace, testName) | ||||||
|  |   await fs.promises.mkdir(path.join(repositoryPath, '.git'), {recursive: true}) | ||||||
|  |  | ||||||
|  |   // Repository URL | ||||||
|  |   repositoryUrl = 'https://github.com/my-org/my-repo' | ||||||
|  |  | ||||||
|  |   // Clean | ||||||
|  |   clean = true | ||||||
|  |  | ||||||
|  |   // Git command manager | ||||||
|  |   git = { | ||||||
|  |     branchDelete: jest.fn(), | ||||||
|  |     branchExists: jest.fn(), | ||||||
|  |     branchList: jest.fn(async () => { | ||||||
|  |       return [] | ||||||
|  |     }), | ||||||
|  |     checkout: jest.fn(), | ||||||
|  |     checkoutDetach: jest.fn(), | ||||||
|  |     config: jest.fn(), | ||||||
|  |     configExists: jest.fn(), | ||||||
|  |     fetch: jest.fn(), | ||||||
|  |     getWorkingDirectory: jest.fn(() => repositoryPath), | ||||||
|  |     init: jest.fn(), | ||||||
|  |     isDetached: jest.fn(), | ||||||
|  |     lfsFetch: jest.fn(), | ||||||
|  |     lfsInstall: jest.fn(), | ||||||
|  |     log1: jest.fn(), | ||||||
|  |     remoteAdd: jest.fn(), | ||||||
|  |     setEnvironmentVariable: jest.fn(), | ||||||
|  |     tagExists: jest.fn(), | ||||||
|  |     tryClean: jest.fn(async () => { | ||||||
|  |       return true | ||||||
|  |     }), | ||||||
|  |     tryConfigUnset: jest.fn(), | ||||||
|  |     tryDisableAutomaticGarbageCollection: jest.fn(), | ||||||
|  |     tryGetFetchUrl: jest.fn(async () => { | ||||||
|  |       // Sanity check - this function shouldn't be called when the .git directory doesn't exist | ||||||
|  |       await fs.promises.stat(path.join(repositoryPath, '.git')) | ||||||
|  |       return repositoryUrl | ||||||
|  |     }), | ||||||
|  |     tryReset: jest.fn(async () => { | ||||||
|  |       return true | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -4,7 +4,7 @@ import * as fsHelper from '../lib/fs-helper' | |||||||
| import * as github from '@actions/github' | import * as github from '@actions/github' | ||||||
| import * as inputHelper from '../lib/input-helper' | import * as inputHelper from '../lib/input-helper' | ||||||
| import * as path from 'path' | import * as path from 'path' | ||||||
| import {ISourceSettings} from '../lib/git-source-provider' | import {IGitSourceSettings} from '../lib/git-source-settings' | ||||||
|  |  | ||||||
| const originalGitHubWorkspace = process.env['GITHUB_WORKSPACE'] | const originalGitHubWorkspace = process.env['GITHUB_WORKSPACE'] | ||||||
| const gitHubWorkspace = path.resolve('/checkout-tests/workspace') | const gitHubWorkspace = path.resolve('/checkout-tests/workspace') | ||||||
| @@ -17,12 +17,18 @@ let originalContext = {...github.context} | |||||||
|  |  | ||||||
| describe('input-helper tests', () => { | describe('input-helper tests', () => { | ||||||
|   beforeAll(() => { |   beforeAll(() => { | ||||||
|     // Mock @actions/core getInput() |     // Mock getInput | ||||||
|     jest.spyOn(core, 'getInput').mockImplementation((name: string) => { |     jest.spyOn(core, 'getInput').mockImplementation((name: string) => { | ||||||
|       return inputs[name] |       return inputs[name] | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|     // Mock @actions/github context |     // Mock error/warning/info/debug | ||||||
|  |     jest.spyOn(core, 'error').mockImplementation(jest.fn()) | ||||||
|  |     jest.spyOn(core, 'warning').mockImplementation(jest.fn()) | ||||||
|  |     jest.spyOn(core, 'info').mockImplementation(jest.fn()) | ||||||
|  |     jest.spyOn(core, 'debug').mockImplementation(jest.fn()) | ||||||
|  |  | ||||||
|  |     // Mock github context | ||||||
|     jest.spyOn(github.context, 'repo', 'get').mockImplementation(() => { |     jest.spyOn(github.context, 'repo', 'get').mockImplementation(() => { | ||||||
|       return { |       return { | ||||||
|         owner: 'some-owner', |         owner: 'some-owner', | ||||||
| @@ -62,7 +68,7 @@ describe('input-helper tests', () => { | |||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   it('sets defaults', () => { |   it('sets defaults', () => { | ||||||
|     const settings: ISourceSettings = inputHelper.getInputs() |     const settings: IGitSourceSettings = inputHelper.getInputs() | ||||||
|     expect(settings).toBeTruthy() |     expect(settings).toBeTruthy() | ||||||
|     expect(settings.authToken).toBeFalsy() |     expect(settings.authToken).toBeFalsy() | ||||||
|     expect(settings.clean).toBe(true) |     expect(settings.clean).toBe(true) | ||||||
| @@ -80,7 +86,7 @@ describe('input-helper tests', () => { | |||||||
|     let originalRef = github.context.ref |     let originalRef = github.context.ref | ||||||
|     try { |     try { | ||||||
|       github.context.ref = 'some-unqualified-ref' |       github.context.ref = 'some-unqualified-ref' | ||||||
|       const settings: ISourceSettings = inputHelper.getInputs() |       const settings: IGitSourceSettings = inputHelper.getInputs() | ||||||
|       expect(settings).toBeTruthy() |       expect(settings).toBeTruthy() | ||||||
|       expect(settings.commit).toBe('1234567890123456789012345678901234567890') |       expect(settings.commit).toBe('1234567890123456789012345678901234567890') | ||||||
|       expect(settings.ref).toBe('refs/heads/some-unqualified-ref') |       expect(settings.ref).toBe('refs/heads/some-unqualified-ref') | ||||||
| @@ -98,7 +104,7 @@ describe('input-helper tests', () => { | |||||||
|  |  | ||||||
|   it('roots path', () => { |   it('roots path', () => { | ||||||
|     inputs.path = 'some-directory/some-subdirectory' |     inputs.path = 'some-directory/some-subdirectory' | ||||||
|     const settings: ISourceSettings = inputHelper.getInputs() |     const settings: IGitSourceSettings = inputHelper.getInputs() | ||||||
|     expect(settings.repositoryPath).toBe( |     expect(settings.repositoryPath).toBe( | ||||||
|       path.join(gitHubWorkspace, 'some-directory', 'some-subdirectory') |       path.join(gitHubWorkspace, 'some-directory', 'some-subdirectory') | ||||||
|     ) |     ) | ||||||
| @@ -106,21 +112,21 @@ describe('input-helper tests', () => { | |||||||
|  |  | ||||||
|   it('sets correct default ref/sha for other repo', () => { |   it('sets correct default ref/sha for other repo', () => { | ||||||
|     inputs.repository = 'some-owner/some-other-repo' |     inputs.repository = 'some-owner/some-other-repo' | ||||||
|     const settings: ISourceSettings = inputHelper.getInputs() |     const settings: IGitSourceSettings = inputHelper.getInputs() | ||||||
|     expect(settings.ref).toBe('refs/heads/master') |     expect(settings.ref).toBe('refs/heads/master') | ||||||
|     expect(settings.commit).toBeFalsy() |     expect(settings.commit).toBeFalsy() | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   it('sets ref to empty when explicit sha', () => { |   it('sets ref to empty when explicit sha', () => { | ||||||
|     inputs.ref = '1111111111222222222233333333334444444444' |     inputs.ref = '1111111111222222222233333333334444444444' | ||||||
|     const settings: ISourceSettings = inputHelper.getInputs() |     const settings: IGitSourceSettings = inputHelper.getInputs() | ||||||
|     expect(settings.ref).toBeFalsy() |     expect(settings.ref).toBeFalsy() | ||||||
|     expect(settings.commit).toBe('1111111111222222222233333333334444444444') |     expect(settings.commit).toBe('1111111111222222222233333333334444444444') | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   it('sets sha to empty when explicit ref', () => { |   it('sets sha to empty when explicit ref', () => { | ||||||
|     inputs.ref = 'refs/heads/some-other-ref' |     inputs.ref = 'refs/heads/some-other-ref' | ||||||
|     const settings: ISourceSettings = inputHelper.getInputs() |     const settings: IGitSourceSettings = inputHelper.getInputs() | ||||||
|     expect(settings.ref).toBe('refs/heads/some-other-ref') |     expect(settings.ref).toBe('refs/heads/some-other-ref') | ||||||
|     expect(settings.commit).toBeFalsy() |     expect(settings.commit).toBeFalsy() | ||||||
|   }) |   }) | ||||||
|   | |||||||
							
								
								
									
										334
									
								
								dist/index.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										334
									
								
								dist/index.js
									
									
									
									
										vendored
									
									
								
							| @@ -5051,6 +5051,98 @@ function coerce (version) { | |||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /***/ }), | ||||||
|  |  | ||||||
|  | /***/ 287: | ||||||
|  | /***/ (function(__unusedmodule, exports, __webpack_require__) { | ||||||
|  |  | ||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||||||
|  |     function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||||||
|  |     return new (P || (P = Promise))(function (resolve, reject) { | ||||||
|  |         function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||||||
|  |         function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||||||
|  |         function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||||||
|  |         step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  | var __importStar = (this && this.__importStar) || function (mod) { | ||||||
|  |     if (mod && mod.__esModule) return mod; | ||||||
|  |     var result = {}; | ||||||
|  |     if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; | ||||||
|  |     result["default"] = mod; | ||||||
|  |     return result; | ||||||
|  | }; | ||||||
|  | Object.defineProperty(exports, "__esModule", { value: true }); | ||||||
|  | const core = __importStar(__webpack_require__(470)); | ||||||
|  | const fs = __importStar(__webpack_require__(747)); | ||||||
|  | const path = __importStar(__webpack_require__(622)); | ||||||
|  | const IS_WINDOWS = process.platform === 'win32'; | ||||||
|  | const HOSTNAME = 'github.com'; | ||||||
|  | const EXTRA_HEADER_KEY = `http.https://${HOSTNAME}/.extraheader`; | ||||||
|  | function createAuthHelper(git, settings) { | ||||||
|  |     return new GitAuthHelper(git, settings); | ||||||
|  | } | ||||||
|  | exports.createAuthHelper = createAuthHelper; | ||||||
|  | class GitAuthHelper { | ||||||
|  |     constructor(gitCommandManager, gitSourceSettings) { | ||||||
|  |         this.git = gitCommandManager; | ||||||
|  |         this.settings = gitSourceSettings || {}; | ||||||
|  |     } | ||||||
|  |     configureAuth() { | ||||||
|  |         return __awaiter(this, void 0, void 0, function* () { | ||||||
|  |             // Remove possible previous values | ||||||
|  |             yield this.removeAuth(); | ||||||
|  |             // Configure new values | ||||||
|  |             yield this.configureToken(); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |     removeAuth() { | ||||||
|  |         return __awaiter(this, void 0, void 0, function* () { | ||||||
|  |             yield this.removeToken(); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |     configureToken() { | ||||||
|  |         return __awaiter(this, void 0, void 0, function* () { | ||||||
|  |             // Configure a placeholder value. This approach avoids the credential being captured | ||||||
|  |             // by process creation audit events, which are commonly logged. For more information, | ||||||
|  |             // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing | ||||||
|  |             const placeholder = `AUTHORIZATION: basic ***`; | ||||||
|  |             yield this.git.config(EXTRA_HEADER_KEY, placeholder); | ||||||
|  |             // Determine the basic credential value | ||||||
|  |             const basicCredential = Buffer.from(`x-access-token:${this.settings.authToken}`, 'utf8').toString('base64'); | ||||||
|  |             core.setSecret(basicCredential); | ||||||
|  |             // Replace the value in the config file | ||||||
|  |             const configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config'); | ||||||
|  |             let content = (yield fs.promises.readFile(configPath)).toString(); | ||||||
|  |             const placeholderIndex = content.indexOf(placeholder); | ||||||
|  |             if (placeholderIndex < 0 || | ||||||
|  |                 placeholderIndex != content.lastIndexOf(placeholder)) { | ||||||
|  |                 throw new Error('Unable to replace auth placeholder in .git/config'); | ||||||
|  |             } | ||||||
|  |             content = content.replace(placeholder, `AUTHORIZATION: basic ${basicCredential}`); | ||||||
|  |             yield fs.promises.writeFile(configPath, content); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |     removeToken() { | ||||||
|  |         return __awaiter(this, void 0, void 0, function* () { | ||||||
|  |             // HTTP extra header | ||||||
|  |             yield this.removeGitConfig(EXTRA_HEADER_KEY); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |     removeGitConfig(configKey) { | ||||||
|  |         return __awaiter(this, void 0, void 0, function* () { | ||||||
|  |             if ((yield this.git.configExists(configKey)) && | ||||||
|  |                 !(yield this.git.tryConfigUnset(configKey))) { | ||||||
|  |                 // Load the config contents | ||||||
|  |                 core.warning(`Failed to remove '${configKey}' from the git config`); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
| /***/ }), | /***/ }), | ||||||
|  |  | ||||||
| /***/ 289: | /***/ 289: | ||||||
| @@ -5085,12 +5177,12 @@ const git_version_1 = __webpack_require__(559); | |||||||
| // Auth header not supported before 2.9 | // Auth header not supported before 2.9 | ||||||
| // Wire protocol v2 not supported before 2.18 | // Wire protocol v2 not supported before 2.18 | ||||||
| exports.MinimumGitVersion = new git_version_1.GitVersion('2.18'); | exports.MinimumGitVersion = new git_version_1.GitVersion('2.18'); | ||||||
| function CreateCommandManager(workingDirectory, lfs) { | function createCommandManager(workingDirectory, lfs) { | ||||||
|     return __awaiter(this, void 0, void 0, function* () { |     return __awaiter(this, void 0, void 0, function* () { | ||||||
|         return yield GitCommandManager.createCommandManager(workingDirectory, lfs); |         return yield GitCommandManager.createCommandManager(workingDirectory, lfs); | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
| exports.CreateCommandManager = CreateCommandManager; | exports.createCommandManager = createCommandManager; | ||||||
| class GitCommandManager { | class GitCommandManager { | ||||||
|     // Private constructor; use createCommandManager() |     // Private constructor; use createCommandManager() | ||||||
|     constructor() { |     constructor() { | ||||||
| @@ -5251,6 +5343,9 @@ class GitCommandManager { | |||||||
|             yield this.execGit(['remote', 'add', remoteName, remoteUrl]); |             yield this.execGit(['remote', 'add', remoteName, remoteUrl]); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |     setEnvironmentVariable(name, value) { | ||||||
|  |         this.gitEnv[name] = value; | ||||||
|  |     } | ||||||
|     tagExists(pattern) { |     tagExists(pattern) { | ||||||
|         return __awaiter(this, void 0, void 0, function* () { |         return __awaiter(this, void 0, void 0, function* () { | ||||||
|             const output = yield this.execGit(['tag', '--list', pattern]); |             const output = yield this.execGit(['tag', '--list', pattern]); | ||||||
| @@ -5420,21 +5515,21 @@ var __importStar = (this && this.__importStar) || function (mod) { | |||||||
| }; | }; | ||||||
| Object.defineProperty(exports, "__esModule", { value: true }); | Object.defineProperty(exports, "__esModule", { value: true }); | ||||||
| const core = __importStar(__webpack_require__(470)); | const core = __importStar(__webpack_require__(470)); | ||||||
| const fs = __importStar(__webpack_require__(747)); |  | ||||||
| const fsHelper = __importStar(__webpack_require__(618)); | const fsHelper = __importStar(__webpack_require__(618)); | ||||||
|  | const gitAuthHelper = __importStar(__webpack_require__(287)); | ||||||
| const gitCommandManager = __importStar(__webpack_require__(289)); | const gitCommandManager = __importStar(__webpack_require__(289)); | ||||||
|  | const gitDirectoryHelper = __importStar(__webpack_require__(438)); | ||||||
| const githubApiHelper = __importStar(__webpack_require__(464)); | const githubApiHelper = __importStar(__webpack_require__(464)); | ||||||
| const io = __importStar(__webpack_require__(1)); | const io = __importStar(__webpack_require__(1)); | ||||||
| const path = __importStar(__webpack_require__(622)); | const path = __importStar(__webpack_require__(622)); | ||||||
| const refHelper = __importStar(__webpack_require__(227)); | const refHelper = __importStar(__webpack_require__(227)); | ||||||
| const stateHelper = __importStar(__webpack_require__(153)); | const stateHelper = __importStar(__webpack_require__(153)); | ||||||
| const serverUrl = 'https://github.com/'; | const hostname = 'github.com'; | ||||||
| const authConfigKey = `http.${serverUrl}.extraheader`; |  | ||||||
| function getSource(settings) { | 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://github.com/${encodeURIComponent(settings.repositoryOwner)}/${encodeURIComponent(settings.repositoryName)}`; |         const repositoryUrl = `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); | ||||||
| @@ -5449,7 +5544,7 @@ function getSource(settings) { | |||||||
|         const git = yield getGitCommandManager(settings); |         const git = yield getGitCommandManager(settings); | ||||||
|         // Prepare existing directory, otherwise recreate |         // Prepare existing directory, otherwise recreate | ||||||
|         if (isExisting) { |         if (isExisting) { | ||||||
|             yield prepareExistingDirectory(git, settings.repositoryPath, repositoryUrl, settings.clean); |             yield gitDirectoryHelper.prepareExistingDirectory(git, settings.repositoryPath, repositoryUrl, settings.clean); | ||||||
|         } |         } | ||||||
|         if (!git) { |         if (!git) { | ||||||
|             // Downloading using REST API |             // Downloading using REST API | ||||||
| @@ -5469,11 +5564,10 @@ function getSource(settings) { | |||||||
|             if (!(yield git.tryDisableAutomaticGarbageCollection())) { |             if (!(yield git.tryDisableAutomaticGarbageCollection())) { | ||||||
|                 core.warning(`Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.`); |                 core.warning(`Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.`); | ||||||
|             } |             } | ||||||
|             // Remove possible previous extraheader |             const authHelper = gitAuthHelper.createAuthHelper(git, settings); | ||||||
|             yield removeGitConfig(git, authConfigKey); |  | ||||||
|             try { |             try { | ||||||
|                 // Config extraheader |                 // Configure auth | ||||||
|                 yield configureAuthToken(git, settings.authToken); |                 yield authHelper.configureAuth(); | ||||||
|                 // LFS install |                 // LFS install | ||||||
|                 if (settings.lfs) { |                 if (settings.lfs) { | ||||||
|                     yield git.lfsInstall(); |                     yield git.lfsInstall(); | ||||||
| @@ -5495,8 +5589,9 @@ function getSource(settings) { | |||||||
|                 yield git.log1(); |                 yield git.log1(); | ||||||
|             } |             } | ||||||
|             finally { |             finally { | ||||||
|  |                 // Remove auth | ||||||
|                 if (!settings.persistCredentials) { |                 if (!settings.persistCredentials) { | ||||||
|                     yield removeGitConfig(git, authConfigKey); |                     yield authHelper.removeAuth(); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| @@ -5512,22 +5607,22 @@ function cleanup(repositoryPath) { | |||||||
|         } |         } | ||||||
|         let git; |         let git; | ||||||
|         try { |         try { | ||||||
|             git = yield gitCommandManager.CreateCommandManager(repositoryPath, false); |             git = yield gitCommandManager.createCommandManager(repositoryPath, false); | ||||||
|         } |         } | ||||||
|         catch (_a) { |         catch (_a) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|         // Remove extraheader |         // Remove auth | ||||||
|         yield removeGitConfig(git, authConfigKey); |         const authHelper = gitAuthHelper.createAuthHelper(git); | ||||||
|  |         yield authHelper.removeAuth(); | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
| exports.cleanup = cleanup; | exports.cleanup = cleanup; | ||||||
| function getGitCommandManager(settings) { | function getGitCommandManager(settings) { | ||||||
|     return __awaiter(this, void 0, void 0, function* () { |     return __awaiter(this, void 0, void 0, function* () { | ||||||
|         core.info(`Working directory is '${settings.repositoryPath}'`); |         core.info(`Working directory is '${settings.repositoryPath}'`); | ||||||
|         let git = null; |  | ||||||
|         try { |         try { | ||||||
|             return yield gitCommandManager.CreateCommandManager(settings.repositoryPath, settings.lfs); |             return yield gitCommandManager.createCommandManager(settings.repositoryPath, settings.lfs); | ||||||
|         } |         } | ||||||
|         catch (err) { |         catch (err) { | ||||||
|             // Git is required for LFS |             // Git is required for LFS | ||||||
| @@ -5535,108 +5630,7 @@ function getGitCommandManager(settings) { | |||||||
|                 throw err; |                 throw err; | ||||||
|             } |             } | ||||||
|             // Otherwise fallback to REST API |             // Otherwise fallback to REST API | ||||||
|             return null; |             return undefined; | ||||||
|         } |  | ||||||
|     }); |  | ||||||
| } |  | ||||||
| function prepareExistingDirectory(git, repositoryPath, repositoryUrl, clean) { |  | ||||||
|     return __awaiter(this, void 0, void 0, function* () { |  | ||||||
|         let remove = false; |  | ||||||
|         // Check whether using git or REST API |  | ||||||
|         if (!git) { |  | ||||||
|             remove = true; |  | ||||||
|         } |  | ||||||
|         // Fetch URL does not match |  | ||||||
|         else if (!fsHelper.directoryExistsSync(path.join(repositoryPath, '.git')) || |  | ||||||
|             repositoryUrl !== (yield git.tryGetFetchUrl())) { |  | ||||||
|             remove = true; |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             // Delete any index.lock and shallow.lock left by a previously canceled run or crashed git process |  | ||||||
|             const lockPaths = [ |  | ||||||
|                 path.join(repositoryPath, '.git', 'index.lock'), |  | ||||||
|                 path.join(repositoryPath, '.git', 'shallow.lock') |  | ||||||
|             ]; |  | ||||||
|             for (const lockPath of lockPaths) { |  | ||||||
|                 try { |  | ||||||
|                     yield io.rmRF(lockPath); |  | ||||||
|                 } |  | ||||||
|                 catch (error) { |  | ||||||
|                     core.debug(`Unable to delete '${lockPath}'. ${error.message}`); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             try { |  | ||||||
|                 // Checkout detached HEAD |  | ||||||
|                 if (!(yield git.isDetached())) { |  | ||||||
|                     yield git.checkoutDetach(); |  | ||||||
|                 } |  | ||||||
|                 // Remove all refs/heads/* |  | ||||||
|                 let branches = yield git.branchList(false); |  | ||||||
|                 for (const branch of branches) { |  | ||||||
|                     yield git.branchDelete(false, branch); |  | ||||||
|                 } |  | ||||||
|                 // Remove all refs/remotes/origin/* to avoid conflicts |  | ||||||
|                 branches = yield git.branchList(true); |  | ||||||
|                 for (const branch of branches) { |  | ||||||
|                     yield git.branchDelete(true, branch); |  | ||||||
|                 } |  | ||||||
|                 // Clean |  | ||||||
|                 if (clean) { |  | ||||||
|                     if (!(yield git.tryClean())) { |  | ||||||
|                         core.debug(`The clean command failed. This might be caused by: 1) path too long, 2) permission issue, or 3) file in use. For futher investigation, manually run 'git clean -ffdx' on the directory '${repositoryPath}'.`); |  | ||||||
|                         remove = true; |  | ||||||
|                     } |  | ||||||
|                     else if (!(yield git.tryReset())) { |  | ||||||
|                         remove = true; |  | ||||||
|                     } |  | ||||||
|                     if (remove) { |  | ||||||
|                         core.warning(`Unable to clean or reset the repository. The repository will be recreated instead.`); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             catch (error) { |  | ||||||
|                 core.warning(`Unable to prepare the existing repository. The repository will be recreated instead.`); |  | ||||||
|                 remove = true; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         if (remove) { |  | ||||||
|             // Delete the contents of the directory. Don't delete the directory itself |  | ||||||
|             // since it might be the current working directory. |  | ||||||
|             core.info(`Deleting the contents of '${repositoryPath}'`); |  | ||||||
|             for (const file of yield fs.promises.readdir(repositoryPath)) { |  | ||||||
|                 yield io.rmRF(path.join(repositoryPath, file)); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     }); |  | ||||||
| } |  | ||||||
| function configureAuthToken(git, authToken) { |  | ||||||
|     return __awaiter(this, void 0, void 0, function* () { |  | ||||||
|         // Configure a placeholder value. This approach avoids the credential being captured |  | ||||||
|         // by process creation audit events, which are commonly logged. For more information, |  | ||||||
|         // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing |  | ||||||
|         const placeholder = `AUTHORIZATION: basic ***`; |  | ||||||
|         yield git.config(authConfigKey, placeholder); |  | ||||||
|         // Determine the basic credential value |  | ||||||
|         const basicCredential = Buffer.from(`x-access-token:${authToken}`, 'utf8').toString('base64'); |  | ||||||
|         core.setSecret(basicCredential); |  | ||||||
|         // Replace the value in the config file |  | ||||||
|         const configPath = path.join(git.getWorkingDirectory(), '.git', 'config'); |  | ||||||
|         let content = (yield fs.promises.readFile(configPath)).toString(); |  | ||||||
|         const placeholderIndex = content.indexOf(placeholder); |  | ||||||
|         if (placeholderIndex < 0 || |  | ||||||
|             placeholderIndex != content.lastIndexOf(placeholder)) { |  | ||||||
|             throw new Error('Unable to replace auth placeholder in .git/config'); |  | ||||||
|         } |  | ||||||
|         content = content.replace(placeholder, `AUTHORIZATION: basic ${basicCredential}`); |  | ||||||
|         yield fs.promises.writeFile(configPath, content); |  | ||||||
|     }); |  | ||||||
| } |  | ||||||
| function removeGitConfig(git, configKey) { |  | ||||||
|     return __awaiter(this, void 0, void 0, function* () { |  | ||||||
|         if ((yield git.configExists(configKey)) && |  | ||||||
|             !(yield git.tryConfigUnset(configKey))) { |  | ||||||
|             // Load the config contents |  | ||||||
|             core.warning(`Failed to remove '${configKey}' from the git config`); |  | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
| @@ -6874,6 +6868,108 @@ function escape(s) { | |||||||
| } | } | ||||||
| //# sourceMappingURL=command.js.map | //# sourceMappingURL=command.js.map | ||||||
|  |  | ||||||
|  | /***/ }), | ||||||
|  |  | ||||||
|  | /***/ 438: | ||||||
|  | /***/ (function(__unusedmodule, exports, __webpack_require__) { | ||||||
|  |  | ||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||||||
|  |     function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||||||
|  |     return new (P || (P = Promise))(function (resolve, reject) { | ||||||
|  |         function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||||||
|  |         function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||||||
|  |         function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||||||
|  |         step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  | var __importStar = (this && this.__importStar) || function (mod) { | ||||||
|  |     if (mod && mod.__esModule) return mod; | ||||||
|  |     var result = {}; | ||||||
|  |     if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; | ||||||
|  |     result["default"] = mod; | ||||||
|  |     return result; | ||||||
|  | }; | ||||||
|  | Object.defineProperty(exports, "__esModule", { value: true }); | ||||||
|  | const core = __importStar(__webpack_require__(470)); | ||||||
|  | const fs = __importStar(__webpack_require__(747)); | ||||||
|  | const fsHelper = __importStar(__webpack_require__(618)); | ||||||
|  | const io = __importStar(__webpack_require__(1)); | ||||||
|  | const path = __importStar(__webpack_require__(622)); | ||||||
|  | function prepareExistingDirectory(git, repositoryPath, repositoryUrl, clean) { | ||||||
|  |     return __awaiter(this, void 0, void 0, function* () { | ||||||
|  |         let remove = false; | ||||||
|  |         // Check whether using git or REST API | ||||||
|  |         if (!git) { | ||||||
|  |             remove = true; | ||||||
|  |         } | ||||||
|  |         // Fetch URL does not match | ||||||
|  |         else if (!fsHelper.directoryExistsSync(path.join(repositoryPath, '.git')) || | ||||||
|  |             repositoryUrl !== (yield git.tryGetFetchUrl())) { | ||||||
|  |             remove = true; | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             // Delete any index.lock and shallow.lock left by a previously canceled run or crashed git process | ||||||
|  |             const lockPaths = [ | ||||||
|  |                 path.join(repositoryPath, '.git', 'index.lock'), | ||||||
|  |                 path.join(repositoryPath, '.git', 'shallow.lock') | ||||||
|  |             ]; | ||||||
|  |             for (const lockPath of lockPaths) { | ||||||
|  |                 try { | ||||||
|  |                     yield io.rmRF(lockPath); | ||||||
|  |                 } | ||||||
|  |                 catch (error) { | ||||||
|  |                     core.debug(`Unable to delete '${lockPath}'. ${error.message}`); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             try { | ||||||
|  |                 // Checkout detached HEAD | ||||||
|  |                 if (!(yield git.isDetached())) { | ||||||
|  |                     yield git.checkoutDetach(); | ||||||
|  |                 } | ||||||
|  |                 // Remove all refs/heads/* | ||||||
|  |                 let branches = yield git.branchList(false); | ||||||
|  |                 for (const branch of branches) { | ||||||
|  |                     yield git.branchDelete(false, branch); | ||||||
|  |                 } | ||||||
|  |                 // Remove all refs/remotes/origin/* to avoid conflicts | ||||||
|  |                 branches = yield git.branchList(true); | ||||||
|  |                 for (const branch of branches) { | ||||||
|  |                     yield git.branchDelete(true, branch); | ||||||
|  |                 } | ||||||
|  |                 // Clean | ||||||
|  |                 if (clean) { | ||||||
|  |                     if (!(yield git.tryClean())) { | ||||||
|  |                         core.debug(`The clean command failed. This might be caused by: 1) path too long, 2) permission issue, or 3) file in use. For futher investigation, manually run 'git clean -ffdx' on the directory '${repositoryPath}'.`); | ||||||
|  |                         remove = true; | ||||||
|  |                     } | ||||||
|  |                     else if (!(yield git.tryReset())) { | ||||||
|  |                         remove = true; | ||||||
|  |                     } | ||||||
|  |                     if (remove) { | ||||||
|  |                         core.warning(`Unable to clean or reset the repository. The repository will be recreated instead.`); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             catch (error) { | ||||||
|  |                 core.warning(`Unable to prepare the existing repository. The repository will be recreated instead.`); | ||||||
|  |                 remove = true; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if (remove) { | ||||||
|  |             // Delete the contents of the directory. Don't delete the directory itself | ||||||
|  |             // since it might be the current working directory. | ||||||
|  |             core.info(`Deleting the contents of '${repositoryPath}'`); | ||||||
|  |             for (const file of yield fs.promises.readdir(repositoryPath)) { | ||||||
|  |                 yield io.rmRF(path.join(repositoryPath, file)); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | exports.prepareExistingDirectory = prepareExistingDirectory; | ||||||
|  |  | ||||||
|  |  | ||||||
| /***/ }), | /***/ }), | ||||||
|  |  | ||||||
| /***/ 453: | /***/ 453: | ||||||
|   | |||||||
| @@ -4,14 +4,11 @@ | |||||||
|   "description": "checkout action", |   "description": "checkout action", | ||||||
|   "main": "lib/main.js", |   "main": "lib/main.js", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "build": "tsc", |     "build": "tsc && ncc build && node lib/misc/generate-docs.js", | ||||||
|     "format": "prettier --write **/*.ts", |     "format": "prettier --write **/*.ts", | ||||||
|     "format-check": "prettier --check **/*.ts", |     "format-check": "prettier --check **/*.ts", | ||||||
|     "lint": "eslint src/**/*.ts", |     "lint": "eslint src/**/*.ts", | ||||||
|     "pack": "ncc build", |     "test": "jest" | ||||||
|     "gendocs": "node lib/misc/generate-docs.js", |  | ||||||
|     "test": "jest", |  | ||||||
|     "all": "npm run build && npm run format && npm run lint && npm run pack && npm run gendocs && npm test" |  | ||||||
|   }, |   }, | ||||||
|   "repository": { |   "repository": { | ||||||
|     "type": "git", |     "type": "git", | ||||||
|   | |||||||
							
								
								
									
										102
									
								
								src/git-auth-helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								src/git-auth-helper.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | |||||||
|  | import * as assert from 'assert' | ||||||
|  | import * as core from '@actions/core' | ||||||
|  | import * as exec from '@actions/exec' | ||||||
|  | import * as fs from 'fs' | ||||||
|  | import * as io from '@actions/io' | ||||||
|  | import * as os from 'os' | ||||||
|  | import * as path from 'path' | ||||||
|  | import * as stateHelper from './state-helper' | ||||||
|  | import {default as uuid} from 'uuid/v4' | ||||||
|  | import {IGitCommandManager} from './git-command-manager' | ||||||
|  | import {IGitSourceSettings} from './git-source-settings' | ||||||
|  |  | ||||||
|  | const IS_WINDOWS = process.platform === 'win32' | ||||||
|  | const HOSTNAME = 'github.com' | ||||||
|  | const EXTRA_HEADER_KEY = `http.https://${HOSTNAME}/.extraheader` | ||||||
|  |  | ||||||
|  | export interface IGitAuthHelper { | ||||||
|  |   configureAuth(): Promise<void> | ||||||
|  |   removeAuth(): Promise<void> | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function createAuthHelper( | ||||||
|  |   git: IGitCommandManager, | ||||||
|  |   settings?: IGitSourceSettings | ||||||
|  | ): IGitAuthHelper { | ||||||
|  |   return new GitAuthHelper(git, settings) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class GitAuthHelper { | ||||||
|  |   private git: IGitCommandManager | ||||||
|  |   private settings: IGitSourceSettings | ||||||
|  |  | ||||||
|  |   constructor( | ||||||
|  |     gitCommandManager: IGitCommandManager, | ||||||
|  |     gitSourceSettings?: IGitSourceSettings | ||||||
|  |   ) { | ||||||
|  |     this.git = gitCommandManager | ||||||
|  |     this.settings = gitSourceSettings || (({} as unknown) as IGitSourceSettings) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async configureAuth(): Promise<void> { | ||||||
|  |     // Remove possible previous values | ||||||
|  |     await this.removeAuth() | ||||||
|  |  | ||||||
|  |     // Configure new values | ||||||
|  |     await this.configureToken() | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async removeAuth(): Promise<void> { | ||||||
|  |     await this.removeToken() | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async configureToken(): Promise<void> { | ||||||
|  |     // Configure a placeholder value. This approach avoids the credential being captured | ||||||
|  |     // by process creation audit events, which are commonly logged. For more information, | ||||||
|  |     // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing | ||||||
|  |     const placeholder = `AUTHORIZATION: basic ***` | ||||||
|  |     await this.git.config(EXTRA_HEADER_KEY, placeholder) | ||||||
|  |  | ||||||
|  |     // Determine the basic credential value | ||||||
|  |     const basicCredential = Buffer.from( | ||||||
|  |       `x-access-token:${this.settings.authToken}`, | ||||||
|  |       'utf8' | ||||||
|  |     ).toString('base64') | ||||||
|  |     core.setSecret(basicCredential) | ||||||
|  |  | ||||||
|  |     // Replace the value in the config file | ||||||
|  |     const configPath = path.join( | ||||||
|  |       this.git.getWorkingDirectory(), | ||||||
|  |       '.git', | ||||||
|  |       'config' | ||||||
|  |     ) | ||||||
|  |     let content = (await fs.promises.readFile(configPath)).toString() | ||||||
|  |     const placeholderIndex = content.indexOf(placeholder) | ||||||
|  |     if ( | ||||||
|  |       placeholderIndex < 0 || | ||||||
|  |       placeholderIndex != content.lastIndexOf(placeholder) | ||||||
|  |     ) { | ||||||
|  |       throw new Error('Unable to replace auth placeholder in .git/config') | ||||||
|  |     } | ||||||
|  |     content = content.replace( | ||||||
|  |       placeholder, | ||||||
|  |       `AUTHORIZATION: basic ${basicCredential}` | ||||||
|  |     ) | ||||||
|  |     await fs.promises.writeFile(configPath, content) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async removeToken(): Promise<void> { | ||||||
|  |     // HTTP extra header | ||||||
|  |     await this.removeGitConfig(EXTRA_HEADER_KEY) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async removeGitConfig(configKey: string): Promise<void> { | ||||||
|  |     if ( | ||||||
|  |       (await this.git.configExists(configKey)) && | ||||||
|  |       !(await this.git.tryConfigUnset(configKey)) | ||||||
|  |     ) { | ||||||
|  |       // Load the config contents | ||||||
|  |       core.warning(`Failed to remove '${configKey}' from the git config`) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -26,6 +26,7 @@ export interface IGitCommandManager { | |||||||
|   lfsInstall(): Promise<void> |   lfsInstall(): Promise<void> | ||||||
|   log1(): Promise<void> |   log1(): Promise<void> | ||||||
|   remoteAdd(remoteName: string, remoteUrl: string): Promise<void> |   remoteAdd(remoteName: string, remoteUrl: string): Promise<void> | ||||||
|  |   setEnvironmentVariable(name: string, value: string): void | ||||||
|   tagExists(pattern: string): Promise<boolean> |   tagExists(pattern: string): Promise<boolean> | ||||||
|   tryClean(): Promise<boolean> |   tryClean(): Promise<boolean> | ||||||
|   tryConfigUnset(configKey: string): Promise<boolean> |   tryConfigUnset(configKey: string): Promise<boolean> | ||||||
| @@ -34,7 +35,7 @@ export interface IGitCommandManager { | |||||||
|   tryReset(): Promise<boolean> |   tryReset(): Promise<boolean> | ||||||
| } | } | ||||||
|  |  | ||||||
| export async function CreateCommandManager( | export async function createCommandManager( | ||||||
|   workingDirectory: string, |   workingDirectory: string, | ||||||
|   lfs: boolean |   lfs: boolean | ||||||
| ): Promise<IGitCommandManager> { | ): Promise<IGitCommandManager> { | ||||||
| @@ -207,6 +208,10 @@ class GitCommandManager { | |||||||
|     await this.execGit(['remote', 'add', remoteName, remoteUrl]) |     await this.execGit(['remote', 'add', remoteName, remoteUrl]) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   setEnvironmentVariable(name: string, value: string): void { | ||||||
|  |     this.gitEnv[name] = value | ||||||
|  |   } | ||||||
|  |  | ||||||
|   async tagExists(pattern: string): Promise<boolean> { |   async tagExists(pattern: string): Promise<boolean> { | ||||||
|     const output = await this.execGit(['tag', '--list', pattern]) |     const output = await this.execGit(['tag', '--list', pattern]) | ||||||
|     return !!output.stdout.trim() |     return !!output.stdout.trim() | ||||||
|   | |||||||
							
								
								
									
										91
									
								
								src/git-directory-helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								src/git-directory-helper.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,91 @@ | |||||||
|  | import * as core from '@actions/core' | ||||||
|  | import * as fs from 'fs' | ||||||
|  | import * as fsHelper from './fs-helper' | ||||||
|  | import * as io from '@actions/io' | ||||||
|  | import * as path from 'path' | ||||||
|  | import {IGitCommandManager} from './git-command-manager' | ||||||
|  |  | ||||||
|  | export async function prepareExistingDirectory( | ||||||
|  |   git: IGitCommandManager | undefined, | ||||||
|  |   repositoryPath: string, | ||||||
|  |   repositoryUrl: string, | ||||||
|  |   clean: boolean | ||||||
|  | ): Promise<void> { | ||||||
|  |   let remove = false | ||||||
|  |  | ||||||
|  |   // Check whether using git or REST API | ||||||
|  |   if (!git) { | ||||||
|  |     remove = true | ||||||
|  |   } | ||||||
|  |   // Fetch URL does not match | ||||||
|  |   else if ( | ||||||
|  |     !fsHelper.directoryExistsSync(path.join(repositoryPath, '.git')) || | ||||||
|  |     repositoryUrl !== (await git.tryGetFetchUrl()) | ||||||
|  |   ) { | ||||||
|  |     remove = true | ||||||
|  |   } else { | ||||||
|  |     // Delete any index.lock and shallow.lock left by a previously canceled run or crashed git process | ||||||
|  |     const lockPaths = [ | ||||||
|  |       path.join(repositoryPath, '.git', 'index.lock'), | ||||||
|  |       path.join(repositoryPath, '.git', 'shallow.lock') | ||||||
|  |     ] | ||||||
|  |     for (const lockPath of lockPaths) { | ||||||
|  |       try { | ||||||
|  |         await io.rmRF(lockPath) | ||||||
|  |       } catch (error) { | ||||||
|  |         core.debug(`Unable to delete '${lockPath}'. ${error.message}`) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       // Checkout detached HEAD | ||||||
|  |       if (!(await git.isDetached())) { | ||||||
|  |         await git.checkoutDetach() | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Remove all refs/heads/* | ||||||
|  |       let branches = await git.branchList(false) | ||||||
|  |       for (const branch of branches) { | ||||||
|  |         await git.branchDelete(false, branch) | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Remove all refs/remotes/origin/* to avoid conflicts | ||||||
|  |       branches = await git.branchList(true) | ||||||
|  |       for (const branch of branches) { | ||||||
|  |         await git.branchDelete(true, branch) | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Clean | ||||||
|  |       if (clean) { | ||||||
|  |         if (!(await git.tryClean())) { | ||||||
|  |           core.debug( | ||||||
|  |             `The clean command failed. This might be caused by: 1) path too long, 2) permission issue, or 3) file in use. For futher investigation, manually run 'git clean -ffdx' on the directory '${repositoryPath}'.` | ||||||
|  |           ) | ||||||
|  |           remove = true | ||||||
|  |         } else if (!(await git.tryReset())) { | ||||||
|  |           remove = true | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (remove) { | ||||||
|  |           core.warning( | ||||||
|  |             `Unable to clean or reset the repository. The repository will be recreated instead.` | ||||||
|  |           ) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } catch (error) { | ||||||
|  |       core.warning( | ||||||
|  |         `Unable to prepare the existing repository. The repository will be recreated instead.` | ||||||
|  |       ) | ||||||
|  |       remove = true | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (remove) { | ||||||
|  |     // Delete the contents of the directory. Don't delete the directory itself | ||||||
|  |     // since it might be the current working directory. | ||||||
|  |     core.info(`Deleting the contents of '${repositoryPath}'`) | ||||||
|  |     for (const file of await fs.promises.readdir(repositoryPath)) { | ||||||
|  |       await io.rmRF(path.join(repositoryPath, file)) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,36 +1,24 @@ | |||||||
| import * as core from '@actions/core' | import * as core from '@actions/core' | ||||||
| import * as fs from 'fs' |  | ||||||
| import * as fsHelper from './fs-helper' | import * as fsHelper from './fs-helper' | ||||||
|  | import * as gitAuthHelper from './git-auth-helper' | ||||||
| import * as gitCommandManager from './git-command-manager' | import * as gitCommandManager from './git-command-manager' | ||||||
|  | import * as gitDirectoryHelper from './git-directory-helper' | ||||||
| import * as githubApiHelper from './github-api-helper' | import * as githubApiHelper from './github-api-helper' | ||||||
| import * as io from '@actions/io' | import * as io from '@actions/io' | ||||||
| import * as path from 'path' | import * as path from 'path' | ||||||
| import * as refHelper from './ref-helper' | import * as refHelper from './ref-helper' | ||||||
| import * as stateHelper from './state-helper' | import * as stateHelper from './state-helper' | ||||||
| import {IGitCommandManager} from './git-command-manager' | import {IGitCommandManager} from './git-command-manager' | ||||||
|  | import {IGitSourceSettings} from './git-source-settings' | ||||||
|  |  | ||||||
| const serverUrl = 'https://github.com/' | const hostname = 'github.com' | ||||||
| const authConfigKey = `http.${serverUrl}.extraheader` |  | ||||||
|  |  | ||||||
| export interface ISourceSettings { | export async function getSource(settings: IGitSourceSettings): Promise<void> { | ||||||
|   repositoryPath: string |  | ||||||
|   repositoryOwner: string |  | ||||||
|   repositoryName: string |  | ||||||
|   ref: string |  | ||||||
|   commit: string |  | ||||||
|   clean: boolean |  | ||||||
|   fetchDepth: number |  | ||||||
|   lfs: boolean |  | ||||||
|   authToken: string |  | ||||||
|   persistCredentials: boolean |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export async function getSource(settings: ISourceSettings): Promise<void> { |  | ||||||
|   // Repository URL |   // Repository URL | ||||||
|   core.info( |   core.info( | ||||||
|     `Syncing repository: ${settings.repositoryOwner}/${settings.repositoryName}` |     `Syncing repository: ${settings.repositoryOwner}/${settings.repositoryName}` | ||||||
|   ) |   ) | ||||||
|   const repositoryUrl = `https://github.com/${encodeURIComponent( |   const repositoryUrl = `https://${hostname}/${encodeURIComponent( | ||||||
|     settings.repositoryOwner |     settings.repositoryOwner | ||||||
|   )}/${encodeURIComponent(settings.repositoryName)}` |   )}/${encodeURIComponent(settings.repositoryName)}` | ||||||
|  |  | ||||||
| @@ -51,7 +39,7 @@ export async function getSource(settings: ISourceSettings): Promise<void> { | |||||||
|  |  | ||||||
|   // Prepare existing directory, otherwise recreate |   // Prepare existing directory, otherwise recreate | ||||||
|   if (isExisting) { |   if (isExisting) { | ||||||
|     await prepareExistingDirectory( |     await gitDirectoryHelper.prepareExistingDirectory( | ||||||
|       git, |       git, | ||||||
|       settings.repositoryPath, |       settings.repositoryPath, | ||||||
|       repositoryUrl, |       repositoryUrl, | ||||||
| @@ -92,12 +80,10 @@ export async function getSource(settings: ISourceSettings): Promise<void> { | |||||||
|       ) |       ) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Remove possible previous extraheader |     const authHelper = gitAuthHelper.createAuthHelper(git, settings) | ||||||
|     await removeGitConfig(git, authConfigKey) |  | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       // Config extraheader |       // Configure auth | ||||||
|       await configureAuthToken(git, settings.authToken) |       await authHelper.configureAuth() | ||||||
|  |  | ||||||
|       // LFS install |       // LFS install | ||||||
|       if (settings.lfs) { |       if (settings.lfs) { | ||||||
| @@ -128,8 +114,9 @@ export async function getSource(settings: ISourceSettings): Promise<void> { | |||||||
|       // Dump some info about the checked out commit |       // Dump some info about the checked out commit | ||||||
|       await git.log1() |       await git.log1() | ||||||
|     } finally { |     } finally { | ||||||
|  |       // Remove auth | ||||||
|       if (!settings.persistCredentials) { |       if (!settings.persistCredentials) { | ||||||
|         await removeGitConfig(git, authConfigKey) |         await authHelper.removeAuth() | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -146,22 +133,22 @@ export async function cleanup(repositoryPath: string): Promise<void> { | |||||||
|  |  | ||||||
|   let git: IGitCommandManager |   let git: IGitCommandManager | ||||||
|   try { |   try { | ||||||
|     git = await gitCommandManager.CreateCommandManager(repositoryPath, false) |     git = await gitCommandManager.createCommandManager(repositoryPath, false) | ||||||
|   } catch { |   } catch { | ||||||
|     return |     return | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Remove extraheader |   // Remove auth | ||||||
|   await removeGitConfig(git, authConfigKey) |   const authHelper = gitAuthHelper.createAuthHelper(git) | ||||||
|  |   await authHelper.removeAuth() | ||||||
| } | } | ||||||
|  |  | ||||||
| async function getGitCommandManager( | async function getGitCommandManager( | ||||||
|   settings: ISourceSettings |   settings: IGitSourceSettings | ||||||
| ): Promise<IGitCommandManager> { | ): Promise<IGitCommandManager | undefined> { | ||||||
|   core.info(`Working directory is '${settings.repositoryPath}'`) |   core.info(`Working directory is '${settings.repositoryPath}'`) | ||||||
|   let git = (null as unknown) as IGitCommandManager |  | ||||||
|   try { |   try { | ||||||
|     return await gitCommandManager.CreateCommandManager( |     return await gitCommandManager.createCommandManager( | ||||||
|       settings.repositoryPath, |       settings.repositoryPath, | ||||||
|       settings.lfs |       settings.lfs | ||||||
|     ) |     ) | ||||||
| @@ -172,138 +159,6 @@ async function getGitCommandManager( | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Otherwise fallback to REST API |     // Otherwise fallback to REST API | ||||||
|     return (null as unknown) as IGitCommandManager |     return undefined | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| async function prepareExistingDirectory( |  | ||||||
|   git: IGitCommandManager, |  | ||||||
|   repositoryPath: string, |  | ||||||
|   repositoryUrl: string, |  | ||||||
|   clean: boolean |  | ||||||
| ): Promise<void> { |  | ||||||
|   let remove = false |  | ||||||
|  |  | ||||||
|   // Check whether using git or REST API |  | ||||||
|   if (!git) { |  | ||||||
|     remove = true |  | ||||||
|   } |  | ||||||
|   // Fetch URL does not match |  | ||||||
|   else if ( |  | ||||||
|     !fsHelper.directoryExistsSync(path.join(repositoryPath, '.git')) || |  | ||||||
|     repositoryUrl !== (await git.tryGetFetchUrl()) |  | ||||||
|   ) { |  | ||||||
|     remove = true |  | ||||||
|   } else { |  | ||||||
|     // Delete any index.lock and shallow.lock left by a previously canceled run or crashed git process |  | ||||||
|     const lockPaths = [ |  | ||||||
|       path.join(repositoryPath, '.git', 'index.lock'), |  | ||||||
|       path.join(repositoryPath, '.git', 'shallow.lock') |  | ||||||
|     ] |  | ||||||
|     for (const lockPath of lockPaths) { |  | ||||||
|       try { |  | ||||||
|         await io.rmRF(lockPath) |  | ||||||
|       } catch (error) { |  | ||||||
|         core.debug(`Unable to delete '${lockPath}'. ${error.message}`) |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     try { |  | ||||||
|       // Checkout detached HEAD |  | ||||||
|       if (!(await git.isDetached())) { |  | ||||||
|         await git.checkoutDetach() |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       // Remove all refs/heads/* |  | ||||||
|       let branches = await git.branchList(false) |  | ||||||
|       for (const branch of branches) { |  | ||||||
|         await git.branchDelete(false, branch) |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       // Remove all refs/remotes/origin/* to avoid conflicts |  | ||||||
|       branches = await git.branchList(true) |  | ||||||
|       for (const branch of branches) { |  | ||||||
|         await git.branchDelete(true, branch) |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       // Clean |  | ||||||
|       if (clean) { |  | ||||||
|         if (!(await git.tryClean())) { |  | ||||||
|           core.debug( |  | ||||||
|             `The clean command failed. This might be caused by: 1) path too long, 2) permission issue, or 3) file in use. For futher investigation, manually run 'git clean -ffdx' on the directory '${repositoryPath}'.` |  | ||||||
|           ) |  | ||||||
|           remove = true |  | ||||||
|         } else if (!(await git.tryReset())) { |  | ||||||
|           remove = true |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (remove) { |  | ||||||
|           core.warning( |  | ||||||
|             `Unable to clean or reset the repository. The repository will be recreated instead.` |  | ||||||
|           ) |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } catch (error) { |  | ||||||
|       core.warning( |  | ||||||
|         `Unable to prepare the existing repository. The repository will be recreated instead.` |  | ||||||
|       ) |  | ||||||
|       remove = true |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   if (remove) { |  | ||||||
|     // Delete the contents of the directory. Don't delete the directory itself |  | ||||||
|     // since it might be the current working directory. |  | ||||||
|     core.info(`Deleting the contents of '${repositoryPath}'`) |  | ||||||
|     for (const file of await fs.promises.readdir(repositoryPath)) { |  | ||||||
|       await io.rmRF(path.join(repositoryPath, file)) |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| async function configureAuthToken( |  | ||||||
|   git: IGitCommandManager, |  | ||||||
|   authToken: string |  | ||||||
| ): Promise<void> { |  | ||||||
|   // Configure a placeholder value. This approach avoids the credential being captured |  | ||||||
|   // by process creation audit events, which are commonly logged. For more information, |  | ||||||
|   // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing |  | ||||||
|   const placeholder = `AUTHORIZATION: basic ***` |  | ||||||
|   await git.config(authConfigKey, placeholder) |  | ||||||
|  |  | ||||||
|   // Determine the basic credential value |  | ||||||
|   const basicCredential = Buffer.from( |  | ||||||
|     `x-access-token:${authToken}`, |  | ||||||
|     'utf8' |  | ||||||
|   ).toString('base64') |  | ||||||
|   core.setSecret(basicCredential) |  | ||||||
|  |  | ||||||
|   // Replace the value in the config file |  | ||||||
|   const configPath = path.join(git.getWorkingDirectory(), '.git', 'config') |  | ||||||
|   let content = (await fs.promises.readFile(configPath)).toString() |  | ||||||
|   const placeholderIndex = content.indexOf(placeholder) |  | ||||||
|   if ( |  | ||||||
|     placeholderIndex < 0 || |  | ||||||
|     placeholderIndex != content.lastIndexOf(placeholder) |  | ||||||
|   ) { |  | ||||||
|     throw new Error('Unable to replace auth placeholder in .git/config') |  | ||||||
|   } |  | ||||||
|   content = content.replace( |  | ||||||
|     placeholder, |  | ||||||
|     `AUTHORIZATION: basic ${basicCredential}` |  | ||||||
|   ) |  | ||||||
|   await fs.promises.writeFile(configPath, content) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| async function removeGitConfig( |  | ||||||
|   git: IGitCommandManager, |  | ||||||
|   configKey: string |  | ||||||
| ): Promise<void> { |  | ||||||
|   if ( |  | ||||||
|     (await git.configExists(configKey)) && |  | ||||||
|     !(await git.tryConfigUnset(configKey)) |  | ||||||
|   ) { |  | ||||||
|     // Load the config contents |  | ||||||
|     core.warning(`Failed to remove '${configKey}' from the git config`) |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								src/git-source-settings.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/git-source-settings.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | export interface IGitSourceSettings { | ||||||
|  |   repositoryPath: string | ||||||
|  |   repositoryOwner: string | ||||||
|  |   repositoryName: string | ||||||
|  |   ref: string | ||||||
|  |   commit: string | ||||||
|  |   clean: boolean | ||||||
|  |   fetchDepth: number | ||||||
|  |   lfs: boolean | ||||||
|  |   authToken: string | ||||||
|  |   persistCredentials: boolean | ||||||
|  | } | ||||||
| @@ -2,10 +2,10 @@ import * as core from '@actions/core' | |||||||
| import * as fsHelper from './fs-helper' | import * as fsHelper from './fs-helper' | ||||||
| import * as github from '@actions/github' | import * as github from '@actions/github' | ||||||
| import * as path from 'path' | import * as path from 'path' | ||||||
| import {ISourceSettings} from './git-source-provider' | import {IGitSourceSettings} from './git-source-settings' | ||||||
|  |  | ||||||
| export function getInputs(): ISourceSettings { | export function getInputs(): IGitSourceSettings { | ||||||
|   const result = ({} as unknown) as ISourceSettings |   const result = ({} as unknown) as IGitSourceSettings | ||||||
|  |  | ||||||
|   // GitHub workspace |   // GitHub workspace | ||||||
|   let githubWorkspacePath = process.env['GITHUB_WORKSPACE'] |   let githubWorkspacePath = process.env['GITHUB_WORKSPACE'] | ||||||
|   | |||||||
| @@ -1,4 +1,3 @@ | |||||||
| import * as core from '@actions/core' |  | ||||||
| import * as coreCommand from '@actions/core/lib/command' | import * as coreCommand from '@actions/core/lib/command' | ||||||
|  |  | ||||||
| /** | /** | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 eric sciple
					eric sciple