diff --git a/__test__/git-auth-helper.int.test.ts b/__test__/git-config-helper.int.test.ts similarity index 68% rename from __test__/git-auth-helper.int.test.ts rename to __test__/git-config-helper.int.test.ts index 98c5398..08d3beb 100644 --- a/__test__/git-auth-helper.int.test.ts +++ b/__test__/git-config-helper.int.test.ts @@ -1,32 +1,32 @@ import {GitCommandManager} from '../lib/git-command-manager' -import {GitAuthHelper} from '../lib/git-auth-helper' +import {GitConfigHelper} from '../lib/git-config-helper' const REPO_PATH = '/git/local/test-base' const extraheaderConfigKey = 'http.https://github.com/.extraheader' -describe('git-auth-helper tests', () => { +describe('git-config-helper integration tests', () => { let git: GitCommandManager - let gitAuthHelper: GitAuthHelper + let gitConfigHelper: GitConfigHelper beforeAll(async () => { git = await GitCommandManager.create(REPO_PATH) - gitAuthHelper = new GitAuthHelper(git) + gitConfigHelper = await GitConfigHelper.create(git) }) it('tests save and restore with no persisted auth', async () => { - await gitAuthHelper.savePersistedAuth() - await gitAuthHelper.restorePersistedAuth() + await gitConfigHelper.savePersistedAuth() + await gitConfigHelper.restorePersistedAuth() }) it('tests configure and removal of auth', async () => { - await gitAuthHelper.configureToken('github-token') + await gitConfigHelper.configureToken('github-token') expect(await git.configExists(extraheaderConfigKey)).toBeTruthy() expect(await git.getConfigValue(extraheaderConfigKey)).toEqual( 'AUTHORIZATION: basic eC1hY2Nlc3MtdG9rZW46Z2l0aHViLXRva2Vu' ) - await gitAuthHelper.removeAuth() + await gitConfigHelper.removeAuth() expect(await git.configExists(extraheaderConfigKey)).toBeFalsy() }) @@ -34,31 +34,31 @@ describe('git-auth-helper tests', () => { const extraheaderConfigValue = 'AUTHORIZATION: basic ***persisted-auth***' await git.config(extraheaderConfigKey, extraheaderConfigValue) - await gitAuthHelper.savePersistedAuth() + await gitConfigHelper.savePersistedAuth() const exists = await git.configExists(extraheaderConfigKey) expect(exists).toBeFalsy() - await gitAuthHelper.restorePersistedAuth() + await gitConfigHelper.restorePersistedAuth() const configValue = await git.getConfigValue(extraheaderConfigKey) expect(configValue).toEqual(extraheaderConfigValue) - await gitAuthHelper.removeAuth() + await gitConfigHelper.removeAuth() }) it('tests adding and removing the safe.directory config', async () => { await git.config('safe.directory', '/another-value', true, true) - await gitAuthHelper.removeSafeDirectory() - await gitAuthHelper.addSafeDirectory() + await gitConfigHelper.removeSafeDirectory() + await gitConfigHelper.addSafeDirectory() expect( await git.configExists('safe.directory', REPO_PATH, true) ).toBeTruthy() - await gitAuthHelper.addSafeDirectory() - await gitAuthHelper.removeSafeDirectory() + await gitConfigHelper.addSafeDirectory() + await gitConfigHelper.removeSafeDirectory() expect( await git.configExists('safe.directory', REPO_PATH, true) diff --git a/__test__/git-config-helper.unit.test.ts b/__test__/git-config-helper.unit.test.ts new file mode 100644 index 0000000..083b5f9 --- /dev/null +++ b/__test__/git-config-helper.unit.test.ts @@ -0,0 +1,83 @@ +import {GitConfigHelper} from '../lib/git-config-helper' + +describe('git-config-helper unit tests', () => { + test('parseGitRemote successfully parses HTTPS remote URLs', async () => { + const remote1 = GitConfigHelper.parseGitRemote( + 'https://github.com/peter-evans/create-pull-request' + ) + expect(remote1.hostname).toEqual('github.com') + expect(remote1.protocol).toEqual('HTTPS') + expect(remote1.repository).toEqual('peter-evans/create-pull-request') + + const remote2 = GitConfigHelper.parseGitRemote( + 'https://xxx:x-oauth-basic@github.com/peter-evans/create-pull-request' + ) + expect(remote2.hostname).toEqual('github.com') + expect(remote2.protocol).toEqual('HTTPS') + expect(remote2.repository).toEqual('peter-evans/create-pull-request') + + const remote3 = GitConfigHelper.parseGitRemote( + 'https://github.com/peter-evans/create-pull-request.git' + ) + expect(remote3.hostname).toEqual('github.com') + expect(remote3.protocol).toEqual('HTTPS') + expect(remote3.repository).toEqual('peter-evans/create-pull-request') + + const remote4 = GitConfigHelper.parseGitRemote( + 'https://github.com/peter-evans/ungit' + ) + expect(remote4.hostname).toEqual('github.com') + expect(remote4.protocol).toEqual('HTTPS') + expect(remote4.repository).toEqual('peter-evans/ungit') + + const remote5 = GitConfigHelper.parseGitRemote( + 'https://github.com/peter-evans/ungit.git' + ) + expect(remote5.hostname).toEqual('github.com') + expect(remote5.protocol).toEqual('HTTPS') + expect(remote5.repository).toEqual('peter-evans/ungit') + + const remote6 = GitConfigHelper.parseGitRemote( + 'https://github.internal.company/peter-evans/create-pull-request' + ) + expect(remote6.hostname).toEqual('github.internal.company') + expect(remote6.protocol).toEqual('HTTPS') + expect(remote6.repository).toEqual('peter-evans/create-pull-request') + }) + + test('parseGitRemote successfully parses SSH remote URLs', async () => { + const remote1 = GitConfigHelper.parseGitRemote( + 'git@github.com:peter-evans/create-pull-request.git' + ) + expect(remote1.hostname).toEqual('github.com') + expect(remote1.protocol).toEqual('SSH') + expect(remote1.repository).toEqual('peter-evans/create-pull-request') + + const remote2 = GitConfigHelper.parseGitRemote( + 'git@github.com:peter-evans/ungit.git' + ) + expect(remote2.hostname).toEqual('github.com') + expect(remote2.protocol).toEqual('SSH') + expect(remote2.repository).toEqual('peter-evans/ungit') + + const remote3 = GitConfigHelper.parseGitRemote( + 'git@github.internal.company:peter-evans/create-pull-request.git' + ) + expect(remote3.hostname).toEqual('github.internal.company') + expect(remote3.protocol).toEqual('SSH') + expect(remote3.repository).toEqual('peter-evans/create-pull-request') + }) + + test('parseGitRemote fails to parse a remote URL', async () => { + const remoteUrl = 'https://github.com/peter-evans' + try { + GitConfigHelper.parseGitRemote(remoteUrl) + // Fail the test if an error wasn't thrown + expect(true).toEqual(false) + } catch (e: any) { + expect(e.message).toEqual( + `The format of '${remoteUrl}' is not a valid GitHub repository URL` + ) + } + }) +}) diff --git a/__test__/utils.unit.test.ts b/__test__/utils.unit.test.ts index 257575d..b7805d2 100644 --- a/__test__/utils.unit.test.ts +++ b/__test__/utils.unit.test.ts @@ -44,63 +44,6 @@ describe('utils tests', () => { ) }) - test('getRemoteDetail successfully parses remote URLs', async () => { - const remote1 = utils.getRemoteDetail( - 'https://github.com/peter-evans/create-pull-request' - ) - expect(remote1.protocol).toEqual('HTTPS') - expect(remote1.repository).toEqual('peter-evans/create-pull-request') - - const remote2 = utils.getRemoteDetail( - 'https://xxx:x-oauth-basic@github.com/peter-evans/create-pull-request' - ) - expect(remote2.protocol).toEqual('HTTPS') - expect(remote2.repository).toEqual('peter-evans/create-pull-request') - - const remote3 = utils.getRemoteDetail( - 'git@github.com:peter-evans/create-pull-request.git' - ) - expect(remote3.protocol).toEqual('SSH') - expect(remote3.repository).toEqual('peter-evans/create-pull-request') - - const remote4 = utils.getRemoteDetail( - 'https://github.com/peter-evans/create-pull-request.git' - ) - expect(remote4.protocol).toEqual('HTTPS') - expect(remote4.repository).toEqual('peter-evans/create-pull-request') - - const remote5 = utils.getRemoteDetail( - 'https://github.com/peter-evans/ungit' - ) - expect(remote5.protocol).toEqual('HTTPS') - expect(remote5.repository).toEqual('peter-evans/ungit') - - const remote6 = utils.getRemoteDetail( - 'https://github.com/peter-evans/ungit.git' - ) - expect(remote6.protocol).toEqual('HTTPS') - expect(remote6.repository).toEqual('peter-evans/ungit') - - const remote7 = utils.getRemoteDetail( - 'git@github.com:peter-evans/ungit.git' - ) - expect(remote7.protocol).toEqual('SSH') - expect(remote7.repository).toEqual('peter-evans/ungit') - }) - - test('getRemoteDetail fails to parse a remote URL', async () => { - const remoteUrl = 'https://github.com/peter-evans' - try { - utils.getRemoteDetail(remoteUrl) - // Fail the test if an error wasn't thrown - expect(true).toEqual(false) - } catch (e: any) { - expect(e.message).toEqual( - `The format of '${remoteUrl}' is not a valid GitHub repository URL` - ) - } - }) - test('getRemoteUrl successfully returns remote URLs', async () => { const url1 = utils.getRemoteUrl( 'HTTPS', diff --git a/dist/index.js b/dist/index.js index d9ec1c9..a3a6a61 100644 --- a/dist/index.js +++ b/dist/index.js @@ -316,46 +316,21 @@ const core = __importStar(__nccwpck_require__(2186)); const create_or_update_branch_1 = __nccwpck_require__(8363); const github_helper_1 = __nccwpck_require__(446); const git_command_manager_1 = __nccwpck_require__(738); -const git_auth_helper_1 = __nccwpck_require__(2565); +const git_config_helper_1 = __nccwpck_require__(8384); const utils = __importStar(__nccwpck_require__(918)); function createPullRequest(inputs) { return __awaiter(this, void 0, void 0, function* () { - let gitAuthHelper, git; + let gitConfigHelper, git; try { - if (!inputs.token) { - throw new Error(`Input 'token' not supplied. Unable to continue.`); - } - if (inputs.bodyPath) { - if (!utils.fileExistsSync(inputs.bodyPath)) { - throw new Error(`File '${inputs.bodyPath}' does not exist.`); - } - // Update the body input with the contents of the file - inputs.body = utils.readFile(inputs.bodyPath); - } - // 65536 characters is the maximum allowed for the pull request body. - if (inputs.body.length > 65536) { - core.warning(`Pull request body is too long. Truncating to 65536 characters.`); - const truncateWarning = '...*[Pull request body truncated]*'; - inputs.body = - inputs.body.substring(0, 65536 - truncateWarning.length) + - truncateWarning; - } - // Get the repository path - const repoPath = utils.getRepoPath(inputs.path); - // Create a git command manager - git = yield git_command_manager_1.GitCommandManager.create(repoPath); - // Save and unset the extraheader auth config if it exists core.startGroup('Prepare git configuration'); - gitAuthHelper = new git_auth_helper_1.GitAuthHelper(git); - yield gitAuthHelper.addSafeDirectory(); - yield gitAuthHelper.savePersistedAuth(); + const repoPath = utils.getRepoPath(inputs.path); + git = yield git_command_manager_1.GitCommandManager.create(repoPath); + gitConfigHelper = yield git_config_helper_1.GitConfigHelper.create(git); core.endGroup(); - // Init the GitHub client - const githubHelper = new github_helper_1.GitHubHelper(inputs.token); core.startGroup('Determining the base and head repositories'); - // Determine the base repository from git config - const remoteUrl = yield git.tryGetRemoteUrl(); - const baseRemote = utils.getRemoteDetail(remoteUrl); + const baseRemote = gitConfigHelper.getGitRemote(); + // Init the GitHub client + const githubHelper = new github_helper_1.GitHubHelper(baseRemote.hostname, inputs.token); // Determine the head repository; the target for the pull request branch const branchRemoteName = inputs.pushToFork ? 'fork' : 'origin'; const branchRepository = inputs.pushToFork @@ -382,7 +357,7 @@ function createPullRequest(inputs) { // Configure auth if (baseRemote.protocol == 'HTTPS') { core.startGroup('Configuring credential for HTTPS authentication'); - yield gitAuthHelper.configureToken(inputs.gitToken); + yield gitConfigHelper.configureToken(inputs.gitToken); core.endGroup(); } core.startGroup('Checking the base repository state'); @@ -509,14 +484,11 @@ function createPullRequest(inputs) { core.setFailed(utils.getErrorMessage(error)); } finally { - // Remove auth and restore persisted auth config if it existed core.startGroup('Restore git configuration'); if (inputs.pushToFork) { yield git.exec(['remote', 'rm', 'fork']); } - yield gitAuthHelper.removeAuth(); - yield gitAuthHelper.restorePersistedAuth(); - yield gitAuthHelper.removeSafeDirectory(); + yield gitConfigHelper.close(); core.endGroup(); } }); @@ -524,163 +496,6 @@ function createPullRequest(inputs) { exports.createPullRequest = createPullRequest; -/***/ }), - -/***/ 2565: -/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { - -"use strict"; - -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; -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()); - }); -}; -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.GitAuthHelper = void 0; -const core = __importStar(__nccwpck_require__(2186)); -const fs = __importStar(__nccwpck_require__(7147)); -const path = __importStar(__nccwpck_require__(1017)); -const url_1 = __nccwpck_require__(7310); -const utils = __importStar(__nccwpck_require__(918)); -class GitAuthHelper { - constructor(git) { - this.gitConfigPath = ''; - this.safeDirectoryConfigKey = 'safe.directory'; - this.safeDirectoryAdded = false; - this.extraheaderConfigPlaceholderValue = 'AUTHORIZATION: basic ***'; - this.extraheaderConfigValueRegex = '^AUTHORIZATION:'; - this.persistedExtraheaderConfigValue = ''; - this.git = git; - this.workingDirectory = this.git.getWorkingDirectory(); - const serverUrl = this.getServerUrl(); - this.extraheaderConfigKey = `http.${serverUrl.origin}/.extraheader`; - } - addSafeDirectory() { - return __awaiter(this, void 0, void 0, function* () { - const exists = yield this.git.configExists(this.safeDirectoryConfigKey, this.workingDirectory, true); - if (!exists) { - yield this.git.config(this.safeDirectoryConfigKey, this.workingDirectory, true, true); - this.safeDirectoryAdded = true; - } - }); - } - removeSafeDirectory() { - return __awaiter(this, void 0, void 0, function* () { - if (this.safeDirectoryAdded) { - yield this.git.tryConfigUnset(this.safeDirectoryConfigKey, this.workingDirectory, true); - } - }); - } - savePersistedAuth() { - return __awaiter(this, void 0, void 0, function* () { - // Save and unset persisted extraheader credential in git config if it exists - this.persistedExtraheaderConfigValue = yield this.getAndUnset(); - }); - } - restorePersistedAuth() { - return __awaiter(this, void 0, void 0, function* () { - if (this.persistedExtraheaderConfigValue) { - try { - yield this.setExtraheaderConfig(this.persistedExtraheaderConfigValue); - core.info('Persisted git credentials restored'); - } - catch (e) { - core.warning(utils.getErrorMessage(e)); - } - } - }); - } - configureToken(token) { - return __awaiter(this, void 0, void 0, function* () { - // Encode and configure the basic credential for HTTPS access - const basicCredential = Buffer.from(`x-access-token:${token}`, 'utf8').toString('base64'); - core.setSecret(basicCredential); - const extraheaderConfigValue = `AUTHORIZATION: basic ${basicCredential}`; - yield this.setExtraheaderConfig(extraheaderConfigValue); - }); - } - removeAuth() { - return __awaiter(this, void 0, void 0, function* () { - yield this.getAndUnset(); - }); - } - setExtraheaderConfig(extraheaderConfigValue) { - 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 - // See https://github.com/actions/checkout/blob/main/src/git-auth-helper.ts#L267-L274 - yield this.git.config(this.extraheaderConfigKey, this.extraheaderConfigPlaceholderValue); - // Replace the placeholder - yield this.gitConfigStringReplace(this.extraheaderConfigPlaceholderValue, extraheaderConfigValue); - }); - } - getAndUnset() { - return __awaiter(this, void 0, void 0, function* () { - let configValue = ''; - // Save and unset persisted extraheader credential in git config if it exists - if (yield this.git.configExists(this.extraheaderConfigKey, this.extraheaderConfigValueRegex)) { - configValue = yield this.git.getConfigValue(this.extraheaderConfigKey, this.extraheaderConfigValueRegex); - if (yield this.git.tryConfigUnset(this.extraheaderConfigKey, this.extraheaderConfigValueRegex)) { - core.info(`Unset config key '${this.extraheaderConfigKey}'`); - } - else { - core.warning(`Failed to unset config key '${this.extraheaderConfigKey}'`); - } - } - return configValue; - }); - } - gitConfigStringReplace(find, replace) { - return __awaiter(this, void 0, void 0, function* () { - if (this.gitConfigPath.length === 0) { - const gitDir = yield this.git.getGitDirectory(); - this.gitConfigPath = path.join(this.workingDirectory, gitDir, 'config'); - } - let content = (yield fs.promises.readFile(this.gitConfigPath)).toString(); - const index = content.indexOf(find); - if (index < 0 || index != content.lastIndexOf(find)) { - throw new Error(`Unable to replace '${find}' in ${this.gitConfigPath}`); - } - content = content.replace(find, replace); - yield fs.promises.writeFile(this.gitConfigPath, content); - }); - } - getServerUrl() { - return new url_1.URL(process.env['GITHUB_SERVER_URL'] || 'https://github.com'); - } -} -exports.GitAuthHelper = GitAuthHelper; - - /***/ }), /***/ 738: @@ -1012,6 +827,208 @@ class GitOutput { } +/***/ }), + +/***/ 8384: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +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()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.GitConfigHelper = void 0; +const core = __importStar(__nccwpck_require__(2186)); +const fs = __importStar(__nccwpck_require__(7147)); +const path = __importStar(__nccwpck_require__(1017)); +const url_1 = __nccwpck_require__(7310); +const utils = __importStar(__nccwpck_require__(918)); +class GitConfigHelper { + constructor(git) { + this.gitConfigPath = ''; + this.safeDirectoryConfigKey = 'safe.directory'; + this.safeDirectoryAdded = false; + this.remoteUrl = ''; + this.extraheaderConfigKey = ''; + this.extraheaderConfigPlaceholderValue = 'AUTHORIZATION: basic ***'; + this.extraheaderConfigValueRegex = '^AUTHORIZATION:'; + this.persistedExtraheaderConfigValue = ''; + this.git = git; + this.workingDirectory = this.git.getWorkingDirectory(); + } + static create(git) { + return __awaiter(this, void 0, void 0, function* () { + const gitConfigHelper = new GitConfigHelper(git); + yield gitConfigHelper.addSafeDirectory(); + yield gitConfigHelper.fetchRemoteDetail(); + yield gitConfigHelper.savePersistedAuth(); + return gitConfigHelper; + }); + } + close() { + return __awaiter(this, void 0, void 0, function* () { + // Remove auth and restore persisted auth config if it existed + yield this.removeAuth(); + yield this.restorePersistedAuth(); + yield this.removeSafeDirectory(); + }); + } + addSafeDirectory() { + return __awaiter(this, void 0, void 0, function* () { + const exists = yield this.git.configExists(this.safeDirectoryConfigKey, this.workingDirectory, true); + if (!exists) { + yield this.git.config(this.safeDirectoryConfigKey, this.workingDirectory, true, true); + this.safeDirectoryAdded = true; + } + }); + } + removeSafeDirectory() { + return __awaiter(this, void 0, void 0, function* () { + if (this.safeDirectoryAdded) { + yield this.git.tryConfigUnset(this.safeDirectoryConfigKey, this.workingDirectory, true); + } + }); + } + fetchRemoteDetail() { + return __awaiter(this, void 0, void 0, function* () { + this.remoteUrl = yield this.git.tryGetRemoteUrl(); + }); + } + getGitRemote() { + return GitConfigHelper.parseGitRemote(this.remoteUrl); + } + static parseGitRemote(remoteUrl) { + const httpsUrlPattern = new RegExp('^(https?)://(?:.+@)?(.+?)/(.+/.+?)(\\.git)?$', 'i'); + const sshUrlPattern = new RegExp('^git@(.+?):(.+/.+)\\.git$', 'i'); + const httpsMatch = remoteUrl.match(httpsUrlPattern); + if (httpsMatch) { + return { + hostname: httpsMatch[2], + protocol: 'HTTPS', + repository: httpsMatch[3] + }; + } + const sshMatch = remoteUrl.match(sshUrlPattern); + if (sshMatch) { + return { + hostname: sshMatch[1], + protocol: 'SSH', + repository: sshMatch[2] + }; + } + throw new Error(`The format of '${remoteUrl}' is not a valid GitHub repository URL`); + } + savePersistedAuth() { + return __awaiter(this, void 0, void 0, function* () { + const serverUrl = new url_1.URL(`https://${this.getGitRemote().hostname}`); + this.extraheaderConfigKey = `http.${serverUrl.origin}/.extraheader`; + // Save and unset persisted extraheader credential in git config if it exists + this.persistedExtraheaderConfigValue = yield this.getAndUnset(); + }); + } + restorePersistedAuth() { + return __awaiter(this, void 0, void 0, function* () { + if (this.persistedExtraheaderConfigValue) { + try { + yield this.setExtraheaderConfig(this.persistedExtraheaderConfigValue); + core.info('Persisted git credentials restored'); + } + catch (e) { + core.warning(utils.getErrorMessage(e)); + } + } + }); + } + configureToken(token) { + return __awaiter(this, void 0, void 0, function* () { + // Encode and configure the basic credential for HTTPS access + const basicCredential = Buffer.from(`x-access-token:${token}`, 'utf8').toString('base64'); + core.setSecret(basicCredential); + const extraheaderConfigValue = `AUTHORIZATION: basic ${basicCredential}`; + yield this.setExtraheaderConfig(extraheaderConfigValue); + }); + } + removeAuth() { + return __awaiter(this, void 0, void 0, function* () { + yield this.getAndUnset(); + }); + } + setExtraheaderConfig(extraheaderConfigValue) { + 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 + // See https://github.com/actions/checkout/blob/main/src/git-auth-helper.ts#L267-L274 + yield this.git.config(this.extraheaderConfigKey, this.extraheaderConfigPlaceholderValue); + // Replace the placeholder + yield this.gitConfigStringReplace(this.extraheaderConfigPlaceholderValue, extraheaderConfigValue); + }); + } + getAndUnset() { + return __awaiter(this, void 0, void 0, function* () { + let configValue = ''; + // Save and unset persisted extraheader credential in git config if it exists + if (yield this.git.configExists(this.extraheaderConfigKey, this.extraheaderConfigValueRegex)) { + configValue = yield this.git.getConfigValue(this.extraheaderConfigKey, this.extraheaderConfigValueRegex); + if (yield this.git.tryConfigUnset(this.extraheaderConfigKey, this.extraheaderConfigValueRegex)) { + core.info(`Unset config key '${this.extraheaderConfigKey}'`); + } + else { + core.warning(`Failed to unset config key '${this.extraheaderConfigKey}'`); + } + } + return configValue; + }); + } + gitConfigStringReplace(find, replace) { + return __awaiter(this, void 0, void 0, function* () { + if (this.gitConfigPath.length === 0) { + const gitDir = yield this.git.getGitDirectory(); + this.gitConfigPath = path.join(this.workingDirectory, gitDir, 'config'); + } + let content = (yield fs.promises.readFile(this.gitConfigPath)).toString(); + const index = content.indexOf(find); + if (index < 0 || index != content.lastIndexOf(find)) { + throw new Error(`Unable to replace '${find}' in ${this.gitConfigPath}`); + } + content = content.replace(find, replace); + yield fs.promises.writeFile(this.gitConfigPath, content); + }); + } +} +exports.GitConfigHelper = GitConfigHelper; + + /***/ }), /***/ 446: @@ -1058,12 +1075,17 @@ const octokit_client_1 = __nccwpck_require__(5040); const utils = __importStar(__nccwpck_require__(918)); const ERROR_PR_REVIEW_TOKEN_SCOPE = 'Validation Failed: "Could not resolve to a node with the global id of'; class GitHubHelper { - constructor(token) { + constructor(githubServerHostname, token) { const options = {}; if (token) { options.auth = `${token}`; } - options.baseUrl = process.env['GITHUB_API_URL'] || 'https://api.github.com'; + if (githubServerHostname !== 'github.com') { + options.baseUrl = `https://${githubServerHostname}/api/v3`; + } + else { + options.baseUrl = 'https://api.github.com'; + } this.octokit = new octokit_client_1.Octokit(options); } parseRepository(repository) { @@ -1239,9 +1261,24 @@ function run() { draft: core.getBooleanInput('draft') }; core.debug(`Inputs: ${(0, util_1.inspect)(inputs)}`); + if (!inputs.token) { + throw new Error(`Input 'token' not supplied. Unable to continue.`); + } if (!inputs.gitToken) { inputs.gitToken = inputs.token; } + if (inputs.bodyPath) { + if (!utils.fileExistsSync(inputs.bodyPath)) { + throw new Error(`File '${inputs.bodyPath}' does not exist.`); + } + // Update the body input with the contents of the file + inputs.body = utils.readFile(inputs.bodyPath); + } + // 65536 characters is the maximum allowed for the pull request body. + if (inputs.body.length > 65536) { + core.warning(`Pull request body is too long. Truncating to 65536 characters.`); + inputs.body = inputs.body.substring(0, 65536); + } yield (0, create_pull_request_1.createPullRequest)(inputs); } catch (error) { @@ -1309,7 +1346,7 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.getErrorMessage = exports.readFile = exports.fileExistsSync = exports.parseDisplayNameEmail = exports.randomString = exports.secondsSinceEpoch = exports.getRemoteUrl = exports.getRemoteDetail = exports.getRepoPath = exports.stripOrgPrefixFromTeams = exports.getStringAsArray = exports.getInputAsArray = void 0; +exports.getErrorMessage = exports.readFile = exports.fileExistsSync = exports.parseDisplayNameEmail = exports.randomString = exports.secondsSinceEpoch = exports.getRemoteUrl = exports.getRepoPath = exports.stripOrgPrefixFromTeams = exports.getStringAsArray = exports.getInputAsArray = void 0; const core = __importStar(__nccwpck_require__(2186)); const fs = __importStar(__nccwpck_require__(7147)); const path = __importStar(__nccwpck_require__(1017)); @@ -1348,36 +1385,6 @@ function getRepoPath(relativePath) { return repoPath; } exports.getRepoPath = getRepoPath; -function getRemoteDetail(remoteUrl) { - // Parse the protocol and github repository from a URL - // e.g. HTTPS, peter-evans/create-pull-request - const githubUrl = process.env['GITHUB_SERVER_URL'] || 'https://github.com'; - const githubServerMatch = githubUrl.match(/^https?:\/\/(.+)$/i); - if (!githubServerMatch) { - throw new Error('Could not parse GitHub Server name'); - } - const hostname = githubServerMatch[1]; - const httpsUrlPattern = new RegExp('^https?://.*@?' + hostname + '/(.+/.+?)(\\.git)?$', 'i'); - const sshUrlPattern = new RegExp('^git@' + hostname + ':(.+/.+)\\.git$', 'i'); - const httpsMatch = remoteUrl.match(httpsUrlPattern); - if (httpsMatch) { - return { - hostname, - protocol: 'HTTPS', - repository: httpsMatch[1] - }; - } - const sshMatch = remoteUrl.match(sshUrlPattern); - if (sshMatch) { - return { - hostname, - protocol: 'SSH', - repository: sshMatch[1] - }; - } - throw new Error(`The format of '${remoteUrl}' is not a valid GitHub repository URL`); -} -exports.getRemoteDetail = getRemoteDetail; function getRemoteUrl(protocol, hostname, repository) { return protocol == 'HTTPS' ? `https://${hostname}/${repository}` diff --git a/src/create-pull-request.ts b/src/create-pull-request.ts index 43e45c2..55bdfd8 100644 --- a/src/create-pull-request.ts +++ b/src/create-pull-request.ts @@ -6,7 +6,7 @@ import { } from './create-or-update-branch' import {GitHubHelper} from './github-helper' import {GitCommandManager} from './git-command-manager' -import {GitAuthHelper} from './git-auth-helper' +import {GitConfigHelper} from './git-config-helper' import * as utils from './utils' export interface Inputs { @@ -35,48 +35,18 @@ export interface Inputs { } export async function createPullRequest(inputs: Inputs): Promise { - let gitAuthHelper, git + let gitConfigHelper, git try { - if (!inputs.token) { - throw new Error(`Input 'token' not supplied. Unable to continue.`) - } - if (inputs.bodyPath) { - if (!utils.fileExistsSync(inputs.bodyPath)) { - throw new Error(`File '${inputs.bodyPath}' does not exist.`) - } - // Update the body input with the contents of the file - inputs.body = utils.readFile(inputs.bodyPath) - } - // 65536 characters is the maximum allowed for the pull request body. - if (inputs.body.length > 65536) { - core.warning( - `Pull request body is too long. Truncating to 65536 characters.` - ) - const truncateWarning = '...*[Pull request body truncated]*' - inputs.body = - inputs.body.substring(0, 65536 - truncateWarning.length) + - truncateWarning - } - - // Get the repository path - const repoPath = utils.getRepoPath(inputs.path) - // Create a git command manager - git = await GitCommandManager.create(repoPath) - - // Save and unset the extraheader auth config if it exists core.startGroup('Prepare git configuration') - gitAuthHelper = new GitAuthHelper(git) - await gitAuthHelper.addSafeDirectory() - await gitAuthHelper.savePersistedAuth() + const repoPath = utils.getRepoPath(inputs.path) + git = await GitCommandManager.create(repoPath) + gitConfigHelper = await GitConfigHelper.create(git) core.endGroup() - // Init the GitHub client - const githubHelper = new GitHubHelper(inputs.token) - core.startGroup('Determining the base and head repositories') - // Determine the base repository from git config - const remoteUrl = await git.tryGetRemoteUrl() - const baseRemote = utils.getRemoteDetail(remoteUrl) + const baseRemote = gitConfigHelper.getGitRemote() + // Init the GitHub client + const githubHelper = new GitHubHelper(baseRemote.hostname, inputs.token) // Determine the head repository; the target for the pull request branch const branchRemoteName = inputs.pushToFork ? 'fork' : 'origin' const branchRepository = inputs.pushToFork @@ -121,7 +91,7 @@ export async function createPullRequest(inputs: Inputs): Promise { // Configure auth if (baseRemote.protocol == 'HTTPS') { core.startGroup('Configuring credential for HTTPS authentication') - await gitAuthHelper.configureToken(inputs.gitToken) + await gitConfigHelper.configureToken(inputs.gitToken) core.endGroup() } @@ -281,14 +251,11 @@ export async function createPullRequest(inputs: Inputs): Promise { } catch (error) { core.setFailed(utils.getErrorMessage(error)) } finally { - // Remove auth and restore persisted auth config if it existed core.startGroup('Restore git configuration') if (inputs.pushToFork) { await git.exec(['remote', 'rm', 'fork']) } - await gitAuthHelper.removeAuth() - await gitAuthHelper.restorePersistedAuth() - await gitAuthHelper.removeSafeDirectory() + await gitConfigHelper.close() core.endGroup() } } diff --git a/src/git-auth-helper.ts b/src/git-config-helper.ts similarity index 72% rename from src/git-auth-helper.ts rename to src/git-config-helper.ts index 1b6c375..6c515e0 100644 --- a/src/git-auth-helper.ts +++ b/src/git-config-helper.ts @@ -5,22 +5,42 @@ import * as path from 'path' import {URL} from 'url' import * as utils from './utils' -export class GitAuthHelper { +interface GitRemote { + hostname: string + protocol: string + repository: string +} + +export class GitConfigHelper { private git: GitCommandManager private gitConfigPath = '' private workingDirectory: string private safeDirectoryConfigKey = 'safe.directory' private safeDirectoryAdded = false - private extraheaderConfigKey: string + private remoteUrl = '' + private extraheaderConfigKey = '' private extraheaderConfigPlaceholderValue = 'AUTHORIZATION: basic ***' private extraheaderConfigValueRegex = '^AUTHORIZATION:' private persistedExtraheaderConfigValue = '' - constructor(git: GitCommandManager) { + private constructor(git: GitCommandManager) { this.git = git this.workingDirectory = this.git.getWorkingDirectory() - const serverUrl = this.getServerUrl() - this.extraheaderConfigKey = `http.${serverUrl.origin}/.extraheader` + } + + static async create(git: GitCommandManager): Promise { + const gitConfigHelper = new GitConfigHelper(git) + await gitConfigHelper.addSafeDirectory() + await gitConfigHelper.fetchRemoteDetail() + await gitConfigHelper.savePersistedAuth() + return gitConfigHelper + } + + async close(): Promise { + // Remove auth and restore persisted auth config if it existed + await this.removeAuth() + await this.restorePersistedAuth() + await this.removeSafeDirectory() } async addSafeDirectory(): Promise { @@ -50,7 +70,47 @@ export class GitAuthHelper { } } + async fetchRemoteDetail(): Promise { + this.remoteUrl = await this.git.tryGetRemoteUrl() + } + + getGitRemote(): GitRemote { + return GitConfigHelper.parseGitRemote(this.remoteUrl) + } + + static parseGitRemote(remoteUrl: string): GitRemote { + const httpsUrlPattern = new RegExp( + '^(https?)://(?:.+@)?(.+?)/(.+/.+?)(\\.git)?$', + 'i' + ) + const sshUrlPattern = new RegExp('^git@(.+?):(.+/.+)\\.git$', 'i') + + const httpsMatch = remoteUrl.match(httpsUrlPattern) + if (httpsMatch) { + return { + hostname: httpsMatch[2], + protocol: 'HTTPS', + repository: httpsMatch[3] + } + } + + const sshMatch = remoteUrl.match(sshUrlPattern) + if (sshMatch) { + return { + hostname: sshMatch[1], + protocol: 'SSH', + repository: sshMatch[2] + } + } + + throw new Error( + `The format of '${remoteUrl}' is not a valid GitHub repository URL` + ) + } + async savePersistedAuth(): Promise { + const serverUrl = new URL(`https://${this.getGitRemote().hostname}`) + this.extraheaderConfigKey = `http.${serverUrl.origin}/.extraheader` // Save and unset persisted extraheader credential in git config if it exists this.persistedExtraheaderConfigValue = await this.getAndUnset() } @@ -144,8 +204,4 @@ export class GitAuthHelper { content = content.replace(find, replace) await fs.promises.writeFile(this.gitConfigPath, content) } - - private getServerUrl(): URL { - return new URL(process.env['GITHUB_SERVER_URL'] || 'https://github.com') - } } diff --git a/src/github-helper.ts b/src/github-helper.ts index cc596a9..850c6ef 100644 --- a/src/github-helper.ts +++ b/src/github-helper.ts @@ -20,12 +20,16 @@ interface Pull { export class GitHubHelper { private octokit: InstanceType - constructor(token: string) { + constructor(githubServerHostname: string, token: string) { const options: OctokitOptions = {} if (token) { options.auth = `${token}` } - options.baseUrl = process.env['GITHUB_API_URL'] || 'https://api.github.com' + if (githubServerHostname !== 'github.com') { + options.baseUrl = `https://${githubServerHostname}/api/v3` + } else { + options.baseUrl = 'https://api.github.com' + } this.octokit = new Octokit(options) } diff --git a/src/main.ts b/src/main.ts index 3b47e03..55cabd2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -31,9 +31,26 @@ async function run(): Promise { } core.debug(`Inputs: ${inspect(inputs)}`) + if (!inputs.token) { + throw new Error(`Input 'token' not supplied. Unable to continue.`) + } if (!inputs.gitToken) { inputs.gitToken = inputs.token } + if (inputs.bodyPath) { + if (!utils.fileExistsSync(inputs.bodyPath)) { + throw new Error(`File '${inputs.bodyPath}' does not exist.`) + } + // Update the body input with the contents of the file + inputs.body = utils.readFile(inputs.bodyPath) + } + // 65536 characters is the maximum allowed for the pull request body. + if (inputs.body.length > 65536) { + core.warning( + `Pull request body is too long. Truncating to 65536 characters.` + ) + inputs.body = inputs.body.substring(0, 65536) + } await createPullRequest(inputs) } catch (error) { diff --git a/src/utils.ts b/src/utils.ts index 9188bee..b501dd4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -41,53 +41,6 @@ export function getRepoPath(relativePath?: string): string { return repoPath } -interface RemoteDetail { - hostname: string - protocol: string - repository: string -} - -export function getRemoteDetail(remoteUrl: string): RemoteDetail { - // Parse the protocol and github repository from a URL - // e.g. HTTPS, peter-evans/create-pull-request - const githubUrl = process.env['GITHUB_SERVER_URL'] || 'https://github.com' - - const githubServerMatch = githubUrl.match(/^https?:\/\/(.+)$/i) - if (!githubServerMatch) { - throw new Error('Could not parse GitHub Server name') - } - - const hostname = githubServerMatch[1] - - const httpsUrlPattern = new RegExp( - '^https?://.*@?' + hostname + '/(.+/.+?)(\\.git)?$', - 'i' - ) - const sshUrlPattern = new RegExp('^git@' + hostname + ':(.+/.+)\\.git$', 'i') - - const httpsMatch = remoteUrl.match(httpsUrlPattern) - if (httpsMatch) { - return { - hostname, - protocol: 'HTTPS', - repository: httpsMatch[1] - } - } - - const sshMatch = remoteUrl.match(sshUrlPattern) - if (sshMatch) { - return { - hostname, - protocol: 'SSH', - repository: sshMatch[1] - } - } - - throw new Error( - `The format of '${remoteUrl}' is not a valid GitHub repository URL` - ) -} - export function getRemoteUrl( protocol: string, hostname: string,