Compare commits

...

236 Commits

Author SHA1 Message Date
ebc5e02585 Merge pull request #429 from peter-evans/dev
v3
2020-07-23 15:19:55 +09:00
c01f8fadc7 Remove unnecessary scripts 2020-07-23 15:04:58 +09:00
c0c9424b03 Update changes documentation 2020-07-23 14:52:44 +09:00
7649c29e89 Update documentation 2020-07-23 14:35:35 +09:00
125f154b50 Update documentation 2020-07-20 22:12:30 +09:00
8c01dce3ac Restore the branch-suffix input 2020-07-20 19:15:11 +09:00
3c6aade49b Fix link to action.yml 2020-07-19 20:47:45 +09:00
76c09b178c Default author and committer to actions.yml defaults 2020-07-19 20:23:36 +09:00
2f62e00e1b Rename test command param from branch to ref 2020-07-19 17:20:46 +09:00
16354377ad Update documentation 2020-07-19 17:04:47 +09:00
809468fcd7 Update documentation 2020-07-19 17:01:54 +09:00
216b3681c3 Update workflow 2020-07-19 16:20:13 +09:00
6fa44e144d Set defaults in action.yml 2020-07-19 15:09:44 +09:00
b5b1bc17a4 Remove branch-suffix input 2020-07-19 13:58:50 +09:00
794518a553 Move fileExistsSync to utils 2020-07-18 17:55:42 +09:00
f4ee4a8333 Unshallow on fetch 2020-07-18 16:04:36 +09:00
053b501145 Redesign from request-to-parent to push-to-fork 2020-07-18 15:35:30 +09:00
1a182d0679 Fix function name 2020-07-17 21:06:16 +09:00
24012f5c84 Refactor extraheader auth handling 2020-07-17 20:54:39 +09:00
a6a1a418bf Use force-with-lease to push the pr branch 2020-07-17 10:37:09 +09:00
5429e57002 Keep env var output for backwards compat 2020-07-16 19:42:19 +09:00
3c32fac1fe Tidy log output into groups 2020-07-16 19:13:28 +09:00
803cc5ea8a Remove python related code and artifacts 2020-07-16 18:18:58 +09:00
4ba9ca3d10 Convert action to typescript 2020-07-16 17:57:42 +09:00
40e70b8f7b Update documentation 2020-07-01 09:42:27 +09:00
b7eabdce59 Update documentation 2020-06-30 16:39:01 +09:00
43dc723813 Update documentation 2020-06-30 16:05:44 +09:00
4d3b0a48ef Merge pull request #394 from peter-evans/update-distribution
Update distribution
2020-06-27 17:10:28 +09:00
e9a825aacd Update distribution 2020-06-27 08:06:58 +00:00
3509fd45ae Merge pull request #389 from peter-evans/dev
Parse repo urls with credentials
2020-06-27 17:03:41 +09:00
14ee9d1df2 Parse repo urls with credentials 2020-06-27 16:45:24 +09:00
5d969a55c1 Add gradle example 2020-06-05 18:23:04 +09:00
44130f6fc9 Merge pull request #369 from peter-evans/types
Add missing types
2020-06-03 17:09:43 +09:00
86ccd8cdef Add missing types 2020-06-03 16:47:02 +09:00
7e7fa32a5f Merge pull request #364 from peter-evans/fix-ci
Fix ci artifacts
2020-06-02 18:21:51 +09:00
5f7beeb2ff Fix ci artifacts 2020-06-02 18:16:23 +09:00
926d56fcba Update documentation 2020-06-02 16:44:14 +09:00
8d744a2cd3 Remove reaction-token 2020-06-02 15:58:02 +09:00
c2d829c681 Update documentation 2020-05-27 11:43:25 +09:00
d77392faf0 Update slash command dispatch token 2020-05-25 17:15:16 +09:00
ccd2b64012 Add rebase slash command 2020-05-25 16:12:13 +09:00
58fb221778 Merge pull request #339 from peter-evans/update-distribution
Update distribution
2020-05-24 09:36:26 +09:00
7a856e8b5d Update distribution 2020-05-24 00:34:21 +00:00
65d7a66451 Merge pull request #293 from peter-evans/renovate/setuptools-46.x
Update dependency setuptools to v46.4.0
2020-05-24 09:30:21 +09:00
44a7f59b6f Update dependency setuptools to v46.4.0 2020-05-23 06:24:24 +00:00
8acaf6bb4c Merge pull request #330 from peter-evans/int-tests
Integration testing
2020-05-23 15:16:37 +09:00
498d78cb23 Add integration testing 2020-05-23 14:43:18 +09:00
172ec762f8 Merge pull request #301 from peter-evans/update-distribution
Update distribution
2020-05-17 18:28:48 +09:00
5cb0d674f3 Update distribution 2020-05-17 09:26:30 +00:00
eb892d7803 Merge pull request #308 from peter-evans/typescript
Typescript
2020-05-17 18:24:18 +09:00
f8274253bd Convert to typescript 2020-05-17 18:02:41 +09:00
0f423da02c Merge pull request #315 from peter-evans/input-def
Add missing draft input definition
2020-05-17 17:55:03 +09:00
9573f479a0 Add missing draft input definition 2020-05-17 17:47:54 +09:00
eb4cde120d Update documentation 2020-05-17 14:42:46 +09:00
25902ccdd1 Remove dockerhub-description workflow 2020-05-16 14:36:18 +09:00
39b337e8bb Remove codeowners 2020-05-14 10:19:06 +09:00
7e70d8e63c Skip test job for pull requests from forks 2020-05-13 17:25:18 +09:00
1a640f5b01 Update README 2020-05-11 14:31:26 +09:00
9faa8dc1d9 Update pull request example image 2020-05-11 14:28:02 +09:00
afcf57957d Merge pull request #287 from peter-evans/update-distribution
Update distribution
2020-05-11 14:09:44 +09:00
8cc3564bf3 Update distribution 2020-05-11 05:09:19 +00:00
6910c5cd03 Merge pull request #271 from peter-evans/renovate/setuptools-46.x
Update dependency setuptools to v46.2.0
2020-05-11 14:07:27 +09:00
14836c6ff3 Update dependency setuptools to v46.2.0 2020-05-11 05:04:42 +00:00
8fbfcfbcbb Revert "Temporarily use deprecated output"
This reverts commit fbb7e0e650.
2020-05-11 14:01:43 +09:00
326f260418 Merge pull request #278 from peter-evans/update-distribution
Update distribution
2020-05-11 13:59:07 +09:00
75104b7d7e Update distribution 2020-05-11 04:58:23 +00:00
fbb7e0e650 Temporarily use deprecated output 2020-05-11 13:55:32 +09:00
0f1e60a1f8 Merge pull request #264 from peter-evans/dev
Deprecate pr_number, project and project-column
2020-05-11 13:50:08 +09:00
caa116d991 Deprecate project and project-column 2020-05-10 19:02:35 +09:00
d2f72f0799 Update workflows 2020-05-10 18:09:48 +09:00
ded05960f3 Deprecate pr_number output 2020-05-10 18:06:32 +09:00
eb605db8a3 Fix casing 2020-05-10 17:58:21 +09:00
b11e4c665b Merge pull request #263 from christopherthielen/patch-1
docs: request-on-parent  ->  request-to-parent
2020-05-09 07:43:25 +09:00
65327d17a5 docs: request-on-parent -> request-to-parent 2020-05-08 11:03:56 -07:00
0837238e66 Merge pull request #239 from peter-evans/update-distribution
Update distribution
2020-05-06 12:47:46 +09:00
17bd947e89 Update distribution 2020-05-06 03:42:38 +00:00
24b42ba7f4 Merge pull request #240 from peter-evans/renovate/gitpython-3.x
Update dependency GitPython to v3.1.2
2020-05-06 12:40:52 +09:00
c0aaf5bab9 Update documentation 2020-05-06 11:37:48 +09:00
968cb0f4d9 Ignore documentation paths 2020-05-06 11:31:35 +09:00
9d6f73d546 Update documentation 2020-05-06 11:23:37 +09:00
70d240d0c4 Add update dependencies workflow 2020-05-06 10:51:06 +09:00
7bb7d96c96 Update dependency GitPython to v3.1.2 2020-05-06 01:49:21 +00:00
66fcd19e8d Whitelist pip requirements updates 2020-05-06 10:46:29 +09:00
d93f2b46fd Update dependency jest to v26.0.1 2020-05-05 12:23:22 +00:00
246328e3d8 Add ci badge 2020-05-05 14:24:35 +09:00
61cff7c673 Update dependency jest to v26 2020-05-04 19:59:11 +00:00
46ba7bdfe8 Merge pull request #229 from peter-evans/update-distribution
Update distribution
2020-05-03 12:18:11 +09:00
d650be7389 Update distribution 2020-05-03 03:15:56 +00:00
2f7173349f Fix dist requirements version 2020-05-03 12:13:39 +09:00
4ca95026d7 Ignore dist for renovate updates 2020-05-03 12:12:19 +09:00
64c4efd526 Merge pull request #219 from peter-evans/renovate/pygithub-1.x
Update dependency PyGithub to v1.51
2020-05-03 11:46:05 +09:00
16e35685ce Update dependency PyGithub to v1.51 2020-05-03 02:36:00 +00:00
3b12cf0165 Merge pull request #220 from peter-evans/fix-ci
Fix CI workflow
2020-05-03 11:35:02 +09:00
2a283f5fc3 Fix ci workflow 2020-05-03 11:31:45 +09:00
8ed207bcca Merge pull request #214 from peter-evans/update-distribution
Update distribution
2020-05-02 21:56:54 +09:00
bd1f6727cd Update distribution 2020-05-02 11:27:54 +00:00
9a3acf8f32 Update dependency jest to v25.5.4 2020-05-02 11:26:02 +00:00
b38fd9eb87 Merge pull request #206 from peter-evans/renovate/actions-tool-cache-1.x
Update dependency @actions/tool-cache to v1.3.4
2020-05-02 19:28:31 +09:00
4a9e76e377 Update dependency @actions/tool-cache to v1.3.4 2020-05-02 09:57:02 +00:00
8cb4c8b741 Merge pull request #201 from peter-evans/update-distribution
Update distribution
2020-05-02 17:47:24 +09:00
b9eb5dd95e Update distribution 2020-05-02 08:45:04 +00:00
5502904068 Merge pull request #191 from peter-evans/renovate/actions-core-1.x
Update dependency @actions/core to v1.2.4
2020-05-02 17:43:01 +09:00
56ad1fed7b Update dependency @actions/core to v1.2.4 2020-05-02 08:40:54 +00:00
2132f428f6 Merge pull request #192 from peter-evans/renovate/actions-exec-1.x
Update dependency @actions/exec to v1.0.4
2020-05-02 17:40:28 +09:00
c558d39395 Update dependency @actions/exec to v1.0.4 2020-05-02 08:30:46 +00:00
db640fa8db Merge pull request #168 from peter-evans/renovate/jest-monorepo
Update dependency jest to v25.5.3
2020-05-02 16:07:29 +09:00
95d6677567 Merge pull request #171 from peter-evans/ci
Add CI workflow
2020-05-02 16:04:14 +09:00
1f4e24248b Update test suite command 2020-05-02 16:02:02 +09:00
08595270b5 Filter by comment author 2020-05-02 15:32:34 +09:00
237244614a Remove author search condition 2020-05-02 15:25:52 +09:00
6295d61f0c Rename job 2020-05-02 15:19:44 +09:00
d4024e2876 Add test suite help comment 2020-05-02 15:13:50 +09:00
ee96ad03d9 Auto merge dev dependency updates 2020-05-02 14:55:39 +09:00
755b39d2ff Pin dependencies 2020-05-02 14:55:07 +09:00
9f95ac6c53 Setup python for missing pip dependency 2020-05-02 14:50:21 +09:00
cedbe4ad47 Add ci workflow 2020-05-02 14:46:15 +09:00
0e48ed8743 Update dependency jest to v25.5.3 2020-04-30 22:19:12 +00:00
e7291b422e Merge pull request #166 from peter-evans/renovate/jest-monorepo
Update dependency jest to v25.5.0
2020-04-29 08:08:26 +09:00
eb824681a8 Update dependency jest to v25.5.0 2020-04-28 19:53:37 +00:00
4cc13107a9 Merge pull request #165 from peter-evans/dev
Update dependency PyGithub to v1.50
2020-04-27 12:06:54 +09:00
c71b8e4206 Update vendored dependencies 2020-04-27 11:48:52 +09:00
e2bf7f9b75 Update dependency PyGithub to v1.50 2020-04-27 11:47:17 +09:00
e1f4cfdcd4 Merge pull request #162 from peter-evans/renovate/jest-monorepo
Update dependency jest to v25.4.0
2020-04-20 10:43:20 +09:00
b3f0552507 Update dependency jest to v25.4.0 2020-04-19 21:52:02 +00:00
f4be118b21 Merge pull request #160 from peter-evans/dev
Update dependency GitPython to v3.1.1
2020-04-14 12:02:29 +09:00
c9f22f86fb Vendor wheel 2020-04-14 11:48:20 +09:00
35d5f3c8ae Vendor setuptools 2020-04-14 11:43:07 +09:00
000a0fc06a Update vendored dependencies 2020-04-13 17:53:37 +09:00
2a59f517a7 Update dependency GitPython to v3.1.1 2020-04-13 17:51:52 +09:00
48ce89bc7d Update documentation 2020-04-13 09:57:57 +09:00
6570353abb Update README 2020-04-13 09:36:37 +09:00
8f6cecd6c4 Update workflow 2020-04-10 17:11:29 +09:00
e14ef3b543 Update README 2020-04-10 17:10:06 +09:00
c5778e5181 Merge pull request #157 from peter-evans/renovate/jest-monorepo
Update dependency jest to v25.3.0
2020-04-09 00:23:26 +09:00
374fc61fef Update dependency jest to v25.3.0 2020-04-08 14:53:23 +00:00
6fa547cc6f Update documentation 2020-04-06 09:37:35 +09:00
4db3619128 Merge pull request #154 from peter-evans/renovate/zeit-ncc-0.x
Update dependency @zeit/ncc to v0.22.1
2020-04-06 09:10:47 +09:00
989a8308ec Update dependency @zeit/ncc to v0.22.1 2020-04-05 20:34:21 +00:00
6249109e58 Update documentation 2020-04-05 18:17:05 +09:00
c9b850c450 Update workflow 2020-04-04 18:26:59 +09:00
340e629d2f Merge pull request #152 from peter-evans/dev
Add input for draft pull requests
2020-04-04 09:50:05 +09:00
abc19caa82 Add input for draft pull requests 2020-04-04 09:47:58 +09:00
3474dda921 Update documentation 2020-04-03 17:26:11 +09:00
ad11b10aa4 Merge pull request #151 from peter-evans/renovate/jest-monorepo
Update dependency jest to v25.2.7
2020-04-03 17:14:23 +09:00
86aa5be8bf Update dependency jest to v25.2.7 2020-04-03 07:58:49 +00:00
6867319cf3 Merge pull request #149 from peter-evans/renovate/jest-monorepo
Update dependency jest to v25.2.6
2020-04-02 21:58:25 +09:00
cc84a2389e Update dependency jest to v25.2.6 2020-04-02 10:36:04 +00:00
7e7150d0e8 Merge pull request #147 from peter-evans/dev
Default token to github.token
2020-04-01 19:09:39 +09:00
eb99d45ce6 Default token to github.token 2020-04-01 18:50:53 +09:00
115b7391e1 Revert "Update documentation"
This reverts commit 628c2d7d35.
2020-03-30 17:36:34 +09:00
8305970523 Merge pull request #146 from peter-evans/renovate/jest-monorepo
Update dependency jest to v25.2.4
2020-03-30 09:02:53 +09:00
32f5c5dd5f Update dependency jest to v25.2.4 2020-03-29 20:05:26 +00:00
628c2d7d35 Update documentation 2020-03-29 21:34:47 +09:00
37582e8764 Update documentation 2020-03-29 21:27:56 +09:00
1e6b4d1790 Merge pull request #145 from peter-evans/dev
Create pull requests in the parent repository of a checked out fork
2020-03-29 21:02:48 +09:00
a70c6ebe2a Merge pull request #144 from jderusse/fork
Allow custom repository when pushin in fork
2020-03-29 20:47:00 +09:00
5bd05538d0 Update src/cpr/create_or_update_pull_request.py
Co-Authored-By: Peter Evans <peter-evans@users.noreply.github.com>
2020-03-29 13:36:48 +02:00
26bc40eea1 Add link in TOC 2020-03-29 12:44:57 +02:00
6bb0e7771c Apply suggestions from code review
Co-Authored-By: Peter Evans <peter-evans@users.noreply.github.com>
2020-03-29 12:43:21 +02:00
4c347a4514 Update docs/concepts-guidelines.md
Co-Authored-By: Tobias Nyholm <tobias.nyholm@gmail.com>
2020-03-28 16:42:57 +01:00
cff2c3381d Add doc 2020-03-28 13:27:58 +01:00
e48dab0c1c Add PR creation from Fork 2020-03-28 13:10:12 +01:00
ac9f92d6e7 Merge pull request #141 from peter-evans/renovate/jest-monorepo
Update dependency jest to v25.2.3
2020-03-27 08:15:40 +09:00
3ff256ce08 Update dependency jest to v25.2.3 2020-03-26 20:38:32 +00:00
0bcb10560b Merge pull request #140 from peter-evans/renovate/jest-monorepo
Update dependency jest to v25.2.1
2020-03-26 19:03:52 +09:00
3c4b2793c1 Update dependency jest to v25.2.1 2020-03-26 09:43:37 +00:00
22870d7816 Merge pull request #139 from peter-evans/renovate/jest-monorepo
Update dependency jest to v25.2.0
2020-03-26 10:23:31 +09:00
e324a22ee1 Update dependency jest to v25.2.0 2020-03-25 18:18:56 +00:00
8b60386018 Merge pull request #135 from peter-evans/renovate/zeit-ncc-0.x
Update dependency @zeit/ncc to v0.22.0
2020-03-24 09:20:19 +09:00
6852d55922 Update dependency @zeit/ncc to v0.22.0 2020-03-23 22:31:46 +00:00
32e5bb80a5 Merge pull request #134 from peter-evans/dev
Update dependencies
2020-03-21 11:38:19 +09:00
619cf2115d Update dependencies due to security vulnerabilities 2020-03-21 11:31:34 +09:00
69008aa567 Update vendored dependencies 2020-03-21 11:29:37 +09:00
ae4278bf24 Update dependency PyGithub to v1.47 2020-03-21 11:23:42 +09:00
88da40fea7 Merge pull request #132 from peter-evans/dev
Update dependency GitPython to v3.1.0
2020-03-07 09:08:24 +09:00
694e068136 Update vendored dependencies 2020-03-07 08:57:59 +09:00
64c34f6885 Update README 2020-03-03 09:49:52 +09:00
52ada17960 Update dependency GitPython to v3.1.0 2020-02-25 13:12:00 +09:00
ce00b952cf Merge pull request #128 from peter-evans/dev
Unset and restore authorization extraheader only
2020-02-22 17:02:15 +09:00
0d42c285a3 Unset and restore authorization extraheader only 2020-02-22 16:56:42 +09:00
ea1eaf1734 Merge pull request #127 from peter-evans/dev
Unset and restore extraheader config option
2020-02-22 14:53:46 +09:00
d5c5ea3e20 Unset and restore extraheader config option 2020-02-22 14:08:54 +09:00
c7b64af0a4 Merge pull request #123 from peter-evans/dev
Authenticate with git extraheader
2020-02-18 19:49:13 +09:00
289fda9fea Authenticate with git extraheader 2020-02-18 19:35:15 +09:00
b021b9e27a Update vendored dependencies 2020-02-18 15:59:18 +09:00
c26314237b Update dependency GitPython to v3.0.8 2020-02-18 15:57:13 +09:00
ed7dd8d236 Update documentation 2020-02-14 20:10:27 +09:00
ca0e9d75fd Update documentation 2020-02-14 00:23:21 +09:00
87c27ee3eb Merge pull request #118 from peter-evans/dev
Assume python3 on PATH when running in a container
2020-02-14 00:07:03 +09:00
4beea725d3 Call python3 when running in a container 2020-02-13 17:37:08 +09:00
9d58699da5 Skip python setup when running in a container 2020-02-13 16:26:04 +09:00
bceebba814 Merge pull request #117 from peter-evans/renovate/zeit-ncc-0.x
Update dependency @zeit/ncc to v0.21.1
2020-02-13 09:58:46 +09:00
0b82710b2e Update dependency @zeit/ncc to v0.21.1 2020-02-12 16:54:11 +00:00
4aaaa0e760 Update README 2020-02-11 16:36:16 +09:00
918bfcb8a3 Update README 2020-02-11 16:28:16 +09:00
1ecfd1ae40 Merge pull request #116 from peter-evans/dev
Update dependency PyGithub to v1.46
2020-02-11 14:03:17 +09:00
4dd195d7c3 Update vendored dependencies 2020-02-11 13:51:59 +09:00
72b5f45bb4 Update dependency PyGithub to v1.46 2020-02-11 13:49:50 +09:00
5394814b39 Update documentation 2020-02-11 01:21:05 +09:00
83918398f5 Update documentation 2020-02-11 01:15:59 +09:00
b6a458d96a Merge pull request #114 from peter-evans/dev
Add support for ssh protocol
2020-02-10 23:06:03 +09:00
7b5ff6b642 Add support for ssh protocol 2020-02-10 22:52:00 +09:00
82eddd8828 Merge pull request #112 from peter-evans/dev
Skip python setup for alpine linux
2020-02-10 18:47:40 +09:00
6df2a462d1 Skip python setup for alpine linux 2020-02-10 09:53:43 +09:00
3689bd07d7 Merge pull request #110 from peter-evans/dev
Update dependency GitPython to v3.0.7
2020-02-08 16:45:18 +09:00
943f19ac64 Update vendored dependencies 2020-02-08 16:12:36 +09:00
e59d6c7fff Update dependency GitPython to v3.0.7 2020-02-08 16:09:50 +09:00
c65a4f39b3 Update documentation 2020-02-08 16:04:23 +09:00
2d18371789 Update documentation 2020-02-08 12:52:06 +09:00
cc7020a609 Merge pull request #107 from peter-evans/dev
Determine target github repository from git config
2020-02-07 19:01:09 +09:00
d8700620d6 Determine target github repository from git config 2020-02-07 11:05:01 +09:00
b7064071dc Move assets 2020-01-25 16:29:22 +09:00
339e82d37b Update documentation 2020-01-24 13:00:38 +09:00
a319015452 Merge pull request #102 from peter-evans/dev
Vendor python dependencies
2020-01-23 18:59:19 +09:00
df0d07269b Vendor python dependencies 2020-01-23 16:59:19 +09:00
2aa04baf2e Merge pull request #101 from peter-evans/dev
Update title and body when pr exists
2020-01-23 09:54:43 +09:00
d29f1e9296 Update title and body when pr exists 2020-01-23 09:47:35 +09:00
8ee12880b0 Update documentation 2020-01-21 22:35:00 +09:00
8d39de8771 Update documentation 2020-01-21 22:27:02 +09:00
c202684c92 Merge pull request #98 from peter-evans/dev
Follow the python minor version
2020-01-17 17:48:11 +09:00
e970adccb4 Follow the python minor version 2020-01-17 17:39:10 +09:00
9f9af3e969 Merge pull request #97 from peter-evans/dev
Update Python to version 3.8.1
2020-01-17 15:52:01 +09:00
615d7c82e3 Update Python to version 3.8.1 2020-01-17 15:46:49 +09:00
8246a6aea9 Merge pull request #95 from peter-evans/dev
Add path input
2020-01-10 00:35:16 +09:00
1e09ec2f22 Add path input 2020-01-10 00:11:42 +09:00
4c018f4174 Merge pull request #93 from peter-evans/renovate/zeit-ncc-0.x
Update dependency @zeit/ncc to v0.21.0
2020-01-05 08:08:32 +09:00
080aaa90aa Update dependency @zeit/ncc to v0.21.0 2020-01-04 15:45:00 +00:00
6ac64298f6 Update slash command 2020-01-04 21:44:01 +09:00
76e932f3c9 Update slash command 2020-01-04 21:38:54 +09:00
06078e295b Update examples 2020-01-04 20:02:09 +09:00
004434a414 Update examples 2020-01-04 12:08:41 +09:00
4661d6d7af Update docs 2020-01-03 19:22:46 +09:00
a972260284 Update docs 2020-01-03 17:50:03 +09:00
f1d6c2dca3 Update example screenshot 2020-01-03 14:52:21 +09:00
aa1cba4c18 Update cpr example workflow 2020-01-03 14:44:00 +09:00
9b33a4edd8 Add slash command for cpr example 2020-01-03 14:37:39 +09:00
58 changed files with 21715 additions and 7424 deletions

3
.eslintignore Normal file
View File

@ -0,0 +1,3 @@
dist/
lib/
node_modules/

19
.eslintrc.json Normal file
View 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
View File

@ -1 +0,0 @@
* @peter-evans

134
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,134 @@
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
- run: rm -rf dist
- uses: actions/download-artifact@v2
with:
name: dist
path: dist
- name: Create Pull Request
uses: peter-evans/create-pull-request@v2
with:
commit-message: Update distribution
committer: GitHub <noreply@github.com>
author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
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

View File

@ -0,0 +1,46 @@
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>
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

View File

@ -1,36 +0,0 @@
name: Create Pull Request
on:
repository_dispatch:
types: [create-pull-request]
jobs:
createPullRequest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Create report file
run: date +%s > report.txt
- name: Create Pull Request
id: cpr
uses: ./
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: Add report file
committer: Peter Evans <peter-evans@users.noreply.github.com>
title: '[Example] Add report file'
body: |
New report
- Contains *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
project: Example Project
project-column: To do
branch: example-patches
- name: Check outputs
run: |
echo "Pull Request Number - ${{ env.PULL_REQUEST_NUMBER }}"
echo "Pull Request Number - ${{ steps.cpr.outputs.pr_number }}"

View File

@ -1,19 +0,0 @@
name: Update Docker Hub Description
on:
push:
branches:
- master
paths:
- README.md
- .github/workflows/dockerhub-description.yml
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

View File

@ -9,8 +9,35 @@ jobs:
- name: Slash Command Dispatch
uses: peter-evans/slash-command-dispatch@v1
with:
token: ${{ secrets.REPO_ACCESS_TOKEN }}
reaction-token: ${{ secrets.GITHUB_TOKEN }}
commands: test, pytest, clean
permission: admin
repository: peter-evans/create-pull-request-tests
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
View 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@v2
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

5
.gitignore vendored
View File

@ -1,3 +1,4 @@
__pycache__
node_modules
lib/
node_modules/
.DS_Store

3
.prettierignore Normal file
View File

@ -0,0 +1,3 @@
dist/
lib/
node_modules/

11
.prettierrc.json Normal file
View File

@ -0,0 +1,11 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": false,
"arrowParens": "avoid",
"parser": "typescript"
}

162
README.md
View File

@ -1,4 +1,5 @@
# <img width="24" height="24" src="assets/logo.svg"> Create Pull Request
# <img width="24" height="24" src="docs/assets/logo.svg"> Create Pull Request
[![CI](https://github.com/peter-evans/create-pull-request/workflows/CI/badge.svg)](https://github.com/peter-evans/create-pull-request/actions?query=workflow%3ACI)
[![GitHub Marketplace](https://img.shields.io/badge/Marketplace-Create%20Pull%20Request-blue.svg?colorA=24292e&colorB=0366d6&style=flat&longCache=true&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAM6wAADOsB5dZE0gAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAERSURBVCiRhZG/SsMxFEZPfsVJ61jbxaF0cRQRcRJ9hlYn30IHN/+9iquDCOIsblIrOjqKgy5aKoJQj4O3EEtbPwhJbr6Te28CmdSKeqzeqr0YbfVIrTBKakvtOl5dtTkK+v4HfA9PEyBFCY9AGVgCBLaBp1jPAyfAJ/AAdIEG0dNAiyP7+K1qIfMdonZic6+WJoBJvQlvuwDqcXadUuqPA1NKAlexbRTAIMvMOCjTbMwl1LtI/6KWJ5Q6rT6Ht1MA58AX8Apcqqt5r2qhrgAXQC3CZ6i1+KMd9TRu3MvA3aH/fFPnBodb6oe6HM8+lYHrGdRXW8M9bMZtPXUji69lmf5Cmamq7quNLFZXD9Rq7v0Bpc1o/tp0fisAAAAASUVORK5CYII=)](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.
@ -18,61 +19,57 @@ Create Pull Request action will:
## Documentation
- [Concepts and guidelines](docs/concepts-guidelines.md)
- [Concepts, guidelines and advanced usage](docs/concepts-guidelines.md)
- [Examples](docs/examples.md)
- [Updating from v1](docs/updating.md)
- [Updating to v3](docs/updating.md)
## Usage
```yml
- name: Create Pull Request
uses: peter-evans/create-pull-request@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
uses: peter-evans/create-pull-request@v3
```
You can also pin to a [specific release](https://github.com/peter-evans/create-pull-request/releases) version in the format `@v2.x.x`
**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](https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line) instead of the default `GITHUB_TOKEN`.
See [this issue](https://github.com/peter-evans/create-pull-request/issues/48) for further details.
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
These inputs are *all optional*. If not set, sensible default values will be used.
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. See [Committer and author](#committer-and-author) for details. |
| `author` | The author name and email address in the format `Display Name <email@address.com>`. | Defaults to the GitHub Actions bot user. See [Committer and author](#committer-and-author) for details. |
| `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>` |
| `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 separated list of labels. | |
| `assignees` | A comma separated list of assignees (GitHub usernames). | |
| `reviewers` | A comma separated list of reviewers (GitHub usernames) to request a review from. | |
| `team-reviewers` | A comma separated list of GitHub teams to request a review from. | |
| `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. | |
| `project` | The name of the project for which a card should be created. Requires `project-column`. | |
| `project-column` | The name of the project column under which a card should be created. Requires `project`. | |
| `branch` | The branch name. See [Branch naming](#branch-naming) for details. | `create-pull-request/patch` |
| `base` | Sets the pull request base branch. | Defaults to the branch checked out in the workflow. |
| `branch-suffix` | The branch suffix type. Valid values are `random`, `timestamp` and `short-commit-hash`. See [Branch naming](#branch-naming) for details. | |
| `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` |
**Outputs**
### Action outputs
The pull request number is output as both an environment variable and a step output.
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
id: cpr
uses: peter-evans/create-pull-request@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
uses: peter-evans/create-pull-request@v3
- name: Check outputs
run: |
echo "Pull Request Number - ${{ env.PULL_REQUEST_NUMBER }}"
echo "Pull Request Number - ${{ steps.cpr.outputs.pr_number }}"
echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
```
### Checkout
@ -86,42 +83,34 @@ If there is some reason you need to use `actions/checkout@v1` the following step
- run: git checkout "${GITHUB_REF:11}"
```
### Branch naming
### Action behaviour
For branch naming there are two strategies. Create a fixed-name pull request branch that will be updated with new changes until it is merged or closed, OR, always create a new unique branch each time there are changes to be committed.
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.
#### Strategy A - Create and update a pull request branch (default)
How the action behaves:
This strategy is the default behaviour of the action. The input `branch` defaults to `create-pull-request/patch`. Changes will be committed to this branch and a pull request created. Any subsequent changes will be committed to the *same* branch and reflected in the open pull request. If the pull request is merged or closed a new one will be created. If subsequent changes cause the branch to no longer differ from the base the pull request will be automatically closed and the branch deleted.
- 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.
#### Strategy B - Always create a new pull request branch
For further details about how the action works and usage guidelines, see [Concepts, guidelines and advanced usage](docs/concepts-guidelines.md).
For this strategy there are three options to suffix the branch name.
The branch name is defined by the input `branch` and defaults to `create-pull-request/patch`. The following options are values for `branch-suffix`.
#### Alternative strategy - Always create a new pull request branch
- `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`
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.
To use this strategy, set input `branch-suffix` with one of the following options.
- `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`
- `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`
### 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.
### Committer and author
If neither `committer` or `author` inputs are supplied the action will default to making commits that appear to be made by the GitHub Actions bot user.
In most cases, where the committer and author are the same, just the committer can be set.
```yml
- name: Create Pull Request
uses: peter-evans/create-pull-request@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
committer: Peter Evans <peter-evans@users.noreply.github.com>
```
### Controlling commits
As well as relying on the action to handle uncommitted changes, you can additionally make your own commits before the action runs.
@ -141,9 +130,28 @@ As well as relying on the action to handle uncommitted changes, you can addition
- name: Uncommitted change
run: date +%s > report.txt
- name: Create Pull Request
uses: peter-evans/create-pull-request@v2
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.
### Create a project card
To create a project card for the pull request, pass the `pull-request-number` step output to [create-or-update-project-card](https://github.com/peter-evans/create-or-update-project-card) action.
```yml
- name: Create Pull Request
id: cpr
uses: peter-evans/create-pull-request@v3
- name: Create or Update Project Card
uses: peter-evans/create-or-update-project-card@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
project-name: My project
column-name: My column
issue-number: ${{ steps.cpr.outputs.pull-request-number }}
```
## Reference Example
@ -160,39 +168,45 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Create report file
- name: Make changes to pull request
run: date +%s > report.txt
- name: Create Pull Request
id: cpr
uses: peter-evans/create-pull-request@v2
uses: peter-evans/create-pull-request@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: Add report file
committer: Peter Evans <peter-evans@users.noreply.github.com>
author: Peter Evans <peter-evans@users.noreply.github.com>
title: '[Example] Add report file'
token: ${{ secrets.PAT }}
commit-message: Update report
committer: GitHub <noreply@github.com>
author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
branch: example-patches
title: '[Example] Update report'
body: |
New report
- Contains *today's* date
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
labels: |
report
automated pr
assignees: peter-evans
reviewers: peter-evans
team-reviewers: |
owners
maintainers
milestone: 1
project: Example Project
project-column: To do
branch: example-patches
- name: Check outputs
draft: false
- name: Check output
run: |
echo "Pull Request Number - ${{ env.PULL_REQUEST_NUMBER }}"
echo "Pull Request Number - ${{ steps.cpr.outputs.pr_number }}"
echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
```
This reference configuration will create pull requests that look like this:
An example based on the above reference configuration creates pull requests that look like this:
![Pull Request Example](assets/pull-request-example.png)
![Pull Request Example](docs/assets/pull-request-example.png)
## License

File diff suppressed because it is too large Load Diff

40
__test__/entrypoint.sh Executable file
View 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

View 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
View 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
View 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`
)
}
})
})

View File

@ -2,44 +2,66 @@ name: 'Create Pull Request'
description: 'Creates a pull request for changes to your repository in the actions workspace'
inputs:
token:
description: 'The GitHub authentication token'
required: true
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.'
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.'
title:
description: 'The title of the pull request.'
body:
description: 'The body of the pull request.'
labels:
description: 'A comma separated list of labels.'
assignees:
description: 'A comma separated list of assignees (GitHub usernames).'
reviewers:
description: 'A comma separated list of reviewers (GitHub usernames) to request a review from.'
team-reviewers:
description: 'A comma separated list of GitHub teams to request a review from.'
milestone:
description: 'The number of the milestone to associate this pull request with.'
project:
description: 'The name of the project for which a card should be created.'
project-column:
description: 'The name of the project column under which a card should be created.'
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>'
branch:
description: 'The pull request branch name.'
base:
description: 'The pull request base branch.'
default: 'create-pull-request/patch'
branch-suffix:
description: 'The branch suffix type.'
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:
pr_number:
pull-request-number:
description: 'The pull request number'
runs:
using: 'node12'
main: 'dist/index.js'
branding:
icon: 'git-pull-request'
icon: 'git-pull-request'
color: 'gray-dark'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 417 KiB

14373
dist/index.js vendored

File diff suppressed because it is too large Load Diff

31
dist/src/common.py vendored
View File

@ -1,31 +0,0 @@
#!/usr/bin/env python3
import random
import re
import string
def get_random_string(length=7, chars=string.ascii_lowercase + string.digits):
return "".join(random.choice(chars) for _ in range(length))
def parse_display_name_email(display_name_email):
# Parse the name and email address from a string in the following format
# Display Name <email@address.com>
pattern = re.compile(r"^([^<]+)\s*<([^>]+)>$")
# Check we have a match
match = pattern.match(display_name_email)
if match is None:
raise ValueError(
f"The format of '{display_name_email}' is not a valid email address with display name"
)
# Check that name and email are not just whitespace
name = match.group(1).strip()
email = match.group(2).strip()
if len(name) == 0 or len(email) == 0:
raise ValueError(
f"The format of '{display_name_email}' is not a valid email address with display name"
)
return name, email

View File

@ -1,145 +0,0 @@
#!/usr/bin/env python3
""" Create or Update Branch """
import common as cmn
from git import Repo, GitCommandError
import os
CHERRYPICK_EMPTY = (
"The previous cherry-pick is now empty, possibly due to conflict resolution."
)
def fetch_successful(repo, repo_url, branch):
try:
repo.git.fetch(repo_url, f"{branch}:refs/remotes/origin/{branch}")
except GitCommandError:
return False
return True
def is_ahead(repo, branch_1, branch_2):
# Return true if branch_2 is ahead of branch_1
return (
int(repo.git.rev_list("--right-only", "--count", f"{branch_1}...{branch_2}"))
> 0
)
def is_behind(repo, branch_1, branch_2):
# Return true if branch_2 is behind branch_1
return (
int(repo.git.rev_list("--left-only", "--count", f"{branch_1}...{branch_2}")) > 0
)
def is_even(repo, branch_1, branch_2):
# Return true if branch_2 is even with branch_1
return not is_ahead(repo, branch_1, branch_2) and not is_behind(
repo, branch_1, branch_2
)
def has_diff(repo, branch_1, branch_2):
diff = repo.git.diff(f"{branch_1}..{branch_2}")
return len(diff) > 0
def create_or_update_branch(repo, repo_url, commit_message, base, branch):
# Set the default return values
action = "none"
diff = False
# Get the working base. This may or may not be the actual base.
working_base = repo.git.symbolic_ref("HEAD", "--short")
# If the base is not specified it is assumed to be the working base
if base is None:
base = working_base
# Save the working base changes to a temporary branch
temp_branch = cmn.get_random_string(length=20)
repo.git.checkout("HEAD", b=temp_branch)
# Commit any uncomitted changes
if repo.is_dirty(untracked_files=True):
print(f"Uncommitted changes found. Adding a commit.")
repo.git.add("-A")
repo.git.commit(m=commit_message)
# Perform fetch and reset the working base
# Commits made during the workflow will be removed
repo.git.fetch("--force", repo_url, f"{working_base}:{working_base}")
# If the working base is not the base, rebase the temp branch commits
if working_base != base:
print(
f"Rebasing commits made to branch '{working_base}' on to base branch '{base}'"
)
# Checkout the actual base
repo.git.fetch("--force", repo_url, f"{base}:{base}")
repo.git.checkout(base)
# Cherrypick commits from the temporary branch starting from the working base
commits = repo.git.rev_list("--reverse", f"{working_base}..{temp_branch}", ".")
for commit in commits.splitlines():
try:
repo.git.cherry_pick(
"--strategy",
"recursive",
"--strategy-option",
"theirs",
f"{commit}",
)
except GitCommandError as e:
if CHERRYPICK_EMPTY not in e.stderr:
print("Unexpected error: ", e)
raise
# Reset the temp branch to the working index
repo.git.checkout("-B", temp_branch, "HEAD")
# Reset the base
repo.git.fetch("--force", repo_url, f"{base}:{base}")
# Try to fetch the pull request branch
if not fetch_successful(repo, repo_url, branch):
# The pull request branch does not exist
print(f"Pull request branch '{branch}' does not exist yet")
# Create the pull request branch
repo.git.checkout("HEAD", b=branch)
# Check if the pull request branch is ahead of the base
diff = is_ahead(repo, base, branch)
if diff:
action = "created"
print(f"Created branch '{branch}'")
else:
print(
f"Branch '{branch}' is not ahead of base '{base}' and will not be created"
)
else:
# The pull request branch exists
print(
f"Pull request branch '{branch}' already exists as remote branch 'origin/{branch}'"
)
# Checkout the pull request branch
repo.git.checkout(branch)
if has_diff(repo, branch, temp_branch):
# 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
print(f"Resetting '{branch}'")
repo.git.checkout("-B", branch, temp_branch)
# repo.git.switch("-C", branch, temp_branch)
# 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 not is_even(repo, f"origin/{branch}", branch):
action = "updated"
print(f"Updated branch '{branch}'")
else:
print(f"Branch '{branch}' is even with its remote and will not be updated")
# Check if the pull request branch is ahead of the base
diff = is_ahead(repo, base, branch)
# Delete the temporary branch
repo.git.branch("--delete", "--force", temp_branch)
return {"action": action, "diff": diff, "base": base}

View File

@ -1,126 +0,0 @@
#!/usr/bin/env python3
""" Create or Update Pull Request """
from github import Github, GithubException
import os
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 create_project_card(github_repo, project_name, project_column_name, pull_request):
# Locate the project by name
project = None
for project_item in github_repo.get_projects("all"):
if project_item.name == project_name:
project = project_item
break
if not project:
print("::error::Project not found. Unable to create project card.")
return
# Locate the column by name
column = None
for column_item in project.get_columns():
if column_item.name == project_column_name:
column = column_item
break
if not column:
print("::error::Project column not found. Unable to create project card.")
return
# Create a project card for the pull request
column.create_card(content_id=pull_request.id, content_type="PullRequest")
print(
"Added pull request #%d to project '%s' under column '%s'"
% (pull_request.number, project.name, column.name)
)
def create_or_update_pull_request(
github_token,
github_repository,
branch,
base,
title,
body,
labels,
assignees,
milestone,
reviewers,
team_reviewers,
project_name,
project_column_name,
):
# Create the pull request
github_repo = Github(github_token).get_repo(github_repository)
try:
pull_request = github_repo.create_pull(
title=title, body=body, base=base, head=branch
)
print(f"Created pull request #{pull_request.number} ({branch} => {base})")
except GithubException as e:
if e.status == 422:
# A pull request exists for this branch and base
head_branch = "{}:{}".format(github_repository.split("/")[0], branch)
# Get the pull request
pull_request = github_repo.get_pulls(
state="open", base=base, head=head_branch
)[0]
print(f"Updated pull request #{pull_request.number} ({branch} => {base})")
else:
print(str(e))
raise
# Set the output variables
os.system(f"echo ::set-env name=PULL_REQUEST_NUMBER::{pull_request.number}")
os.system(f"echo ::set-output name=pr_number::{pull_request.number}")
# Set labels, assignees and milestone
if labels is not None:
print(f"Applying labels '{labels}'")
pull_request.as_issue().edit(labels=cs_string_to_list(labels))
if assignees is not None:
print(f"Applying assignees '{assignees}'")
pull_request.as_issue().edit(assignees=cs_string_to_list(assignees))
if milestone is not None:
print(f"Applying milestone '{milestone}'")
milestone = github_repo.get_milestone(int(milestone))
pull_request.as_issue().edit(milestone=milestone)
# Set pull request reviewers
if reviewers is not None:
print(f"Requesting reviewers '{reviewers}'")
try:
pull_request.create_review_request(reviewers=cs_string_to_list(reviewers))
except GithubException as e:
# Likely caused by "Review cannot be requested from pull request author."
if e.status == 422:
print("Request reviewers failed - {}".format(e.data["message"]))
# Set pull request team reviewers
if team_reviewers is not None:
print(f"Requesting team reviewers '{team_reviewers}'")
pull_request.create_review_request(
team_reviewers=cs_string_to_list(team_reviewers)
)
# Create a project card for the pull request
if project_name is not None and project_column_name is not None:
try:
create_project_card(
github_repo, project_name, project_column_name, pull_request
)
except GithubException as e:
# Likely caused by "Project already has the associated issue."
if e.status == 422:
print(
"Create project card failed - {}".format(
e.data["errors"][0]["message"]
)
)

View File

@ -1,204 +0,0 @@
#!/usr/bin/env python3
""" Create Pull Request """
import common as cmn
import create_or_update_branch as coub
import create_or_update_pull_request as coupr
from git import Repo, GitCommandError
import json
import os
import sys
import time
# Default the committer and author to the GitHub Actions bot
DEFAULT_COMMITTER = "GitHub <noreply@github.com>"
DEFAULT_AUTHOR = (
"github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
)
DEFAULT_COMMIT_MESSAGE = "[create-pull-request] automated change"
DEFAULT_TITLE = "Changes by create-pull-request action"
DEFAULT_BODY = (
"Automated changes by "
+ "[create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub action"
)
DEFAULT_BRANCH = "create-pull-request/patch"
def get_git_config_value(repo, name):
try:
return repo.git.config("--get", name)
except GitCommandError:
return None
def git_user_config_is_set(repo):
name = get_git_config_value(repo, "user.name")
email = get_git_config_value(repo, "user.email")
if name is not None and email is not None:
print(f"Git user already configured as '{name} <{email}>'")
return True
committer_name = get_git_config_value(repo, "committer.name")
committer_email = get_git_config_value(repo, "committer.email")
author_name = get_git_config_value(repo, "author.name")
author_email = get_git_config_value(repo, "author.email")
if (
committer_name is not None
and committer_email is not None
and author_name is not None
and author_email is not None
):
print(
f"Git committer already configured as '{committer_name} <{committer_email}>'"
)
print(f"Git author already configured as '{author_name} <{author_email}>'")
return True
return False
def set_committer_author(repo, committer, author):
# If either committer or author is supplied they will be cross used
if committer is None and author is not None:
print("Supplied author will also be used as the committer.")
committer = author
if author is None and committer is not None:
print("Supplied committer will also be used as the author.")
author = committer
# If no committer/author has been supplied but user configuration already
# exists in git config we can exit and use the existing config as-is.
if committer is None and author is None:
if git_user_config_is_set(repo):
return
# Set defaults if no committer/author has been supplied
if committer is None and author is None:
committer = DEFAULT_COMMITTER
author = DEFAULT_AUTHOR
# Set git environment. This will not persist after the action completes.
committer_name, committer_email = cmn.parse_display_name_email(committer)
author_name, author_email = cmn.parse_display_name_email(author)
repo.git.update_environment(
GIT_COMMITTER_NAME=committer_name,
GIT_COMMITTER_EMAIL=committer_email,
GIT_AUTHOR_NAME=author_name,
GIT_AUTHOR_EMAIL=author_email,
)
print(f"Configured git committer as '{committer_name} <{committer_email}>'")
print(f"Configured git author as '{author_name} <{author_email}>'")
# Get required environment variables
github_token = os.environ["GITHUB_TOKEN"]
github_repository = os.environ["GITHUB_REPOSITORY"]
# Get environment variables with defaults
branch = os.getenv("CPR_BRANCH", DEFAULT_BRANCH)
commit_message = os.getenv("CPR_COMMIT_MESSAGE", DEFAULT_COMMIT_MESSAGE)
# Get environment variables with a default of 'None'
committer = os.environ.get("CPR_COMMITTER")
author = os.environ.get("CPR_AUTHOR")
base = os.environ.get("CPR_BASE")
# Set the repo to the working directory
repo = Repo(os.getcwd())
# 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
try:
working_base = repo.git.symbolic_ref("HEAD", "--short")
except GitCommandError as e:
print(f"::debug::{e.stderr}")
print(
f"::error::The checked out ref is not a valid base for a pull request. "
+ "Unable to continue. Exiting."
)
sys.exit(1)
# 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 working_base.startswith(branch):
print(
f"::error::Working base branch '{working_base}' was created by this action. "
+ "Unable to continue. Exiting."
)
sys.exit(1)
# Fetch an optional environment variable to determine the branch suffix
branch_suffix = os.environ.get("CPR_BRANCH_SUFFIX")
if branch_suffix is not None:
if branch_suffix == "short-commit-hash":
# Suffix with the short SHA1 hash
branch = "{}-{}".format(branch, repo.git.rev_parse("--short", "HEAD"))
elif branch_suffix == "timestamp":
# Suffix with the current timestamp
branch = "{}-{}".format(branch, int(time.time()))
elif branch_suffix == "random":
# Suffix with a 7 character random string
branch = "{}-{}".format(branch, cmn.get_random_string())
else:
print(
f"::error::Branch suffix '{branch_suffix}' is not a valid value. "
+ "Unable to continue. Exiting."
)
sys.exit(1)
# Output head branch
print(f"Pull request branch to create or update set to '{branch}'")
# Set the committer and author
try:
set_committer_author(repo, committer, author)
except ValueError as e:
print(f"::error::{e} " + "Unable to continue. Exiting.")
sys.exit(1)
# Set the repository URL
repo_url = f"https://x-access-token:{github_token}@github.com/{github_repository}"
# Create or update the pull request branch
result = coub.create_or_update_branch(repo, repo_url, commit_message, base, branch)
if result["action"] in ["created", "updated"]:
# The branch was created or updated
print(f"Pushing pull request branch to 'origin/{branch}'")
repo.git.push("--force", repo_url, f"HEAD:refs/heads/{branch}")
# Set the base. It would have been 'None' if not specified as an input
base = result["base"]
# If there is no longer a diff with the base delete the branch and exit
if not result["diff"]:
print(f"Branch '{branch}' no longer differs from base branch '{base}'")
print(f"Closing pull request and deleting branch '{branch}'")
repo.git.push("--delete", "--force", repo_url, f"refs/heads/{branch}")
sys.exit()
# Fetch optional environment variables with default values
title = os.getenv("CPR_TITLE", DEFAULT_TITLE)
body = os.getenv("CPR_BODY", DEFAULT_BODY)
# Create or update the pull request
coupr.create_or_update_pull_request(
github_token,
github_repository,
branch,
base,
title,
body,
os.environ.get("CPR_LABELS"),
os.environ.get("CPR_ASSIGNEES"),
os.environ.get("CPR_MILESTONE"),
os.environ.get("CPR_REVIEWERS"),
os.environ.get("CPR_TEAM_REVIEWERS"),
os.environ.get("CPR_PROJECT_NAME"),
os.environ.get("CPR_PROJECT_COLUMN_NAME"),
)

View File

@ -1,2 +0,0 @@
GitPython==3.0.5
PyGithub==1.45

View File

@ -1,52 +0,0 @@
const core = require("@actions/core");
const tc = require("@actions/tool-cache");
const path = require("path");
const semver = require("semver");
/**
* Setup for Python from the GitHub Actions tool cache
* Converted from https://github.com/actions/setup-python
*
* @param {string} versionSpec version of Python
* @param {string} arch architecture (x64|x32)
*/
let setupPython = function(versionSpec, arch) {
return new Promise((resolve, reject) => {
const IS_WINDOWS = process.platform === "win32";
// Find the version of Python we want in the tool cache
const installDir = tc.find("Python", versionSpec, arch);
core.debug(`installDir: ${installDir}`);
// Set paths
core.exportVariable("pythonLocation", installDir);
core.addPath(installDir);
if (IS_WINDOWS) {
core.addPath(path.join(installDir, "Scripts"));
} else {
core.addPath(path.join(installDir, "bin"));
}
if (IS_WINDOWS) {
// Add --user directory
// `installDir` from tool cache should look like $AGENT_TOOLSDIRECTORY/Python/<semantic version>/x64/
// So if `findLocalTool` succeeded above, we must have a conformant `installDir`
const version = path.basename(path.dirname(installDir));
const major = semver.major(version);
const minor = semver.minor(version);
const userScriptsDir = path.join(
process.env["APPDATA"] || "",
"Python",
`Python${major}${minor}`,
"Scripts"
);
core.addPath(userScriptsDir);
}
// On Linux and macOS, pip will create the --user directory and add it to PATH as needed.
resolve();
});
};
module.exports = setupPython;

View File

@ -1,39 +0,0 @@
#!/usr/bin/env python3
""" Test Common """
import common as cmn
import pytest
def test_get_random_string():
assert len(cmn.get_random_string()) == 7
assert len(cmn.get_random_string(length=20)) == 20
def test_parse_display_name_email_success():
name, email = cmn.parse_display_name_email("abc def <abc@def.com>")
assert name == "abc def"
assert email == "abc@def.com"
name, email = cmn.parse_display_name_email(
"github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
)
assert name == "github-actions[bot]"
assert email == "41898282+github-actions[bot]@users.noreply.github.com"
def test_parse_display_name_email_failure():
display_name_email = "abc@def.com"
with pytest.raises(ValueError) as e_info:
cmn.parse_display_name_email(display_name_email)
assert (
e_info.value.args[0]
== f"The format of '{display_name_email}' is not a valid email address with display name"
)
display_name_email = " < >"
with pytest.raises(ValueError) as e_info:
cmn.parse_display_name_email(display_name_email)
assert (
e_info.value.args[0]
== f"The format of '{display_name_email}' is not a valid email address with display name"
)

View File

@ -1,757 +0,0 @@
#!/usr/bin/env python3
""" Test Create or Update Branch """
import create_or_update_branch as coub
from git import Repo
import os
import pytest
import sys
import time
# Set git repo
REPO_PATH = os.getenv("COUB_REPO_PATH", os.getcwd())
repo = Repo(REPO_PATH)
# Set git environment
author_name = "github-actions[bot]"
author_email = "41898282+github-actions[bot]@users.noreply.github.com"
committer_name = "GitHub"
committer_email = "noreply@github.com"
repo.git.update_environment(
GIT_AUTHOR_NAME=author_name,
GIT_AUTHOR_EMAIL=author_email,
GIT_COMMITTER_NAME=committer_name,
GIT_COMMITTER_EMAIL=committer_email,
)
REPO_URL = repo.git.config("--get", "remote.origin.url")
TRACKED_FILE = "tracked-file.txt"
UNTRACKED_FILE = "untracked-file.txt"
DEFAULT_BRANCH = "tests/master"
NOT_BASE_BRANCH = "tests/branch-that-is-not-the-base"
NOT_EXIST_BRANCH = "tests/branch-that-does-not-exist"
COMMIT_MESSAGE = "[create-pull-request] automated change"
BRANCH = "tests/create-pull-request/patch"
BASE = DEFAULT_BRANCH
def create_tracked_change(content=None):
if content is None:
content = str(time.time())
# Create a tracked file change
with open(os.path.join(REPO_PATH, TRACKED_FILE), "w") as f:
f.write(content)
return content
def create_untracked_change(content=None):
if content is None:
content = str(time.time())
# Create an untracked file change
with open(os.path.join(REPO_PATH, UNTRACKED_FILE), "w") as f:
f.write(content)
return content
def get_tracked_content():
# Read the content of the tracked file
with open(os.path.join(REPO_PATH, TRACKED_FILE), "r") as f:
return f.read()
def get_untracked_content():
# Read the content of the untracked file
with open(os.path.join(REPO_PATH, UNTRACKED_FILE), "r") as f:
return f.read()
def create_changes(tracked_content=None, untracked_content=None):
tracked_content = create_tracked_change(tracked_content)
untracked_content = create_untracked_change(untracked_content)
return tracked_content, untracked_content
def create_commits(number=2, final_tracked_content=None, final_untracked_content=None):
for i in range(number):
commit_number = i + 1
if commit_number == number:
tracked_content, untracked_content = create_changes(
final_tracked_content, final_untracked_content
)
else:
tracked_content, untracked_content = create_changes()
repo.git.add("-A")
repo.git.commit(m=f"Commit {commit_number}")
return tracked_content, untracked_content
@pytest.fixture(scope="module", autouse=True)
def before_after_all():
print("Before all tests")
# Check there are no local changes that might be
# destroyed by running these tests
assert not repo.is_dirty(untracked_files=True)
# Create a new default branch for the test run
repo.remotes.origin.fetch()
repo.git.checkout("master")
repo.git.checkout("HEAD", b=NOT_BASE_BRANCH)
create_tracked_change()
repo.git.add("-A")
repo.git.commit(m="This commit should not appear in pr branches")
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{NOT_BASE_BRANCH}")
# Create a new default branch for the test run
repo.git.checkout("master")
repo.git.checkout("HEAD", b=DEFAULT_BRANCH)
create_tracked_change()
repo.git.add("-A")
repo.git.commit(m="Add file to be a tracked file for tests")
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{DEFAULT_BRANCH}")
yield
print("After all tests")
repo.git.checkout("master")
# Delete the "not base branch" created for the test run
repo.git.branch("--delete", "--force", NOT_BASE_BRANCH)
repo.git.push("--delete", "--force", REPO_URL, f"refs/heads/{NOT_BASE_BRANCH}")
# Delete the default branch created for the test run
repo.git.branch("--delete", "--force", DEFAULT_BRANCH)
repo.git.push("--delete", "--force", REPO_URL, f"refs/heads/{DEFAULT_BRANCH}")
def before_test():
print("Before test")
# Checkout the default branch
repo.git.checkout(DEFAULT_BRANCH)
def after_test(delete_remote=True):
print("After test")
# Output git log
print(repo.git.log("-5", pretty="oneline"))
# Delete the pull request branch if it exists
repo.git.checkout(DEFAULT_BRANCH)
print(f"Deleting {BRANCH}")
for branch in repo.branches:
if branch.name == BRANCH:
repo.git.branch("--delete", "--force", BRANCH)
break
if delete_remote:
print(f"Deleting origin/{BRANCH}")
for ref in repo.remotes.origin.refs:
if ref.name == f"origin/{BRANCH}":
repo.git.push("--delete", "--force", REPO_URL, f"refs/heads/{BRANCH}")
repo.remotes.origin.fetch("--prune")
break
@pytest.fixture(autouse=True)
def before_after_tests():
before_test()
yield
after_test()
# Tests if a branch exists and can be fetched
def coub_fetch_successful():
assert coub.fetch_successful(repo, REPO_URL, NOT_BASE_BRANCH)
assert not coub.fetch_successful(repo, REPO_URL, NOT_EXIST_BRANCH)
# Tests no changes resulting in no new branch being created
def coub_no_changes_on_create():
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "none"
# Tests create and update with a tracked file change
def coub_tracked_changes():
# Create a tracked file change
tracked_content = create_tracked_change()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Create a tracked file change
tracked_content = create_tracked_change()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "updated"
assert result["diff"]
assert get_tracked_content() == tracked_content
# Tests create and update with an untracked file change
def coub_untracked_changes():
# Create an untracked file change
untracked_content = create_untracked_change()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "created"
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Create an untracked file change
untracked_content = create_untracked_change()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "updated"
assert result["diff"]
assert get_untracked_content() == untracked_content
# Tests create and update with identical changes
# The pull request branch will not be updated
def coub_identical_changes():
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Create identical tracked and untracked file changes
create_changes(tracked_content, untracked_content)
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "none"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Tests create and update with commits on the base inbetween
def coub_commits_on_base():
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Create commits on the base
create_commits()
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{DEFAULT_BRANCH}")
repo.remotes.origin.fetch()
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "updated"
assert result["diff"]
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Tests create and then an update with no changes
# This effectively reverts the branch back to match the base and results in no diff
def coub_changes_no_diff():
# Save the default branch tracked content
default_tracked_content = get_tracked_content()
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Running with no update effectively reverts the branch back to match the base
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "updated"
assert result["diff"] == False
assert get_tracked_content() == default_tracked_content
# Tests create and update with commits on the base inbetween
# The changes on base effectively revert the branch back to match the base and results in no diff
def coub_commits_on_base_no_diff():
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Create commits on the base
tracked_content, untracked_content = create_commits()
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{DEFAULT_BRANCH}")
repo.remotes.origin.fetch()
# Create the same tracked and untracked file changes that were made to the base
create_changes(tracked_content, untracked_content)
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "updated"
assert result["diff"] == False
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Tests create and update with commits on the working base (during the workflow)
def coub_commits_on_working_base():
# Create commits on the working base
tracked_content, untracked_content = create_commits()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Create commits on the working base
tracked_content, untracked_content = create_commits()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "updated"
assert result["diff"]
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Tests create and update with changes and commits on the working base (during the workflow)
def coub_changes_and_commits_on_working_base():
# Create commits on the working base
create_commits()
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Create commits on the working base
create_commits()
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "updated"
assert result["diff"]
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Tests create and update with changes and commits on the working base (during the workflow)
# with commits on the base inbetween
def coub_changes_and_commits_on_base_and_working_base():
# Create commits on the working base
create_commits()
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Create commits on the base
create_commits()
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{DEFAULT_BRANCH}")
repo.remotes.origin.fetch()
# Create commits on the working base
create_commits()
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "updated"
assert result["diff"]
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Working Base is Not Base (WBNB)
# Tests no changes resulting in no new branch being created
def coub_wbnb_no_changes_on_create():
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "none"
# Working Base is Not Base (WBNB)
# Tests create and update with a tracked file change
def coub_wbnb_tracked_changes():
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create a tracked file change
tracked_content = create_tracked_change()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create a tracked file change
tracked_content = create_tracked_change()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "updated"
assert result["diff"]
assert get_tracked_content() == tracked_content
# Working Base is Not Base (WBNB)
# Tests create and update with an untracked file change
def coub_wbnb_untracked_changes():
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create an untracked file change
untracked_content = create_untracked_change()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "created"
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create an untracked file change
untracked_content = create_untracked_change()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "updated"
assert result["diff"]
assert get_untracked_content() == untracked_content
# Working Base is Not Base (WBNB)
# Tests create and update with identical changes
# The pull request branch will not be updated
def coub_wbnb_identical_changes():
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create identical tracked and untracked file changes
create_changes(tracked_content, untracked_content)
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "none"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Working Base is Not Base (WBNB)
# Tests create and update with commits on the base inbetween
def coub_wbnb_commits_on_base():
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Create commits on the base
create_commits()
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{DEFAULT_BRANCH}")
repo.remotes.origin.fetch()
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "updated"
assert result["diff"]
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Working Base is Not Base (WBNB)
# Tests create and then an update with no changes
# This effectively reverts the branch back to match the base and results in no diff
def coub_wbnb_changes_no_diff():
# Save the default branch tracked content
default_tracked_content = get_tracked_content()
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Running with no update effectively reverts the branch back to match the base
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "updated"
assert result["diff"] == False
assert get_tracked_content() == default_tracked_content
# Working Base is Not Base (WBNB)
# Tests create and update with commits on the base inbetween
# The changes on base effectively revert the branch back to match the base and results in no diff
# This scenario will cause cherrypick to fail due to an empty commit.
# The commit is empty because the changes now exist on the base.
def coub_wbnb_commits_on_base_no_diff():
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Create commits on the base
tracked_content, untracked_content = create_commits()
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{DEFAULT_BRANCH}")
repo.remotes.origin.fetch()
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create the same tracked and untracked file changes that were made to the base
create_changes(tracked_content, untracked_content)
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "updated"
assert result["diff"] == False
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Working Base is Not Base (WBNB)
# Tests create and update with commits on the working base (during the workflow)
def coub_wbnb_commits_on_working_base():
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create commits on the working base
tracked_content, untracked_content = create_commits()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create commits on the working base
tracked_content, untracked_content = create_commits()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "updated"
assert result["diff"]
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Working Base is Not Base (WBNB)
# Tests create and update with changes and commits on the working base (during the workflow)
def coub_wbnb_changes_and_commits_on_working_base():
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create commits on the working base
create_commits()
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create commits on the working base
create_commits()
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "updated"
assert result["diff"]
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Working Base is Not Base (WBNB)
# Tests create and update with changes and commits on the working base (during the workflow)
# with commits on the base inbetween
def coub_wbnb_changes_and_commits_on_base_and_working_base():
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create commits on the working base
create_commits()
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Create commits on the base
create_commits()
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{DEFAULT_BRANCH}")
repo.remotes.origin.fetch()
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create commits on the working base
create_commits()
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "updated"
assert result["diff"]
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# pytest -v -s ~/git/create-pull-request/src
test_coub_fetch_successful = coub_fetch_successful
test_coub_no_changes_on_create = coub_no_changes_on_create
test_coub_tracked_changes = coub_tracked_changes
test_coub_untracked_changes = coub_untracked_changes
test_coub_identical_changes = coub_identical_changes
test_coub_commits_on_base = coub_commits_on_base
test_coub_changes_no_diff = coub_changes_no_diff
test_coub_commits_on_base_no_diff = coub_commits_on_base_no_diff
test_coub_commits_on_working_base = coub_commits_on_working_base
test_coub_changes_and_commits_on_working_base = coub_changes_and_commits_on_working_base
test_coub_changes_and_commits_on_base_and_working_base = (
coub_changes_and_commits_on_base_and_working_base
)
# WBNB
test_coub_wbnb_no_changes_on_create = coub_wbnb_no_changes_on_create
test_coub_wbnb_tracked_changes = coub_wbnb_tracked_changes
test_coub_wbnb_untracked_changes = coub_wbnb_untracked_changes
test_coub_wbnb_identical_changes = coub_wbnb_identical_changes
test_coub_wbnb_commits_on_base = coub_wbnb_commits_on_base
test_coub_wbnb_changes_no_diff = coub_wbnb_changes_no_diff
test_coub_wbnb_commits_on_base_no_diff = coub_wbnb_commits_on_base_no_diff
test_coub_wbnb_commits_on_working_base = coub_wbnb_commits_on_working_base
test_coub_wbnb_changes_and_commits_on_working_base = (
coub_wbnb_changes_and_commits_on_working_base
)
test_coub_wbnb_changes_and_commits_on_base_and_working_base = (
coub_wbnb_changes_and_commits_on_base_and_working_base
)

View File

Before

Width:  |  Height:  |  Size: 207 KiB

After

Width:  |  Height:  |  Size: 207 KiB

View File

Before

Width:  |  Height:  |  Size: 416 B

After

Width:  |  Height:  |  Size: 416 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

View File

@ -1,6 +1,6 @@
# Concepts and guidelines
# Concepts, guidelines and advanced usage
This document covers terminology, how the action works, and general usage guidelines.
This document covers terminology, how the action works, general usage guidelines, and advanced usage.
- [Terminology](#terminology)
- [Events and checkout](#events-and-checkout)
@ -9,6 +9,15 @@ This document covers terminology, how the action works, and general usage guidel
- [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
@ -30,7 +39,7 @@ The default can be overridden by specifying a `ref` on checkout.
```yml
- uses: actions/checkout@v2
with:
ref: master
ref: develop
```
## How the action works
@ -45,7 +54,7 @@ Workflow steps:
The following git diagram shows how the action creates and updates a pull request branch.
![Create Pull Request GitGraph](../assets/cpr-gitgraph.png)
![Create Pull Request GitGraph](assets/cpr-gitgraph.png)
## Guidelines
@ -82,7 +91,7 @@ Workflows triggered by `pull_request` events will by default check out a [merge
### Restrictions on forked repositories
GitHub Actions have imposed restrictions on events triggered by a forked repository. For example, the `pull_request` event triggered by a fork opening a pull request in the upstream repository.
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.
@ -93,7 +102,7 @@ GitHub Actions have imposed restrictions on events triggered by a forked reposit
[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 the action will be unable to commit changes to a branch.
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.
@ -105,3 +114,251 @@ jobs:
# 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
```

View File

@ -1,8 +1,11 @@
# 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)
- [Keep Go up to date](#keep-go-up-to-date)
- [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)
@ -16,72 +19,168 @@
- [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-deps:
update-dep:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '10.x'
node-version: '12.x'
- name: Update dependencies
id: vars
run: |
npm install -g npm-check-updates
ncu -u
npx -p npm-check-updates ncu -u
npm install
- name: Create Pull Request
uses: peter-evans/create-pull-request@v2
uses: peter-evans/create-pull-request@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: update dependencies
title: Automated Dependency Updates
body: This is an auto-generated PR with dependency updates.
branch: dep-updates
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
```
### Keep Go up to date
Keep Go up to date with [ensure-latest-go](https://github.com/jmhodges/ensure-latest-go) action.
The above workflow works best in combination with a build workflow triggered on `push` and `pull_request`.
```yml
name: Keeping Go up to date
name: CI
on:
schedule:
- cron: 47 4 * * *
push:
branches:
- master
branches: [master]
pull_request:
branches: [master]
jobs:
fresh_go:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
ref: master
- uses: jmhodges/ensure-latest-go@v1.0.2
id: ensure_go
- run: echo "##[set-output name=pr_title;]update to latest Go release ${{ steps.ensure_go.outputs.go_version}}"
id: pr_title_maker
- name: Create pull request
uses: peter-evans/create-pull-request@v2
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:
token: ${{ secrets.GITHUB_TOKEN }}
title: ${{ steps.pr_title_maker.outputs.pr_title }}
body: Auto-generated pull request created by the GitHub Actions [create-pull-request](https://github.com/peter-evans/create-pull-request) and [ensure-latest-go](https://github.com/jmhodges/ensure-latest-go).
commit-message: ${{ steps.pr_title_maker.outputs.pr_title }}
branch: ensure-latest-go/patch-${{ steps.ensure_go.outputs.go_version }}
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
@ -126,9 +225,8 @@ jobs:
# Update current release
echo ${{ steps.swagger-ui.outputs.release_tag }} > swagger-ui.version
- name: Create Pull Request
uses: peter-evans/create-pull-request@v2
uses: peter-evans/create-pull-request@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: Update swagger-ui to ${{ steps.swagger-ui.outputs.release_tag }}
title: Update SwaggerUI to ${{ steps.swagger-ui.outputs.release_tag }}
body: |
@ -169,9 +267,8 @@ jobs:
--domains quotes.toscrape.com \
http://quotes.toscrape.com/
- name: Create Pull Request
uses: peter-evans/create-pull-request@v2
uses: peter-evans/create-pull-request@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: update local website copy
title: Automated Updates to Local Website Copy
body: This is an auto-generated PR with website updates.
@ -215,7 +312,7 @@ An `on: repository_dispatch` workflow can be triggered from another workflow wit
```yml
- name: Repository Dispatch
uses: peter-evans/repository-dispatch@v1.0.0
uses: peter-evans/repository-dispatch@v1
with:
token: ${{ secrets.REPO_ACCESS_TOKEN }}
repository: username/my-repo
@ -225,7 +322,7 @@ An `on: repository_dispatch` workflow can be triggered from another workflow wit
## Use case: Create a pull request to modify/fix pull requests
**Note**: While the following approach does work in some cases, 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.
**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.
@ -256,7 +353,7 @@ jobs:
ref: ${{ github.head_ref }}
- name: autopep8
id: autopep8
uses: peter-evans/autopep8@v1.1.0
uses: peter-evans/autopep8@v1
with:
args: --exit-code --recursive --in-place --aggressive --aggressive .
- name: Set autopep8 branch name
@ -264,9 +361,8 @@ jobs:
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@v2
uses: peter-evans/create-pull-request@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: autopep8 action fixes
title: Fixes by autopep8 action
body: This is an auto-generated PR with fixes by autopep8.
@ -323,9 +419,8 @@ The recommended method is to use [`set-output`](https://help.github.com/en/githu
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@v2
uses: peter-evans/create-pull-request@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
title: ${{ steps.vars.outputs.pr_title }}
body: ${{ steps.vars.outputs.pr_body }}
```
@ -339,9 +434,8 @@ Alternatively, [`set-env`](https://help.github.com/en/github/automating-your-wor
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@v2
uses: peter-evans/create-pull-request@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
title: ${{ env.PULL_REQUEST_TITLE }}
body: ${{ env.PULL_REQUEST_BODY }}
```

View File

@ -1,11 +1,59 @@
# Updating from `v1` to `v2`
## Updating from `v2` to `v3`
## Breaking changes
### Breaking changes
- The `author` input now defaults to the user who triggered the workflow run. This default is set via [action.yml](../action.yml) as `${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>`, where `github.actor` is the GitHub user account associated with the run. For example, `peter-evans <peter-evans@users.noreply.github.com>`.
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}"
@ -18,7 +66,7 @@
If neither `author` or `committer` are set the action will default to making commits as the GitHub Actions bot user.
## New features
### 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.

View File

@ -1,68 +0,0 @@
const { inspect } = require("util");
const core = require("@actions/core");
const exec = require("@actions/exec");
const setupPython = require("./src/setup-python");
async function run() {
try {
// Allows ncc to find assets to be included in the distribution
const src = __dirname + "/src";
core.debug(`src: ${src}`);
// Setup Python from the tool cache
setupPython("3.8.0", "x64");
// Install requirements
await exec.exec("pip", [
"install",
"--requirement",
`${src}/requirements.txt`
]);
// Fetch action inputs
const inputs = {
token: core.getInput("token"),
commitMessage: core.getInput("commit-message"),
committer: core.getInput("committer"),
author: core.getInput("author"),
title: core.getInput("title"),
body: core.getInput("body"),
labels: core.getInput("labels"),
assignees: core.getInput("assignees"),
reviewers: core.getInput("reviewers"),
teamReviewers: core.getInput("team-reviewers"),
milestone: core.getInput("milestone"),
project: core.getInput("project"),
projectColumn: core.getInput("project-column"),
branch: core.getInput("branch"),
base: core.getInput("base"),
branchSuffix: core.getInput("branch-suffix"),
};
core.debug(`Inputs: ${inspect(inputs)}`);
// Set environment variables from inputs.
if (inputs.token) process.env.GITHUB_TOKEN = inputs.token;
if (inputs.commitMessage) process.env.CPR_COMMIT_MESSAGE = inputs.commitMessage;
if (inputs.committer) process.env.CPR_COMMITTER = inputs.committer;
if (inputs.author) process.env.CPR_AUTHOR = inputs.author;
if (inputs.title) process.env.CPR_TITLE = inputs.title;
if (inputs.body) process.env.CPR_BODY = inputs.body;
if (inputs.labels) process.env.CPR_LABELS = inputs.labels;
if (inputs.assignees) process.env.CPR_ASSIGNEES = inputs.assignees;
if (inputs.reviewers) process.env.CPR_REVIEWERS = inputs.reviewers;
if (inputs.teamReviewers) process.env.CPR_TEAM_REVIEWERS = inputs.teamReviewers;
if (inputs.milestone) process.env.CPR_MILESTONE = inputs.milestone;
if (inputs.project) process.env.CPR_PROJECT_NAME = inputs.project;
if (inputs.projectColumn) process.env.CPR_PROJECT_COLUMN_NAME = inputs.projectColumn;
if (inputs.branch) process.env.CPR_BRANCH = inputs.branch;
if (inputs.base) process.env.CPR_BASE = inputs.base;
if (inputs.branchSuffix) process.env.CPR_BRANCH_SUFFIX = inputs.branchSuffix;
// Execute python script
await exec.exec("python", [`${src}/create_pull_request.py`]);
} catch (error) {
core.setFailed(error.message);
}
}
run();

11
jest.config.js Normal file
View 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
}

8058
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,27 @@
{
"name": "create-pull-request",
"version": "2.0.0",
"version": "3.0.0",
"private": true,
"description": "Creates a pull request for changes to your repository in the actions workspace",
"main": "index.js",
"main": "lib/main.js",
"scripts": {
"package": "ncc build index.js -o dist"
"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": [],
"keywords": [
"actions",
"pull",
"request"
],
"author": "Peter Evans",
"license": "MIT",
"bugs": {
@ -18,11 +29,27 @@
},
"homepage": "https://github.com/peter-evans/create-pull-request",
"dependencies": {
"@actions/core": "^1.1.1",
"@actions/exec": "^1.0.1",
"@actions/tool-cache": "^1.1.2"
"@actions/core": "1.2.4",
"@actions/exec": "1.0.4",
"@actions/tool-cache": "1.3.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": {
"@zeit/ncc": "0.20.5"
"@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"
}
}

View File

@ -1,5 +0,0 @@
{
"extends": [
"config:base"
]
}

View File

@ -1,31 +0,0 @@
#!/usr/bin/env python3
import random
import re
import string
def get_random_string(length=7, chars=string.ascii_lowercase + string.digits):
return "".join(random.choice(chars) for _ in range(length))
def parse_display_name_email(display_name_email):
# Parse the name and email address from a string in the following format
# Display Name <email@address.com>
pattern = re.compile(r"^([^<]+)\s*<([^>]+)>$")
# Check we have a match
match = pattern.match(display_name_email)
if match is None:
raise ValueError(
f"The format of '{display_name_email}' is not a valid email address with display name"
)
# Check that name and email are not just whitespace
name = match.group(1).strip()
email = match.group(2).strip()
if len(name) == 0 or len(email) == 0:
raise ValueError(
f"The format of '{display_name_email}' is not a valid email address with display name"
)
return name, email

View File

@ -0,0 +1,195 @@
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
): 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'])
await git.commit(['-m', commitMessage])
}
// 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
}

218
src/create-pull-request.ts Normal file
View File

@ -0,0 +1,218 @@
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
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
)
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()
}
}

View File

@ -1,145 +0,0 @@
#!/usr/bin/env python3
""" Create or Update Branch """
import common as cmn
from git import Repo, GitCommandError
import os
CHERRYPICK_EMPTY = (
"The previous cherry-pick is now empty, possibly due to conflict resolution."
)
def fetch_successful(repo, repo_url, branch):
try:
repo.git.fetch(repo_url, f"{branch}:refs/remotes/origin/{branch}")
except GitCommandError:
return False
return True
def is_ahead(repo, branch_1, branch_2):
# Return true if branch_2 is ahead of branch_1
return (
int(repo.git.rev_list("--right-only", "--count", f"{branch_1}...{branch_2}"))
> 0
)
def is_behind(repo, branch_1, branch_2):
# Return true if branch_2 is behind branch_1
return (
int(repo.git.rev_list("--left-only", "--count", f"{branch_1}...{branch_2}")) > 0
)
def is_even(repo, branch_1, branch_2):
# Return true if branch_2 is even with branch_1
return not is_ahead(repo, branch_1, branch_2) and not is_behind(
repo, branch_1, branch_2
)
def has_diff(repo, branch_1, branch_2):
diff = repo.git.diff(f"{branch_1}..{branch_2}")
return len(diff) > 0
def create_or_update_branch(repo, repo_url, commit_message, base, branch):
# Set the default return values
action = "none"
diff = False
# Get the working base. This may or may not be the actual base.
working_base = repo.git.symbolic_ref("HEAD", "--short")
# If the base is not specified it is assumed to be the working base
if base is None:
base = working_base
# Save the working base changes to a temporary branch
temp_branch = cmn.get_random_string(length=20)
repo.git.checkout("HEAD", b=temp_branch)
# Commit any uncomitted changes
if repo.is_dirty(untracked_files=True):
print(f"Uncommitted changes found. Adding a commit.")
repo.git.add("-A")
repo.git.commit(m=commit_message)
# Perform fetch and reset the working base
# Commits made during the workflow will be removed
repo.git.fetch("--force", repo_url, f"{working_base}:{working_base}")
# If the working base is not the base, rebase the temp branch commits
if working_base != base:
print(
f"Rebasing commits made to branch '{working_base}' on to base branch '{base}'"
)
# Checkout the actual base
repo.git.fetch("--force", repo_url, f"{base}:{base}")
repo.git.checkout(base)
# Cherrypick commits from the temporary branch starting from the working base
commits = repo.git.rev_list("--reverse", f"{working_base}..{temp_branch}", ".")
for commit in commits.splitlines():
try:
repo.git.cherry_pick(
"--strategy",
"recursive",
"--strategy-option",
"theirs",
f"{commit}",
)
except GitCommandError as e:
if CHERRYPICK_EMPTY not in e.stderr:
print("Unexpected error: ", e)
raise
# Reset the temp branch to the working index
repo.git.checkout("-B", temp_branch, "HEAD")
# Reset the base
repo.git.fetch("--force", repo_url, f"{base}:{base}")
# Try to fetch the pull request branch
if not fetch_successful(repo, repo_url, branch):
# The pull request branch does not exist
print(f"Pull request branch '{branch}' does not exist yet")
# Create the pull request branch
repo.git.checkout("HEAD", b=branch)
# Check if the pull request branch is ahead of the base
diff = is_ahead(repo, base, branch)
if diff:
action = "created"
print(f"Created branch '{branch}'")
else:
print(
f"Branch '{branch}' is not ahead of base '{base}' and will not be created"
)
else:
# The pull request branch exists
print(
f"Pull request branch '{branch}' already exists as remote branch 'origin/{branch}'"
)
# Checkout the pull request branch
repo.git.checkout(branch)
if has_diff(repo, branch, temp_branch):
# 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
print(f"Resetting '{branch}'")
repo.git.checkout("-B", branch, temp_branch)
# repo.git.switch("-C", branch, temp_branch)
# 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 not is_even(repo, f"origin/{branch}", branch):
action = "updated"
print(f"Updated branch '{branch}'")
else:
print(f"Branch '{branch}' is even with its remote and will not be updated")
# Check if the pull request branch is ahead of the base
diff = is_ahead(repo, base, branch)
# Delete the temporary branch
repo.git.branch("--delete", "--force", temp_branch)
return {"action": action, "diff": diff, "base": base}

View File

@ -1,126 +0,0 @@
#!/usr/bin/env python3
""" Create or Update Pull Request """
from github import Github, GithubException
import os
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 create_project_card(github_repo, project_name, project_column_name, pull_request):
# Locate the project by name
project = None
for project_item in github_repo.get_projects("all"):
if project_item.name == project_name:
project = project_item
break
if not project:
print("::error::Project not found. Unable to create project card.")
return
# Locate the column by name
column = None
for column_item in project.get_columns():
if column_item.name == project_column_name:
column = column_item
break
if not column:
print("::error::Project column not found. Unable to create project card.")
return
# Create a project card for the pull request
column.create_card(content_id=pull_request.id, content_type="PullRequest")
print(
"Added pull request #%d to project '%s' under column '%s'"
% (pull_request.number, project.name, column.name)
)
def create_or_update_pull_request(
github_token,
github_repository,
branch,
base,
title,
body,
labels,
assignees,
milestone,
reviewers,
team_reviewers,
project_name,
project_column_name,
):
# Create the pull request
github_repo = Github(github_token).get_repo(github_repository)
try:
pull_request = github_repo.create_pull(
title=title, body=body, base=base, head=branch
)
print(f"Created pull request #{pull_request.number} ({branch} => {base})")
except GithubException as e:
if e.status == 422:
# A pull request exists for this branch and base
head_branch = "{}:{}".format(github_repository.split("/")[0], branch)
# Get the pull request
pull_request = github_repo.get_pulls(
state="open", base=base, head=head_branch
)[0]
print(f"Updated pull request #{pull_request.number} ({branch} => {base})")
else:
print(str(e))
raise
# Set the output variables
os.system(f"echo ::set-env name=PULL_REQUEST_NUMBER::{pull_request.number}")
os.system(f"echo ::set-output name=pr_number::{pull_request.number}")
# Set labels, assignees and milestone
if labels is not None:
print(f"Applying labels '{labels}'")
pull_request.as_issue().edit(labels=cs_string_to_list(labels))
if assignees is not None:
print(f"Applying assignees '{assignees}'")
pull_request.as_issue().edit(assignees=cs_string_to_list(assignees))
if milestone is not None:
print(f"Applying milestone '{milestone}'")
milestone = github_repo.get_milestone(int(milestone))
pull_request.as_issue().edit(milestone=milestone)
# Set pull request reviewers
if reviewers is not None:
print(f"Requesting reviewers '{reviewers}'")
try:
pull_request.create_review_request(reviewers=cs_string_to_list(reviewers))
except GithubException as e:
# Likely caused by "Review cannot be requested from pull request author."
if e.status == 422:
print("Request reviewers failed - {}".format(e.data["message"]))
# Set pull request team reviewers
if team_reviewers is not None:
print(f"Requesting team reviewers '{team_reviewers}'")
pull_request.create_review_request(
team_reviewers=cs_string_to_list(team_reviewers)
)
# Create a project card for the pull request
if project_name is not None and project_column_name is not None:
try:
create_project_card(
github_repo, project_name, project_column_name, pull_request
)
except GithubException as e:
# Likely caused by "Project already has the associated issue."
if e.status == 422:
print(
"Create project card failed - {}".format(
e.data["errors"][0]["message"]
)
)

View File

@ -1,204 +0,0 @@
#!/usr/bin/env python3
""" Create Pull Request """
import common as cmn
import create_or_update_branch as coub
import create_or_update_pull_request as coupr
from git import Repo, GitCommandError
import json
import os
import sys
import time
# Default the committer and author to the GitHub Actions bot
DEFAULT_COMMITTER = "GitHub <noreply@github.com>"
DEFAULT_AUTHOR = (
"github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
)
DEFAULT_COMMIT_MESSAGE = "[create-pull-request] automated change"
DEFAULT_TITLE = "Changes by create-pull-request action"
DEFAULT_BODY = (
"Automated changes by "
+ "[create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub action"
)
DEFAULT_BRANCH = "create-pull-request/patch"
def get_git_config_value(repo, name):
try:
return repo.git.config("--get", name)
except GitCommandError:
return None
def git_user_config_is_set(repo):
name = get_git_config_value(repo, "user.name")
email = get_git_config_value(repo, "user.email")
if name is not None and email is not None:
print(f"Git user already configured as '{name} <{email}>'")
return True
committer_name = get_git_config_value(repo, "committer.name")
committer_email = get_git_config_value(repo, "committer.email")
author_name = get_git_config_value(repo, "author.name")
author_email = get_git_config_value(repo, "author.email")
if (
committer_name is not None
and committer_email is not None
and author_name is not None
and author_email is not None
):
print(
f"Git committer already configured as '{committer_name} <{committer_email}>'"
)
print(f"Git author already configured as '{author_name} <{author_email}>'")
return True
return False
def set_committer_author(repo, committer, author):
# If either committer or author is supplied they will be cross used
if committer is None and author is not None:
print("Supplied author will also be used as the committer.")
committer = author
if author is None and committer is not None:
print("Supplied committer will also be used as the author.")
author = committer
# If no committer/author has been supplied but user configuration already
# exists in git config we can exit and use the existing config as-is.
if committer is None and author is None:
if git_user_config_is_set(repo):
return
# Set defaults if no committer/author has been supplied
if committer is None and author is None:
committer = DEFAULT_COMMITTER
author = DEFAULT_AUTHOR
# Set git environment. This will not persist after the action completes.
committer_name, committer_email = cmn.parse_display_name_email(committer)
author_name, author_email = cmn.parse_display_name_email(author)
repo.git.update_environment(
GIT_COMMITTER_NAME=committer_name,
GIT_COMMITTER_EMAIL=committer_email,
GIT_AUTHOR_NAME=author_name,
GIT_AUTHOR_EMAIL=author_email,
)
print(f"Configured git committer as '{committer_name} <{committer_email}>'")
print(f"Configured git author as '{author_name} <{author_email}>'")
# Get required environment variables
github_token = os.environ["GITHUB_TOKEN"]
github_repository = os.environ["GITHUB_REPOSITORY"]
# Get environment variables with defaults
branch = os.getenv("CPR_BRANCH", DEFAULT_BRANCH)
commit_message = os.getenv("CPR_COMMIT_MESSAGE", DEFAULT_COMMIT_MESSAGE)
# Get environment variables with a default of 'None'
committer = os.environ.get("CPR_COMMITTER")
author = os.environ.get("CPR_AUTHOR")
base = os.environ.get("CPR_BASE")
# Set the repo to the working directory
repo = Repo(os.getcwd())
# 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
try:
working_base = repo.git.symbolic_ref("HEAD", "--short")
except GitCommandError as e:
print(f"::debug::{e.stderr}")
print(
f"::error::The checked out ref is not a valid base for a pull request. "
+ "Unable to continue. Exiting."
)
sys.exit(1)
# 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 working_base.startswith(branch):
print(
f"::error::Working base branch '{working_base}' was created by this action. "
+ "Unable to continue. Exiting."
)
sys.exit(1)
# Fetch an optional environment variable to determine the branch suffix
branch_suffix = os.environ.get("CPR_BRANCH_SUFFIX")
if branch_suffix is not None:
if branch_suffix == "short-commit-hash":
# Suffix with the short SHA1 hash
branch = "{}-{}".format(branch, repo.git.rev_parse("--short", "HEAD"))
elif branch_suffix == "timestamp":
# Suffix with the current timestamp
branch = "{}-{}".format(branch, int(time.time()))
elif branch_suffix == "random":
# Suffix with a 7 character random string
branch = "{}-{}".format(branch, cmn.get_random_string())
else:
print(
f"::error::Branch suffix '{branch_suffix}' is not a valid value. "
+ "Unable to continue. Exiting."
)
sys.exit(1)
# Output head branch
print(f"Pull request branch to create or update set to '{branch}'")
# Set the committer and author
try:
set_committer_author(repo, committer, author)
except ValueError as e:
print(f"::error::{e} " + "Unable to continue. Exiting.")
sys.exit(1)
# Set the repository URL
repo_url = f"https://x-access-token:{github_token}@github.com/{github_repository}"
# Create or update the pull request branch
result = coub.create_or_update_branch(repo, repo_url, commit_message, base, branch)
if result["action"] in ["created", "updated"]:
# The branch was created or updated
print(f"Pushing pull request branch to 'origin/{branch}'")
repo.git.push("--force", repo_url, f"HEAD:refs/heads/{branch}")
# Set the base. It would have been 'None' if not specified as an input
base = result["base"]
# If there is no longer a diff with the base delete the branch and exit
if not result["diff"]:
print(f"Branch '{branch}' no longer differs from base branch '{base}'")
print(f"Closing pull request and deleting branch '{branch}'")
repo.git.push("--delete", "--force", repo_url, f"refs/heads/{branch}")
sys.exit()
# Fetch optional environment variables with default values
title = os.getenv("CPR_TITLE", DEFAULT_TITLE)
body = os.getenv("CPR_BODY", DEFAULT_BODY)
# Create or update the pull request
coupr.create_or_update_pull_request(
github_token,
github_repository,
branch,
base,
title,
body,
os.environ.get("CPR_LABELS"),
os.environ.get("CPR_ASSIGNEES"),
os.environ.get("CPR_MILESTONE"),
os.environ.get("CPR_REVIEWERS"),
os.environ.get("CPR_TEAM_REVIEWERS"),
os.environ.get("CPR_PROJECT_NAME"),
os.environ.get("CPR_PROJECT_COLUMN_NAME"),
)

126
src/git-auth-helper.ts Normal file
View 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
View 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
View 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
}
}
}
}
}

35
src/main.ts Normal file
View File

@ -0,0 +1,35 @@
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'),
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
View 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)

View File

@ -1,2 +0,0 @@
GitPython==3.0.5
PyGithub==1.45

View File

@ -1,52 +0,0 @@
const core = require("@actions/core");
const tc = require("@actions/tool-cache");
const path = require("path");
const semver = require("semver");
/**
* Setup for Python from the GitHub Actions tool cache
* Converted from https://github.com/actions/setup-python
*
* @param {string} versionSpec version of Python
* @param {string} arch architecture (x64|x32)
*/
let setupPython = function(versionSpec, arch) {
return new Promise((resolve, reject) => {
const IS_WINDOWS = process.platform === "win32";
// Find the version of Python we want in the tool cache
const installDir = tc.find("Python", versionSpec, arch);
core.debug(`installDir: ${installDir}`);
// Set paths
core.exportVariable("pythonLocation", installDir);
core.addPath(installDir);
if (IS_WINDOWS) {
core.addPath(path.join(installDir, "Scripts"));
} else {
core.addPath(path.join(installDir, "bin"));
}
if (IS_WINDOWS) {
// Add --user directory
// `installDir` from tool cache should look like $AGENT_TOOLSDIRECTORY/Python/<semantic version>/x64/
// So if `findLocalTool` succeeded above, we must have a conformant `installDir`
const version = path.basename(path.dirname(installDir));
const major = semver.major(version);
const minor = semver.minor(version);
const userScriptsDir = path.join(
process.env["APPDATA"] || "",
"Python",
`Python${major}${minor}`,
"Scripts"
);
core.addPath(userScriptsDir);
}
// On Linux and macOS, pip will create the --user directory and add it to PATH as needed.
resolve();
});
};
module.exports = setupPython;

View File

@ -1,39 +0,0 @@
#!/usr/bin/env python3
""" Test Common """
import common as cmn
import pytest
def test_get_random_string():
assert len(cmn.get_random_string()) == 7
assert len(cmn.get_random_string(length=20)) == 20
def test_parse_display_name_email_success():
name, email = cmn.parse_display_name_email("abc def <abc@def.com>")
assert name == "abc def"
assert email == "abc@def.com"
name, email = cmn.parse_display_name_email(
"github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
)
assert name == "github-actions[bot]"
assert email == "41898282+github-actions[bot]@users.noreply.github.com"
def test_parse_display_name_email_failure():
display_name_email = "abc@def.com"
with pytest.raises(ValueError) as e_info:
cmn.parse_display_name_email(display_name_email)
assert (
e_info.value.args[0]
== f"The format of '{display_name_email}' is not a valid email address with display name"
)
display_name_email = " < >"
with pytest.raises(ValueError) as e_info:
cmn.parse_display_name_email(display_name_email)
assert (
e_info.value.args[0]
== f"The format of '{display_name_email}' is not a valid email address with display name"
)

View File

@ -1,757 +0,0 @@
#!/usr/bin/env python3
""" Test Create or Update Branch """
import create_or_update_branch as coub
from git import Repo
import os
import pytest
import sys
import time
# Set git repo
REPO_PATH = os.getenv("COUB_REPO_PATH", os.getcwd())
repo = Repo(REPO_PATH)
# Set git environment
author_name = "github-actions[bot]"
author_email = "41898282+github-actions[bot]@users.noreply.github.com"
committer_name = "GitHub"
committer_email = "noreply@github.com"
repo.git.update_environment(
GIT_AUTHOR_NAME=author_name,
GIT_AUTHOR_EMAIL=author_email,
GIT_COMMITTER_NAME=committer_name,
GIT_COMMITTER_EMAIL=committer_email,
)
REPO_URL = repo.git.config("--get", "remote.origin.url")
TRACKED_FILE = "tracked-file.txt"
UNTRACKED_FILE = "untracked-file.txt"
DEFAULT_BRANCH = "tests/master"
NOT_BASE_BRANCH = "tests/branch-that-is-not-the-base"
NOT_EXIST_BRANCH = "tests/branch-that-does-not-exist"
COMMIT_MESSAGE = "[create-pull-request] automated change"
BRANCH = "tests/create-pull-request/patch"
BASE = DEFAULT_BRANCH
def create_tracked_change(content=None):
if content is None:
content = str(time.time())
# Create a tracked file change
with open(os.path.join(REPO_PATH, TRACKED_FILE), "w") as f:
f.write(content)
return content
def create_untracked_change(content=None):
if content is None:
content = str(time.time())
# Create an untracked file change
with open(os.path.join(REPO_PATH, UNTRACKED_FILE), "w") as f:
f.write(content)
return content
def get_tracked_content():
# Read the content of the tracked file
with open(os.path.join(REPO_PATH, TRACKED_FILE), "r") as f:
return f.read()
def get_untracked_content():
# Read the content of the untracked file
with open(os.path.join(REPO_PATH, UNTRACKED_FILE), "r") as f:
return f.read()
def create_changes(tracked_content=None, untracked_content=None):
tracked_content = create_tracked_change(tracked_content)
untracked_content = create_untracked_change(untracked_content)
return tracked_content, untracked_content
def create_commits(number=2, final_tracked_content=None, final_untracked_content=None):
for i in range(number):
commit_number = i + 1
if commit_number == number:
tracked_content, untracked_content = create_changes(
final_tracked_content, final_untracked_content
)
else:
tracked_content, untracked_content = create_changes()
repo.git.add("-A")
repo.git.commit(m=f"Commit {commit_number}")
return tracked_content, untracked_content
@pytest.fixture(scope="module", autouse=True)
def before_after_all():
print("Before all tests")
# Check there are no local changes that might be
# destroyed by running these tests
assert not repo.is_dirty(untracked_files=True)
# Create a new default branch for the test run
repo.remotes.origin.fetch()
repo.git.checkout("master")
repo.git.checkout("HEAD", b=NOT_BASE_BRANCH)
create_tracked_change()
repo.git.add("-A")
repo.git.commit(m="This commit should not appear in pr branches")
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{NOT_BASE_BRANCH}")
# Create a new default branch for the test run
repo.git.checkout("master")
repo.git.checkout("HEAD", b=DEFAULT_BRANCH)
create_tracked_change()
repo.git.add("-A")
repo.git.commit(m="Add file to be a tracked file for tests")
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{DEFAULT_BRANCH}")
yield
print("After all tests")
repo.git.checkout("master")
# Delete the "not base branch" created for the test run
repo.git.branch("--delete", "--force", NOT_BASE_BRANCH)
repo.git.push("--delete", "--force", REPO_URL, f"refs/heads/{NOT_BASE_BRANCH}")
# Delete the default branch created for the test run
repo.git.branch("--delete", "--force", DEFAULT_BRANCH)
repo.git.push("--delete", "--force", REPO_URL, f"refs/heads/{DEFAULT_BRANCH}")
def before_test():
print("Before test")
# Checkout the default branch
repo.git.checkout(DEFAULT_BRANCH)
def after_test(delete_remote=True):
print("After test")
# Output git log
print(repo.git.log("-5", pretty="oneline"))
# Delete the pull request branch if it exists
repo.git.checkout(DEFAULT_BRANCH)
print(f"Deleting {BRANCH}")
for branch in repo.branches:
if branch.name == BRANCH:
repo.git.branch("--delete", "--force", BRANCH)
break
if delete_remote:
print(f"Deleting origin/{BRANCH}")
for ref in repo.remotes.origin.refs:
if ref.name == f"origin/{BRANCH}":
repo.git.push("--delete", "--force", REPO_URL, f"refs/heads/{BRANCH}")
repo.remotes.origin.fetch("--prune")
break
@pytest.fixture(autouse=True)
def before_after_tests():
before_test()
yield
after_test()
# Tests if a branch exists and can be fetched
def coub_fetch_successful():
assert coub.fetch_successful(repo, REPO_URL, NOT_BASE_BRANCH)
assert not coub.fetch_successful(repo, REPO_URL, NOT_EXIST_BRANCH)
# Tests no changes resulting in no new branch being created
def coub_no_changes_on_create():
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "none"
# Tests create and update with a tracked file change
def coub_tracked_changes():
# Create a tracked file change
tracked_content = create_tracked_change()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Create a tracked file change
tracked_content = create_tracked_change()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "updated"
assert result["diff"]
assert get_tracked_content() == tracked_content
# Tests create and update with an untracked file change
def coub_untracked_changes():
# Create an untracked file change
untracked_content = create_untracked_change()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "created"
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Create an untracked file change
untracked_content = create_untracked_change()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "updated"
assert result["diff"]
assert get_untracked_content() == untracked_content
# Tests create and update with identical changes
# The pull request branch will not be updated
def coub_identical_changes():
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Create identical tracked and untracked file changes
create_changes(tracked_content, untracked_content)
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "none"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Tests create and update with commits on the base inbetween
def coub_commits_on_base():
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Create commits on the base
create_commits()
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{DEFAULT_BRANCH}")
repo.remotes.origin.fetch()
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "updated"
assert result["diff"]
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Tests create and then an update with no changes
# This effectively reverts the branch back to match the base and results in no diff
def coub_changes_no_diff():
# Save the default branch tracked content
default_tracked_content = get_tracked_content()
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Running with no update effectively reverts the branch back to match the base
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "updated"
assert result["diff"] == False
assert get_tracked_content() == default_tracked_content
# Tests create and update with commits on the base inbetween
# The changes on base effectively revert the branch back to match the base and results in no diff
def coub_commits_on_base_no_diff():
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Create commits on the base
tracked_content, untracked_content = create_commits()
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{DEFAULT_BRANCH}")
repo.remotes.origin.fetch()
# Create the same tracked and untracked file changes that were made to the base
create_changes(tracked_content, untracked_content)
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "updated"
assert result["diff"] == False
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Tests create and update with commits on the working base (during the workflow)
def coub_commits_on_working_base():
# Create commits on the working base
tracked_content, untracked_content = create_commits()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Create commits on the working base
tracked_content, untracked_content = create_commits()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "updated"
assert result["diff"]
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Tests create and update with changes and commits on the working base (during the workflow)
def coub_changes_and_commits_on_working_base():
# Create commits on the working base
create_commits()
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Create commits on the working base
create_commits()
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "updated"
assert result["diff"]
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Tests create and update with changes and commits on the working base (during the workflow)
# with commits on the base inbetween
def coub_changes_and_commits_on_base_and_working_base():
# Create commits on the working base
create_commits()
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Create commits on the base
create_commits()
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{DEFAULT_BRANCH}")
repo.remotes.origin.fetch()
# Create commits on the working base
create_commits()
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, None, BRANCH)
assert result["action"] == "updated"
assert result["diff"]
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Working Base is Not Base (WBNB)
# Tests no changes resulting in no new branch being created
def coub_wbnb_no_changes_on_create():
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "none"
# Working Base is Not Base (WBNB)
# Tests create and update with a tracked file change
def coub_wbnb_tracked_changes():
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create a tracked file change
tracked_content = create_tracked_change()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create a tracked file change
tracked_content = create_tracked_change()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "updated"
assert result["diff"]
assert get_tracked_content() == tracked_content
# Working Base is Not Base (WBNB)
# Tests create and update with an untracked file change
def coub_wbnb_untracked_changes():
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create an untracked file change
untracked_content = create_untracked_change()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "created"
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create an untracked file change
untracked_content = create_untracked_change()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "updated"
assert result["diff"]
assert get_untracked_content() == untracked_content
# Working Base is Not Base (WBNB)
# Tests create and update with identical changes
# The pull request branch will not be updated
def coub_wbnb_identical_changes():
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create identical tracked and untracked file changes
create_changes(tracked_content, untracked_content)
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "none"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Working Base is Not Base (WBNB)
# Tests create and update with commits on the base inbetween
def coub_wbnb_commits_on_base():
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Create commits on the base
create_commits()
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{DEFAULT_BRANCH}")
repo.remotes.origin.fetch()
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "updated"
assert result["diff"]
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Working Base is Not Base (WBNB)
# Tests create and then an update with no changes
# This effectively reverts the branch back to match the base and results in no diff
def coub_wbnb_changes_no_diff():
# Save the default branch tracked content
default_tracked_content = get_tracked_content()
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Running with no update effectively reverts the branch back to match the base
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "updated"
assert result["diff"] == False
assert get_tracked_content() == default_tracked_content
# Working Base is Not Base (WBNB)
# Tests create and update with commits on the base inbetween
# The changes on base effectively revert the branch back to match the base and results in no diff
# This scenario will cause cherrypick to fail due to an empty commit.
# The commit is empty because the changes now exist on the base.
def coub_wbnb_commits_on_base_no_diff():
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Create commits on the base
tracked_content, untracked_content = create_commits()
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{DEFAULT_BRANCH}")
repo.remotes.origin.fetch()
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create the same tracked and untracked file changes that were made to the base
create_changes(tracked_content, untracked_content)
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "updated"
assert result["diff"] == False
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Working Base is Not Base (WBNB)
# Tests create and update with commits on the working base (during the workflow)
def coub_wbnb_commits_on_working_base():
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create commits on the working base
tracked_content, untracked_content = create_commits()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create commits on the working base
tracked_content, untracked_content = create_commits()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "updated"
assert result["diff"]
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Working Base is Not Base (WBNB)
# Tests create and update with changes and commits on the working base (during the workflow)
def coub_wbnb_changes_and_commits_on_working_base():
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create commits on the working base
create_commits()
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create commits on the working base
create_commits()
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "updated"
assert result["diff"]
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Working Base is Not Base (WBNB)
# Tests create and update with changes and commits on the working base (during the workflow)
# with commits on the base inbetween
def coub_wbnb_changes_and_commits_on_base_and_working_base():
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create commits on the working base
create_commits()
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "created"
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# Push pull request branch to remote
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{BRANCH}")
repo.remotes.origin.fetch()
after_test(delete_remote=False)
before_test()
# Create commits on the base
create_commits()
repo.git.push("--force", REPO_URL, f"HEAD:refs/heads/{DEFAULT_BRANCH}")
repo.remotes.origin.fetch()
# Set the working base to a branch that is not the pull request base
repo.git.checkout(NOT_BASE_BRANCH)
# Create commits on the working base
create_commits()
# Create tracked and untracked file changes
tracked_content, untracked_content = create_changes()
result = coub.create_or_update_branch(repo, REPO_URL, COMMIT_MESSAGE, BASE, BRANCH)
assert result["action"] == "updated"
assert result["diff"]
assert get_tracked_content() == tracked_content
assert get_untracked_content() == untracked_content
# pytest -v -s ~/git/create-pull-request/src
test_coub_fetch_successful = coub_fetch_successful
test_coub_no_changes_on_create = coub_no_changes_on_create
test_coub_tracked_changes = coub_tracked_changes
test_coub_untracked_changes = coub_untracked_changes
test_coub_identical_changes = coub_identical_changes
test_coub_commits_on_base = coub_commits_on_base
test_coub_changes_no_diff = coub_changes_no_diff
test_coub_commits_on_base_no_diff = coub_commits_on_base_no_diff
test_coub_commits_on_working_base = coub_commits_on_working_base
test_coub_changes_and_commits_on_working_base = coub_changes_and_commits_on_working_base
test_coub_changes_and_commits_on_base_and_working_base = (
coub_changes_and_commits_on_base_and_working_base
)
# WBNB
test_coub_wbnb_no_changes_on_create = coub_wbnb_no_changes_on_create
test_coub_wbnb_tracked_changes = coub_wbnb_tracked_changes
test_coub_wbnb_untracked_changes = coub_wbnb_untracked_changes
test_coub_wbnb_identical_changes = coub_wbnb_identical_changes
test_coub_wbnb_commits_on_base = coub_wbnb_commits_on_base
test_coub_wbnb_changes_no_diff = coub_wbnb_changes_no_diff
test_coub_wbnb_commits_on_base_no_diff = coub_wbnb_commits_on_base_no_diff
test_coub_wbnb_commits_on_working_base = coub_wbnb_commits_on_working_base
test_coub_wbnb_changes_and_commits_on_working_base = (
coub_wbnb_changes_and_commits_on_working_base
)
test_coub_wbnb_changes_and_commits_on_base_and_working_base = (
coub_wbnb_changes_and_commits_on_base_and_working_base
)

139
src/utils.ts Normal file
View 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
View 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"]
}