Compare commits
1 commit
main
...
v4-graphql
Author | SHA1 | Date | |
---|---|---|---|
|
0bc3ec0e0a |
34 changed files with 71201 additions and 35918 deletions
37
.github/workflows/ci.yml
vendored
37
.github/workflows/ci.yml
vendored
|
@ -19,23 +19,21 @@ jobs:
|
|||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20.x
|
||||
node-version: 16.x
|
||||
cache: npm
|
||||
- name: Install Docker
|
||||
run: apt update && apt install docker.io -y
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npm run format-check
|
||||
- run: npm run lint
|
||||
- run: npm run test
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: dist
|
||||
path: dist
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: action.yml
|
||||
path: action.yml
|
||||
|
@ -48,16 +46,16 @@ jobs:
|
|||
matrix:
|
||||
target: [built, committed]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: main
|
||||
- if: matrix.target == 'built' || github.event_name == 'pull_request'
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: dist
|
||||
path: dist
|
||||
- if: matrix.target == 'built' || github.event_name == 'pull_request'
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: action.yml
|
||||
path: .
|
||||
|
@ -70,8 +68,8 @@ jobs:
|
|||
uses: ./
|
||||
with:
|
||||
commit-message: '[CI] test ${{ matrix.target }}'
|
||||
committer: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
||||
author: ${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>
|
||||
committer: GitHub <noreply@github.com>
|
||||
author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
|
||||
title: '[CI] test ${{ matrix.target }}'
|
||||
body: |
|
||||
- CI test case for target '${{ matrix.target }}'
|
||||
|
@ -82,7 +80,7 @@ jobs:
|
|||
branch: ci-test-${{ matrix.target }}-${{ github.sha }}
|
||||
|
||||
- name: Close Pull
|
||||
uses: peter-evans/close-pull@v3
|
||||
uses: peter-evans/close-pull@v2
|
||||
with:
|
||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
comment: '[CI] test ${{ matrix.target }}'
|
||||
|
@ -94,7 +92,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Find Comment
|
||||
uses: peter-evans/find-comment@v3
|
||||
uses: peter-evans/find-comment@v2
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ github.event.number }}
|
||||
|
@ -103,7 +101,7 @@ jobs:
|
|||
|
||||
- if: steps.fc.outputs.comment-id == ''
|
||||
name: Create comment
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
uses: peter-evans/create-or-update-comment@v2
|
||||
with:
|
||||
issue-number: ${{ github.event.number }}
|
||||
body: |
|
||||
|
@ -111,22 +109,19 @@ jobs:
|
|||
```
|
||||
/test repository=${{ github.event.pull_request.head.repo.full_name }} ref=${{ github.event.pull_request.head.ref }} build=true
|
||||
```
|
||||
```
|
||||
/test repository=${{ github.event.pull_request.head.repo.full_name }} ref=${{ github.event.pull_request.head.ref }} build=true sign-commits=true
|
||||
```
|
||||
|
||||
package:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs: [test]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: dist
|
||||
path: dist
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.ACTIONS_BOT_TOKEN }}
|
||||
commit-message: 'build: update distribution'
|
||||
|
|
8
.github/workflows/cpr-example-command.yml
vendored
8
.github/workflows/cpr-example-command.yml
vendored
|
@ -6,7 +6,7 @@ jobs:
|
|||
createPullRequest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Make changes to pull request
|
||||
run: date +%s > report.txt
|
||||
|
@ -16,8 +16,8 @@ jobs:
|
|||
uses: ./
|
||||
with:
|
||||
commit-message: Update report
|
||||
committer: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
||||
author: ${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>
|
||||
committer: GitHub <noreply@github.com>
|
||||
author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
|
||||
signoff: false
|
||||
title: '[Example] Update report'
|
||||
body: |
|
||||
|
@ -42,7 +42,7 @@ jobs:
|
|||
echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}"
|
||||
|
||||
- name: Add reaction
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
uses: peter-evans/create-or-update-comment@v2
|
||||
with:
|
||||
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
|
||||
comment-id: ${{ github.event.client_payload.github.payload.comment.id }}
|
||||
|
|
4
.github/workflows/slash-command-dispatch.yml
vendored
4
.github/workflows/slash-command-dispatch.yml
vendored
|
@ -7,7 +7,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Slash Command Dispatch
|
||||
uses: peter-evans/slash-command-dispatch@v4
|
||||
uses: peter-evans/slash-command-dispatch@v3
|
||||
with:
|
||||
token: ${{ secrets.ACTIONS_BOT_TOKEN }}
|
||||
config: >
|
||||
|
@ -19,7 +19,7 @@ jobs:
|
|||
"named_args": true
|
||||
},
|
||||
{
|
||||
"command": "testv5",
|
||||
"command": "testv4",
|
||||
"permission": "admin",
|
||||
"repository": "peter-evans/create-pull-request-tests",
|
||||
"named_args": true
|
||||
|
|
5
.github/workflows/update-major-version.yml
vendored
5
.github/workflows/update-major-version.yml
vendored
|
@ -11,14 +11,13 @@ on:
|
|||
type: choice
|
||||
description: The major version tag to update
|
||||
options:
|
||||
- v6
|
||||
- v7
|
||||
- v4
|
||||
|
||||
jobs:
|
||||
tag:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.ACTIONS_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
|
132
README.md
132
README.md
|
@ -15,27 +15,26 @@ Create Pull Request action will:
|
|||
- tracked (modified) files
|
||||
- commits made during the workflow that have not been pushed
|
||||
2. Commit all changes to a new branch, or update an existing pull request branch.
|
||||
3. Create or update a pull request to merge the branch into the base—the branch checked out in the workflow.
|
||||
3. Create a pull request to merge the new branch into the base—the branch checked out in the workflow.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Concepts, guidelines and advanced usage](docs/concepts-guidelines.md)
|
||||
- [Examples](docs/examples.md)
|
||||
- [Updating to v7](docs/updating.md)
|
||||
- [Common issues](docs/common-issues.md)
|
||||
- [Updating to v4](docs/updating.md)
|
||||
|
||||
## Usage
|
||||
|
||||
```yml
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
```
|
||||
|
||||
You can also pin to a [specific release](https://github.com/peter-evans/create-pull-request/releases) version in the format `@v7.x.x`
|
||||
You can also pin to a [specific release](https://github.com/peter-evans/create-pull-request/releases) version in the format `@v4.x.x`
|
||||
|
||||
### Workflow permissions
|
||||
|
||||
|
@ -48,90 +47,35 @@ For repositories belonging to an organization, this setting can be managed by ad
|
|||
|
||||
All inputs are **optional**. If not set, sensible defaults will be used.
|
||||
|
||||
**Note**: If you want pull requests created by this action to trigger an `on: push` or `on: pull_request` workflow then you cannot use the default `GITHUB_TOKEN`. See the [documentation here](docs/concepts-guidelines.md#triggering-further-workflow-runs) for workarounds.
|
||||
|
||||
| Name | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `token` | The token that the action will use to create and update the pull request. See [token](#token). | `GITHUB_TOKEN` |
|
||||
| `branch-token` | The token that the action will use to create and update the branch. See [branch-token](#branch-token). | Defaults to the value of `token` |
|
||||
| `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` |
|
||||
| `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 <email@address.com>`. Defaults to the GitHub Actions bot user on github.com. | `github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>` |
|
||||
| `author` | The author name and email address in the format `Display Name <email@address.com>`. Defaults to the user who triggered the workflow run. | `${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>` |
|
||||
| `commit-message` | The message to use when committing changes. | `[create-pull-request] automated change` |
|
||||
| `committer` | The committer name and email address in the format `Display Name <email@address.com>`. Defaults to the GitHub Actions bot user. | `GitHub <noreply@github.com>` |
|
||||
| `author` | The author name and email address in the format `Display Name <email@address.com>`. Defaults to the user who triggered the workflow run. | `${{ github.actor }} <${{ 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` |
|
||||
| `delete-branch` | Delete the `branch` when closing pull requests, and when undeleted after merging. | `false` |
|
||||
| `branch-suffix` | The branch suffix type when using the alternative branching strategy. Valid values are `random`, `timestamp` and `short-commit-hash`. See [Alternative strategy](#alternative-strategy---always-create-a-new-pull-request-branch) for details. | |
|
||||
| `base` | Sets the pull request base branch. | Defaults to the branch checked out in the workflow. |
|
||||
| `push-to-fork` | A fork of the checked-out parent repository to which the pull request branch will be pushed. e.g. `owner/repo-fork`. The pull request will be created to merge the fork's branch into the parent's base. See [push pull request branches to a fork](docs/concepts-guidelines.md#push-pull-request-branches-to-a-fork) for details. | |
|
||||
| `sign-commits` | Sign commits as `github-actions[bot]` when using `GITHUB_TOKEN`, or your own bot when using [GitHub App tokens](docs/concepts-guidelines.md#authenticating-with-github-app-generated-tokens). See [commit signing](docs/concepts-guidelines.md#commit-signature-verification-for-bots) for details. | `false` |
|
||||
| `title` | The title of the pull request. | `Changes by create-pull-request action` |
|
||||
| `body` | The body of the pull request. | `Automated changes by [create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub action` |
|
||||
| `body-path` | The path to a file containing the pull request body. Takes precedence over `body`. | |
|
||||
| `labels` | A comma or newline-separated list of labels. | |
|
||||
| `assignees` | A comma or newline-separated list of assignees (GitHub usernames). | |
|
||||
| `reviewers` | A comma or newline-separated list of reviewers (GitHub usernames) to request a review from. | |
|
||||
| `team-reviewers` | A comma or newline-separated list of GitHub teams to request a review from. Note that a `repo` scoped [PAT](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token), or equivalent [GitHub App permissions](docs/concepts-guidelines.md#authenticating-with-github-app-generated-tokens), are required. | |
|
||||
| `team-reviewers` | A comma or newline-separated list of GitHub teams to request a review from. Note that a `repo` scoped [PAT](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) may be required. See [this issue](https://github.com/peter-evans/create-pull-request/issues/155). If using a GitHub App, refer to [Authenticating with GitHub App generated tokens](docs/concepts-guidelines.md#authenticating-with-github-app-generated-tokens) for the proper permissions. | |
|
||||
| `milestone` | The number of the milestone to associate this pull request with. | |
|
||||
| `draft` | Create a [draft pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests#draft-pull-requests). Valid values are `true` (only on create), `always-true` (on create and update), and `false`. | `false` |
|
||||
| `maintainer-can-modify` | Indicates whether [maintainers can modify](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork) the pull request. | `true` |
|
||||
|
||||
#### token
|
||||
|
||||
The token input defaults to the repository's `GITHUB_TOKEN`.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> - If you want pull requests created by this action to trigger an `on: push` or `on: pull_request` workflow then you cannot use the default `GITHUB_TOKEN`. See the [documentation here](docs/concepts-guidelines.md#triggering-further-workflow-runs) for further details.
|
||||
> - If using the repository's `GITHUB_TOKEN` and your repository was created after 2nd February 2023, the [default permission is read-only](https://github.blog/changelog/2023-02-02-github-actions-updating-the-default-github_token-permissions-to-read-only/). Elevate the [permissions](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token#defining-access-for-the-github_token-permissions) in your workflow.
|
||||
> ```yml
|
||||
> permissions:
|
||||
> contents: write
|
||||
> pull-requests: write
|
||||
> ```
|
||||
|
||||
Other token options:
|
||||
- Classic [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) with `repo` scope.
|
||||
- Fine-grained [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) with `contents: write` and `pull-requests: write` scopes.
|
||||
- [GitHub App tokens](docs/concepts-guidelines.md#authenticating-with-github-app-generated-tokens) with `contents: write` and `pull-requests: write` scopes.
|
||||
|
||||
> [!TIP]
|
||||
> If pull requests could contain changes to Actions workflows you may also need the `workflows` scope.
|
||||
|
||||
#### branch-token
|
||||
|
||||
The action first creates a branch, and then creates a pull request for the branch.
|
||||
For some rare use cases it can be useful, or even necessary, to use different tokens for these operations.
|
||||
It is not advisable to use this input unless you know you need to.
|
||||
|
||||
#### commit-message
|
||||
|
||||
In addition to a message, the `commit-message` input can also be used to populate the commit description. Leave a single blank line between the message and description.
|
||||
|
||||
```yml
|
||||
commit-message: |
|
||||
the first line is the commit message
|
||||
|
||||
the commit description starts
|
||||
after a blank line and can be
|
||||
multiple lines
|
||||
```
|
||||
|
||||
#### delete-branch
|
||||
|
||||
The `delete-branch` feature doesn't delete branches immediately on merge. (It can't do that because it would require the merge to somehow trigger the action.)
|
||||
The intention of the feature is that when the action next runs it will delete the `branch` if there is no diff.
|
||||
|
||||
Enabling this feature leads to the following behaviour:
|
||||
1. If a pull request was merged and the branch is left undeleted, when the action next runs it will delete the branch if there is no further diff.
|
||||
2. If a pull request is open, but there is now no longer a diff and the PR is unnecessary, the action will delete the branch causing the PR to close.
|
||||
|
||||
If you want branches to be deleted immediately on merge then you should use GitHub's `Automatically delete head branches` feature in your repository settings.
|
||||
|
||||
#### Proxy support
|
||||
| `draft` | Create a [draft pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests#draft-pull-requests). It is not possible to change draft status after creation except through the web interface. | `false` |
|
||||
|
||||
For self-hosted runners behind a corporate proxy set the `https_proxy` environment variable.
|
||||
```yml
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
env:
|
||||
https_proxy: http://<proxy_address>:<port>
|
||||
```
|
||||
|
@ -142,10 +86,8 @@ The following outputs can be used by subsequent workflow steps.
|
|||
|
||||
- `pull-request-number` - The pull request number.
|
||||
- `pull-request-url` - The URL of the pull request.
|
||||
- `pull-request-operation` - The pull request operation performed by the action, `created`, `updated`, `closed` or `none`.
|
||||
- `pull-request-operation` - The pull request operation performed by the action, `created`, `updated` or `closed`.
|
||||
- `pull-request-head-sha` - The commit SHA of the pull request branch.
|
||||
- `pull-request-branch` - The branch name of the pull request.
|
||||
- `pull-request-commits-verified` - Whether GitHub considers the signature of the branch's commits to be verified; `true` or `false`.
|
||||
|
||||
Step outputs can be accessed as in the following example.
|
||||
Note that in order to read the step outputs the action step must have an id.
|
||||
|
@ -153,7 +95,7 @@ Note that in order to read the step outputs the action step must have an id.
|
|||
```yml
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
- name: Check outputs
|
||||
if: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
run: |
|
||||
|
@ -172,7 +114,7 @@ How the action behaves:
|
|||
- If there are changes (i.e. a diff exists with the checked-out base branch), the changes will be pushed to a new `branch` and a pull request created.
|
||||
- If there are no changes (i.e. no diff exists with the checked-out base branch), no pull request will be created and the action exits silently.
|
||||
- If a pull request already exists it will be updated if necessary. Local changes in the Actions workspace, or changes on the base branch, can cause an update. If no update is required the action exits silently.
|
||||
- If a pull request exists and new changes on the base branch make the pull request unnecessary (i.e. there is no longer a diff between the pull request branch and the base), the pull request is automatically closed. Additionally, if [`delete-branch`](#delete-branch) is set to `true` the `branch` will be deleted.
|
||||
- If a pull request exists and new changes on the base branch make the pull request unnecessary (i.e. there is no longer a diff between the pull request branch and the base), the pull request is automatically closed. Additionally, if `delete-branch` is set to `true` the `branch` will be deleted.
|
||||
|
||||
For further details about how the action works and usage guidelines, see [Concepts, guidelines and advanced usage](docs/concepts-guidelines.md).
|
||||
|
||||
|
@ -212,11 +154,11 @@ If there are files or directories you want to ignore you can simply add them to
|
|||
|
||||
You can control which files are committed with the `add-paths` input.
|
||||
Paths should follow git's [pathspec](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefpathspecapathspec) syntax.
|
||||
File changes that do not match one of the paths will be stashed and restored after the action has completed.
|
||||
All file changes that do not match one of the paths will be discarded.
|
||||
|
||||
```yml
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
add-paths: |
|
||||
*.java
|
||||
|
@ -230,7 +172,7 @@ Note that the repository must be checked out on a branch with a remote, it won't
|
|||
|
||||
```yml
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Create commits
|
||||
run: |
|
||||
git config user.name 'Peter Evans'
|
||||
|
@ -243,7 +185,25 @@ Note that the repository must be checked out on a branch with a remote, it won't
|
|||
- name: Uncommitted change
|
||||
run: date +%s > report.txt
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
```
|
||||
|
||||
### Create a project card
|
||||
|
||||
To create a project card for the pull request, pass the `pull-request-number` step output to [create-or-update-project-card](https://github.com/peter-evans/create-or-update-project-card) action.
|
||||
|
||||
```yml
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
|
||||
- name: Create or Update Project Card
|
||||
if: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
uses: peter-evans/create-or-update-project-card@v2
|
||||
with:
|
||||
project-name: My project
|
||||
column-name: My column
|
||||
issue-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
```
|
||||
|
||||
### Auto-merge
|
||||
|
@ -263,19 +223,19 @@ jobs:
|
|||
createPullRequest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Make changes to pull request
|
||||
run: date +%s > report.txt
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
commit-message: Update report
|
||||
committer: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
||||
author: ${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>
|
||||
committer: GitHub <noreply@github.com>
|
||||
author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
|
||||
signoff: false
|
||||
branch: example-patches
|
||||
delete-branch: true
|
||||
|
@ -292,8 +252,8 @@ jobs:
|
|||
assignees: peter-evans
|
||||
reviewers: peter-evans
|
||||
team-reviewers: |
|
||||
developers
|
||||
qa-team
|
||||
owners
|
||||
maintainers
|
||||
milestone: 1
|
||||
draft: false
|
||||
```
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
import {
|
||||
createOrUpdateBranch,
|
||||
tryFetch,
|
||||
getWorkingBaseAndType,
|
||||
buildBranchCommits
|
||||
getWorkingBaseAndType
|
||||
} from '../lib/create-or-update-branch'
|
||||
import * as fs from 'fs'
|
||||
import {GitCommandManager} from '../lib/git-command-manager'
|
||||
import * as path from 'path'
|
||||
import {v4 as uuidv4} from 'uuid'
|
||||
|
||||
const REPO_PATH = '/git/local/repos/test-base'
|
||||
const REPO_PATH = '/git/local/test-base'
|
||||
const REMOTE_NAME = 'origin'
|
||||
|
||||
const TRACKED_FILE = 'a/tracked-file.txt'
|
||||
|
@ -23,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/repos/test-fork.git'
|
||||
const FORK_REMOTE_URL = 'git://127.0.0.1/test-fork.git'
|
||||
const FORK_REMOTE_NAME = 'fork'
|
||||
|
||||
const ADD_PATHS_DEFAULT = []
|
||||
|
@ -141,22 +140,10 @@ describe('create-or-update-branch tests', () => {
|
|||
})
|
||||
|
||||
async function beforeTest(): Promise<void> {
|
||||
await git.fetch(
|
||||
[`${DEFAULT_BRANCH}:${DEFAULT_BRANCH}`],
|
||||
REMOTE_NAME,
|
||||
['--force', '--update-head-ok'],
|
||||
true
|
||||
)
|
||||
await git.checkout(DEFAULT_BRANCH)
|
||||
}
|
||||
|
||||
async function afterTest(deleteRemote = true): Promise<void> {
|
||||
await git.fetch(
|
||||
[`${DEFAULT_BRANCH}:${DEFAULT_BRANCH}`],
|
||||
REMOTE_NAME,
|
||||
['--force', '--update-head-ok'],
|
||||
true
|
||||
)
|
||||
await git.checkout(DEFAULT_BRANCH)
|
||||
try {
|
||||
// Get the upstream branch if it exists
|
||||
|
@ -211,8 +198,8 @@ describe('create-or-update-branch tests', () => {
|
|||
}
|
||||
|
||||
it('tests if a branch exists and can be fetched', async () => {
|
||||
expect(await tryFetch(git, REMOTE_NAME, NOT_BASE_BRANCH, 1)).toBeTruthy()
|
||||
expect(await tryFetch(git, REMOTE_NAME, NOT_EXIST_BRANCH, 1)).toBeFalsy()
|
||||
expect(await tryFetch(git, REMOTE_NAME, NOT_BASE_BRANCH)).toBeTruthy()
|
||||
expect(await tryFetch(git, REMOTE_NAME, NOT_EXIST_BRANCH)).toBeFalsy()
|
||||
})
|
||||
|
||||
it('tests getWorkingBaseAndType on a checked out ref', async () => {
|
||||
|
@ -230,88 +217,6 @@ describe('create-or-update-branch tests', () => {
|
|||
expect(workingBaseType).toEqual('commit')
|
||||
})
|
||||
|
||||
it('tests buildBranchCommits with no diff', async () => {
|
||||
await git.checkout(BRANCH, BASE)
|
||||
const branchCommits = await buildBranchCommits(git, BASE, BRANCH)
|
||||
expect(branchCommits.length).toEqual(0)
|
||||
})
|
||||
|
||||
it('tests buildBranchCommits with addition and modification', async () => {
|
||||
await git.checkout(BRANCH, BASE)
|
||||
await createChanges()
|
||||
const UNTRACKED_EXE_FILE = 'a/script.sh'
|
||||
const filepath = path.join(REPO_PATH, UNTRACKED_EXE_FILE)
|
||||
await fs.promises.writeFile(filepath, '#!/usr/bin/env bash', {mode: 0o755})
|
||||
await git.exec(['add', '-A'])
|
||||
await git.commit(['-m', 'Test changes'])
|
||||
|
||||
const branchCommits = await buildBranchCommits(git, BASE, BRANCH)
|
||||
|
||||
expect(branchCommits.length).toEqual(1)
|
||||
expect(branchCommits[0].subject).toEqual('Test changes')
|
||||
expect(branchCommits[0].changes.length).toEqual(3)
|
||||
expect(branchCommits[0].changes[0].mode).toEqual('100755')
|
||||
expect(branchCommits[0].changes[0].path).toEqual(UNTRACKED_EXE_FILE)
|
||||
expect(branchCommits[0].changes[0].status).toEqual('A')
|
||||
expect(branchCommits[0].changes[1].mode).toEqual('100644')
|
||||
expect(branchCommits[0].changes[1].path).toEqual(TRACKED_FILE)
|
||||
expect(branchCommits[0].changes[1].status).toEqual('M')
|
||||
expect(branchCommits[0].changes[2].mode).toEqual('100644')
|
||||
expect(branchCommits[0].changes[2].path).toEqual(UNTRACKED_FILE)
|
||||
expect(branchCommits[0].changes[2].status).toEqual('A')
|
||||
})
|
||||
|
||||
it('tests buildBranchCommits with addition and deletion', async () => {
|
||||
await git.checkout(BRANCH, BASE)
|
||||
await createChanges()
|
||||
const TRACKED_FILE_NEW_PATH = 'c/tracked-file.txt'
|
||||
const filepath = path.join(REPO_PATH, TRACKED_FILE_NEW_PATH)
|
||||
await fs.promises.mkdir(path.dirname(filepath), {recursive: true})
|
||||
await fs.promises.rename(path.join(REPO_PATH, TRACKED_FILE), filepath)
|
||||
await git.exec(['add', '-A'])
|
||||
await git.commit(['-m', 'Test changes'])
|
||||
|
||||
const branchCommits = await buildBranchCommits(git, BASE, BRANCH)
|
||||
|
||||
expect(branchCommits.length).toEqual(1)
|
||||
expect(branchCommits[0].subject).toEqual('Test changes')
|
||||
expect(branchCommits[0].changes.length).toEqual(3)
|
||||
expect(branchCommits[0].changes[0].mode).toEqual('100644')
|
||||
expect(branchCommits[0].changes[0].path).toEqual(TRACKED_FILE)
|
||||
expect(branchCommits[0].changes[0].status).toEqual('D')
|
||||
expect(branchCommits[0].changes[1].mode).toEqual('100644')
|
||||
expect(branchCommits[0].changes[1].path).toEqual(UNTRACKED_FILE)
|
||||
expect(branchCommits[0].changes[1].status).toEqual('A')
|
||||
expect(branchCommits[0].changes[2].mode).toEqual('100644')
|
||||
expect(branchCommits[0].changes[2].path).toEqual(TRACKED_FILE_NEW_PATH)
|
||||
expect(branchCommits[0].changes[2].status).toEqual('A')
|
||||
})
|
||||
|
||||
it('tests buildBranchCommits with multiple commits', async () => {
|
||||
await git.checkout(BRANCH, BASE)
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await createChanges()
|
||||
await git.exec(['add', '-A'])
|
||||
await git.commit(['-m', `Test changes ${i}`])
|
||||
}
|
||||
|
||||
const branchCommits = await buildBranchCommits(git, BASE, BRANCH)
|
||||
|
||||
expect(branchCommits.length).toEqual(3)
|
||||
for (let i = 0; i < 3; i++) {
|
||||
expect(branchCommits[i].subject).toEqual(`Test changes ${i}`)
|
||||
expect(branchCommits[i].changes.length).toEqual(2)
|
||||
const untrackedFileStatus = i == 0 ? 'A' : 'M'
|
||||
|
||||
expect(branchCommits[i].changes[0].mode).toEqual('100644')
|
||||
expect(branchCommits[i].changes[0].path).toEqual(TRACKED_FILE)
|
||||
expect(branchCommits[i].changes[0].status).toEqual('M')
|
||||
expect(branchCommits[i].changes[1].mode).toEqual('100644')
|
||||
expect(branchCommits[i].changes[1].path).toEqual(UNTRACKED_FILE)
|
||||
expect(branchCommits[i].changes[1].status).toEqual(untrackedFileStatus)
|
||||
}
|
||||
})
|
||||
|
||||
it('tests no changes resulting in no new branch being created', async () => {
|
||||
const commitMessage = uuidv4()
|
||||
const result = await createOrUpdateBranch(
|
||||
|
@ -340,7 +245,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(trackedContent)
|
||||
expect(
|
||||
|
@ -369,7 +273,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeTruthy()
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(_trackedContent)
|
||||
|
@ -391,7 +294,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(untrackedContent)
|
||||
expect(
|
||||
|
@ -420,7 +322,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeTruthy()
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(_untrackedContent)
|
||||
|
@ -444,7 +345,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
|
@ -474,7 +374,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('not-updated')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
|
@ -496,7 +395,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
|
@ -534,7 +432,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeTruthy()
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(_changes.tracked)
|
||||
|
@ -566,7 +463,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
|
@ -595,7 +491,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeFalsy()
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(defaultTrackedContent)
|
||||
|
@ -617,7 +512,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
|
@ -658,7 +552,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeFalsy()
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(_changes.tracked)
|
||||
|
@ -668,76 +561,6 @@ describe('create-or-update-branch tests', () => {
|
|||
).toBeTruthy()
|
||||
})
|
||||
|
||||
it('tests create, commit with partial changes on the base, and update', async () => {
|
||||
// This is an edge case where the changes for a single commit are partially merged to the base
|
||||
|
||||
// Create tracked and untracked file changes
|
||||
const changes = await createChanges()
|
||||
const commitMessage = uuidv4()
|
||||
const result = await createOrUpdateBranch(
|
||||
git,
|
||||
commitMessage,
|
||||
'',
|
||||
BRANCH,
|
||||
REMOTE_NAME,
|
||||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
expect(
|
||||
await gitLogMatches([commitMessage, INIT_COMMIT_MESSAGE])
|
||||
).toBeTruthy()
|
||||
|
||||
// Push pull request branch to remote
|
||||
await git.push([
|
||||
'--force-with-lease',
|
||||
REMOTE_NAME,
|
||||
`HEAD:refs/heads/${BRANCH}`
|
||||
])
|
||||
|
||||
await afterTest(false)
|
||||
await beforeTest()
|
||||
|
||||
// Create a commit on the base with a partial merge of the changes
|
||||
await createFile(TRACKED_FILE, changes.tracked)
|
||||
const baseCommitMessage = uuidv4()
|
||||
await git.exec(['add', '-A'])
|
||||
await git.commit(['-m', baseCommitMessage])
|
||||
await git.push([
|
||||
'--force',
|
||||
REMOTE_NAME,
|
||||
`HEAD:refs/heads/${DEFAULT_BRANCH}`
|
||||
])
|
||||
|
||||
// Create the same tracked and untracked file changes
|
||||
const _changes = await createChanges(changes.tracked, changes.untracked)
|
||||
const _commitMessage = uuidv4()
|
||||
const _result = await createOrUpdateBranch(
|
||||
git,
|
||||
_commitMessage,
|
||||
'',
|
||||
BRANCH,
|
||||
REMOTE_NAME,
|
||||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeTruthy()
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(_changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(_changes.untracked)
|
||||
expect(
|
||||
await gitLogMatches([
|
||||
_commitMessage,
|
||||
baseCommitMessage,
|
||||
INIT_COMMIT_MESSAGE
|
||||
])
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
||||
it('tests create, squash merge, and update with identical changes', async () => {
|
||||
// Branches that have been squash merged appear to have a diff with the base due to
|
||||
// different commits for the same changes. To prevent creating pull requests
|
||||
|
@ -756,7 +579,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
|
@ -800,7 +622,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeFalsy()
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(_changes.tracked)
|
||||
|
@ -826,7 +647,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
|
@ -865,7 +685,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeTruthy()
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(_changes.tracked)
|
||||
|
@ -888,7 +707,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(commits.changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(
|
||||
|
@ -920,7 +738,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeTruthy()
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(_commits.changes.tracked)
|
||||
|
@ -947,7 +764,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
|
@ -983,7 +799,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeTruthy()
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(_changes.tracked)
|
||||
|
@ -1012,7 +827,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
|
@ -1056,7 +870,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeTruthy()
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(_changes.tracked)
|
||||
|
@ -1084,7 +897,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
|
@ -1114,7 +926,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeTruthy()
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(_changes.tracked)
|
||||
|
@ -1137,7 +948,6 @@ describe('create-or-update-branch tests', () => {
|
|||
true,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
|
@ -1174,7 +984,6 @@ describe('create-or-update-branch tests', () => {
|
|||
true,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeTruthy()
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(_changes.tracked)
|
||||
|
@ -1204,7 +1013,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_MULTI
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
|
@ -1234,7 +1042,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_MULTI
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeTruthy()
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(_changes.tracked)
|
||||
|
@ -1257,7 +1064,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_WILDCARD
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
|
@ -1287,7 +1093,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_WILDCARD
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeTruthy()
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(_changes.tracked)
|
||||
|
@ -1310,55 +1115,10 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
['nonexistent/*']
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('none')
|
||||
expect(await gitLogMatches([INIT_COMMIT_MESSAGE])).toBeTruthy()
|
||||
})
|
||||
|
||||
it('tests create consecutive branches with restored changes from stash', async () => {
|
||||
const BRANCHA = `${BRANCH}-a`
|
||||
const BRANCHB = `${BRANCH}-b`
|
||||
|
||||
// Create tracked and untracked file changes
|
||||
const changes = await createChanges()
|
||||
const commitMessage = uuidv4()
|
||||
const resultA = await createOrUpdateBranch(
|
||||
git,
|
||||
commitMessage,
|
||||
'',
|
||||
BRANCHA,
|
||||
REMOTE_NAME,
|
||||
false,
|
||||
['a']
|
||||
)
|
||||
const resultB = await createOrUpdateBranch(
|
||||
git,
|
||||
commitMessage,
|
||||
'',
|
||||
BRANCHB,
|
||||
REMOTE_NAME,
|
||||
false,
|
||||
['b']
|
||||
)
|
||||
await git.checkout(BRANCHA)
|
||||
expect(resultA.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(
|
||||
await gitLogMatches([commitMessage, INIT_COMMIT_MESSAGE])
|
||||
).toBeTruthy()
|
||||
await git.checkout(BRANCHB)
|
||||
expect(resultB.action).toEqual('created')
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
expect(
|
||||
await gitLogMatches([commitMessage, INIT_COMMIT_MESSAGE])
|
||||
).toBeTruthy()
|
||||
|
||||
// Delete the local branches
|
||||
await git.checkout(DEFAULT_BRANCH)
|
||||
await git.exec(['branch', '--delete', '--force', BRANCHA])
|
||||
await git.exec(['branch', '--delete', '--force', BRANCHB])
|
||||
})
|
||||
|
||||
// Working Base is Not Base (WBNB)
|
||||
|
||||
it('tests no changes resulting in no new branch being created (WBNB)', async () => {
|
||||
|
@ -1375,7 +1135,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('none')
|
||||
expect(await gitLogMatches([INIT_COMMIT_MESSAGE])).toBeTruthy()
|
||||
})
|
||||
|
@ -1396,7 +1155,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(trackedContent)
|
||||
expect(
|
||||
|
@ -1428,7 +1186,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeTruthy()
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(_trackedContent)
|
||||
|
@ -1453,7 +1210,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(untrackedContent)
|
||||
expect(
|
||||
|
@ -1485,7 +1241,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeTruthy()
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(_untrackedContent)
|
||||
|
@ -1512,7 +1267,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
|
@ -1545,7 +1299,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('not-updated')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
|
@ -1570,7 +1323,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
|
@ -1611,7 +1363,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeTruthy()
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(_changes.tracked)
|
||||
|
@ -1619,7 +1370,8 @@ describe('create-or-update-branch tests', () => {
|
|||
expect(
|
||||
await gitLogMatches([
|
||||
_commitMessage,
|
||||
commits.commitMsgs[0] // fetch depth of base is 1
|
||||
...commits.commitMsgs,
|
||||
INIT_COMMIT_MESSAGE
|
||||
])
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
@ -1645,7 +1397,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
|
@ -1677,7 +1428,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeFalsy()
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(defaultTrackedContent)
|
||||
|
@ -1704,7 +1454,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
|
@ -1748,90 +1497,12 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeFalsy()
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(_changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(_changes.untracked)
|
||||
expect(
|
||||
await gitLogMatches([
|
||||
commits.commitMsgs[0] // fetch depth of base is 1
|
||||
])
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
||||
it('tests create, commit with partial changes on the base, and update (WBNB)', async () => {
|
||||
// This is an edge case where the changes for a single commit are partially merged to the base
|
||||
|
||||
// Set the working base to a branch that is not the pull request base
|
||||
await git.checkout(NOT_BASE_BRANCH)
|
||||
|
||||
// Create tracked and untracked file changes
|
||||
const changes = await createChanges()
|
||||
const commitMessage = uuidv4()
|
||||
const result = await createOrUpdateBranch(
|
||||
git,
|
||||
commitMessage,
|
||||
BASE,
|
||||
BRANCH,
|
||||
REMOTE_NAME,
|
||||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
expect(
|
||||
await gitLogMatches([commitMessage, INIT_COMMIT_MESSAGE])
|
||||
).toBeTruthy()
|
||||
|
||||
// Push pull request branch to remote
|
||||
await git.push([
|
||||
'--force-with-lease',
|
||||
REMOTE_NAME,
|
||||
`HEAD:refs/heads/${BRANCH}`
|
||||
])
|
||||
|
||||
await afterTest(false)
|
||||
await beforeTest()
|
||||
|
||||
// Create a commit on the base with a partial merge of the changes
|
||||
await createFile(TRACKED_FILE, changes.tracked)
|
||||
const baseCommitMessage = uuidv4()
|
||||
await git.exec(['add', '-A'])
|
||||
await git.commit(['-m', baseCommitMessage])
|
||||
await git.push([
|
||||
'--force',
|
||||
REMOTE_NAME,
|
||||
`HEAD:refs/heads/${DEFAULT_BRANCH}`
|
||||
])
|
||||
|
||||
// Set the working base to a branch that is not the pull request base
|
||||
await git.checkout(NOT_BASE_BRANCH)
|
||||
|
||||
// Create the same tracked and untracked file changes
|
||||
const _changes = await createChanges(changes.tracked, changes.untracked)
|
||||
const _commitMessage = uuidv4()
|
||||
const _result = await createOrUpdateBranch(
|
||||
git,
|
||||
_commitMessage,
|
||||
BASE,
|
||||
BRANCH,
|
||||
REMOTE_NAME,
|
||||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeTruthy()
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(_changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(_changes.untracked)
|
||||
expect(
|
||||
await gitLogMatches([
|
||||
_commitMessage,
|
||||
baseCommitMessage // fetch depth of base is 1
|
||||
])
|
||||
await gitLogMatches([...commits.commitMsgs, INIT_COMMIT_MESSAGE])
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
||||
|
@ -1856,7 +1527,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
|
@ -1903,15 +1573,12 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeFalsy()
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(_changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(_changes.untracked)
|
||||
expect(
|
||||
await gitLogMatches([
|
||||
commits.commitMsgs[0] // fetch depth of base is 1
|
||||
])
|
||||
await gitLogMatches([...commits.commitMsgs, INIT_COMMIT_MESSAGE])
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
||||
|
@ -1934,7 +1601,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
|
@ -1976,7 +1642,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeTruthy()
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(_changes.tracked)
|
||||
|
@ -2002,7 +1667,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(commits.changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(
|
||||
|
@ -2037,7 +1701,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeTruthy()
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(_commits.changes.tracked)
|
||||
|
@ -2067,7 +1730,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
|
@ -2106,7 +1768,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeTruthy()
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(_changes.tracked)
|
||||
|
@ -2138,7 +1799,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
|
@ -2185,7 +1845,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeTruthy()
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(_changes.tracked)
|
||||
|
@ -2194,7 +1853,8 @@ describe('create-or-update-branch tests', () => {
|
|||
await gitLogMatches([
|
||||
_commitMessage,
|
||||
..._commits.commitMsgs,
|
||||
commitsOnBase.commitMsgs[0] // fetch depth of base is 1
|
||||
...commitsOnBase.commitMsgs,
|
||||
INIT_COMMIT_MESSAGE
|
||||
])
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
@ -2215,7 +1875,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
|
@ -2248,7 +1907,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeTruthy()
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(_changes.tracked)
|
||||
|
@ -2278,7 +1936,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
|
@ -2312,7 +1969,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeTruthy()
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(_changes.tracked)
|
||||
|
@ -2339,7 +1995,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(result.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
|
@ -2381,7 +2036,6 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
expect(_result.action).toEqual('updated')
|
||||
expect(_result.hasDiffWithBase).toBeTruthy()
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(_changes.tracked)
|
||||
|
@ -2389,7 +2043,8 @@ describe('create-or-update-branch tests', () => {
|
|||
expect(
|
||||
await gitLogMatches([
|
||||
_commitMessage,
|
||||
commitsOnBase.commitMsgs[0] // fetch depth of base is 1
|
||||
...commitsOnBase.commitMsgs,
|
||||
INIT_COMMIT_MESSAGE
|
||||
])
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
@ -2413,56 +2068,7 @@ describe('create-or-update-branch tests', () => {
|
|||
false,
|
||||
ADD_PATHS_DEFAULT
|
||||
)
|
||||
await git.checkout(BRANCH)
|
||||
// The action cannot successfully create the branch
|
||||
expect(result.action).toEqual('none')
|
||||
})
|
||||
|
||||
it('tests create consecutive branches with restored changes from stash in detached HEAD state (WBNR)', async () => {
|
||||
// Checkout the HEAD commit SHA
|
||||
const headSha = await git.revParse('HEAD')
|
||||
await git.checkout(headSha)
|
||||
|
||||
const BRANCHA = `${BRANCH}-a`
|
||||
const BRANCHB = `${BRANCH}-b`
|
||||
|
||||
// Create tracked and untracked file changes
|
||||
const changes = await createChanges()
|
||||
const commitMessage = uuidv4()
|
||||
const resultA = await createOrUpdateBranch(
|
||||
git,
|
||||
commitMessage,
|
||||
BASE,
|
||||
BRANCHA,
|
||||
REMOTE_NAME,
|
||||
false,
|
||||
['a']
|
||||
)
|
||||
const resultB = await createOrUpdateBranch(
|
||||
git,
|
||||
commitMessage,
|
||||
BASE,
|
||||
BRANCHB,
|
||||
REMOTE_NAME,
|
||||
false,
|
||||
['b']
|
||||
)
|
||||
await git.checkout(BRANCHA)
|
||||
expect(resultA.action).toEqual('created')
|
||||
expect(await getFileContent(TRACKED_FILE)).toEqual(changes.tracked)
|
||||
expect(
|
||||
await gitLogMatches([commitMessage, INIT_COMMIT_MESSAGE])
|
||||
).toBeTruthy()
|
||||
await git.checkout(BRANCHB)
|
||||
expect(resultB.action).toEqual('created')
|
||||
expect(await getFileContent(UNTRACKED_FILE)).toEqual(changes.untracked)
|
||||
expect(
|
||||
await gitLogMatches([commitMessage, INIT_COMMIT_MESSAGE])
|
||||
).toBeTruthy()
|
||||
|
||||
// Delete the local branches
|
||||
await git.checkout(DEFAULT_BRANCH)
|
||||
await git.exec(['branch', '--delete', '--force', BRANCHA])
|
||||
await git.exec(['branch', '--delete', '--force', BRANCHB])
|
||||
})
|
||||
})
|
||||
|
|
|
@ -5,31 +5,23 @@ set -euo pipefail
|
|||
WORKINGDIR=$PWD
|
||||
|
||||
# Create and serve a remote repo
|
||||
mkdir -p /git/remote/repos
|
||||
mkdir -p /git/remote
|
||||
git config --global init.defaultBranch main
|
||||
git init --bare /git/remote/repos/test-base.git
|
||||
git init --bare /git/remote/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 initial commits
|
||||
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
|
||||
# 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
|
||||
git config --global user.email "you@example.com"
|
||||
git config --global user.name "Your Name"
|
||||
echo "#test-base" > README_TEMP.md
|
||||
echo "#test-base" > README.md
|
||||
git add .
|
||||
git commit -m "initial commit"
|
||||
git commit --allow-empty -m "empty commit for tests"
|
||||
echo "#test-base :sparkles:" > README_TEMP.md
|
||||
git add .
|
||||
git commit -m "add sparkles" -m "Change description:
|
||||
- updates README_TEMP.md to add sparkles to the title"
|
||||
mv README_TEMP.md README.md
|
||||
git add .
|
||||
git commit -m "rename readme"
|
||||
git push -u
|
||||
git log -1 --pretty=oneline
|
||||
git config --global --unset user.email
|
||||
|
@ -38,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/repos/test-base.git /git/remote/repos/test-fork.git
|
||||
cd /git/remote/repos/test-fork.git
|
||||
git clone --mirror git://127.0.0.1/test-base.git /git/remote/test-fork.git
|
||||
cd /git/remote/test-fork.git
|
||||
git log -1 --pretty=oneline
|
||||
|
||||
# Restore the working directory
|
||||
|
|
49
__test__/git-auth-helper.int.test.ts
Normal file
49
__test__/git-auth-helper.int.test.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import {GitCommandManager} from '../lib/git-command-manager'
|
||||
import {GitAuthHelper} from '../lib/git-auth-helper'
|
||||
|
||||
const REPO_PATH = '/git/local/test-base'
|
||||
|
||||
const extraheaderConfigKey = 'http.https://github.com/.extraheader'
|
||||
|
||||
describe('git-auth-helper tests', () => {
|
||||
let git: GitCommandManager
|
||||
let gitAuthHelper: GitAuthHelper
|
||||
|
||||
beforeAll(async () => {
|
||||
git = await GitCommandManager.create(REPO_PATH)
|
||||
gitAuthHelper = new GitAuthHelper(git)
|
||||
})
|
||||
|
||||
it('tests save and restore with no persisted auth', async () => {
|
||||
await gitAuthHelper.savePersistedAuth()
|
||||
await gitAuthHelper.restorePersistedAuth()
|
||||
})
|
||||
|
||||
it('tests configure and removal of auth', async () => {
|
||||
await gitAuthHelper.configureToken('github-token')
|
||||
expect(await git.configExists(extraheaderConfigKey)).toBeTruthy()
|
||||
expect(await git.getConfigValue(extraheaderConfigKey)).toEqual(
|
||||
'AUTHORIZATION: basic eC1hY2Nlc3MtdG9rZW46Z2l0aHViLXRva2Vu'
|
||||
)
|
||||
|
||||
await gitAuthHelper.removeAuth()
|
||||
expect(await git.configExists(extraheaderConfigKey)).toBeFalsy()
|
||||
})
|
||||
|
||||
it('tests save and restore of persisted auth', async () => {
|
||||
const extraheaderConfigValue = 'AUTHORIZATION: basic ***persisted-auth***'
|
||||
await git.config(extraheaderConfigKey, extraheaderConfigValue)
|
||||
|
||||
await gitAuthHelper.savePersistedAuth()
|
||||
|
||||
const exists = await git.configExists(extraheaderConfigKey)
|
||||
expect(exists).toBeFalsy()
|
||||
|
||||
await gitAuthHelper.restorePersistedAuth()
|
||||
|
||||
const configValue = await git.getConfigValue(extraheaderConfigKey)
|
||||
expect(configValue).toEqual(extraheaderConfigValue)
|
||||
|
||||
await gitAuthHelper.removeAuth()
|
||||
})
|
||||
})
|
|
@ -1,48 +0,0 @@
|
|||
import {GitCommandManager} from '../lib/git-command-manager'
|
||||
|
||||
const REPO_PATH = '/git/local/repos/test-base'
|
||||
|
||||
describe('git-command-manager integration tests', () => {
|
||||
let git: GitCommandManager
|
||||
|
||||
beforeAll(async () => {
|
||||
git = await GitCommandManager.create(REPO_PATH)
|
||||
await git.checkout('main')
|
||||
})
|
||||
|
||||
it('tests getCommit', async () => {
|
||||
const initialCommit = await git.getCommit('HEAD^^^')
|
||||
const emptyCommit = await git.getCommit('HEAD^^')
|
||||
const modifiedCommit = await git.getCommit('HEAD^')
|
||||
const headCommit = await git.getCommit('HEAD')
|
||||
|
||||
expect(initialCommit.subject).toEqual('initial commit')
|
||||
expect(initialCommit.signed).toBeFalsy()
|
||||
expect(initialCommit.changes[0].mode).toEqual('100644')
|
||||
expect(initialCommit.changes[0].status).toEqual('A')
|
||||
expect(initialCommit.changes[0].path).toEqual('README_TEMP.md')
|
||||
|
||||
expect(emptyCommit.subject).toEqual('empty commit for tests')
|
||||
expect(emptyCommit.tree).toEqual(initialCommit.tree) // empty commits have no tree and reference the parent's
|
||||
expect(emptyCommit.parents[0]).toEqual(initialCommit.sha)
|
||||
expect(emptyCommit.signed).toBeFalsy()
|
||||
expect(emptyCommit.changes).toEqual([])
|
||||
|
||||
expect(modifiedCommit.subject).toEqual('add sparkles')
|
||||
expect(modifiedCommit.parents[0]).toEqual(emptyCommit.sha)
|
||||
expect(modifiedCommit.signed).toBeFalsy()
|
||||
expect(modifiedCommit.changes[0].mode).toEqual('100644')
|
||||
expect(modifiedCommit.changes[0].status).toEqual('M')
|
||||
expect(modifiedCommit.changes[0].path).toEqual('README_TEMP.md')
|
||||
|
||||
expect(headCommit.subject).toEqual('rename readme')
|
||||
expect(headCommit.parents[0]).toEqual(modifiedCommit.sha)
|
||||
expect(headCommit.signed).toBeFalsy()
|
||||
expect(headCommit.changes[0].mode).toEqual('100644')
|
||||
expect(headCommit.changes[0].status).toEqual('A')
|
||||
expect(headCommit.changes[0].path).toEqual('README.md')
|
||||
expect(headCommit.changes[1].mode).toEqual('100644')
|
||||
expect(headCommit.changes[1].status).toEqual('D')
|
||||
expect(headCommit.changes[1].path).toEqual('README_TEMP.md')
|
||||
})
|
||||
})
|
|
@ -1,85 +0,0 @@
|
|||
import {GitCommandManager} from '../lib/git-command-manager'
|
||||
import {GitConfigHelper} from '../lib/git-config-helper'
|
||||
|
||||
const REPO_PATH = '/git/local/repos/test-base'
|
||||
|
||||
const extraheaderConfigKey = 'http.https://127.0.0.1/.extraheader'
|
||||
|
||||
describe('git-config-helper integration tests', () => {
|
||||
let git: GitCommandManager
|
||||
|
||||
beforeAll(async () => {
|
||||
git = await GitCommandManager.create(REPO_PATH)
|
||||
})
|
||||
|
||||
it('tests save and restore with no persisted auth', async () => {
|
||||
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.close()
|
||||
expect(await git.configExists(extraheaderConfigKey)).toBeFalsy()
|
||||
})
|
||||
|
||||
it('tests save and restore of persisted auth', async () => {
|
||||
const extraheaderConfigValue = 'AUTHORIZATION: basic ***persisted-auth***'
|
||||
await git.config(extraheaderConfigKey, extraheaderConfigValue)
|
||||
|
||||
const gitConfigHelper = await GitConfigHelper.create(git)
|
||||
|
||||
const exists = await git.configExists(extraheaderConfigKey)
|
||||
expect(exists).toBeFalsy()
|
||||
|
||||
await gitConfigHelper.close()
|
||||
|
||||
const configValue = await git.getConfigValue(extraheaderConfigKey)
|
||||
expect(configValue).toEqual(extraheaderConfigValue)
|
||||
|
||||
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 () => {
|
||||
const gitConfigHelper = await GitConfigHelper.create(git)
|
||||
|
||||
expect(
|
||||
await git.configExists('safe.directory', REPO_PATH, true)
|
||||
).toBeTruthy()
|
||||
|
||||
await gitConfigHelper.close()
|
||||
|
||||
expect(
|
||||
await git.configExists('safe.directory', REPO_PATH, true)
|
||||
).toBeFalsy()
|
||||
})
|
||||
})
|
|
@ -1,93 +0,0 @@
|
|||
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 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 {
|
||||
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`
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
|
@ -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:20-alpine
|
||||
FROM node:16-alpine
|
||||
RUN apk --no-cache add git git-daemon
|
||||
RUN npm install jest jest-environment-jsdom --global
|
||||
WORKDIR /cpr
|
||||
|
|
|
@ -25,18 +25,6 @@ describe('utils tests', () => {
|
|||
expect(array2.length).toEqual(0)
|
||||
})
|
||||
|
||||
test('stripOrgPrefixFromTeams strips org prefixes correctly', async () => {
|
||||
const array = utils.stripOrgPrefixFromTeams([
|
||||
'org/team1',
|
||||
'org/team2',
|
||||
'team3'
|
||||
])
|
||||
expect(array.length).toEqual(3)
|
||||
expect(array[0]).toEqual('team1')
|
||||
expect(array[1]).toEqual('team2')
|
||||
expect(array[2]).toEqual('team3')
|
||||
})
|
||||
|
||||
test('getRepoPath successfully returns the path to the repository', async () => {
|
||||
expect(utils.getRepoPath()).toEqual(process.env['GITHUB_WORKSPACE'])
|
||||
expect(utils.getRepoPath('foo')).toEqual(
|
||||
|
@ -44,6 +32,63 @@ 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',
|
||||
|
|
29
action.yml
29
action.yml
|
@ -2,12 +2,8 @@ name: 'Create Pull Request'
|
|||
description: 'Creates a pull request for changes to your repository in the actions workspace'
|
||||
inputs:
|
||||
token:
|
||||
description: 'The token that the action will use to create and update the pull request.'
|
||||
description: 'GITHUB_TOKEN or a `repo` scoped Personal Access Token (PAT)'
|
||||
default: ${{ github.token }}
|
||||
branch-token:
|
||||
description: >
|
||||
The token that the action will use to create and update the branch.
|
||||
Defaults to the value of `token`.
|
||||
path:
|
||||
description: >
|
||||
Relative path under $GITHUB_WORKSPACE to the repository.
|
||||
|
@ -24,12 +20,12 @@ inputs:
|
|||
description: >
|
||||
The committer name and email address in the format `Display Name <email@address.com>`.
|
||||
Defaults to the GitHub Actions bot user.
|
||||
default: 'github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>'
|
||||
default: 'GitHub <noreply@github.com>'
|
||||
author:
|
||||
description: >
|
||||
The author name and email address in the format `Display Name <email@address.com>`.
|
||||
Defaults to the user who triggered the workflow run.
|
||||
default: '${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>'
|
||||
default: '${{ github.actor }} <${{ 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
|
||||
|
@ -38,7 +34,8 @@ inputs:
|
|||
default: 'create-pull-request/patch'
|
||||
delete-branch:
|
||||
description: >
|
||||
Delete the `branch` if it doesn't have an active pull request associated with it.
|
||||
Delete the `branch` when closing pull requests, and when undeleted after merging.
|
||||
Recommend `true`.
|
||||
default: false
|
||||
branch-suffix:
|
||||
description: 'The branch suffix type when using the alternative branching strategy.'
|
||||
|
@ -51,17 +48,12 @@ inputs:
|
|||
A fork of the checked out parent repository to which the pull request branch will be pushed.
|
||||
e.g. `owner/repo-fork`.
|
||||
The pull request will be created to merge the fork's branch into the parent's base.
|
||||
sign-commits:
|
||||
description: 'Sign commits as `github-actions[bot]` when using `GITHUB_TOKEN`, or your own bot when using GitHub App tokens.'
|
||||
default: false
|
||||
title:
|
||||
description: 'The title of the pull request.'
|
||||
default: 'Changes by create-pull-request action'
|
||||
body:
|
||||
description: 'The body of the pull request.'
|
||||
default: 'Automated changes by [create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub action'
|
||||
body-path:
|
||||
description: 'The path to a file containing the pull request body. Takes precedence over `body`.'
|
||||
labels:
|
||||
description: 'A comma or newline separated list of labels.'
|
||||
assignees:
|
||||
|
@ -75,13 +67,8 @@ inputs:
|
|||
milestone:
|
||||
description: 'The number of the milestone to associate the pull request with.'
|
||||
draft:
|
||||
description: >
|
||||
Create a draft pull request.
|
||||
Valid values are `true` (only on create), `always-true` (on create and update), and `false`.
|
||||
description: 'Create a draft pull request. It is not possible to change draft status after creation except through the web interface'
|
||||
default: false
|
||||
maintainer-can-modify:
|
||||
description: 'Indicates whether maintainers can modify the pull request.'
|
||||
default: true
|
||||
outputs:
|
||||
pull-request-number:
|
||||
description: 'The pull request number'
|
||||
|
@ -91,10 +78,8 @@ outputs:
|
|||
description: 'The pull request operation performed by the action, `created`, `updated` or `closed`.'
|
||||
pull-request-head-sha:
|
||||
description: 'The commit SHA of the pull request branch.'
|
||||
pull-request-branch:
|
||||
description: 'The pull request branch name'
|
||||
runs:
|
||||
using: 'node20'
|
||||
using: 'node16'
|
||||
main: 'dist/index.js'
|
||||
branding:
|
||||
icon: 'git-pull-request'
|
||||
|
|
16
dist/790.index.js
vendored
16
dist/790.index.js
vendored
|
@ -1,16 +0,0 @@
|
|||
"use strict";
|
||||
exports.id = 790;
|
||||
exports.ids = [790];
|
||||
exports.modules = {
|
||||
|
||||
/***/ 790:
|
||||
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
|
||||
|
||||
var y=Object.defineProperty;var c=(R,o)=>y(R,"name",{value:o,configurable:!0});__webpack_require__(3024),__webpack_require__(6760);const node=__webpack_require__(117);__webpack_require__(7067),__webpack_require__(4708),__webpack_require__(8522),__webpack_require__(7075),__webpack_require__(4573),__webpack_require__(7975),__webpack_require__(7713),__webpack_require__(3136),__webpack_require__(7030);let s=0;const S={START_BOUNDARY:s++,HEADER_FIELD_START:s++,HEADER_FIELD:s++,HEADER_VALUE_START:s++,HEADER_VALUE:s++,HEADER_VALUE_ALMOST_DONE:s++,HEADERS_ALMOST_DONE:s++,PART_DATA_START:s++,PART_DATA:s++,END:s++};let f=1;const F={PART_BOUNDARY:f,LAST_BOUNDARY:f*=2},LF=10,CR=13,SPACE=32,HYPHEN=45,COLON=58,A=97,Z=122,lower=c(R=>R|32,"lower"),noop=c(()=>{},"noop"),g=class g{constructor(o){this.index=0,this.flags=0,this.onHeaderEnd=noop,this.onHeaderField=noop,this.onHeadersEnd=noop,this.onHeaderValue=noop,this.onPartBegin=noop,this.onPartData=noop,this.onPartEnd=noop,this.boundaryChars={},o=`\r
|
||||
--`+o;const t=new Uint8Array(o.length);for(let n=0;n<o.length;n++)t[n]=o.charCodeAt(n),this.boundaryChars[t[n]]=!0;this.boundary=t,this.lookbehind=new Uint8Array(this.boundary.length+8),this.state=S.START_BOUNDARY}write(o){let t=0;const n=o.length;let E=this.index,{lookbehind:l,boundary:h,boundaryChars:H,index:e,state:a,flags:d}=this;const b=this.boundary.length,m=b-1,O=o.length;let r,P;const u=c(D=>{this[D+"Mark"]=t},"mark"),i=c(D=>{delete this[D+"Mark"]},"clear"),T=c((D,p,_,N)=>{(p===void 0||p!==_)&&this[D](N&&N.subarray(p,_))},"callback"),L=c((D,p)=>{const _=D+"Mark";_ in this&&(p?(T(D,this[_],t,o),delete this[_]):(T(D,this[_],o.length,o),this[_]=0))},"dataCallback");for(t=0;t<n;t++)switch(r=o[t],a){case S.START_BOUNDARY:if(e===h.length-2){if(r===HYPHEN)d|=F.LAST_BOUNDARY;else if(r!==CR)return;e++;break}else if(e-1===h.length-2){if(d&F.LAST_BOUNDARY&&r===HYPHEN)a=S.END,d=0;else if(!(d&F.LAST_BOUNDARY)&&r===LF)e=0,T("onPartBegin"),a=S.HEADER_FIELD_START;else return;break}r!==h[e+2]&&(e=-2),r===h[e+2]&&e++;break;case S.HEADER_FIELD_START:a=S.HEADER_FIELD,u("onHeaderField"),e=0;case S.HEADER_FIELD:if(r===CR){i("onHeaderField"),a=S.HEADERS_ALMOST_DONE;break}if(e++,r===HYPHEN)break;if(r===COLON){if(e===1)return;L("onHeaderField",!0),a=S.HEADER_VALUE_START;break}if(P=lower(r),P<A||P>Z)return;break;case S.HEADER_VALUE_START:if(r===SPACE)break;u("onHeaderValue"),a=S.HEADER_VALUE;case S.HEADER_VALUE:r===CR&&(L("onHeaderValue",!0),T("onHeaderEnd"),a=S.HEADER_VALUE_ALMOST_DONE);break;case S.HEADER_VALUE_ALMOST_DONE:if(r!==LF)return;a=S.HEADER_FIELD_START;break;case S.HEADERS_ALMOST_DONE:if(r!==LF)return;T("onHeadersEnd"),a=S.PART_DATA_START;break;case S.PART_DATA_START:a=S.PART_DATA,u("onPartData");case S.PART_DATA:if(E=e,e===0){for(t+=m;t<O&&!(o[t]in H);)t+=b;t-=m,r=o[t]}if(e<h.length)h[e]===r?(e===0&&L("onPartData",!0),e++):e=0;else if(e===h.length)e++,r===CR?d|=F.PART_BOUNDARY:r===HYPHEN?d|=F.LAST_BOUNDARY:e=0;else if(e-1===h.length)if(d&F.PART_BOUNDARY){if(e=0,r===LF){d&=~F.PART_BOUNDARY,T("onPartEnd"),T("onPartBegin"),a=S.HEADER_FIELD_START;break}}else d&F.LAST_BOUNDARY&&r===HYPHEN?(T("onPartEnd"),a=S.END,d=0):e=0;if(e>0)l[e-1]=r;else if(E>0){const D=new Uint8Array(l.buffer,l.byteOffset,l.byteLength);T("onPartData",0,E,D),E=0,u("onPartData"),t--}break;case S.END:break;default:throw new Error(`Unexpected state entered: ${a}`)}L("onHeaderField"),L("onHeaderValue"),L("onPartData"),this.index=e,this.state=a,this.flags=d}end(){if(this.state===S.HEADER_FIELD_START&&this.index===0||this.state===S.PART_DATA&&this.index===this.boundary.length)this.onPartEnd();else if(this.state!==S.END)throw new Error("MultipartParser.end(): stream ended unexpectedly")}};c(g,"MultipartParser");let MultipartParser=g;function _fileName(R){const o=R.match(/\bfilename=("(.*?)"|([^()<>@,;:\\"/[\]?={}\s\t]+))($|;\s)/i);if(!o)return;const t=o[2]||o[3]||"";let n=t.slice(t.lastIndexOf("\\")+1);return n=n.replace(/%22/g,'"'),n=n.replace(/&#(\d{4});/g,(E,l)=>String.fromCharCode(l)),n}c(_fileName,"_fileName");async function toFormData(R,o){if(!/multipart/i.test(o))throw new TypeError("Failed to fetch");const t=o.match(/boundary=(?:"([^"]+)"|([^;]+))/i);if(!t)throw new TypeError("no or bad content-type header, no multipart boundary");const n=new MultipartParser(t[1]||t[2]);let E,l,h,H,e,a;const d=[],b=new node.FormData,m=c(i=>{h+=u.decode(i,{stream:!0})},"onPartData"),O=c(i=>{d.push(i)},"appendToFile"),r=c(()=>{const i=new node.File(d,a,{type:e});b.append(H,i)},"appendFileToFormData"),P=c(()=>{b.append(H,h)},"appendEntryToFormData"),u=new TextDecoder("utf-8");u.decode(),n.onPartBegin=function(){n.onPartData=m,n.onPartEnd=P,E="",l="",h="",H="",e="",a=null,d.length=0},n.onHeaderField=function(i){E+=u.decode(i,{stream:!0})},n.onHeaderValue=function(i){l+=u.decode(i,{stream:!0})},n.onHeaderEnd=function(){if(l+=u.decode(),E=E.toLowerCase(),E==="content-disposition"){const i=l.match(/\bname=("([^"]*)"|([^()<>@,;:\\"/[\]?={}\s\t]+))/i);i&&(H=i[2]||i[3]||""),a=_fileName(l),a&&(n.onPartData=O,n.onPartEnd=r)}else E==="content-type"&&(e=l);l="",E=""};for await(const i of R)n.write(i);return n.end(),b}c(toFormData,"toFormData"),exports.toFormData=toFormData;
|
||||
|
||||
|
||||
/***/ })
|
||||
|
||||
};
|
||||
;
|
1010
dist/bridge.js
vendored
Normal file
1010
dist/bridge.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
977
dist/events.js
vendored
Normal file
977
dist/events.js
vendored
Normal file
|
@ -0,0 +1,977 @@
|
|||
// Copyright Joyent, Inc. and other Node contributors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
// persons to whom the Software is furnished to do so, subject to the
|
||||
// following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included
|
||||
// in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
// USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
// Modified by the vm2 team to make this a standalone module to be loaded into the sandbox.
|
||||
|
||||
'use strict';
|
||||
|
||||
const host = fromhost;
|
||||
|
||||
const {
|
||||
Boolean,
|
||||
Error,
|
||||
String,
|
||||
Symbol
|
||||
} = globalThis;
|
||||
|
||||
const ReflectApply = Reflect.apply;
|
||||
const ReflectOwnKeys = Reflect.ownKeys;
|
||||
|
||||
const ErrorCaptureStackTrace = Error.captureStackTrace;
|
||||
|
||||
const NumberIsNaN = Number.isNaN;
|
||||
|
||||
const ObjectCreate = Object.create;
|
||||
const ObjectDefineProperty = Object.defineProperty;
|
||||
const ObjectDefineProperties = Object.defineProperties;
|
||||
const ObjectGetPrototypeOf = Object.getPrototypeOf;
|
||||
|
||||
const SymbolFor = Symbol.for;
|
||||
|
||||
function uncurryThis(func) {
|
||||
return (thiz, ...args) => ReflectApply(func, thiz, args);
|
||||
}
|
||||
|
||||
const ArrayPrototypeIndexOf = uncurryThis(Array.prototype.indexOf);
|
||||
const ArrayPrototypeJoin = uncurryThis(Array.prototype.join);
|
||||
const ArrayPrototypeSlice = uncurryThis(Array.prototype.slice);
|
||||
const ArrayPrototypeSplice = uncurryThis(Array.prototype.splice);
|
||||
const ArrayPrototypeUnshift = uncurryThis(Array.prototype.unshift);
|
||||
|
||||
const kRejection = SymbolFor('nodejs.rejection');
|
||||
|
||||
function inspect(obj) {
|
||||
return typeof obj === 'symbol' ? obj.toString() : `${obj}`;
|
||||
}
|
||||
|
||||
function spliceOne(list, index) {
|
||||
for (; index + 1 < list.length; index++)
|
||||
list[index] = list[index + 1];
|
||||
list.pop();
|
||||
}
|
||||
|
||||
function assert(what, message) {
|
||||
if (!what) throw new Error(message);
|
||||
}
|
||||
|
||||
function E(key, msg, Base) {
|
||||
return function NodeError(...args) {
|
||||
const error = new Base();
|
||||
const message = ReflectApply(msg, error, args);
|
||||
ObjectDefineProperties(error, {
|
||||
message: {
|
||||
value: message,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
},
|
||||
toString: {
|
||||
value() {
|
||||
return `${this.name} [${key}]: ${this.message}`;
|
||||
},
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
},
|
||||
});
|
||||
error.code = key;
|
||||
return error;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const ERR_INVALID_ARG_TYPE = E('ERR_INVALID_ARG_TYPE',
|
||||
(name, expected, actual) => {
|
||||
assert(typeof name === 'string', "'name' must be a string");
|
||||
if (!ArrayIsArray(expected)) {
|
||||
expected = [expected];
|
||||
}
|
||||
|
||||
let msg = 'The ';
|
||||
if (StringPrototypeEndsWith(name, ' argument')) {
|
||||
// For cases like 'first argument'
|
||||
msg += `${name} `;
|
||||
} else {
|
||||
const type = StringPrototypeIncludes(name, '.') ? 'property' : 'argument';
|
||||
msg += `"${name}" ${type} `;
|
||||
}
|
||||
msg += 'must be ';
|
||||
|
||||
const types = [];
|
||||
const instances = [];
|
||||
const other = [];
|
||||
|
||||
for (const value of expected) {
|
||||
assert(typeof value === 'string',
|
||||
'All expected entries have to be of type string');
|
||||
if (ArrayPrototypeIncludes(kTypes, value)) {
|
||||
ArrayPrototypePush(types, StringPrototypeToLowerCase(value));
|
||||
} else if (RegExpPrototypeTest(classRegExp, value)) {
|
||||
ArrayPrototypePush(instances, value);
|
||||
} else {
|
||||
assert(value !== 'object',
|
||||
'The value "object" should be written as "Object"');
|
||||
ArrayPrototypePush(other, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Special handle `object` in case other instances are allowed to outline
|
||||
// the differences between each other.
|
||||
if (instances.length > 0) {
|
||||
const pos = ArrayPrototypeIndexOf(types, 'object');
|
||||
if (pos !== -1) {
|
||||
ArrayPrototypeSplice(types, pos, 1);
|
||||
ArrayPrototypePush(instances, 'Object');
|
||||
}
|
||||
}
|
||||
|
||||
if (types.length > 0) {
|
||||
if (types.length > 2) {
|
||||
const last = ArrayPrototypePop(types);
|
||||
msg += `one of type ${ArrayPrototypeJoin(types, ', ')}, or ${last}`;
|
||||
} else if (types.length === 2) {
|
||||
msg += `one of type ${types[0]} or ${types[1]}`;
|
||||
} else {
|
||||
msg += `of type ${types[0]}`;
|
||||
}
|
||||
if (instances.length > 0 || other.length > 0)
|
||||
msg += ' or ';
|
||||
}
|
||||
|
||||
if (instances.length > 0) {
|
||||
if (instances.length > 2) {
|
||||
const last = ArrayPrototypePop(instances);
|
||||
msg +=
|
||||
`an instance of ${ArrayPrototypeJoin(instances, ', ')}, or ${last}`;
|
||||
} else {
|
||||
msg += `an instance of ${instances[0]}`;
|
||||
if (instances.length === 2) {
|
||||
msg += ` or ${instances[1]}`;
|
||||
}
|
||||
}
|
||||
if (other.length > 0)
|
||||
msg += ' or ';
|
||||
}
|
||||
|
||||
if (other.length > 0) {
|
||||
if (other.length > 2) {
|
||||
const last = ArrayPrototypePop(other);
|
||||
msg += `one of ${ArrayPrototypeJoin(other, ', ')}, or ${last}`;
|
||||
} else if (other.length === 2) {
|
||||
msg += `one of ${other[0]} or ${other[1]}`;
|
||||
} else {
|
||||
if (StringPrototypeToLowerCase(other[0]) !== other[0])
|
||||
msg += 'an ';
|
||||
msg += `${other[0]}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (actual == null) {
|
||||
msg += `. Received ${actual}`;
|
||||
} else if (typeof actual === 'function' && actual.name) {
|
||||
msg += `. Received function ${actual.name}`;
|
||||
} else if (typeof actual === 'object') {
|
||||
if (actual.constructor && actual.constructor.name) {
|
||||
msg += `. Received an instance of ${actual.constructor.name}`;
|
||||
} else {
|
||||
const inspected = inspect(actual, { depth: -1 });
|
||||
msg += `. Received ${inspected}`;
|
||||
}
|
||||
} else {
|
||||
let inspected = inspect(actual, { colors: false });
|
||||
if (inspected.length > 25)
|
||||
inspected = `${StringPrototypeSlice(inspected, 0, 25)}...`;
|
||||
msg += `. Received type ${typeof actual} (${inspected})`;
|
||||
}
|
||||
return msg;
|
||||
}, TypeError);
|
||||
|
||||
const ERR_INVALID_THIS = E('ERR_INVALID_THIS', s => `Value of "this" must be of type ${s}`, TypeError);
|
||||
|
||||
const ERR_OUT_OF_RANGE = E('ERR_OUT_OF_RANGE',
|
||||
(str, range, input, replaceDefaultBoolean = false) => {
|
||||
assert(range, 'Missing "range" argument');
|
||||
let msg = replaceDefaultBoolean ? str :
|
||||
`The value of "${str}" is out of range.`;
|
||||
const received = inspect(input);
|
||||
msg += ` It must be ${range}. Received ${received}`;
|
||||
return msg;
|
||||
}, RangeError);
|
||||
|
||||
const ERR_UNHANDLED_ERROR = E('ERR_UNHANDLED_ERROR',
|
||||
err => {
|
||||
const msg = 'Unhandled error.';
|
||||
if (err === undefined) return msg;
|
||||
return `${msg} (${err})`;
|
||||
}, Error);
|
||||
|
||||
function validateBoolean(value, name) {
|
||||
if (typeof value !== 'boolean')
|
||||
throw new ERR_INVALID_ARG_TYPE(name, 'boolean', value);
|
||||
}
|
||||
|
||||
function validateFunction(value, name) {
|
||||
if (typeof value !== 'function')
|
||||
throw new ERR_INVALID_ARG_TYPE(name, 'Function', value);
|
||||
}
|
||||
|
||||
function validateString(value, name) {
|
||||
if (typeof value !== 'string')
|
||||
throw new ERR_INVALID_ARG_TYPE(name, 'string', value);
|
||||
}
|
||||
|
||||
function nc(cond, e) {
|
||||
return cond === undefined || cond === null ? e : cond;
|
||||
}
|
||||
|
||||
function oc(base, key) {
|
||||
return base === undefined || base === null ? undefined : base[key];
|
||||
}
|
||||
|
||||
const kCapture = Symbol('kCapture');
|
||||
const kErrorMonitor = host.kErrorMonitor || Symbol('events.errorMonitor');
|
||||
const kMaxEventTargetListeners = Symbol('events.maxEventTargetListeners');
|
||||
const kMaxEventTargetListenersWarned =
|
||||
Symbol('events.maxEventTargetListenersWarned');
|
||||
|
||||
const kIsEventTarget = SymbolFor('nodejs.event_target');
|
||||
|
||||
function isEventTarget(obj) {
|
||||
return oc(oc(obj, 'constructor'), kIsEventTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new `EventEmitter` instance.
|
||||
* @param {{ captureRejections?: boolean; }} [opts]
|
||||
* @constructs {EventEmitter}
|
||||
*/
|
||||
function EventEmitter(opts) {
|
||||
EventEmitter.init.call(this, opts);
|
||||
}
|
||||
module.exports = EventEmitter;
|
||||
if (host.once) module.exports.once = host.once;
|
||||
if (host.on) module.exports.on = host.on;
|
||||
if (host.getEventListeners) module.exports.getEventListeners = host.getEventListeners;
|
||||
// Backwards-compat with node 0.10.x
|
||||
EventEmitter.EventEmitter = EventEmitter;
|
||||
|
||||
EventEmitter.usingDomains = false;
|
||||
|
||||
EventEmitter.captureRejectionSymbol = kRejection;
|
||||
ObjectDefineProperty(EventEmitter, 'captureRejections', {
|
||||
get() {
|
||||
return EventEmitter.prototype[kCapture];
|
||||
},
|
||||
set(value) {
|
||||
validateBoolean(value, 'EventEmitter.captureRejections');
|
||||
|
||||
EventEmitter.prototype[kCapture] = value;
|
||||
},
|
||||
enumerable: true
|
||||
});
|
||||
|
||||
if (host.EventEmitterReferencingAsyncResource) {
|
||||
const kAsyncResource = Symbol('kAsyncResource');
|
||||
const EventEmitterReferencingAsyncResource = host.EventEmitterReferencingAsyncResource;
|
||||
|
||||
class EventEmitterAsyncResource extends EventEmitter {
|
||||
/**
|
||||
* @param {{
|
||||
* name?: string,
|
||||
* triggerAsyncId?: number,
|
||||
* requireManualDestroy?: boolean,
|
||||
* }} [options]
|
||||
*/
|
||||
constructor(options = undefined) {
|
||||
let name;
|
||||
if (typeof options === 'string') {
|
||||
name = options;
|
||||
options = undefined;
|
||||
} else {
|
||||
if (new.target === EventEmitterAsyncResource) {
|
||||
validateString(oc(options, 'name'), 'options.name');
|
||||
}
|
||||
name = oc(options, 'name') || new.target.name;
|
||||
}
|
||||
super(options);
|
||||
|
||||
this[kAsyncResource] =
|
||||
new EventEmitterReferencingAsyncResource(this, name, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {symbol,string} event
|
||||
* @param {...any} args
|
||||
* @returns {boolean}
|
||||
*/
|
||||
emit(event, ...args) {
|
||||
if (this[kAsyncResource] === undefined)
|
||||
throw new ERR_INVALID_THIS('EventEmitterAsyncResource');
|
||||
const { asyncResource } = this;
|
||||
ArrayPrototypeUnshift(args, super.emit, this, event);
|
||||
return ReflectApply(asyncResource.runInAsyncScope, asyncResource,
|
||||
args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
emitDestroy() {
|
||||
if (this[kAsyncResource] === undefined)
|
||||
throw new ERR_INVALID_THIS('EventEmitterAsyncResource');
|
||||
this.asyncResource.emitDestroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
get asyncId() {
|
||||
if (this[kAsyncResource] === undefined)
|
||||
throw new ERR_INVALID_THIS('EventEmitterAsyncResource');
|
||||
return this.asyncResource.asyncId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
get triggerAsyncId() {
|
||||
if (this[kAsyncResource] === undefined)
|
||||
throw new ERR_INVALID_THIS('EventEmitterAsyncResource');
|
||||
return this.asyncResource.triggerAsyncId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {EventEmitterReferencingAsyncResource}
|
||||
*/
|
||||
get asyncResource() {
|
||||
if (this[kAsyncResource] === undefined)
|
||||
throw new ERR_INVALID_THIS('EventEmitterAsyncResource');
|
||||
return this[kAsyncResource];
|
||||
}
|
||||
}
|
||||
EventEmitter.EventEmitterAsyncResource = EventEmitterAsyncResource;
|
||||
}
|
||||
|
||||
EventEmitter.errorMonitor = kErrorMonitor;
|
||||
|
||||
// The default for captureRejections is false
|
||||
ObjectDefineProperty(EventEmitter.prototype, kCapture, {
|
||||
value: false,
|
||||
writable: true,
|
||||
enumerable: false
|
||||
});
|
||||
|
||||
EventEmitter.prototype._events = undefined;
|
||||
EventEmitter.prototype._eventsCount = 0;
|
||||
EventEmitter.prototype._maxListeners = undefined;
|
||||
|
||||
// By default EventEmitters will print a warning if more than 10 listeners are
|
||||
// added to it. This is a useful default which helps finding memory leaks.
|
||||
let defaultMaxListeners = 10;
|
||||
|
||||
function checkListener(listener) {
|
||||
validateFunction(listener, 'listener');
|
||||
}
|
||||
|
||||
ObjectDefineProperty(EventEmitter, 'defaultMaxListeners', {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
return defaultMaxListeners;
|
||||
},
|
||||
set: function(arg) {
|
||||
if (typeof arg !== 'number' || arg < 0 || NumberIsNaN(arg)) {
|
||||
throw new ERR_OUT_OF_RANGE('defaultMaxListeners',
|
||||
'a non-negative number',
|
||||
arg);
|
||||
}
|
||||
defaultMaxListeners = arg;
|
||||
}
|
||||
});
|
||||
|
||||
ObjectDefineProperties(EventEmitter, {
|
||||
kMaxEventTargetListeners: {
|
||||
value: kMaxEventTargetListeners,
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
},
|
||||
kMaxEventTargetListenersWarned: {
|
||||
value: kMaxEventTargetListenersWarned,
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Sets the max listeners.
|
||||
* @param {number} n
|
||||
* @param {EventTarget[] | EventEmitter[]} [eventTargets]
|
||||
* @returns {void}
|
||||
*/
|
||||
EventEmitter.setMaxListeners =
|
||||
function(n = defaultMaxListeners, ...eventTargets) {
|
||||
if (typeof n !== 'number' || n < 0 || NumberIsNaN(n))
|
||||
throw new ERR_OUT_OF_RANGE('n', 'a non-negative number', n);
|
||||
if (eventTargets.length === 0) {
|
||||
defaultMaxListeners = n;
|
||||
} else {
|
||||
for (let i = 0; i < eventTargets.length; i++) {
|
||||
const target = eventTargets[i];
|
||||
if (isEventTarget(target)) {
|
||||
target[kMaxEventTargetListeners] = n;
|
||||
target[kMaxEventTargetListenersWarned] = false;
|
||||
} else if (typeof target.setMaxListeners === 'function') {
|
||||
target.setMaxListeners(n);
|
||||
} else {
|
||||
throw new ERR_INVALID_ARG_TYPE(
|
||||
'eventTargets',
|
||||
['EventEmitter', 'EventTarget'],
|
||||
target);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// If you're updating this function definition, please also update any
|
||||
// re-definitions, such as the one in the Domain module (lib/domain.js).
|
||||
EventEmitter.init = function(opts) {
|
||||
|
||||
if (this._events === undefined ||
|
||||
this._events === ObjectGetPrototypeOf(this)._events) {
|
||||
this._events = ObjectCreate(null);
|
||||
this._eventsCount = 0;
|
||||
}
|
||||
|
||||
this._maxListeners = this._maxListeners || undefined;
|
||||
|
||||
|
||||
if (oc(opts, 'captureRejections')) {
|
||||
validateBoolean(opts.captureRejections, 'options.captureRejections');
|
||||
this[kCapture] = Boolean(opts.captureRejections);
|
||||
} else {
|
||||
// Assigning the kCapture property directly saves an expensive
|
||||
// prototype lookup in a very sensitive hot path.
|
||||
this[kCapture] = EventEmitter.prototype[kCapture];
|
||||
}
|
||||
};
|
||||
|
||||
function addCatch(that, promise, type, args) {
|
||||
if (!that[kCapture]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle Promises/A+ spec, then could be a getter
|
||||
// that throws on second use.
|
||||
try {
|
||||
const then = promise.then;
|
||||
|
||||
if (typeof then === 'function') {
|
||||
then.call(promise, undefined, function(err) {
|
||||
// The callback is called with nextTick to avoid a follow-up
|
||||
// rejection from this promise.
|
||||
process.nextTick(emitUnhandledRejectionOrErr, that, err, type, args);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
that.emit('error', err);
|
||||
}
|
||||
}
|
||||
|
||||
function emitUnhandledRejectionOrErr(ee, err, type, args) {
|
||||
if (typeof ee[kRejection] === 'function') {
|
||||
ee[kRejection](err, type, ...args);
|
||||
} else {
|
||||
// We have to disable the capture rejections mechanism, otherwise
|
||||
// we might end up in an infinite loop.
|
||||
const prev = ee[kCapture];
|
||||
|
||||
// If the error handler throws, it is not catchable and it
|
||||
// will end up in 'uncaughtException'. We restore the previous
|
||||
// value of kCapture in case the uncaughtException is present
|
||||
// and the exception is handled.
|
||||
try {
|
||||
ee[kCapture] = false;
|
||||
ee.emit('error', err);
|
||||
} finally {
|
||||
ee[kCapture] = prev;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases the max listeners of the event emitter.
|
||||
* @param {number} n
|
||||
* @returns {EventEmitter}
|
||||
*/
|
||||
EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
|
||||
if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) {
|
||||
throw new ERR_OUT_OF_RANGE('n', 'a non-negative number', n);
|
||||
}
|
||||
this._maxListeners = n;
|
||||
return this;
|
||||
};
|
||||
|
||||
function _getMaxListeners(that) {
|
||||
if (that._maxListeners === undefined)
|
||||
return EventEmitter.defaultMaxListeners;
|
||||
return that._maxListeners;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current max listener value for the event emitter.
|
||||
* @returns {number}
|
||||
*/
|
||||
EventEmitter.prototype.getMaxListeners = function getMaxListeners() {
|
||||
return _getMaxListeners(this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Synchronously calls each of the listeners registered
|
||||
* for the event.
|
||||
* @param {string | symbol} type
|
||||
* @param {...any} [args]
|
||||
* @returns {boolean}
|
||||
*/
|
||||
EventEmitter.prototype.emit = function emit(type, ...args) {
|
||||
let doError = (type === 'error');
|
||||
|
||||
const events = this._events;
|
||||
if (events !== undefined) {
|
||||
if (doError && events[kErrorMonitor] !== undefined)
|
||||
this.emit(kErrorMonitor, ...args);
|
||||
doError = (doError && events.error === undefined);
|
||||
} else if (!doError)
|
||||
return false;
|
||||
|
||||
// If there is no 'error' event listener then throw.
|
||||
if (doError) {
|
||||
let er;
|
||||
if (args.length > 0)
|
||||
er = args[0];
|
||||
if (er instanceof Error) {
|
||||
try {
|
||||
const capture = {};
|
||||
ErrorCaptureStackTrace(capture, EventEmitter.prototype.emit);
|
||||
} catch (e) {}
|
||||
|
||||
// Note: The comments on the `throw` lines are intentional, they show
|
||||
// up in Node's output if this results in an unhandled exception.
|
||||
throw er; // Unhandled 'error' event
|
||||
}
|
||||
|
||||
let stringifiedEr;
|
||||
try {
|
||||
stringifiedEr = inspect(er);
|
||||
} catch (e) {
|
||||
stringifiedEr = er;
|
||||
}
|
||||
|
||||
// At least give some kind of context to the user
|
||||
const err = new ERR_UNHANDLED_ERROR(stringifiedEr);
|
||||
err.context = er;
|
||||
throw err; // Unhandled 'error' event
|
||||
}
|
||||
|
||||
const handler = events[type];
|
||||
|
||||
if (handler === undefined)
|
||||
return false;
|
||||
|
||||
if (typeof handler === 'function') {
|
||||
const result = handler.apply(this, args);
|
||||
|
||||
// We check if result is undefined first because that
|
||||
// is the most common case so we do not pay any perf
|
||||
// penalty
|
||||
if (result !== undefined && result !== null) {
|
||||
addCatch(this, result, type, args);
|
||||
}
|
||||
} else {
|
||||
const len = handler.length;
|
||||
const listeners = arrayClone(handler);
|
||||
for (let i = 0; i < len; ++i) {
|
||||
const result = listeners[i].apply(this, args);
|
||||
|
||||
// We check if result is undefined first because that
|
||||
// is the most common case so we do not pay any perf
|
||||
// penalty.
|
||||
// This code is duplicated because extracting it away
|
||||
// would make it non-inlineable.
|
||||
if (result !== undefined && result !== null) {
|
||||
addCatch(this, result, type, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
function _addListener(target, type, listener, prepend) {
|
||||
let m;
|
||||
let events;
|
||||
let existing;
|
||||
|
||||
checkListener(listener);
|
||||
|
||||
events = target._events;
|
||||
if (events === undefined) {
|
||||
events = target._events = ObjectCreate(null);
|
||||
target._eventsCount = 0;
|
||||
} else {
|
||||
// To avoid recursion in the case that type === "newListener"! Before
|
||||
// adding it to the listeners, first emit "newListener".
|
||||
if (events.newListener !== undefined) {
|
||||
target.emit('newListener', type,
|
||||
nc(listener.listener, listener));
|
||||
|
||||
// Re-assign `events` because a newListener handler could have caused the
|
||||
// this._events to be assigned to a new object
|
||||
events = target._events;
|
||||
}
|
||||
existing = events[type];
|
||||
}
|
||||
|
||||
if (existing === undefined) {
|
||||
// Optimize the case of one listener. Don't need the extra array object.
|
||||
events[type] = listener;
|
||||
++target._eventsCount;
|
||||
} else {
|
||||
if (typeof existing === 'function') {
|
||||
// Adding the second element, need to change to array.
|
||||
existing = events[type] =
|
||||
prepend ? [listener, existing] : [existing, listener];
|
||||
// If we've already got an array, just append.
|
||||
} else if (prepend) {
|
||||
existing.unshift(listener);
|
||||
} else {
|
||||
existing.push(listener);
|
||||
}
|
||||
|
||||
// Check for listener leak
|
||||
m = _getMaxListeners(target);
|
||||
if (m > 0 && existing.length > m && !existing.warned) {
|
||||
existing.warned = true;
|
||||
// No error code for this since it is a Warning
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const w = new Error('Possible EventEmitter memory leak detected. ' +
|
||||
`${existing.length} ${String(type)} listeners ` +
|
||||
`added to ${inspect(target, { depth: -1 })}. Use ` +
|
||||
'emitter.setMaxListeners() to increase limit');
|
||||
w.name = 'MaxListenersExceededWarning';
|
||||
w.emitter = target;
|
||||
w.type = type;
|
||||
w.count = existing.length;
|
||||
process.emitWarning(w);
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener to the event emitter.
|
||||
* @param {string | symbol} type
|
||||
* @param {Function} listener
|
||||
* @returns {EventEmitter}
|
||||
*/
|
||||
EventEmitter.prototype.addListener = function addListener(type, listener) {
|
||||
return _addListener(this, type, listener, false);
|
||||
};
|
||||
|
||||
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
|
||||
|
||||
/**
|
||||
* Adds the `listener` function to the beginning of
|
||||
* the listeners array.
|
||||
* @param {string | symbol} type
|
||||
* @param {Function} listener
|
||||
* @returns {EventEmitter}
|
||||
*/
|
||||
EventEmitter.prototype.prependListener =
|
||||
function prependListener(type, listener) {
|
||||
return _addListener(this, type, listener, true);
|
||||
};
|
||||
|
||||
function onceWrapper() {
|
||||
if (!this.fired) {
|
||||
this.target.removeListener(this.type, this.wrapFn);
|
||||
this.fired = true;
|
||||
if (arguments.length === 0)
|
||||
return this.listener.call(this.target);
|
||||
return this.listener.apply(this.target, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
function _onceWrap(target, type, listener) {
|
||||
const state = { fired: false, wrapFn: undefined, target, type, listener };
|
||||
const wrapped = onceWrapper.bind(state);
|
||||
wrapped.listener = listener;
|
||||
state.wrapFn = wrapped;
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a one-time `listener` function to the event emitter.
|
||||
* @param {string | symbol} type
|
||||
* @param {Function} listener
|
||||
* @returns {EventEmitter}
|
||||
*/
|
||||
EventEmitter.prototype.once = function once(type, listener) {
|
||||
checkListener(listener);
|
||||
|
||||
this.on(type, _onceWrap(this, type, listener));
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a one-time `listener` function to the beginning of
|
||||
* the listeners array.
|
||||
* @param {string | symbol} type
|
||||
* @param {Function} listener
|
||||
* @returns {EventEmitter}
|
||||
*/
|
||||
EventEmitter.prototype.prependOnceListener =
|
||||
function prependOnceListener(type, listener) {
|
||||
checkListener(listener);
|
||||
|
||||
this.prependListener(type, _onceWrap(this, type, listener));
|
||||
return this;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Removes the specified `listener` from the listeners array.
|
||||
* @param {string | symbol} type
|
||||
* @param {Function} listener
|
||||
* @returns {EventEmitter}
|
||||
*/
|
||||
EventEmitter.prototype.removeListener =
|
||||
function removeListener(type, listener) {
|
||||
checkListener(listener);
|
||||
|
||||
const events = this._events;
|
||||
if (events === undefined)
|
||||
return this;
|
||||
|
||||
const list = events[type];
|
||||
if (list === undefined)
|
||||
return this;
|
||||
|
||||
if (list === listener || list.listener === listener) {
|
||||
if (--this._eventsCount === 0)
|
||||
this._events = ObjectCreate(null);
|
||||
else {
|
||||
delete events[type];
|
||||
if (events.removeListener)
|
||||
this.emit('removeListener', type, list.listener || listener);
|
||||
}
|
||||
} else if (typeof list !== 'function') {
|
||||
let position = -1;
|
||||
|
||||
for (let i = list.length - 1; i >= 0; i--) {
|
||||
if (list[i] === listener || list[i].listener === listener) {
|
||||
position = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (position < 0)
|
||||
return this;
|
||||
|
||||
if (position === 0)
|
||||
list.shift();
|
||||
else {
|
||||
spliceOne(list, position);
|
||||
}
|
||||
|
||||
if (list.length === 1)
|
||||
events[type] = list[0];
|
||||
|
||||
if (events.removeListener !== undefined)
|
||||
this.emit('removeListener', type, listener);
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
|
||||
|
||||
/**
|
||||
* Removes all listeners from the event emitter. (Only
|
||||
* removes listeners for a specific event name if specified
|
||||
* as `type`).
|
||||
* @param {string | symbol} [type]
|
||||
* @returns {EventEmitter}
|
||||
*/
|
||||
EventEmitter.prototype.removeAllListeners =
|
||||
function removeAllListeners(type) {
|
||||
const events = this._events;
|
||||
if (events === undefined)
|
||||
return this;
|
||||
|
||||
// Not listening for removeListener, no need to emit
|
||||
if (events.removeListener === undefined) {
|
||||
if (arguments.length === 0) {
|
||||
this._events = ObjectCreate(null);
|
||||
this._eventsCount = 0;
|
||||
} else if (events[type] !== undefined) {
|
||||
if (--this._eventsCount === 0)
|
||||
this._events = ObjectCreate(null);
|
||||
else
|
||||
delete events[type];
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
// Emit removeListener for all listeners on all events
|
||||
if (arguments.length === 0) {
|
||||
for (const key of ReflectOwnKeys(events)) {
|
||||
if (key === 'removeListener') continue;
|
||||
this.removeAllListeners(key);
|
||||
}
|
||||
this.removeAllListeners('removeListener');
|
||||
this._events = ObjectCreate(null);
|
||||
this._eventsCount = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
const listeners = events[type];
|
||||
|
||||
if (typeof listeners === 'function') {
|
||||
this.removeListener(type, listeners);
|
||||
} else if (listeners !== undefined) {
|
||||
// LIFO order
|
||||
for (let i = listeners.length - 1; i >= 0; i--) {
|
||||
this.removeListener(type, listeners[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
function _listeners(target, type, unwrap) {
|
||||
const events = target._events;
|
||||
|
||||
if (events === undefined)
|
||||
return [];
|
||||
|
||||
const evlistener = events[type];
|
||||
if (evlistener === undefined)
|
||||
return [];
|
||||
|
||||
if (typeof evlistener === 'function')
|
||||
return unwrap ? [evlistener.listener || evlistener] : [evlistener];
|
||||
|
||||
return unwrap ?
|
||||
unwrapListeners(evlistener) : arrayClone(evlistener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of the array of listeners for the event name
|
||||
* specified as `type`.
|
||||
* @param {string | symbol} type
|
||||
* @returns {Function[]}
|
||||
*/
|
||||
EventEmitter.prototype.listeners = function listeners(type) {
|
||||
return _listeners(this, type, true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a copy of the array of listeners and wrappers for
|
||||
* the event name specified as `type`.
|
||||
* @param {string | symbol} type
|
||||
* @returns {Function[]}
|
||||
*/
|
||||
EventEmitter.prototype.rawListeners = function rawListeners(type) {
|
||||
return _listeners(this, type, false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the number of listeners listening to the event name
|
||||
* specified as `type`.
|
||||
* @deprecated since v3.2.0
|
||||
* @param {EventEmitter} emitter
|
||||
* @param {string | symbol} type
|
||||
* @returns {number}
|
||||
*/
|
||||
EventEmitter.listenerCount = function(emitter, type) {
|
||||
if (typeof emitter.listenerCount === 'function') {
|
||||
return emitter.listenerCount(type);
|
||||
}
|
||||
return emitter.listenerCount(type);
|
||||
};
|
||||
|
||||
EventEmitter.prototype.listenerCount = listenerCount;
|
||||
|
||||
/**
|
||||
* Returns the number of listeners listening to event name
|
||||
* specified as `type`.
|
||||
* @param {string | symbol} type
|
||||
* @returns {number}
|
||||
*/
|
||||
function listenerCount(type) {
|
||||
const events = this._events;
|
||||
|
||||
if (events !== undefined) {
|
||||
const evlistener = events[type];
|
||||
|
||||
if (typeof evlistener === 'function') {
|
||||
return 1;
|
||||
} else if (evlistener !== undefined) {
|
||||
return evlistener.length;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array listing the events for which
|
||||
* the emitter has registered listeners.
|
||||
* @returns {any[]}
|
||||
*/
|
||||
EventEmitter.prototype.eventNames = function eventNames() {
|
||||
return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : [];
|
||||
};
|
||||
|
||||
function arrayClone(arr) {
|
||||
// At least since V8 8.3, this implementation is faster than the previous
|
||||
// which always used a simple for-loop
|
||||
switch (arr.length) {
|
||||
case 2: return [arr[0], arr[1]];
|
||||
case 3: return [arr[0], arr[1], arr[2]];
|
||||
case 4: return [arr[0], arr[1], arr[2], arr[3]];
|
||||
case 5: return [arr[0], arr[1], arr[2], arr[3], arr[4]];
|
||||
case 6: return [arr[0], arr[1], arr[2], arr[3], arr[4], arr[5]];
|
||||
}
|
||||
return ArrayPrototypeSlice(arr);
|
||||
}
|
||||
|
||||
function unwrapListeners(arr) {
|
||||
const ret = arrayClone(arr);
|
||||
for (let i = 0; i < ret.length; ++i) {
|
||||
const orig = ret[i].listener;
|
||||
if (typeof orig === 'function')
|
||||
ret[i] = orig;
|
||||
}
|
||||
return ret;
|
||||
}
|
90488
dist/index.js
vendored
90488
dist/index.js
vendored
File diff suppressed because one or more lines are too long
469
dist/setup-node-sandbox.js
vendored
Normal file
469
dist/setup-node-sandbox.js
vendored
Normal file
|
@ -0,0 +1,469 @@
|
|||
/* global host, data, VMError */
|
||||
|
||||
'use strict';
|
||||
|
||||
const LocalError = Error;
|
||||
const LocalTypeError = TypeError;
|
||||
const LocalWeakMap = WeakMap;
|
||||
|
||||
const {
|
||||
apply: localReflectApply,
|
||||
defineProperty: localReflectDefineProperty
|
||||
} = Reflect;
|
||||
|
||||
const {
|
||||
set: localWeakMapSet,
|
||||
get: localWeakMapGet
|
||||
} = LocalWeakMap.prototype;
|
||||
|
||||
const {
|
||||
isArray: localArrayIsArray
|
||||
} = Array;
|
||||
|
||||
function uncurryThis(func) {
|
||||
return (thiz, ...args) => localReflectApply(func, thiz, args);
|
||||
}
|
||||
|
||||
const localArrayPrototypeSlice = uncurryThis(Array.prototype.slice);
|
||||
const localArrayPrototypeIncludes = uncurryThis(Array.prototype.includes);
|
||||
const localArrayPrototypePush = uncurryThis(Array.prototype.push);
|
||||
const localArrayPrototypeIndexOf = uncurryThis(Array.prototype.indexOf);
|
||||
const localArrayPrototypeSplice = uncurryThis(Array.prototype.splice);
|
||||
const localStringPrototypeStartsWith = uncurryThis(String.prototype.startsWith);
|
||||
const localStringPrototypeSlice = uncurryThis(String.prototype.slice);
|
||||
const localStringPrototypeIndexOf = uncurryThis(String.prototype.indexOf);
|
||||
|
||||
const {
|
||||
argv: optionArgv,
|
||||
env: optionEnv,
|
||||
console: optionConsole,
|
||||
vm,
|
||||
resolver,
|
||||
extensions
|
||||
} = data;
|
||||
|
||||
function ensureSandboxArray(a) {
|
||||
return localArrayPrototypeSlice(a);
|
||||
}
|
||||
|
||||
const globalPaths = ensureSandboxArray(resolver.globalPaths);
|
||||
|
||||
class Module {
|
||||
|
||||
constructor(id, path, parent) {
|
||||
this.id = id;
|
||||
this.filename = id;
|
||||
this.path = path;
|
||||
this.parent = parent;
|
||||
this.loaded = false;
|
||||
this.paths = path ? ensureSandboxArray(resolver.genLookupPaths(path)) : [];
|
||||
this.children = [];
|
||||
this.exports = {};
|
||||
}
|
||||
|
||||
_updateChildren(child, isNew) {
|
||||
const children = this.children;
|
||||
if (children && (isNew || !localArrayPrototypeIncludes(children, child))) {
|
||||
localArrayPrototypePush(children, child);
|
||||
}
|
||||
}
|
||||
|
||||
require(id) {
|
||||
return requireImpl(this, id, false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const originalRequire = Module.prototype.require;
|
||||
const cacheBuiltins = {__proto__: null};
|
||||
|
||||
function requireImpl(mod, id, direct) {
|
||||
if (direct && mod.require !== originalRequire) {
|
||||
return mod.require(id);
|
||||
}
|
||||
const filename = resolver.resolve(mod, id, undefined, Module._extensions, direct);
|
||||
if (localStringPrototypeStartsWith(filename, 'node:')) {
|
||||
id = localStringPrototypeSlice(filename, 5);
|
||||
let nmod = cacheBuiltins[id];
|
||||
if (!nmod) {
|
||||
nmod = resolver.loadBuiltinModule(vm, id);
|
||||
if (!nmod) throw new VMError(`Cannot find module '${filename}'`, 'ENOTFOUND');
|
||||
cacheBuiltins[id] = nmod;
|
||||
}
|
||||
return nmod;
|
||||
}
|
||||
|
||||
const cachedModule = Module._cache[filename];
|
||||
if (cachedModule !== undefined) {
|
||||
mod._updateChildren(cachedModule, false);
|
||||
return cachedModule.exports;
|
||||
}
|
||||
|
||||
let nmod = cacheBuiltins[id];
|
||||
if (nmod) return nmod;
|
||||
nmod = resolver.loadBuiltinModule(vm, id);
|
||||
if (nmod) {
|
||||
cacheBuiltins[id] = nmod;
|
||||
return nmod;
|
||||
}
|
||||
|
||||
const path = resolver.pathDirname(filename);
|
||||
const module = new Module(filename, path, mod);
|
||||
resolver.registerModule(module, filename, path, mod, direct);
|
||||
mod._updateChildren(module, true);
|
||||
try {
|
||||
Module._cache[filename] = module;
|
||||
const handler = findBestExtensionHandler(filename);
|
||||
handler(module, filename);
|
||||
module.loaded = true;
|
||||
} catch (e) {
|
||||
delete Module._cache[filename];
|
||||
const children = mod.children;
|
||||
if (localArrayIsArray(children)) {
|
||||
const index = localArrayPrototypeIndexOf(children, module);
|
||||
if (index !== -1) {
|
||||
localArrayPrototypeSplice(children, index, 1);
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
return module.exports;
|
||||
}
|
||||
|
||||
Module.builtinModules = ensureSandboxArray(resolver.getBuiltinModulesList());
|
||||
Module.globalPaths = globalPaths;
|
||||
Module._extensions = {__proto__: null};
|
||||
Module._cache = {__proto__: null};
|
||||
|
||||
{
|
||||
const keys = Object.getOwnPropertyNames(extensions);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
const handler = extensions[key];
|
||||
Module._extensions[key] = (mod, filename) => handler(mod, filename);
|
||||
}
|
||||
}
|
||||
|
||||
function findBestExtensionHandler(filename) {
|
||||
const name = resolver.pathBasename(filename);
|
||||
for (let i = 0; (i = localStringPrototypeIndexOf(name, '.', i + 1)) !== -1;) {
|
||||
const ext = localStringPrototypeSlice(name, i);
|
||||
const handler = Module._extensions[ext];
|
||||
if (handler) return handler;
|
||||
}
|
||||
const js = Module._extensions['.js'];
|
||||
if (js) return js;
|
||||
const keys = Object.getOwnPropertyNames(Module._extensions);
|
||||
if (keys.length === 0) throw new VMError(`Failed to load '${filename}': Unknown type.`, 'ELOADFAIL');
|
||||
return Module._extensions[keys[0]];
|
||||
}
|
||||
|
||||
function createRequireForModule(mod) {
|
||||
// eslint-disable-next-line no-shadow
|
||||
function require(id) {
|
||||
return requireImpl(mod, id, true);
|
||||
}
|
||||
function resolve(id, options) {
|
||||
return resolver.resolve(mod, id, options, Module._extensions, true);
|
||||
}
|
||||
require.resolve = resolve;
|
||||
function paths(id) {
|
||||
return ensureSandboxArray(resolver.lookupPaths(mod, id));
|
||||
}
|
||||
resolve.paths = paths;
|
||||
|
||||
require.extensions = Module._extensions;
|
||||
|
||||
require.cache = Module._cache;
|
||||
|
||||
return require;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare sandbox.
|
||||
*/
|
||||
|
||||
const TIMERS = new LocalWeakMap();
|
||||
|
||||
class Timeout {
|
||||
}
|
||||
|
||||
class Interval {
|
||||
}
|
||||
|
||||
class Immediate {
|
||||
}
|
||||
|
||||
function clearTimer(timer) {
|
||||
const obj = localReflectApply(localWeakMapGet, TIMERS, [timer]);
|
||||
if (obj) {
|
||||
obj.clear(obj.value);
|
||||
}
|
||||
}
|
||||
|
||||
// This is a function and not an arrow function, since the original is also a function
|
||||
// eslint-disable-next-line no-shadow
|
||||
global.setTimeout = function setTimeout(callback, delay, ...args) {
|
||||
if (typeof callback !== 'function') throw new LocalTypeError('"callback" argument must be a function');
|
||||
const obj = new Timeout(callback, args);
|
||||
const cb = () => {
|
||||
localReflectApply(callback, null, args);
|
||||
};
|
||||
const tmr = host.setTimeout(cb, delay);
|
||||
|
||||
const ref = {
|
||||
__proto__: null,
|
||||
clear: host.clearTimeout,
|
||||
value: tmr
|
||||
};
|
||||
|
||||
localReflectApply(localWeakMapSet, TIMERS, [obj, ref]);
|
||||
return obj;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
global.setInterval = function setInterval(callback, interval, ...args) {
|
||||
if (typeof callback !== 'function') throw new LocalTypeError('"callback" argument must be a function');
|
||||
const obj = new Interval();
|
||||
const cb = () => {
|
||||
localReflectApply(callback, null, args);
|
||||
};
|
||||
const tmr = host.setInterval(cb, interval);
|
||||
|
||||
const ref = {
|
||||
__proto__: null,
|
||||
clear: host.clearInterval,
|
||||
value: tmr
|
||||
};
|
||||
|
||||
localReflectApply(localWeakMapSet, TIMERS, [obj, ref]);
|
||||
return obj;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
global.setImmediate = function setImmediate(callback, ...args) {
|
||||
if (typeof callback !== 'function') throw new LocalTypeError('"callback" argument must be a function');
|
||||
const obj = new Immediate();
|
||||
const cb = () => {
|
||||
localReflectApply(callback, null, args);
|
||||
};
|
||||
const tmr = host.setImmediate(cb);
|
||||
|
||||
const ref = {
|
||||
__proto__: null,
|
||||
clear: host.clearImmediate,
|
||||
value: tmr
|
||||
};
|
||||
|
||||
localReflectApply(localWeakMapSet, TIMERS, [obj, ref]);
|
||||
return obj;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
global.clearTimeout = function clearTimeout(timeout) {
|
||||
clearTimer(timeout);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
global.clearInterval = function clearInterval(interval) {
|
||||
clearTimer(interval);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
global.clearImmediate = function clearImmediate(immediate) {
|
||||
clearTimer(immediate);
|
||||
};
|
||||
|
||||
const localProcess = host.process;
|
||||
|
||||
function vmEmitArgs(event, args) {
|
||||
const allargs = [event];
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (!localReflectDefineProperty(allargs, i + 1, {
|
||||
__proto__: null,
|
||||
value: args[i],
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
})) throw new LocalError('Unexpected');
|
||||
}
|
||||
return localReflectApply(vm.emit, vm, allargs);
|
||||
}
|
||||
|
||||
const LISTENERS = new LocalWeakMap();
|
||||
const LISTENER_HANDLER = new LocalWeakMap();
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} name
|
||||
* @param {*} handler
|
||||
* @this process
|
||||
* @return {this}
|
||||
*/
|
||||
function addListener(name, handler) {
|
||||
if (name !== 'beforeExit' && name !== 'exit') {
|
||||
throw new LocalError(`Access denied to listen for '${name}' event.`);
|
||||
}
|
||||
|
||||
let cb = localReflectApply(localWeakMapGet, LISTENERS, [handler]);
|
||||
if (!cb) {
|
||||
cb = () => {
|
||||
handler();
|
||||
};
|
||||
localReflectApply(localWeakMapSet, LISTENER_HANDLER, [cb, handler]);
|
||||
localReflectApply(localWeakMapSet, LISTENERS, [handler, cb]);
|
||||
}
|
||||
|
||||
localProcess.on(name, cb);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @this process
|
||||
* @return {this}
|
||||
*/
|
||||
// eslint-disable-next-line no-shadow
|
||||
function process() {
|
||||
return this;
|
||||
}
|
||||
|
||||
const baseUptime = localProcess.uptime();
|
||||
|
||||
// FIXME wrong class structure
|
||||
global.process = {
|
||||
__proto__: process.prototype,
|
||||
argv: optionArgv !== undefined ? optionArgv : [],
|
||||
title: localProcess.title,
|
||||
version: localProcess.version,
|
||||
versions: localProcess.versions,
|
||||
arch: localProcess.arch,
|
||||
platform: localProcess.platform,
|
||||
env: optionEnv !== undefined ? optionEnv : {},
|
||||
pid: localProcess.pid,
|
||||
features: localProcess.features,
|
||||
nextTick: function nextTick(callback, ...args) {
|
||||
if (typeof callback !== 'function') {
|
||||
throw new LocalError('Callback must be a function.');
|
||||
}
|
||||
|
||||
localProcess.nextTick(()=>{
|
||||
localReflectApply(callback, null, args);
|
||||
});
|
||||
},
|
||||
hrtime: function hrtime(time) {
|
||||
return localProcess.hrtime(time);
|
||||
},
|
||||
uptime: function uptime() {
|
||||
return localProcess.uptime() - baseUptime;
|
||||
},
|
||||
cwd: function cwd() {
|
||||
return localProcess.cwd();
|
||||
},
|
||||
addListener,
|
||||
on: addListener,
|
||||
|
||||
once: function once(name, handler) {
|
||||
if (name !== 'beforeExit' && name !== 'exit') {
|
||||
throw new LocalError(`Access denied to listen for '${name}' event.`);
|
||||
}
|
||||
|
||||
let triggered = false;
|
||||
const cb = () => {
|
||||
if (triggered) return;
|
||||
triggered = true;
|
||||
localProcess.removeListener(name, cb);
|
||||
handler();
|
||||
};
|
||||
localReflectApply(localWeakMapSet, LISTENER_HANDLER, [cb, handler]);
|
||||
|
||||
localProcess.on(name, cb);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
listeners: function listeners(name) {
|
||||
if (name !== 'beforeExit' && name !== 'exit') {
|
||||
// Maybe add ({__proto__:null})[name] to throw when name fails in https://tc39.es/ecma262/#sec-topropertykey.
|
||||
return [];
|
||||
}
|
||||
|
||||
// Filter out listeners, which were not created in this sandbox
|
||||
const all = localProcess.listeners(name);
|
||||
const filtered = [];
|
||||
let j = 0;
|
||||
for (let i = 0; i < all.length; i++) {
|
||||
const h = localReflectApply(localWeakMapGet, LISTENER_HANDLER, [all[i]]);
|
||||
if (h) {
|
||||
if (!localReflectDefineProperty(filtered, j, {
|
||||
__proto__: null,
|
||||
value: h,
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
})) throw new LocalError('Unexpected');
|
||||
j++;
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
},
|
||||
|
||||
removeListener: function removeListener(name, handler) {
|
||||
if (name !== 'beforeExit' && name !== 'exit') {
|
||||
return this;
|
||||
}
|
||||
|
||||
const cb = localReflectApply(localWeakMapGet, LISTENERS, [handler]);
|
||||
if (cb) localProcess.removeListener(name, cb);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
umask: function umask() {
|
||||
if (arguments.length) {
|
||||
throw new LocalError('Access denied to set umask.');
|
||||
}
|
||||
|
||||
return localProcess.umask();
|
||||
}
|
||||
};
|
||||
|
||||
if (optionConsole === 'inherit') {
|
||||
global.console = host.console;
|
||||
} else if (optionConsole === 'redirect') {
|
||||
global.console = {
|
||||
debug(...args) {
|
||||
vmEmitArgs('console.debug', args);
|
||||
},
|
||||
log(...args) {
|
||||
vmEmitArgs('console.log', args);
|
||||
},
|
||||
info(...args) {
|
||||
vmEmitArgs('console.info', args);
|
||||
},
|
||||
warn(...args) {
|
||||
vmEmitArgs('console.warn', args);
|
||||
},
|
||||
error(...args) {
|
||||
vmEmitArgs('console.error', args);
|
||||
},
|
||||
dir(...args) {
|
||||
vmEmitArgs('console.dir', args);
|
||||
},
|
||||
time() {},
|
||||
timeEnd() {},
|
||||
trace(...args) {
|
||||
vmEmitArgs('console.trace', args);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
__proto__: null,
|
||||
Module,
|
||||
jsonParse: JSON.parse,
|
||||
createRequireForModule,
|
||||
requireImpl
|
||||
};
|
457
dist/setup-sandbox.js
vendored
Normal file
457
dist/setup-sandbox.js
vendored
Normal file
|
@ -0,0 +1,457 @@
|
|||
/* global host, bridge, data, context */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
Object: localObject,
|
||||
Array: localArray,
|
||||
Error: LocalError,
|
||||
Reflect: localReflect,
|
||||
Proxy: LocalProxy,
|
||||
WeakMap: LocalWeakMap,
|
||||
Function: localFunction,
|
||||
Promise: localPromise,
|
||||
eval: localEval
|
||||
} = global;
|
||||
|
||||
const {
|
||||
freeze: localObjectFreeze
|
||||
} = localObject;
|
||||
|
||||
const {
|
||||
getPrototypeOf: localReflectGetPrototypeOf,
|
||||
apply: localReflectApply,
|
||||
deleteProperty: localReflectDeleteProperty,
|
||||
has: localReflectHas,
|
||||
defineProperty: localReflectDefineProperty,
|
||||
setPrototypeOf: localReflectSetPrototypeOf,
|
||||
getOwnPropertyDescriptor: localReflectGetOwnPropertyDescriptor
|
||||
} = localReflect;
|
||||
|
||||
const {
|
||||
isArray: localArrayIsArray
|
||||
} = localArray;
|
||||
|
||||
const {
|
||||
ensureThis,
|
||||
ReadOnlyHandler,
|
||||
from,
|
||||
fromWithFactory,
|
||||
readonlyFactory,
|
||||
connect,
|
||||
addProtoMapping,
|
||||
VMError,
|
||||
ReadOnlyMockHandler
|
||||
} = bridge;
|
||||
|
||||
const {
|
||||
allowAsync,
|
||||
GeneratorFunction,
|
||||
AsyncFunction,
|
||||
AsyncGeneratorFunction
|
||||
} = data;
|
||||
|
||||
const {
|
||||
get: localWeakMapGet,
|
||||
set: localWeakMapSet
|
||||
} = LocalWeakMap.prototype;
|
||||
|
||||
function localUnexpected() {
|
||||
return new VMError('Should not happen');
|
||||
}
|
||||
|
||||
// global is originally prototype of host.Object so it can be used to climb up from the sandbox.
|
||||
if (!localReflectSetPrototypeOf(context, localObject.prototype)) throw localUnexpected();
|
||||
|
||||
Object.defineProperties(global, {
|
||||
global: {value: global, writable: true, configurable: true, enumerable: true},
|
||||
globalThis: {value: global, writable: true, configurable: true},
|
||||
GLOBAL: {value: global, writable: true, configurable: true},
|
||||
root: {value: global, writable: true, configurable: true},
|
||||
Error: {value: LocalError}
|
||||
});
|
||||
|
||||
if (!localReflectDefineProperty(global, 'VMError', {
|
||||
__proto__: null,
|
||||
value: VMError,
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
})) throw localUnexpected();
|
||||
|
||||
// Fixes buffer unsafe allocation
|
||||
/* eslint-disable no-use-before-define */
|
||||
class BufferHandler extends ReadOnlyHandler {
|
||||
|
||||
apply(target, thiz, args) {
|
||||
if (args.length > 0 && typeof args[0] === 'number') {
|
||||
return LocalBuffer.alloc(args[0]);
|
||||
}
|
||||
return localReflectApply(LocalBuffer.from, LocalBuffer, args);
|
||||
}
|
||||
|
||||
construct(target, args, newTarget) {
|
||||
if (args.length > 0 && typeof args[0] === 'number') {
|
||||
return LocalBuffer.alloc(args[0]);
|
||||
}
|
||||
return localReflectApply(LocalBuffer.from, LocalBuffer, args);
|
||||
}
|
||||
|
||||
}
|
||||
/* eslint-enable no-use-before-define */
|
||||
|
||||
const LocalBuffer = fromWithFactory(obj => new BufferHandler(obj), host.Buffer);
|
||||
|
||||
|
||||
if (!localReflectDefineProperty(global, 'Buffer', {
|
||||
__proto__: null,
|
||||
value: LocalBuffer,
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
})) throw localUnexpected();
|
||||
|
||||
addProtoMapping(LocalBuffer.prototype, host.Buffer.prototype, 'Uint8Array');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} size Size of new buffer
|
||||
* @this LocalBuffer
|
||||
* @return {LocalBuffer}
|
||||
*/
|
||||
function allocUnsafe(size) {
|
||||
return LocalBuffer.alloc(size);
|
||||
}
|
||||
|
||||
connect(allocUnsafe, host.Buffer.allocUnsafe);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} size Size of new buffer
|
||||
* @this LocalBuffer
|
||||
* @return {LocalBuffer}
|
||||
*/
|
||||
function allocUnsafeSlow(size) {
|
||||
return LocalBuffer.alloc(size);
|
||||
}
|
||||
|
||||
connect(allocUnsafeSlow, host.Buffer.allocUnsafeSlow);
|
||||
|
||||
/**
|
||||
* Replacement for Buffer inspect
|
||||
*
|
||||
* @param {*} recurseTimes
|
||||
* @param {*} ctx
|
||||
* @this LocalBuffer
|
||||
* @return {string}
|
||||
*/
|
||||
function inspect(recurseTimes, ctx) {
|
||||
// Mimic old behavior, could throw but didn't pass a test.
|
||||
const max = host.INSPECT_MAX_BYTES;
|
||||
const actualMax = Math.min(max, this.length);
|
||||
const remaining = this.length - max;
|
||||
let str = this.hexSlice(0, actualMax).replace(/(.{2})/g, '$1 ').trim();
|
||||
if (remaining > 0) str += ` ... ${remaining} more byte${remaining > 1 ? 's' : ''}`;
|
||||
return `<${this.constructor.name} ${str}>`;
|
||||
}
|
||||
|
||||
connect(inspect, host.Buffer.prototype.inspect);
|
||||
|
||||
connect(localFunction.prototype.bind, host.Function.prototype.bind);
|
||||
|
||||
connect(localObject.prototype.__defineGetter__, host.Object.prototype.__defineGetter__);
|
||||
connect(localObject.prototype.__defineSetter__, host.Object.prototype.__defineSetter__);
|
||||
connect(localObject.prototype.__lookupGetter__, host.Object.prototype.__lookupGetter__);
|
||||
connect(localObject.prototype.__lookupSetter__, host.Object.prototype.__lookupSetter__);
|
||||
|
||||
/*
|
||||
* PrepareStackTrace sanitization
|
||||
*/
|
||||
|
||||
const oldPrepareStackTraceDesc = localReflectGetOwnPropertyDescriptor(LocalError, 'prepareStackTrace');
|
||||
|
||||
let currentPrepareStackTrace = LocalError.prepareStackTrace;
|
||||
const wrappedPrepareStackTrace = new LocalWeakMap();
|
||||
if (typeof currentPrepareStackTrace === 'function') {
|
||||
wrappedPrepareStackTrace.set(currentPrepareStackTrace, currentPrepareStackTrace);
|
||||
}
|
||||
|
||||
let OriginalCallSite;
|
||||
LocalError.prepareStackTrace = (e, sst) => {
|
||||
OriginalCallSite = sst[0].constructor;
|
||||
};
|
||||
new LocalError().stack;
|
||||
if (typeof OriginalCallSite === 'function') {
|
||||
LocalError.prepareStackTrace = undefined;
|
||||
|
||||
function makeCallSiteGetters(list) {
|
||||
const callSiteGetters = [];
|
||||
for (let i=0; i<list.length; i++) {
|
||||
const name = list[i];
|
||||
const func = OriginalCallSite.prototype[name];
|
||||
callSiteGetters[i] = {__proto__: null,
|
||||
name,
|
||||
propName: '_' + name,
|
||||
func: (thiz) => {
|
||||
return localReflectApply(func, thiz, []);
|
||||
}
|
||||
};
|
||||
}
|
||||
return callSiteGetters;
|
||||
}
|
||||
|
||||
function applyCallSiteGetters(thiz, callSite, getters) {
|
||||
for (let i=0; i<getters.length; i++) {
|
||||
const getter = getters[i];
|
||||
localReflectDefineProperty(thiz, getter.propName, {
|
||||
__proto__: null,
|
||||
value: getter.func(callSite)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const callSiteGetters = makeCallSiteGetters([
|
||||
'getTypeName',
|
||||
'getFunctionName',
|
||||
'getMethodName',
|
||||
'getFileName',
|
||||
'getLineNumber',
|
||||
'getColumnNumber',
|
||||
'getEvalOrigin',
|
||||
'isToplevel',
|
||||
'isEval',
|
||||
'isNative',
|
||||
'isConstructor',
|
||||
'isAsync',
|
||||
'isPromiseAll',
|
||||
'getPromiseIndex'
|
||||
]);
|
||||
|
||||
class CallSite {
|
||||
constructor(callSite) {
|
||||
applyCallSiteGetters(this, callSite, callSiteGetters);
|
||||
}
|
||||
getThis() {
|
||||
return undefined;
|
||||
}
|
||||
getFunction() {
|
||||
return undefined;
|
||||
}
|
||||
toString() {
|
||||
return 'CallSite {}';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (let i=0; i<callSiteGetters.length; i++) {
|
||||
const name = callSiteGetters[i].name;
|
||||
const funcProp = localReflectGetOwnPropertyDescriptor(OriginalCallSite.prototype, name);
|
||||
if (!funcProp) continue;
|
||||
const propertyName = callSiteGetters[i].propName;
|
||||
const func = {func() {
|
||||
return this[propertyName];
|
||||
}}.func;
|
||||
const nameProp = localReflectGetOwnPropertyDescriptor(func, 'name');
|
||||
if (!nameProp) throw localUnexpected();
|
||||
nameProp.value = name;
|
||||
if (!localReflectDefineProperty(func, 'name', nameProp)) throw localUnexpected();
|
||||
funcProp.value = func;
|
||||
if (!localReflectDefineProperty(CallSite.prototype, name, funcProp)) throw localUnexpected();
|
||||
}
|
||||
|
||||
if (!localReflectDefineProperty(LocalError, 'prepareStackTrace', {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
get() {
|
||||
return currentPrepareStackTrace;
|
||||
},
|
||||
set(value) {
|
||||
if (typeof(value) !== 'function') {
|
||||
currentPrepareStackTrace = value;
|
||||
return;
|
||||
}
|
||||
const wrapped = localReflectApply(localWeakMapGet, wrappedPrepareStackTrace, [value]);
|
||||
if (wrapped) {
|
||||
currentPrepareStackTrace = wrapped;
|
||||
return;
|
||||
}
|
||||
const newWrapped = (error, sst) => {
|
||||
if (localArrayIsArray(sst)) {
|
||||
for (let i=0; i < sst.length; i++) {
|
||||
const cs = sst[i];
|
||||
if (typeof cs === 'object' && localReflectGetPrototypeOf(cs) === OriginalCallSite.prototype) {
|
||||
sst[i] = new CallSite(cs);
|
||||
}
|
||||
}
|
||||
}
|
||||
return value(error, sst);
|
||||
};
|
||||
localReflectApply(localWeakMapSet, wrappedPrepareStackTrace, [value, newWrapped]);
|
||||
localReflectApply(localWeakMapSet, wrappedPrepareStackTrace, [newWrapped, newWrapped]);
|
||||
currentPrepareStackTrace = newWrapped;
|
||||
}
|
||||
})) throw localUnexpected();
|
||||
} else if (oldPrepareStackTraceDesc) {
|
||||
localReflectDefineProperty(LocalError, 'prepareStackTrace', oldPrepareStackTraceDesc);
|
||||
} else {
|
||||
localReflectDeleteProperty(LocalError, 'prepareStackTrace');
|
||||
}
|
||||
|
||||
/*
|
||||
* Exception sanitization
|
||||
*/
|
||||
|
||||
const withProxy = localObjectFreeze({
|
||||
__proto__: null,
|
||||
has(target, key) {
|
||||
if (key === host.INTERNAL_STATE_NAME) return false;
|
||||
return localReflectHas(target, key);
|
||||
}
|
||||
});
|
||||
|
||||
const interanState = localObjectFreeze({
|
||||
__proto__: null,
|
||||
wrapWith(x) {
|
||||
if (x === null || x === undefined) return x;
|
||||
return new LocalProxy(localObject(x), withProxy);
|
||||
},
|
||||
handleException: ensureThis,
|
||||
import(what) {
|
||||
throw new VMError('Dynamic Import not supported');
|
||||
}
|
||||
});
|
||||
|
||||
if (!localReflectDefineProperty(global, host.INTERNAL_STATE_NAME, {
|
||||
__proto__: null,
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: interanState
|
||||
})) throw localUnexpected();
|
||||
|
||||
/*
|
||||
* Eval sanitization
|
||||
*/
|
||||
|
||||
function throwAsync() {
|
||||
return new VMError('Async not available');
|
||||
}
|
||||
|
||||
function makeFunction(inputArgs, isAsync, isGenerator) {
|
||||
const lastArgs = inputArgs.length - 1;
|
||||
let code = lastArgs >= 0 ? `${inputArgs[lastArgs]}` : '';
|
||||
let args = lastArgs > 0 ? `${inputArgs[0]}` : '';
|
||||
for (let i = 1; i < lastArgs; i++) {
|
||||
args += `,${inputArgs[i]}`;
|
||||
}
|
||||
try {
|
||||
code = host.transformAndCheck(args, code, isAsync, isGenerator, allowAsync);
|
||||
} catch (e) {
|
||||
throw bridge.from(e);
|
||||
}
|
||||
return localEval(code);
|
||||
}
|
||||
|
||||
const FunctionHandler = {
|
||||
__proto__: null,
|
||||
apply(target, thiz, args) {
|
||||
return makeFunction(args, this.isAsync, this.isGenerator);
|
||||
},
|
||||
construct(target, args, newTarget) {
|
||||
return makeFunction(args, this.isAsync, this.isGenerator);
|
||||
}
|
||||
};
|
||||
|
||||
const EvalHandler = {
|
||||
__proto__: null,
|
||||
apply(target, thiz, args) {
|
||||
if (args.length === 0) return undefined;
|
||||
let code = `${args[0]}`;
|
||||
try {
|
||||
code = host.transformAndCheck(null, code, false, false, allowAsync);
|
||||
} catch (e) {
|
||||
throw bridge.from(e);
|
||||
}
|
||||
return localEval(code);
|
||||
}
|
||||
};
|
||||
|
||||
const AsyncErrorHandler = {
|
||||
__proto__: null,
|
||||
apply(target, thiz, args) {
|
||||
throw throwAsync();
|
||||
},
|
||||
construct(target, args, newTarget) {
|
||||
throw throwAsync();
|
||||
}
|
||||
};
|
||||
|
||||
function makeCheckFunction(isAsync, isGenerator) {
|
||||
if (isAsync && !allowAsync) return AsyncErrorHandler;
|
||||
return {
|
||||
__proto__: FunctionHandler,
|
||||
isAsync,
|
||||
isGenerator
|
||||
};
|
||||
}
|
||||
|
||||
function overrideWithProxy(obj, prop, value, handler) {
|
||||
const proxy = new LocalProxy(value, handler);
|
||||
if (!localReflectDefineProperty(obj, prop, {__proto__: null, value: proxy})) throw localUnexpected();
|
||||
return proxy;
|
||||
}
|
||||
|
||||
const proxiedFunction = overrideWithProxy(localFunction.prototype, 'constructor', localFunction, makeCheckFunction(false, false));
|
||||
if (GeneratorFunction) {
|
||||
if (!localReflectSetPrototypeOf(GeneratorFunction, proxiedFunction)) throw localUnexpected();
|
||||
overrideWithProxy(GeneratorFunction.prototype, 'constructor', GeneratorFunction, makeCheckFunction(false, true));
|
||||
}
|
||||
if (AsyncFunction) {
|
||||
if (!localReflectSetPrototypeOf(AsyncFunction, proxiedFunction)) throw localUnexpected();
|
||||
overrideWithProxy(AsyncFunction.prototype, 'constructor', AsyncFunction, makeCheckFunction(true, false));
|
||||
}
|
||||
if (AsyncGeneratorFunction) {
|
||||
if (!localReflectSetPrototypeOf(AsyncGeneratorFunction, proxiedFunction)) throw localUnexpected();
|
||||
overrideWithProxy(AsyncGeneratorFunction.prototype, 'constructor', AsyncGeneratorFunction, makeCheckFunction(true, true));
|
||||
}
|
||||
|
||||
global.Function = proxiedFunction;
|
||||
global.eval = new LocalProxy(localEval, EvalHandler);
|
||||
|
||||
/*
|
||||
* Promise sanitization
|
||||
*/
|
||||
|
||||
if (localPromise && !allowAsync) {
|
||||
|
||||
const PromisePrototype = localPromise.prototype;
|
||||
|
||||
overrideWithProxy(PromisePrototype, 'then', PromisePrototype.then, AsyncErrorHandler);
|
||||
// This seems not to work, and will produce
|
||||
// UnhandledPromiseRejectionWarning: TypeError: Method Promise.prototype.then called on incompatible receiver [object Object].
|
||||
// This is likely caused since the host.Promise.prototype.then cannot use the VM Proxy object.
|
||||
// Contextify.connect(host.Promise.prototype.then, Promise.prototype.then);
|
||||
|
||||
if (PromisePrototype.finally) {
|
||||
overrideWithProxy(PromisePrototype, 'finally', PromisePrototype.finally, AsyncErrorHandler);
|
||||
// Contextify.connect(host.Promise.prototype.finally, Promise.prototype.finally);
|
||||
}
|
||||
if (Promise.prototype.catch) {
|
||||
overrideWithProxy(PromisePrototype, 'catch', PromisePrototype.catch, AsyncErrorHandler);
|
||||
// Contextify.connect(host.Promise.prototype.catch, Promise.prototype.catch);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function readonly(other, mock) {
|
||||
// Note: other@other(unsafe) mock@other(unsafe) returns@this(unsafe) throws@this(unsafe)
|
||||
if (!mock) return fromWithFactory(readonlyFactory, other);
|
||||
const tmock = from(mock);
|
||||
return fromWithFactory(obj=>new ReadOnlyMockHandler(obj, tmock), other);
|
||||
}
|
||||
|
||||
return {
|
||||
__proto__: null,
|
||||
readonly,
|
||||
global
|
||||
};
|
|
@ -1,53 +0,0 @@
|
|||
# Common issues
|
||||
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Create using an existing branch as the PR branch](#create-using-an-existing-branch-as-the-pr-branch)
|
||||
- [Frequently requested features](#use-case-create-a-pull-request-to-update-x-on-release)
|
||||
- [Disable force updates to existing PR branches](#disable-force-updates-to-existing-pr-branches)
|
||||
- [Add a no-verify option to bypass git hooks](#add-a-no-verify-option-to-bypass-git-hooks)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Create using an existing branch as the PR branch
|
||||
|
||||
A common point of confusion is to try and use an existing branch containing changes to raise in a PR as the `branch` input. This will not work because the action is primarily designed to be used in workflows where the PR branch does not exist yet. The action creates and manages the PR branch itself.
|
||||
|
||||
If you have an existing branch that you just want to create a PR for, then I recommend using the official [GitHub CLI](https://cli.github.com/manual/gh_pr_create) in a workflow step.
|
||||
|
||||
Alternatively, if you are trying to keep a branch up to date with another branch, then you can follow [this example](https://github.com/peter-evans/create-pull-request/blob/main/docs/examples.md#keep-a-branch-up-to-date-with-another).
|
||||
|
||||
## Frequently requested features
|
||||
|
||||
### Disable force updates to existing PR branches
|
||||
|
||||
This behaviour is fundamental to how the action works and is a conscious design decision. The "rule" that I based this design on is that when a workflow executes the action to create or update a PR, the result of those two possible actions should never be different. The easiest way to maintain that consistency is to rebase the PR branch and force push it.
|
||||
|
||||
If you want to avoid this behaviour there are some things that might work depending on your use case:
|
||||
- Check if the pull request branch exists in a separate step before the action runs and act accordingly.
|
||||
- Use the [alternative strategy](https://github.com/peter-evans/create-pull-request#alternative-strategy---always-create-a-new-pull-request-branch) of always creating a new PR that won't be updated by the action.
|
||||
- [Create your own commits](https://github.com/peter-evans/create-pull-request#create-your-own-commits) each time the action is created/updated.
|
||||
|
||||
### Add a no-verify option to bypass git hooks
|
||||
|
||||
Presently, there is no plan to add this feature to the action.
|
||||
The reason is that I'm trying very hard to keep the interface for this action to a minimum to prevent it becoming bloated and complicated.
|
||||
|
||||
Git hooks must be installed after a repository is checked out in order for them to work.
|
||||
So the straightforward solution is to just not install them during the workflow where this action is used.
|
||||
|
||||
- If hooks are automatically enabled by a framework, use an option provided by the framework to disable them. For example, for Husky users, they can be disabled with the `--ignore-scripts` flag, or by setting the `HUSKY` environment variable when the action runs.
|
||||
```yml
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
env:
|
||||
HUSKY: '0'
|
||||
```
|
||||
- If hooks are installed in a script, then add a condition checking if the `CI` environment variable exists.
|
||||
```sh
|
||||
#!/bin/sh
|
||||
|
||||
[ -n "$CI" ] && exit 0
|
||||
```
|
||||
- If preventing the hooks installing is problematic, just delete them in a workflow step before the action runs.
|
||||
```yml
|
||||
- run: rm .git/hooks -rf
|
||||
```
|
|
@ -15,12 +15,8 @@ This document covers terminology, how the action works, general usage guidelines
|
|||
- [Creating pull requests in a remote repository](#creating-pull-requests-in-a-remote-repository)
|
||||
- [Push using SSH (deploy keys)](#push-using-ssh-deploy-keys)
|
||||
- [Push pull request branches to a fork](#push-pull-request-branches-to-a-fork)
|
||||
- [Pushing to a fork with fine-grained permissions](#pushing-to-a-fork-with-fine-grained-permissions)
|
||||
- [Authenticating with GitHub App generated tokens](#authenticating-with-github-app-generated-tokens)
|
||||
- [Creating pull requests in a remote repository using GitHub App generated tokens](#creating-pull-requests-in-a-remote-repository-using-github-app-generated-tokens)
|
||||
- [Commit signing](#commit-signing)
|
||||
- [Commit signature verification for bots](#commit-signature-verification-for-bots)
|
||||
- [GPG commit signature verification](#gpg-commit-signature-verification)
|
||||
- [GPG commit signature verification](#gpg-commit-signature-verification)
|
||||
- [Running in a container or on self-hosted runners](#running-in-a-container-or-on-self-hosted-runners)
|
||||
|
||||
## Terminology
|
||||
|
@ -40,7 +36,7 @@ For each [event type](https://docs.github.com/en/actions/reference/events-that-t
|
|||
The default can be overridden by specifying a `ref` on checkout.
|
||||
|
||||
```yml
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: develop
|
||||
```
|
||||
|
@ -77,7 +73,7 @@ jobs:
|
|||
example:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
```
|
||||
|
||||
There may be use cases where it makes sense to execute the workflow on a branch that is not the base of the pull request. In these cases, the base branch can be specified with the `base` action input. The action will attempt to rebase changes made during the workflow on to the actual base.
|
||||
|
@ -92,7 +88,7 @@ In these cases, you *must supply* the `base` input so the action can rebase chan
|
|||
Workflows triggered by [`pull_request`](https://docs.github.com/en/actions/reference/events-that-trigger-workflows#pull_request) events will by default check out a merge commit. Set the `base` input as follows to base the new pull request on the current pull request's branch.
|
||||
|
||||
```yml
|
||||
- uses: peter-evans/create-pull-request@v7
|
||||
- uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
base: ${{ github.head_ref }}
|
||||
```
|
||||
|
@ -100,7 +96,7 @@ Workflows triggered by [`pull_request`](https://docs.github.com/en/actions/refer
|
|||
Workflows triggered by [`release`](https://docs.github.com/en/actions/reference/events-that-trigger-workflows#release) events will by default check out a tag. For most use cases, you will need to set the `base` input to the branch name of the tagged commit.
|
||||
|
||||
```yml
|
||||
- uses: peter-evans/create-pull-request@v7
|
||||
- uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
base: main
|
||||
```
|
||||
|
@ -150,15 +146,13 @@ There are a number of workarounds with different pros and cons.
|
|||
|
||||
- Use the default `GITHUB_TOKEN` and allow the action to create pull requests that have no checks enabled. Manually close pull requests and immediately reopen them. This will enable `on: pull_request` workflows to run and be added as checks. To prevent merging of pull requests without checks erroneously, use [branch protection rules](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests).
|
||||
|
||||
- Create draft pull requests by setting the `draft: always-true` input, and configure your workflow to trigger `on: ready_for_review`. The workflow will run when users manually click the "Ready for review" button on the draft pull requests. If the pull request is updated by the action, the `always-true` mode ensures that the pull request will be converted back to a draft.
|
||||
|
||||
- Use a [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) created on an account that has write access to the repository that pull requests are being created in. This is the standard workaround and [recommended by GitHub](https://docs.github.com/en/actions/using-workflows/triggering-a-workflow#triggering-a-workflow-from-a-workflow). It's advisable to use a dedicated [machine account](https://docs.github.com/en/github/site-policy/github-terms-of-service#3-account-requirements) that has collaborator access to the repository, rather than creating a PAT on a personal user account. Also note that because the account that owns the PAT will be the creator of pull requests, that user account will be unable to perform actions such as request changes or approve the pull request.
|
||||
- Use a `repo` scoped [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) created on an account that has write access to the repository that pull requests are being created in. This is the standard workaround and [recommended by GitHub](https://docs.github.com/en/actions/using-workflows/triggering-a-workflow#triggering-a-workflow-from-a-workflow). However, the PAT cannot be scoped to a specific repository so the token becomes a very sensitive secret. If this is a concern, the PAT can instead be created for a dedicated [machine account](https://docs.github.com/en/github/site-policy/github-terms-of-service#3-account-requirements) that has collaborator access to the repository. Also note that because the account that owns the PAT will be the creator of pull requests, that user account will be unable to perform actions such as request changes or approve the pull request.
|
||||
|
||||
- Use [SSH (deploy keys)](#push-using-ssh-deploy-keys) to push the pull request branch. This is arguably more secure than using a PAT because deploy keys can be set per repository. However, this method will only trigger `on: push` workflows.
|
||||
|
||||
- Use a [machine account that creates pull requests from its own fork](#push-pull-request-branches-to-a-fork). This is the most secure because the PAT created only grants access to the machine account's fork, not the main repository. This method will trigger `on: pull_request` workflows to run. Workflows triggered `on: push` will not run because the push event is in the fork.
|
||||
|
||||
- Use a [GitHub App to generate a token](#authenticating-with-github-app-generated-tokens) that can be used with this action. GitHub App generated tokens are more secure than using a Classic PAT because access permissions can be set with finer granularity and are scoped to only repositories where the App is installed. This method will trigger both `on: push` and `on: pull_request` workflows.
|
||||
- Use a [GitHub App to generate a token](#authenticating-with-github-app-generated-tokens) that can be used with this action. GitHub App generated tokens are more secure than using a PAT because GitHub App access permissions can be set with finer granularity and are scoped to only repositories where the App is installed. This method will trigger both `on: push` and `on: pull_request` workflows.
|
||||
|
||||
### Security
|
||||
|
||||
|
@ -176,29 +170,26 @@ This action uses [ncc](https://github.com/vercel/ncc) to compile the Node.js cod
|
|||
|
||||
### Creating pull requests in a remote repository
|
||||
|
||||
Checking out a branch from a different repository from where the workflow is executing will make *that repository* the target for the created pull request. In this case, the `GITHUB_TOKEN` will not work and one of the other [token options](../README.md#token) must be used.
|
||||
Checking out a branch from a different repository from where the workflow is executing will make *that repository* the target for the created pull request. In this case, a `repo` scoped [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) is required.
|
||||
|
||||
```yml
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
repository: owner/repo
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- uses: peter-evans/create-pull-request@v7
|
||||
- uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
```
|
||||
|
||||
### Push using SSH (deploy keys)
|
||||
|
||||
[Deploy keys](https://developer.github.com/v3/guides/managing-deploy-keys/#deploy-keys) can be set per repository and so are arguably more secure than using a Classic [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token).
|
||||
[Deploy keys](https://developer.github.com/v3/guides/managing-deploy-keys/#deploy-keys) can be set per repository and so are arguably more secure than using a `repo` scoped [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token).
|
||||
Allowing the action to push with a configured deploy key will trigger `on: push` workflows. This makes it an alternative to using a PAT to trigger checks for pull requests.
|
||||
|
||||
> [!NOTE]
|
||||
> You cannot use deploy keys alone to [create a pull request in a remote repository](#creating-pull-requests-in-a-remote-repository) because then using a PAT would become a requirement.
|
||||
> This method only makes sense if creating a pull request in the repository where the workflow is running.
|
||||
Note that you cannot use deploy keys alone to [create a pull request in a remote repository](#creating-pull-requests-in-a-remote-repository) because then using a PAT would become a requirement. This method only makes sense if creating a pull request in the repository where the workflow is running.
|
||||
|
||||
How to use SSH (deploy keys) with create-pull-request action:
|
||||
|
||||
|
@ -209,14 +200,14 @@ How to use SSH (deploy keys) with create-pull-request action:
|
|||
|
||||
```yml
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
```
|
||||
|
||||
### Push pull request branches to a fork
|
||||
|
@ -225,76 +216,31 @@ Instead of pushing pull request branches to the repository you want to update, y
|
|||
This allows you to employ the [principle of least privilege](https://en.wikipedia.org/wiki/Principle_of_least_privilege) by using a dedicated user acting as a [machine account](https://docs.github.com/en/github/site-policy/github-terms-of-service#3-account-requirements).
|
||||
This user only has `read` access to the main repository.
|
||||
It will use their own fork to push code and create the pull request.
|
||||
|
||||
> [!NOTE]
|
||||
> If you choose to not give the machine account `write` access to the parent repository, the following inputs cannot be used: `labels`, `assignees`, `reviewers`, `team-reviewers` and `milestone`.
|
||||
Note that if you choose to use this method (not give the machine account `write` access to the repository) the following inputs cannot be used: `labels`, `assignees`, `reviewers`, `team-reviewers` and `milestone`.
|
||||
|
||||
1. Create a new GitHub user and login.
|
||||
2. Fork the repository that you will be creating pull requests in.
|
||||
3. Create a Classic [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) with `repo` and `workflow` scopes.
|
||||
3. Create a [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token).
|
||||
4. Logout and log back into your main user account.
|
||||
5. Add a secret to your repository containing the above PAT.
|
||||
6. As shown in the following example workflow, set the `push-to-fork` input to the full repository name of the fork.
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- uses: peter-evans/create-pull-request@v7
|
||||
- uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.MACHINE_USER_PAT }}
|
||||
push-to-fork: machine-user/fork-of-repository
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> You can also combine `push-to-fork` with [creating pull requests in a remote repository](#creating-pull-requests-in-a-remote-repository).
|
||||
|
||||
#### Pushing to a fork with fine-grained permissions
|
||||
|
||||
Using a fine-grained [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) or [GitHub App](#authenticating-with-github-app-generated-tokens) with `push-to-fork` can be achieved, but comes with some caveats.
|
||||
|
||||
When using `push-to-fork`, the action needs permissions for two different repositories.
|
||||
It needs `contents: write` for the fork to push the branch, and `pull-requests: write` for the parent repository to create the pull request.
|
||||
|
||||
There are two main scenarios:
|
||||
1. The parent and fork have different owners. In this case, it's not possible to create a token that is scoped to both repositories so different tokens must be used for each.
|
||||
2. The parent and fork both have the same owner (i.e. they exist in the same org). In this case, a single token can be scoped to both repositories, but the permissions granted cannot be different. So it would defeat the purpose of using `push-to-fork`, and you might as well just create the pull request directly on the parent repository.
|
||||
|
||||
For the first scenario, the solution is to scope the token for the fork, and use the `branch-token` input to push the branch.
|
||||
The `token` input will then default to the repository's `GITHUB_TOKEN`, which will be used to create the pull request.
|
||||
|
||||
> [!NOTE]
|
||||
> Solution limitations:
|
||||
> - Since `GITHUB_TOKEN` will be used to create the pull request, the workflow *must* be executing in the parent repository where the pull request should be created.
|
||||
> - `maintainer-can-modify` *must* be set to `false`, because the `GITHUB_TOKEN` will not have `write` access to the head branch in the fork.
|
||||
|
||||
The following is an example of pushing to a fork using GitHub App tokens.
|
||||
```yaml
|
||||
- uses: actions/create-github-app-token@v1
|
||||
id: generate-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
owner: owner
|
||||
repositories: fork-of-repo
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
branch-token: ${{ steps.generate-token.outputs.token }}
|
||||
push-to-fork: owner/fork-of-repo
|
||||
maintainer-can-modify: false
|
||||
```
|
||||
|
||||
### Authenticating with GitHub App generated tokens
|
||||
|
||||
A GitHub App can be created for the sole purpose of generating tokens for use with GitHub actions.
|
||||
GitHub App generated tokens can be configured with fine-grained permissions and are scoped to only repositories where the App is installed.
|
||||
These tokens can be used in place of `GITHUB_TOKEN` or a [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token).
|
||||
GitHub App generated tokens are more secure than using a PAT because GitHub App access permissions can be set with finer granularity and are scoped to only repositories where the App is installed.
|
||||
|
||||
1. Create a minimal [GitHub App](https://docs.github.com/en/developers/apps/creating-a-github-app), setting the following fields:
|
||||
|
||||
|
@ -303,115 +249,36 @@ GitHub App generated tokens can be configured with fine-grained permissions and
|
|||
- Uncheck `Active` under `Webhook`. You do not need to enter a `Webhook URL`.
|
||||
- Under `Repository permissions: Contents` select `Access: Read & write`.
|
||||
- Under `Repository permissions: Pull requests` select `Access: Read & write`.
|
||||
- Under `Repository permissions: Workflows` select `Access: Read & write`.
|
||||
- **NOTE**: Only needed if pull requests could contain changes to Actions workflows.
|
||||
- Under `Organization permissions: Members` select `Access: Read-only`.
|
||||
- **NOTE**: Only needed if you would like add teams as reviewers to PRs.
|
||||
|
||||
2. Create a Private key from the App settings page and store it securely.
|
||||
|
||||
3. Install the App on repositories that the action will require access to in order to create pull requests.
|
||||
3. Install the App on any repository where workflows will run requiring tokens.
|
||||
|
||||
4. Set secrets on your repository containing the GitHub App ID, and the private key you created in step 2. e.g. `APP_ID`, `APP_PRIVATE_KEY`.
|
||||
|
||||
5. The following example workflow shows how to use [actions/create-github-app-token](https://github.com/actions/create-github-app-token) to generate a token for use with this action.
|
||||
5. The following example workflow shows how to use [tibdex/github-app-token](https://github.com/tibdex/github-app-token) to generate a token for use with this action.
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@v1
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: tibdex/github-app-token@v1
|
||||
id: generate-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
app_id: ${{ secrets.APP_ID }}
|
||||
private_key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
```
|
||||
|
||||
#### Creating pull requests in a remote repository using GitHub App generated tokens
|
||||
|
||||
For this case a token must be generated from the GitHub App installation of the remote repository.
|
||||
|
||||
In the following example, a pull request is being created in remote repo `owner/repo`.
|
||||
```yaml
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@v1
|
||||
id: generate-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
owner: owner
|
||||
repositories: repo
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }} # necessary if the repo is private
|
||||
repository: owner/repo
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
```
|
||||
|
||||
### Commit signing
|
||||
|
||||
[Commit signature verification](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification) is a feature where GitHub will mark signed commits as "verified" to give confidence that changes are from a trusted source.
|
||||
Some organizations require commit signing, and enforce it with branch protection rules.
|
||||
|
||||
The action supports two methods to sign commits, [commit signature verification for bots](#commit-signature-verification-for-bots), and [GPG commit signature verification](#gpg-commit-signature-verification).
|
||||
|
||||
#### Commit signature verification for bots
|
||||
|
||||
The action can sign commits as `github-actions[bot]` when using the repository's default `GITHUB_TOKEN`, or your own bot when using [GitHub App tokens](#authenticating-with-github-app-generated-tokens).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> - When setting `sign-commits: true` the action will ignore the `committer` and `author` inputs.
|
||||
> - If you attempt to use a [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) the action will create the pull request, but commits will *not* be signed. Commit signing is only supported with bot generated tokens.
|
||||
> - The GitHub API has a 40MiB limit when creating git blobs. An error will be raised if there are files in the pull request larger than this. If you hit this limit, use [GPG commit signature verification](#gpg-commit-signature-verification) instead.
|
||||
|
||||
In this example the `token` input is not supplied, so the action will use the repository's default `GITHUB_TOKEN`. This will sign commits as `github-actions[bot]`.
|
||||
```yaml
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
sign-commits: true
|
||||
```
|
||||
|
||||
In this example, the `token` input is generated using a GitHub App. This will sign commits as `<application-name>[bot]`.
|
||||
```yaml
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/create-github-app-token@v1
|
||||
id: generate-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
sign-commits: true
|
||||
```
|
||||
|
||||
#### GPG commit signature verification
|
||||
### GPG commit signature verification
|
||||
|
||||
The action can use GPG to sign commits with a GPG key that you generate yourself.
|
||||
|
||||
|
@ -431,24 +298,23 @@ The action can use GPG to sign commits with a GPG key that you generate yourself
|
|||
|
||||
6. The following example workflow shows how to use [crazy-max/ghaction-import-gpg](https://github.com/crazy-max/ghaction-import-gpg) to import your GPG key and allow the action to sign commits.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The `committer` email address *MUST* match the email address used to create your GPG key.
|
||||
Note that the `committer` email address *MUST* match the email address used to create your GPG key.
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: crazy-max/ghaction-import-gpg@v5
|
||||
- uses: crazy-max/ghaction-import-gpg@v3
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
git_user_signingkey: true
|
||||
git_commit_gpgsign: true
|
||||
git-user-signingkey: true
|
||||
git-commit-gpgsign: true
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
committer: example <email@example.com>
|
||||
|
@ -473,12 +339,12 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: apk --no-cache add git
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
```
|
||||
|
||||
**Ubuntu container example:**
|
||||
|
@ -496,10 +362,10 @@ jobs:
|
|||
add-apt-repository -y ppa:git-core/ppa
|
||||
apt-get install -y git
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
```
|
||||
|
|
|
@ -19,7 +19,9 @@
|
|||
- [autopep8](#autopep8)
|
||||
- [Misc workflow tips](#misc-workflow-tips)
|
||||
- [Filtering push events](#filtering-push-events)
|
||||
- [Bypassing git hooks](#bypassing-git-hooks)
|
||||
- [Dynamic configuration using variables](#dynamic-configuration-using-variables)
|
||||
- [Setting the pull request body from a file](#setting-the-pull-request-body-from-a-file)
|
||||
- [Using a markdown template](#using-a-markdown-template)
|
||||
- [Debugging GitHub Actions](#debugging-github-actions)
|
||||
|
||||
|
@ -42,14 +44,14 @@ jobs:
|
|||
updateAuthors:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Update AUTHORS
|
||||
run: |
|
||||
git log --format='%aN <%aE>%n%cN <%cE>' | sort -u > AUTHORS
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
commit-message: update authors
|
||||
title: Update AUTHORS
|
||||
|
@ -73,7 +75,7 @@ jobs:
|
|||
productionPromotion:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: production
|
||||
- name: Reset promotion branch
|
||||
|
@ -81,7 +83,7 @@ jobs:
|
|||
git fetch origin main:main
|
||||
git reset --hard main
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
branch: production-promotion
|
||||
```
|
||||
|
@ -106,7 +108,7 @@ jobs:
|
|||
updateChangelog:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Update Changelog
|
||||
|
@ -116,7 +118,7 @@ jobs:
|
|||
./git-chglog -o CHANGELOG.md
|
||||
rm git-chglog
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
commit-message: update changelog
|
||||
title: Update Changelog
|
||||
|
@ -144,7 +146,7 @@ jobs:
|
|||
update-dep:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16.x'
|
||||
|
@ -153,7 +155,7 @@ jobs:
|
|||
npx -p npm-check-updates ncu -u
|
||||
npm install
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
commit-message: Update dependencies
|
||||
|
@ -180,7 +182,7 @@ jobs:
|
|||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
|
@ -204,7 +206,7 @@ jobs:
|
|||
update-dep:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
|
@ -214,7 +216,7 @@ jobs:
|
|||
- name: Perform dependency resolution and write new lockfiles
|
||||
run: ./gradlew dependencies --write-locks
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
commit-message: Update dependencies
|
||||
|
@ -242,14 +244,14 @@ jobs:
|
|||
update-dep:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Update dependencies
|
||||
run: |
|
||||
cargo install cargo-edit
|
||||
cargo update
|
||||
cargo upgrade --to-lockfile
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
commit-message: Update dependencies
|
||||
|
@ -277,7 +279,7 @@ jobs:
|
|||
updateSwagger:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Get Latest Swagger UI Release
|
||||
id: swagger-ui
|
||||
run: |
|
||||
|
@ -307,7 +309,7 @@ jobs:
|
|||
# Update current release
|
||||
echo ${{ steps.swagger-ui.outputs.release_tag }} > swagger-ui.version
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
commit-message: Update swagger-ui to ${{ steps.swagger-ui.outputs.release_tag }}
|
||||
title: Update SwaggerUI to ${{ steps.swagger-ui.outputs.release_tag }}
|
||||
|
@ -324,7 +326,7 @@ jobs:
|
|||
|
||||
### Keep a fork up-to-date with its upstream
|
||||
|
||||
This example is designed to be run in a separate repository from the fork repository itself.
|
||||
This example is designed to be run in a seperate repository from the fork repository itself.
|
||||
The aim of this is to prevent committing anything to the fork's default branch would cause it to differ from the upstream.
|
||||
|
||||
In the following example workflow, `owner/repo` is the upstream repository and `fork-owner/repo` is the fork. It assumes the default branch of the upstream repository is called `main`.
|
||||
|
@ -342,7 +344,7 @@ jobs:
|
|||
updateFork:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
repository: fork-owner/repo
|
||||
- name: Reset the default branch with upstream changes
|
||||
|
@ -351,7 +353,7 @@ jobs:
|
|||
git fetch upstream main:upstream-main
|
||||
git reset --hard upstream-main
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
branch: upstream-changes
|
||||
|
@ -370,7 +372,7 @@ jobs:
|
|||
format:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Download website
|
||||
run: |
|
||||
wget \
|
||||
|
@ -384,7 +386,7 @@ jobs:
|
|||
--domains quotes.toscrape.com \
|
||||
http://quotes.toscrape.com/
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
commit-message: update local website copy
|
||||
title: Automated Updates to Local Website Copy
|
||||
|
@ -466,7 +468,7 @@ jobs:
|
|||
if: startsWith(github.head_ref, 'autopep8-patches') == false && github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
- name: autopep8
|
||||
|
@ -481,7 +483,7 @@ jobs:
|
|||
echo "branch-name=$branch-name" >> $GITHUB_OUTPUT
|
||||
- name: Create Pull Request
|
||||
if: steps.autopep8.outputs.exit-code == 2
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
commit-message: autopep8 action fixes
|
||||
title: Fixes by autopep8 action
|
||||
|
@ -515,16 +517,28 @@ jobs:
|
|||
if: startsWith(github.ref, 'refs/heads/')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
...
|
||||
|
||||
someOtherJob:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
...
|
||||
```
|
||||
|
||||
### Bypassing git hooks
|
||||
|
||||
If you have git hooks that prevent the action from working correctly you can remove them before running the action.
|
||||
|
||||
```yml
|
||||
# Remove git hooks
|
||||
- run: rm -rf .git/hooks
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
```
|
||||
|
||||
### Dynamic configuration using variables
|
||||
|
||||
The following examples show how configuration for the action can be dynamically defined in a previous workflow step.
|
||||
|
@ -540,12 +554,31 @@ Note that the step where output variables are defined must have an id.
|
|||
echo "pr_title=$pr_title" >> $GITHUB_OUTPUT
|
||||
echo "pr_body=$pr_body" >> $GITHUB_OUTPUT
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
title: ${{ steps.vars.outputs.pr_title }}
|
||||
body: ${{ steps.vars.outputs.pr_body }}
|
||||
```
|
||||
|
||||
### Setting the pull request body from a file
|
||||
|
||||
This example shows how file content can be read into a variable and passed to the action.
|
||||
|
||||
```yml
|
||||
- id: get-pr-body
|
||||
run: |
|
||||
body=$(cat pr-body.txt)
|
||||
delimiter="$(openssl rand -hex 8)"
|
||||
echo "body<<$delimiter" >> $GITHUB_OUTPUT
|
||||
echo "$body" >> $GITHUB_OUTPUT
|
||||
echo "$delimiter" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
body: ${{ steps.get-pr-body.outputs.body }}
|
||||
```
|
||||
|
||||
### Using a markdown template
|
||||
|
||||
In this example, a markdown template file is added to the repository at `.github/pull-request-template.md` with the following content.
|
||||
|
@ -566,7 +599,7 @@ The template is rendered using the [render-template](https://github.com/chuhlomi
|
|||
bar: that
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
body: ${{ steps.template.outputs.result }}
|
||||
```
|
||||
|
|
|
@ -1,59 +1,6 @@
|
|||
## Updating from `v6` to `v7`
|
||||
|
||||
### Behaviour changes
|
||||
|
||||
- Action input `git-token` has been renamed `branch-token`, to be more clear about its purpose. The `branch-token` is the token that the action will use to create and update the branch.
|
||||
- The action now handles requests that have been rate-limited by GitHub. Requests hitting a primary rate limit will retry twice, for a total of three attempts. Requests hitting a secondary rate limit will not be retried.
|
||||
- The `pull-request-operation` output now returns `none` when no operation was executed.
|
||||
- Removed deprecated output environment variable `PULL_REQUEST_NUMBER`. Please use the `pull-request-number` action output instead.
|
||||
|
||||
### What's new
|
||||
|
||||
- The action can now sign commits as `github-actions[bot]` when using `GITHUB_TOKEN`, or your own bot when using [GitHub App tokens](concepts-guidelines.md#authenticating-with-github-app-generated-tokens). See [commit signing](concepts-guidelines.md#commit-signature-verification-for-bots) for details.
|
||||
- Action input `draft` now accepts a new value `always-true`. This will set the pull request to draft status when the pull request is updated, as well as on creation.
|
||||
- A new action input `maintainer-can-modify` indicates whether [maintainers can modify](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork) the pull request. The default is `true`, which retains the existing behaviour of the action.
|
||||
- A new output `pull-request-commits-verified` returns `true` or `false`, indicating whether GitHub considers the signature of the branch's commits to be verified.
|
||||
|
||||
## Updating from `v5` to `v6`
|
||||
|
||||
### Behaviour changes
|
||||
|
||||
- The default values for `author` and `committer` have changed. See "What's new" below for details. If you are overriding the default values you will not be affected by this change.
|
||||
- On completion, the action now removes the temporary git remote configuration it adds when using `push-to-fork`. This should not affect you unless you were using the temporary configuration for some other purpose after the action completes.
|
||||
|
||||
### What's new
|
||||
|
||||
- Updated runtime to Node.js 20
|
||||
- The action now requires a minimum version of [v2.308.0](https://github.com/actions/runner/releases/tag/v2.308.0) for the Actions runner. Update self-hosted runners to v2.308.0 or later to ensure compatibility.
|
||||
- The default value for `author` has been changed to `${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>`. The change adds the `${{ github.actor_id }}+` prefix to the email address to align with GitHub's standard format for the author email address.
|
||||
- The default value for `committer` has been changed to `github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>`. This is to align with the default GitHub Actions bot user account.
|
||||
- Adds input `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. This input defaults to the value of `token`. Use this input if you would like the action to use a different token for git operations than the one used for the GitHub API.
|
||||
- `push-to-fork` now supports pushing to sibling repositories in the same network.
|
||||
- Previously, when using `push-to-fork`, the action did not remove temporary git remote configuration it adds during execution. This has been fixed and the configuration is now removed when the action completes.
|
||||
- If the pull request body is truncated due to exceeding the maximum length, the action will now suffix the body with the message "...*[Pull request body truncated]*" to indicate that the body has been truncated.
|
||||
- The action now uses `--unshallow` only when necessary, rather than as a default argument of `git fetch`. This should improve performance, particularly for large git repositories with extensive commit history.
|
||||
- The action can now be executed on one GitHub server and create pull requests on a *different* GitHub server. Server products include GitHub hosted (github.com), GitHub Enterprise Server (GHES), and GitHub Enterprise Cloud (GHEC). For example, the action can be executed on GitHub hosted and create pull requests on a GHES or GHEC instance.
|
||||
|
||||
## Updating from `v4` to `v5`
|
||||
|
||||
### Behaviour changes
|
||||
|
||||
- The action will no longer leave the local repository checked out on the pull request `branch`. Instead, it will leave the repository checked out on the branch or commit that it was when the action started.
|
||||
- When using `add-paths`, uncommitted changes will no longer be destroyed. They will be stashed and restored at the end of the action run.
|
||||
|
||||
### What's new
|
||||
|
||||
- Adds input `body-path`, the path to a file containing the pull request body.
|
||||
- At the end of the action run the local repository is now checked out on the branch or commit that it was when the action started.
|
||||
- Any uncommitted tracked or untracked changes are now stashed and restored at the end of the action run. Currently, this can only occur when using the `add-paths` input, which allows for changes to not be committed. Previously, any uncommitted changes would be destroyed.
|
||||
- The proxy implementation has been revised but is not expected to have any change in behaviour. It continues to support the standard environment variables `http_proxy`, `https_proxy` and `no_proxy`.
|
||||
- Now sets the git `safe.directory` configuration for the local repository path. The configuration is removed when the action completes. Fixes issue https://github.com/peter-evans/create-pull-request/issues/1170.
|
||||
- Now determines the git directory path using the `git rev-parse --git-dir` command. This allows users with custom repository configurations to use the action.
|
||||
- Improved handling of the `team-reviewers` input and associated errors.
|
||||
|
||||
## Updating from `v3` to `v4`
|
||||
|
||||
### Behaviour changes
|
||||
### Breaking changes
|
||||
|
||||
- The `add-paths` input no longer accepts `-A` as a valid value. When committing all new and modified files the `add-paths` input should be omitted.
|
||||
|
||||
|
@ -67,7 +14,7 @@
|
|||
|
||||
## Updating from `v2` to `v3`
|
||||
|
||||
### Behaviour changes
|
||||
### Breaking changes
|
||||
|
||||
- The `author` input now defaults to the user who triggered the workflow run. This default is set via [action.yml](../action.yml) as `${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>`, where `github.actor` is the GitHub user account associated with the run. For example, `peter-evans <peter-evans@users.noreply.github.com>`.
|
||||
|
||||
|
@ -115,7 +62,7 @@
|
|||
|
||||
## Updating from `v1` to `v2`
|
||||
|
||||
### Behaviour changes
|
||||
### Breaking changes
|
||||
|
||||
- `v2` now expects repositories to be checked out with `actions/checkout@v2`
|
||||
|
||||
|
|
11088
package-lock.json
generated
11088
package-lock.json
generated
File diff suppressed because it is too large
Load diff
49
package.json
49
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "create-pull-request",
|
||||
"version": "7.0.0",
|
||||
"version": "4.0.0",
|
||||
"private": true,
|
||||
"description": "Creates a pull request for changes to your repository in the actions workspace",
|
||||
"main": "lib/main.js",
|
||||
|
@ -29,35 +29,30 @@
|
|||
},
|
||||
"homepage": "https://github.com/peter-evans/create-pull-request",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.11.1",
|
||||
"@actions/core": "^1.10.0",
|
||||
"@actions/exec": "^1.1.1",
|
||||
"@octokit/core": "^6.1.2",
|
||||
"@octokit/plugin-paginate-rest": "^11.3.6",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^13.2.6",
|
||||
"@octokit/plugin-throttling": "^9.3.2",
|
||||
"node-fetch-native": "^1.6.4",
|
||||
"p-limit": "^6.1.0",
|
||||
"uuid": "^9.0.1"
|
||||
"@octokit/core": "^4.2.0",
|
||||
"@octokit/plugin-paginate-rest": "^5.0.1",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^6.8.1",
|
||||
"proxy-agent": "^5.0.0",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^18.19.67",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@vercel/ncc": "^0.38.3",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-import-resolver-typescript": "^3.7.0",
|
||||
"eslint-plugin-github": "^4.10.2",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jest": "^27.9.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"jest": "^29.7.0",
|
||||
"jest-circus": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/node": "^18.15.10",
|
||||
"@typescript-eslint/parser": "^5.57.0",
|
||||
"@vercel/ncc": "^0.36.1",
|
||||
"eslint": "^8.36.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.3",
|
||||
"eslint-plugin-github": "^4.7.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jest": "^27.2.1",
|
||||
"jest": "^29.5.0",
|
||||
"jest-circus": "^29.4.2",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"prettier": "^3.4.2",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typescript": "^5.7.2",
|
||||
"undici": "^6.21.0"
|
||||
"prettier": "^2.8.7",
|
||||
"ts-jest": "^29.0.5",
|
||||
"typescript": "^4.9.5"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
import * as core from '@actions/core'
|
||||
import {GitCommandManager, Commit} from './git-command-manager'
|
||||
import {GitCommandManager} from './git-command-manager'
|
||||
import {v4 as uuidv4} from 'uuid'
|
||||
import * as utils from './utils'
|
||||
|
||||
const CHERRYPICK_EMPTY =
|
||||
'The previous cherry-pick is now empty, possibly due to conflict resolution.'
|
||||
const NOTHING_TO_COMMIT = 'nothing to commit, working tree clean'
|
||||
|
||||
const FETCH_DEPTH_MARGIN = 10
|
||||
|
||||
export enum WorkingBaseType {
|
||||
Branch = 'branch',
|
||||
Commit = 'commit'
|
||||
|
@ -34,13 +31,11 @@ export async function getWorkingBaseAndType(
|
|||
export async function tryFetch(
|
||||
git: GitCommandManager,
|
||||
remote: string,
|
||||
branch: string,
|
||||
depth: number
|
||||
branch: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await git.fetch([`${branch}:refs/remotes/${remote}/${branch}`], remote, [
|
||||
'--force',
|
||||
`--depth=${depth}`
|
||||
'--force'
|
||||
])
|
||||
return true
|
||||
} catch {
|
||||
|
@ -48,27 +43,6 @@ export async function tryFetch(
|
|||
}
|
||||
}
|
||||
|
||||
export async function buildBranchCommits(
|
||||
git: GitCommandManager,
|
||||
base: string,
|
||||
branch: string
|
||||
): Promise<Commit[]> {
|
||||
const output = await git.exec(['log', '--format=%H', `${base}..${branch}`])
|
||||
const shas = output.stdout
|
||||
.split('\n')
|
||||
.filter(x => x !== '')
|
||||
.reverse()
|
||||
const commits: Commit[] = []
|
||||
for (const sha of shas) {
|
||||
const commit = await git.getCommit(sha)
|
||||
commits.push(commit)
|
||||
for (const unparsedChange of commit.unparsedChanges) {
|
||||
core.warning(`Skipping unexpected diff entry: ${unparsedChange}`)
|
||||
}
|
||||
}
|
||||
return commits
|
||||
}
|
||||
|
||||
// Return the number of commits that branch2 is ahead of branch1
|
||||
async function commitsAhead(
|
||||
git: GitCommandManager,
|
||||
|
@ -125,31 +99,6 @@ async function isEven(
|
|||
)
|
||||
}
|
||||
|
||||
// Return true if the specified number of commits on branch1 and branch2 have a diff
|
||||
async function commitsHaveDiff(
|
||||
git: GitCommandManager,
|
||||
branch1: string,
|
||||
branch2: string,
|
||||
depth: number
|
||||
): Promise<boolean> {
|
||||
// Some action use cases lead to the depth being a very large number and the diff fails.
|
||||
// I've made this check optional for now because it was a fix for an edge case that is
|
||||
// very rare, anyway.
|
||||
try {
|
||||
const diff1 = (
|
||||
await git.exec(['diff', '--stat', `${branch1}..${branch1}~${depth}`])
|
||||
).stdout.trim()
|
||||
const diff2 = (
|
||||
await git.exec(['diff', '--stat', `${branch2}..${branch2}~${depth}`])
|
||||
).stdout.trim()
|
||||
return diff1 !== diff2
|
||||
} catch (error) {
|
||||
core.info('Failed optional check of commits diff; Skipping.')
|
||||
core.debug(utils.getErrorMessage(error))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function splitLines(multilineString: string): string[] {
|
||||
return multilineString
|
||||
.split('\n')
|
||||
|
@ -157,15 +106,6 @@ function splitLines(multilineString: string): string[] {
|
|||
.filter(x => x !== '')
|
||||
}
|
||||
|
||||
interface CreateOrUpdateBranchResult {
|
||||
action: string
|
||||
base: string
|
||||
hasDiffWithBase: boolean
|
||||
baseCommit: Commit
|
||||
headSha: string
|
||||
branchCommits: Commit[]
|
||||
}
|
||||
|
||||
export async function createOrUpdateBranch(
|
||||
git: GitCommandManager,
|
||||
commitMessage: string,
|
||||
|
@ -188,6 +128,14 @@ export async function createOrUpdateBranch(
|
|||
base = base ? base : workingBase
|
||||
const baseRemote = 'origin'
|
||||
|
||||
// Set the default return values
|
||||
const result: CreateOrUpdateBranchResult = {
|
||||
action: 'none',
|
||||
base: base,
|
||||
hasDiffWithBase: false,
|
||||
headSha: ''
|
||||
}
|
||||
|
||||
// Save the working base changes to a temporary branch
|
||||
const tempBranch = uuidv4()
|
||||
await git.checkout(tempBranch, 'HEAD')
|
||||
|
@ -215,15 +163,25 @@ export async function createOrUpdateBranch(
|
|||
}
|
||||
}
|
||||
|
||||
// Stash any uncommitted tracked and untracked changes
|
||||
const stashed = await git.stashPush(['--include-untracked'])
|
||||
// Remove uncommitted tracked and untracked changes
|
||||
await git.exec(['reset', '--hard'])
|
||||
await git.exec(['clean', '-f', '-d'])
|
||||
|
||||
// Reset the working base
|
||||
// Perform fetch and reset the working base
|
||||
// Commits made during the workflow will be removed
|
||||
if (workingBaseType == WorkingBaseType.Branch) {
|
||||
core.info(`Resetting working base branch '${workingBase}'`)
|
||||
await git.checkout(workingBase)
|
||||
await git.exec(['reset', '--hard', `${baseRemote}/${workingBase}`])
|
||||
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'
|
||||
])
|
||||
} else {
|
||||
// If the remote is 'origin' we can git reset
|
||||
await git.checkout(workingBase)
|
||||
await git.exec(['reset', '--hard', `${baseRemote}/${workingBase}`])
|
||||
}
|
||||
}
|
||||
|
||||
// If the working base is not the base, rebase the temp branch commits
|
||||
|
@ -232,13 +190,8 @@ export async function createOrUpdateBranch(
|
|||
core.info(
|
||||
`Rebasing commits made to ${workingBaseType} '${workingBase}' on to base branch '${base}'`
|
||||
)
|
||||
const fetchArgs = ['--force']
|
||||
if (branchRemoteName != 'fork') {
|
||||
// If pushing to a fork we cannot shallow fetch otherwise the 'shallow update not allowed' error occurs
|
||||
fetchArgs.push('--depth=1')
|
||||
}
|
||||
// Checkout the actual base
|
||||
await git.fetch([`${base}:${base}`], baseRemote, fetchArgs)
|
||||
await git.fetch([`${base}:${base}`], baseRemote, ['--force'])
|
||||
await git.checkout(base)
|
||||
// Cherrypick commits from the temporary branch starting from the working base
|
||||
const commits = await git.revList(
|
||||
|
@ -257,29 +210,19 @@ export async function createOrUpdateBranch(
|
|||
// Reset the temp branch to the working index
|
||||
await git.checkout(tempBranch, 'HEAD')
|
||||
// Reset the base
|
||||
await git.fetch([`${base}:${base}`], baseRemote, fetchArgs)
|
||||
await git.fetch([`${base}:${base}`], baseRemote, ['--force'])
|
||||
}
|
||||
|
||||
// Determine the fetch depth for the pull request branch (best effort)
|
||||
const tempBranchCommitsAhead = await commitsAhead(git, base, tempBranch)
|
||||
const fetchDepth =
|
||||
tempBranchCommitsAhead > 0
|
||||
? tempBranchCommitsAhead + FETCH_DEPTH_MARGIN
|
||||
: FETCH_DEPTH_MARGIN
|
||||
|
||||
let action = 'none'
|
||||
let hasDiffWithBase = false
|
||||
|
||||
// Try to fetch the pull request branch
|
||||
if (!(await tryFetch(git, branchRemoteName, branch, fetchDepth))) {
|
||||
if (!(await tryFetch(git, branchRemoteName, branch))) {
|
||||
// The pull request branch does not exist
|
||||
core.info(`Pull request branch '${branch}' does not exist yet.`)
|
||||
// Create the pull request branch
|
||||
await git.checkout(branch, tempBranch)
|
||||
// Check if the pull request branch is ahead of the base
|
||||
hasDiffWithBase = await isAhead(git, base, branch)
|
||||
if (hasDiffWithBase) {
|
||||
action = 'created'
|
||||
result.hasDiffWithBase = await isAhead(git, base, branch)
|
||||
if (result.hasDiffWithBase) {
|
||||
result.action = 'created'
|
||||
core.info(`Created branch '${branch}'`)
|
||||
} else {
|
||||
core.info(
|
||||
|
@ -296,26 +239,21 @@ export async function createOrUpdateBranch(
|
|||
|
||||
// Reset the branch if one of the following conditions is true.
|
||||
// - If the branch differs from the recreated temp branch.
|
||||
// - If the number of commits ahead of the base branch differs between the branch and
|
||||
// temp branch. This catches a case where the base branch has been force pushed to
|
||||
// a new commit.
|
||||
// - If the recreated temp branch is not ahead of the base. This means there will be
|
||||
// no pull request diff after the branch is reset. This will reset any undeleted
|
||||
// branches after merging. In particular, it catches a case where the branch was
|
||||
// squash merged but not deleted. We need to reset to make sure it doesn't appear
|
||||
// to have a diff with the base due to different commits for the same changes.
|
||||
// - If the diff of the commits ahead of the base branch differs between the branch and
|
||||
// temp branch. This catches a case where changes have been partially merged to the
|
||||
// base. The overall diff is the same, but the branch needs to be rebased to show
|
||||
// the correct diff.
|
||||
//
|
||||
// - If the number of commits ahead of the base branch differs between the branch and
|
||||
// temp branch. This catches a case where the base branch has been force pushed to
|
||||
// a new commit.
|
||||
// For changes on base this reset is equivalent to a rebase of the pull request branch.
|
||||
const tempBranchCommitsAhead = await commitsAhead(git, base, tempBranch)
|
||||
const branchCommitsAhead = await commitsAhead(git, base, branch)
|
||||
if (
|
||||
(await git.hasDiff([`${branch}..${tempBranch}`])) ||
|
||||
branchCommitsAhead != tempBranchCommitsAhead ||
|
||||
!(tempBranchCommitsAhead > 0) || // !isAhead
|
||||
(await commitsHaveDiff(git, branch, tempBranch, tempBranchCommitsAhead))
|
||||
!(tempBranchCommitsAhead > 0) // !isAhead
|
||||
) {
|
||||
core.info(`Resetting '${branch}'`)
|
||||
// Alternatively, git switch -C branch tempBranch
|
||||
|
@ -326,47 +264,31 @@ export async function createOrUpdateBranch(
|
|||
// If the branch was reset or updated it will be ahead
|
||||
// It may be behind if a reset now results in no diff with the base
|
||||
if (!(await isEven(git, `${branchRemoteName}/${branch}`, branch))) {
|
||||
action = 'updated'
|
||||
result.action = 'updated'
|
||||
core.info(`Updated branch '${branch}'`)
|
||||
} else {
|
||||
action = 'not-updated'
|
||||
result.action = 'not-updated'
|
||||
core.info(
|
||||
`Branch '${branch}' is even with its remote and will not be updated`
|
||||
)
|
||||
}
|
||||
|
||||
// Check if the pull request branch is ahead of the base
|
||||
hasDiffWithBase = await isAhead(git, base, branch)
|
||||
result.hasDiffWithBase = await isAhead(git, base, branch)
|
||||
}
|
||||
|
||||
// Get the base and head SHAs
|
||||
const baseSha = await git.revParse(base)
|
||||
const baseCommit = await git.getCommit(baseSha)
|
||||
const headSha = await git.revParse(branch)
|
||||
|
||||
let branchCommits: Commit[] = []
|
||||
if (hasDiffWithBase) {
|
||||
// Build the branch commits
|
||||
branchCommits = await buildBranchCommits(git, base, branch)
|
||||
}
|
||||
// Get the pull request branch SHA
|
||||
result.headSha = await git.revParse('HEAD')
|
||||
|
||||
// Delete the temporary branch
|
||||
await git.exec(['branch', '--delete', '--force', tempBranch])
|
||||
|
||||
// Checkout the working base to leave the local repository as it was found
|
||||
await git.checkout(workingBase)
|
||||
|
||||
// Restore any stashed changes
|
||||
if (stashed) {
|
||||
await git.stashPop()
|
||||
}
|
||||
|
||||
return {
|
||||
action: action,
|
||||
base: base,
|
||||
hasDiffWithBase: hasDiffWithBase,
|
||||
baseCommit: baseCommit,
|
||||
headSha: headSha,
|
||||
branchCommits: branchCommits
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
interface CreateOrUpdateBranchResult {
|
||||
action: string
|
||||
base: string
|
||||
hasDiffWithBase: boolean
|
||||
headSha: string
|
||||
}
|
||||
|
|
|
@ -6,12 +6,11 @@ import {
|
|||
} from './create-or-update-branch'
|
||||
import {GitHubHelper} from './github-helper'
|
||||
import {GitCommandManager} from './git-command-manager'
|
||||
import {GitConfigHelper} from './git-config-helper'
|
||||
import {GitAuthHelper} from './git-auth-helper'
|
||||
import * as utils from './utils'
|
||||
|
||||
export interface Inputs {
|
||||
token: string
|
||||
branchToken: string
|
||||
path: string
|
||||
addPaths: string[]
|
||||
commitMessage: string
|
||||
|
@ -23,38 +22,41 @@ export interface Inputs {
|
|||
branchSuffix: string
|
||||
base: string
|
||||
pushToFork: string
|
||||
signCommits: boolean
|
||||
title: string
|
||||
body: string
|
||||
bodyPath: string
|
||||
labels: string[]
|
||||
assignees: string[]
|
||||
reviewers: string[]
|
||||
teamReviewers: string[]
|
||||
milestone: number
|
||||
draft: {
|
||||
value: boolean
|
||||
always: boolean
|
||||
}
|
||||
maintainerCanModify: boolean
|
||||
draft: boolean
|
||||
}
|
||||
|
||||
export async function createPullRequest(inputs: Inputs): Promise<void> {
|
||||
let gitConfigHelper, git
|
||||
let gitAuthHelper
|
||||
try {
|
||||
core.startGroup('Prepare git configuration')
|
||||
if (!inputs.token) {
|
||||
throw new Error(`Input 'token' not supplied. Unable to continue.`)
|
||||
}
|
||||
|
||||
// Get the repository path
|
||||
const repoPath = utils.getRepoPath(inputs.path)
|
||||
git = await GitCommandManager.create(repoPath)
|
||||
gitConfigHelper = await GitConfigHelper.create(git)
|
||||
// Create a git command manager
|
||||
const git = await GitCommandManager.create(repoPath)
|
||||
|
||||
// Save and unset the extraheader auth config if it exists
|
||||
core.startGroup('Save persisted git credentials')
|
||||
gitAuthHelper = new GitAuthHelper(git)
|
||||
await gitAuthHelper.savePersistedAuth()
|
||||
core.endGroup()
|
||||
|
||||
// Init the GitHub client
|
||||
const githubHelper = new GitHubHelper(inputs.token)
|
||||
|
||||
core.startGroup('Determining the base and head repositories')
|
||||
const baseRemote = gitConfigHelper.getGitRemote()
|
||||
// Init the GitHub clients
|
||||
const apiUrl = await GitHubHelper.determineApiUrl(baseRemote.hostname)
|
||||
core.info(`Using API base URL: ${apiUrl}`)
|
||||
const ghBranch = new GitHubHelper(apiUrl, inputs.branchToken)
|
||||
const ghPull = new GitHubHelper(apiUrl, inputs.token)
|
||||
// Determine the base repository from git config
|
||||
const remoteUrl = await git.tryGetRemoteUrl()
|
||||
const baseRemote = utils.getRemoteDetail(remoteUrl)
|
||||
// Determine the head repository; the target for the pull request branch
|
||||
const branchRemoteName = inputs.pushToFork ? 'fork' : 'origin'
|
||||
const branchRepository = inputs.pushToFork
|
||||
|
@ -65,22 +67,12 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
|
|||
core.info(
|
||||
`Checking if '${branchRepository}' is a fork of '${baseRemote.repository}'`
|
||||
)
|
||||
const baseParentRepository = await ghBranch.getRepositoryParent(
|
||||
baseRemote.repository
|
||||
const parentRepository = await githubHelper.getRepositoryParent(
|
||||
branchRepository
|
||||
)
|
||||
const branchParentRepository =
|
||||
await ghBranch.getRepositoryParent(branchRepository)
|
||||
if (branchParentRepository == null) {
|
||||
if (parentRepository != baseRemote.repository) {
|
||||
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.`
|
||||
`Repository '${branchRepository}' is not a fork of '${baseRemote.repository}'. Unable to continue.`
|
||||
)
|
||||
}
|
||||
// Add a remote for the fork
|
||||
|
@ -99,7 +91,7 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
|
|||
// Configure auth
|
||||
if (baseRemote.protocol == 'HTTPS') {
|
||||
core.startGroup('Configuring credential for HTTPS authentication')
|
||||
await gitConfigHelper.configureToken(inputs.branchToken)
|
||||
await gitAuthHelper.configureToken(inputs.token)
|
||||
core.endGroup()
|
||||
}
|
||||
|
||||
|
@ -182,11 +174,6 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
|
|||
)
|
||||
core.endGroup()
|
||||
|
||||
// Action outputs
|
||||
const outputs = new Map<string, string>()
|
||||
outputs.set('pull-request-branch', inputs.branch)
|
||||
outputs.set('pull-request-operation', 'none')
|
||||
|
||||
// Create or update the pull request branch
|
||||
core.startGroup('Create or update the pull request branch')
|
||||
const result = await createOrUpdateBranch(
|
||||
|
@ -198,9 +185,6 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
|
|||
inputs.signoff,
|
||||
inputs.addPaths
|
||||
)
|
||||
outputs.set('pull-request-head-sha', result.headSha)
|
||||
// Set the base. It would have been '' if not specified as an input
|
||||
inputs.base = result.base
|
||||
core.endGroup()
|
||||
|
||||
if (['created', 'updated'].includes(result.action)) {
|
||||
|
@ -208,55 +192,39 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
|
|||
core.startGroup(
|
||||
`Pushing pull request branch to '${branchRemoteName}/${inputs.branch}'`
|
||||
)
|
||||
if (inputs.signCommits) {
|
||||
// Create signed commits via the GitHub API
|
||||
const stashed = await git.stashPush(['--include-untracked'])
|
||||
await git.checkout(inputs.branch)
|
||||
const pushSignedCommitsResult = await ghBranch.pushSignedCommits(
|
||||
result.branchCommits,
|
||||
result.baseCommit,
|
||||
repoPath,
|
||||
branchRepository,
|
||||
inputs.branch
|
||||
)
|
||||
outputs.set('pull-request-head-sha', pushSignedCommitsResult.sha)
|
||||
outputs.set(
|
||||
'pull-request-commits-verified',
|
||||
pushSignedCommitsResult.verified.toString()
|
||||
)
|
||||
await git.checkout('-')
|
||||
if (stashed) {
|
||||
await git.stashPop()
|
||||
}
|
||||
} else {
|
||||
await git.push([
|
||||
'--force-with-lease',
|
||||
branchRemoteName,
|
||||
`${inputs.branch}:refs/heads/${inputs.branch}`
|
||||
])
|
||||
}
|
||||
await git.push([
|
||||
'--force-with-lease',
|
||||
branchRemoteName,
|
||||
`HEAD:refs/heads/${inputs.branch}`
|
||||
])
|
||||
core.endGroup()
|
||||
}
|
||||
|
||||
// Set the base. It would have been '' if not specified as an input
|
||||
inputs.base = result.base
|
||||
|
||||
if (result.hasDiffWithBase) {
|
||||
// Create or update the pull request
|
||||
core.startGroup('Create or update the pull request')
|
||||
const pull = await ghPull.createOrUpdatePullRequest(
|
||||
const pull = await githubHelper.createOrUpdatePullRequest(
|
||||
inputs,
|
||||
baseRemote.repository,
|
||||
branchRepository
|
||||
)
|
||||
outputs.set('pull-request-number', pull.number.toString())
|
||||
outputs.set('pull-request-url', pull.html_url)
|
||||
core.endGroup()
|
||||
|
||||
// Set outputs
|
||||
core.startGroup('Setting outputs')
|
||||
core.setOutput('pull-request-number', pull.number)
|
||||
core.setOutput('pull-request-url', pull.html_url)
|
||||
if (pull.created) {
|
||||
outputs.set('pull-request-operation', 'created')
|
||||
core.setOutput('pull-request-operation', 'created')
|
||||
} else if (result.action == 'updated') {
|
||||
outputs.set('pull-request-operation', 'updated')
|
||||
// The pull request was updated AND the branch was updated.
|
||||
// Convert back to draft if 'draft: always-true' is set.
|
||||
if (inputs.draft.always && pull.draft !== undefined && !pull.draft) {
|
||||
await ghPull.convertToDraft(pull.node_id)
|
||||
}
|
||||
core.setOutput('pull-request-operation', 'updated')
|
||||
}
|
||||
core.setOutput('pull-request-head-sha', result.headSha)
|
||||
// Deprecated
|
||||
core.exportVariable('PULL_REQUEST_NUMBER', pull.number)
|
||||
core.endGroup()
|
||||
} else {
|
||||
// There is no longer a diff with the base
|
||||
|
@ -273,53 +241,20 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
|
|||
branchRemoteName,
|
||||
`refs/heads/${inputs.branch}`
|
||||
])
|
||||
outputs.set('pull-request-operation', 'closed')
|
||||
// Set outputs
|
||||
core.startGroup('Setting outputs')
|
||||
core.setOutput('pull-request-operation', 'closed')
|
||||
core.endGroup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
core.startGroup('Setting outputs')
|
||||
// If the head commit is signed, get its verification status if we don't already know it.
|
||||
// This can happen if the branch wasn't updated (action = 'not-updated'), or GPG commit signing is in use.
|
||||
if (
|
||||
!outputs.has('pull-request-commits-verified') &&
|
||||
result.branchCommits.length > 0 &&
|
||||
result.branchCommits[result.branchCommits.length - 1].signed
|
||||
) {
|
||||
// Using the local head commit SHA because in this case commits have not been pushed via the API.
|
||||
core.info(`Checking verification status of head commit ${result.headSha}`)
|
||||
try {
|
||||
const headCommit = await ghBranch.getCommit(
|
||||
result.headSha,
|
||||
branchRepository
|
||||
)
|
||||
outputs.set(
|
||||
'pull-request-commits-verified',
|
||||
headCommit.verified.toString()
|
||||
)
|
||||
} catch (error) {
|
||||
core.warning('Failed to check verification status of head commit.')
|
||||
core.debug(utils.getErrorMessage(error))
|
||||
}
|
||||
}
|
||||
if (!outputs.has('pull-request-commits-verified')) {
|
||||
outputs.set('pull-request-commits-verified', 'false')
|
||||
}
|
||||
|
||||
// Set outputs
|
||||
for (const [key, value] of outputs) {
|
||||
core.info(`${key} = ${value}`)
|
||||
core.setOutput(key, value)
|
||||
}
|
||||
core.endGroup()
|
||||
} catch (error) {
|
||||
core.setFailed(utils.getErrorMessage(error))
|
||||
} finally {
|
||||
core.startGroup('Restore git configuration')
|
||||
if (inputs.pushToFork) {
|
||||
await git.exec(['remote', 'rm', 'fork'])
|
||||
}
|
||||
await gitConfigHelper.close()
|
||||
// Remove auth and restore persisted auth config if it existed
|
||||
core.startGroup('Restore persisted git credentials')
|
||||
await gitAuthHelper.removeAuth()
|
||||
await gitAuthHelper.restorePersistedAuth()
|
||||
core.endGroup()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,122 +5,26 @@ import * as path from 'path'
|
|||
import {URL} from 'url'
|
||||
import * as utils from './utils'
|
||||
|
||||
interface GitRemote {
|
||||
hostname: string
|
||||
protocol: string
|
||||
repository: string
|
||||
}
|
||||
|
||||
export class GitConfigHelper {
|
||||
export class GitAuthHelper {
|
||||
private git: GitCommandManager
|
||||
private gitConfigPath = ''
|
||||
private workingDirectory: string
|
||||
private safeDirectoryConfigKey = 'safe.directory'
|
||||
private safeDirectoryAdded = false
|
||||
private remoteUrl = ''
|
||||
private extraheaderConfigKey = ''
|
||||
private gitConfigPath: string
|
||||
private extraheaderConfigKey: string
|
||||
private extraheaderConfigPlaceholderValue = 'AUTHORIZATION: basic ***'
|
||||
private extraheaderConfigValueRegex = '^AUTHORIZATION:'
|
||||
private persistedExtraheaderConfigValue = ''
|
||||
|
||||
private constructor(git: GitCommandManager) {
|
||||
constructor(git: GitCommandManager) {
|
||||
this.git = git
|
||||
this.workingDirectory = this.git.getWorkingDirectory()
|
||||
}
|
||||
|
||||
static async create(git: GitCommandManager): Promise<GitConfigHelper> {
|
||||
const gitConfigHelper = new GitConfigHelper(git)
|
||||
await gitConfigHelper.addSafeDirectory()
|
||||
await gitConfigHelper.fetchRemoteDetail()
|
||||
await gitConfigHelper.savePersistedAuth()
|
||||
return gitConfigHelper
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
// Remove auth and restore persisted auth config if it existed
|
||||
await this.removeAuth()
|
||||
await this.restorePersistedAuth()
|
||||
await this.removeSafeDirectory()
|
||||
}
|
||||
|
||||
async addSafeDirectory(): Promise<void> {
|
||||
const exists = await this.git.configExists(
|
||||
this.safeDirectoryConfigKey,
|
||||
this.workingDirectory,
|
||||
true
|
||||
)
|
||||
if (!exists) {
|
||||
await this.git.config(
|
||||
this.safeDirectoryConfigKey,
|
||||
this.workingDirectory,
|
||||
true,
|
||||
true
|
||||
)
|
||||
this.safeDirectoryAdded = true
|
||||
}
|
||||
}
|
||||
|
||||
async removeSafeDirectory(): Promise<void> {
|
||||
if (this.safeDirectoryAdded) {
|
||||
await this.git.tryConfigUnset(
|
||||
this.safeDirectoryConfigKey,
|
||||
this.workingDirectory,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async fetchRemoteDetail(): Promise<void> {
|
||||
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 httpsMatch = remoteUrl.match(httpsUrlPattern)
|
||||
if (httpsMatch) {
|
||||
return {
|
||||
hostname: httpsMatch[2],
|
||||
protocol: 'HTTPS',
|
||||
repository: httpsMatch[3]
|
||||
}
|
||||
}
|
||||
|
||||
const sshUrlPattern = new RegExp('^git@(.+?):(.+/.+)\\.git$', 'i')
|
||||
const sshMatch = remoteUrl.match(sshUrlPattern)
|
||||
if (sshMatch) {
|
||||
return {
|
||||
hostname: sshMatch[1],
|
||||
protocol: 'SSH',
|
||||
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`
|
||||
this.gitConfigPath = path.join(
|
||||
this.git.getWorkingDirectory(),
|
||||
'.git',
|
||||
'config'
|
||||
)
|
||||
const serverUrl = this.getServerUrl()
|
||||
this.extraheaderConfigKey = `http.${serverUrl.origin}/.extraheader`
|
||||
}
|
||||
|
||||
async savePersistedAuth(): Promise<void> {
|
||||
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()
|
||||
}
|
||||
|
@ -202,10 +106,6 @@ export class GitConfigHelper {
|
|||
find: string,
|
||||
replace: string
|
||||
): Promise<void> {
|
||||
if (this.gitConfigPath.length === 0) {
|
||||
const gitDir = await this.git.getGitDirectory()
|
||||
this.gitConfigPath = path.join(this.workingDirectory, gitDir, 'config')
|
||||
}
|
||||
let content = (await fs.promises.readFile(this.gitConfigPath)).toString()
|
||||
const index = content.indexOf(find)
|
||||
if (index < 0 || index != content.lastIndexOf(find)) {
|
||||
|
@ -214,4 +114,14 @@ export class GitConfigHelper {
|
|||
content = content.replace(find, replace)
|
||||
await fs.promises.writeFile(this.gitConfigPath, content)
|
||||
}
|
||||
|
||||
private getServerUrl(): URL {
|
||||
// todo: remove GITHUB_URL after support for GHES Alpha is no longer needed
|
||||
// See https://github.com/actions/checkout/blob/main/src/url-helper.ts#L22-L29
|
||||
return new URL(
|
||||
process.env['GITHUB_SERVER_URL'] ||
|
||||
process.env['GITHUB_URL'] ||
|
||||
'https://github.com'
|
||||
)
|
||||
}
|
||||
}
|
|
@ -5,22 +5,6 @@ import * as path from 'path'
|
|||
|
||||
const tagsRefSpec = '+refs/tags/*:refs/tags/*'
|
||||
|
||||
export type Commit = {
|
||||
sha: string
|
||||
tree: string
|
||||
parents: string[]
|
||||
signed: boolean
|
||||
subject: string
|
||||
body: string
|
||||
changes: {
|
||||
mode: string
|
||||
dstSha: string
|
||||
status: 'A' | 'M' | 'D'
|
||||
path: string
|
||||
}[]
|
||||
unparsedChanges: string[]
|
||||
}
|
||||
|
||||
export class GitCommandManager {
|
||||
private gitPath: string
|
||||
private workingDirectory: string
|
||||
|
@ -88,15 +72,14 @@ export class GitCommandManager {
|
|||
async config(
|
||||
configKey: string,
|
||||
configValue: string,
|
||||
globalConfig?: boolean,
|
||||
add?: boolean
|
||||
globalConfig?: boolean
|
||||
): Promise<void> {
|
||||
const args: string[] = ['config', globalConfig ? '--global' : '--local']
|
||||
if (add) {
|
||||
args.push('--add')
|
||||
}
|
||||
args.push(...[configKey, configValue])
|
||||
await this.exec(args)
|
||||
await this.exec([
|
||||
'config',
|
||||
globalConfig ? '--global' : '--local',
|
||||
configKey,
|
||||
configValue
|
||||
])
|
||||
}
|
||||
|
||||
async configExists(
|
||||
|
@ -121,8 +104,7 @@ export class GitCommandManager {
|
|||
async fetch(
|
||||
refSpec: string[],
|
||||
remoteName?: string,
|
||||
options?: string[],
|
||||
unshallow = false
|
||||
options?: string[]
|
||||
): Promise<void> {
|
||||
const args = ['-c', 'protocol.version=2', 'fetch']
|
||||
if (!refSpec.some(x => x === tagsRefSpec)) {
|
||||
|
@ -130,9 +112,7 @@ export class GitCommandManager {
|
|||
}
|
||||
|
||||
args.push('--progress', '--no-recurse-submodules')
|
||||
|
||||
if (
|
||||
unshallow &&
|
||||
utils.fileExistsSync(path.join(this.workingDirectory, '.git', 'shallow'))
|
||||
) {
|
||||
args.push('--unshallow')
|
||||
|
@ -154,48 +134,6 @@ export class GitCommandManager {
|
|||
await this.exec(args)
|
||||
}
|
||||
|
||||
async getCommit(ref: string): Promise<Commit> {
|
||||
const endOfBody = '###EOB###'
|
||||
const output = await this.exec([
|
||||
'show',
|
||||
'--raw',
|
||||
'--cc',
|
||||
'--no-renames',
|
||||
'--no-abbrev',
|
||||
`--format=%H%n%T%n%P%n%G?%n%s%n%b%n${endOfBody}`,
|
||||
ref
|
||||
])
|
||||
const lines = output.stdout.split('\n')
|
||||
const endOfBodyIndex = lines.lastIndexOf(endOfBody)
|
||||
const detailLines = lines.slice(0, endOfBodyIndex)
|
||||
|
||||
const unparsedChanges: string[] = []
|
||||
return <Commit>{
|
||||
sha: detailLines[0],
|
||||
tree: detailLines[1],
|
||||
parents: detailLines[2].split(' '),
|
||||
signed: detailLines[3] !== 'N',
|
||||
subject: detailLines[4],
|
||||
body: detailLines.slice(5, endOfBodyIndex).join('\n'),
|
||||
changes: lines.slice(endOfBodyIndex + 2, -1).map(line => {
|
||||
const change = line.match(
|
||||
/^:(\d{6}) (\d{6}) \w{40} (\w{40}) ([AMD])\s+(.*)$/
|
||||
)
|
||||
if (change) {
|
||||
return {
|
||||
mode: change[4] === 'D' ? change[1] : change[2],
|
||||
dstSha: change[3],
|
||||
status: change[4],
|
||||
path: change[5]
|
||||
}
|
||||
} else {
|
||||
unparsedChanges.push(line)
|
||||
}
|
||||
}),
|
||||
unparsedChanges: unparsedChanges
|
||||
}
|
||||
}
|
||||
|
||||
async getConfigValue(configKey: string, configValue = '.'): Promise<string> {
|
||||
const output = await this.exec([
|
||||
'config',
|
||||
|
@ -207,10 +145,6 @@ export class GitCommandManager {
|
|||
return output.stdout.trim().split(`${configKey} `)[1]
|
||||
}
|
||||
|
||||
getGitDirectory(): Promise<string> {
|
||||
return this.revParse('--git-dir')
|
||||
}
|
||||
|
||||
getWorkingDirectory(): string {
|
||||
return this.workingDirectory
|
||||
}
|
||||
|
@ -276,23 +210,6 @@ export class GitCommandManager {
|
|||
return output.stdout.trim()
|
||||
}
|
||||
|
||||
async stashPush(options?: string[]): Promise<boolean> {
|
||||
const args = ['stash', 'push']
|
||||
if (options) {
|
||||
args.push(...options)
|
||||
}
|
||||
const output = await this.exec(args)
|
||||
return output.stdout.trim() !== 'No local changes to save'
|
||||
}
|
||||
|
||||
async stashPop(options?: string[]): Promise<void> {
|
||||
const args = ['stash', 'pop']
|
||||
if (options) {
|
||||
args.push(...options)
|
||||
}
|
||||
await this.exec(args)
|
||||
}
|
||||
|
||||
async status(options?: string[]): Promise<string> {
|
||||
const args = ['status']
|
||||
if (options) {
|
||||
|
|
|
@ -1,16 +1,10 @@
|
|||
import * as core from '@actions/core'
|
||||
import {Inputs} from './create-pull-request'
|
||||
import {Commit} from './git-command-manager'
|
||||
import {Octokit, OctokitOptions, throttleOptions} from './octokit-client'
|
||||
import pLimit from 'p-limit'
|
||||
import {Octokit, OctokitOptions} from './octokit-client'
|
||||
import * as utils from './utils'
|
||||
|
||||
const ERROR_PR_ALREADY_EXISTS = 'A pull request already exists for'
|
||||
const ERROR_PR_REVIEW_TOKEN_SCOPE =
|
||||
'Validation Failed: "Could not resolve to a node with the global id of'
|
||||
const ERROR_PR_FORK_COLLAB = `Fork collab can't be granted by someone without permission`
|
||||
|
||||
const blobCreationLimit = pLimit(8)
|
||||
const ERROR_PR_REVIEW_FROM_AUTHOR =
|
||||
'Review cannot be requested from pull request author'
|
||||
|
||||
interface Repository {
|
||||
owner: string
|
||||
|
@ -20,75 +14,21 @@ interface Repository {
|
|||
interface Pull {
|
||||
number: number
|
||||
html_url: string
|
||||
node_id: string
|
||||
draft?: boolean
|
||||
created: boolean
|
||||
}
|
||||
|
||||
interface CommitResponse {
|
||||
sha: string
|
||||
tree: string
|
||||
verified: boolean
|
||||
}
|
||||
|
||||
type TreeObject = {
|
||||
path: string
|
||||
mode: '100644' | '100755' | '040000' | '160000' | '120000'
|
||||
sha: string | null
|
||||
type: 'blob' | 'commit'
|
||||
}
|
||||
|
||||
export class GitHubHelper {
|
||||
private octokit: InstanceType<typeof Octokit>
|
||||
|
||||
constructor(apiUrl: string, token: string) {
|
||||
constructor(token: string) {
|
||||
const options: OctokitOptions = {}
|
||||
if (token) {
|
||||
options.auth = `${token}`
|
||||
}
|
||||
options.baseUrl = apiUrl
|
||||
options.throttle = throttleOptions
|
||||
options.baseUrl = process.env['GITHUB_API_URL'] || 'https://api.github.com'
|
||||
this.octokit = new Octokit(options)
|
||||
}
|
||||
|
||||
static async determineApiUrl(hostname: string): Promise<string> {
|
||||
if (hostname === 'github.com') {
|
||||
return 'https://api.github.com'
|
||||
}
|
||||
|
||||
const baseUrl = `https://${hostname}`
|
||||
const possiblePaths = [
|
||||
'/api/v4/version',
|
||||
'/api/forgejo/v1/version',
|
||||
'/api/v1/version'
|
||||
]
|
||||
|
||||
for (const path of possiblePaths) {
|
||||
try {
|
||||
const url = `${baseUrl}${path}`
|
||||
const response = await fetch(url, {method: 'GET', redirect: 'manual'}) // GitLab redirects
|
||||
// invalid API paths
|
||||
// to login prompt
|
||||
// which returns 200
|
||||
|
||||
const contentType = response.headers.get('Content-Type') || ''
|
||||
if (
|
||||
(response.ok || [401, 403].includes(response.status)) && // We might get 401, 403
|
||||
// as we're unauthorised
|
||||
contentType.includes('application/json')
|
||||
) {
|
||||
return path.includes('/version') ? url.replace('/version', '') : url
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors and try the next path
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unable to determine API base URL for hostname: ${hostname}`
|
||||
)
|
||||
}
|
||||
|
||||
private parseRepository(repository: string): Repository {
|
||||
const [owner, repo] = repository.split('/')
|
||||
return {
|
||||
|
@ -104,6 +44,7 @@ export class GitHubHelper {
|
|||
): Promise<Pull> {
|
||||
const [headOwner] = headRepository.split('/')
|
||||
const headBranch = `${headOwner}:${inputs.branch}`
|
||||
const headBranchFull = `${headRepository}:${inputs.branch}`
|
||||
|
||||
// Try to create the pull request
|
||||
try {
|
||||
|
@ -112,11 +53,9 @@ export class GitHubHelper {
|
|||
...this.parseRepository(baseRepository),
|
||||
title: inputs.title,
|
||||
head: headBranch,
|
||||
head_repo: headRepository,
|
||||
base: inputs.base,
|
||||
body: inputs.body,
|
||||
draft: inputs.draft.value,
|
||||
maintainer_can_modify: inputs.maintainerCanModify
|
||||
draft: inputs.draft
|
||||
})
|
||||
core.info(
|
||||
`Created pull request #${pull.number} (${headBranch} => ${inputs.base})`
|
||||
|
@ -124,22 +63,13 @@ export class GitHubHelper {
|
|||
return {
|
||||
number: pull.number,
|
||||
html_url: pull.html_url,
|
||||
node_id: pull.node_id,
|
||||
draft: pull.draft,
|
||||
created: true
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMessage = utils.getErrorMessage(e)
|
||||
if (errorMessage.includes(ERROR_PR_ALREADY_EXISTS)) {
|
||||
if (
|
||||
utils.getErrorMessage(e).includes(`A pull request already exists for`)
|
||||
) {
|
||||
core.info(`A pull request already exists for ${headBranch}`)
|
||||
} else if (errorMessage.includes(ERROR_PR_FORK_COLLAB)) {
|
||||
core.warning(
|
||||
'An attempt was made to create a pull request using a token that does not have write access to the head branch.'
|
||||
)
|
||||
core.warning(
|
||||
`For this case, set input 'maintainer-can-modify' to 'false' to allow pull request creation.`
|
||||
)
|
||||
throw e
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
|
@ -150,7 +80,7 @@ export class GitHubHelper {
|
|||
const {data: pulls} = await this.octokit.rest.pulls.list({
|
||||
...this.parseRepository(baseRepository),
|
||||
state: 'open',
|
||||
head: headBranch,
|
||||
head: headBranchFull,
|
||||
base: inputs.base
|
||||
})
|
||||
core.info(`Attempting update of pull request`)
|
||||
|
@ -166,18 +96,18 @@ export class GitHubHelper {
|
|||
return {
|
||||
number: pull.number,
|
||||
html_url: pull.html_url,
|
||||
node_id: pull.node_id,
|
||||
draft: pull.draft,
|
||||
created: false
|
||||
}
|
||||
}
|
||||
|
||||
async getRepositoryParent(headRepository: string): Promise<string | null> {
|
||||
async getRepositoryParent(headRepository: string): Promise<string> {
|
||||
const {data: headRepo} = await this.octokit.rest.repos.get({
|
||||
...this.parseRepository(headRepository)
|
||||
})
|
||||
if (!headRepo.parent) {
|
||||
return null
|
||||
throw new Error(
|
||||
`Repository '${headRepository}' is not a fork. Unable to continue.`
|
||||
)
|
||||
}
|
||||
return headRepo.parent.full_name
|
||||
}
|
||||
|
@ -228,11 +158,10 @@ export class GitHubHelper {
|
|||
requestReviewersParams['reviewers'] = inputs.reviewers
|
||||
core.info(`Requesting reviewers '${inputs.reviewers}'`)
|
||||
}
|
||||
if (inputs.teamReviewers.length > 0) {
|
||||
const teams = utils.stripOrgPrefixFromTeams(inputs.teamReviewers)
|
||||
requestReviewersParams['team_reviewers'] = teams
|
||||
core.info(`Requesting team reviewers '${teams}'`)
|
||||
}
|
||||
// if (inputs.teamReviewers.length > 0) {
|
||||
// requestReviewersParams['team_reviewers'] = inputs.teamReviewers
|
||||
// core.info(`Requesting team reviewers '${inputs.teamReviewers}'`)
|
||||
// }
|
||||
if (Object.keys(requestReviewersParams).length > 0) {
|
||||
try {
|
||||
await this.octokit.rest.pulls.requestReviewers({
|
||||
|
@ -241,199 +170,155 @@ export class GitHubHelper {
|
|||
...requestReviewersParams
|
||||
})
|
||||
} catch (e) {
|
||||
if (utils.getErrorMessage(e).includes(ERROR_PR_REVIEW_TOKEN_SCOPE)) {
|
||||
core.error(
|
||||
`Unable to request reviewers. If requesting team reviewers a 'repo' scoped PAT is required.`
|
||||
)
|
||||
if (utils.getErrorMessage(e).includes(ERROR_PR_REVIEW_FROM_AUTHOR)) {
|
||||
core.warning(ERROR_PR_REVIEW_FROM_AUTHOR)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
const orgs = inputs.teamReviewers.map(team => {
|
||||
if (!team.includes('/')) {
|
||||
throw new Error(
|
||||
`Team ${team} is not in the correct format. It should be in the format org/team`
|
||||
)
|
||||
}
|
||||
return team.split('/')[0]
|
||||
})
|
||||
const distinctOrgs = [...new Set(orgs)]
|
||||
core.debug(`distinctOrgs: ${distinctOrgs}`)
|
||||
|
||||
const orgTeams = await Promise.all(
|
||||
distinctOrgs.map(org => this.getOrgTeams(org))
|
||||
)
|
||||
|
||||
const teamIds = inputs.teamReviewers.map(team => {
|
||||
const [org, teamName] = team.split('/')
|
||||
const orgTeam = orgTeams.find(
|
||||
orgTeam => orgTeam.organization.login === org
|
||||
)
|
||||
if (!orgTeam) {
|
||||
throw new Error(`Org ${org} not found`)
|
||||
}
|
||||
const teamId = orgTeam.organization.teams.edges.find(
|
||||
team => team.node.slug === teamName
|
||||
)?.node.id
|
||||
if (!teamId) {
|
||||
throw new Error(`Team ${teamName} not found in ${org}`)
|
||||
}
|
||||
return teamId
|
||||
})
|
||||
core.debug(`teamIds: ${teamIds}`)
|
||||
|
||||
if (teamIds.length > 0) {
|
||||
const repository = this.parseRepository(baseRepository)
|
||||
const pullNodeId = await this.getPullNodeId(
|
||||
repository.owner,
|
||||
repository.repo,
|
||||
pull.number
|
||||
)
|
||||
core.debug(`pullNodeId: ${pullNodeId}`)
|
||||
|
||||
await this.requestReviewers(pullNodeId, teamIds)
|
||||
}
|
||||
|
||||
return pull
|
||||
}
|
||||
|
||||
async pushSignedCommits(
|
||||
branchCommits: Commit[],
|
||||
baseCommit: Commit,
|
||||
repoPath: string,
|
||||
branchRepository: string,
|
||||
branch: string
|
||||
): Promise<CommitResponse> {
|
||||
let headCommit: CommitResponse = {
|
||||
sha: baseCommit.sha,
|
||||
tree: baseCommit.tree,
|
||||
verified: false
|
||||
}
|
||||
for (const commit of branchCommits) {
|
||||
headCommit = await this.createCommit(
|
||||
commit,
|
||||
headCommit,
|
||||
repoPath,
|
||||
branchRepository
|
||||
)
|
||||
}
|
||||
await this.createOrUpdateRef(branchRepository, branch, headCommit.sha)
|
||||
return headCommit
|
||||
}
|
||||
|
||||
private async createCommit(
|
||||
commit: Commit,
|
||||
parentCommit: CommitResponse,
|
||||
repoPath: string,
|
||||
branchRepository: string
|
||||
): Promise<CommitResponse> {
|
||||
const repository = this.parseRepository(branchRepository)
|
||||
// In the case of an empty commit, the tree references the parent's tree
|
||||
let treeSha = parentCommit.tree
|
||||
if (commit.changes.length > 0) {
|
||||
core.info(`Creating tree objects for local commit ${commit.sha}`)
|
||||
const treeObjects = await Promise.all(
|
||||
commit.changes.map(async ({path, mode, status, dstSha}) => {
|
||||
if (mode === '160000') {
|
||||
// submodule
|
||||
core.info(`Creating tree object for submodule commit at '${path}'`)
|
||||
return <TreeObject>{
|
||||
path,
|
||||
mode,
|
||||
sha: dstSha,
|
||||
type: 'commit'
|
||||
}
|
||||
} else {
|
||||
let sha: string | null = null
|
||||
if (status === 'A' || status === 'M') {
|
||||
try {
|
||||
const {data: blob} = await blobCreationLimit(() =>
|
||||
this.octokit.rest.git.createBlob({
|
||||
...repository,
|
||||
content: utils.readFileBase64([repoPath, path]),
|
||||
encoding: 'base64'
|
||||
})
|
||||
)
|
||||
sha = blob.sha
|
||||
} catch (error) {
|
||||
core.error(
|
||||
`Error creating blob for file '${path}': ${utils.getErrorMessage(error)}`
|
||||
)
|
||||
throw error
|
||||
async getOrgTeams(orgName: string): Promise<OrgTeams> {
|
||||
const query = `
|
||||
query($orgName: String!, $teamCount: Int!) {
|
||||
organization(login: $orgName) {
|
||||
login
|
||||
teams(first: $teamCount) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
slug
|
||||
}
|
||||
}
|
||||
core.info(
|
||||
`Creating tree object for blob at '${path}' with status '${status}'`
|
||||
)
|
||||
return <TreeObject>{
|
||||
path,
|
||||
mode,
|
||||
sha,
|
||||
type: 'blob'
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const chunkSize = 100
|
||||
const chunkedTreeObjects: TreeObject[][] = Array.from(
|
||||
{length: Math.ceil(treeObjects.length / chunkSize)},
|
||||
(_, i) => treeObjects.slice(i * chunkSize, i * chunkSize + chunkSize)
|
||||
)
|
||||
|
||||
core.info(`Creating tree for local commit ${commit.sha}`)
|
||||
for (let i = 0; i < chunkedTreeObjects.length; i++) {
|
||||
const {data: tree} = await this.octokit.rest.git.createTree({
|
||||
...repository,
|
||||
base_tree: treeSha,
|
||||
tree: chunkedTreeObjects[i]
|
||||
})
|
||||
treeSha = tree.sha
|
||||
if (chunkedTreeObjects.length > 1) {
|
||||
core.info(
|
||||
`Created tree ${treeSha} of multipart tree (${i + 1} of ${chunkedTreeObjects.length})`
|
||||
)
|
||||
}
|
||||
}
|
||||
core.info(`Created tree ${treeSha} for local commit ${commit.sha}`)
|
||||
}
|
||||
|
||||
const {data: remoteCommit} = await this.octokit.rest.git.createCommit({
|
||||
...repository,
|
||||
parents: [parentCommit.sha],
|
||||
tree: treeSha,
|
||||
message: `${commit.subject}\n\n${commit.body}`
|
||||
`
|
||||
const teamCount = 100
|
||||
return this.octokit.graphql<OrgTeams>(query, {
|
||||
orgName,
|
||||
teamCount
|
||||
})
|
||||
core.info(
|
||||
`Created commit ${remoteCommit.sha} for local commit ${commit.sha}`
|
||||
)
|
||||
core.info(
|
||||
`Commit verified: ${remoteCommit.verification.verified}; reason: ${remoteCommit.verification.reason}`
|
||||
)
|
||||
return {
|
||||
sha: remoteCommit.sha,
|
||||
tree: remoteCommit.tree.sha,
|
||||
verified: remoteCommit.verification.verified
|
||||
}
|
||||
}
|
||||
|
||||
async getCommit(
|
||||
sha: string,
|
||||
branchRepository: string
|
||||
): Promise<CommitResponse> {
|
||||
const repository = this.parseRepository(branchRepository)
|
||||
const {data: remoteCommit} = await this.octokit.rest.git.getCommit({
|
||||
...repository,
|
||||
commit_sha: sha
|
||||
})
|
||||
return {
|
||||
sha: remoteCommit.sha,
|
||||
tree: remoteCommit.tree.sha,
|
||||
verified: remoteCommit.verification.verified
|
||||
}
|
||||
}
|
||||
|
||||
private async createOrUpdateRef(
|
||||
branchRepository: string,
|
||||
branch: string,
|
||||
newHead: string
|
||||
) {
|
||||
const repository = this.parseRepository(branchRepository)
|
||||
const branchExists = await this.octokit.rest.repos
|
||||
.getBranch({
|
||||
...repository,
|
||||
branch: branch
|
||||
})
|
||||
.then(
|
||||
() => true,
|
||||
() => false
|
||||
)
|
||||
|
||||
if (branchExists) {
|
||||
core.info(`Branch ${branch} exists; Updating ref`)
|
||||
await this.octokit.rest.git.updateRef({
|
||||
...repository,
|
||||
sha: newHead,
|
||||
ref: `heads/${branch}`,
|
||||
force: true
|
||||
})
|
||||
} else {
|
||||
core.info(`Branch ${branch} does not exist; Creating ref`)
|
||||
await this.octokit.rest.git.createRef({
|
||||
...repository,
|
||||
sha: newHead,
|
||||
ref: `refs/heads/${branch}`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async convertToDraft(id: string): Promise<void> {
|
||||
core.info(`Converting pull request to draft`)
|
||||
await this.octokit.graphql({
|
||||
query: `mutation($pullRequestId: ID!) {
|
||||
convertPullRequestToDraft(input: {pullRequestId: $pullRequestId}) {
|
||||
pullRequest {
|
||||
isDraft
|
||||
async getPullNodeId(
|
||||
owner: string,
|
||||
repo: string,
|
||||
pullNumber: number
|
||||
): Promise<string> {
|
||||
const query = `
|
||||
query($owner: String!, $repo: String!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: ${pullNumber}) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}`,
|
||||
pullRequestId: id
|
||||
})
|
||||
}
|
||||
`
|
||||
|
||||
return (
|
||||
await this.octokit.graphql<PullRequestResponse>(query, {
|
||||
owner,
|
||||
repo
|
||||
})
|
||||
).repository.pullRequest.id
|
||||
}
|
||||
|
||||
async requestReviewers(
|
||||
pullRequestId: string,
|
||||
teamIds: string[]
|
||||
): Promise<void> {
|
||||
const mutation = `
|
||||
mutation($input: RequestReviewsInput!) {
|
||||
requestReviews(input: $input) {
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
await this.octokit
|
||||
.graphql(mutation, {
|
||||
input: {
|
||||
pullRequestId,
|
||||
teamIds: teamIds,
|
||||
union: true
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
core.info('Reviews requested successfully')
|
||||
})
|
||||
.catch(error => {
|
||||
core.error(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
interface OrgTeams {
|
||||
organization: {
|
||||
login: string
|
||||
teams: {
|
||||
edges: {
|
||||
node: {
|
||||
id: string
|
||||
slug: string
|
||||
}
|
||||
}[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface PullRequestResponse {
|
||||
repository: {
|
||||
pullRequest: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
35
src/main.ts
35
src/main.ts
|
@ -3,19 +3,10 @@ import {Inputs, createPullRequest} from './create-pull-request'
|
|||
import {inspect} from 'util'
|
||||
import * as utils from './utils'
|
||||
|
||||
function getDraftInput(): {value: boolean; always: boolean} {
|
||||
if (core.getInput('draft') === 'always-true') {
|
||||
return {value: true, always: true}
|
||||
} else {
|
||||
return {value: core.getBooleanInput('draft'), always: false}
|
||||
}
|
||||
}
|
||||
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
const inputs: Inputs = {
|
||||
token: core.getInput('token'),
|
||||
branchToken: core.getInput('branch-token'),
|
||||
path: core.getInput('path'),
|
||||
addPaths: utils.getInputAsArray('add-paths'),
|
||||
commitMessage: core.getInput('commit-message'),
|
||||
|
@ -27,41 +18,17 @@ async function run(): Promise<void> {
|
|||
branchSuffix: core.getInput('branch-suffix'),
|
||||
base: core.getInput('base'),
|
||||
pushToFork: core.getInput('push-to-fork'),
|
||||
signCommits: core.getBooleanInput('sign-commits'),
|
||||
title: core.getInput('title'),
|
||||
body: core.getInput('body'),
|
||||
bodyPath: core.getInput('body-path'),
|
||||
labels: utils.getInputAsArray('labels'),
|
||||
assignees: utils.getInputAsArray('assignees'),
|
||||
reviewers: utils.getInputAsArray('reviewers'),
|
||||
teamReviewers: utils.getInputAsArray('team-reviewers'),
|
||||
milestone: Number(core.getInput('milestone')),
|
||||
draft: getDraftInput(),
|
||||
maintainerCanModify: core.getBooleanInput('maintainer-can-modify')
|
||||
draft: core.getBooleanInput('draft')
|
||||
}
|
||||
core.debug(`Inputs: ${inspect(inputs)}`)
|
||||
|
||||
if (!inputs.token) {
|
||||
throw new Error(`Input 'token' not supplied. Unable to continue.`)
|
||||
}
|
||||
if (!inputs.branchToken) {
|
||||
inputs.branchToken = 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) {
|
||||
core.setFailed(utils.getErrorMessage(error))
|
||||
|
|
|
@ -1,40 +1,28 @@
|
|||
import * as core from '@actions/core'
|
||||
import {Octokit as OctokitCore} from '@octokit/core'
|
||||
import {Octokit as Core} from '@octokit/core'
|
||||
import {paginateRest} from '@octokit/plugin-paginate-rest'
|
||||
import {restEndpointMethods} from '@octokit/plugin-rest-endpoint-methods'
|
||||
import {throttling} from '@octokit/plugin-throttling'
|
||||
import {fetch} from 'node-fetch-native/proxy'
|
||||
import ProxyAgent from 'proxy-agent'
|
||||
export {RestEndpointMethodTypes} from '@octokit/plugin-rest-endpoint-methods'
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
export {OctokitOptions} from '@octokit/core/dist-types/types'
|
||||
|
||||
export const Octokit = OctokitCore.plugin(
|
||||
export const Octokit = Core.plugin(
|
||||
paginateRest,
|
||||
restEndpointMethods,
|
||||
throttling,
|
||||
autoProxyAgent
|
||||
)
|
||||
|
||||
export const throttleOptions = {
|
||||
onRateLimit: (retryAfter, options, _, retryCount) => {
|
||||
core.debug(`Hit rate limit for request ${options.method} ${options.url}`)
|
||||
// Retries twice for a total of three attempts
|
||||
if (retryCount < 2) {
|
||||
core.debug(`Retrying after ${retryAfter} seconds!`)
|
||||
return true
|
||||
}
|
||||
},
|
||||
onSecondaryRateLimit: (retryAfter, options) => {
|
||||
core.warning(
|
||||
`Hit secondary rate limit for request ${options.method} ${options.url}`
|
||||
)
|
||||
core.warning(`Requests may be retried after ${retryAfter} seconds.`)
|
||||
}
|
||||
}
|
||||
|
||||
// Octokit plugin to support the standard environment variables http_proxy, https_proxy and no_proxy
|
||||
function autoProxyAgent(octokit: OctokitCore) {
|
||||
function autoProxyAgent(octokit: Core) {
|
||||
const proxy =
|
||||
process.env.https_proxy ||
|
||||
process.env.HTTPS_PROXY ||
|
||||
process.env.http_proxy ||
|
||||
process.env.HTTP_PROXY
|
||||
|
||||
if (!proxy) return
|
||||
|
||||
const agent = new ProxyAgent()
|
||||
octokit.hook.before('request', options => {
|
||||
options.request.fetch = fetch
|
||||
options.request.agent = agent
|
||||
})
|
||||
}
|
||||
|
|
71
src/utils.ts
71
src/utils.ts
|
@ -16,16 +16,6 @@ export function getStringAsArray(str: string): string[] {
|
|||
.filter(x => x !== '')
|
||||
}
|
||||
|
||||
export function stripOrgPrefixFromTeams(teams: string[]): string[] {
|
||||
return teams.map(team => {
|
||||
const slashIndex = team.lastIndexOf('/')
|
||||
if (slashIndex > 0) {
|
||||
return team.substring(slashIndex + 1)
|
||||
}
|
||||
return team
|
||||
})
|
||||
}
|
||||
|
||||
export function getRepoPath(relativePath?: string): string {
|
||||
let githubWorkspacePath = process.env['GITHUB_WORKSPACE']
|
||||
if (!githubWorkspacePath) {
|
||||
|
@ -41,6 +31,53 @@ 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,
|
||||
|
@ -122,20 +159,6 @@ export function fileExistsSync(path: string): boolean {
|
|||
return false
|
||||
}
|
||||
|
||||
export function readFile(path: string): string {
|
||||
return fs.readFileSync(path, 'utf-8')
|
||||
}
|
||||
|
||||
export function readFileBase64(pathParts: string[]): string {
|
||||
const resolvedPath = path.resolve(...pathParts)
|
||||
if (fs.lstatSync(resolvedPath).isSymbolicLink()) {
|
||||
return fs
|
||||
.readlinkSync(resolvedPath, {encoding: 'buffer'})
|
||||
.toString('base64')
|
||||
}
|
||||
return fs.readFileSync(resolvedPath).toString('base64')
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
function hasErrorCode(error: any): error is {code: string} {
|
||||
return typeof (error && error.code) === 'string'
|
||||
|
|
Loading…
Reference in a new issue