From 10a399b4b0349048d92bec231665b6806e7aa691 Mon Sep 17 00:00:00 2001 From: Teko <112829523+Teko012@users.noreply.github.com> Date: Wed, 12 Apr 2023 10:07:30 +0200 Subject: [PATCH 01/10] feat: update author and committer input defaults * Update github-actions[bot] * Update author to new email format --- .github/workflows/ci.yml | 4 ++-- .github/workflows/cpr-example-command.yml | 4 ++-- README.md | 8 ++++---- action.yml | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f3537c..a9c04ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,8 +68,8 @@ jobs: uses: ./ with: commit-message: '[CI] test ${{ matrix.target }}' - committer: GitHub - author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com> + committer: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> + author: ${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com> title: '[CI] test ${{ matrix.target }}' body: | - CI test case for target '${{ matrix.target }}' diff --git a/.github/workflows/cpr-example-command.yml b/.github/workflows/cpr-example-command.yml index 52501f5..f98a66c 100644 --- a/.github/workflows/cpr-example-command.yml +++ b/.github/workflows/cpr-example-command.yml @@ -16,8 +16,8 @@ jobs: uses: ./ with: commit-message: Update report - committer: GitHub - author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com> + committer: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> + author: ${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com> signoff: false title: '[Example] Update report' body: | diff --git a/README.md b/README.md index 62c4442..7ed5976 100644 --- a/README.md +++ b/README.md @@ -56,8 +56,8 @@ All inputs are **optional**. If not set, sensible defaults will be used. | `path` | Relative path under `GITHUB_WORKSPACE` to the repository. | `GITHUB_WORKSPACE` | | `add-paths` | A comma or newline-separated list of file paths to commit. Paths should follow git's [pathspec](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefpathspecapathspec) syntax. If no paths are specified, all new and modified files are added. See [Add specific paths](#add-specific-paths). | | | `commit-message` | The message to use when committing changes. See [commit-message](#commit-message). | `[create-pull-request] automated change` | -| `committer` | The committer name and email address in the format `Display Name `. Defaults to the GitHub Actions bot user. | `GitHub ` | -| `author` | The author name and email address in the format `Display Name `. Defaults to the user who triggered the workflow run. | `${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>` | +| `committer` | The committer name and email address in the format `Display Name `. Defaults to the GitHub Actions bot user. | `github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>` | +| `author` | The author name and email address in the format `Display Name `. Defaults to the user who triggered the workflow run. | `${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>` | | `signoff` | Add [`Signed-off-by`](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---signoff) line by the committer at the end of the commit log message. | `false` | | `branch` | The pull request branch name. | `create-pull-request/patch` | | `delete-branch` | Delete the `branch` if it doesn't have an active pull request associated with it. See [delete-branch](#delete-branch). | `false` | @@ -258,8 +258,8 @@ jobs: with: token: ${{ secrets.PAT }} commit-message: Update report - committer: GitHub - author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com> + committer: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> + author: ${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com> signoff: false branch: example-patches delete-branch: true diff --git a/action.yml b/action.yml index 39652f6..5bd317e 100644 --- a/action.yml +++ b/action.yml @@ -20,12 +20,12 @@ inputs: description: > The committer name and email address in the format `Display Name `. Defaults to the GitHub Actions bot user. - default: 'GitHub ' + default: 'github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>' author: description: > The author name and email address in the format `Display Name `. Defaults to the user who triggered the workflow run. - default: '${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>' + default: '${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>' signoff: description: 'Add `Signed-off-by` line by the committer at the end of the commit log message.' default: false From be547fcbbb2f5642593d2c7a3c775761bd71a6c7 Mon Sep 17 00:00:00 2001 From: Peter Evans <18365890+peter-evans@users.noreply.github.com> Date: Mon, 25 Sep 2023 11:52:47 +0100 Subject: [PATCH 02/10] feat: optional input for git ops token --- README.md | 1 + action.yml | 4 ++++ dist/index.js | 6 +++++- src/create-pull-request.ts | 3 ++- src/main.ts | 5 +++++ 5 files changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7ed5976..b1c0e0a 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ All inputs are **optional**. If not set, sensible defaults will be used. | Name | Description | Default | | --- | --- | --- | | `token` | `GITHUB_TOKEN` (permissions `contents: write` and `pull-requests: write`) or a `repo` scoped [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token). | `GITHUB_TOKEN` | +| `git-token` | The [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) that the action will use for git operations. | Defaults to the value of `token` | | `path` | Relative path under `GITHUB_WORKSPACE` to the repository. | `GITHUB_WORKSPACE` | | `add-paths` | A comma or newline-separated list of file paths to commit. Paths should follow git's [pathspec](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefpathspecapathspec) syntax. If no paths are specified, all new and modified files are added. See [Add specific paths](#add-specific-paths). | | | `commit-message` | The message to use when committing changes. See [commit-message](#commit-message). | `[create-pull-request] automated change` | diff --git a/action.yml b/action.yml index 5bd317e..9993f0f 100644 --- a/action.yml +++ b/action.yml @@ -4,6 +4,10 @@ inputs: token: description: 'GITHUB_TOKEN or a `repo` scoped Personal Access Token (PAT)' default: ${{ github.token }} + git-token: + description: > + The Personal Access Token (PAT) that the action will use for git operations. + Defaults to the value of `token`. path: description: > Relative path under $GITHUB_WORKSPACE to the repository. diff --git a/dist/index.js b/dist/index.js index 66d4115..ef00255 100644 --- a/dist/index.js +++ b/dist/index.js @@ -376,7 +376,7 @@ function createPullRequest(inputs) { // Configure auth if (baseRemote.protocol == 'HTTPS') { core.startGroup('Configuring credential for HTTPS authentication'); - yield gitAuthHelper.configureToken(inputs.token); + yield gitAuthHelper.configureToken(inputs.gitToken); core.endGroup(); } core.startGroup('Checking the base repository state'); @@ -1206,6 +1206,7 @@ function run() { try { const inputs = { token: core.getInput('token'), + gitToken: core.getInput('git-token'), path: core.getInput('path'), addPaths: utils.getInputAsArray('add-paths'), commitMessage: core.getInput('commit-message'), @@ -1228,6 +1229,9 @@ function run() { draft: core.getBooleanInput('draft') }; core.debug(`Inputs: ${(0, util_1.inspect)(inputs)}`); + if (!inputs.gitToken) { + inputs.gitToken = inputs.token; + } yield (0, create_pull_request_1.createPullRequest)(inputs); } catch (error) { diff --git a/src/create-pull-request.ts b/src/create-pull-request.ts index 526c5a4..692c557 100644 --- a/src/create-pull-request.ts +++ b/src/create-pull-request.ts @@ -11,6 +11,7 @@ import * as utils from './utils' export interface Inputs { token: string + gitToken: string path: string addPaths: string[] commitMessage: string @@ -106,7 +107,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.token) + await gitAuthHelper.configureToken(inputs.gitToken) core.endGroup() } diff --git a/src/main.ts b/src/main.ts index 711d0c3..3b47e03 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,6 +7,7 @@ async function run(): Promise { try { const inputs: Inputs = { token: core.getInput('token'), + gitToken: core.getInput('git-token'), path: core.getInput('path'), addPaths: utils.getInputAsArray('add-paths'), commitMessage: core.getInput('commit-message'), @@ -30,6 +31,10 @@ async function run(): Promise { } core.debug(`Inputs: ${inspect(inputs)}`) + if (!inputs.gitToken) { + inputs.gitToken = inputs.token + } + await createPullRequest(inputs) } catch (error) { core.setFailed(utils.getErrorMessage(error)) From bc1906950d721ef453a359bd1172d6626ca8fb35 Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Fri, 29 Sep 2023 10:24:09 -0500 Subject: [PATCH 03/10] feat: allow push-to-fork to push to sibling repos (#2414) Fixes #2412. --- src/create-pull-request.ts | 17 ++++++++++++++--- src/github-helper.ts | 6 ++---- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/create-pull-request.ts b/src/create-pull-request.ts index 692c557..12dddb8 100644 --- a/src/create-pull-request.ts +++ b/src/create-pull-request.ts @@ -84,11 +84,22 @@ export async function createPullRequest(inputs: Inputs): Promise { core.info( `Checking if '${branchRepository}' is a fork of '${baseRemote.repository}'` ) - const parentRepository = + const baseParentRepository = await githubHelper.getRepositoryParent( + baseRemote.repository + ) + const branchParentRepository = await githubHelper.getRepositoryParent(branchRepository) - if (parentRepository != baseRemote.repository) { + if (branchParentRepository == null) { throw new Error( - `Repository '${branchRepository}' is not a fork of '${baseRemote.repository}'. Unable to continue.` + `Repository '${branchRepository}' is not a fork. Unable to continue.` + ) + } + if ( + branchParentRepository != baseRemote.repository && + baseParentRepository != branchParentRepository + ) { + throw new Error( + `Repository '${branchRepository}' is not a fork of '${baseRemote.repository}', nor are they siblings. Unable to continue.` ) } // Add a remote for the fork diff --git a/src/github-helper.ts b/src/github-helper.ts index d1d224e..cc596a9 100644 --- a/src/github-helper.ts +++ b/src/github-helper.ts @@ -101,14 +101,12 @@ export class GitHubHelper { } } - async getRepositoryParent(headRepository: string): Promise { + async getRepositoryParent(headRepository: string): Promise { const {data: headRepo} = await this.octokit.rest.repos.get({ ...this.parseRepository(headRepository) }) if (!headRepo.parent) { - throw new Error( - `Repository '${headRepository}' is not a fork. Unable to continue.` - ) + return null } return headRepo.parent.full_name } From 85246161a0f52c3badcc0972d3b97665cea88157 Mon Sep 17 00:00:00 2001 From: Peter Evans <18365890+peter-evans@users.noreply.github.com> Date: Fri, 29 Sep 2023 16:26:05 +0100 Subject: [PATCH 04/10] build: update dist --- dist/index.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/dist/index.js b/dist/index.js index ef00255..1c84bf4 100644 --- a/dist/index.js +++ b/dist/index.js @@ -363,9 +363,14 @@ function createPullRequest(inputs) { if (inputs.pushToFork) { // Check if the supplied fork is really a fork of the base core.info(`Checking if '${branchRepository}' is a fork of '${baseRemote.repository}'`); - const parentRepository = yield githubHelper.getRepositoryParent(branchRepository); - if (parentRepository != baseRemote.repository) { - throw new Error(`Repository '${branchRepository}' is not a fork of '${baseRemote.repository}'. Unable to continue.`); + const baseParentRepository = yield githubHelper.getRepositoryParent(baseRemote.repository); + const branchParentRepository = yield githubHelper.getRepositoryParent(branchRepository); + if (branchParentRepository == null) { + throw new Error(`Repository '${branchRepository}' is not a fork. Unable to continue.`); + } + if (branchParentRepository != baseRemote.repository && + baseParentRepository != branchParentRepository) { + throw new Error(`Repository '${branchRepository}' is not a fork of '${baseRemote.repository}', nor are they siblings. Unable to continue.`); } // Add a remote for the fork const remoteUrl = utils.getRemoteUrl(baseRemote.protocol, baseRemote.hostname, branchRepository); @@ -1104,7 +1109,7 @@ class GitHubHelper { return __awaiter(this, void 0, void 0, function* () { const { data: headRepo } = yield this.octokit.rest.repos.get(Object.assign({}, this.parseRepository(headRepository))); if (!headRepo.parent) { - throw new Error(`Repository '${headRepository}' is not a fork. Unable to continue.`); + return null; } return headRepo.parent.full_name; }); From 21d8ea09d56b55e7381af60c0427786e5b3948df Mon Sep 17 00:00:00 2001 From: Peter Evans <18365890+peter-evans@users.noreply.github.com> Date: Thu, 30 Nov 2023 14:54:03 +0000 Subject: [PATCH 05/10] feat: update action runtime to node 20 (#2340) --- .github/workflows/ci.yml | 2 +- __test__/integration-tests.sh | 2 +- action.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9c04ee..04529c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 16.x + node-version: 20.x cache: npm - run: npm ci - run: npm run build diff --git a/__test__/integration-tests.sh b/__test__/integration-tests.sh index 5780b57..0fcaa54 100755 --- a/__test__/integration-tests.sh +++ b/__test__/integration-tests.sh @@ -8,7 +8,7 @@ if [[ "$(docker images -q $IMAGE 2> /dev/null)" == "" || $ARG1 == "build" ]]; th echo "Building Docker image $IMAGE ..." cat > Dockerfile << EOF -FROM node:16-alpine +FROM node:20-alpine RUN apk --no-cache add git git-daemon RUN npm install jest jest-environment-jsdom --global WORKDIR /cpr diff --git a/action.yml b/action.yml index 9993f0f..e265d04 100644 --- a/action.yml +++ b/action.yml @@ -84,7 +84,7 @@ outputs: pull-request-head-sha: description: 'The commit SHA of the pull request branch.' runs: - using: 'node16' + using: 'node20' main: 'dist/index.js' branding: icon: 'git-pull-request' From 9606fe7fd0c02fef10e9bd06a59645b37786d551 Mon Sep 17 00:00:00 2001 From: Peter Evans <18365890+peter-evans@users.noreply.github.com> Date: Thu, 19 Oct 2023 16:55:00 +0100 Subject: [PATCH 06/10] feat: add truncate warning to pull request body --- dist/index.js | 5 ++++- src/create-pull-request.ts | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/dist/index.js b/dist/index.js index 1c84bf4..cb568b3 100644 --- a/dist/index.js +++ b/dist/index.js @@ -337,7 +337,10 @@ function createPullRequest(inputs) { // 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); + 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); diff --git a/src/create-pull-request.ts b/src/create-pull-request.ts index 12dddb8..1e7b8ba 100644 --- a/src/create-pull-request.ts +++ b/src/create-pull-request.ts @@ -52,7 +52,10 @@ export async function createPullRequest(inputs: Inputs): Promise { core.warning( `Pull request body is too long. Truncating to 65536 characters.` ) - inputs.body = inputs.body.substring(0, 65536) + const truncateWarning = '...*[Pull request body truncated]*' + inputs.body = + inputs.body.substring(0, 65536 - truncateWarning.length) + + truncateWarning } // Get the repository path From 873341b21cb24e12deb9fbde71c1f590ff27e70e Mon Sep 17 00:00:00 2001 From: Peter Evans <18365890+peter-evans@users.noreply.github.com> Date: Thu, 30 Nov 2023 14:36:37 +0000 Subject: [PATCH 07/10] perf: unshallow only when necessary --- dist/index.js | 9 ++++----- src/create-or-update-branch.ts | 9 ++++++--- src/git-command-manager.ts | 5 ++++- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/dist/index.js b/dist/index.js index cb568b3..7efc7df 100644 --- a/dist/index.js +++ b/dist/index.js @@ -171,9 +171,7 @@ function createOrUpdateBranch(git, commitMessage, base, branch, branchRemoteName if (branchRemoteName == 'fork') { // If pushing to a fork we must fetch with 'unshallow' to avoid the following error on git push // ! [remote rejected] HEAD -> tests/push-branch-to-fork (shallow update not allowed) - yield git.fetch([`${workingBase}:${workingBase}`], baseRemote, [ - '--force' - ]); + yield git.fetch([`${workingBase}:${workingBase}`], baseRemote, ['--force'], true); } else { // If the remote is 'origin' we can git reset @@ -801,14 +799,15 @@ class GitCommandManager { return output.exitCode === 0; }); } - fetch(refSpec, remoteName, options) { + fetch(refSpec, remoteName, options, unshallow = false) { return __awaiter(this, void 0, void 0, function* () { const args = ['-c', 'protocol.version=2', 'fetch']; if (!refSpec.some(x => x === tagsRefSpec)) { args.push('--no-tags'); } args.push('--progress', '--no-recurse-submodules'); - if (utils.fileExistsSync(path.join(this.workingDirectory, '.git', 'shallow'))) { + if (unshallow && + utils.fileExistsSync(path.join(this.workingDirectory, '.git', 'shallow'))) { args.push('--unshallow'); } if (options) { diff --git a/src/create-or-update-branch.ts b/src/create-or-update-branch.ts index cb1fcbb..1c55ea5 100644 --- a/src/create-or-update-branch.ts +++ b/src/create-or-update-branch.ts @@ -180,9 +180,12 @@ export async function createOrUpdateBranch( if (branchRemoteName == 'fork') { // If pushing to a fork we must fetch with 'unshallow' to avoid the following error on git push // ! [remote rejected] HEAD -> tests/push-branch-to-fork (shallow update not allowed) - await git.fetch([`${workingBase}:${workingBase}`], baseRemote, [ - '--force' - ]) + await git.fetch( + [`${workingBase}:${workingBase}`], + baseRemote, + ['--force'], + true + ) } else { // If the remote is 'origin' we can git reset await git.checkout(workingBase) diff --git a/src/git-command-manager.ts b/src/git-command-manager.ts index b77f078..d5671a8 100644 --- a/src/git-command-manager.ts +++ b/src/git-command-manager.ts @@ -105,7 +105,8 @@ export class GitCommandManager { async fetch( refSpec: string[], remoteName?: string, - options?: string[] + options?: string[], + unshallow = false ): Promise { const args = ['-c', 'protocol.version=2', 'fetch'] if (!refSpec.some(x => x === tagsRefSpec)) { @@ -113,7 +114,9 @@ export class GitCommandManager { } args.push('--progress', '--no-recurse-submodules') + if ( + unshallow && utils.fileExistsSync(path.join(this.workingDirectory, '.git', 'shallow')) ) { args.push('--unshallow') From 02a8f71e34146f88dd40b7a4a664a895ee1bbfe8 Mon Sep 17 00:00:00 2001 From: Peter Evans <18365890+peter-evans@users.noreply.github.com> Date: Thu, 30 Nov 2023 15:20:05 +0000 Subject: [PATCH 08/10] fix: remove the remote for the fork on completion --- dist/index.js | 7 +++++-- src/create-pull-request.ts | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/dist/index.js b/dist/index.js index 7efc7df..d9ec1c9 100644 --- a/dist/index.js +++ b/dist/index.js @@ -320,7 +320,7 @@ const git_auth_helper_1 = __nccwpck_require__(2565); const utils = __importStar(__nccwpck_require__(918)); function createPullRequest(inputs) { return __awaiter(this, void 0, void 0, function* () { - let gitAuthHelper; + let gitAuthHelper, git; try { if (!inputs.token) { throw new Error(`Input 'token' not supplied. Unable to continue.`); @@ -343,7 +343,7 @@ function createPullRequest(inputs) { // Get the repository path const repoPath = utils.getRepoPath(inputs.path); // Create a git command manager - const git = yield git_command_manager_1.GitCommandManager.create(repoPath); + 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); @@ -511,6 +511,9 @@ function createPullRequest(inputs) { 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(); diff --git a/src/create-pull-request.ts b/src/create-pull-request.ts index 1e7b8ba..43e45c2 100644 --- a/src/create-pull-request.ts +++ b/src/create-pull-request.ts @@ -35,7 +35,7 @@ export interface Inputs { } export async function createPullRequest(inputs: Inputs): Promise { - let gitAuthHelper + let gitAuthHelper, git try { if (!inputs.token) { throw new Error(`Input 'token' not supplied. Unable to continue.`) @@ -61,7 +61,7 @@ export async function createPullRequest(inputs: Inputs): Promise { // Get the repository path const repoPath = utils.getRepoPath(inputs.path) // Create a git command manager - const git = await GitCommandManager.create(repoPath) + git = await GitCommandManager.create(repoPath) // Save and unset the extraheader auth config if it exists core.startGroup('Prepare git configuration') @@ -283,6 +283,9 @@ export async function createPullRequest(inputs: Inputs): Promise { } 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() From 1e6bff9d94fd310c0d3545970fcfd1868676f64b Mon Sep 17 00:00:00 2001 From: Peter Evans <18365890+peter-evans@users.noreply.github.com> Date: Tue, 19 Dec 2023 09:39:50 +0000 Subject: [PATCH 09/10] feat: infer github server and api urls --- ....test.ts => git-config-helper.int.test.ts} | 30 +- __test__/git-config-helper.unit.test.ts | 83 ++++ __test__/utils.unit.test.ts | 57 --- dist/index.js | 463 +++++++++--------- src/create-pull-request.ts | 53 +- ...it-auth-helper.ts => git-config-helper.ts} | 74 ++- src/github-helper.ts | 8 +- src/main.ts | 17 + src/utils.ts | 47 -- 9 files changed, 431 insertions(+), 401 deletions(-) rename __test__/{git-auth-helper.int.test.ts => git-config-helper.int.test.ts} (68%) create mode 100644 __test__/git-config-helper.unit.test.ts rename src/{git-auth-helper.ts => git-config-helper.ts} (72%) 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, From 098a6d3703c03162def3ef65f78e4e3c87a587d2 Mon Sep 17 00:00:00 2001 From: Peter Evans <18365890+peter-evans@users.noreply.github.com> Date: Tue, 19 Dec 2023 11:29:19 +0000 Subject: [PATCH 10/10] test: integration test fixes --- __test__/create-or-update-branch.int.test.ts | 4 +- __test__/entrypoint.sh | 14 +++--- __test__/git-config-helper.int.test.ts | 52 +++++++++++++------- __test__/git-config-helper.unit.test.ts | 10 ++++ dist/index.js | 12 ++++- src/git-config-helper.ts | 14 +++++- 6 files changed, 76 insertions(+), 30 deletions(-) diff --git a/__test__/create-or-update-branch.int.test.ts b/__test__/create-or-update-branch.int.test.ts index 09fa8ae..0f12254 100644 --- a/__test__/create-or-update-branch.int.test.ts +++ b/__test__/create-or-update-branch.int.test.ts @@ -8,7 +8,7 @@ import {GitCommandManager} from '../lib/git-command-manager' import * as path from 'path' import {v4 as uuidv4} from 'uuid' -const REPO_PATH = '/git/local/test-base' +const REPO_PATH = '/git/local/repos/test-base' const REMOTE_NAME = 'origin' const TRACKED_FILE = 'a/tracked-file.txt' @@ -22,7 +22,7 @@ const INIT_COMMIT_MESSAGE = 'Add file to be a tracked file for tests' const BRANCH = 'tests/create-pull-request/patch' const BASE = DEFAULT_BRANCH -const FORK_REMOTE_URL = 'git://127.0.0.1/test-fork.git' +const FORK_REMOTE_URL = 'git://127.0.0.1/repos/test-fork.git' const FORK_REMOTE_NAME = 'fork' const ADD_PATHS_DEFAULT = [] diff --git a/__test__/entrypoint.sh b/__test__/entrypoint.sh index 7dc7ccd..fa08b90 100755 --- a/__test__/entrypoint.sh +++ b/__test__/entrypoint.sh @@ -5,18 +5,18 @@ set -euo pipefail WORKINGDIR=$PWD # Create and serve a remote repo -mkdir -p /git/remote +mkdir -p /git/remote/repos git config --global init.defaultBranch main -git init --bare /git/remote/test-base.git +git init --bare /git/remote/repos/test-base.git git daemon --verbose --enable=receive-pack --base-path=/git/remote --export-all /git/remote &>/dev/null & # Give the daemon time to start sleep 2 # Create a local clone and make an initial commit -mkdir -p /git/local -git clone git://127.0.0.1/test-base.git /git/local/test-base -cd /git/local/test-base +mkdir -p /git/local/repos +git clone git://127.0.0.1/repos/test-base.git /git/local/repos/test-base +cd /git/local/repos/test-base git config --global user.email "you@example.com" git config --global user.name "Your Name" echo "#test-base" > README.md @@ -30,8 +30,8 @@ git config -l # Clone a server-side fork of the base repo cd $WORKINGDIR -git clone --mirror git://127.0.0.1/test-base.git /git/remote/test-fork.git -cd /git/remote/test-fork.git +git clone --mirror git://127.0.0.1/repos/test-base.git /git/remote/repos/test-fork.git +cd /git/remote/repos/test-fork.git git log -1 --pretty=oneline # Restore the working directory diff --git a/__test__/git-config-helper.int.test.ts b/__test__/git-config-helper.int.test.ts index 08d3beb..040a080 100644 --- a/__test__/git-config-helper.int.test.ts +++ b/__test__/git-config-helper.int.test.ts @@ -1,9 +1,9 @@ import {GitCommandManager} from '../lib/git-command-manager' import {GitConfigHelper} from '../lib/git-config-helper' -const REPO_PATH = '/git/local/test-base' +const REPO_PATH = '/git/local/repos/test-base' -const extraheaderConfigKey = 'http.https://github.com/.extraheader' +const extraheaderConfigKey = 'http.https://127.0.0.1/.extraheader' describe('git-config-helper integration tests', () => { let git: GitCommandManager @@ -11,22 +11,22 @@ describe('git-config-helper integration tests', () => { beforeAll(async () => { git = await GitCommandManager.create(REPO_PATH) - gitConfigHelper = await GitConfigHelper.create(git) }) it('tests save and restore with no persisted auth', async () => { - await gitConfigHelper.savePersistedAuth() - await gitConfigHelper.restorePersistedAuth() + const gitConfigHelper = await GitConfigHelper.create(git) + await gitConfigHelper.close() }) it('tests configure and removal of auth', async () => { + const gitConfigHelper = await GitConfigHelper.create(git) await gitConfigHelper.configureToken('github-token') expect(await git.configExists(extraheaderConfigKey)).toBeTruthy() expect(await git.getConfigValue(extraheaderConfigKey)).toEqual( 'AUTHORIZATION: basic eC1hY2Nlc3MtdG9rZW46Z2l0aHViLXRva2Vu' ) - await gitConfigHelper.removeAuth() + await gitConfigHelper.close() expect(await git.configExists(extraheaderConfigKey)).toBeFalsy() }) @@ -34,37 +34,53 @@ describe('git-config-helper integration tests', () => { const extraheaderConfigValue = 'AUTHORIZATION: basic ***persisted-auth***' await git.config(extraheaderConfigKey, extraheaderConfigValue) - await gitConfigHelper.savePersistedAuth() + const gitConfigHelper = await GitConfigHelper.create(git) const exists = await git.configExists(extraheaderConfigKey) expect(exists).toBeFalsy() - await gitConfigHelper.restorePersistedAuth() + await gitConfigHelper.close() const configValue = await git.getConfigValue(extraheaderConfigKey) expect(configValue).toEqual(extraheaderConfigValue) - await gitConfigHelper.removeAuth() + const unset = await git.tryConfigUnset( + extraheaderConfigKey, + '^AUTHORIZATION:' + ) + expect(unset).toBeTruthy() + }) + + it('tests not adding/removing the safe.directory config when it already exists', async () => { + await git.config('safe.directory', '/another-value', true, true) + + const gitConfigHelper = await GitConfigHelper.create(git) + + expect( + await git.configExists('safe.directory', '/another-value', true) + ).toBeTruthy() + + await gitConfigHelper.close() + + const unset = await git.tryConfigUnset( + 'safe.directory', + '/another-value', + true + ) + expect(unset).toBeTruthy() }) it('tests adding and removing the safe.directory config', async () => { - await git.config('safe.directory', '/another-value', true, true) - - await gitConfigHelper.removeSafeDirectory() - await gitConfigHelper.addSafeDirectory() + const gitConfigHelper = await GitConfigHelper.create(git) expect( await git.configExists('safe.directory', REPO_PATH, true) ).toBeTruthy() - await gitConfigHelper.addSafeDirectory() - await gitConfigHelper.removeSafeDirectory() + await gitConfigHelper.close() expect( await git.configExists('safe.directory', REPO_PATH, true) ).toBeFalsy() - expect( - await git.configExists('safe.directory', '/another-value', true) - ).toBeTruthy() }) }) diff --git a/__test__/git-config-helper.unit.test.ts b/__test__/git-config-helper.unit.test.ts index 083b5f9..8527471 100644 --- a/__test__/git-config-helper.unit.test.ts +++ b/__test__/git-config-helper.unit.test.ts @@ -68,6 +68,16 @@ describe('git-config-helper unit tests', () => { expect(remote3.repository).toEqual('peter-evans/create-pull-request') }) + test('parseGitRemote successfully parses GIT remote URLs', async () => { + // Unauthenticated git protocol for integration tests only + const remote1 = GitConfigHelper.parseGitRemote( + 'git://127.0.0.1/repos/test-base.git' + ) + expect(remote1.hostname).toEqual('127.0.0.1') + expect(remote1.protocol).toEqual('GIT') + expect(remote1.repository).toEqual('repos/test-base') + }) + test('parseGitRemote fails to parse a remote URL', async () => { const remoteUrl = 'https://github.com/peter-evans' try { diff --git a/dist/index.js b/dist/index.js index a3a6a61..1089e16 100644 --- a/dist/index.js +++ b/dist/index.js @@ -929,7 +929,6 @@ class GitConfigHelper { } 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 { @@ -938,6 +937,7 @@ class GitConfigHelper { repository: httpsMatch[3] }; } + const sshUrlPattern = new RegExp('^git@(.+?):(.+/.+)\\.git$', 'i'); const sshMatch = remoteUrl.match(sshUrlPattern); if (sshMatch) { return { @@ -946,6 +946,16 @@ class GitConfigHelper { repository: sshMatch[2] }; } + // Unauthenticated git protocol for integration tests only + const gitUrlPattern = new RegExp('^git://(.+?)/(.+/.+)\\.git$', 'i'); + const gitMatch = remoteUrl.match(gitUrlPattern); + if (gitMatch) { + return { + hostname: gitMatch[1], + protocol: 'GIT', + repository: gitMatch[2] + }; + } throw new Error(`The format of '${remoteUrl}' is not a valid GitHub repository URL`); } savePersistedAuth() { diff --git a/src/git-config-helper.ts b/src/git-config-helper.ts index 6c515e0..f5f484b 100644 --- a/src/git-config-helper.ts +++ b/src/git-config-helper.ts @@ -83,8 +83,6 @@ export class GitConfigHelper { '^(https?)://(?:.+@)?(.+?)/(.+/.+?)(\\.git)?$', 'i' ) - const sshUrlPattern = new RegExp('^git@(.+?):(.+/.+)\\.git$', 'i') - const httpsMatch = remoteUrl.match(httpsUrlPattern) if (httpsMatch) { return { @@ -94,6 +92,7 @@ export class GitConfigHelper { } } + const sshUrlPattern = new RegExp('^git@(.+?):(.+/.+)\\.git$', 'i') const sshMatch = remoteUrl.match(sshUrlPattern) if (sshMatch) { return { @@ -103,6 +102,17 @@ export class GitConfigHelper { } } + // Unauthenticated git protocol for integration tests only + const gitUrlPattern = new RegExp('^git://(.+?)/(.+/.+)\\.git$', 'i') + const gitMatch = remoteUrl.match(gitUrlPattern) + if (gitMatch) { + return { + hostname: gitMatch[1], + protocol: 'GIT', + repository: gitMatch[2] + } + } + throw new Error( `The format of '${remoteUrl}' is not a valid GitHub repository URL` )