From f2190623701cfebaaf26554b39e1e29cc4a1f2fb Mon Sep 17 00:00:00 2001 From: eric sciple Date: Mon, 2 Mar 2020 11:33:30 -0500 Subject: [PATCH] more unit tests and corresponding refactoring (#174) --- .github/workflows/test.yml | 2 - .gitignore | 1 + __test__/git-auth-helper.test.ts | 200 ++++++++++++++ __test__/git-directory-helper.test.ts | 382 ++++++++++++++++++++++++++ __test__/input-helper.test.ts | 24 +- dist/index.js | 334 ++++++++++++++-------- package.json | 7 +- src/git-auth-helper.ts | 102 +++++++ src/git-command-manager.ts | 7 +- src/git-directory-helper.ts | 91 ++++++ src/git-source-provider.ts | 185 ++----------- src/git-source-settings.ts | 12 + src/input-helper.ts | 6 +- src/state-helper.ts | 1 - 14 files changed, 1049 insertions(+), 305 deletions(-) create mode 100644 __test__/git-auth-helper.test.ts create mode 100644 __test__/git-directory-helper.test.ts create mode 100644 src/git-auth-helper.ts create mode 100644 src/git-directory-helper.ts create mode 100644 src/git-source-settings.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c5856f6..8c0e759 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,8 +19,6 @@ jobs: - run: npm run build - run: npm run format-check - run: npm run lint - - run: npm run pack - - run: npm run gendocs - run: npm test - name: Verify no unstaged changes run: __test__/verify-no-unstaged-changes.sh diff --git a/.gitignore b/.gitignore index 46f1072..2f909c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ +__test__/_temp lib/ node_modules/ \ No newline at end of file diff --git a/__test__/git-auth-helper.test.ts b/__test__/git-auth-helper.test.ts new file mode 100644 index 0000000..fab9f37 --- /dev/null +++ b/__test__/git-auth-helper.test.ts @@ -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 + 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 { + 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 => { + 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 => { + 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: '' + } +} diff --git a/__test__/git-directory-helper.test.ts b/__test__/git-directory-helper.test.ts new file mode 100644 index 0000000..4383e1d --- /dev/null +++ b/__test__/git-directory-helper.test.ts @@ -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 + 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 + 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 + 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 + 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 + 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 { + 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 + }) + } +} diff --git a/__test__/input-helper.test.ts b/__test__/input-helper.test.ts index be4b166..53c7000 100644 --- a/__test__/input-helper.test.ts +++ b/__test__/input-helper.test.ts @@ -4,7 +4,7 @@ import * as fsHelper from '../lib/fs-helper' import * as github from '@actions/github' import * as inputHelper from '../lib/input-helper' 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 gitHubWorkspace = path.resolve('/checkout-tests/workspace') @@ -17,12 +17,18 @@ let originalContext = {...github.context} describe('input-helper tests', () => { beforeAll(() => { - // Mock @actions/core getInput() + // Mock getInput jest.spyOn(core, 'getInput').mockImplementation((name: string) => { 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(() => { return { owner: 'some-owner', @@ -62,7 +68,7 @@ describe('input-helper tests', () => { }) it('sets defaults', () => { - const settings: ISourceSettings = inputHelper.getInputs() + const settings: IGitSourceSettings = inputHelper.getInputs() expect(settings).toBeTruthy() expect(settings.authToken).toBeFalsy() expect(settings.clean).toBe(true) @@ -80,7 +86,7 @@ describe('input-helper tests', () => { let originalRef = github.context.ref try { github.context.ref = 'some-unqualified-ref' - const settings: ISourceSettings = inputHelper.getInputs() + const settings: IGitSourceSettings = inputHelper.getInputs() expect(settings).toBeTruthy() expect(settings.commit).toBe('1234567890123456789012345678901234567890') expect(settings.ref).toBe('refs/heads/some-unqualified-ref') @@ -98,7 +104,7 @@ describe('input-helper tests', () => { it('roots path', () => { inputs.path = 'some-directory/some-subdirectory' - const settings: ISourceSettings = inputHelper.getInputs() + const settings: IGitSourceSettings = inputHelper.getInputs() expect(settings.repositoryPath).toBe( path.join(gitHubWorkspace, 'some-directory', 'some-subdirectory') ) @@ -106,21 +112,21 @@ describe('input-helper tests', () => { it('sets correct default ref/sha for 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.commit).toBeFalsy() }) it('sets ref to empty when explicit sha', () => { inputs.ref = '1111111111222222222233333333334444444444' - const settings: ISourceSettings = inputHelper.getInputs() + const settings: IGitSourceSettings = inputHelper.getInputs() expect(settings.ref).toBeFalsy() expect(settings.commit).toBe('1111111111222222222233333333334444444444') }) it('sets sha to empty when explicit 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.commit).toBeFalsy() }) diff --git a/dist/index.js b/dist/index.js index 36e9e00..4b06982 100644 --- a/dist/index.js +++ b/dist/index.js @@ -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: @@ -5085,12 +5177,12 @@ const git_version_1 = __webpack_require__(559); // Auth header not supported before 2.9 // Wire protocol v2 not supported before 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 yield GitCommandManager.createCommandManager(workingDirectory, lfs); }); } -exports.CreateCommandManager = CreateCommandManager; +exports.createCommandManager = createCommandManager; class GitCommandManager { // Private constructor; use createCommandManager() constructor() { @@ -5251,6 +5343,9 @@ class GitCommandManager { yield this.execGit(['remote', 'add', remoteName, remoteUrl]); }); } + setEnvironmentVariable(name, value) { + this.gitEnv[name] = value; + } tagExists(pattern) { return __awaiter(this, void 0, void 0, function* () { 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 }); const core = __importStar(__webpack_require__(470)); -const fs = __importStar(__webpack_require__(747)); const fsHelper = __importStar(__webpack_require__(618)); +const gitAuthHelper = __importStar(__webpack_require__(287)); const gitCommandManager = __importStar(__webpack_require__(289)); +const gitDirectoryHelper = __importStar(__webpack_require__(438)); const githubApiHelper = __importStar(__webpack_require__(464)); const io = __importStar(__webpack_require__(1)); const path = __importStar(__webpack_require__(622)); const refHelper = __importStar(__webpack_require__(227)); const stateHelper = __importStar(__webpack_require__(153)); -const serverUrl = 'https://github.com/'; -const authConfigKey = `http.${serverUrl}.extraheader`; +const hostname = 'github.com'; function getSource(settings) { return __awaiter(this, void 0, void 0, function* () { // Repository URL 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 if (fsHelper.fileExistsSync(settings.repositoryPath)) { yield io.rmRF(settings.repositoryPath); @@ -5449,7 +5544,7 @@ function getSource(settings) { const git = yield getGitCommandManager(settings); // Prepare existing directory, otherwise recreate if (isExisting) { - yield prepareExistingDirectory(git, settings.repositoryPath, repositoryUrl, settings.clean); + yield gitDirectoryHelper.prepareExistingDirectory(git, settings.repositoryPath, repositoryUrl, settings.clean); } if (!git) { // Downloading using REST API @@ -5469,11 +5564,10 @@ function getSource(settings) { 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.`); } - // Remove possible previous extraheader - yield removeGitConfig(git, authConfigKey); + const authHelper = gitAuthHelper.createAuthHelper(git, settings); try { - // Config extraheader - yield configureAuthToken(git, settings.authToken); + // Configure auth + yield authHelper.configureAuth(); // LFS install if (settings.lfs) { yield git.lfsInstall(); @@ -5495,8 +5589,9 @@ function getSource(settings) { yield git.log1(); } finally { + // Remove auth if (!settings.persistCredentials) { - yield removeGitConfig(git, authConfigKey); + yield authHelper.removeAuth(); } } } @@ -5512,22 +5607,22 @@ function cleanup(repositoryPath) { } let git; try { - git = yield gitCommandManager.CreateCommandManager(repositoryPath, false); + git = yield gitCommandManager.createCommandManager(repositoryPath, false); } catch (_a) { return; } - // Remove extraheader - yield removeGitConfig(git, authConfigKey); + // Remove auth + const authHelper = gitAuthHelper.createAuthHelper(git); + yield authHelper.removeAuth(); }); } exports.cleanup = cleanup; function getGitCommandManager(settings) { return __awaiter(this, void 0, void 0, function* () { core.info(`Working directory is '${settings.repositoryPath}'`); - let git = null; try { - return yield gitCommandManager.CreateCommandManager(settings.repositoryPath, settings.lfs); + return yield gitCommandManager.createCommandManager(settings.repositoryPath, settings.lfs); } catch (err) { // Git is required for LFS @@ -5535,108 +5630,7 @@ function getGitCommandManager(settings) { throw err; } // Otherwise fallback to REST API - return null; - } - }); -} -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`); + return undefined; } }); } @@ -6874,6 +6868,108 @@ function escape(s) { } //# 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: diff --git a/package.json b/package.json index 226b979..c84ef2b 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,11 @@ "description": "checkout action", "main": "lib/main.js", "scripts": { - "build": "tsc", + "build": "tsc && ncc build && node lib/misc/generate-docs.js", "format": "prettier --write **/*.ts", "format-check": "prettier --check **/*.ts", "lint": "eslint src/**/*.ts", - "pack": "ncc build", - "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" + "test": "jest" }, "repository": { "type": "git", diff --git a/src/git-auth-helper.ts b/src/git-auth-helper.ts new file mode 100644 index 0000000..7f7f11f --- /dev/null +++ b/src/git-auth-helper.ts @@ -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 + removeAuth(): Promise +} + +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 { + // Remove possible previous values + await this.removeAuth() + + // Configure new values + await this.configureToken() + } + + async removeAuth(): Promise { + await this.removeToken() + } + + private async configureToken(): Promise { + // 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 { + // HTTP extra header + await this.removeGitConfig(EXTRA_HEADER_KEY) + } + + private async removeGitConfig(configKey: string): Promise { + if ( + (await this.git.configExists(configKey)) && + !(await this.git.tryConfigUnset(configKey)) + ) { + // Load the config contents + core.warning(`Failed to remove '${configKey}' from the git config`) + } + } +} diff --git a/src/git-command-manager.ts b/src/git-command-manager.ts index 74489c2..2b0d054 100644 --- a/src/git-command-manager.ts +++ b/src/git-command-manager.ts @@ -26,6 +26,7 @@ export interface IGitCommandManager { lfsInstall(): Promise log1(): Promise remoteAdd(remoteName: string, remoteUrl: string): Promise + setEnvironmentVariable(name: string, value: string): void tagExists(pattern: string): Promise tryClean(): Promise tryConfigUnset(configKey: string): Promise @@ -34,7 +35,7 @@ export interface IGitCommandManager { tryReset(): Promise } -export async function CreateCommandManager( +export async function createCommandManager( workingDirectory: string, lfs: boolean ): Promise { @@ -207,6 +208,10 @@ class GitCommandManager { await this.execGit(['remote', 'add', remoteName, remoteUrl]) } + setEnvironmentVariable(name: string, value: string): void { + this.gitEnv[name] = value + } + async tagExists(pattern: string): Promise { const output = await this.execGit(['tag', '--list', pattern]) return !!output.stdout.trim() diff --git a/src/git-directory-helper.ts b/src/git-directory-helper.ts new file mode 100644 index 0000000..bfaa4a9 --- /dev/null +++ b/src/git-directory-helper.ts @@ -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 { + 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)) + } + } +} diff --git a/src/git-source-provider.ts b/src/git-source-provider.ts index e0dd58e..34b822c 100644 --- a/src/git-source-provider.ts +++ b/src/git-source-provider.ts @@ -1,36 +1,24 @@ import * as core from '@actions/core' -import * as fs from 'fs' import * as fsHelper from './fs-helper' +import * as gitAuthHelper from './git-auth-helper' import * as gitCommandManager from './git-command-manager' +import * as gitDirectoryHelper from './git-directory-helper' import * as githubApiHelper from './github-api-helper' import * as io from '@actions/io' import * as path from 'path' import * as refHelper from './ref-helper' import * as stateHelper from './state-helper' import {IGitCommandManager} from './git-command-manager' +import {IGitSourceSettings} from './git-source-settings' -const serverUrl = 'https://github.com/' -const authConfigKey = `http.${serverUrl}.extraheader` +const hostname = 'github.com' -export interface ISourceSettings { - 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 { +export async function getSource(settings: IGitSourceSettings): Promise { // Repository URL core.info( `Syncing repository: ${settings.repositoryOwner}/${settings.repositoryName}` ) - const repositoryUrl = `https://github.com/${encodeURIComponent( + const repositoryUrl = `https://${hostname}/${encodeURIComponent( settings.repositoryOwner )}/${encodeURIComponent(settings.repositoryName)}` @@ -51,7 +39,7 @@ export async function getSource(settings: ISourceSettings): Promise { // Prepare existing directory, otherwise recreate if (isExisting) { - await prepareExistingDirectory( + await gitDirectoryHelper.prepareExistingDirectory( git, settings.repositoryPath, repositoryUrl, @@ -92,12 +80,10 @@ export async function getSource(settings: ISourceSettings): Promise { ) } - // Remove possible previous extraheader - await removeGitConfig(git, authConfigKey) - + const authHelper = gitAuthHelper.createAuthHelper(git, settings) try { - // Config extraheader - await configureAuthToken(git, settings.authToken) + // Configure auth + await authHelper.configureAuth() // LFS install if (settings.lfs) { @@ -128,8 +114,9 @@ export async function getSource(settings: ISourceSettings): Promise { // Dump some info about the checked out commit await git.log1() } finally { + // Remove auth if (!settings.persistCredentials) { - await removeGitConfig(git, authConfigKey) + await authHelper.removeAuth() } } } @@ -146,22 +133,22 @@ export async function cleanup(repositoryPath: string): Promise { let git: IGitCommandManager try { - git = await gitCommandManager.CreateCommandManager(repositoryPath, false) + git = await gitCommandManager.createCommandManager(repositoryPath, false) } catch { return } - // Remove extraheader - await removeGitConfig(git, authConfigKey) + // Remove auth + const authHelper = gitAuthHelper.createAuthHelper(git) + await authHelper.removeAuth() } async function getGitCommandManager( - settings: ISourceSettings -): Promise { + settings: IGitSourceSettings +): Promise { core.info(`Working directory is '${settings.repositoryPath}'`) - let git = (null as unknown) as IGitCommandManager try { - return await gitCommandManager.CreateCommandManager( + return await gitCommandManager.createCommandManager( settings.repositoryPath, settings.lfs ) @@ -172,138 +159,6 @@ async function getGitCommandManager( } // Otherwise fallback to REST API - return (null as unknown) as IGitCommandManager - } -} - -async function prepareExistingDirectory( - git: IGitCommandManager, - repositoryPath: string, - repositoryUrl: string, - clean: boolean -): Promise { - 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 { - // 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 { - if ( - (await git.configExists(configKey)) && - !(await git.tryConfigUnset(configKey)) - ) { - // Load the config contents - core.warning(`Failed to remove '${configKey}' from the git config`) + return undefined } } diff --git a/src/git-source-settings.ts b/src/git-source-settings.ts new file mode 100644 index 0000000..49d8825 --- /dev/null +++ b/src/git-source-settings.ts @@ -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 +} diff --git a/src/input-helper.ts b/src/input-helper.ts index 54339da..e50dbc5 100644 --- a/src/input-helper.ts +++ b/src/input-helper.ts @@ -2,10 +2,10 @@ import * as core from '@actions/core' import * as fsHelper from './fs-helper' import * as github from '@actions/github' import * as path from 'path' -import {ISourceSettings} from './git-source-provider' +import {IGitSourceSettings} from './git-source-settings' -export function getInputs(): ISourceSettings { - const result = ({} as unknown) as ISourceSettings +export function getInputs(): IGitSourceSettings { + const result = ({} as unknown) as IGitSourceSettings // GitHub workspace let githubWorkspacePath = process.env['GITHUB_WORKSPACE'] diff --git a/src/state-helper.ts b/src/state-helper.ts index 0f86a14..da15d86 100644 --- a/src/state-helper.ts +++ b/src/state-helper.ts @@ -1,4 +1,3 @@ -import * as core from '@actions/core' import * as coreCommand from '@actions/core/lib/command' /**