Compare commits
394 Commits
v1.5.1-mul
...
v3.1.0
Author | SHA1 | Date | |
---|---|---|---|
40b1b245eb | |||
64c8d492d5 | |||
28fddffcf8 | |||
ba8309ff9c | |||
ee482f51bd | |||
bf7dd38b26 | |||
71a37c5c33 | |||
f9af0fbfc0 | |||
0c57887fe8 | |||
7ad9f6e012 | |||
dd364f2835 | |||
ddd52205b6 | |||
a81d0faef6 | |||
fc03a820b2 | |||
b2cdf099d0 | |||
f3d08d3f0b | |||
3a21b8d668 | |||
132b02c8e1 | |||
9507cdc7ac | |||
f4bf0d40c3 | |||
cf325ed84b | |||
ebc5e02585 | |||
c01f8fadc7 | |||
c0c9424b03 | |||
7649c29e89 | |||
125f154b50 | |||
8c01dce3ac | |||
3c6aade49b | |||
76c09b178c | |||
2f62e00e1b | |||
16354377ad | |||
809468fcd7 | |||
216b3681c3 | |||
6fa44e144d | |||
b5b1bc17a4 | |||
794518a553 | |||
f4ee4a8333 | |||
053b501145 | |||
1a182d0679 | |||
24012f5c84 | |||
a6a1a418bf | |||
5429e57002 | |||
3c32fac1fe | |||
803cc5ea8a | |||
4ba9ca3d10 | |||
40e70b8f7b | |||
b7eabdce59 | |||
43dc723813 | |||
4d3b0a48ef | |||
e9a825aacd | |||
3509fd45ae | |||
14ee9d1df2 | |||
5d969a55c1 | |||
44130f6fc9 | |||
86ccd8cdef | |||
7e7fa32a5f | |||
5f7beeb2ff | |||
926d56fcba | |||
8d744a2cd3 | |||
c2d829c681 | |||
d77392faf0 | |||
ccd2b64012 | |||
58fb221778 | |||
7a856e8b5d | |||
65d7a66451 | |||
44a7f59b6f | |||
8acaf6bb4c | |||
498d78cb23 | |||
172ec762f8 | |||
5cb0d674f3 | |||
eb892d7803 | |||
f8274253bd | |||
0f423da02c | |||
9573f479a0 | |||
eb4cde120d | |||
25902ccdd1 | |||
39b337e8bb | |||
7e70d8e63c | |||
1a640f5b01 | |||
9faa8dc1d9 | |||
afcf57957d | |||
8cc3564bf3 | |||
6910c5cd03 | |||
14836c6ff3 | |||
8fbfcfbcbb | |||
326f260418 | |||
75104b7d7e | |||
fbb7e0e650 | |||
0f1e60a1f8 | |||
caa116d991 | |||
d2f72f0799 | |||
ded05960f3 | |||
eb605db8a3 | |||
b11e4c665b | |||
65327d17a5 | |||
0837238e66 | |||
17bd947e89 | |||
24b42ba7f4 | |||
c0aaf5bab9 | |||
968cb0f4d9 | |||
9d6f73d546 | |||
70d240d0c4 | |||
7bb7d96c96 | |||
66fcd19e8d | |||
d93f2b46fd | |||
246328e3d8 | |||
61cff7c673 | |||
46ba7bdfe8 | |||
d650be7389 | |||
2f7173349f | |||
4ca95026d7 | |||
64c4efd526 | |||
16e35685ce | |||
3b12cf0165 | |||
2a283f5fc3 | |||
8ed207bcca | |||
bd1f6727cd | |||
9a3acf8f32 | |||
b38fd9eb87 | |||
4a9e76e377 | |||
8cb4c8b741 | |||
b9eb5dd95e | |||
5502904068 | |||
56ad1fed7b | |||
2132f428f6 | |||
c558d39395 | |||
db640fa8db | |||
95d6677567 | |||
1f4e24248b | |||
08595270b5 | |||
237244614a | |||
6295d61f0c | |||
d4024e2876 | |||
ee96ad03d9 | |||
755b39d2ff | |||
9f95ac6c53 | |||
cedbe4ad47 | |||
0e48ed8743 | |||
e7291b422e | |||
eb824681a8 | |||
4cc13107a9 | |||
c71b8e4206 | |||
e2bf7f9b75 | |||
e1f4cfdcd4 | |||
b3f0552507 | |||
f4be118b21 | |||
c9f22f86fb | |||
35d5f3c8ae | |||
000a0fc06a | |||
2a59f517a7 | |||
48ce89bc7d | |||
6570353abb | |||
8f6cecd6c4 | |||
e14ef3b543 | |||
c5778e5181 | |||
374fc61fef | |||
6fa547cc6f | |||
4db3619128 | |||
989a8308ec | |||
6249109e58 | |||
c9b850c450 | |||
340e629d2f | |||
abc19caa82 | |||
3474dda921 | |||
ad11b10aa4 | |||
86aa5be8bf | |||
6867319cf3 | |||
cc84a2389e | |||
7e7150d0e8 | |||
eb99d45ce6 | |||
115b7391e1 | |||
8305970523 | |||
32f5c5dd5f | |||
628c2d7d35 | |||
37582e8764 | |||
1e6b4d1790 | |||
a70c6ebe2a | |||
5bd05538d0 | |||
26bc40eea1 | |||
6bb0e7771c | |||
4c347a4514 | |||
cff2c3381d | |||
e48dab0c1c | |||
ac9f92d6e7 | |||
3ff256ce08 | |||
0bcb10560b | |||
3c4b2793c1 | |||
22870d7816 | |||
e324a22ee1 | |||
8b60386018 | |||
6852d55922 | |||
32e5bb80a5 | |||
619cf2115d | |||
69008aa567 | |||
ae4278bf24 | |||
88da40fea7 | |||
694e068136 | |||
64c34f6885 | |||
52ada17960 | |||
ce00b952cf | |||
0d42c285a3 | |||
ea1eaf1734 | |||
d5c5ea3e20 | |||
c7b64af0a4 | |||
289fda9fea | |||
b021b9e27a | |||
c26314237b | |||
ed7dd8d236 | |||
ca0e9d75fd | |||
87c27ee3eb | |||
4beea725d3 | |||
9d58699da5 | |||
bceebba814 | |||
0b82710b2e | |||
4aaaa0e760 | |||
918bfcb8a3 | |||
1ecfd1ae40 | |||
4dd195d7c3 | |||
72b5f45bb4 | |||
5394814b39 | |||
83918398f5 | |||
b6a458d96a | |||
7b5ff6b642 | |||
82eddd8828 | |||
6df2a462d1 | |||
3689bd07d7 | |||
943f19ac64 | |||
e59d6c7fff | |||
c65a4f39b3 | |||
2d18371789 | |||
cc7020a609 | |||
d8700620d6 | |||
b7064071dc | |||
339e82d37b | |||
a319015452 | |||
df0d07269b | |||
2aa04baf2e | |||
d29f1e9296 | |||
8ee12880b0 | |||
8d39de8771 | |||
c202684c92 | |||
e970adccb4 | |||
9f9af3e969 | |||
615d7c82e3 | |||
8246a6aea9 | |||
1e09ec2f22 | |||
4c018f4174 | |||
080aaa90aa | |||
6ac64298f6 | |||
76e932f3c9 | |||
06078e295b | |||
004434a414 | |||
4661d6d7af | |||
a972260284 | |||
f1d6c2dca3 | |||
aa1cba4c18 | |||
9b33a4edd8 | |||
938e6aea6f | |||
699d304845 | |||
2ce0b003ee | |||
10653669e8 | |||
c39f04a86a | |||
430f28afbf | |||
8e07307f04 | |||
c8fed30c65 | |||
11c388252c | |||
e41956c7e7 | |||
f3583f71f8 | |||
e8e53ed431 | |||
cff82c7ec9 | |||
c388aba95a | |||
02f0ffd558 | |||
b616fd026e | |||
ba79823749 | |||
0af5090b86 | |||
b750589747 | |||
894d4b1412 | |||
95615643f0 | |||
fb48683668 | |||
0fb7b7c32a | |||
3e4ab24cbb | |||
8c86d0f83f | |||
673922cfd2 | |||
b11cb71e96 | |||
bdadb3b4a0 | |||
9bdc8b271f | |||
c208033c6a | |||
01aec28fd9 | |||
484de7fc89 | |||
71f4fe31a8 | |||
883f800b96 | |||
b3805d65e3 | |||
4f8d5b9d3e | |||
efbd4fa1ef | |||
b7565b81a7 | |||
ec6352b0a0 | |||
ce4df54075 | |||
c855a500d1 | |||
baf52378ae | |||
e6ebd7ca5c | |||
83fef4e8a1 | |||
d3dc225920 | |||
6342438c4e | |||
324fe6cdde | |||
4faa5d5a97 | |||
381bc30cef | |||
7531167f24 | |||
66a1436815 | |||
1d1fedd99c | |||
823751817d | |||
1edcf07a19 | |||
4c95214a9b | |||
b37c0a038c | |||
d9841567d1 | |||
2cce94bfb0 | |||
d968e8b11b | |||
6c73093f9b | |||
d17c882ef3 | |||
d5f4e48a66 | |||
484d8bd85d | |||
74b6a9337d | |||
2dd62d446e | |||
b6a98c049d | |||
67e8822279 | |||
956a240181 | |||
179aca65d7 | |||
e4c8b68e33 | |||
731211fd81 | |||
006313b45a | |||
3c86e9a421 | |||
e49ebca896 | |||
d9652cf06a | |||
4517bf7ce3 | |||
04aa84a2af | |||
11baa5dc07 | |||
1281ebd51a | |||
46dc4f23d5 | |||
d469aaf0ef | |||
5692e041c6 | |||
55e7b1ec28 | |||
d99b58f9d5 | |||
b8ce455e56 | |||
c9e477ec05 | |||
979d417e9d | |||
b74a77cefc | |||
2af7d9797a | |||
b72489d629 | |||
1461de7c9d | |||
3061b7bc6e | |||
98ee7d63d3 | |||
066b4398c0 | |||
028e1bbdd5 | |||
8b8594e5e4 | |||
cefb562076 | |||
9eccdd1603 | |||
f429dfef59 | |||
963e38fd7d | |||
f411b5902d | |||
97dd1f3ef4 | |||
3413a77ac7 | |||
206756cd93 | |||
263b5927d1 | |||
a0ad81c6c7 | |||
23846c7f83 | |||
96ac64ddca | |||
a2ed0052e5 | |||
6e7e889e01 | |||
7e18a60b72 | |||
0be4e396cf | |||
b86bbce924 | |||
e1e9dd3f1c | |||
8c2a43987b | |||
c5ff844b40 | |||
1f47cf3861 | |||
bcdc7ae6f9 | |||
91ca84eab5 | |||
875d189c30 | |||
7c3f5f4833 | |||
7a7e797068 | |||
a41f37fc96 | |||
10554ac6c3 | |||
2439726cfe | |||
4f04f4efd9 | |||
32d0abbc7c | |||
ed5b8b1eea | |||
ea241dfc79 | |||
0e70fec054 | |||
5077e8d6bd | |||
901e854f30 | |||
f3b1bd6331 | |||
51ade9f54b | |||
3c86dbf9e6 | |||
b2d426ea4e | |||
fbe0cd3d1a |
3
.eslintignore
Normal file
3
.eslintignore
Normal file
@ -0,0 +1,3 @@
|
||||
dist/
|
||||
lib/
|
||||
node_modules/
|
19
.eslintrc.json
Normal file
19
.eslintrc.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"env": { "node": true, "jest": true },
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": { "ecmaVersion": 9, "sourceType": "module" },
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:import/errors",
|
||||
"plugin:import/warnings",
|
||||
"plugin:import/typescript",
|
||||
"plugin:prettier/recommended",
|
||||
"prettier/@typescript-eslint"
|
||||
],
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"rules": {
|
||||
"@typescript-eslint/camelcase": "off"
|
||||
}
|
||||
}
|
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -1 +0,0 @@
|
||||
* @peter-evans
|
7
.github/ISSUE_TEMPLATE.md
vendored
Normal file
7
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
### Subject of the issue
|
||||
|
||||
Describe your issue here.
|
||||
|
||||
### Steps to reproduce
|
||||
|
||||
If this issue is describing a possible bug please provide (or link to) your GitHub Actions workflow.
|
131
.github/workflows/ci.yml
vendored
Normal file
131
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,131 @@
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths-ignore:
|
||||
- 'README.md'
|
||||
- 'docs/**'
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths-ignore:
|
||||
- 'README.md'
|
||||
- 'docs/**'
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npm run format-check
|
||||
- run: npm run lint
|
||||
- run: npm run test
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: dist
|
||||
path: dist
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: action.yml
|
||||
path: action.yml
|
||||
|
||||
test:
|
||||
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
needs: [build]
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
target: [built, committed]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: master
|
||||
- if: matrix.target == 'built' || github.event_name == 'pull_request'
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: dist
|
||||
path: dist
|
||||
- if: matrix.target == 'built' || github.event_name == 'pull_request'
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: action.yml
|
||||
path: .
|
||||
|
||||
- name: Create change
|
||||
run: date +%s > report.txt
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: ./
|
||||
with:
|
||||
commit-message: '[CI] test ${{ matrix.target }}'
|
||||
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 }}'
|
||||
|
||||
Auto-generated by [create-pull-request][1]
|
||||
|
||||
[1]: https://github.com/peter-evans/create-pull-request
|
||||
branch: ci-test-${{ matrix.target }}
|
||||
|
||||
- name: Close Pull
|
||||
uses: peter-evans/close-pull@v1
|
||||
with:
|
||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
comment: '[CI] test ${{ matrix.target }}'
|
||||
delete-branch: true
|
||||
|
||||
commentTestSuiteHelp:
|
||||
if: github.event_name == 'pull_request'
|
||||
needs: [test]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Find Comment
|
||||
uses: peter-evans/find-comment@v1
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ github.event.number }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: Full test suite slash command
|
||||
|
||||
- if: steps.fc.outputs.comment-id == ''
|
||||
name: Create comment
|
||||
uses: peter-evans/create-or-update-comment@v1
|
||||
with:
|
||||
issue-number: ${{ github.event.number }}
|
||||
body: |
|
||||
Full test suite slash command (repository admin only)
|
||||
```
|
||||
/test repository=${{ github.event.pull_request.head.repo.full_name }} ref=${{ github.event.pull_request.head.ref }} build=true
|
||||
```
|
||||
|
||||
package:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||
needs: [test]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: dist
|
||||
path: dist
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
with:
|
||||
commit-message: Update distribution
|
||||
title: Update distribution
|
||||
body: |
|
||||
- Updates the distribution for changes on `master`
|
||||
|
||||
Auto-generated by [create-pull-request][1]
|
||||
|
||||
[1]: https://github.com/peter-evans/create-pull-request
|
||||
branch: update-distribution
|
47
.github/workflows/cpr-example-command.yml
vendored
Normal file
47
.github/workflows/cpr-example-command.yml
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
name: Create Pull Request Example Command
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [cpr-example-command]
|
||||
jobs:
|
||||
createPullRequest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Make changes to pull request
|
||||
run: date +%s > report.txt
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: ./
|
||||
with:
|
||||
commit-message: Update report
|
||||
committer: GitHub <noreply@github.com>
|
||||
author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
|
||||
signoff: false
|
||||
title: '[Example] Update report'
|
||||
body: |
|
||||
Update report
|
||||
- Updated with *today's* date
|
||||
- Auto-generated by [create-pull-request][1]
|
||||
|
||||
[1]: https://github.com/peter-evans/create-pull-request
|
||||
labels: |
|
||||
report
|
||||
automated pr
|
||||
assignees: peter-evans
|
||||
reviewers: peter-evans
|
||||
milestone: 1
|
||||
draft: false
|
||||
branch: example-patches
|
||||
|
||||
- name: Check output
|
||||
run: |
|
||||
echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
|
||||
|
||||
- name: Add reaction
|
||||
uses: peter-evans/create-or-update-comment@v1
|
||||
with:
|
||||
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
|
||||
comment-id: ${{ github.event.client_payload.github.payload.comment.id }}
|
||||
reaction-type: hooray
|
42
.github/workflows/create-pull-request-multi.yml
vendored
42
.github/workflows/create-pull-request-multi.yml
vendored
@ -1,42 +0,0 @@
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [create-pull-request-multi]
|
||||
name: create-pull-request workflow
|
||||
jobs:
|
||||
createPullRequest:
|
||||
name: Testing on ${{ matrix.platform }}
|
||||
strategy:
|
||||
matrix:
|
||||
platform: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Create report file
|
||||
if: matrix.platform == 'ubuntu-latest' || matrix.platform == 'macos-latest'
|
||||
run: date +%s > report.txt
|
||||
- name: Create report file (windows)
|
||||
if: matrix.platform == 'windows-latest'
|
||||
run: echo %DATE% %TIME% > report.txt
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@multi-platform-release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
COMMIT_MESSAGE: Add report file
|
||||
COMMIT_AUTHOR_EMAIL: peter-evans@users.noreply.github.com
|
||||
COMMIT_AUTHOR_NAME: Peter Evans
|
||||
PULL_REQUEST_TITLE: '[Example] Add report file'
|
||||
PULL_REQUEST_BODY: >
|
||||
This PR is auto-generated by
|
||||
[create-pull-request](https://github.com/peter-evans/create-pull-request).
|
||||
PULL_REQUEST_LABELS: report, automated pr
|
||||
PULL_REQUEST_ASSIGNEES: peter-evans
|
||||
PULL_REQUEST_REVIEWERS: peter-evans
|
||||
PULL_REQUEST_MILESTONE: 1
|
||||
PULL_REQUEST_BRANCH: example-patches
|
||||
BRANCH_SUFFIX: 'random'
|
||||
- name: Check output environment variable
|
||||
if: matrix.platform == 'ubuntu-latest' || matrix.platform == 'macos-latest'
|
||||
run: echo "Pull Request Number - $PULL_REQUEST_NUMBER"
|
||||
- name: Check output environment variable (windows)
|
||||
if: matrix.platform == 'windows-latest'
|
||||
run: echo Pull Request Number - %PULL_REQUEST_NUMBER%
|
30
.github/workflows/create-pull-request.yml
vendored
30
.github/workflows/create-pull-request.yml
vendored
@ -1,30 +0,0 @@
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [create-pull-request]
|
||||
name: create-pull-request workflow
|
||||
jobs:
|
||||
createPullRequest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Create report file
|
||||
run: date +%s > report.txt
|
||||
- name: Create Pull Request
|
||||
uses: ./
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
COMMIT_MESSAGE: Add report file
|
||||
COMMIT_AUTHOR_EMAIL: peter-evans@users.noreply.github.com
|
||||
COMMIT_AUTHOR_NAME: Peter Evans
|
||||
PULL_REQUEST_TITLE: '[Example] Add report file'
|
||||
PULL_REQUEST_BODY: >
|
||||
This PR is auto-generated by
|
||||
[create-pull-request](https://github.com/peter-evans/create-pull-request).
|
||||
PULL_REQUEST_LABELS: report, automated pr
|
||||
PULL_REQUEST_ASSIGNEES: peter-evans
|
||||
PULL_REQUEST_REVIEWERS: peter-evans
|
||||
PULL_REQUEST_MILESTONE: 1
|
||||
PULL_REQUEST_BRANCH: example-patches
|
||||
BRANCH_SUFFIX: short-commit-hash
|
||||
- name: Check output environment variable
|
||||
run: echo "Pull Request Number - $PULL_REQUEST_NUMBER"
|
16
.github/workflows/push.yml
vendored
16
.github/workflows/push.yml
vendored
@ -1,16 +0,0 @@
|
||||
name: Update Docker Hub Description
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
dockerHubDescription:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Docker Hub Description
|
||||
uses: peter-evans/dockerhub-description@v2.1.0
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
DOCKERHUB_REPOSITORY: peterevans/create-pull-request
|
43
.github/workflows/slash-command-dispatch.yml
vendored
Normal file
43
.github/workflows/slash-command-dispatch.yml
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
name: Slash Command Dispatch
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
jobs:
|
||||
slashCommandDispatch:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Slash Command Dispatch
|
||||
uses: peter-evans/slash-command-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.ACTIONS_BOT_TOKEN }}
|
||||
config: >
|
||||
[
|
||||
{
|
||||
"command": "test",
|
||||
"permission": "admin",
|
||||
"repository": "peter-evans/create-pull-request-tests",
|
||||
"named_args": true
|
||||
},
|
||||
{
|
||||
"command": "pytest",
|
||||
"permission": "admin",
|
||||
"repository": "peter-evans/create-pull-request-tests",
|
||||
"named_args": true
|
||||
},
|
||||
{
|
||||
"command": "clean",
|
||||
"permission": "admin",
|
||||
"repository": "peter-evans/create-pull-request-tests"
|
||||
},
|
||||
{
|
||||
"command": "cpr-example",
|
||||
"permission": "admin",
|
||||
"issue_type": "issue"
|
||||
},
|
||||
{
|
||||
"command": "rebase",
|
||||
"permission": "admin",
|
||||
"repository": "peter-evans/slash-command-dispatch-processor",
|
||||
"issue_type": "pull-request"
|
||||
}
|
||||
]
|
31
.github/workflows/update-dep.yml
vendored
Normal file
31
.github/workflows/update-dep.yml
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
name: Update Dependencies
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 1 * * 4'
|
||||
jobs:
|
||||
update-dep:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12.x'
|
||||
- name: Update dependencies
|
||||
run: |
|
||||
npx -p npm-check-updates ncu -u
|
||||
npm install
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
with:
|
||||
token: ${{ secrets.ACTIONS_BOT_TOKEN }}
|
||||
commit-message: Update dependencies
|
||||
committer: GitHub <noreply@github.com>
|
||||
author: actions-bot <actions-bot@users.noreply.github.com>
|
||||
title: Update dependencies
|
||||
body: |
|
||||
- Dependency updates
|
||||
|
||||
Auto-generated by [create-pull-request][1]
|
||||
|
||||
[1]: https://github.com/peter-evans/create-pull-request
|
||||
branch: update-dependencies
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1 +1,4 @@
|
||||
lib/
|
||||
node_modules/
|
||||
|
||||
.DS_Store
|
||||
|
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@ -0,0 +1,3 @@
|
||||
dist/
|
||||
lib/
|
||||
node_modules/
|
11
.prettierrc.json
Normal file
11
.prettierrc.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"bracketSpacing": false,
|
||||
"arrowParens": "avoid",
|
||||
"parser": "typescript"
|
||||
}
|
20
Dockerfile
20
Dockerfile
@ -1,20 +0,0 @@
|
||||
FROM alpine:3.10.2
|
||||
|
||||
LABEL maintainer="Peter Evans <mail@peterevans.dev>"
|
||||
LABEL repository="https://github.com/peter-evans/create-pull-request"
|
||||
LABEL homepage="https://github.com/peter-evans/create-pull-request"
|
||||
|
||||
LABEL com.github.actions.name="Create Pull Request"
|
||||
LABEL com.github.actions.description="Creates a pull request for changes to your repository in the actions workspace"
|
||||
LABEL com.github.actions.icon="git-pull-request"
|
||||
LABEL com.github.actions.color="gray-dark"
|
||||
|
||||
COPY LICENSE README.md /
|
||||
|
||||
RUN apk add python3-dev git git-lfs
|
||||
|
||||
COPY requirements.txt /tmp/
|
||||
RUN pip3 install --requirement /tmp/requirements.txt
|
||||
|
||||
COPY create-pull-request.py /create-pull-request.py
|
||||
ENTRYPOINT [ "/create-pull-request.py" ]
|
251
README.md
251
README.md
@ -1,4 +1,5 @@
|
||||
# <img width="24" height="24" src="logo.svg"> Create Pull Request
|
||||
# <img width="24" height="24" src="docs/assets/logo.svg"> Create Pull Request
|
||||
[](https://github.com/peter-evans/create-pull-request/actions?query=workflow%3ACI)
|
||||
[](https://github.com/marketplace/actions/create-pull-request)
|
||||
|
||||
A GitHub action to create a pull request for changes to your repository in the actions workspace.
|
||||
@ -9,151 +10,205 @@ The changes will be automatically committed to a new branch and a pull request c
|
||||
|
||||
Create Pull Request action will:
|
||||
|
||||
1. Check for repository changes in the Actions workspace. This includes untracked (new) files as well as modified files.
|
||||
2. Commit all changes to a new branch, or update an existing pull request branch. The commit will be made using the name and email of the `HEAD` commit author.
|
||||
3. Create a pull request to merge the new branch into the currently active branch executing the workflow.
|
||||
1. Check for repository changes in the Actions workspace. This includes:
|
||||
- untracked (new) files
|
||||
- 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 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 v3](docs/updating.md)
|
||||
|
||||
## Usage
|
||||
|
||||
Linux
|
||||
```yml
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v1.5.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
```
|
||||
|
||||
Multi platform - Linux, MacOS, Windows (beta)
|
||||
You can also pin to a [specific release](https://github.com/peter-evans/create-pull-request/releases) version in the format `@v3.x.x`
|
||||
|
||||
### Action inputs
|
||||
|
||||
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](https://github.com/peter-evans/create-pull-request/blob/master/docs/concepts-guidelines.md#triggering-further-workflow-runs) for workarounds.
|
||||
|
||||
| Name | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `token` | `GITHUB_TOKEN` or a `repo` scoped [Personal Access Token (PAT)](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line). | `GITHUB_TOKEN` |
|
||||
| `path` | Relative path under `GITHUB_WORKSPACE` to the repository. | `GITHUB_WORKSPACE` |
|
||||
| `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` line by the committer at the end of the commit log message. | `false` |
|
||||
| `branch` | The pull request branch name. | `create-pull-request/patch` |
|
||||
| `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](https://github.com/peter-evans/create-pull-request/blob/master/docs/concepts-guidelines.md#push-pull-request-branches-to-a-fork) for details. | |
|
||||
| `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` |
|
||||
| `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://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line) may be required. See [this issue](https://github.com/peter-evans/create-pull-request/issues/155). | |
|
||||
| `milestone` | The number of the milestone to associate this pull request with. | |
|
||||
| `draft` | Create a [draft pull request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests#draft-pull-requests). | `false` |
|
||||
|
||||
### Action outputs
|
||||
|
||||
The pull request number is output as a step output.
|
||||
Note that in order to read the step output the action step must have an id.
|
||||
|
||||
```yml
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v1.5.0-multi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
- name: Check outputs
|
||||
run: |
|
||||
echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
|
||||
```
|
||||
|
||||
**Note**: If you want pull requests created by this action to trigger an `on: pull_request` workflow then you must use a Personal Access Token instead of the default `GITHUB_TOKEN`.
|
||||
See [this issue](https://github.com/peter-evans/create-pull-request/issues/48) for further details.
|
||||
### Checkout
|
||||
|
||||
### Environment variables
|
||||
This action expects repositories to be checked out with `actions/checkout@v2`.
|
||||
|
||||
These variables are all optional. If not set, a default value will be used.
|
||||
If there is some reason you need to use `actions/checkout@v1` the following step can be added to checkout the branch.
|
||||
|
||||
- `COMMIT_MESSAGE` - The message to use when committing changes.
|
||||
- `PULL_REQUEST_TITLE` - The title of the pull request.
|
||||
- `PULL_REQUEST_BODY` - The body of the pull request.
|
||||
- `PULL_REQUEST_LABELS` - A comma separated list of labels.
|
||||
- `PULL_REQUEST_ASSIGNEES` - A comma separated list of assignees (GitHub usernames).
|
||||
- `PULL_REQUEST_REVIEWERS` - A comma separated list of reviewers (GitHub usernames) to request a review from.
|
||||
- `PULL_REQUEST_TEAM_REVIEWERS` - A comma separated list of GitHub teams to request a review from.
|
||||
- `PULL_REQUEST_MILESTONE` - The number of the milestone to associate this pull request with.
|
||||
- `PULL_REQUEST_BRANCH` - The branch name. See **Branch naming** below for details.
|
||||
- `BRANCH_SUFFIX` - The branch suffix type. Valid values are `short-commit-hash` (default), `timestamp`, `random` and `none`. See **Branch naming** below for details.
|
||||
```yml
|
||||
- uses: actions/checkout@v1
|
||||
- run: git checkout "${GITHUB_REF:11}"
|
||||
```
|
||||
|
||||
Output environment variables
|
||||
### Action behaviour
|
||||
|
||||
- `PULL_REQUEST_NUMBER` - The number of the pull request created.
|
||||
The default behaviour of the action is to create a pull request that will be continually updated with new changes until it is merged or closed.
|
||||
Changes are committed and pushed to a fixed-name branch, the name of which can be configured with the `branch` input.
|
||||
Any subsequent changes will be committed to the *same* branch and reflected in the open pull request.
|
||||
|
||||
The following parameters are available for debugging and troubleshooting.
|
||||
How the action behaves:
|
||||
|
||||
- `DEBUG_EVENT` - If present, outputs the event data that triggered the workflow.
|
||||
- `SKIP_IGNORE` - If present, the `ignore_event` function will be skipped.
|
||||
- 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 and there are no further changes (i.e. no diff with the current pull request branch) then 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 base and pull request branch), the pull request is automatically closed and the branch deleted.
|
||||
|
||||
### Branch naming
|
||||
For further details about how the action works and usage guidelines, see [Concepts, guidelines and advanced usage](docs/concepts-guidelines.md).
|
||||
|
||||
For branch naming there are two strategies. Always create a new branch each time there are changes to be committed, OR, create a fixed-name pull request branch that will be updated with any new commits until it is merged or closed.
|
||||
#### Alternative strategy - Always create a new pull request branch
|
||||
|
||||
#### Strategy A - Always create a new pull request branch (default)
|
||||
For some use cases it may be desirable to always create a new unique branch each time there are changes to be committed.
|
||||
This strategy is *not recommended* because if not used carefully it could result in multiple pull requests being created unnecessarily. If in doubt, use the [default strategy](#action-behaviour) of creating an updating a fixed-name branch.
|
||||
|
||||
For this strategy there are three options to suffix the branch name.
|
||||
The branch name is defined by the variable `PULL_REQUEST_BRANCH` and defaults to `create-pull-request/patch`. The following options are values for `BRANCH_SUFFIX`.
|
||||
To use this strategy, set input `branch-suffix` with one of the following options.
|
||||
|
||||
- `short-commit-hash` (default) - Commits will be made to a branch suffixed with the short SHA1 commit hash. e.g. `create-pull-request/patch-fcdfb59`, `create-pull-request/patch-394710b`
|
||||
- `random` - Commits will be made to a branch suffixed with a random alpha-numeric string. e.g. `create-pull-request/patch-6qj97jr`, `create-pull-request/patch-5jrjhvd`
|
||||
|
||||
- `timestamp` - Commits will be made to a branch suffixed by a timestamp. e.g. `create-pull-request/patch-1569322532`, `create-pull-request/patch-1569322552`
|
||||
|
||||
- `random` - Commits will be made to a branch suffixed with a random alpha-numeric string. This option should be used if multiple pull requests will be created during the execution of a workflow. e.g. `create-pull-request/patch-6qj97jr`, `create-pull-request/patch-5jrjhvd`
|
||||
- `short-commit-hash` - Commits will be made to a branch suffixed with the short SHA1 commit hash. e.g. `create-pull-request/patch-fcdfb59`, `create-pull-request/patch-394710b`
|
||||
|
||||
#### Strategy B - Create and update a pull request branch
|
||||
### Controlling commits
|
||||
|
||||
To use this strategy, set `BRANCH_SUFFIX` to the value `none`. The variable `PULL_REQUEST_BRANCH` defaults to `create-pull-request/patch`. Commits will be made to this branch and a pull request created. Any subsequent changes will be committed to the *same* branch and reflected in the existing pull request.
|
||||
As well as relying on the action to handle uncommitted changes, you can additionally make your own commits before the action runs.
|
||||
|
||||
```yml
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Create commits
|
||||
run: |
|
||||
git config user.name 'Peter Evans'
|
||||
git config user.email 'peter-evans@users.noreply.github.com'
|
||||
date +%s > report.txt
|
||||
git commit -am "Modify tracked file during workflow"
|
||||
date +%s > new-report.txt
|
||||
git add -A
|
||||
git commit -m "Add untracked file during workflow"
|
||||
- name: Uncommitted change
|
||||
run: date +%s > report.txt
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
```
|
||||
|
||||
### Ignoring files
|
||||
|
||||
If there are files or directories you want to ignore you can simply add them to a `.gitignore` file at the root of your repository. The action will respect this file.
|
||||
|
||||
## Examples
|
||||
### Create a project card
|
||||
|
||||
Here is an example that sets all the main environment variables.
|
||||
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
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [create-pull-request]
|
||||
name: create-pull-request workflow
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
|
||||
- name: Create or Update Project Card
|
||||
uses: peter-evans/create-or-update-project-card@v1
|
||||
with:
|
||||
project-name: My project
|
||||
column-name: My column
|
||||
issue-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
```
|
||||
|
||||
## Reference Example
|
||||
|
||||
The following workflow is a reference example that sets many of the main inputs.
|
||||
|
||||
See [examples](docs/examples.md) for more realistic use cases.
|
||||
|
||||
```yml
|
||||
name: Create Pull Request
|
||||
on: push
|
||||
jobs:
|
||||
createPullRequest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Create report file
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Make changes to pull request
|
||||
run: date +%s > report.txt
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v1.5.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
COMMIT_MESSAGE: Add report file
|
||||
PULL_REQUEST_TITLE: '[Example] Add report file'
|
||||
PULL_REQUEST_BODY: >
|
||||
This PR is auto-generated by
|
||||
[create-pull-request](https://github.com/peter-evans/create-pull-request).
|
||||
PULL_REQUEST_LABELS: report, automated pr
|
||||
PULL_REQUEST_ASSIGNEES: peter-evans
|
||||
PULL_REQUEST_REVIEWERS: peter-evans
|
||||
PULL_REQUEST_MILESTONE: 1
|
||||
PULL_REQUEST_BRANCH: example-patches
|
||||
BRANCH_SUFFIX: short-commit-hash
|
||||
- name: Check output environment variable
|
||||
run: echo "Pull Request Number - $PULL_REQUEST_NUMBER"
|
||||
```
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
commit-message: Update report
|
||||
committer: GitHub <noreply@github.com>
|
||||
author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
|
||||
signoff: false
|
||||
branch: example-patches
|
||||
title: '[Example] Update report'
|
||||
body: |
|
||||
Update report
|
||||
- Updated with *today's* date
|
||||
- Auto-generated by [create-pull-request][1]
|
||||
|
||||
This configuration will create pull requests that look like this:
|
||||
[1]: https://github.com/peter-evans/create-pull-request
|
||||
labels: |
|
||||
report
|
||||
automated pr
|
||||
assignees: peter-evans
|
||||
reviewers: peter-evans
|
||||
team-reviewers: |
|
||||
owners
|
||||
maintainers
|
||||
milestone: 1
|
||||
draft: false
|
||||
|
||||

|
||||
|
||||
### Dynamic configuration using variables
|
||||
|
||||
The following examples show how configuration for the action can be dynamically defined in a previous workflow step.
|
||||
|
||||
The recommended method is to use `set-output`. Note that the step where output variables are defined must have an id.
|
||||
|
||||
```yml
|
||||
- name: Set output variables
|
||||
id: vars
|
||||
- name: Check output
|
||||
run: |
|
||||
echo ::set-output name=pr_title::"[Test] Add report file $(date +%d-%m-%Y)"
|
||||
echo ::set-output name=pr_body::"This PR was auto-generated on $(date +%d-%m-%Y) \
|
||||
by [create-pull-request](https://github.com/peter-evans/create-pull-request)."
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v1.5.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PULL_REQUEST_TITLE: ${{ steps.vars.outputs.pr_title }}
|
||||
PULL_REQUEST_BODY: ${{ steps.vars.outputs.pr_body }}
|
||||
echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
|
||||
```
|
||||
|
||||
Since the action reads environment variables from the system, it's technically not necessary to explicitly pass them as long as they exist in the environment. So the following method using `set-env` *also* works, but explicitly passing the configuration parameters using the previous method is perferred for its clarity.
|
||||
An example based on the above reference configuration creates pull requests that look like this:
|
||||
|
||||
```yml
|
||||
- name: Set environment variables
|
||||
run: |
|
||||
echo ::set-env name=PULL_REQUEST_TITLE::"[Test] Add report file $(date +%d-%m-%Y)"
|
||||
echo ::set-env name=PULL_REQUEST_BODY::"This PR was auto-generated on $(date +%d-%m-%Y) \
|
||||
by [create-pull-request](https://github.com/peter-evans/create-pull-request)."
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v1.5.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||

|
||||
|
||||
## License
|
||||
|
||||
|
1453
__test__/create-or-update-branch.int.test.ts
Normal file
1453
__test__/create-or-update-branch.int.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
40
__test__/entrypoint.sh
Executable file
40
__test__/entrypoint.sh
Executable file
@ -0,0 +1,40 @@
|
||||
#!/bin/sh -l
|
||||
set -euo pipefail
|
||||
|
||||
# Save the working directory
|
||||
WORKINGDIR=$PWD
|
||||
|
||||
# Create and serve a remote repo
|
||||
mkdir -p /git/remote
|
||||
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 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.md
|
||||
git add .
|
||||
git commit -m "initial commit"
|
||||
git push -u
|
||||
git log -1 --pretty=oneline
|
||||
git config --global --unset user.email
|
||||
git config --global --unset user.name
|
||||
git config -l
|
||||
|
||||
# Clone a server-side fork of the base repo
|
||||
cd $WORKINGDIR
|
||||
git clone --mirror git://127.0.0.1/test-base.git /git/remote/test-fork.git
|
||||
cd /git/remote/test-fork.git
|
||||
git log -1 --pretty=oneline
|
||||
|
||||
# Restore the working directory
|
||||
cd $WORKINGDIR
|
||||
|
||||
# Execute integration tests
|
||||
jest int --runInBand
|
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()
|
||||
})
|
||||
})
|
23
__test__/integration-tests.sh
Executable file
23
__test__/integration-tests.sh
Executable file
@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
IMAGE="cpr-integration-tests:latest"
|
||||
ARG1=${1:-}
|
||||
|
||||
if [[ "$(docker images -q $IMAGE 2> /dev/null)" == "" || $ARG1 == "build" ]]; then
|
||||
echo "Building Docker image $IMAGE ..."
|
||||
|
||||
cat > Dockerfile << EOF
|
||||
FROM node:12-alpine
|
||||
RUN apk --no-cache add git git-daemon
|
||||
RUN npm install jest --global
|
||||
WORKDIR /cpr
|
||||
COPY __test__/entrypoint.sh /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
EOF
|
||||
|
||||
docker build -t $IMAGE .
|
||||
rm Dockerfile
|
||||
fi
|
||||
|
||||
docker run -v $PWD:/cpr $IMAGE
|
124
__test__/utils.unit.test.ts
Normal file
124
__test__/utils.unit.test.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import * as path from 'path'
|
||||
import * as utils from '../lib/utils'
|
||||
|
||||
const originalGitHubWorkspace = process.env['GITHUB_WORKSPACE']
|
||||
|
||||
describe('utils tests', () => {
|
||||
beforeAll(() => {
|
||||
// GitHub workspace
|
||||
process.env['GITHUB_WORKSPACE'] = __dirname
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
// Restore GitHub workspace
|
||||
delete process.env['GITHUB_WORKSPACE']
|
||||
if (originalGitHubWorkspace) {
|
||||
process.env['GITHUB_WORKSPACE'] = originalGitHubWorkspace
|
||||
}
|
||||
})
|
||||
|
||||
test('getStringAsArray splits string input by newlines and commas', async () => {
|
||||
const array = utils.getStringAsArray('1, 2, 3\n4, 5, 6')
|
||||
expect(array.length).toEqual(6)
|
||||
|
||||
const array2 = utils.getStringAsArray('')
|
||||
expect(array2.length).toEqual(0)
|
||||
})
|
||||
|
||||
test('getRepoPath successfully returns the path to the repository', async () => {
|
||||
expect(utils.getRepoPath()).toEqual(process.env['GITHUB_WORKSPACE'])
|
||||
expect(utils.getRepoPath('foo')).toEqual(
|
||||
path.resolve(process.env['GITHUB_WORKSPACE'] || '', 'foo')
|
||||
)
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
|
||||
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) {
|
||||
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', 'peter-evans/create-pull-request')
|
||||
expect(url1).toEqual('https://github.com/peter-evans/create-pull-request')
|
||||
|
||||
const url2 = utils.getRemoteUrl('SSH', 'peter-evans/create-pull-request')
|
||||
expect(url2).toEqual('git@github.com:peter-evans/create-pull-request.git')
|
||||
})
|
||||
|
||||
test('secondsSinceEpoch returns the number of seconds since the Epoch', async () => {
|
||||
const seconds = `${utils.secondsSinceEpoch()}`
|
||||
expect(seconds.length).toEqual(10)
|
||||
})
|
||||
|
||||
test('randomString returns strings of length 7', async () => {
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
expect(utils.randomString().length).toEqual(7)
|
||||
}
|
||||
})
|
||||
|
||||
test('parseDisplayNameEmail successfully parses display name email formats', async () => {
|
||||
const parsed1 = utils.parseDisplayNameEmail('abc def <abc@def.com>')
|
||||
expect(parsed1.name).toEqual('abc def')
|
||||
expect(parsed1.email).toEqual('abc@def.com')
|
||||
|
||||
const parsed2 = utils.parseDisplayNameEmail(
|
||||
'github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>'
|
||||
)
|
||||
expect(parsed2.name).toEqual('github-actions[bot]')
|
||||
expect(parsed2.email).toEqual(
|
||||
'41898282+github-actions[bot]@users.noreply.github.com'
|
||||
)
|
||||
})
|
||||
|
||||
test('parseDisplayNameEmail fails to parse display name email formats', async () => {
|
||||
const displayNameEmail1 = 'abc@def.com'
|
||||
try {
|
||||
utils.parseDisplayNameEmail(displayNameEmail1)
|
||||
// Fail the test if an error wasn't thrown
|
||||
expect(true).toEqual(false)
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual(
|
||||
`The format of '${displayNameEmail1}' is not a valid email address with display name`
|
||||
)
|
||||
}
|
||||
|
||||
const displayNameEmail2 = ' < >'
|
||||
try {
|
||||
utils.parseDisplayNameEmail(displayNameEmail2)
|
||||
// Fail the test if an error wasn't thrown
|
||||
expect(true).toEqual(false)
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual(
|
||||
`The format of '${displayNameEmail2}' is not a valid email address with display name`
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
69
action.yml
69
action.yml
@ -1,9 +1,70 @@
|
||||
name: 'Create Pull Request'
|
||||
author: 'Peter Evans'
|
||||
description: 'Creates a pull request for changes to your repository in the actions workspace'
|
||||
inputs:
|
||||
token:
|
||||
description: 'GITHUB_TOKEN or a `repo` scoped Personal Access Token (PAT)'
|
||||
default: ${{ github.token }}
|
||||
path:
|
||||
description: >
|
||||
Relative path under $GITHUB_WORKSPACE to the repository.
|
||||
Defaults to $GITHUB_WORKSPACE.
|
||||
commit-message:
|
||||
description: 'The message to use when committing changes.'
|
||||
default: '[create-pull-request] automated change'
|
||||
committer:
|
||||
description: >
|
||||
The committer name and email address in the format `Display Name <email@address.com>`.
|
||||
Defaults to the GitHub Actions bot user.
|
||||
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 }}@users.noreply.github.com>'
|
||||
signoff:
|
||||
description: 'Add `Signed-off-by` line by the committer at the end of the commit log message.'
|
||||
default: false
|
||||
branch:
|
||||
description: 'The pull request branch name.'
|
||||
default: 'create-pull-request/patch'
|
||||
branch-suffix:
|
||||
description: 'The branch suffix type when using the alternative branching strategy.'
|
||||
base:
|
||||
description: >
|
||||
The pull request base branch.
|
||||
Defaults to the branch checked out in the workflow.
|
||||
push-to-fork:
|
||||
description: >
|
||||
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.
|
||||
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'
|
||||
labels:
|
||||
description: 'A comma or newline separated list of labels.'
|
||||
assignees:
|
||||
description: 'A comma or newline separated list of assignees (GitHub usernames).'
|
||||
reviewers:
|
||||
description: 'A comma or newline separated list of reviewers (GitHub usernames) to request a review from.'
|
||||
team-reviewers:
|
||||
description: >
|
||||
A comma or newline separated list of GitHub teams to request a review from.
|
||||
Note that a `repo` scoped Personal Access Token (PAT) may be required.
|
||||
milestone:
|
||||
description: 'The number of the milestone to associate the pull request with.'
|
||||
draft:
|
||||
description: 'Create a draft pull request'
|
||||
default: false
|
||||
outputs:
|
||||
pull-request-number:
|
||||
description: 'The pull request number'
|
||||
runs:
|
||||
using: 'docker'
|
||||
image: 'docker://peterevans/create-pull-request:1.5.1'
|
||||
using: 'node12'
|
||||
main: 'dist/index.js'
|
||||
branding:
|
||||
icon: 'git-pull-request'
|
||||
icon: 'git-pull-request'
|
||||
color: 'gray-dark'
|
||||
|
@ -1,221 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
''' Create Pull Request '''
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import sys
|
||||
import time
|
||||
from git import Repo
|
||||
from github import Github
|
||||
|
||||
|
||||
def get_github_event(github_event_path):
|
||||
with open(github_event_path) as f:
|
||||
github_event = json.load(f)
|
||||
if bool(os.environ.get('DEBUG_EVENT')):
|
||||
print(os.environ['GITHUB_EVENT_NAME'])
|
||||
print(json.dumps(github_event, sort_keys=True, indent=2))
|
||||
return github_event
|
||||
|
||||
|
||||
def ignore_event(event_name, event_data):
|
||||
if event_name == "push":
|
||||
# Ignore push events on deleted branches
|
||||
# The event we want to ignore occurs when a PR is created but the repository owner decides
|
||||
# not to commit the changes. They close the PR and delete the branch. This creates a
|
||||
# "push" event that we want to ignore, otherwise it will create another branch and PR on
|
||||
# the same commit.
|
||||
deleted = "{deleted}".format(**event_data)
|
||||
if deleted == "True":
|
||||
print("Ignoring delete branch event.")
|
||||
return True
|
||||
ref = "{ref}".format(**event_data)
|
||||
if not ref.startswith('refs/heads/'):
|
||||
print("Ignoring events for tags and remotes.")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_head_short_sha1(repo):
|
||||
return repo.git.rev_parse('--short', 'HEAD')
|
||||
|
||||
|
||||
def get_random_suffix(size=7, chars=string.ascii_lowercase + string.digits):
|
||||
return ''.join(random.choice(chars) for _ in range(size))
|
||||
|
||||
|
||||
def remote_branch_exists(repo, branch):
|
||||
for ref in repo.remotes.origin.refs:
|
||||
if ref.name == ("origin/%s" % branch):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_author_default(event_name, event_data):
|
||||
if event_name == "push":
|
||||
email = "{head_commit[author][email]}".format(**event_data)
|
||||
name = "{head_commit[author][name]}".format(**event_data)
|
||||
else:
|
||||
email = os.environ['GITHUB_ACTOR'] + '@users.noreply.github.com'
|
||||
name = os.environ['GITHUB_ACTOR']
|
||||
return email, name
|
||||
|
||||
|
||||
def set_git_config(git, email, name):
|
||||
git.config('--global', 'user.email', '"%s"' % email)
|
||||
git.config('--global', 'user.name', '"%s"' % name)
|
||||
|
||||
|
||||
def set_git_remote_url(git, token, github_repository):
|
||||
git.remote('set-url', 'origin', "https://x-access-token:%s@github.com/%s" % (token, github_repository))
|
||||
|
||||
|
||||
def checkout_branch(git, remote_exists, branch):
|
||||
if remote_exists:
|
||||
git.stash('--include-untracked')
|
||||
git.checkout(branch)
|
||||
try:
|
||||
git.stash('pop')
|
||||
except:
|
||||
git.checkout('--theirs', '.')
|
||||
git.reset()
|
||||
else:
|
||||
git.checkout('HEAD', b=branch)
|
||||
|
||||
|
||||
def push_changes(git, branch, commit_message):
|
||||
git.add('-A')
|
||||
git.commit(m=commit_message)
|
||||
return git.push('-f', '--set-upstream', 'origin', branch)
|
||||
|
||||
|
||||
def cs_string_to_list(str):
|
||||
# Split the comma separated string into a list
|
||||
l = [i.strip() for i in str.split(',')]
|
||||
# Remove empty strings
|
||||
return list(filter(None, l))
|
||||
|
||||
|
||||
def process_event(event_name, event_data, repo, branch, base, remote_exists):
|
||||
# Fetch required environment variables
|
||||
github_token = os.environ['GITHUB_TOKEN']
|
||||
github_repository = os.environ['GITHUB_REPOSITORY']
|
||||
# Fetch optional environment variables with default values
|
||||
commit_message = os.getenv(
|
||||
'COMMIT_MESSAGE',
|
||||
"Auto-committed changes by create-pull-request action")
|
||||
title = os.getenv(
|
||||
'PULL_REQUEST_TITLE',
|
||||
"Auto-generated by create-pull-request action")
|
||||
body = os.getenv(
|
||||
'PULL_REQUEST_BODY', "Auto-generated pull request by "
|
||||
"[create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub Action")
|
||||
# Fetch optional environment variables with no default values
|
||||
pull_request_labels = os.environ.get('PULL_REQUEST_LABELS')
|
||||
pull_request_assignees = os.environ.get('PULL_REQUEST_ASSIGNEES')
|
||||
pull_request_milestone = os.environ.get('PULL_REQUEST_MILESTONE')
|
||||
pull_request_reviewers = os.environ.get('PULL_REQUEST_REVIEWERS')
|
||||
pull_request_team_reviewers = os.environ.get('PULL_REQUEST_TEAM_REVIEWERS')
|
||||
|
||||
# Update URL for the 'origin' remote
|
||||
set_git_remote_url(repo.git, github_token, github_repository)
|
||||
|
||||
# Push the local changes to the remote branch
|
||||
print("Pushing changes.")
|
||||
push_result = push_changes(repo.git, branch, commit_message)
|
||||
print(push_result)
|
||||
|
||||
# If the remote existed then a PR likely exists and we can finish here
|
||||
if remote_exists:
|
||||
print("Updated pull request branch %s." % branch)
|
||||
sys.exit()
|
||||
|
||||
# Create the pull request
|
||||
print("Creating a request to pull %s into %s." % (branch, base))
|
||||
github_repo = Github(github_token).get_repo(github_repository)
|
||||
pull_request = github_repo.create_pull(
|
||||
title=title,
|
||||
body=body,
|
||||
base=base,
|
||||
head=branch)
|
||||
print("Created pull request %d." % pull_request.number)
|
||||
os.system('echo ::set-env name=PULL_REQUEST_NUMBER::%d' % pull_request.number)
|
||||
|
||||
# Set labels, assignees and milestone
|
||||
if pull_request_labels is not None:
|
||||
print("Applying labels")
|
||||
pull_request.as_issue().edit(labels=cs_string_to_list(pull_request_labels))
|
||||
if pull_request_assignees is not None:
|
||||
print("Applying assignees")
|
||||
pull_request.as_issue().edit(assignees=cs_string_to_list(pull_request_assignees))
|
||||
if pull_request_milestone is not None:
|
||||
print("Applying milestone")
|
||||
milestone = github_repo.get_milestone(int(pull_request_milestone))
|
||||
pull_request.as_issue().edit(milestone=milestone)
|
||||
|
||||
# Set pull request reviewers and team reviewers
|
||||
if pull_request_reviewers is not None:
|
||||
print("Requesting reviewers")
|
||||
pull_request.create_review_request(reviewers=cs_string_to_list(pull_request_reviewers))
|
||||
if pull_request_team_reviewers is not None:
|
||||
print("Requesting team reviewers")
|
||||
pull_request.create_review_request(team_reviewers=cs_string_to_list(pull_request_team_reviewers))
|
||||
|
||||
|
||||
# Get the JSON event data
|
||||
event_name = os.environ['GITHUB_EVENT_NAME']
|
||||
event_data = get_github_event(os.environ['GITHUB_EVENT_PATH'])
|
||||
# Check if this event should be ignored
|
||||
skip_ignore_event = bool(os.environ.get('SKIP_IGNORE'))
|
||||
if skip_ignore_event or not ignore_event(event_name, event_data):
|
||||
# Set the repo to the working directory
|
||||
repo = Repo(os.getcwd())
|
||||
|
||||
# Fetch/Set the branch name
|
||||
branch = os.getenv('PULL_REQUEST_BRANCH', 'create-pull-request/patch')
|
||||
# Set the current branch as the target base branch
|
||||
base = os.environ['GITHUB_REF'][11:]
|
||||
|
||||
# Skip if the current branch is a PR branch created by this action
|
||||
if base.startswith(branch):
|
||||
print("Branch '%s' was created by this action. Skipping." % base)
|
||||
sys.exit()
|
||||
|
||||
# Fetch an optional environment variable to determine the branch suffix
|
||||
branch_suffix = os.getenv('BRANCH_SUFFIX', 'short-commit-hash')
|
||||
if branch_suffix == "short-commit-hash":
|
||||
# Suffix with the short SHA1 hash
|
||||
branch = "%s-%s" % (branch, get_head_short_sha1(repo))
|
||||
elif branch_suffix == "timestamp":
|
||||
# Suffix with the current timestamp
|
||||
branch = "%s-%s" % (branch, int(time.time()))
|
||||
elif branch_suffix == "random":
|
||||
# Suffix with the current timestamp
|
||||
branch = "%s-%s" % (branch, get_random_suffix())
|
||||
|
||||
# Check if the remote branch exists
|
||||
remote_exists = remote_branch_exists(repo, branch)
|
||||
|
||||
# If using short commit hash prefixes, check if a remote
|
||||
# branch already exists for this HEAD commit
|
||||
if branch_suffix == 'short-commit-hash' and remote_exists:
|
||||
print("Pull request branch '%s' already exists for this commit. Skipping." % branch)
|
||||
sys.exit()
|
||||
|
||||
# Get the default for author email and name
|
||||
author_email, author_name = get_author_default(event_name, event_data)
|
||||
# Set commit author overrides
|
||||
author_email = os.getenv('COMMIT_AUTHOR_EMAIL', author_email)
|
||||
author_name = os.getenv('COMMIT_AUTHOR_NAME', author_name)
|
||||
# Set git configuration
|
||||
set_git_config(repo.git, author_email, author_name)
|
||||
# Checkout branch
|
||||
checkout_branch(repo.git, remote_exists, branch)
|
||||
|
||||
# Check if there are changes to pull request
|
||||
if repo.is_dirty() or len(repo.untracked_files) > 0:
|
||||
print("Repository has modified or untracked files.")
|
||||
process_event(event_name, event_data, repo, branch, base, remote_exists)
|
||||
else:
|
||||
print("Repository has no modified or untracked files. Skipping.")
|
11180
dist/index.js
vendored
Normal file
11180
dist/index.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
68
docs/assets/cpr-gitgraph.htm
Normal file
68
docs/assets/cpr-gitgraph.htm
Normal file
@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>create-pull-request GitHub action</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- partial:index.partial.html -->
|
||||
<div id="graph-container"></div>
|
||||
<!-- partial -->
|
||||
<script src='https://cdn.jsdelivr.net/npm/@gitgraph/js'></script>
|
||||
<script>
|
||||
const graphContainer = document.getElementById("graph-container");
|
||||
|
||||
const customTemplate = GitgraphJS.templateExtend(GitgraphJS.TemplateName.Metro, {
|
||||
commit: {
|
||||
message: {
|
||||
displayAuthor: false,
|
||||
displayHash: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Instantiate the graph.
|
||||
const gitgraph = GitgraphJS.createGitgraph(graphContainer, {
|
||||
template: customTemplate,
|
||||
orientation: "vertical-reverse"
|
||||
});
|
||||
|
||||
const master = gitgraph.branch("master");
|
||||
master.commit("Last commit on base");
|
||||
const localMaster = gitgraph.branch("<#1> master (local)");
|
||||
localMaster.commit({
|
||||
subject: "<uncommited changes>",
|
||||
body: "Changes made to the local base during the workflow",
|
||||
})
|
||||
const remotePatch = gitgraph.branch("create-pull-request/patch");
|
||||
remotePatch.merge({
|
||||
branch: localMaster,
|
||||
commitOptions: {
|
||||
subject: "[create-pull-request] automated change",
|
||||
body: "Changes pushed to create the remote branch",
|
||||
},
|
||||
});
|
||||
master.commit("New commit on base");
|
||||
|
||||
const localMaster2 = gitgraph.branch("<#2> master (local)");
|
||||
localMaster2.commit({
|
||||
subject: "<uncommited changes>",
|
||||
body: "Changes made to the updated local base during the workflow",
|
||||
})
|
||||
remotePatch.merge({
|
||||
branch: localMaster2,
|
||||
commitOptions: {
|
||||
subject: "[create-pull-request] automated change",
|
||||
body: "Changes force pushed to update the remote branch",
|
||||
},
|
||||
});
|
||||
|
||||
master.merge(remotePatch);
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
BIN
docs/assets/cpr-gitgraph.png
Normal file
BIN
docs/assets/cpr-gitgraph.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 207 KiB |
Before Width: | Height: | Size: 416 B After Width: | Height: | Size: 416 B |
BIN
docs/assets/pull-request-example.png
Normal file
BIN
docs/assets/pull-request-example.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 327 KiB |
364
docs/concepts-guidelines.md
Normal file
364
docs/concepts-guidelines.md
Normal file
@ -0,0 +1,364 @@
|
||||
# Concepts, guidelines and advanced usage
|
||||
|
||||
This document covers terminology, how the action works, general usage guidelines, and advanced usage.
|
||||
|
||||
- [Terminology](#terminology)
|
||||
- [Events and checkout](#events-and-checkout)
|
||||
- [How the action works](#how-the-action-works)
|
||||
- [Guidelines](#guidelines)
|
||||
- [Providing a consistent base](#providing-a-consistent-base)
|
||||
- [Pull request events](#pull-request-events)
|
||||
- [Restrictions on forked repositories](#restrictions-on-forked-repositories)
|
||||
- [Triggering further workflow runs](#triggering-further-workflow-runs)
|
||||
- [Security](#security)
|
||||
- [Advanced usage](#advanced-usage)
|
||||
- [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)
|
||||
- [Authenticating with GitHub App generated tokens](#authenticating-with-github-app-generated-tokens)
|
||||
- [Running in a container or on self-hosted runners](#running-in-a-container-or-on-self-hosted-runners)
|
||||
- [Creating pull requests on tag push](#creating-pull-requests-on-tag-push)
|
||||
|
||||
## Terminology
|
||||
|
||||
[Pull requests](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests#about-pull-requests) are proposed changes to a repository branch that can be reviewed by a repository's collaborators before being accepted or rejected.
|
||||
|
||||
A pull request references two branches:
|
||||
|
||||
- The `base` of a pull request is the branch you intend to change once the proposed changes are merged.
|
||||
- The `branch` of a pull request represents what you intend the `base` to look like when merged. It is the `base` branch *plus* changes that have been made to it.
|
||||
|
||||
## Events and checkout
|
||||
|
||||
For each [event type](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/events-that-trigger-workflows) there is a default `GITHUB_SHA` that will be checked out by the GitHub Actions [checkout](https://github.com/actions/checkout) action.
|
||||
|
||||
The majority of events will default to checking out the "last commit on default branch," which in most cases will be the latest commit on `master`.
|
||||
|
||||
The default can be overridden by specifying a `ref` on checkout.
|
||||
|
||||
```yml
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: develop
|
||||
```
|
||||
|
||||
## How the action works
|
||||
|
||||
By default, the action expects to be executed on the pull request `base`—the branch you intend to modify with the proposed changes.
|
||||
|
||||
Workflow steps:
|
||||
|
||||
1. Checkout the `base` branch
|
||||
2. Make changes
|
||||
3. Execute `create-pull-request` action
|
||||
|
||||
The following git diagram shows how the action creates and updates a pull request branch.
|
||||
|
||||

|
||||
|
||||
## Guidelines
|
||||
|
||||
### Providing a consistent base
|
||||
|
||||
For the action to work correctly it should be executed in a workflow that checks out a *consistent base* branch. This will be the base of the pull request unless overridden with the `base` input.
|
||||
|
||||
This means your workflow should be consistently checking out the branch that you intend to modify once the PR is merged.
|
||||
|
||||
In the following example, the [`push`](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/events-that-trigger-workflows#push-event-push) and [`create`](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/events-that-trigger-workflows#create-event-create) events both trigger the same workflow. This will cause the checkout action to checkout commits from inconsistent branches. Do *not* do this. It will cause multiple pull requests to be created for each additional `base` the action is executed against.
|
||||
|
||||
```yml
|
||||
on:
|
||||
push:
|
||||
create:
|
||||
jobs:
|
||||
example:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
```
|
||||
|
||||
Although rare, 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.
|
||||
|
||||
### Pull request events
|
||||
|
||||
Workflows triggered by `pull_request` events will by default check out a [merge commit](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/events-that-trigger-workflows#pull-request-event-pull_request). To prevent the merge commit being included in created pull requests it is necessary to checkout the `head_ref`.
|
||||
|
||||
```yml
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
```
|
||||
|
||||
### Restrictions on forked repositories
|
||||
|
||||
GitHub Actions have imposed restrictions on events triggered by a forked repository. Specifically, the `pull_request` event triggered by a fork opening a pull request in the upstream repository.
|
||||
|
||||
- Events from forks cannot access secrets, except for for the default `GITHUB_TOKEN`.
|
||||
> With the exception of GITHUB_TOKEN, secrets are not passed to the runner when a workflow is triggered from a forked repository.
|
||||
|
||||
[GitHub Actions: Using encrypted secrets in a workflow](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets#using-encrypted-secrets-in-a-workflow)
|
||||
|
||||
- The `GITHUB_TOKEN` has read-only access when an event is triggered by a forked repository.
|
||||
|
||||
[GitHub Actions: Permissions for the GITHUB_TOKEN](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/authenticating-with-the-github_token#permissions-for-the-github_token)
|
||||
|
||||
These restrictions mean that during a `pull_request` event triggered by a forked repository, actions have no write access to GitHub resources and will fail on attempt.
|
||||
|
||||
A job condition can be added to prevent workflows from executing when triggered by a repository fork.
|
||||
|
||||
```yml
|
||||
on: pull_request
|
||||
jobs:
|
||||
example:
|
||||
runs-on: ubuntu-latest
|
||||
# Check if the event is not triggered by a fork
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
```
|
||||
|
||||
### Triggering further workflow runs
|
||||
|
||||
Pull requests created by the action using the default `GITHUB_TOKEN` cannot trigger other workflows. If you have `on: pull_request` or `on: push` workflows acting as checks on pull requests, they will not run.
|
||||
|
||||
> When you use the repository's GITHUB_TOKEN to perform tasks on behalf of the GitHub Actions app, events triggered by the GITHUB_TOKEN will not create a new workflow run.
|
||||
|
||||
[GitHub Actions: Events that trigger workflows](https://help.github.com/en/actions/reference/events-that-trigger-workflows#triggering-new-workflows-using-a-personal-access-token)
|
||||
|
||||
#### Workarounds to trigger further workflow runs
|
||||
|
||||
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.
|
||||
- Use a `repo` scoped [Personal Access Token (PAT)](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line) 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://help.github.com/en/actions/reference/events-that-trigger-workflows#triggering-new-workflows-using-a-personal-access-token). 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://help.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 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
|
||||
|
||||
From a security perspective it's good practice to fork third-party actions, review the code, and use your fork of the action in workflows.
|
||||
By using third-party actions directly the risk exists that it could be modified to do something malicious, such as capturing secrets.
|
||||
|
||||
Alternatively, use the action directly and reference the commit hash for the version you want to target.
|
||||
```
|
||||
- uses: thirdparty/foo-action@172ec762f2ac8e050062398456fccd30444f8f30
|
||||
```
|
||||
|
||||
This action uses [ncc](https://github.com/zeit/ncc) to compile the Node.js code and dependencies into a single Javascript file under the [dist](https://github.com/peter-evans/create-pull-request/tree/master/dist) directory.
|
||||
|
||||
## Advanced usage
|
||||
|
||||
### 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, a `repo` scoped [Personal Access Token (PAT)](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line) is required.
|
||||
|
||||
```yml
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
repository: owner/repo
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- uses: peter-evans/create-pull-request@v3
|
||||
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 `repo` scoped [Personal Access Token (PAT)](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line).
|
||||
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.
|
||||
|
||||
How to use SSH (deploy keys) with create-pull-request action:
|
||||
|
||||
1. [Create a new SSH key pair](https://help.github.com/en/github/authenticating-to-github/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent#generating-a-new-ssh-key) for your repository. Do not set a passphrase.
|
||||
2. Copy the contents of the public key (.pub file) to a new repository [deploy key](https://developer.github.com/v3/guides/managing-deploy-keys/#deploy-keys) and check the box to "Allow write access."
|
||||
3. Add a secret to the repository containing the entire contents of the private key.
|
||||
4. As shown in the example below, configure `actions/checkout` to use the deploy key you have created.
|
||||
|
||||
```yml
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
```
|
||||
|
||||
### Push pull request branches to a fork
|
||||
|
||||
Instead of pushing pull request branches to the repository you want to update, you can push them to a fork of that repository.
|
||||
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://help.github.com/en/github/site-policy/github-terms-of-service#3-account-requirements).
|
||||
This user has no access to the main repository.
|
||||
It will use their own fork to push code and create the pull request.
|
||||
|
||||
1. Create a new GitHub user and login.
|
||||
2. Fork the repository that you will be creating pull requests in.
|
||||
3. Create a [Personal Access Token (PAT)](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line).
|
||||
4. Logout and log back in to 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@v2
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- uses: peter-evans/create-pull-request@v3
|
||||
with:
|
||||
token: ${{ secrets.MACHINE_USER_PAT }}
|
||||
push-to-fork: machine-user/fork-of-repository
|
||||
```
|
||||
|
||||
### Authenticating with GitHub App generated tokens
|
||||
|
||||
A GitHub App can be created for the sole purpose of generating tokens for use with GitHub actions.
|
||||
These tokens can be used in place of `GITHUB_TOKEN` or a [Personal Access Token (PAT)](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line).
|
||||
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://developer.github.com/apps/building-github-apps/creating-a-github-app/), setting the following fields:
|
||||
|
||||
- Set `GitHub App name`.
|
||||
- Set `Homepage URL` to anything you like, such as your GitHub profile page.
|
||||
- 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`.
|
||||
|
||||
2. Create a Private key from the App settings page and store it securely.
|
||||
|
||||
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 [tibdex/github-app-token](https://github.com/tibdex/github-app-token) to generate a token for use with this action.
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: tibdex/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@v3
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
```
|
||||
|
||||
### Running in a container or on self-hosted runners
|
||||
|
||||
This action can be run inside a container, or on [self-hosted runners](https://docs.github.com/en/actions/hosting-your-own-runners), by installing the necessary dependencies.
|
||||
|
||||
This action requires `git` to be installed and on the `PATH`. Note that `actions/checkout` requires Git 2.18 or higher to be installed, otherwise it will just download the source of the repository instead of cloning it.
|
||||
|
||||
The following examples of running in a container show the dependencies being installed during the workflow, but they could also be pre-installed in a custom image.
|
||||
|
||||
**Alpine container example:**
|
||||
```yml
|
||||
jobs:
|
||||
createPullRequestAlpine:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: alpine
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: apk --no-cache add git
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
```
|
||||
|
||||
**Ubuntu container example:**
|
||||
```yml
|
||||
jobs:
|
||||
createPullRequestAlpine:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ubuntu
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y software-properties-common
|
||||
add-apt-repository -y ppa:git-core/ppa
|
||||
apt-get install -y git
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
```
|
||||
|
||||
### Creating pull requests on tag push
|
||||
|
||||
An `on: push` workflow will also trigger when tags are pushed.
|
||||
During these events, the `actions/checkout` action will check out the `ref/tags/<tag>` git ref by default.
|
||||
This means the repository will *not* be checked out on an active branch.
|
||||
|
||||
If you would like to run `create-pull-request` action on the tagged commit you can achieve this by creating a temporary branch as follows.
|
||||
|
||||
```yml
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
jobs:
|
||||
example:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Create a temporary tag branch
|
||||
run: |
|
||||
git config --global user.name 'GitHub'
|
||||
git config --global user.email 'noreply@github.com'
|
||||
git checkout -b temp-${GITHUB_REF:10}
|
||||
git push --set-upstream origin temp-${GITHUB_REF:10}
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
with:
|
||||
base: master
|
||||
|
||||
- name: Delete tag branch
|
||||
run: |
|
||||
git push --delete origin temp-${GITHUB_REF:10}
|
||||
```
|
||||
|
||||
This is an alternative, simpler workflow to the one above. However, this is not guaranteed to checkout the tagged commit.
|
||||
There is a chance that in between the tag being pushed and checking out the `master` branch in the workflow, another commit is made to `master`. If that possibility is not a concern, this workflow will work fine.
|
||||
|
||||
```yml
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
jobs:
|
||||
example:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: master
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
```
|
503
docs/examples.md
Normal file
503
docs/examples.md
Normal file
@ -0,0 +1,503 @@
|
||||
# Examples
|
||||
|
||||
- [Use case: Create a pull request to update X on push](#use-case-create-a-pull-request-to-update-x-on-push)
|
||||
- [Update project authors](#update-project-authors)
|
||||
- [Keep a branch up-to-date with another](#keep-a-branch-up-to-date-with-another)
|
||||
- [Use case: Create a pull request to update X periodically](#use-case-create-a-pull-request-to-update-x-periodically)
|
||||
- [Update NPM dependencies](#update-npm-dependencies)
|
||||
- [Update Gradle dependencies](#update-gradle-dependencies)
|
||||
- [Update SwaggerUI for GitHub Pages](#update-swaggerui-for-github-pages)
|
||||
- [Spider and download a website](#spider-and-download-a-website)
|
||||
- [Use case: Create a pull request to update X by calling the GitHub API](#use-case-create-a-pull-request-to-update-x-by-calling-the-github-api)
|
||||
- [Call the GitHub API from an external service](#call-the-github-api-from-an-external-service)
|
||||
- [Call the GitHub API from another GitHub Actions workflow](#call-the-github-api-from-another-github-actions-workflow)
|
||||
- [Use case: Create a pull request to modify/fix pull requests](#use-case-create-a-pull-request-to-modifyfix-pull-requests)
|
||||
- [autopep8](#autopep8)
|
||||
- [Misc workflow tips](#misc-workflow-tips)
|
||||
- [Filtering push events](#filtering-push-events)
|
||||
- [Dynamic configuration using variables](#dynamic-configuration-using-variables)
|
||||
- [Debugging GitHub Actions](#debugging-github-actions)
|
||||
|
||||
|
||||
## Use case: Create a pull request to update X on push
|
||||
|
||||
This pattern will work well for updating any kind of static content based on pushed changes. Care should be taken when using this pattern in repositories with a high frequency of commits.
|
||||
|
||||
### Update project authors
|
||||
|
||||
Raises a pull request to update a file called `AUTHORS` with the git user names and email addresses of contributors.
|
||||
|
||||
```yml
|
||||
name: Update AUTHORS
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
updateAuthors:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
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@v3
|
||||
with:
|
||||
commit-message: update authors
|
||||
title: Update AUTHORS
|
||||
body: Credit new contributors by updating AUTHORS
|
||||
branch: update-authors
|
||||
```
|
||||
|
||||
### Keep a branch up-to-date with another
|
||||
|
||||
This is a use case where a branch should be kept up to date with another by opening a pull request to update it. The pull request should then be updated with new changes until it is merged or closed.
|
||||
|
||||
In this example scenario, a branch called `production` should be updated via pull request to keep it in sync with `master`. Merging the pull request is effectively promoting those changes to production.
|
||||
|
||||
```yml
|
||||
name: Create production promotion pull request
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
productionPromotion:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: production
|
||||
- name: Reset promotion branch
|
||||
run: |
|
||||
git fetch origin master:master
|
||||
git reset --hard master
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
with:
|
||||
branch: production-promotion
|
||||
```
|
||||
|
||||
## Use case: Create a pull request to update X periodically
|
||||
|
||||
This pattern will work well for updating any kind of static content from an external source. The workflow executes on a schedule and raises a pull request when there are changes.
|
||||
|
||||
### Update NPM dependencies
|
||||
|
||||
This workflow will create a pull request for npm dependencies.
|
||||
It works best in combination with a build workflow triggered on `push` and `pull_request`.
|
||||
A [Personal Access Token (PAT)](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line) can be used in order for the creation of the pull request to trigger further workflows. See the [documentation here](https://github.com/peter-evans/create-pull-request/blob/master/docs/concepts-guidelines.md#triggering-further-workflow-runs) for further details.
|
||||
|
||||
```yml
|
||||
name: Update Dependencies
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 10 * * 1'
|
||||
jobs:
|
||||
update-dep:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12.x'
|
||||
- name: Update dependencies
|
||||
run: |
|
||||
npx -p npm-check-updates ncu -u
|
||||
npm install
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
commit-message: Update dependencies
|
||||
title: Update dependencies
|
||||
body: |
|
||||
- Dependency updates
|
||||
|
||||
Auto-generated by [create-pull-request][1]
|
||||
|
||||
[1]: https://github.com/peter-evans/create-pull-request
|
||||
branch: update-dependencies
|
||||
```
|
||||
|
||||
The above workflow works best in combination with a build workflow triggered on `push` and `pull_request`.
|
||||
|
||||
```yml
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- run: npm ci
|
||||
- run: npm run test
|
||||
- run: npm run build
|
||||
```
|
||||
|
||||
### Update Gradle dependencies
|
||||
|
||||
The following workflow will create a pull request for Gradle dependencies.
|
||||
It requires first configuring your project to use Gradle lockfiles.
|
||||
See [here](https://github.com/peter-evans/gradle-auto-dependency-updates) for how to configure your project and use the following workflow.
|
||||
|
||||
```yml
|
||||
name: Update Dependencies
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 1 * * 1'
|
||||
jobs:
|
||||
update-dep:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
- name: Perform dependency resolution and write new lockfiles
|
||||
run: ./gradlew dependencies --write-locks
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
commit-message: Update dependencies
|
||||
title: Update dependencies
|
||||
body: |
|
||||
- Dependency updates
|
||||
|
||||
Auto-generated by [create-pull-request][1]
|
||||
|
||||
[1]: https://github.com/peter-evans/create-pull-request
|
||||
branch: update-dependencies
|
||||
```
|
||||
|
||||
### Update SwaggerUI for GitHub Pages
|
||||
|
||||
When using [GitHub Pages to host Swagger documentation](https://github.com/peter-evans/swagger-github-pages), this workflow updates the repository with the latest distribution of [SwaggerUI](https://github.com/swagger-api/swagger-ui).
|
||||
|
||||
You must create a file called `swagger-ui.version` at the root of your repository before running.
|
||||
```yml
|
||||
name: Update Swagger UI
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 10 * * *'
|
||||
jobs:
|
||||
updateSwagger:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Get Latest Swagger UI Release
|
||||
id: swagger-ui
|
||||
run: |
|
||||
echo ::set-output name=release_tag::$(curl -sL https://api.github.com/repos/swagger-api/swagger-ui/releases/latest | jq -r ".tag_name")
|
||||
echo ::set-output name=current_tag::$(<swagger-ui.version)
|
||||
- name: Update Swagger UI
|
||||
if: steps.swagger-ui.outputs.current_tag != steps.swagger-ui.outputs.release_tag
|
||||
env:
|
||||
RELEASE_TAG: ${{ steps.swagger-ui.outputs.release_tag }}
|
||||
SWAGGER_YAML: "swagger.yaml"
|
||||
run: |
|
||||
# Delete the dist directory and index.html
|
||||
rm -fr dist index.html
|
||||
# Download the release
|
||||
curl -sL -o $RELEASE_TAG https://api.github.com/repos/swagger-api/swagger-ui/tarball/$RELEASE_TAG
|
||||
# Extract the dist directory
|
||||
tar -xzf $RELEASE_TAG --strip-components=1 $(tar -tzf $RELEASE_TAG | head -1 | cut -f1 -d"/")/dist
|
||||
rm $RELEASE_TAG
|
||||
# Move index.html to the root
|
||||
mv dist/index.html .
|
||||
# Fix references in index.html
|
||||
sed -i "s|https://petstore.swagger.io/v2/swagger.json|$SWAGGER_YAML|g" index.html
|
||||
sed -i "s|href=\"./|href=\"dist/|g" index.html
|
||||
sed -i "s|src=\"./|src=\"dist/|g" index.html
|
||||
# Update current release
|
||||
echo ${{ steps.swagger-ui.outputs.release_tag }} > swagger-ui.version
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
with:
|
||||
commit-message: Update swagger-ui to ${{ steps.swagger-ui.outputs.release_tag }}
|
||||
title: Update SwaggerUI to ${{ steps.swagger-ui.outputs.release_tag }}
|
||||
body: |
|
||||
Updates [swagger-ui][1] to ${{ steps.swagger-ui.outputs.release_tag }}
|
||||
|
||||
Auto-generated by [create-pull-request][2]
|
||||
|
||||
[1]: https://github.com/swagger-api/swagger-ui
|
||||
[2]: https://github.com/peter-evans/create-pull-request
|
||||
labels: dependencies, automated pr
|
||||
branch: swagger-ui-updates
|
||||
```
|
||||
|
||||
### Spider and download a website
|
||||
|
||||
This workflow spiders a website and downloads the content. Any changes to the website will be raised in a pull request.
|
||||
|
||||
```yml
|
||||
name: Download Website
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 10 * * *'
|
||||
jobs:
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Download website
|
||||
run: |
|
||||
wget \
|
||||
--recursive \
|
||||
--level=2 \
|
||||
--wait=1 \
|
||||
--no-clobber \
|
||||
--page-requisites \
|
||||
--html-extension \
|
||||
--convert-links \
|
||||
--domains quotes.toscrape.com \
|
||||
http://quotes.toscrape.com/
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
with:
|
||||
commit-message: update local website copy
|
||||
title: Automated Updates to Local Website Copy
|
||||
body: This is an auto-generated PR with website updates.
|
||||
branch: website-updates
|
||||
```
|
||||
|
||||
## Use case: Create a pull request to update X by calling the GitHub API
|
||||
|
||||
You can use the GitHub API to trigger a webhook event called [`repository_dispatch`](https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows#external-events-repository_dispatch) when you want to trigger a workflow for activity that happens outside of GitHub.
|
||||
This pattern will work well for updating any kind of static content from an external source.
|
||||
|
||||
You can modify any of the examples in the previous section to work in this fashion.
|
||||
|
||||
Set the workflow to execute `on: repository_dispatch`.
|
||||
|
||||
```yml
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [create-pull-request]
|
||||
```
|
||||
|
||||
### Call the GitHub API from an external service
|
||||
|
||||
An `on: repository_dispatch` workflow can be triggered by a call to the GitHub API as follows.
|
||||
|
||||
- `[username]` is a GitHub username
|
||||
- `[token]` is a `repo` scoped [Personal Access Token](https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line)
|
||||
- `[repository]` is the name of the repository the workflow resides in.
|
||||
|
||||
```
|
||||
curl -XPOST -u "[username]:[token]" \
|
||||
-H "Accept: application/vnd.github.everest-preview+json" \
|
||||
-H "Content-Type: application/json" \
|
||||
https://api.github.com/repos/[username]/[repository]/dispatches \
|
||||
--data '{"event_type": "create-pull-request"}'
|
||||
```
|
||||
|
||||
### Call the GitHub API from another GitHub Actions workflow
|
||||
|
||||
An `on: repository_dispatch` workflow can be triggered from another workflow with [repository-dispatch](https://github.com/peter-evans/repository-dispatch) action.
|
||||
|
||||
```yml
|
||||
- name: Repository Dispatch
|
||||
uses: peter-evans/repository-dispatch@v1
|
||||
with:
|
||||
token: ${{ secrets.REPO_ACCESS_TOKEN }}
|
||||
repository: username/my-repo
|
||||
event-type: create-pull-request
|
||||
client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}"}'
|
||||
```
|
||||
|
||||
## Use case: Create a pull request to modify/fix pull requests
|
||||
|
||||
**Note**: While the following approach does work, my strong recommendation would be to use a slash command style "ChatOps" solution for operations on pull requests. See [slash-command-dispatch](https://github.com/peter-evans/slash-command-dispatch) for such a solution.
|
||||
|
||||
This is a pattern that lends itself to automated code linting and fixing. A pull request can be created to fix or modify something during an `on: pull_request` workflow. The pull request containing the fix will be raised with the original pull request as the base. This can be then be merged to update the original pull request and pass any required tests.
|
||||
|
||||
Note that due to [limitations on forked repositories](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/authenticating-with-the-github_token#permissions-for-the-github_token) workflows for this use case do not work for pull requests raised from forks.
|
||||
|
||||
### autopep8
|
||||
|
||||
The following is an example workflow for a use case where [autopep8 action](https://github.com/peter-evans/autopep8) runs as both a check on pull requests and raises a further pull request to apply code fixes.
|
||||
|
||||
How it works:
|
||||
|
||||
1. When a pull request is raised the workflow executes as a check
|
||||
2. If autopep8 makes any fixes a pull request will be raised for those fixes to be merged into the current pull request branch. The workflow then deliberately causes the check to fail.
|
||||
3. When the pull request containing the fixes is merged the workflow runs again. This time autopep8 makes no changes and the check passes.
|
||||
4. The original pull request can now be merged.
|
||||
|
||||
```yml
|
||||
name: autopep8
|
||||
on: pull_request
|
||||
jobs:
|
||||
autopep8:
|
||||
# Check if the PR is not raised by this workflow and is not from a fork
|
||||
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@v2
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
- name: autopep8
|
||||
id: autopep8
|
||||
uses: peter-evans/autopep8@v1
|
||||
with:
|
||||
args: --exit-code --recursive --in-place --aggressive --aggressive .
|
||||
- name: Set autopep8 branch name
|
||||
id: vars
|
||||
run: echo ::set-output name=branch-name::"autopep8-patches/${{ github.head_ref }}"
|
||||
- name: Create Pull Request
|
||||
if: steps.autopep8.outputs.exit-code == 2
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
with:
|
||||
commit-message: autopep8 action fixes
|
||||
title: Fixes by autopep8 action
|
||||
body: This is an auto-generated PR with fixes by autopep8.
|
||||
labels: autopep8, automated pr
|
||||
branch: ${{ steps.vars.outputs.branch-name }}
|
||||
- name: Fail if autopep8 made changes
|
||||
if: steps.autopep8.outputs.exit-code == 2
|
||||
run: exit 1
|
||||
```
|
||||
|
||||
## Misc workflow tips
|
||||
|
||||
### Filtering push events
|
||||
|
||||
For workflows using `on: push` you may want to ignore push events for tags and only execute for branches. Specifying `branches` causes only events on branches to trigger the workflow. The `'**'` wildcard will match any branch name.
|
||||
|
||||
```yml
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
```
|
||||
|
||||
If you have a workflow that contains jobs to handle push events on branches as well as tags, you can make sure that the job where you use `create-pull-request` action only executes when `github.ref` is a branch by using an `if` condition as follows.
|
||||
|
||||
```yml
|
||||
on: push
|
||||
jobs:
|
||||
createPullRequest:
|
||||
if: startsWith(github.ref, 'refs/heads/')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
...
|
||||
|
||||
someOtherJob:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
...
|
||||
```
|
||||
|
||||
### Dynamic configuration using variables
|
||||
|
||||
The following examples show how configuration for the action can be dynamically defined in a previous workflow step.
|
||||
|
||||
The recommended method is to use [`set-output`](https://help.github.com/en/github/automating-your-workflow-with-github-actions/development-tools-for-github-actions#set-an-output-parameter-set-output). Note that the step where output variables are defined must have an id.
|
||||
|
||||
```yml
|
||||
- name: Set output variables
|
||||
id: vars
|
||||
run: |
|
||||
echo ::set-output name=pr_title::"[Test] Add report file $(date +%d-%m-%Y)"
|
||||
echo ::set-output name=pr_body::"This PR was auto-generated on $(date +%d-%m-%Y) \
|
||||
by [create-pull-request](https://github.com/peter-evans/create-pull-request)."
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
with:
|
||||
title: ${{ steps.vars.outputs.pr_title }}
|
||||
body: ${{ steps.vars.outputs.pr_body }}
|
||||
```
|
||||
|
||||
Alternatively, [`set-env`](https://help.github.com/en/github/automating-your-workflow-with-github-actions/development-tools-for-github-actions#set-an-environment-variable-set-env) can be used to create environment variables.
|
||||
|
||||
```yml
|
||||
- name: Set environment variables
|
||||
run: |
|
||||
echo ::set-env name=PULL_REQUEST_TITLE::"[Test] Add report file $(date +%d-%m-%Y)"
|
||||
echo ::set-env name=PULL_REQUEST_BODY::"This PR was auto-generated on $(date +%d-%m-%Y) \
|
||||
by [create-pull-request](https://github.com/peter-evans/create-pull-request)."
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
with:
|
||||
title: ${{ env.PULL_REQUEST_TITLE }}
|
||||
body: ${{ env.PULL_REQUEST_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.
|
||||
The content must be [escaped to preserve newlines](https://github.community/t/set-output-truncates-multiline-strings/16852/3).
|
||||
|
||||
```yml
|
||||
- id: get-pr-body
|
||||
run: |
|
||||
body=$(cat pr-body.txt)
|
||||
body="${body//'%'/'%25'}"
|
||||
body="${body//$'\n'/'%0A'}"
|
||||
body="${body//$'\r'/'%0D'}"
|
||||
echo ::set-output name=body::$body
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
with:
|
||||
body: ${{ steps.get-pr-body.outputs.body }}
|
||||
```
|
||||
|
||||
### Debugging GitHub Actions
|
||||
|
||||
#### Runner Diagnostic Logging
|
||||
|
||||
[Runner diagnostic logging](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/managing-a-workflow-run#enabling-runner-diagnostic-logging) provides additional log files that contain information about how a runner is executing an action.
|
||||
To enable runner diagnostic logging, set the secret `ACTIONS_RUNNER_DEBUG` to `true` in the repository that contains the workflow.
|
||||
|
||||
#### Step Debug Logging
|
||||
|
||||
[Step debug logging](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/managing-a-workflow-run#enabling-step-debug-logging) increases the verbosity of a job's logs during and after a job's execution.
|
||||
To enable step debug logging set the secret `ACTIONS_STEP_DEBUG` to `true` in the repository that contains the workflow.
|
||||
|
||||
#### Output Various Contexts
|
||||
|
||||
```yml
|
||||
steps:
|
||||
- name: Dump GitHub context
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- name: Dump job context
|
||||
env:
|
||||
JOB_CONTEXT: ${{ toJson(job) }}
|
||||
run: echo "$JOB_CONTEXT"
|
||||
- name: Dump steps context
|
||||
env:
|
||||
STEPS_CONTEXT: ${{ toJson(steps) }}
|
||||
run: echo "$STEPS_CONTEXT"
|
||||
- name: Dump runner context
|
||||
env:
|
||||
RUNNER_CONTEXT: ${{ toJson(runner) }}
|
||||
run: echo "$RUNNER_CONTEXT"
|
||||
- name: Dump strategy context
|
||||
env:
|
||||
STRATEGY_CONTEXT: ${{ toJson(strategy) }}
|
||||
run: echo "$STRATEGY_CONTEXT"
|
||||
- name: Dump matrix context
|
||||
env:
|
||||
MATRIX_CONTEXT: ${{ toJson(matrix) }}
|
||||
run: echo "$MATRIX_CONTEXT"
|
||||
```
|
73
docs/updating.md
Normal file
73
docs/updating.md
Normal file
@ -0,0 +1,73 @@
|
||||
## Updating from `v2` to `v3`
|
||||
|
||||
### 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>`.
|
||||
|
||||
To continue to use the `v2` default, set the `author` input as follows.
|
||||
```yaml
|
||||
- uses: peter-evans/create-pull-request@v3
|
||||
with:
|
||||
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
||||
```
|
||||
|
||||
- The `author` and `committer` inputs are no longer cross-used if only one is supplied. Additionally, when neither input is set, the `author` and `committer` are no longer determined from an existing identity set in git config. In both cases, the inputs will fall back to their default set in [action.yml](../action.yml).
|
||||
|
||||
- Deprecated inputs `project` and `project-column` have been removed in favour of an additional action step. See [Create a project card](https://github.com/peter-evans/create-pull-request#create-a-project-card) for details.
|
||||
|
||||
- Deprecated output `pr_number` has been removed in favour of `pull-request-number`.
|
||||
|
||||
- Input `request-to-parent` has been removed in favour of `push-to-fork`. This greatly simplifies pushing the pull request branch to a fork of the parent repository. See [Push pull request branches to a fork](concepts-guidelines.md#push-pull-request-branches-to-a-fork) for details.
|
||||
|
||||
e.g.
|
||||
```yaml
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- uses: peter-evans/create-pull-request@v3
|
||||
with:
|
||||
token: ${{ secrets.MACHINE_USER_PAT }}
|
||||
push-to-fork: machine-user/fork-of-repository
|
||||
```
|
||||
|
||||
### New features
|
||||
|
||||
- The action has been converted to Typescript giving it a significant performance improvement.
|
||||
|
||||
- If you run this action in a container, or on [self-hosted runners](https://docs.github.com/en/actions/hosting-your-own-runners), `python` and `pip` are no longer required dependencies. See [Running in a container or on self-hosted runners](concepts-guidelines.md#running-in-a-container-or-on-self-hosted-runners) for details.
|
||||
|
||||
- Inputs `labels`, `assignees`, `reviewers` and `team-reviewers` can now be newline separated, or comma separated.
|
||||
e.g.
|
||||
```yml
|
||||
labels: |
|
||||
chore
|
||||
dependencies
|
||||
automated
|
||||
```
|
||||
|
||||
## Updating from `v1` to `v2`
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- `v2` now expects repositories to be checked out with `actions/checkout@v2`
|
||||
|
||||
To use `actions/checkout@v1` the following step to checkout the branch is necessary.
|
||||
```yml
|
||||
- uses: actions/checkout@v1
|
||||
- name: Checkout branch
|
||||
run: git checkout "${GITHUB_REF:11}"
|
||||
```
|
||||
|
||||
- The two branch naming strategies have been swapped. Fixed branch naming strategy is now the default. i.e. `branch-suffix: none` is now the default and should be removed from configuration if set.
|
||||
|
||||
- `author-name`, `author-email`, `committer-name`, `committer-email` have been removed in favour of `author` and `committer`.
|
||||
They can both be set in the format `Display Name <email@address.com>`
|
||||
|
||||
If neither `author` or `committer` are set the action will default to making commits as the GitHub Actions bot user.
|
||||
|
||||
### New features
|
||||
|
||||
- Unpushed commits made during the workflow before the action runs will now be considered as changes to be raised in the pull request. See [Controlling commits](https://github.com/peter-evans/create-pull-request#controlling-commits) for details.
|
||||
- New commits made to the pull request base will now be taken into account when pull requests are updated.
|
||||
- If an updated pull request no longer differs from its base it will automatically be closed and the pull request branch deleted.
|
11
jest.config.js
Normal file
11
jest.config.js
Normal file
@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
clearMocks: true,
|
||||
moduleFileExtensions: ['js', 'ts'],
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/*.test.ts'],
|
||||
testRunner: 'jest-circus/runner',
|
||||
transform: {
|
||||
'^.+\\.ts$': 'ts-jest'
|
||||
},
|
||||
verbose: true
|
||||
}
|
8195
package-lock.json
generated
Normal file
8195
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
54
package.json
Normal file
54
package.json
Normal file
@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "create-pull-request",
|
||||
"version": "3.0.0",
|
||||
"private": true,
|
||||
"description": "Creates a pull request for changes to your repository in the actions workspace",
|
||||
"main": "lib/main.js",
|
||||
"scripts": {
|
||||
"build": "tsc && ncc build",
|
||||
"format": "prettier --write '**/*.ts'",
|
||||
"format-check": "prettier --check '**/*.ts'",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"test:unit": "jest unit",
|
||||
"test:int": "__test__/integration-tests.sh",
|
||||
"test": "npm run test:unit && npm run test:int"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/peter-evans/create-pull-request.git"
|
||||
},
|
||||
"keywords": [
|
||||
"actions",
|
||||
"pull",
|
||||
"request"
|
||||
],
|
||||
"author": "Peter Evans",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/peter-evans/create-pull-request/issues"
|
||||
},
|
||||
"homepage": "https://github.com/peter-evans/create-pull-request",
|
||||
"dependencies": {
|
||||
"@actions/core": "1.2.4",
|
||||
"@actions/exec": "1.0.4",
|
||||
"@octokit/core": "3.1.0",
|
||||
"@octokit/plugin-paginate-rest": "2.2.3",
|
||||
"@octokit/plugin-rest-endpoint-methods": "4.0.0",
|
||||
"uuid": "8.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "25.2.2",
|
||||
"@types/node": "14.0.1",
|
||||
"@typescript-eslint/parser": "2.33.0",
|
||||
"@zeit/ncc": "0.22.1",
|
||||
"eslint": "7.0.0",
|
||||
"eslint-plugin-github": "3.4.1",
|
||||
"eslint-plugin-jest": "23.11.0",
|
||||
"jest": "26.0.1",
|
||||
"jest-circus": "26.0.1",
|
||||
"js-yaml": "3.13.1",
|
||||
"prettier": "2.0.5",
|
||||
"ts-jest": "25.5.1",
|
||||
"typescript": "3.9.2"
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 201 KiB |
@ -1,5 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base"
|
||||
]
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
GitPython==3.0.3
|
||||
PyGithub==1.43.8
|
200
src/create-or-update-branch.ts
Normal file
200
src/create-or-update-branch.ts
Normal file
@ -0,0 +1,200 @@
|
||||
import * as core from '@actions/core'
|
||||
import {GitCommandManager} from './git-command-manager'
|
||||
import {v4 as uuidv4} from 'uuid'
|
||||
|
||||
const CHERRYPICK_EMPTY =
|
||||
'The previous cherry-pick is now empty, possibly due to conflict resolution.'
|
||||
|
||||
export async function tryFetch(
|
||||
git: GitCommandManager,
|
||||
remote: string,
|
||||
branch: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await git.fetch([`${branch}:refs/remotes/${remote}/${branch}`], remote)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Return true if branch2 is ahead of branch1
|
||||
async function isAhead(
|
||||
git: GitCommandManager,
|
||||
branch1: string,
|
||||
branch2: string
|
||||
): Promise<boolean> {
|
||||
const result = await git.revList(
|
||||
[`${branch1}...${branch2}`],
|
||||
['--right-only', '--count']
|
||||
)
|
||||
return Number(result) > 0
|
||||
}
|
||||
|
||||
// Return true if branch2 is behind branch1
|
||||
async function isBehind(
|
||||
git: GitCommandManager,
|
||||
branch1: string,
|
||||
branch2: string
|
||||
): Promise<boolean> {
|
||||
const result = await git.revList(
|
||||
[`${branch1}...${branch2}`],
|
||||
['--left-only', '--count']
|
||||
)
|
||||
return Number(result) > 0
|
||||
}
|
||||
|
||||
// Return true if branch2 is even with branch1
|
||||
async function isEven(
|
||||
git: GitCommandManager,
|
||||
branch1: string,
|
||||
branch2: string
|
||||
): Promise<boolean> {
|
||||
return (
|
||||
!(await isAhead(git, branch1, branch2)) &&
|
||||
!(await isBehind(git, branch1, branch2))
|
||||
)
|
||||
}
|
||||
|
||||
async function hasDiff(
|
||||
git: GitCommandManager,
|
||||
branch1: string,
|
||||
branch2: string
|
||||
): Promise<boolean> {
|
||||
const result = await git.diff([`${branch1}..${branch2}`])
|
||||
return result.length > 0
|
||||
}
|
||||
|
||||
function splitLines(multilineString: string): string[] {
|
||||
return multilineString
|
||||
.split('\n')
|
||||
.map(s => s.trim())
|
||||
.filter(x => x !== '')
|
||||
}
|
||||
|
||||
export async function createOrUpdateBranch(
|
||||
git: GitCommandManager,
|
||||
commitMessage: string,
|
||||
base: string,
|
||||
branch: string,
|
||||
branchRemoteName: string,
|
||||
signoff: boolean
|
||||
): Promise<CreateOrUpdateBranchResult> {
|
||||
// Get the working base. This may or may not be the actual base.
|
||||
const workingBase = await git.symbolicRef('HEAD', ['--short'])
|
||||
// If the base is not specified it is assumed to be the working base.
|
||||
base = base ? base : workingBase
|
||||
const baseRemote = 'origin'
|
||||
|
||||
// Set the default return values
|
||||
const result: CreateOrUpdateBranchResult = {
|
||||
action: 'none',
|
||||
base: base,
|
||||
hasDiffWithBase: false
|
||||
}
|
||||
|
||||
// Save the working base changes to a temporary branch
|
||||
const tempBranch = uuidv4()
|
||||
await git.checkout(tempBranch, 'HEAD')
|
||||
// Commit any uncomitted changes
|
||||
if (await git.isDirty(true)) {
|
||||
core.info('Uncommitted changes found. Adding a commit.')
|
||||
await git.exec(['add', '-A'])
|
||||
const params = ['-m', commitMessage]
|
||||
if (signoff) {
|
||||
params.push('--signoff')
|
||||
}
|
||||
await git.commit(params)
|
||||
}
|
||||
|
||||
// Perform fetch and reset the working base
|
||||
// Commits made during the workflow will be removed
|
||||
await git.fetch([`${workingBase}:${workingBase}`], baseRemote, ['--force'])
|
||||
|
||||
// If the working base is not the base, rebase the temp branch commits
|
||||
if (workingBase != base) {
|
||||
core.info(
|
||||
`Rebasing commits made to branch '${workingBase}' on to base branch '${base}'`
|
||||
)
|
||||
// Checkout the actual base
|
||||
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(
|
||||
[`${workingBase}..${tempBranch}`, '.'],
|
||||
['--reverse']
|
||||
)
|
||||
for (const commit of splitLines(commits)) {
|
||||
const result = await git.cherryPick(
|
||||
['--strategy=recursive', '--strategy-option=theirs', commit],
|
||||
true
|
||||
)
|
||||
if (result.exitCode != 0 && !result.stderr.includes(CHERRYPICK_EMPTY)) {
|
||||
throw new Error(`Unexpected error: ${result.stderr}`)
|
||||
}
|
||||
}
|
||||
// Reset the temp branch to the working index
|
||||
await git.checkout(tempBranch, 'HEAD')
|
||||
// Reset the base
|
||||
await git.fetch([`${base}:${base}`], baseRemote, ['--force'])
|
||||
}
|
||||
|
||||
// Try to fetch the pull request branch
|
||||
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, 'HEAD')
|
||||
// Check if the pull request branch is ahead of the base
|
||||
result.hasDiffWithBase = await isAhead(git, base, branch)
|
||||
if (result.hasDiffWithBase) {
|
||||
result.action = 'created'
|
||||
core.info(`Created branch '${branch}'`)
|
||||
} else {
|
||||
core.info(
|
||||
`Branch '${branch}' is not ahead of base '${base}' and will not be created`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// The pull request branch exists
|
||||
core.info(
|
||||
`Pull request branch '${branch}' already exists as remote branch '${branchRemoteName}/${branch}'`
|
||||
)
|
||||
// Checkout the pull request branch
|
||||
await git.checkout(branch)
|
||||
|
||||
if (await hasDiff(git, branch, tempBranch)) {
|
||||
// If the branch differs from the recreated temp version then the branch is reset
|
||||
// For changes on base this action is similar to a rebase of the pull request branch
|
||||
core.info(`Resetting '${branch}'`)
|
||||
// Alternatively, git switch -C branch tempBranch
|
||||
await git.checkout(branch, tempBranch)
|
||||
}
|
||||
|
||||
// Check if the pull request branch has been updated
|
||||
// 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))) {
|
||||
result.action = 'updated'
|
||||
core.info(`Updated branch '${branch}'`)
|
||||
} else {
|
||||
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
|
||||
result.hasDiffWithBase = await isAhead(git, base, branch)
|
||||
}
|
||||
|
||||
// Delete the temporary branch
|
||||
await git.exec(['branch', '--delete', '--force', tempBranch])
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
interface CreateOrUpdateBranchResult {
|
||||
action: string
|
||||
base: string
|
||||
hasDiffWithBase: boolean
|
||||
}
|
220
src/create-pull-request.ts
Normal file
220
src/create-pull-request.ts
Normal file
@ -0,0 +1,220 @@
|
||||
import * as core from '@actions/core'
|
||||
import {createOrUpdateBranch} from './create-or-update-branch'
|
||||
import {GitHubHelper} from './github-helper'
|
||||
import {GitCommandManager} from './git-command-manager'
|
||||
import {GitAuthHelper} from './git-auth-helper'
|
||||
import * as utils from './utils'
|
||||
|
||||
export interface Inputs {
|
||||
token: string
|
||||
path: string
|
||||
commitMessage: string
|
||||
committer: string
|
||||
author: string
|
||||
signoff: boolean
|
||||
branch: string
|
||||
branchSuffix: string
|
||||
base: string
|
||||
pushToFork: string
|
||||
title: string
|
||||
body: string
|
||||
labels: string[]
|
||||
assignees: string[]
|
||||
reviewers: string[]
|
||||
teamReviewers: string[]
|
||||
milestone: number
|
||||
draft: boolean
|
||||
}
|
||||
|
||||
export async function createPullRequest(inputs: Inputs): Promise<void> {
|
||||
let gitAuthHelper
|
||||
try {
|
||||
// Get the repository path
|
||||
const repoPath = utils.getRepoPath(inputs.path)
|
||||
// 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')
|
||||
// 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
|
||||
? inputs.pushToFork
|
||||
: baseRemote.repository
|
||||
if (inputs.pushToFork) {
|
||||
// Check if the supplied fork is really a fork of the base
|
||||
const parentRepository = await githubHelper.getRepositoryParent(
|
||||
branchRepository
|
||||
)
|
||||
if (parentRepository != baseRemote.repository) {
|
||||
throw new Error(
|
||||
`Repository '${branchRepository}' is not a fork of '${baseRemote.repository}'. Unable to continue.`
|
||||
)
|
||||
}
|
||||
// Add a remote for the fork
|
||||
const remoteUrl = utils.getRemoteUrl(
|
||||
baseRemote.protocol,
|
||||
branchRepository
|
||||
)
|
||||
await git.exec(['remote', 'add', 'fork', remoteUrl])
|
||||
}
|
||||
core.endGroup()
|
||||
core.info(
|
||||
`Pull request branch target repository set to ${branchRepository}`
|
||||
)
|
||||
|
||||
// Configure auth
|
||||
if (baseRemote.protocol == 'HTTPS') {
|
||||
core.startGroup('Configuring credential for HTTPS authentication')
|
||||
await gitAuthHelper.configureToken(inputs.token)
|
||||
core.endGroup()
|
||||
}
|
||||
|
||||
// Determine if the checked out ref is a valid base for a pull request
|
||||
// The action needs the checked out HEAD ref to be a branch
|
||||
// This check will fail in the following cases:
|
||||
// - HEAD is detached
|
||||
// - HEAD is a merge commit (pull_request events)
|
||||
// - HEAD is a tag
|
||||
core.startGroup('Checking the checked out ref')
|
||||
const symbolicRefResult = await git.exec(
|
||||
['symbolic-ref', 'HEAD', '--short'],
|
||||
true
|
||||
)
|
||||
if (symbolicRefResult.exitCode != 0) {
|
||||
core.debug(`${symbolicRefResult.stderr}`)
|
||||
throw new Error(
|
||||
'The checked out ref is not a valid base for a pull request. Unable to continue.'
|
||||
)
|
||||
}
|
||||
const workingBase = symbolicRefResult.stdout.trim()
|
||||
// Exit if the working base is a PR branch created by this action.
|
||||
// This may occur when using a PAT instead of GITHUB_TOKEN because
|
||||
// a PAT allows workflow actions to trigger further events.
|
||||
if (workingBase.startsWith(inputs.branch)) {
|
||||
throw new Error(
|
||||
`Working base branch '${workingBase}' was created by this action. Unable to continue.`
|
||||
)
|
||||
}
|
||||
core.endGroup()
|
||||
|
||||
// Apply the branch suffix if set
|
||||
if (inputs.branchSuffix) {
|
||||
switch (inputs.branchSuffix) {
|
||||
case 'short-commit-hash':
|
||||
// Suffix with the short SHA1 hash
|
||||
inputs.branch = `${inputs.branch}-${await git.revParse('HEAD', [
|
||||
'--short'
|
||||
])}`
|
||||
break
|
||||
case 'timestamp':
|
||||
// Suffix with the current timestamp
|
||||
inputs.branch = `${inputs.branch}-${utils.secondsSinceEpoch()}`
|
||||
break
|
||||
case 'random':
|
||||
// Suffix with a 7 character random string
|
||||
inputs.branch = `${inputs.branch}-${utils.randomString()}`
|
||||
break
|
||||
default:
|
||||
throw new Error(
|
||||
`Branch suffix '${inputs.branchSuffix}' is not a valid value. Unable to continue.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Output head branch
|
||||
core.info(
|
||||
`Pull request branch to create or update set to '${inputs.branch}'`
|
||||
)
|
||||
|
||||
// Configure the committer and author
|
||||
core.startGroup('Configuring the committer and author')
|
||||
const parsedAuthor = utils.parseDisplayNameEmail(inputs.author)
|
||||
const parsedCommitter = utils.parseDisplayNameEmail(inputs.committer)
|
||||
git.setIdentityGitOptions([
|
||||
'-c',
|
||||
`author.name=${parsedAuthor.name}`,
|
||||
'-c',
|
||||
`author.email=${parsedAuthor.email}`,
|
||||
'-c',
|
||||
`committer.name=${parsedCommitter.name}`,
|
||||
'-c',
|
||||
`committer.email=${parsedCommitter.email}`
|
||||
])
|
||||
core.info(
|
||||
`Configured git committer as '${parsedCommitter.name} <${parsedCommitter.email}>'`
|
||||
)
|
||||
core.info(
|
||||
`Configured git author as '${parsedAuthor.name} <${parsedAuthor.email}>'`
|
||||
)
|
||||
core.endGroup()
|
||||
|
||||
// Create or update the pull request branch
|
||||
core.startGroup('Create or update the pull request branch')
|
||||
const result = await createOrUpdateBranch(
|
||||
git,
|
||||
inputs.commitMessage,
|
||||
inputs.base,
|
||||
inputs.branch,
|
||||
branchRemoteName,
|
||||
inputs.signoff
|
||||
)
|
||||
core.endGroup()
|
||||
|
||||
if (['created', 'updated'].includes(result.action)) {
|
||||
// The branch was created or updated
|
||||
core.startGroup(
|
||||
`Pushing pull request branch to '${branchRemoteName}/${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
|
||||
await githubHelper.createOrUpdatePullRequest(
|
||||
inputs,
|
||||
baseRemote.repository,
|
||||
branchRepository
|
||||
)
|
||||
} else {
|
||||
// If there is no longer a diff with the base delete the branch
|
||||
core.info(
|
||||
`Branch '${inputs.branch}' no longer differs from base branch '${inputs.base}'`
|
||||
)
|
||||
core.info(`Closing pull request and deleting branch '${inputs.branch}'`)
|
||||
await git.push([
|
||||
'--delete',
|
||||
'--force',
|
||||
branchRemoteName,
|
||||
`refs/heads/${inputs.branch}`
|
||||
])
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
core.setFailed(error.message)
|
||||
} finally {
|
||||
// Remove auth and restore persisted auth config if it existed
|
||||
core.startGroup('Restore persisted git credentials')
|
||||
await gitAuthHelper.removeAuth()
|
||||
await gitAuthHelper.restorePersistedAuth()
|
||||
core.endGroup()
|
||||
}
|
||||
}
|
126
src/git-auth-helper.ts
Normal file
126
src/git-auth-helper.ts
Normal file
@ -0,0 +1,126 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as fs from 'fs'
|
||||
import {GitCommandManager} from './git-command-manager'
|
||||
import * as path from 'path'
|
||||
import {URL} from 'url'
|
||||
|
||||
export class GitAuthHelper {
|
||||
private git: GitCommandManager
|
||||
private gitConfigPath: string
|
||||
private extraheaderConfigKey: string
|
||||
private extraheaderConfigPlaceholderValue = 'AUTHORIZATION: basic ***'
|
||||
private extraheaderConfigValueRegex = '^AUTHORIZATION:'
|
||||
private persistedExtraheaderConfigValue = ''
|
||||
|
||||
constructor(git: GitCommandManager) {
|
||||
this.git = git
|
||||
this.gitConfigPath = path.join(
|
||||
this.git.getWorkingDirectory(),
|
||||
'.git',
|
||||
'config'
|
||||
)
|
||||
const serverUrl = this.getServerUrl()
|
||||
this.extraheaderConfigKey = `http.${serverUrl.origin}/.extraheader`
|
||||
}
|
||||
|
||||
async savePersistedAuth(): Promise<void> {
|
||||
// Save and unset persisted extraheader credential in git config if it exists
|
||||
this.persistedExtraheaderConfigValue = await this.getAndUnset()
|
||||
}
|
||||
|
||||
async restorePersistedAuth(): Promise<void> {
|
||||
if (this.persistedExtraheaderConfigValue) {
|
||||
try {
|
||||
await this.setExtraheaderConfig(this.persistedExtraheaderConfigValue)
|
||||
core.info('Persisted git credentials restored')
|
||||
} catch (e) {
|
||||
core.warning(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async configureToken(token: string): Promise<void> {
|
||||
// Encode and configure the basic credential for HTTPS access
|
||||
const basicCredential = Buffer.from(
|
||||
`x-access-token:${token}`,
|
||||
'utf8'
|
||||
).toString('base64')
|
||||
core.setSecret(basicCredential)
|
||||
const extraheaderConfigValue = `AUTHORIZATION: basic ${basicCredential}`
|
||||
await this.setExtraheaderConfig(extraheaderConfigValue)
|
||||
}
|
||||
|
||||
async removeAuth(): Promise<void> {
|
||||
await this.getAndUnset()
|
||||
}
|
||||
|
||||
private async setExtraheaderConfig(
|
||||
extraheaderConfigValue: string
|
||||
): Promise<void> {
|
||||
// Configure a placeholder value. This approach avoids the credential being captured
|
||||
// by process creation audit events, which are commonly logged. For more information,
|
||||
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
|
||||
// See https://github.com/actions/checkout/blob/main/src/git-auth-helper.ts#L267-L274
|
||||
await this.git.config(
|
||||
this.extraheaderConfigKey,
|
||||
this.extraheaderConfigPlaceholderValue
|
||||
)
|
||||
// Replace the placeholder
|
||||
await this.gitConfigStringReplace(
|
||||
this.extraheaderConfigPlaceholderValue,
|
||||
extraheaderConfigValue
|
||||
)
|
||||
}
|
||||
|
||||
private async getAndUnset(): Promise<string> {
|
||||
let configValue = ''
|
||||
// Save and unset persisted extraheader credential in git config if it exists
|
||||
if (
|
||||
await this.git.configExists(
|
||||
this.extraheaderConfigKey,
|
||||
this.extraheaderConfigValueRegex
|
||||
)
|
||||
) {
|
||||
configValue = await this.git.getConfigValue(
|
||||
this.extraheaderConfigKey,
|
||||
this.extraheaderConfigValueRegex
|
||||
)
|
||||
if (
|
||||
await this.git.tryConfigUnset(
|
||||
this.extraheaderConfigKey,
|
||||
this.extraheaderConfigValueRegex
|
||||
)
|
||||
) {
|
||||
core.info(`Unset config key '${this.extraheaderConfigKey}'`)
|
||||
} else {
|
||||
core.warning(
|
||||
`Failed to unset config key '${this.extraheaderConfigKey}'`
|
||||
)
|
||||
}
|
||||
}
|
||||
return configValue
|
||||
}
|
||||
|
||||
private async gitConfigStringReplace(
|
||||
find: string,
|
||||
replace: string
|
||||
): Promise<void> {
|
||||
let content = (await fs.promises.readFile(this.gitConfigPath)).toString()
|
||||
const index = content.indexOf(find)
|
||||
if (index < 0 || index != content.lastIndexOf(find)) {
|
||||
throw new Error(`Unable to replace '${find}' in ${this.gitConfigPath}`)
|
||||
}
|
||||
content = content.replace(find, replace)
|
||||
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'
|
||||
)
|
||||
}
|
||||
}
|
294
src/git-command-manager.ts
Normal file
294
src/git-command-manager.ts
Normal file
@ -0,0 +1,294 @@
|
||||
import * as exec from '@actions/exec'
|
||||
import * as io from '@actions/io'
|
||||
import * as utils from './utils'
|
||||
import * as path from 'path'
|
||||
|
||||
const tagsRefSpec = '+refs/tags/*:refs/tags/*'
|
||||
|
||||
export class GitCommandManager {
|
||||
private gitPath: string
|
||||
private workingDirectory: string
|
||||
// Git options used when commands require an identity
|
||||
private identityGitOptions?: string[]
|
||||
|
||||
private constructor(workingDirectory: string, gitPath: string) {
|
||||
this.workingDirectory = workingDirectory
|
||||
this.gitPath = gitPath
|
||||
}
|
||||
|
||||
static async create(workingDirectory: string): Promise<GitCommandManager> {
|
||||
const gitPath = await io.which('git', true)
|
||||
return new GitCommandManager(workingDirectory, gitPath)
|
||||
}
|
||||
|
||||
setIdentityGitOptions(identityGitOptions: string[]): void {
|
||||
this.identityGitOptions = identityGitOptions
|
||||
}
|
||||
|
||||
async checkout(ref: string, startPoint?: string): Promise<void> {
|
||||
const args = ['checkout', '--progress']
|
||||
if (startPoint) {
|
||||
args.push('-B', ref, startPoint)
|
||||
} else {
|
||||
args.push(ref)
|
||||
}
|
||||
await this.exec(args)
|
||||
}
|
||||
|
||||
async cherryPick(
|
||||
options?: string[],
|
||||
allowAllExitCodes = false
|
||||
): Promise<GitOutput> {
|
||||
const args = ['cherry-pick']
|
||||
if (this.identityGitOptions) {
|
||||
args.unshift(...this.identityGitOptions)
|
||||
}
|
||||
|
||||
if (options) {
|
||||
args.push(...options)
|
||||
}
|
||||
|
||||
return await this.exec(args, allowAllExitCodes)
|
||||
}
|
||||
|
||||
async commit(options?: string[]): Promise<void> {
|
||||
const args = ['commit']
|
||||
if (this.identityGitOptions) {
|
||||
args.unshift(...this.identityGitOptions)
|
||||
}
|
||||
|
||||
if (options) {
|
||||
args.push(...options)
|
||||
}
|
||||
|
||||
await this.exec(args)
|
||||
}
|
||||
|
||||
async config(
|
||||
configKey: string,
|
||||
configValue: string,
|
||||
globalConfig?: boolean
|
||||
): Promise<void> {
|
||||
await this.exec([
|
||||
'config',
|
||||
globalConfig ? '--global' : '--local',
|
||||
configKey,
|
||||
configValue
|
||||
])
|
||||
}
|
||||
|
||||
async configExists(
|
||||
configKey: string,
|
||||
configValue = '.',
|
||||
globalConfig?: boolean
|
||||
): Promise<boolean> {
|
||||
const output = await this.exec(
|
||||
[
|
||||
'config',
|
||||
globalConfig ? '--global' : '--local',
|
||||
'--name-only',
|
||||
'--get-regexp',
|
||||
configKey,
|
||||
configValue
|
||||
],
|
||||
true
|
||||
)
|
||||
return output.exitCode === 0
|
||||
}
|
||||
|
||||
async diff(options?: string[]): Promise<string> {
|
||||
const args = ['-c', 'core.pager=cat', 'diff']
|
||||
if (options) {
|
||||
args.push(...options)
|
||||
}
|
||||
const output = await this.exec(args)
|
||||
return output.stdout.trim()
|
||||
}
|
||||
|
||||
async fetch(
|
||||
refSpec: string[],
|
||||
remoteName?: string,
|
||||
options?: string[]
|
||||
): Promise<void> {
|
||||
const args = ['-c', 'protocol.version=2', 'fetch']
|
||||
if (!refSpec.some(x => x === tagsRefSpec)) {
|
||||
args.push('--no-tags')
|
||||
}
|
||||
|
||||
args.push('--progress', '--no-recurse-submodules')
|
||||
if (
|
||||
utils.fileExistsSync(path.join(this.workingDirectory, '.git', 'shallow'))
|
||||
) {
|
||||
args.push('--unshallow')
|
||||
}
|
||||
|
||||
if (options) {
|
||||
args.push(...options)
|
||||
}
|
||||
|
||||
if (remoteName) {
|
||||
args.push(remoteName)
|
||||
} else {
|
||||
args.push('origin')
|
||||
}
|
||||
for (const arg of refSpec) {
|
||||
args.push(arg)
|
||||
}
|
||||
|
||||
await this.exec(args)
|
||||
}
|
||||
|
||||
async getConfigValue(configKey: string, configValue = '.'): Promise<string> {
|
||||
const output = await this.exec([
|
||||
'config',
|
||||
'--local',
|
||||
'--get-regexp',
|
||||
configKey,
|
||||
configValue
|
||||
])
|
||||
return output.stdout.trim().split(`${configKey} `)[1]
|
||||
}
|
||||
|
||||
getWorkingDirectory(): string {
|
||||
return this.workingDirectory
|
||||
}
|
||||
|
||||
async isDirty(untracked: boolean): Promise<boolean> {
|
||||
const diffArgs = ['--abbrev=40', '--full-index', '--raw']
|
||||
// Check staged changes
|
||||
if (await this.diff([...diffArgs, '--staged'])) {
|
||||
return true
|
||||
}
|
||||
// Check working index changes
|
||||
if (await this.diff(diffArgs)) {
|
||||
return true
|
||||
}
|
||||
// Check untracked changes
|
||||
if (untracked && (await this.status(['--porcelain', '-unormal']))) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async push(options?: string[]): Promise<void> {
|
||||
const args = ['push']
|
||||
if (options) {
|
||||
args.push(...options)
|
||||
}
|
||||
await this.exec(args)
|
||||
}
|
||||
|
||||
async revList(
|
||||
commitExpression: string[],
|
||||
options?: string[]
|
||||
): Promise<string> {
|
||||
const args = ['rev-list']
|
||||
if (options) {
|
||||
args.push(...options)
|
||||
}
|
||||
args.push(...commitExpression)
|
||||
const output = await this.exec(args)
|
||||
return output.stdout.trim()
|
||||
}
|
||||
|
||||
async revParse(ref: string, options?: string[]): Promise<string> {
|
||||
const args = ['rev-parse']
|
||||
if (options) {
|
||||
args.push(...options)
|
||||
}
|
||||
args.push(ref)
|
||||
const output = await this.exec(args)
|
||||
return output.stdout.trim()
|
||||
}
|
||||
|
||||
async status(options?: string[]): Promise<string> {
|
||||
const args = ['status']
|
||||
if (options) {
|
||||
args.push(...options)
|
||||
}
|
||||
const output = await this.exec(args)
|
||||
return output.stdout.trim()
|
||||
}
|
||||
|
||||
async symbolicRef(ref: string, options?: string[]): Promise<string> {
|
||||
const args = ['symbolic-ref', ref]
|
||||
if (options) {
|
||||
args.push(...options)
|
||||
}
|
||||
const output = await this.exec(args)
|
||||
return output.stdout.trim()
|
||||
}
|
||||
|
||||
async tryConfigUnset(
|
||||
configKey: string,
|
||||
configValue = '.',
|
||||
globalConfig?: boolean
|
||||
): Promise<boolean> {
|
||||
const output = await this.exec(
|
||||
[
|
||||
'config',
|
||||
globalConfig ? '--global' : '--local',
|
||||
'--unset',
|
||||
configKey,
|
||||
configValue
|
||||
],
|
||||
true
|
||||
)
|
||||
return output.exitCode === 0
|
||||
}
|
||||
|
||||
async tryGetRemoteUrl(): Promise<string> {
|
||||
const output = await this.exec(
|
||||
['config', '--local', '--get', 'remote.origin.url'],
|
||||
true
|
||||
)
|
||||
|
||||
if (output.exitCode !== 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const stdout = output.stdout.trim()
|
||||
if (stdout.includes('\n')) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return stdout
|
||||
}
|
||||
|
||||
async exec(args: string[], allowAllExitCodes = false): Promise<GitOutput> {
|
||||
const result = new GitOutput()
|
||||
|
||||
const env = {}
|
||||
for (const key of Object.keys(process.env)) {
|
||||
env[key] = process.env[key]
|
||||
}
|
||||
|
||||
const stdout: string[] = []
|
||||
const stderr: string[] = []
|
||||
|
||||
const options = {
|
||||
cwd: this.workingDirectory,
|
||||
env,
|
||||
ignoreReturnCode: allowAllExitCodes,
|
||||
listeners: {
|
||||
stdout: (data: Buffer) => {
|
||||
stdout.push(data.toString())
|
||||
},
|
||||
stderr: (data: Buffer) => {
|
||||
stderr.push(data.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.exitCode = await exec.exec(`"${this.gitPath}"`, args, options)
|
||||
result.stdout = stdout.join('')
|
||||
result.stderr = stderr.join('')
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
class GitOutput {
|
||||
stdout = ''
|
||||
stderr = ''
|
||||
exitCode = 0
|
||||
}
|
161
src/github-helper.ts
Normal file
161
src/github-helper.ts
Normal file
@ -0,0 +1,161 @@
|
||||
import * as core from '@actions/core'
|
||||
import {Inputs} from './create-pull-request'
|
||||
import {Octokit, OctokitOptions} from './octokit-client'
|
||||
|
||||
const ERROR_PR_REVIEW_FROM_AUTHOR =
|
||||
'Review cannot be requested from pull request author'
|
||||
|
||||
interface Repository {
|
||||
owner: string
|
||||
repo: string
|
||||
}
|
||||
|
||||
export class GitHubHelper {
|
||||
private octokit: InstanceType<typeof Octokit>
|
||||
|
||||
constructor(token: string) {
|
||||
const options: OctokitOptions = {}
|
||||
if (token) {
|
||||
options.auth = `${token}`
|
||||
}
|
||||
this.octokit = new Octokit(options)
|
||||
}
|
||||
|
||||
private parseRepository(repository: string): Repository {
|
||||
const [owner, repo] = repository.split('/')
|
||||
return {
|
||||
owner: owner,
|
||||
repo: repo
|
||||
}
|
||||
}
|
||||
|
||||
private async createOrUpdate(
|
||||
inputs: Inputs,
|
||||
baseRepository: string,
|
||||
headBranch: string
|
||||
): Promise<number> {
|
||||
// Try to create the pull request
|
||||
try {
|
||||
const {data: pull} = await this.octokit.pulls.create({
|
||||
...this.parseRepository(baseRepository),
|
||||
title: inputs.title,
|
||||
head: headBranch,
|
||||
base: inputs.base,
|
||||
body: inputs.body,
|
||||
draft: inputs.draft
|
||||
})
|
||||
core.info(
|
||||
`Created pull request #${pull.number} (${headBranch} => ${inputs.base})`
|
||||
)
|
||||
return pull.number
|
||||
} catch (e) {
|
||||
if (
|
||||
!e.message ||
|
||||
!e.message.includes(`A pull request already exists for ${headBranch}`)
|
||||
) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// Update the pull request that exists for this branch and base
|
||||
const {data: pulls} = await this.octokit.pulls.list({
|
||||
...this.parseRepository(baseRepository),
|
||||
state: 'open',
|
||||
head: headBranch,
|
||||
base: inputs.base
|
||||
})
|
||||
const {data: pull} = await this.octokit.pulls.update({
|
||||
...this.parseRepository(baseRepository),
|
||||
pull_number: pulls[0].number,
|
||||
title: inputs.title,
|
||||
body: inputs.body,
|
||||
draft: inputs.draft
|
||||
})
|
||||
core.info(
|
||||
`Updated pull request #${pull.number} (${headBranch} => ${inputs.base})`
|
||||
)
|
||||
return pull.number
|
||||
}
|
||||
|
||||
async getRepositoryParent(headRepository: string): Promise<string> {
|
||||
const {data: headRepo} = await this.octokit.repos.get({
|
||||
...this.parseRepository(headRepository)
|
||||
})
|
||||
if (!headRepo.parent) {
|
||||
throw new Error(
|
||||
`Repository '${headRepository}' is not a fork. Unable to continue.`
|
||||
)
|
||||
}
|
||||
return headRepo.parent.full_name
|
||||
}
|
||||
|
||||
async createOrUpdatePullRequest(
|
||||
inputs: Inputs,
|
||||
baseRepository: string,
|
||||
headRepository: string
|
||||
): Promise<void> {
|
||||
const [headOwner] = headRepository.split('/')
|
||||
const headBranch = `${headOwner}:${inputs.branch}`
|
||||
|
||||
// Create or update the pull request
|
||||
const pullNumber = await this.createOrUpdate(
|
||||
inputs,
|
||||
baseRepository,
|
||||
headBranch
|
||||
)
|
||||
|
||||
// Set outputs
|
||||
core.startGroup('Setting outputs')
|
||||
core.setOutput('pull-request-number', pullNumber)
|
||||
core.exportVariable('PULL_REQUEST_NUMBER', pullNumber)
|
||||
core.endGroup()
|
||||
|
||||
// Set milestone, labels and assignees
|
||||
const updateIssueParams = {}
|
||||
if (inputs.milestone) {
|
||||
updateIssueParams['milestone'] = inputs.milestone
|
||||
core.info(`Applying milestone '${inputs.milestone}'`)
|
||||
}
|
||||
if (inputs.labels.length > 0) {
|
||||
updateIssueParams['labels'] = inputs.labels
|
||||
core.info(`Applying labels '${inputs.labels}'`)
|
||||
}
|
||||
if (inputs.assignees.length > 0) {
|
||||
updateIssueParams['assignees'] = inputs.assignees
|
||||
core.info(`Applying assignees '${inputs.assignees}'`)
|
||||
}
|
||||
if (Object.keys(updateIssueParams).length > 0) {
|
||||
await this.octokit.issues.update({
|
||||
...this.parseRepository(baseRepository),
|
||||
issue_number: pullNumber,
|
||||
...updateIssueParams
|
||||
})
|
||||
}
|
||||
|
||||
// Request reviewers and team reviewers
|
||||
const requestReviewersParams = {}
|
||||
if (inputs.reviewers.length > 0) {
|
||||
requestReviewersParams['reviewers'] = inputs.reviewers
|
||||
core.info(`Requesting reviewers '${inputs.reviewers}'`)
|
||||
}
|
||||
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.pulls.requestReviewers({
|
||||
...this.parseRepository(baseRepository),
|
||||
pull_number: pullNumber,
|
||||
...requestReviewersParams
|
||||
})
|
||||
} catch (e) {
|
||||
if (e.message && e.message.includes(ERROR_PR_REVIEW_FROM_AUTHOR)) {
|
||||
core.warning(ERROR_PR_REVIEW_FROM_AUTHOR)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
36
src/main.ts
Normal file
36
src/main.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import * as core from '@actions/core'
|
||||
import {Inputs, createPullRequest} from './create-pull-request'
|
||||
import {inspect} from 'util'
|
||||
import * as utils from './utils'
|
||||
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
const inputs: Inputs = {
|
||||
token: core.getInput('token'),
|
||||
path: core.getInput('path'),
|
||||
commitMessage: core.getInput('commit-message'),
|
||||
committer: core.getInput('committer'),
|
||||
author: core.getInput('author'),
|
||||
signoff: core.getInput('signoff') === 'true',
|
||||
branch: core.getInput('branch'),
|
||||
branchSuffix: core.getInput('branch-suffix'),
|
||||
base: core.getInput('base'),
|
||||
pushToFork: core.getInput('push-to-fork'),
|
||||
title: core.getInput('title'),
|
||||
body: core.getInput('body'),
|
||||
labels: utils.getInputAsArray('labels'),
|
||||
assignees: utils.getInputAsArray('assignees'),
|
||||
reviewers: utils.getInputAsArray('reviewers'),
|
||||
teamReviewers: utils.getInputAsArray('team-reviewers'),
|
||||
milestone: Number(core.getInput('milestone')),
|
||||
draft: core.getInput('draft') === 'true'
|
||||
}
|
||||
core.debug(`Inputs: ${inspect(inputs)}`)
|
||||
|
||||
await createPullRequest(inputs)
|
||||
} catch (error) {
|
||||
core.setFailed(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
run()
|
7
src/octokit-client.ts
Normal file
7
src/octokit-client.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import {Octokit as Core} from '@octokit/core'
|
||||
import {paginateRest} from '@octokit/plugin-paginate-rest'
|
||||
import {restEndpointMethods} from '@octokit/plugin-rest-endpoint-methods'
|
||||
export {RestEndpointMethodTypes} from '@octokit/plugin-rest-endpoint-methods'
|
||||
export {OctokitOptions} from '@octokit/core/dist-types/types'
|
||||
|
||||
export const Octokit = Core.plugin(paginateRest, restEndpointMethods)
|
139
src/utils.ts
Normal file
139
src/utils.ts
Normal file
@ -0,0 +1,139 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
export function getInputAsArray(
|
||||
name: string,
|
||||
options?: core.InputOptions
|
||||
): string[] {
|
||||
return getStringAsArray(core.getInput(name, options))
|
||||
}
|
||||
|
||||
export function getStringAsArray(str: string): string[] {
|
||||
return str
|
||||
.split(/[\n,]+/)
|
||||
.map(s => s.trim())
|
||||
.filter(x => x !== '')
|
||||
}
|
||||
|
||||
export function getRepoPath(relativePath?: string): string {
|
||||
let githubWorkspacePath = process.env['GITHUB_WORKSPACE']
|
||||
if (!githubWorkspacePath) {
|
||||
throw new Error('GITHUB_WORKSPACE not defined')
|
||||
}
|
||||
githubWorkspacePath = path.resolve(githubWorkspacePath)
|
||||
core.debug(`githubWorkspacePath: ${githubWorkspacePath}`)
|
||||
|
||||
let repoPath = githubWorkspacePath
|
||||
if (relativePath) repoPath = path.resolve(repoPath, relativePath)
|
||||
|
||||
core.debug(`repoPath: ${repoPath}`)
|
||||
return repoPath
|
||||
}
|
||||
|
||||
interface RemoteDetail {
|
||||
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 httpsUrlPattern = /^https:\/\/.*@?github.com\/(.+\/.+)$/i
|
||||
const sshUrlPattern = /^git@github.com:(.+\/.+).git$/i
|
||||
|
||||
const httpsMatch = remoteUrl.match(httpsUrlPattern)
|
||||
if (httpsMatch) {
|
||||
return {
|
||||
protocol: 'HTTPS',
|
||||
repository: httpsMatch[1]
|
||||
}
|
||||
}
|
||||
|
||||
const sshMatch = remoteUrl.match(sshUrlPattern)
|
||||
if (sshMatch) {
|
||||
return {
|
||||
protocol: 'SSH',
|
||||
repository: sshMatch[1]
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`The format of '${remoteUrl}' is not a valid GitHub repository URL`
|
||||
)
|
||||
}
|
||||
|
||||
export function getRemoteUrl(protocol: string, repository: string): string {
|
||||
return protocol == 'HTTPS'
|
||||
? `https://github.com/${repository}`
|
||||
: `git@github.com:${repository}.git`
|
||||
}
|
||||
|
||||
export function secondsSinceEpoch(): number {
|
||||
const now = new Date()
|
||||
return Math.round(now.getTime() / 1000)
|
||||
}
|
||||
|
||||
export function randomString(): string {
|
||||
return Math.random().toString(36).substr(2, 7)
|
||||
}
|
||||
|
||||
interface DisplayNameEmail {
|
||||
name: string
|
||||
email: string
|
||||
}
|
||||
|
||||
export function parseDisplayNameEmail(
|
||||
displayNameEmail: string
|
||||
): DisplayNameEmail {
|
||||
// Parse the name and email address from a string in the following format
|
||||
// Display Name <email@address.com>
|
||||
const pattern = /^([^<]+)\s*<([^>]+)>$/i
|
||||
|
||||
// Check we have a match
|
||||
const match = displayNameEmail.match(pattern)
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
`The format of '${displayNameEmail}' is not a valid email address with display name`
|
||||
)
|
||||
}
|
||||
|
||||
// Check that name and email are not just whitespace
|
||||
const name = match[1].trim()
|
||||
const email = match[2].trim()
|
||||
if (!name || !email) {
|
||||
throw new Error(
|
||||
`The format of '${displayNameEmail}' is not a valid email address with display name`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
name: name,
|
||||
email: email
|
||||
}
|
||||
}
|
||||
|
||||
export function fileExistsSync(path: string): boolean {
|
||||
if (!path) {
|
||||
throw new Error("Arg 'path' must not be empty")
|
||||
}
|
||||
|
||||
let stats: fs.Stats
|
||||
try {
|
||||
stats = fs.statSync(path)
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return false
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Encountered an error when checking whether path '${path}' exists: ${error.message}`
|
||||
)
|
||||
}
|
||||
|
||||
if (!stats.isDirectory()) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"lib": [
|
||||
"es6"
|
||||
],
|
||||
"outDir": "./lib",
|
||||
"rootDir": "./src",
|
||||
"declaration": true,
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"exclude": ["__test__", "lib", "node_modules"]
|
||||
}
|
Reference in New Issue
Block a user