Compare commits
45 commits
main
...
signed-com
Author | SHA1 | Date | |
---|---|---|---|
|
740d9850a7 | ||
|
e7f5ea9fd9 | ||
|
66ddf90dac | ||
|
fd3e742ffd | ||
|
eea4f44785 | ||
|
5a9be5875b | ||
|
3d665e5aea | ||
|
bb1f2b1327 | ||
|
1da4bbe67c | ||
|
d93a919a26 | ||
|
a2d4746d68 | ||
|
2c262e8e92 | ||
|
7b7dc5777f | ||
|
c1be170c86 | ||
|
822f3b39c1 | ||
|
197e74c6e1 | ||
|
c7909f9b04 | ||
|
491f77f4d6 | ||
|
93858f721d | ||
|
2668dc956a | ||
|
b0303827bb | ||
|
90b04fe25b | ||
|
2707da835d | ||
|
e4c51477d1 | ||
|
0a237f343d | ||
|
77c6c11180 | ||
|
477c78c3f2 | ||
|
7f459482cc | ||
|
018afb52b6 | ||
|
3a7a677a14 | ||
|
74416df758 | ||
|
7575ead361 | ||
|
3d409de49f | ||
|
743dcd81f7 | ||
|
0f72e35b7f | ||
|
4a3e69b7f7 | ||
|
525f1f0028 | ||
|
36bba202e3 | ||
|
3563849c8a | ||
|
6c03c11aff | ||
|
27642d5a9e | ||
|
136db6a783 | ||
|
43d45f2e88 | ||
|
24bfe8de6b | ||
|
22fb2d9a65 |
17 changed files with 32174 additions and 8524 deletions
|
@ -65,6 +65,7 @@ All inputs are **optional**. If not set, sensible defaults will be used.
|
||||||
| `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. | |
|
| `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. |
|
| `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. | |
|
| `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` |
|
| `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` | 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`. | |
|
| `body-path` | The path to a file containing the pull request body. Takes precedence over `body`. | |
|
||||||
|
@ -74,6 +75,7 @@ All inputs are **optional**. If not set, sensible defaults will be used.
|
||||||
| `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), or equivalent [GitHub App permissions](docs/concepts-guidelines.md#authenticating-with-github-app-generated-tokens), are required. | |
|
||||||
| `milestone` | The number of the milestone to associate this pull request with. | |
|
| `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). It is not possible to change draft status after creation except through the web interface. | `false` |
|
| `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` |
|
||||||
|
| `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` |
|
||||||
|
|
||||||
#### commit-message
|
#### commit-message
|
||||||
|
|
||||||
|
@ -115,9 +117,10 @@ The following outputs can be used by subsequent workflow steps.
|
||||||
|
|
||||||
- `pull-request-number` - The pull request number.
|
- `pull-request-number` - The pull request number.
|
||||||
- `pull-request-url` - The URL of the pull request.
|
- `pull-request-url` - The URL of the pull request.
|
||||||
- `pull-request-operation` - The pull request operation performed by the action, `created`, `updated` or `closed`.
|
- `pull-request-operation` - The pull request operation performed by the action, `created`, `updated`, `closed` or `none`.
|
||||||
- `pull-request-head-sha` - The commit SHA of the pull request branch.
|
- `pull-request-head-sha` - The commit SHA of the pull request branch.
|
||||||
- `pull-request-branch` - The branch name of the pull request.
|
- `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.
|
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.
|
Note that in order to read the step outputs the action step must have an id.
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import {
|
import {
|
||||||
createOrUpdateBranch,
|
createOrUpdateBranch,
|
||||||
tryFetch,
|
tryFetch,
|
||||||
getWorkingBaseAndType
|
getWorkingBaseAndType,
|
||||||
|
buildBranchCommits
|
||||||
} from '../lib/create-or-update-branch'
|
} from '../lib/create-or-update-branch'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import {GitCommandManager} from '../lib/git-command-manager'
|
import {GitCommandManager} from '../lib/git-command-manager'
|
||||||
|
@ -229,6 +230,77 @@ describe('create-or-update-branch tests', () => {
|
||||||
expect(workingBaseType).toEqual('commit')
|
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).toEqual([
|
||||||
|
{mode: '100755', path: UNTRACKED_EXE_FILE, status: 'A'},
|
||||||
|
{mode: '100644', path: TRACKED_FILE, status: 'M'},
|
||||||
|
{mode: '100644', path: UNTRACKED_FILE, status: '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).toEqual([
|
||||||
|
{mode: '100644', path: TRACKED_FILE, status: 'D'},
|
||||||
|
{mode: '100644', path: UNTRACKED_FILE, status: 'A'},
|
||||||
|
{mode: '100644', path: TRACKED_FILE_NEW_PATH, status: '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).toEqual([
|
||||||
|
{mode: '100644', path: TRACKED_FILE, status: 'M'},
|
||||||
|
{mode: '100644', path: UNTRACKED_FILE, status: untrackedFileStatus}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
it('tests no changes resulting in no new branch being created', async () => {
|
it('tests no changes resulting in no new branch being created', async () => {
|
||||||
const commitMessage = uuidv4()
|
const commitMessage = uuidv4()
|
||||||
const result = await createOrUpdateBranch(
|
const result = await createOrUpdateBranch(
|
||||||
|
|
|
@ -13,7 +13,7 @@ git daemon --verbose --enable=receive-pack --base-path=/git/remote --export-all
|
||||||
# Give the daemon time to start
|
# Give the daemon time to start
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
# Create a local clone and make an initial commit
|
# Create a local clone and make initial commits
|
||||||
mkdir -p /git/local/repos
|
mkdir -p /git/local/repos
|
||||||
git clone git://127.0.0.1/repos/test-base.git /git/local/repos/test-base
|
git clone git://127.0.0.1/repos/test-base.git /git/local/repos/test-base
|
||||||
cd /git/local/repos/test-base
|
cd /git/local/repos/test-base
|
||||||
|
@ -22,6 +22,10 @@ git config --global user.name "Your Name"
|
||||||
echo "#test-base" > README.md
|
echo "#test-base" > README.md
|
||||||
git add .
|
git add .
|
||||||
git commit -m "initial commit"
|
git commit -m "initial commit"
|
||||||
|
echo "#test-base :sparkles:" > README.md
|
||||||
|
git add .
|
||||||
|
git commit -m "add sparkles" -m "Change description:
|
||||||
|
- updates README.md to add sparkles to the title"
|
||||||
git push -u
|
git push -u
|
||||||
git log -1 --pretty=oneline
|
git log -1 --pretty=oneline
|
||||||
git config --global --unset user.email
|
git config --global --unset user.email
|
||||||
|
|
26
__test__/git-command-manager.int.test.ts
Normal file
26
__test__/git-command-manager.int.test.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import {GitCommandManager, Commit} 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 parent = await git.getCommit('HEAD^')
|
||||||
|
const commit = await git.getCommit('HEAD')
|
||||||
|
expect(parent.subject).toEqual('initial commit')
|
||||||
|
expect(parent.changes).toEqual([
|
||||||
|
{mode: '100644', status: 'A', path: 'README.md'}
|
||||||
|
])
|
||||||
|
expect(commit.subject).toEqual('add sparkles')
|
||||||
|
expect(commit.parents[0]).toEqual(parent.sha)
|
||||||
|
expect(commit.changes).toEqual([
|
||||||
|
{mode: '100644', status: 'M', path: 'README.md'}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
|
@ -7,7 +7,6 @@ const extraheaderConfigKey = 'http.https://127.0.0.1/.extraheader'
|
||||||
|
|
||||||
describe('git-config-helper integration tests', () => {
|
describe('git-config-helper integration tests', () => {
|
||||||
let git: GitCommandManager
|
let git: GitCommandManager
|
||||||
let gitConfigHelper: GitConfigHelper
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
git = await GitCommandManager.create(REPO_PATH)
|
git = await GitCommandManager.create(REPO_PATH)
|
||||||
|
|
|
@ -51,6 +51,9 @@ inputs:
|
||||||
A fork of the checked out parent repository to which the pull request branch will be pushed.
|
A fork of the checked out parent repository to which the pull request branch will be pushed.
|
||||||
e.g. `owner/repo-fork`.
|
e.g. `owner/repo-fork`.
|
||||||
The pull request will be created to merge the fork's branch into the parent's base.
|
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:
|
title:
|
||||||
description: 'The title of the pull request.'
|
description: 'The title of the pull request.'
|
||||||
default: 'Changes by create-pull-request action'
|
default: 'Changes by create-pull-request action'
|
||||||
|
@ -74,6 +77,9 @@ inputs:
|
||||||
draft:
|
draft:
|
||||||
description: 'Create a draft pull request. It is not possible to change draft status after creation except through the web interface'
|
description: 'Create a draft pull request. It is not possible to change draft status after creation except through the web interface'
|
||||||
default: false
|
default: false
|
||||||
|
maintainer-can-modify:
|
||||||
|
description: 'Indicates whether maintainers can modify the pull request.'
|
||||||
|
default: true
|
||||||
outputs:
|
outputs:
|
||||||
pull-request-number:
|
pull-request-number:
|
||||||
description: 'The pull request number'
|
description: 'The pull request number'
|
||||||
|
|
36377
dist/index.js
vendored
36377
dist/index.js
vendored
File diff suppressed because one or more lines are too long
|
@ -16,7 +16,9 @@ This document covers terminology, how the action works, general usage guidelines
|
||||||
- [Push using SSH (deploy keys)](#push-using-ssh-deploy-keys)
|
- [Push using SSH (deploy keys)](#push-using-ssh-deploy-keys)
|
||||||
- [Push pull request branches to a fork](#push-pull-request-branches-to-a-fork)
|
- [Push pull request branches to a fork](#push-pull-request-branches-to-a-fork)
|
||||||
- [Authenticating with GitHub App generated tokens](#authenticating-with-github-app-generated-tokens)
|
- [Authenticating with GitHub App generated tokens](#authenticating-with-github-app-generated-tokens)
|
||||||
- [GPG commit signature verification](#gpg-commit-signature-verification)
|
- [Commit signing](#commit-signing)
|
||||||
|
- [Commit signature verification for bots](#commit-signature-verification-for-bots)
|
||||||
|
- [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)
|
- [Running in a container or on self-hosted runners](#running-in-a-container-or-on-self-hosted-runners)
|
||||||
|
|
||||||
## Terminology
|
## Terminology
|
||||||
|
@ -260,17 +262,17 @@ GitHub App generated tokens are more secure than using a PAT because GitHub App
|
||||||
|
|
||||||
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`.
|
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 [tibdex/github-app-token](https://github.com/tibdex/github-app-token) to generate a token for use with this action.
|
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.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: tibdex/github-app-token@v1
|
- uses: actions/create-github-app-token@v1
|
||||||
id: generate-token
|
id: generate-token
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.APP_ID }}
|
app-id: ${{ secrets.APP_ID }}
|
||||||
private_key: ${{ secrets.APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
# Make changes to pull request here
|
# Make changes to pull request here
|
||||||
|
|
||||||
|
@ -280,7 +282,54 @@ GitHub App generated tokens are more secure than using a PAT because GitHub App
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
```
|
```
|
||||||
|
|
||||||
### GPG commit signature verification
|
### 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.
|
||||||
|
|
||||||
|
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@v6
|
||||||
|
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@v6
|
||||||
|
with:
|
||||||
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
|
sign-commits: true
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GPG commit signature verification
|
||||||
|
|
||||||
The action can use GPG to sign commits with a GPG key that you generate yourself.
|
The action can use GPG to sign commits with a GPG key that you generate yourself.
|
||||||
|
|
||||||
|
|
3762
package-lock.json
generated
3762
package-lock.json
generated
File diff suppressed because it is too large
Load diff
13
package.json
13
package.json
|
@ -31,9 +31,11 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.1",
|
"@actions/core": "^1.10.1",
|
||||||
"@actions/exec": "^1.1.1",
|
"@actions/exec": "^1.1.1",
|
||||||
"@octokit/core": "^4.2.4",
|
"@octokit/core": "^6.1.2",
|
||||||
"@octokit/plugin-paginate-rest": "^5.0.1",
|
"@octokit/plugin-paginate-rest": "^11.3.3",
|
||||||
"@octokit/plugin-rest-endpoint-methods": "^6.8.1",
|
"@octokit/plugin-rest-endpoint-methods": "^13.2.4",
|
||||||
|
"@octokit/plugin-throttling": "^9.3.1",
|
||||||
|
"p-limit": "^6.1.0",
|
||||||
"proxy-from-env": "^1.1.0",
|
"proxy-from-env": "^1.1.0",
|
||||||
"undici": "^6.19.7",
|
"undici": "^6.19.7",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
|
@ -41,7 +43,8 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/node": "^18.19.44",
|
"@types/node": "^18.19.44",
|
||||||
"@typescript-eslint/parser": "^5.62.0",
|
"@typescript-eslint/eslint-plugin": "^7.17.0",
|
||||||
|
"@typescript-eslint/parser": "^7.17.0",
|
||||||
"@vercel/ncc": "^0.38.1",
|
"@vercel/ncc": "^0.38.1",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-import-resolver-typescript": "^3.6.1",
|
"eslint-import-resolver-typescript": "^3.6.1",
|
||||||
|
@ -55,6 +58,6 @@
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"ts-jest": "^29.2.4",
|
"ts-jest": "^29.2.4",
|
||||||
"typescript": "^4.9.5"
|
"typescript": "^5.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import {GitCommandManager} from './git-command-manager'
|
import {GitCommandManager, Commit} from './git-command-manager'
|
||||||
import {v4 as uuidv4} from 'uuid'
|
import {v4 as uuidv4} from 'uuid'
|
||||||
|
|
||||||
const CHERRYPICK_EMPTY =
|
const CHERRYPICK_EMPTY =
|
||||||
|
@ -47,6 +47,24 @@ 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)
|
||||||
|
}
|
||||||
|
return commits
|
||||||
|
}
|
||||||
|
|
||||||
// Return the number of commits that branch2 is ahead of branch1
|
// Return the number of commits that branch2 is ahead of branch1
|
||||||
async function commitsAhead(
|
async function commitsAhead(
|
||||||
git: GitCommandManager,
|
git: GitCommandManager,
|
||||||
|
@ -114,7 +132,9 @@ interface CreateOrUpdateBranchResult {
|
||||||
action: string
|
action: string
|
||||||
base: string
|
base: string
|
||||||
hasDiffWithBase: boolean
|
hasDiffWithBase: boolean
|
||||||
|
baseSha: string
|
||||||
headSha: string
|
headSha: string
|
||||||
|
branchCommits: Commit[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createOrUpdateBranch(
|
export async function createOrUpdateBranch(
|
||||||
|
@ -124,7 +144,8 @@ export async function createOrUpdateBranch(
|
||||||
branch: string,
|
branch: string,
|
||||||
branchRemoteName: string,
|
branchRemoteName: string,
|
||||||
signoff: boolean,
|
signoff: boolean,
|
||||||
addPaths: string[]
|
addPaths: string[],
|
||||||
|
signCommits: boolean = false
|
||||||
): Promise<CreateOrUpdateBranchResult> {
|
): Promise<CreateOrUpdateBranchResult> {
|
||||||
// Get the working base.
|
// Get the working base.
|
||||||
// When a ref, it may or may not be the actual base.
|
// When a ref, it may or may not be the actual base.
|
||||||
|
@ -144,7 +165,9 @@ export async function createOrUpdateBranch(
|
||||||
action: 'none',
|
action: 'none',
|
||||||
base: base,
|
base: base,
|
||||||
hasDiffWithBase: false,
|
hasDiffWithBase: false,
|
||||||
headSha: ''
|
baseSha: '',
|
||||||
|
headSha: '',
|
||||||
|
branchCommits: []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the working base changes to a temporary branch
|
// Save the working base changes to a temporary branch
|
||||||
|
@ -289,8 +312,15 @@ export async function createOrUpdateBranch(
|
||||||
result.hasDiffWithBase = await isAhead(git, base, branch)
|
result.hasDiffWithBase = await isAhead(git, base, branch)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the pull request branch SHA
|
// Get the base and head SHAs
|
||||||
result.headSha = await git.revParse('HEAD')
|
result.baseSha = await git.revParse(base)
|
||||||
|
result.headSha = await git.revParse(branch)
|
||||||
|
|
||||||
|
// NOTE: This could always be built and returned. Maybe remove when there is confidence in buildBranchCommits.
|
||||||
|
if (signCommits) {
|
||||||
|
// Build the branch commits
|
||||||
|
result.branchCommits = await buildBranchCommits(git, base, branch)
|
||||||
|
}
|
||||||
|
|
||||||
// Delete the temporary branch
|
// Delete the temporary branch
|
||||||
await git.exec(['branch', '--delete', '--force', tempBranch])
|
await git.exec(['branch', '--delete', '--force', tempBranch])
|
||||||
|
|
|
@ -23,6 +23,7 @@ export interface Inputs {
|
||||||
branchSuffix: string
|
branchSuffix: string
|
||||||
base: string
|
base: string
|
||||||
pushToFork: string
|
pushToFork: string
|
||||||
|
signCommits: boolean
|
||||||
title: string
|
title: string
|
||||||
body: string
|
body: string
|
||||||
bodyPath: string
|
bodyPath: string
|
||||||
|
@ -32,6 +33,7 @@ export interface Inputs {
|
||||||
teamReviewers: string[]
|
teamReviewers: string[]
|
||||||
milestone: number
|
milestone: number
|
||||||
draft: boolean
|
draft: boolean
|
||||||
|
maintainerCanModify: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createPullRequest(inputs: Inputs): Promise<void> {
|
export async function createPullRequest(inputs: Inputs): Promise<void> {
|
||||||
|
@ -45,8 +47,9 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
|
||||||
|
|
||||||
core.startGroup('Determining the base and head repositories')
|
core.startGroup('Determining the base and head repositories')
|
||||||
const baseRemote = gitConfigHelper.getGitRemote()
|
const baseRemote = gitConfigHelper.getGitRemote()
|
||||||
// Init the GitHub client
|
// Init the GitHub clients
|
||||||
const githubHelper = new GitHubHelper(baseRemote.hostname, inputs.token)
|
const ghBranch = new GitHubHelper(baseRemote.hostname, inputs.gitToken)
|
||||||
|
const ghPull = new GitHubHelper(baseRemote.hostname, inputs.token)
|
||||||
// Determine the head repository; the target for the pull request branch
|
// Determine the head repository; the target for the pull request branch
|
||||||
const branchRemoteName = inputs.pushToFork ? 'fork' : 'origin'
|
const branchRemoteName = inputs.pushToFork ? 'fork' : 'origin'
|
||||||
const branchRepository = inputs.pushToFork
|
const branchRepository = inputs.pushToFork
|
||||||
|
@ -57,11 +60,11 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
|
||||||
core.info(
|
core.info(
|
||||||
`Checking if '${branchRepository}' is a fork of '${baseRemote.repository}'`
|
`Checking if '${branchRepository}' is a fork of '${baseRemote.repository}'`
|
||||||
)
|
)
|
||||||
const baseParentRepository = await githubHelper.getRepositoryParent(
|
const baseParentRepository = await ghBranch.getRepositoryParent(
|
||||||
baseRemote.repository
|
baseRemote.repository
|
||||||
)
|
)
|
||||||
const branchParentRepository =
|
const branchParentRepository =
|
||||||
await githubHelper.getRepositoryParent(branchRepository)
|
await ghBranch.getRepositoryParent(branchRepository)
|
||||||
if (branchParentRepository == null) {
|
if (branchParentRepository == null) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Repository '${branchRepository}' is not a fork. Unable to continue.`
|
`Repository '${branchRepository}' is not a fork. Unable to continue.`
|
||||||
|
@ -174,6 +177,12 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
|
||||||
)
|
)
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
|
|
||||||
|
// Action outputs
|
||||||
|
const outputs = new Map<string, string>()
|
||||||
|
outputs.set('pull-request-branch', inputs.branch)
|
||||||
|
outputs.set('pull-request-operation', 'none')
|
||||||
|
outputs.set('pull-request-commits-verified', 'false')
|
||||||
|
|
||||||
// Create or update the pull request branch
|
// Create or update the pull request branch
|
||||||
core.startGroup('Create or update the pull request branch')
|
core.startGroup('Create or update the pull request branch')
|
||||||
const result = await createOrUpdateBranch(
|
const result = await createOrUpdateBranch(
|
||||||
|
@ -183,8 +192,12 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
|
||||||
inputs.branch,
|
inputs.branch,
|
||||||
branchRemoteName,
|
branchRemoteName,
|
||||||
inputs.signoff,
|
inputs.signoff,
|
||||||
inputs.addPaths
|
inputs.addPaths,
|
||||||
|
inputs.signCommits
|
||||||
)
|
)
|
||||||
|
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()
|
core.endGroup()
|
||||||
|
|
||||||
if (['created', 'updated'].includes(result.action)) {
|
if (['created', 'updated'].includes(result.action)) {
|
||||||
|
@ -192,38 +205,50 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
|
||||||
core.startGroup(
|
core.startGroup(
|
||||||
`Pushing pull request branch to '${branchRemoteName}/${inputs.branch}'`
|
`Pushing pull request branch to '${branchRemoteName}/${inputs.branch}'`
|
||||||
)
|
)
|
||||||
await git.push([
|
if (inputs.signCommits) {
|
||||||
'--force-with-lease',
|
// Create signed commits via the GitHub API
|
||||||
branchRemoteName,
|
const stashed = await git.stashPush(['--include-untracked'])
|
||||||
`${inputs.branch}:refs/heads/${inputs.branch}`
|
await git.checkout(inputs.branch)
|
||||||
])
|
const pushSignedCommitsResult = await ghBranch.pushSignedCommits(
|
||||||
|
result.branchCommits,
|
||||||
|
result.baseSha,
|
||||||
|
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}`
|
||||||
|
])
|
||||||
|
}
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the base. It would have been '' if not specified as an input
|
|
||||||
inputs.base = result.base
|
|
||||||
|
|
||||||
if (result.hasDiffWithBase) {
|
if (result.hasDiffWithBase) {
|
||||||
// Create or update the pull request
|
|
||||||
core.startGroup('Create or update the pull request')
|
core.startGroup('Create or update the pull request')
|
||||||
const pull = await githubHelper.createOrUpdatePullRequest(
|
const pull = await ghPull.createOrUpdatePullRequest(
|
||||||
inputs,
|
inputs,
|
||||||
baseRemote.repository,
|
baseRemote.repository,
|
||||||
branchRepository
|
branchRepository
|
||||||
)
|
)
|
||||||
core.endGroup()
|
outputs.set('pull-request-number', pull.number.toString())
|
||||||
|
outputs.set('pull-request-url', pull.html_url)
|
||||||
// Set outputs
|
|
||||||
core.startGroup('Setting outputs')
|
|
||||||
core.setOutput('pull-request-number', pull.number)
|
|
||||||
core.setOutput('pull-request-url', pull.html_url)
|
|
||||||
if (pull.created) {
|
if (pull.created) {
|
||||||
core.setOutput('pull-request-operation', 'created')
|
outputs.set('pull-request-operation', 'created')
|
||||||
} else if (result.action == 'updated') {
|
} else if (result.action == 'updated') {
|
||||||
core.setOutput('pull-request-operation', 'updated')
|
outputs.set('pull-request-operation', 'updated')
|
||||||
}
|
}
|
||||||
core.setOutput('pull-request-head-sha', result.headSha)
|
|
||||||
core.setOutput('pull-request-branch', inputs.branch)
|
|
||||||
// Deprecated
|
// Deprecated
|
||||||
core.exportVariable('PULL_REQUEST_NUMBER', pull.number)
|
core.exportVariable('PULL_REQUEST_NUMBER', pull.number)
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
|
@ -242,13 +267,18 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
|
||||||
branchRemoteName,
|
branchRemoteName,
|
||||||
`refs/heads/${inputs.branch}`
|
`refs/heads/${inputs.branch}`
|
||||||
])
|
])
|
||||||
// Set outputs
|
outputs.set('pull-request-operation', 'closed')
|
||||||
core.startGroup('Setting outputs')
|
|
||||||
core.setOutput('pull-request-operation', 'closed')
|
|
||||||
core.endGroup()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set outputs
|
||||||
|
core.startGroup('Setting outputs')
|
||||||
|
for (const [key, value] of outputs) {
|
||||||
|
core.info(`${key} = ${value}`)
|
||||||
|
core.setOutput(key, value)
|
||||||
|
}
|
||||||
|
core.endGroup()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
core.setFailed(utils.getErrorMessage(error))
|
core.setFailed(utils.getErrorMessage(error))
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -5,6 +5,19 @@ import * as path from 'path'
|
||||||
|
|
||||||
const tagsRefSpec = '+refs/tags/*:refs/tags/*'
|
const tagsRefSpec = '+refs/tags/*:refs/tags/*'
|
||||||
|
|
||||||
|
export type Commit = {
|
||||||
|
sha: string
|
||||||
|
tree: string
|
||||||
|
parents: string[]
|
||||||
|
subject: string
|
||||||
|
body: string
|
||||||
|
changes: {
|
||||||
|
mode: string
|
||||||
|
status: 'A' | 'M' | 'D'
|
||||||
|
path: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
export class GitCommandManager {
|
export class GitCommandManager {
|
||||||
private gitPath: string
|
private gitPath: string
|
||||||
private workingDirectory: string
|
private workingDirectory: string
|
||||||
|
@ -138,6 +151,43 @@ export class GitCommandManager {
|
||||||
await this.exec(args)
|
await this.exec(args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getCommit(ref: string): Promise<Commit> {
|
||||||
|
const endOfBody = '###EOB###'
|
||||||
|
const output = await this.exec([
|
||||||
|
'show',
|
||||||
|
'--raw',
|
||||||
|
'--cc',
|
||||||
|
'--diff-filter=AMD',
|
||||||
|
`--format=%H%n%T%n%P%n%s%n%b%n${endOfBody}`,
|
||||||
|
ref
|
||||||
|
])
|
||||||
|
const lines = output.stdout.split('\n')
|
||||||
|
const endOfBodyIndex = lines.lastIndexOf(endOfBody)
|
||||||
|
const detailLines = lines.slice(0, endOfBodyIndex)
|
||||||
|
|
||||||
|
return <Commit>{
|
||||||
|
sha: detailLines[0],
|
||||||
|
tree: detailLines[1],
|
||||||
|
parents: detailLines[2].split(' '),
|
||||||
|
subject: detailLines[3],
|
||||||
|
body: detailLines.slice(4, endOfBodyIndex).join('\n'),
|
||||||
|
changes: lines.slice(endOfBodyIndex + 2, -1).map(line => {
|
||||||
|
const change = line.match(
|
||||||
|
/^:(\d{6}) (\d{6}) \w{7} \w{7} ([AMD])\s+(.*)$/
|
||||||
|
)
|
||||||
|
if (change) {
|
||||||
|
return {
|
||||||
|
mode: change[3] === 'D' ? change[1] : change[2],
|
||||||
|
status: change[3],
|
||||||
|
path: change[4]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unexpected line format: ${line}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getConfigValue(configKey: string, configValue = '.'): Promise<string> {
|
async getConfigValue(configKey: string, configValue = '.'): Promise<string> {
|
||||||
const output = await this.exec([
|
const output = await this.exec([
|
||||||
'config',
|
'config',
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import {Inputs} from './create-pull-request'
|
import {Inputs} from './create-pull-request'
|
||||||
import {Octokit, OctokitOptions} from './octokit-client'
|
import {Commit} from './git-command-manager'
|
||||||
|
import {Octokit, OctokitOptions, throttleOptions} from './octokit-client'
|
||||||
|
import pLimit from 'p-limit'
|
||||||
import * as utils from './utils'
|
import * as utils from './utils'
|
||||||
|
|
||||||
|
const ERROR_PR_ALREADY_EXISTS = 'A pull request already exists for'
|
||||||
const ERROR_PR_REVIEW_TOKEN_SCOPE =
|
const ERROR_PR_REVIEW_TOKEN_SCOPE =
|
||||||
'Validation Failed: "Could not resolve to a node with the global id of'
|
'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)
|
||||||
|
|
||||||
interface Repository {
|
interface Repository {
|
||||||
owner: string
|
owner: string
|
||||||
|
@ -17,6 +23,18 @@ interface Pull {
|
||||||
created: boolean
|
created: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CommitResponse {
|
||||||
|
sha: string
|
||||||
|
verified: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type TreeObject = {
|
||||||
|
path: string
|
||||||
|
mode: '100644' | '100755' | '040000' | '160000' | '120000'
|
||||||
|
sha: string | null
|
||||||
|
type: 'blob'
|
||||||
|
}
|
||||||
|
|
||||||
export class GitHubHelper {
|
export class GitHubHelper {
|
||||||
private octokit: InstanceType<typeof Octokit>
|
private octokit: InstanceType<typeof Octokit>
|
||||||
|
|
||||||
|
@ -30,6 +48,7 @@ export class GitHubHelper {
|
||||||
} else {
|
} else {
|
||||||
options.baseUrl = 'https://api.github.com'
|
options.baseUrl = 'https://api.github.com'
|
||||||
}
|
}
|
||||||
|
options.throttle = throttleOptions
|
||||||
this.octokit = new Octokit(options)
|
this.octokit = new Octokit(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,7 +78,8 @@ export class GitHubHelper {
|
||||||
head_repo: headRepository,
|
head_repo: headRepository,
|
||||||
base: inputs.base,
|
base: inputs.base,
|
||||||
body: inputs.body,
|
body: inputs.body,
|
||||||
draft: inputs.draft
|
draft: inputs.draft,
|
||||||
|
maintainer_can_modify: inputs.maintainerCanModify
|
||||||
})
|
})
|
||||||
core.info(
|
core.info(
|
||||||
`Created pull request #${pull.number} (${headBranch} => ${inputs.base})`
|
`Created pull request #${pull.number} (${headBranch} => ${inputs.base})`
|
||||||
|
@ -70,10 +90,17 @@ export class GitHubHelper {
|
||||||
created: true
|
created: true
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (
|
const errorMessage = utils.getErrorMessage(e)
|
||||||
utils.getErrorMessage(e).includes(`A pull request already exists for`)
|
if (errorMessage.includes(ERROR_PR_ALREADY_EXISTS)) {
|
||||||
) {
|
|
||||||
core.info(`A pull request already exists for ${headBranch}`)
|
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 {
|
} else {
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
@ -184,4 +211,121 @@ export class GitHubHelper {
|
||||||
|
|
||||||
return pull
|
return pull
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async pushSignedCommits(
|
||||||
|
branchCommits: Commit[],
|
||||||
|
baseSha: string,
|
||||||
|
repoPath: string,
|
||||||
|
branchRepository: string,
|
||||||
|
branch: string
|
||||||
|
): Promise<CommitResponse> {
|
||||||
|
let headCommit: CommitResponse = {
|
||||||
|
sha: baseSha,
|
||||||
|
verified: false
|
||||||
|
}
|
||||||
|
for (const commit of branchCommits) {
|
||||||
|
headCommit = await this.createCommit(
|
||||||
|
commit,
|
||||||
|
[headCommit.sha],
|
||||||
|
repoPath,
|
||||||
|
branchRepository
|
||||||
|
)
|
||||||
|
}
|
||||||
|
await this.createOrUpdateRef(branchRepository, branch, headCommit.sha)
|
||||||
|
return headCommit
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createCommit(
|
||||||
|
commit: Commit,
|
||||||
|
parents: string[],
|
||||||
|
repoPath: string,
|
||||||
|
branchRepository: string
|
||||||
|
): Promise<CommitResponse> {
|
||||||
|
const repository = this.parseRepository(branchRepository)
|
||||||
|
let treeSha = commit.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}) => {
|
||||||
|
let sha: string | null = null
|
||||||
|
if (status === 'A' || status === 'M') {
|
||||||
|
core.info(`Creating blob for file '${path}'`)
|
||||||
|
const {data: blob} = await blobCreationLimit(() =>
|
||||||
|
this.octokit.rest.git.createBlob({
|
||||||
|
...repository,
|
||||||
|
content: utils.readFileBase64([repoPath, path]),
|
||||||
|
encoding: 'base64'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
sha = blob.sha
|
||||||
|
}
|
||||||
|
return <TreeObject>{
|
||||||
|
path,
|
||||||
|
mode,
|
||||||
|
sha,
|
||||||
|
type: 'blob'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
core.info(`Creating tree for local commit ${commit.sha}`)
|
||||||
|
const {data: tree} = await this.octokit.rest.git.createTree({
|
||||||
|
...repository,
|
||||||
|
base_tree: parents[0],
|
||||||
|
tree: treeObjects
|
||||||
|
})
|
||||||
|
treeSha = tree.sha
|
||||||
|
core.info(`Created tree ${treeSha} for local commit ${commit.sha}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const {data: remoteCommit} = await this.octokit.rest.git.createCommit({
|
||||||
|
...repository,
|
||||||
|
parents: parents,
|
||||||
|
tree: treeSha,
|
||||||
|
message: `${commit.subject}\n\n${commit.body}`
|
||||||
|
})
|
||||||
|
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,
|
||||||
|
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}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ async function run(): Promise<void> {
|
||||||
branchSuffix: core.getInput('branch-suffix'),
|
branchSuffix: core.getInput('branch-suffix'),
|
||||||
base: core.getInput('base'),
|
base: core.getInput('base'),
|
||||||
pushToFork: core.getInput('push-to-fork'),
|
pushToFork: core.getInput('push-to-fork'),
|
||||||
|
signCommits: core.getBooleanInput('sign-commits'),
|
||||||
title: core.getInput('title'),
|
title: core.getInput('title'),
|
||||||
body: core.getInput('body'),
|
body: core.getInput('body'),
|
||||||
bodyPath: core.getInput('body-path'),
|
bodyPath: core.getInput('body-path'),
|
||||||
|
@ -27,7 +28,8 @@ async function run(): Promise<void> {
|
||||||
reviewers: utils.getInputAsArray('reviewers'),
|
reviewers: utils.getInputAsArray('reviewers'),
|
||||||
teamReviewers: utils.getInputAsArray('team-reviewers'),
|
teamReviewers: utils.getInputAsArray('team-reviewers'),
|
||||||
milestone: Number(core.getInput('milestone')),
|
milestone: Number(core.getInput('milestone')),
|
||||||
draft: core.getBooleanInput('draft')
|
draft: core.getBooleanInput('draft'),
|
||||||
|
maintainerCanModify: core.getBooleanInput('maintainer-can-modify')
|
||||||
}
|
}
|
||||||
core.debug(`Inputs: ${inspect(inputs)}`)
|
core.debug(`Inputs: ${inspect(inputs)}`)
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,38 @@
|
||||||
import {Octokit as Core} from '@octokit/core'
|
import * as core from '@actions/core'
|
||||||
|
import {Octokit as OctokitCore} from '@octokit/core'
|
||||||
import {paginateRest} from '@octokit/plugin-paginate-rest'
|
import {paginateRest} from '@octokit/plugin-paginate-rest'
|
||||||
import {restEndpointMethods} from '@octokit/plugin-rest-endpoint-methods'
|
import {restEndpointMethods} from '@octokit/plugin-rest-endpoint-methods'
|
||||||
|
import {throttling} from '@octokit/plugin-throttling'
|
||||||
import {getProxyForUrl} from 'proxy-from-env'
|
import {getProxyForUrl} from 'proxy-from-env'
|
||||||
import {ProxyAgent, fetch as undiciFetch} from 'undici'
|
import {ProxyAgent, fetch as undiciFetch} from 'undici'
|
||||||
export {RestEndpointMethodTypes} from '@octokit/plugin-rest-endpoint-methods'
|
export {RestEndpointMethodTypes} from '@octokit/plugin-rest-endpoint-methods'
|
||||||
|
// eslint-disable-next-line import/no-unresolved
|
||||||
export {OctokitOptions} from '@octokit/core/dist-types/types'
|
export {OctokitOptions} from '@octokit/core/dist-types/types'
|
||||||
|
|
||||||
export const Octokit = Core.plugin(
|
export const Octokit = OctokitCore.plugin(
|
||||||
paginateRest,
|
paginateRest,
|
||||||
restEndpointMethods,
|
restEndpointMethods,
|
||||||
|
throttling,
|
||||||
autoProxyAgent
|
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.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const proxyFetch =
|
const proxyFetch =
|
||||||
(proxyUrl: string): typeof undiciFetch =>
|
(proxyUrl: string): typeof undiciFetch =>
|
||||||
(url, opts) => {
|
(url, opts) => {
|
||||||
|
@ -24,7 +45,7 @@ const proxyFetch =
|
||||||
}
|
}
|
||||||
|
|
||||||
// Octokit plugin to support the standard environment variables http_proxy, https_proxy and no_proxy
|
// Octokit plugin to support the standard environment variables http_proxy, https_proxy and no_proxy
|
||||||
function autoProxyAgent(octokit: Core) {
|
function autoProxyAgent(octokit: OctokitCore) {
|
||||||
octokit.hook.before('request', options => {
|
octokit.hook.before('request', options => {
|
||||||
const proxy = getProxyForUrl(options.baseUrl)
|
const proxy = getProxyForUrl(options.baseUrl)
|
||||||
if (proxy) {
|
if (proxy) {
|
||||||
|
|
|
@ -126,6 +126,10 @@ export function readFile(path: string): string {
|
||||||
return fs.readFileSync(path, 'utf-8')
|
return fs.readFileSync(path, 'utf-8')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function readFileBase64(pathParts: string[]): string {
|
||||||
|
return fs.readFileSync(path.resolve(...pathParts)).toString('base64')
|
||||||
|
}
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
function hasErrorCode(error: any): error is {code: string} {
|
function hasErrorCode(error: any): error is {code: string} {
|
||||||
return typeof (error && error.code) === 'string'
|
return typeof (error && error.code) === 'string'
|
||||||
|
|
Loading…
Reference in a new issue