Compare commits
799 Commits
v2.1.0
...
signed-com
Author | SHA1 | Date | |
---|---|---|---|
740d9850a7 | |||
e7f5ea9fd9 | |||
66ddf90dac | |||
fd3e742ffd | |||
eea4f44785 | |||
5a9be5875b | |||
3d665e5aea | |||
bb1f2b1327 | |||
1da4bbe67c | |||
d93a919a26 | |||
a2d4746d68 | |||
2c262e8e92 | |||
7b7dc5777f | |||
c1be170c86 | |||
822f3b39c1 | |||
197e74c6e1 | |||
c7909f9b04 | |||
491f77f4d6 | |||
93858f721d | |||
2668dc956a | |||
b0303827bb | |||
90b04fe25b | |||
2707da835d | |||
e4c51477d1 | |||
0a237f343d | |||
77c6c11180 | |||
477c78c3f2 | |||
7f459482cc | |||
018afb52b6 | |||
3a7a677a14 | |||
74416df758 | |||
7575ead361 | |||
3d409de49f | |||
743dcd81f7 | |||
0f72e35b7f | |||
4a3e69b7f7 | |||
525f1f0028 | |||
36bba202e3 | |||
3563849c8a | |||
6c03c11aff | |||
27642d5a9e | |||
136db6a783 | |||
43d45f2e88 | |||
24bfe8de6b | |||
22fb2d9a65 | |||
ba864ad40c | |||
b57f967cb0 | |||
c2b738937b | |||
3707121594 | |||
058cde4277 | |||
a0390aed21 | |||
93bc7fd9cd | |||
a7dcd19d46 | |||
ed59299f13 | |||
cb4a03daab | |||
ca2ba77cd9 | |||
d884c1e53f | |||
85c93e4595 | |||
cd5c7e4b8b | |||
67f9e1be23 | |||
c5a7806660 | |||
4383ba9ef0 | |||
36f7648874 | |||
5f7c1586fd | |||
db1713da3a | |||
ca98a71ccc | |||
ce008085c8 | |||
7318c0b7b6 | |||
e30bbbb3c9 | |||
bad19b8e0b | |||
098cf60ce4 | |||
1ec1358801 | |||
b5ed4c38bc | |||
b67febc3a3 | |||
7c71392b65 | |||
bdffaf9259 | |||
0a0317b2d8 | |||
d9e8da8566 | |||
59e72ed4d2 | |||
15410bdb79 | |||
ce1b5d49b6 | |||
479b20f068 | |||
8c75f4ab5f | |||
5874ea5902 | |||
6d6857d369 | |||
9153d834b6 | |||
c55203cfde | |||
6ce4eca6b6 | |||
36ef0ed92f | |||
8500972a13 | |||
bda5ade93c | |||
70a41aba78 | |||
57a101480a | |||
b3a2c5d525 | |||
02c7da59e8 | |||
bac6da8071 | |||
a4f52f8033 | |||
853c071bcf | |||
d2c126edc7 | |||
43d39c6836 | |||
5a9d206da2 | |||
e0743ed96c | |||
e1529cb8ab | |||
aad52e87e7 | |||
a64ebdd734 | |||
51b40aff5f | |||
49006b2a60 | |||
b1ddad2c99 | |||
bb809027fd | |||
e0037d470c | |||
94b1f99e3a | |||
69c27eaf4a | |||
7ea722a0f6 | |||
5ee839affd | |||
60fc256c67 | |||
0c67723361 | |||
4e288e851b | |||
278e5302dd | |||
5e5e72ede9 | |||
5ac05dbaf3 | |||
583b402db4 | |||
1a0e857f60 | |||
1f76dd3b26 | |||
efda6f2ece | |||
382b1bf0d8 | |||
040164963a | |||
9e49b1873f | |||
798d65c1e2 | |||
ac8dd8903f | |||
5b5eb8d8d0 | |||
7e6ae5af18 | |||
1c0b168d50 | |||
4e1ec66aad | |||
d07501c8ac | |||
f5ba48ac1e | |||
f0f8250914 | |||
fe61cf72ef | |||
4fa9ccf7a7 | |||
79098666d7 | |||
d7c27ba1b1 | |||
c526248631 | |||
2b66fab098 | |||
57390a8261 | |||
76c6f5c20e | |||
2f092153d6 | |||
c643f6fba5 | |||
8c6bf8e9fa | |||
57ae48c0ec | |||
d4d7b4e3a6 | |||
7865f819b0 | |||
ff2bf9be1f | |||
fa448197ec | |||
e42d7ab098 | |||
57a92650b1 | |||
a623c1421e | |||
1efedcfa53 | |||
73d77c9d42 | |||
ae78d6487c | |||
8c11f0ebce | |||
0c31730c33 | |||
c8e3bd8337 | |||
3e02667f27 | |||
5de036d28f | |||
76ff63671c | |||
6daff55809 | |||
3588b1aa44 | |||
60d8781d60 | |||
ae06da3674 | |||
d806186ee3 | |||
0652023649 | |||
8a825a8466 | |||
b5f830072e | |||
a5d4677573 | |||
4c2973e730 | |||
a05bf394be | |||
803660ec69 | |||
21a254626f | |||
bb0c945e6e | |||
bd41655446 | |||
6d9c0cdf58 | |||
4306b59af5 | |||
80507e9341 | |||
4e9c9ce808 | |||
989188a00c | |||
e4b37ab067 | |||
d721b8be51 | |||
023de218b7 | |||
0cd7ff0e63 | |||
fca1e78bd8 | |||
92a21ab9e6 | |||
157a260267 | |||
270c03dcf3 | |||
8290a989c8 | |||
3f6dd507d6 | |||
842881bb7f | |||
9d48be23dc | |||
88e822f486 | |||
ea82a8b8d7 | |||
ab8fb76677 | |||
506056c16d | |||
f908c6b99c | |||
1c8b8845ee | |||
ad8cdb00a0 | |||
5fa7af1a21 | |||
5848fcb314 | |||
e1f6e4238a | |||
f42c0a796a | |||
e70ffc3767 | |||
9fd47c87b5 | |||
020f4502ee | |||
5931b3373e | |||
657fe58e8f | |||
5f8399b325 | |||
eebb6ccce1 | |||
daee43366d | |||
7574e859c5 | |||
f2c0f053fc | |||
19e044019f | |||
153407881e | |||
143be5d671 | |||
51e8ca2340 | |||
712add83f2 | |||
a9e8aabc8b | |||
37be4ffd94 | |||
a5f0e5dc8e | |||
9ef70ee495 | |||
0a287739d3 | |||
4ddb8c8fe7 | |||
7b276d47e7 | |||
007717eeea | |||
30b8ee1ec7 | |||
8e2fa462cf | |||
efbb6ba75d | |||
4cdfca66b8 | |||
8fbd83c22f | |||
371aec12f4 | |||
83165e6fd4 | |||
0183f33490 | |||
37c110a6c3 | |||
284f54f989 | |||
9e5b234402 | |||
2d8e7db84c | |||
041b6ab163 | |||
31de0fdf3f | |||
28295f6636 | |||
8dcaf3883b | |||
2827897dcc | |||
c4f19d3a23 | |||
46035868a3 | |||
c09e1094b6 | |||
eab3ea092e | |||
e179caf91b | |||
f3a21bf340 | |||
5b4a9f6a9e | |||
1847e5d1d6 | |||
c246f7e912 | |||
2dd2b11b09 | |||
05d5a3c3f9 | |||
21479f22fc | |||
36a56dac07 | |||
b7f0c9773b | |||
6a62596740 | |||
d1ed29fe1e | |||
e34fedf492 | |||
495ffbb489 | |||
073c8e6ece | |||
375254568c | |||
09af4e30b5 | |||
38e0b6e68b | |||
a95ef54b72 | |||
021e16bf4a | |||
5141da944e | |||
6217f0d61d | |||
e5cb5210cd | |||
7414bc0848 | |||
f8fe2469e5 | |||
b8f683bda6 | |||
d5f5f4bf24 | |||
6c2fad6b3d | |||
a716f73d72 | |||
24b0f8edad | |||
0e6a687637 | |||
e913bb956f | |||
82604c59cc | |||
951d1b87f0 | |||
df0f02f84a | |||
274dd7d2dc | |||
a38ad37eff | |||
450c1f7d69 | |||
a057a6160f | |||
391a18894a | |||
f5be40c5f3 | |||
4eca541d3c | |||
5a4c812497 | |||
37c8100cff | |||
ec919b7792 | |||
000e3c6002 | |||
85176410d0 | |||
ac971fe9b8 | |||
3019dd596d | |||
e73c69172d | |||
d4a593d321 | |||
58815ba2b6 | |||
29267e1085 | |||
7f1f68b1c1 | |||
38e03b3c64 | |||
3861a52f7a | |||
f14600d64a | |||
9895f5748a | |||
84c5454c6e | |||
f63262067e | |||
aa2f52f5a1 | |||
bb5781b719 | |||
e398a27af3 | |||
d015db1f4a | |||
f962808be0 | |||
2dd85e3f19 | |||
2f0833515b | |||
ca4fd2f0d1 | |||
ea54357f43 | |||
df09107abb | |||
16ae6c427b | |||
852fead4ce | |||
9b996f5c6a | |||
e8211ce8f4 | |||
10f43fe334 | |||
1313abbfb6 | |||
73ff87b6a4 | |||
d4d765620e | |||
646f785c1f | |||
f73101ae67 | |||
78389986b6 | |||
c5bc9fd446 | |||
17815d689c | |||
a8d53a0985 | |||
daf3998db5 | |||
8861bdedc8 | |||
75a5de63f0 | |||
d36c8e0863 | |||
2b011faafd | |||
331d02c7e2 | |||
d7db273d6c | |||
ee93d78b55 | |||
6c704eb7a8 | |||
88bf0de51c | |||
b38e8b0abe | |||
b4d51739f9 | |||
ad43dccb4d | |||
c2f9cef04d | |||
b67934cbcf | |||
ef83023339 | |||
671dc9c9e0 | |||
ddab646771 | |||
3f9dbd5a76 | |||
171dd555b9 | |||
6e59b075e0 | |||
9c5916f06d | |||
33434f1c62 | |||
9ca978d38e | |||
8e154b2a92 | |||
18f90432be | |||
2721abb4d0 | |||
20dac2ed48 | |||
8557470a68 | |||
10db75894f | |||
5a6b15373e | |||
923ad837f1 | |||
f094b77505 | |||
af7c021bb9 | |||
97872c4843 | |||
bd72e1b792 | |||
f1a7646cea | |||
15b68d176d | |||
0dfc93c104 | |||
252fb19db2 | |||
4b867c4939 | |||
4fb3835236 | |||
19ace475b4 | |||
84d431ad62 | |||
d6d5519d05 | |||
0e8dfbd57d | |||
ffa8cc2261 | |||
00fb6900cb | |||
2fe7e77753 | |||
18f7dc018c | |||
89265e8d24 | |||
a7bb76508d | |||
357cebe268 | |||
f22a7da129 | |||
3f60247108 | |||
dcd5fd746d | |||
4b53b6fd1a | |||
10a1849302 | |||
c209d72428 | |||
ef9e028216 | |||
f8f85df783 | |||
b9117f2e0c | |||
598ffcd35c | |||
d8e8e547cc | |||
507420e035 | |||
67df31e08a | |||
f530141cd3 | |||
a5a72ba246 | |||
a6c8b3814a | |||
c0a9598b0e | |||
3c3d696d5b | |||
bd0f84d69c | |||
ad71e1f128 | |||
368c9da6f6 | |||
d8a389d1fa | |||
225cf628aa | |||
d7a8d0affc | |||
2dc79e58de | |||
95767e7d51 | |||
3263596ac4 | |||
3e21ec0c82 | |||
5050a372c9 | |||
7380612b49 | |||
771ad1b5f4 | |||
093c191148 | |||
00cb0abb4d | |||
fa0950476f | |||
b90b9c1e20 | |||
028a63020c | |||
9d59234a82 | |||
210f7aab2c | |||
6bb7394339 | |||
a518698c07 | |||
8be395fdd3 | |||
36d063872e | |||
9825ae65b1 | |||
243251cf92 | |||
28beef91aa | |||
01f7dd1d28 | |||
32c71c837c | |||
a6d621d73e | |||
09f51e6391 | |||
ed04db61de | |||
ea6b55fc9d | |||
88896f707d | |||
423630f7c0 | |||
66cc0cc1e2 | |||
2a63057d1e | |||
f83efdc34a | |||
fff683e9ca | |||
9f83247b25 | |||
670e508eb6 | |||
d9d6fd980e | |||
8bb8511e4d | |||
c1d92ef456 | |||
1ff93da091 | |||
0524c01297 | |||
548adff9dc | |||
28674474a4 | |||
4fb90330a4 | |||
e4c811acf5 | |||
99ccb3479b | |||
13616a4432 | |||
b5b91bc2b0 | |||
5666cd8fe9 | |||
ad897490d5 | |||
0735106af9 | |||
9aeedaa8c2 | |||
52d31873b6 | |||
09b9ac155b | |||
6ec5e3e26b | |||
8b46437b6d | |||
e361fd1788 | |||
052fc72b41 | |||
ed00d4629c | |||
34371f09e5 | |||
c27ea51ae0 | |||
5e9d0ee9ea | |||
b5f41d9b08 | |||
2455e15969 | |||
05bc46786e | |||
adc6552966 | |||
171fc6cce4 | |||
3fb765f674 | |||
d95c81ee98 | |||
8d5ed6557f | |||
7b1819c092 | |||
be0a8c9666 | |||
a0a6157bf1 | |||
9c5ec2e07d | |||
45c510e1f6 | |||
249b80db6b | |||
6c2b44c6ac | |||
76c58cf6a9 | |||
8c603dbb04 | |||
d01e0807ef | |||
ce699aa2d1 | |||
9984f611a7 | |||
ff0beed1b2 | |||
ddeca94037 | |||
0fd77ba8cc | |||
c7f493a800 | |||
91664dfb28 | |||
13ec5274b1 | |||
bcf9790963 | |||
88ea447de7 | |||
da928d5fcc | |||
2465e435b9 | |||
37b2bd1eca | |||
eb13e17e17 | |||
a1ecc20658 | |||
ffcad23634 | |||
f4b52b768a | |||
af682c8fcb | |||
7378b23cb0 | |||
370ae6d537 | |||
ae0797ee12 | |||
e05457394a | |||
44f76dd5b3 | |||
279e66ed27 | |||
ce9dd3641e | |||
1a00b34382 | |||
e17bb55cb7 | |||
1890e1ec35 | |||
a49ee3308e | |||
16fa12ee5f | |||
5ea31358e9 | |||
105f0d3816 | |||
8fb2374109 | |||
a68328a1ee | |||
9bf4b302a5 | |||
da6a868b86 | |||
cbd5e97fef | |||
095c53659b | |||
fc687f898b | |||
3b32bfa4f7 | |||
9c3bf74d10 | |||
b9ed02107c | |||
2f31350598 | |||
d14206169d | |||
82c423c73b | |||
7cf22579c6 | |||
5893420513 | |||
8bdec9b230 | |||
2570a753e0 | |||
1c466bedd5 | |||
a755ad5ffb | |||
52cabd40b9 | |||
12b7e7a141 | |||
8f96fd0252 | |||
5f45e30ff9 | |||
348abed1de | |||
06ab0ad94e | |||
de6e791e81 | |||
184bc2eccd | |||
4dab765cf5 | |||
329417f094 | |||
7311df63a5 | |||
47a0e6b298 | |||
9b02473bb0 | |||
40b1b245eb | |||
64c8d492d5 | |||
28fddffcf8 | |||
ba8309ff9c | |||
ee482f51bd | |||
bf7dd38b26 | |||
71a37c5c33 | |||
f9af0fbfc0 | |||
0c57887fe8 | |||
7ad9f6e012 | |||
dd364f2835 | |||
ddd52205b6 | |||
a81d0faef6 | |||
fc03a820b2 | |||
b2cdf099d0 | |||
f3d08d3f0b | |||
3a21b8d668 | |||
132b02c8e1 | |||
9507cdc7ac | |||
f4bf0d40c3 | |||
cf325ed84b | |||
ebc5e02585 | |||
c01f8fadc7 | |||
c0c9424b03 | |||
7649c29e89 | |||
125f154b50 | |||
8c01dce3ac | |||
3c6aade49b | |||
76c09b178c | |||
2f62e00e1b | |||
16354377ad | |||
809468fcd7 | |||
216b3681c3 | |||
6fa44e144d | |||
b5b1bc17a4 | |||
794518a553 | |||
f4ee4a8333 | |||
053b501145 | |||
1a182d0679 | |||
24012f5c84 | |||
a6a1a418bf | |||
5429e57002 | |||
3c32fac1fe | |||
803cc5ea8a | |||
4ba9ca3d10 | |||
40e70b8f7b | |||
b7eabdce59 | |||
43dc723813 | |||
4d3b0a48ef | |||
e9a825aacd | |||
3509fd45ae | |||
14ee9d1df2 | |||
5d969a55c1 | |||
44130f6fc9 | |||
86ccd8cdef | |||
7e7fa32a5f | |||
5f7beeb2ff | |||
926d56fcba | |||
8d744a2cd3 | |||
c2d829c681 | |||
d77392faf0 | |||
ccd2b64012 | |||
58fb221778 | |||
7a856e8b5d | |||
65d7a66451 | |||
44a7f59b6f | |||
8acaf6bb4c | |||
498d78cb23 | |||
172ec762f8 | |||
5cb0d674f3 | |||
eb892d7803 | |||
f8274253bd | |||
0f423da02c | |||
9573f479a0 | |||
eb4cde120d | |||
25902ccdd1 | |||
39b337e8bb | |||
7e70d8e63c | |||
1a640f5b01 | |||
9faa8dc1d9 | |||
afcf57957d | |||
8cc3564bf3 | |||
6910c5cd03 | |||
14836c6ff3 | |||
8fbfcfbcbb | |||
326f260418 | |||
75104b7d7e | |||
fbb7e0e650 | |||
0f1e60a1f8 | |||
caa116d991 | |||
d2f72f0799 | |||
ded05960f3 | |||
eb605db8a3 | |||
b11e4c665b | |||
65327d17a5 | |||
0837238e66 | |||
17bd947e89 | |||
24b42ba7f4 | |||
c0aaf5bab9 | |||
968cb0f4d9 | |||
9d6f73d546 | |||
70d240d0c4 | |||
7bb7d96c96 | |||
66fcd19e8d | |||
d93f2b46fd | |||
246328e3d8 | |||
61cff7c673 | |||
46ba7bdfe8 | |||
d650be7389 | |||
2f7173349f | |||
4ca95026d7 | |||
64c4efd526 | |||
16e35685ce | |||
3b12cf0165 | |||
2a283f5fc3 | |||
8ed207bcca | |||
bd1f6727cd | |||
9a3acf8f32 | |||
b38fd9eb87 | |||
4a9e76e377 | |||
8cb4c8b741 | |||
b9eb5dd95e | |||
5502904068 | |||
56ad1fed7b | |||
2132f428f6 | |||
c558d39395 | |||
db640fa8db | |||
95d6677567 | |||
1f4e24248b | |||
08595270b5 | |||
237244614a | |||
6295d61f0c | |||
d4024e2876 | |||
ee96ad03d9 | |||
755b39d2ff | |||
9f95ac6c53 | |||
cedbe4ad47 | |||
0e48ed8743 | |||
e7291b422e | |||
eb824681a8 | |||
4cc13107a9 | |||
c71b8e4206 | |||
e2bf7f9b75 | |||
e1f4cfdcd4 | |||
b3f0552507 | |||
f4be118b21 | |||
c9f22f86fb | |||
35d5f3c8ae | |||
000a0fc06a | |||
2a59f517a7 | |||
48ce89bc7d | |||
6570353abb | |||
8f6cecd6c4 | |||
e14ef3b543 | |||
c5778e5181 | |||
374fc61fef | |||
6fa547cc6f | |||
4db3619128 | |||
989a8308ec | |||
6249109e58 | |||
c9b850c450 | |||
340e629d2f | |||
abc19caa82 | |||
3474dda921 | |||
ad11b10aa4 | |||
86aa5be8bf | |||
6867319cf3 | |||
cc84a2389e | |||
7e7150d0e8 | |||
eb99d45ce6 | |||
115b7391e1 | |||
8305970523 | |||
32f5c5dd5f | |||
628c2d7d35 | |||
37582e8764 | |||
1e6b4d1790 | |||
a70c6ebe2a | |||
5bd05538d0 | |||
26bc40eea1 | |||
6bb0e7771c | |||
4c347a4514 | |||
cff2c3381d | |||
e48dab0c1c | |||
ac9f92d6e7 | |||
3ff256ce08 | |||
0bcb10560b | |||
3c4b2793c1 | |||
22870d7816 | |||
e324a22ee1 | |||
8b60386018 | |||
6852d55922 | |||
32e5bb80a5 | |||
619cf2115d | |||
69008aa567 | |||
ae4278bf24 | |||
88da40fea7 | |||
694e068136 | |||
64c34f6885 | |||
52ada17960 | |||
ce00b952cf | |||
0d42c285a3 | |||
ea1eaf1734 | |||
d5c5ea3e20 | |||
c7b64af0a4 | |||
289fda9fea | |||
b021b9e27a | |||
c26314237b | |||
ed7dd8d236 | |||
ca0e9d75fd | |||
87c27ee3eb | |||
4beea725d3 | |||
9d58699da5 | |||
bceebba814 | |||
0b82710b2e | |||
4aaaa0e760 | |||
918bfcb8a3 | |||
1ecfd1ae40 | |||
4dd195d7c3 | |||
72b5f45bb4 | |||
5394814b39 | |||
83918398f5 | |||
b6a458d96a | |||
7b5ff6b642 | |||
82eddd8828 | |||
6df2a462d1 | |||
3689bd07d7 | |||
943f19ac64 | |||
e59d6c7fff | |||
c65a4f39b3 | |||
2d18371789 | |||
cc7020a609 | |||
d8700620d6 | |||
b7064071dc | |||
339e82d37b | |||
a319015452 | |||
df0d07269b | |||
2aa04baf2e | |||
d29f1e9296 | |||
8ee12880b0 | |||
8d39de8771 | |||
c202684c92 | |||
e970adccb4 | |||
9f9af3e969 | |||
615d7c82e3 |
3
.eslintignore
Normal file
3
.eslintignore
Normal file
@ -0,0 +1,3 @@
|
||||
dist/
|
||||
lib/
|
||||
node_modules/
|
23
.eslintrc.json
Normal file
23
.eslintrc.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"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"
|
||||
],
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"rules": {
|
||||
"@typescript-eslint/camelcase": "off"
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"typescript": {}
|
||||
}
|
||||
}
|
||||
}
|
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -1 +0,0 @@
|
||||
* @peter-evans
|
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
github: peter-evans
|
20
.github/dependabot.yml
vendored
Normal file
20
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "tuesday"
|
||||
labels:
|
||||
- "dependencies"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "tuesday"
|
||||
ignore:
|
||||
- dependency-name: "*"
|
||||
update-types: ["version-update:semver-major"]
|
||||
labels:
|
||||
- "dependencies"
|
13
.github/workflows/automerge-dependabot.yml
vendored
Normal file
13
.github/workflows/automerge-dependabot.yml
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
name: Auto-merge Dependabot
|
||||
on: pull_request
|
||||
|
||||
jobs:
|
||||
automerge:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor == 'dependabot[bot]'
|
||||
steps:
|
||||
- uses: peter-evans/enable-pull-request-automerge@v3
|
||||
with:
|
||||
token: ${{ secrets.ACTIONS_BOT_TOKEN }}
|
||||
pull-request-number: ${{ github.event.pull_request.number }}
|
||||
merge-method: squash
|
135
.github/workflows/ci.yml
vendored
Normal file
135
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,135 @@
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- 'README.md'
|
||||
- 'docs/**'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- 'README.md'
|
||||
- 'docs/**'
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npm run format-check
|
||||
- run: npm run lint
|
||||
- run: npm run test
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: dist
|
||||
- uses: actions/upload-artifact@v4
|
||||
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@v4
|
||||
with:
|
||||
ref: main
|
||||
- if: matrix.target == 'built' || github.event_name == 'pull_request'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: dist
|
||||
- if: matrix.target == 'built' || github.event_name == 'pull_request'
|
||||
uses: actions/download-artifact@v4
|
||||
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-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
||||
author: ${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>
|
||||
title: '[CI] test ${{ matrix.target }}'
|
||||
body: |
|
||||
- CI test case for target '${{ matrix.target }}'
|
||||
|
||||
Auto-generated by [create-pull-request][1]
|
||||
|
||||
[1]: https://github.com/peter-evans/create-pull-request
|
||||
branch: ci-test-${{ matrix.target }}-${{ github.sha }}
|
||||
|
||||
- name: Close Pull
|
||||
uses: peter-evans/close-pull@v3
|
||||
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@v3
|
||||
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@v4
|
||||
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/main'
|
||||
needs: [test]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: dist
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ secrets.ACTIONS_BOT_TOKEN }}
|
||||
commit-message: 'build: update distribution'
|
||||
title: Update distribution
|
||||
body: |
|
||||
- Updates the distribution for changes on `main`
|
||||
|
||||
Auto-generated by [create-pull-request][1]
|
||||
|
||||
[1]: https://github.com/peter-evans/create-pull-request
|
||||
branch: update-distribution
|
38
.github/workflows/cpr-example-command.yml
vendored
38
.github/workflows/cpr-example-command.yml
vendored
@ -6,38 +6,44 @@ jobs:
|
||||
createPullRequest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Create report file
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Make changes to pull request
|
||||
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'
|
||||
commit-message: Update report
|
||||
committer: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
||||
author: ${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>
|
||||
signoff: false
|
||||
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
|
||||
milestone: 1
|
||||
project: Example Project
|
||||
project-column: To do
|
||||
draft: false
|
||||
branch: example-patches
|
||||
- name: Check outputs
|
||||
delete-branch: true
|
||||
|
||||
- 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 }}"
|
||||
echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}"
|
||||
|
||||
- name: Add reaction
|
||||
uses: peter-evans/create-or-update-comment@v1
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
|
||||
comment-id: ${{ github.event.client_payload.github.payload.comment.id }}
|
||||
reaction-type: hooray
|
||||
|
19
.github/workflows/dockerhub-description.yml
vendored
19
.github/workflows/dockerhub-description.yml
vendored
@ -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
|
13
.github/workflows/slash-command-dispatch.yml
vendored
13
.github/workflows/slash-command-dispatch.yml
vendored
@ -7,10 +7,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Slash Command Dispatch
|
||||
uses: peter-evans/slash-command-dispatch@v1
|
||||
uses: peter-evans/slash-command-dispatch@v4
|
||||
with:
|
||||
token: ${{ secrets.REPO_ACCESS_TOKEN }}
|
||||
reaction-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
token: ${{ secrets.ACTIONS_BOT_TOKEN }}
|
||||
config: >
|
||||
[
|
||||
{
|
||||
@ -20,7 +19,7 @@ jobs:
|
||||
"named_args": true
|
||||
},
|
||||
{
|
||||
"command": "pytest",
|
||||
"command": "testv5",
|
||||
"permission": "admin",
|
||||
"repository": "peter-evans/create-pull-request-tests",
|
||||
"named_args": true
|
||||
@ -34,5 +33,11 @@ jobs:
|
||||
"command": "cpr-example",
|
||||
"permission": "admin",
|
||||
"issue_type": "issue"
|
||||
},
|
||||
{
|
||||
"command": "rebase",
|
||||
"permission": "admin",
|
||||
"repository": "peter-evans/slash-command-dispatch-processor",
|
||||
"issue_type": "pull-request"
|
||||
}
|
||||
]
|
||||
|
32
.github/workflows/update-major-version.yml
vendored
Normal file
32
.github/workflows/update-major-version.yml
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
name: Update Major Version
|
||||
run-name: Update ${{ github.event.inputs.main_version }} to ${{ github.event.inputs.target }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target:
|
||||
description: The target tag or reference
|
||||
required: true
|
||||
main_version:
|
||||
type: choice
|
||||
description: The major version tag to update
|
||||
options:
|
||||
- v5
|
||||
- v6
|
||||
|
||||
jobs:
|
||||
tag:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.ACTIONS_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
- name: Git config
|
||||
run: |
|
||||
git config user.name actions-bot
|
||||
git config user.email actions-bot@users.noreply.github.com
|
||||
- name: Tag new target
|
||||
run: git tag -f ${{ github.event.inputs.main_version }} ${{ github.event.inputs.target }}
|
||||
- name: Push new tag
|
||||
run: git push origin ${{ github.event.inputs.main_version }} --force
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,3 +1,5 @@
|
||||
__pycache__
|
||||
node_modules
|
||||
lib/
|
||||
node_modules/
|
||||
|
||||
.DS_Store
|
||||
.idea
|
||||
|
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@ -0,0 +1,3 @@
|
||||
dist/
|
||||
lib/
|
||||
node_modules/
|
11
.prettierrc.json
Normal file
11
.prettierrc.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"bracketSpacing": false,
|
||||
"arrowParens": "avoid",
|
||||
"parser": "typescript"
|
||||
}
|
269
README.md
269
README.md
@ -1,5 +1,6 @@
|
||||
# <img width="24" height="24" src="assets/logo.svg"> Create Pull Request
|
||||
[](https://github.com/marketplace/actions/create-pull-request)
|
||||
# <img width="24" height="24" src="docs/assets/logo.svg"> Create Pull Request
|
||||
[](https://github.com/peter-evans/create-pull-request/actions?query=workflow%3ACI)
|
||||
[](https://github.com/marketplace/actions/create-pull-request)
|
||||
|
||||
A GitHub action to create a pull request for changes to your repository in the actions workspace.
|
||||
|
||||
@ -18,119 +19,193 @@ 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 v6](docs/updating.md)
|
||||
- [Common issues](docs/common-issues.md)
|
||||
|
||||
## Usage
|
||||
|
||||
```yml
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
```
|
||||
|
||||
You can also pin to a [specific release](https://github.com/peter-evans/create-pull-request/releases) version in the format `@v2.x.x`
|
||||
You can also pin to a [specific release](https://github.com/peter-evans/create-pull-request/releases) version in the format `@v6.x.x`
|
||||
|
||||
### Workflow permissions
|
||||
|
||||
For this action to work you must explicitly allow GitHub Actions to create pull requests.
|
||||
This setting can be found in a repository's settings under Actions > General > Workflow permissions.
|
||||
|
||||
For repositories belonging to an organization, this setting can be managed by admins in organization settings under Actions > General > Workflow permissions.
|
||||
|
||||
### Action inputs
|
||||
|
||||
With the exception of `token`, all inputs are **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: 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.
|
||||
**Note**: If you want pull requests created by this action to trigger an `on: push` or `on: pull_request` workflow then you cannot use the default `GITHUB_TOKEN`. See the [documentation here](docs/concepts-guidelines.md#triggering-further-workflow-runs) for workarounds.
|
||||
|
||||
| Name | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `token` | `GITHUB_TOKEN` or a `repo` scoped [PAT](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line). | |
|
||||
| `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. |
|
||||
| `token` | `GITHUB_TOKEN` (permissions `contents: write` and `pull-requests: write`) or a `repo` scoped [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token). | `GITHUB_TOKEN` |
|
||||
| `git-token` | The [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) that the action will use for git operations. | Defaults to the value of `token` |
|
||||
| `path` | Relative path under `GITHUB_WORKSPACE` to the repository. | `GITHUB_WORKSPACE` |
|
||||
| `add-paths` | A comma or newline-separated list of file paths to commit. Paths should follow git's [pathspec](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefpathspecapathspec) syntax. If no paths are specified, all new and modified files are added. See [Add specific paths](#add-specific-paths). | |
|
||||
| `commit-message` | The message to use when committing changes. See [commit-message](#commit-message). | `[create-pull-request] automated change` |
|
||||
| `committer` | The committer name and email address in the format `Display Name <email@address.com>`. Defaults to the GitHub Actions bot user on github.com. | `github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>` |
|
||||
| `author` | The author name and email address in the format `Display Name <email@address.com>`. Defaults to the user who triggered the workflow run. | `${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>` |
|
||||
| `signoff` | Add [`Signed-off-by`](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---signoff) line by the committer at the end of the commit log message. | `false` |
|
||||
| `branch` | The pull request branch name. | `create-pull-request/patch` |
|
||||
| `delete-branch` | Delete the `branch` if it doesn't have an active pull request associated with it. See [delete-branch](#delete-branch). | `false` |
|
||||
| `branch-suffix` | The branch suffix type when using the alternative branching strategy. Valid values are `random`, `timestamp` and `short-commit-hash`. See [Alternative strategy](#alternative-strategy---always-create-a-new-pull-request-branch) for details. | |
|
||||
| `base` | Sets the pull request base branch. | Defaults to the branch checked out in the workflow. |
|
||||
| `push-to-fork` | A fork of the checked-out parent repository to which the pull request branch will be pushed. e.g. `owner/repo-fork`. The pull request will be created to merge the fork's branch into the parent's base. See [push pull request branches to a fork](docs/concepts-guidelines.md#push-pull-request-branches-to-a-fork) for details. | |
|
||||
| `sign-commits` | Sign commits as `github-actions[bot]` when using `GITHUB_TOKEN`, or your own bot when using [GitHub App tokens](docs/concepts-guidelines.md#authenticating-with-github-app-generated-tokens). See [commit signing](docs/concepts-guidelines.md#commit-signature-verification-for-bots) for details. | `false` |
|
||||
| `title` | The title of the pull request. | `Changes by create-pull-request action` |
|
||||
| `body` | The body of the pull request. | `Automated changes by [create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub action` |
|
||||
| `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. | |
|
||||
| `body-path` | The path to a file containing the pull request body. Takes precedence over `body`. | |
|
||||
| `labels` | A comma or newline-separated list of labels. | |
|
||||
| `assignees` | A comma or newline-separated list of assignees (GitHub usernames). | |
|
||||
| `reviewers` | A comma or newline-separated list of reviewers (GitHub usernames) to request a review from. | |
|
||||
| `team-reviewers` | A comma or newline-separated list of GitHub teams to request a review from. Note that a `repo` scoped [PAT](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token), or equivalent [GitHub App permissions](docs/concepts-guidelines.md#authenticating-with-github-app-generated-tokens), are required. | |
|
||||
| `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://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests#draft-pull-requests). It is not possible to change draft status after creation except through the web interface. | `false` |
|
||||
| `maintainer-can-modify` | Indicates whether [maintainers can modify](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork) the pull request. | `true` |
|
||||
|
||||
**Outputs**
|
||||
#### commit-message
|
||||
|
||||
The pull request number is output as both an environment variable and a step output.
|
||||
Note that in order to read the step output the action step must have an id.
|
||||
In addition to a message, the `commit-message` input can also be used to populate the commit description. Leave a single blank line between the message and description.
|
||||
|
||||
```yml
|
||||
commit-message: |
|
||||
the first line is the commit message
|
||||
|
||||
the commit description starts
|
||||
after a blank line and can be
|
||||
multiple lines
|
||||
```
|
||||
|
||||
#### delete-branch
|
||||
|
||||
The `delete-branch` feature doesn't delete branches immediately on merge. (It can't do that because it would require the merge to somehow trigger the action.)
|
||||
The intention of the feature is that when the action next runs it will delete the `branch` if there is no diff.
|
||||
|
||||
Enabling this feature leads to the following behaviour:
|
||||
1. If a pull request was merged and the branch is left undeleted, when the action next runs it will delete the branch if there is no further diff.
|
||||
2. If a pull request is open, but there is now no longer a diff and the PR is unnecessary, the action will delete the branch causing the PR to close.
|
||||
|
||||
If you want branches to be deleted immediately on merge then you should use GitHub's `Automatically delete head branches` feature in your repository settings.
|
||||
|
||||
#### Proxy support
|
||||
|
||||
For self-hosted runners behind a corporate proxy set the `https_proxy` environment variable.
|
||||
```yml
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
env:
|
||||
https_proxy: http://<proxy_address>:<port>
|
||||
```
|
||||
|
||||
### Action outputs
|
||||
|
||||
The following outputs can be used by subsequent workflow steps.
|
||||
|
||||
- `pull-request-number` - The pull request number.
|
||||
- `pull-request-url` - The URL of the pull request.
|
||||
- `pull-request-operation` - The pull request operation performed by the action, `created`, `updated`, `closed` or `none`.
|
||||
- `pull-request-head-sha` - The commit SHA of the pull request branch.
|
||||
- `pull-request-branch` - The branch name of the pull request.
|
||||
- `pull-request-commits-verified` - Whether GitHub considers the signature of the branch's commits to be verified; `true` or `false`.
|
||||
|
||||
Step outputs can be accessed as in the following example.
|
||||
Note that in order to read the step outputs the action step must have an id.
|
||||
|
||||
```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@v6
|
||||
- name: Check outputs
|
||||
if: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
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 }}"
|
||||
echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}"
|
||||
```
|
||||
|
||||
### Checkout
|
||||
### Action behaviour
|
||||
|
||||
This action expects repositories to be checked out with `actions/checkout@v2`.
|
||||
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.
|
||||
|
||||
If there is some reason you need to use `actions/checkout@v1` the following step can be added to checkout the branch.
|
||||
How the action behaves:
|
||||
|
||||
```yml
|
||||
- uses: actions/checkout@v1
|
||||
- run: git checkout "${GITHUB_REF:11}"
|
||||
```
|
||||
- If there are changes (i.e. a diff exists with the checked-out base branch), the changes will be pushed to a new `branch` and a pull request created.
|
||||
- If there are no changes (i.e. no diff exists with the checked-out base branch), no pull request will be created and the action exits silently.
|
||||
- If a pull request already exists it will be updated if necessary. Local changes in the Actions workspace, or changes on the base branch, can cause an update. If no update is required the action exits silently.
|
||||
- If a pull request exists and new changes on the base branch make the pull request unnecessary (i.e. there is no longer a diff between the pull request branch and the base), the pull request is automatically closed. Additionally, if [`delete-branch`](#delete-branch) is set to `true` the `branch` will be deleted.
|
||||
|
||||
### Branch naming
|
||||
For further details about how the action works and usage guidelines, see [Concepts, guidelines and advanced usage](docs/concepts-guidelines.md).
|
||||
|
||||
For branch naming there are two strategies. 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.
|
||||
#### Alternative strategy - Always create a new pull request branch
|
||||
|
||||
#### Strategy A - Create and update a pull request branch (default)
|
||||
For some use cases it may be desirable to always create a new unique branch each time there are changes to be committed.
|
||||
This strategy is *not recommended* because if not used carefully it could result in multiple pull requests being created unnecessarily. If in doubt, use the [default strategy](#action-behaviour) of creating an updating a fixed-name branch.
|
||||
|
||||
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.
|
||||
To use this strategy, set input `branch-suffix` with one of the following options.
|
||||
|
||||
#### Strategy B - Always create a new pull request branch
|
||||
|
||||
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`.
|
||||
|
||||
- `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`
|
||||
- `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
|
||||
### Controlling committed files
|
||||
|
||||
The action defaults to adding all new and modified files.
|
||||
If there are files that should not be included in the pull request, you can use the following methods to control the committed content.
|
||||
|
||||
#### Remove files
|
||||
|
||||
The most straightforward way to handle unwanted files is simply to remove them in a step before the action runs.
|
||||
|
||||
```yml
|
||||
- run: |
|
||||
rm -rf temp-dir
|
||||
rm temp-file.txt
|
||||
```
|
||||
|
||||
#### Ignore 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
|
||||
#### Add specific paths
|
||||
|
||||
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.
|
||||
You can control which files are committed with the `add-paths` input.
|
||||
Paths should follow git's [pathspec](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefpathspecapathspec) syntax.
|
||||
File changes that do not match one of the paths will be stashed and restored after the action has completed.
|
||||
|
||||
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
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
committer: Peter Evans <peter-evans@users.noreply.github.com>
|
||||
add-paths: |
|
||||
*.java
|
||||
docs/*.md
|
||||
```
|
||||
|
||||
### Controlling commits
|
||||
#### Create your own commits
|
||||
|
||||
As well as relying on the action to handle uncommitted changes, you can additionally make your own commits before the action runs.
|
||||
Note that the repository must be checked out on a branch with a remote, it won't work for [events which checkout a commit](docs/concepts-guidelines.md#events-which-checkout-a-commit).
|
||||
|
||||
```yml
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Create commits
|
||||
run: |
|
||||
git config user.name 'Peter Evans'
|
||||
@ -143,58 +218,82 @@ 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
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
```
|
||||
|
||||
### 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@v6
|
||||
|
||||
- name: Create or Update Project Card
|
||||
if: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
uses: peter-evans/create-or-update-project-card@v2
|
||||
with:
|
||||
project-name: My project
|
||||
column-name: My column
|
||||
issue-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
```
|
||||
|
||||
### Auto-merge
|
||||
|
||||
Auto-merge can be enabled on a pull request allowing it to be automatically merged once requirements have been satisfied.
|
||||
See [enable-pull-request-automerge](https://github.com/peter-evans/enable-pull-request-automerge) action for usage details.
|
||||
|
||||
## Reference Example
|
||||
|
||||
The following workflow is a reference example that sets all the main inputs.
|
||||
The following workflow sets many of the action's inputs for reference purposes.
|
||||
Check the [defaults](#action-inputs) to avoid setting inputs unnecessarily.
|
||||
|
||||
See [examples](docs/examples.md) for more realistic use cases.
|
||||
|
||||
```yml
|
||||
name: Create Pull Request
|
||||
on: push
|
||||
jobs:
|
||||
createPullRequest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Create report file
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- 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@v6
|
||||
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-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
||||
author: ${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>
|
||||
signoff: false
|
||||
branch: example-patches
|
||||
delete-branch: true
|
||||
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: |
|
||||
developers
|
||||
qa-team
|
||||
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 }}"
|
||||
draft: false
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||

|
||||

|
||||
|
||||
## License
|
||||
|
||||
|
2312
__test__/create-or-update-branch.int.test.ts
Normal file
2312
__test__/create-or-update-branch.int.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
45
__test__/entrypoint.sh
Executable file
45
__test__/entrypoint.sh
Executable file
@ -0,0 +1,45 @@
|
||||
#!/bin/sh -l
|
||||
set -euo pipefail
|
||||
|
||||
# Save the working directory
|
||||
WORKINGDIR=$PWD
|
||||
|
||||
# Create and serve a remote repo
|
||||
mkdir -p /git/remote/repos
|
||||
git config --global init.defaultBranch main
|
||||
git init --bare /git/remote/repos/test-base.git
|
||||
git daemon --verbose --enable=receive-pack --base-path=/git/remote --export-all /git/remote &>/dev/null &
|
||||
|
||||
# Give the daemon time to start
|
||||
sleep 2
|
||||
|
||||
# Create a local clone and make initial commits
|
||||
mkdir -p /git/local/repos
|
||||
git clone git://127.0.0.1/repos/test-base.git /git/local/repos/test-base
|
||||
cd /git/local/repos/test-base
|
||||
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"
|
||||
echo "#test-base :sparkles:" > README.md
|
||||
git add .
|
||||
git commit -m "add sparkles" -m "Change description:
|
||||
- updates README.md to add sparkles to the title"
|
||||
git push -u
|
||||
git 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/repos/test-base.git /git/remote/repos/test-fork.git
|
||||
cd /git/remote/repos/test-fork.git
|
||||
git log -1 --pretty=oneline
|
||||
|
||||
# Restore the working directory
|
||||
cd $WORKINGDIR
|
||||
|
||||
# Execute integration tests
|
||||
jest int --runInBand
|
26
__test__/git-command-manager.int.test.ts
Normal file
26
__test__/git-command-manager.int.test.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import {GitCommandManager, Commit} from '../lib/git-command-manager'
|
||||
|
||||
const REPO_PATH = '/git/local/repos/test-base'
|
||||
|
||||
describe('git-command-manager integration tests', () => {
|
||||
let git: GitCommandManager
|
||||
|
||||
beforeAll(async () => {
|
||||
git = await GitCommandManager.create(REPO_PATH)
|
||||
await git.checkout('main')
|
||||
})
|
||||
|
||||
it('tests getCommit', async () => {
|
||||
const parent = await git.getCommit('HEAD^')
|
||||
const commit = await git.getCommit('HEAD')
|
||||
expect(parent.subject).toEqual('initial commit')
|
||||
expect(parent.changes).toEqual([
|
||||
{mode: '100644', status: 'A', path: 'README.md'}
|
||||
])
|
||||
expect(commit.subject).toEqual('add sparkles')
|
||||
expect(commit.parents[0]).toEqual(parent.sha)
|
||||
expect(commit.changes).toEqual([
|
||||
{mode: '100644', status: 'M', path: 'README.md'}
|
||||
])
|
||||
})
|
||||
})
|
85
__test__/git-config-helper.int.test.ts
Normal file
85
__test__/git-config-helper.int.test.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import {GitCommandManager} from '../lib/git-command-manager'
|
||||
import {GitConfigHelper} from '../lib/git-config-helper'
|
||||
|
||||
const REPO_PATH = '/git/local/repos/test-base'
|
||||
|
||||
const extraheaderConfigKey = 'http.https://127.0.0.1/.extraheader'
|
||||
|
||||
describe('git-config-helper integration tests', () => {
|
||||
let git: GitCommandManager
|
||||
|
||||
beforeAll(async () => {
|
||||
git = await GitCommandManager.create(REPO_PATH)
|
||||
})
|
||||
|
||||
it('tests save and restore with no persisted auth', async () => {
|
||||
const gitConfigHelper = await GitConfigHelper.create(git)
|
||||
await gitConfigHelper.close()
|
||||
})
|
||||
|
||||
it('tests configure and removal of auth', async () => {
|
||||
const gitConfigHelper = await GitConfigHelper.create(git)
|
||||
await gitConfigHelper.configureToken('github-token')
|
||||
expect(await git.configExists(extraheaderConfigKey)).toBeTruthy()
|
||||
expect(await git.getConfigValue(extraheaderConfigKey)).toEqual(
|
||||
'AUTHORIZATION: basic eC1hY2Nlc3MtdG9rZW46Z2l0aHViLXRva2Vu'
|
||||
)
|
||||
|
||||
await gitConfigHelper.close()
|
||||
expect(await git.configExists(extraheaderConfigKey)).toBeFalsy()
|
||||
})
|
||||
|
||||
it('tests save and restore of persisted auth', async () => {
|
||||
const extraheaderConfigValue = 'AUTHORIZATION: basic ***persisted-auth***'
|
||||
await git.config(extraheaderConfigKey, extraheaderConfigValue)
|
||||
|
||||
const gitConfigHelper = await GitConfigHelper.create(git)
|
||||
|
||||
const exists = await git.configExists(extraheaderConfigKey)
|
||||
expect(exists).toBeFalsy()
|
||||
|
||||
await gitConfigHelper.close()
|
||||
|
||||
const configValue = await git.getConfigValue(extraheaderConfigKey)
|
||||
expect(configValue).toEqual(extraheaderConfigValue)
|
||||
|
||||
const unset = await git.tryConfigUnset(
|
||||
extraheaderConfigKey,
|
||||
'^AUTHORIZATION:'
|
||||
)
|
||||
expect(unset).toBeTruthy()
|
||||
})
|
||||
|
||||
it('tests not adding/removing the safe.directory config when it already exists', async () => {
|
||||
await git.config('safe.directory', '/another-value', true, true)
|
||||
|
||||
const gitConfigHelper = await GitConfigHelper.create(git)
|
||||
|
||||
expect(
|
||||
await git.configExists('safe.directory', '/another-value', true)
|
||||
).toBeTruthy()
|
||||
|
||||
await gitConfigHelper.close()
|
||||
|
||||
const unset = await git.tryConfigUnset(
|
||||
'safe.directory',
|
||||
'/another-value',
|
||||
true
|
||||
)
|
||||
expect(unset).toBeTruthy()
|
||||
})
|
||||
|
||||
it('tests adding and removing the safe.directory config', async () => {
|
||||
const gitConfigHelper = await GitConfigHelper.create(git)
|
||||
|
||||
expect(
|
||||
await git.configExists('safe.directory', REPO_PATH, true)
|
||||
).toBeTruthy()
|
||||
|
||||
await gitConfigHelper.close()
|
||||
|
||||
expect(
|
||||
await git.configExists('safe.directory', REPO_PATH, true)
|
||||
).toBeFalsy()
|
||||
})
|
||||
})
|
93
__test__/git-config-helper.unit.test.ts
Normal file
93
__test__/git-config-helper.unit.test.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import {GitConfigHelper} from '../lib/git-config-helper'
|
||||
|
||||
describe('git-config-helper unit tests', () => {
|
||||
test('parseGitRemote successfully parses HTTPS remote URLs', async () => {
|
||||
const remote1 = GitConfigHelper.parseGitRemote(
|
||||
'https://github.com/peter-evans/create-pull-request'
|
||||
)
|
||||
expect(remote1.hostname).toEqual('github.com')
|
||||
expect(remote1.protocol).toEqual('HTTPS')
|
||||
expect(remote1.repository).toEqual('peter-evans/create-pull-request')
|
||||
|
||||
const remote2 = GitConfigHelper.parseGitRemote(
|
||||
'https://xxx:x-oauth-basic@github.com/peter-evans/create-pull-request'
|
||||
)
|
||||
expect(remote2.hostname).toEqual('github.com')
|
||||
expect(remote2.protocol).toEqual('HTTPS')
|
||||
expect(remote2.repository).toEqual('peter-evans/create-pull-request')
|
||||
|
||||
const remote3 = GitConfigHelper.parseGitRemote(
|
||||
'https://github.com/peter-evans/create-pull-request.git'
|
||||
)
|
||||
expect(remote3.hostname).toEqual('github.com')
|
||||
expect(remote3.protocol).toEqual('HTTPS')
|
||||
expect(remote3.repository).toEqual('peter-evans/create-pull-request')
|
||||
|
||||
const remote4 = GitConfigHelper.parseGitRemote(
|
||||
'https://github.com/peter-evans/ungit'
|
||||
)
|
||||
expect(remote4.hostname).toEqual('github.com')
|
||||
expect(remote4.protocol).toEqual('HTTPS')
|
||||
expect(remote4.repository).toEqual('peter-evans/ungit')
|
||||
|
||||
const remote5 = GitConfigHelper.parseGitRemote(
|
||||
'https://github.com/peter-evans/ungit.git'
|
||||
)
|
||||
expect(remote5.hostname).toEqual('github.com')
|
||||
expect(remote5.protocol).toEqual('HTTPS')
|
||||
expect(remote5.repository).toEqual('peter-evans/ungit')
|
||||
|
||||
const remote6 = GitConfigHelper.parseGitRemote(
|
||||
'https://github.internal.company/peter-evans/create-pull-request'
|
||||
)
|
||||
expect(remote6.hostname).toEqual('github.internal.company')
|
||||
expect(remote6.protocol).toEqual('HTTPS')
|
||||
expect(remote6.repository).toEqual('peter-evans/create-pull-request')
|
||||
})
|
||||
|
||||
test('parseGitRemote successfully parses SSH remote URLs', async () => {
|
||||
const remote1 = GitConfigHelper.parseGitRemote(
|
||||
'git@github.com:peter-evans/create-pull-request.git'
|
||||
)
|
||||
expect(remote1.hostname).toEqual('github.com')
|
||||
expect(remote1.protocol).toEqual('SSH')
|
||||
expect(remote1.repository).toEqual('peter-evans/create-pull-request')
|
||||
|
||||
const remote2 = GitConfigHelper.parseGitRemote(
|
||||
'git@github.com:peter-evans/ungit.git'
|
||||
)
|
||||
expect(remote2.hostname).toEqual('github.com')
|
||||
expect(remote2.protocol).toEqual('SSH')
|
||||
expect(remote2.repository).toEqual('peter-evans/ungit')
|
||||
|
||||
const remote3 = GitConfigHelper.parseGitRemote(
|
||||
'git@github.internal.company:peter-evans/create-pull-request.git'
|
||||
)
|
||||
expect(remote3.hostname).toEqual('github.internal.company')
|
||||
expect(remote3.protocol).toEqual('SSH')
|
||||
expect(remote3.repository).toEqual('peter-evans/create-pull-request')
|
||||
})
|
||||
|
||||
test('parseGitRemote successfully parses GIT remote URLs', async () => {
|
||||
// Unauthenticated git protocol for integration tests only
|
||||
const remote1 = GitConfigHelper.parseGitRemote(
|
||||
'git://127.0.0.1/repos/test-base.git'
|
||||
)
|
||||
expect(remote1.hostname).toEqual('127.0.0.1')
|
||||
expect(remote1.protocol).toEqual('GIT')
|
||||
expect(remote1.repository).toEqual('repos/test-base')
|
||||
})
|
||||
|
||||
test('parseGitRemote fails to parse a remote URL', async () => {
|
||||
const remoteUrl = 'https://github.com/peter-evans'
|
||||
try {
|
||||
GitConfigHelper.parseGitRemote(remoteUrl)
|
||||
// Fail the test if an error wasn't thrown
|
||||
expect(true).toEqual(false)
|
||||
} catch (e: any) {
|
||||
expect(e.message).toEqual(
|
||||
`The format of '${remoteUrl}' is not a valid GitHub repository URL`
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
23
__test__/integration-tests.sh
Executable file
23
__test__/integration-tests.sh
Executable file
@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
IMAGE="cpr-integration-tests:latest"
|
||||
ARG1=${1:-}
|
||||
|
||||
if [[ "$(docker images -q $IMAGE 2> /dev/null)" == "" || $ARG1 == "build" ]]; then
|
||||
echo "Building Docker image $IMAGE ..."
|
||||
|
||||
cat > Dockerfile << EOF
|
||||
FROM node:20-alpine
|
||||
RUN apk --no-cache add git git-daemon
|
||||
RUN npm install jest jest-environment-jsdom --global
|
||||
WORKDIR /cpr
|
||||
COPY __test__/entrypoint.sh /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
EOF
|
||||
|
||||
docker build --no-cache -t $IMAGE .
|
||||
rm Dockerfile
|
||||
fi
|
||||
|
||||
docker run -v $PWD:/cpr $IMAGE
|
120
__test__/utils.unit.test.ts
Normal file
120
__test__/utils.unit.test.ts
Normal file
@ -0,0 +1,120 @@
|
||||
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('stripOrgPrefixFromTeams strips org prefixes correctly', async () => {
|
||||
const array = utils.stripOrgPrefixFromTeams([
|
||||
'org/team1',
|
||||
'org/team2',
|
||||
'team3'
|
||||
])
|
||||
expect(array.length).toEqual(3)
|
||||
expect(array[0]).toEqual('team1')
|
||||
expect(array[1]).toEqual('team2')
|
||||
expect(array[2]).toEqual('team3')
|
||||
})
|
||||
|
||||
test('getRepoPath successfully returns the path to the repository', async () => {
|
||||
expect(utils.getRepoPath()).toEqual(process.env['GITHUB_WORKSPACE'])
|
||||
expect(utils.getRepoPath('foo')).toEqual(
|
||||
path.resolve(process.env['GITHUB_WORKSPACE'] || '', 'foo')
|
||||
)
|
||||
})
|
||||
|
||||
test('getRemoteUrl successfully returns remote URLs', async () => {
|
||||
const url1 = utils.getRemoteUrl(
|
||||
'HTTPS',
|
||||
'github.com',
|
||||
'peter-evans/create-pull-request'
|
||||
)
|
||||
expect(url1).toEqual('https://github.com/peter-evans/create-pull-request')
|
||||
|
||||
const url2 = utils.getRemoteUrl(
|
||||
'SSH',
|
||||
'github.com',
|
||||
'peter-evans/create-pull-request'
|
||||
)
|
||||
expect(url2).toEqual('git@github.com:peter-evans/create-pull-request.git')
|
||||
|
||||
const url3 = utils.getRemoteUrl(
|
||||
'HTTPS',
|
||||
'mygithubserver.com',
|
||||
'peter-evans/create-pull-request'
|
||||
)
|
||||
expect(url3).toEqual(
|
||||
'https://mygithubserver.com/peter-evans/create-pull-request'
|
||||
)
|
||||
})
|
||||
|
||||
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: any) {
|
||||
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: any) {
|
||||
expect(e.message).toEqual(
|
||||
`The format of '${displayNameEmail2}' is not a valid email address with display name`
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
110
action.yml
110
action.yml
@ -2,46 +2,98 @@ name: 'Create Pull Request'
|
||||
description: 'Creates a pull request for changes to your repository in the actions workspace'
|
||||
inputs:
|
||||
token:
|
||||
description: 'GITHUB_TOKEN or a repo scoped PAT'
|
||||
required: true
|
||||
description: 'GITHUB_TOKEN or a `repo` scoped Personal Access Token (PAT)'
|
||||
default: ${{ github.token }}
|
||||
git-token:
|
||||
description: >
|
||||
The Personal Access Token (PAT) that the action will use for git operations.
|
||||
Defaults to the value of `token`.
|
||||
path:
|
||||
description: 'Relative path under $GITHUB_WORKSPACE to the repository.'
|
||||
description: >
|
||||
Relative path under $GITHUB_WORKSPACE to the repository.
|
||||
Defaults to $GITHUB_WORKSPACE.
|
||||
add-paths:
|
||||
description: >
|
||||
A comma or newline-separated list of file paths to commit.
|
||||
Paths should follow git's pathspec syntax.
|
||||
Defaults to adding all new and modified files.
|
||||
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-actions[bot] <41898282+github-actions[bot]@users.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_id }}+${{ github.actor }}@users.noreply.github.com>'
|
||||
signoff:
|
||||
description: 'Add `Signed-off-by` line by the committer at the end of the commit log message.'
|
||||
default: false
|
||||
branch:
|
||||
description: 'The pull request branch name.'
|
||||
base:
|
||||
description: 'The pull request base branch.'
|
||||
default: 'create-pull-request/patch'
|
||||
delete-branch:
|
||||
description: >
|
||||
Delete the `branch` if it doesn't have an active pull request associated with it.
|
||||
default: false
|
||||
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.
|
||||
sign-commits:
|
||||
description: 'Sign commits as `github-actions[bot]` when using `GITHUB_TOKEN`, or your own bot when using GitHub App tokens.'
|
||||
default: false
|
||||
title:
|
||||
description: 'The title of the pull request.'
|
||||
default: 'Changes by create-pull-request action'
|
||||
body:
|
||||
description: 'The body of the pull request.'
|
||||
default: 'Automated changes by [create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub action'
|
||||
body-path:
|
||||
description: 'The path to a file containing the pull request body. Takes precedence over `body`.'
|
||||
labels:
|
||||
description: 'A comma or newline separated list of labels.'
|
||||
assignees:
|
||||
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. It is not possible to change draft status after creation except through the web interface'
|
||||
default: false
|
||||
maintainer-can-modify:
|
||||
description: 'Indicates whether maintainers can modify the pull request.'
|
||||
default: true
|
||||
outputs:
|
||||
pr_number:
|
||||
pull-request-number:
|
||||
description: 'The pull request number'
|
||||
pull-request-url:
|
||||
description: 'The URL of the pull request.'
|
||||
pull-request-operation:
|
||||
description: 'The pull request operation performed by the action, `created`, `updated` or `closed`.'
|
||||
pull-request-head-sha:
|
||||
description: 'The commit SHA of the pull request branch.'
|
||||
pull-request-branch:
|
||||
description: 'The pull request branch name'
|
||||
runs:
|
||||
using: 'node12'
|
||||
using: 'node20'
|
||||
main: 'dist/index.js'
|
||||
branding:
|
||||
icon: 'git-pull-request'
|
||||
icon: 'git-pull-request'
|
||||
color: 'gray-dark'
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 207 KiB |
Binary file not shown.
Before Width: | Height: | Size: 414 KiB |
65832
dist/index.js
vendored
65832
dist/index.js
vendored
File diff suppressed because it is too large
Load Diff
31
dist/src/common.py
vendored
31
dist/src/common.py
vendored
@ -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
|
145
dist/src/create_or_update_branch.py
vendored
145
dist/src/create_or_update_branch.py
vendored
@ -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}
|
126
dist/src/create_or_update_pull_request.py
vendored
126
dist/src/create_or_update_pull_request.py
vendored
@ -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"]
|
||||
)
|
||||
)
|
205
dist/src/create_pull_request.py
vendored
205
dist/src/create_pull_request.py
vendored
@ -1,205 +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
|
||||
path = os.getenv("CPR_PATH", os.getcwd())
|
||||
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 path
|
||||
repo = Repo(path)
|
||||
|
||||
# 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"),
|
||||
)
|
2
dist/src/requirements.txt
vendored
2
dist/src/requirements.txt
vendored
@ -1,2 +0,0 @@
|
||||
GitPython==3.0.5
|
||||
PyGithub==1.45
|
52
dist/src/setup-python.js
vendored
52
dist/src/setup-python.js
vendored
@ -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;
|
39
dist/src/test_common.py
vendored
39
dist/src/test_common.py
vendored
@ -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"
|
||||
)
|
757
dist/src/test_create_or_update_branch.py
vendored
757
dist/src/test_create_or_update_branch.py
vendored
@ -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
|
||||
)
|
@ -29,37 +29,37 @@
|
||||
orientation: "vertical-reverse"
|
||||
});
|
||||
|
||||
const master = gitgraph.branch("master");
|
||||
master.commit("Last commit on base");
|
||||
const localMaster = gitgraph.branch("<#1> master (local)");
|
||||
localMaster.commit({
|
||||
subject: "<uncommited changes>",
|
||||
body: "Changes made to the local base during the workflow",
|
||||
const main = gitgraph.branch("main");
|
||||
main.commit("Last commit on base");
|
||||
const localMain = gitgraph.branch("<#1> main (local)");
|
||||
localMain.commit({
|
||||
subject: "<uncommitted changes>",
|
||||
body: "Changes to the local base during the workflow",
|
||||
})
|
||||
const remotePatch = gitgraph.branch("create-pull-request/patch");
|
||||
remotePatch.merge({
|
||||
branch: localMaster,
|
||||
branch: localMain,
|
||||
commitOptions: {
|
||||
subject: "[create-pull-request] automated change",
|
||||
body: "Changes pushed to create the remote branch",
|
||||
},
|
||||
});
|
||||
master.commit("New commit on base");
|
||||
main.commit("New commit on base");
|
||||
|
||||
const localMaster2 = gitgraph.branch("<#2> master (local)");
|
||||
localMaster2.commit({
|
||||
subject: "<uncommited changes>",
|
||||
body: "Changes made to the updated local base during the workflow",
|
||||
const localMain2 = gitgraph.branch("<#2> main (local)");
|
||||
localMain2.commit({
|
||||
subject: "<uncommitted changes>",
|
||||
body: "Changes to the updated local base during the workflow",
|
||||
})
|
||||
remotePatch.merge({
|
||||
branch: localMaster2,
|
||||
branch: localMain2,
|
||||
commitOptions: {
|
||||
subject: "[create-pull-request] automated change",
|
||||
body: "Changes force pushed to update the remote branch",
|
||||
},
|
||||
});
|
||||
|
||||
master.merge(remotePatch);
|
||||
main.merge(remotePatch);
|
||||
|
||||
</script>
|
||||
|
BIN
docs/assets/cpr-gitgraph.png
Normal file
BIN
docs/assets/cpr-gitgraph.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 108 KiB |
Before Width: | Height: | Size: 416 B After Width: | Height: | Size: 416 B |
BIN
docs/assets/pull-request-example.png
Normal file
BIN
docs/assets/pull-request-example.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 327 KiB |
53
docs/common-issues.md
Normal file
53
docs/common-issues.md
Normal file
@ -0,0 +1,53 @@
|
||||
# Common issues
|
||||
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Create using an existing branch as the PR branch](#create-using-an-existing-branch-as-the-pr-branch)
|
||||
- [Frequently requested features](#use-case-create-a-pull-request-to-update-x-on-release)
|
||||
- [Disable force updates to existing PR branches](#disable-force-updates-to-existing-pr-branches)
|
||||
- [Add a no-verify option to bypass git hooks](#add-a-no-verify-option-to-bypass-git-hooks)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Create using an existing branch as the PR branch
|
||||
|
||||
A common point of confusion is to try and use an existing branch containing changes to raise in a PR as the `branch` input. This will not work because the action is primarily designed to be used in workflows where the PR branch does not exist yet. The action creates and manages the PR branch itself.
|
||||
|
||||
If you have an existing branch that you just want to create a PR for, then I recommend using the official [GitHub CLI](https://cli.github.com/manual/gh_pr_create) in a workflow step.
|
||||
|
||||
Alternatively, if you are trying to keep a branch up to date with another branch, then you can follow [this example](https://github.com/peter-evans/create-pull-request/blob/main/docs/examples.md#keep-a-branch-up-to-date-with-another).
|
||||
|
||||
## Frequently requested features
|
||||
|
||||
### Disable force updates to existing PR branches
|
||||
|
||||
This behaviour is fundamental to how the action works and is a conscious design decision. The "rule" that I based this design on is that when a workflow executes the action to create or update a PR, the result of those two possible actions should never be different. The easiest way to maintain that consistency is to rebase the PR branch and force push it.
|
||||
|
||||
If you want to avoid this behaviour there are some things that might work depending on your use case:
|
||||
- Check if the pull request branch exists in a separate step before the action runs and act accordingly.
|
||||
- Use the [alternative strategy](https://github.com/peter-evans/create-pull-request#alternative-strategy---always-create-a-new-pull-request-branch) of always creating a new PR that won't be updated by the action.
|
||||
- [Create your own commits](https://github.com/peter-evans/create-pull-request#create-your-own-commits) each time the action is created/updated.
|
||||
|
||||
### Add a no-verify option to bypass git hooks
|
||||
|
||||
Presently, there is no plan to add this feature to the action.
|
||||
The reason is that I'm trying very hard to keep the interface for this action to a minimum to prevent it becoming bloated and complicated.
|
||||
|
||||
Git hooks must be installed after a repository is checked out in order for them to work.
|
||||
So the straightforward solution is to just not install them during the workflow where this action is used.
|
||||
|
||||
- If hooks are automatically enabled by a framework, use an option provided by the framework to disable them. For example, for Husky users, they can be disabled with the `--ignore-scripts` flag, or by setting the `HUSKY` environment variable when the action runs.
|
||||
```yml
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
env:
|
||||
HUSKY: '0'
|
||||
```
|
||||
- If hooks are installed in a script, then add a condition checking if the `CI` environment variable exists.
|
||||
```sh
|
||||
#!/bin/sh
|
||||
|
||||
[ -n "$CI" ] && exit 0
|
||||
```
|
||||
- If preventing the hooks installing is problematic, just delete them in a workflow step before the action runs.
|
||||
```yml
|
||||
- run: rm .git/hooks -rf
|
||||
```
|
@ -1,18 +1,29 @@
|
||||
# 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)
|
||||
- [How the action works](#how-the-action-works)
|
||||
- [Guidelines](#guidelines)
|
||||
- [Providing a consistent base](#providing-a-consistent-base)
|
||||
- [Pull request events](#pull-request-events)
|
||||
- [Restrictions on forked repositories](#restrictions-on-forked-repositories)
|
||||
- [Events which checkout a commit](#events-which-checkout-a-commit)
|
||||
- [Restrictions on repository forks](#restrictions-on-repository-forks)
|
||||
- [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)
|
||||
- [Commit signing](#commit-signing)
|
||||
- [Commit signature verification for bots](#commit-signature-verification-for-bots)
|
||||
- [GPG commit signature verification](#gpg-commit-signature-verification)
|
||||
- [Running in a container or on self-hosted runners](#running-in-a-container-or-on-self-hosted-runners)
|
||||
|
||||
## Terminology
|
||||
|
||||
[Pull requests](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests#about-pull-requests) are proposed changes to a repository branch that can be reviewed by a repository's collaborators before being accepted or rejected.
|
||||
[Pull requests](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests#about-pull-requests) are proposed changes to a repository branch that can be reviewed by a repository's collaborators before being accepted or rejected.
|
||||
|
||||
A pull request references two branches:
|
||||
|
||||
@ -21,21 +32,20 @@ A pull request references two branches:
|
||||
|
||||
## Events and checkout
|
||||
|
||||
For each [event type](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/events-that-trigger-workflows) there is a default `GITHUB_SHA` that will be checked out by the GitHub Actions [checkout](https://github.com/actions/checkout) action.
|
||||
|
||||
The majority of events will default to checking out the "last commit on default branch," which in most cases will be the latest commit on `master`.
|
||||
This action expects repositories to be checked out with the official GitHub Actions [checkout](https://github.com/actions/checkout) action.
|
||||
For each [event type](https://docs.github.com/en/actions/reference/events-that-trigger-workflows) there is a default `GITHUB_SHA` that will be checked out.
|
||||
|
||||
The default can be overridden by specifying a `ref` on checkout.
|
||||
|
||||
```yml
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: develop
|
||||
```
|
||||
|
||||
## How the action works
|
||||
|
||||
By default, the action expects to be executed on the pull request `base`—the branch you intend to modify with the proposed changes.
|
||||
Unless the `base` input is supplied, the action expects the target repository to be checked out on the pull request `base`—the branch you intend to modify with the proposed changes.
|
||||
|
||||
Workflow steps:
|
||||
|
||||
@ -45,17 +55,17 @@ Workflow steps:
|
||||
|
||||
The following git diagram shows how the action creates and updates a pull request branch.
|
||||
|
||||

|
||||

|
||||
|
||||
## Guidelines
|
||||
|
||||
### Providing a consistent base
|
||||
|
||||
For the action to work correctly it should be executed in a workflow that checks out a *consistent base* branch. This will be the base of the pull request unless overridden with the `base` input.
|
||||
For the action to work correctly it should be executed in a workflow that checks out a *consistent* base branch. This will be the base of the pull request unless overridden with the `base` input.
|
||||
|
||||
This means your workflow should be consistently checking out the branch that you intend to modify once the PR is merged.
|
||||
|
||||
In the following example, the [`push`](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/events-that-trigger-workflows#push-event-push) and [`create`](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/events-that-trigger-workflows#create-event-create) events both trigger the same workflow. This will cause the checkout action to checkout commits from inconsistent branches. Do *not* do this. It will cause multiple pull requests to be created for each additional `base` the action is executed against.
|
||||
In the following example, the [`push`](https://docs.github.com/en/actions/reference/events-that-trigger-workflows#push) and [`create`](https://docs.github.com/en/actions/reference/events-that-trigger-workflows#create) events both trigger the same workflow. This will cause the checkout action to checkout inconsistent branches and commits. Do *not* do this. It will cause multiple pull requests to be created for each additional `base` the action is executed against.
|
||||
|
||||
```yml
|
||||
on:
|
||||
@ -65,35 +75,51 @@ jobs:
|
||||
example:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
```
|
||||
|
||||
Although rare, there may be use cases where it makes sense to execute the workflow on a branch that is not the base of the pull request. In these cases, the base branch can be specified with the `base` action input. The action will attempt to rebase changes made during the workflow on to the actual base.
|
||||
There may be use cases where it makes sense to execute the workflow on a branch that is not the base of the pull request. In these cases, the base branch can be specified with the `base` action input. The action will attempt to rebase changes made during the workflow on to the actual base.
|
||||
|
||||
### Pull request events
|
||||
### Events which checkout a commit
|
||||
|
||||
Workflows triggered by `pull_request` events will by default check out a [merge commit](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/events-that-trigger-workflows#pull-request-event-pull_request). To prevent the merge commit being included in created pull requests it is necessary to checkout the `head_ref`.
|
||||
The [default checkout](#events-and-checkout) for the majority of events will leave the repository checked out on a branch.
|
||||
However, some events such as `release` and `pull_request` will leave the repository in a "detached HEAD" state.
|
||||
This is because they checkout a commit, not a branch.
|
||||
In these cases, you *must supply* the `base` input so the action can rebase changes made during the workflow for the pull request.
|
||||
|
||||
Workflows triggered by [`pull_request`](https://docs.github.com/en/actions/reference/events-that-trigger-workflows#pull_request) events will by default check out a merge commit. Set the `base` input as follows to base the new pull request on the current pull request's branch.
|
||||
|
||||
```yml
|
||||
- uses: actions/checkout@v2
|
||||
- uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
base: ${{ github.head_ref }}
|
||||
```
|
||||
|
||||
### Restrictions on forked repositories
|
||||
Workflows triggered by [`release`](https://docs.github.com/en/actions/reference/events-that-trigger-workflows#release) events will by default check out a tag. For most use cases, you will need to set the `base` input to the branch name of the tagged commit.
|
||||
|
||||
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.
|
||||
```yml
|
||||
- uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
base: main
|
||||
```
|
||||
|
||||
- Events from forks cannot access secrets, except for for the default `GITHUB_TOKEN`.
|
||||
### Restrictions on repository forks
|
||||
|
||||
GitHub Actions have imposed restrictions on workflow runs triggered by public repository forks.
|
||||
Private repositories can be configured to [enable workflows](https://docs.github.com/en/github/administering-a-repository/disabling-or-limiting-github-actions-for-a-repository#enabling-workflows-for-private-repository-forks) from forks to run without restriction.
|
||||
|
||||
The restrictions apply to the `pull_request` event triggered by a fork opening a pull request in the upstream repository.
|
||||
|
||||
- Events from forks cannot access secrets, except for the default `GITHUB_TOKEN`.
|
||||
> With the exception of GITHUB_TOKEN, secrets are not passed to the runner when a workflow is triggered from a forked repository.
|
||||
|
||||
[GitHub Actions: Using encrypted secrets in a workflow](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets#using-encrypted-secrets-in-a-workflow)
|
||||
[GitHub Actions: Using encrypted secrets in a workflow](https://docs.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets#using-encrypted-secrets-in-a-workflow)
|
||||
|
||||
- The `GITHUB_TOKEN` has read-only access when an event is triggered by a forked repository.
|
||||
|
||||
[GitHub Actions: Permissions for the GITHUB_TOKEN](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/authenticating-with-the-github_token#permissions-for-the-github_token)
|
||||
[GitHub Actions: Permissions for the GITHUB_TOKEN](https://docs.github.com/en/actions/configuring-and-managing-workflows/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 any attempt.
|
||||
|
||||
A job condition can be added to prevent workflows from executing when triggered by a repository fork.
|
||||
|
||||
@ -105,3 +131,292 @@ jobs:
|
||||
# Check if the event is not triggered by a fork
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
```
|
||||
|
||||
For further reading regarding the security of pull requests, see this GitHub blog post titled [Keeping your GitHub Actions and workflows secure: Preventing pwn requests](https://securitylab.github.com/research/github-actions-preventing-pwn-requests/)
|
||||
|
||||
### 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, events triggered by the `GITHUB_TOKEN` will not create a new workflow run. This prevents you from accidentally creating recursive workflow runs. For example, if a workflow run pushes code using the repository's `GITHUB_TOKEN`, a new workflow will not run even when the repository contains a workflow configured to run when `push` events occur.
|
||||
|
||||
[GitHub Actions: Triggering a workflow from a workflow](https://docs.github.com/en/actions/using-workflows/triggering-a-workflow#triggering-a-workflow-from-a-workflow)
|
||||
|
||||
#### 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. To prevent merging of pull requests without checks erroneously, use [branch protection rules](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests).
|
||||
|
||||
- Use a `repo` scoped [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) created on an account that has write access to the repository that pull requests are being created in. This is the standard workaround and [recommended by GitHub](https://docs.github.com/en/actions/using-workflows/triggering-a-workflow#triggering-a-workflow-from-a-workflow). However, the PAT cannot be scoped to a specific repository so the token becomes a very sensitive secret. If this is a concern, the PAT can instead be created for a dedicated [machine account](https://docs.github.com/en/github/site-policy/github-terms-of-service#3-account-requirements) that has collaborator access to the repository. Also note that because the account that owns the PAT will be the creator of pull requests, that user account will be unable to perform actions such as request changes or approve the pull request.
|
||||
|
||||
- Use [SSH (deploy keys)](#push-using-ssh-deploy-keys) to push the pull request branch. This is arguably more secure than using a PAT because deploy keys can be set per repository. However, this method will only trigger `on: push` workflows.
|
||||
|
||||
- Use a [machine account that creates pull requests from its own fork](#push-pull-request-branches-to-a-fork). This is the most secure because the PAT created only grants access to the machine account's fork, not the main repository. This method will trigger `on: pull_request` workflows to run. Workflows triggered `on: push` will not run because the push event is in the fork.
|
||||
|
||||
- Use a [GitHub App to generate a token](#authenticating-with-github-app-generated-tokens) that can be used with this action. GitHub App generated tokens are more secure than using a 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.
|
||||
```yml
|
||||
- uses: thirdparty/foo-action@172ec762f2ac8e050062398456fccd30444f8f30
|
||||
```
|
||||
|
||||
This action uses [ncc](https://github.com/vercel/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/main/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://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) is required.
|
||||
|
||||
```yml
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
repository: owner/repo
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- uses: peter-evans/create-pull-request@v6
|
||||
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://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token).
|
||||
Allowing the action to push with a configured deploy key will trigger `on: push` workflows. This makes it an alternative to using a PAT to trigger checks for pull requests.
|
||||
Note that you cannot use deploy keys alone to [create a pull request in a remote repository](#creating-pull-requests-in-a-remote-repository) because then using a PAT would become a requirement. This method only makes sense if creating a pull request in the repository where the workflow is running.
|
||||
|
||||
How to use SSH (deploy keys) with create-pull-request action:
|
||||
|
||||
1. [Create a new SSH key pair](https://docs.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@v4
|
||||
with:
|
||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
```
|
||||
|
||||
### 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://docs.github.com/en/github/site-policy/github-terms-of-service#3-account-requirements).
|
||||
This user only has `read` access to the main repository.
|
||||
It will use their own fork to push code and create the pull request.
|
||||
Note that if you choose to use this method (not give the machine account `write` access to the repository) the following inputs cannot be used: `labels`, `assignees`, `reviewers`, `team-reviewers` and `milestone`.
|
||||
|
||||
1. Create a new GitHub user and login.
|
||||
2. Fork the repository that you will be creating pull requests in.
|
||||
3. Create a [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token).
|
||||
4. Logout and log back into your main user account.
|
||||
5. Add a secret to your repository containing the above PAT.
|
||||
6. As shown in the following example workflow, set the `push-to-fork` input to the full repository name of the fork.
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ secrets.MACHINE_USER_PAT }}
|
||||
push-to-fork: machine-user/fork-of-repository
|
||||
```
|
||||
|
||||
Note: You can also combine `push-to-fork` with [creating pull requests in a remote repository](#creating-pull-requests-in-a-remote-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://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token).
|
||||
GitHub App generated tokens are more secure than using a PAT because GitHub App access permissions can be set with finer granularity and are scoped to only repositories where the App is installed.
|
||||
|
||||
1. Create a minimal [GitHub App](https://docs.github.com/en/developers/apps/creating-a-github-app), setting the following fields:
|
||||
|
||||
- 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`.
|
||||
- Under `Organization permissions: Members` select `Access: Read-only`.
|
||||
- **NOTE**: Only needed if you would like add teams as reviewers to PRs.
|
||||
|
||||
2. Create a Private key from the App settings page and store it securely.
|
||||
|
||||
3. Install the App on any repository where workflows will run requiring tokens.
|
||||
|
||||
4. Set secrets on your repository containing the GitHub App ID, and the private key you created in step 2. e.g. `APP_ID`, `APP_PRIVATE_KEY`.
|
||||
|
||||
5. The following example workflow shows how to use [actions/create-github-app-token](https://github.com/actions/create-github-app-token) to generate a token for use with this action.
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/create-github-app-token@v1
|
||||
id: generate-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
```
|
||||
|
||||
### Commit signing
|
||||
|
||||
[Commit signature verification](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification) is a feature where GitHub will mark signed commits as "verified" to give confidence that changes are from a trusted source. Some organizations require commit signing, and enforce it with branch protection rules.
|
||||
|
||||
The action supports two methods to sign commits, [commit signature verification for bots](#commit-signature-verification-for-bots), and [GPG commit signature verification](#gpg-commit-signature-verification).
|
||||
|
||||
#### Commit signature verification for bots
|
||||
|
||||
The action can sign commits as `github-actions[bot]` when using the repository's default `GITHUB_TOKEN`, or your own bot when using [GitHub App tokens](#authenticating-with-github-app-generated-tokens).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> - When setting `sign-commits: true` the action will ignore the `committer` and `author` inputs.
|
||||
> - If you attempt to use a [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) the action will create the pull request, but commits will not be signed. Commit signing is only supported with bot generated tokens.
|
||||
|
||||
In this example the `token` input is not supplied, so the action will use the repository's default `GITHUB_TOKEN`. This will sign commits as `github-actions[bot]`.
|
||||
```yaml
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
sign-commits: true
|
||||
```
|
||||
|
||||
In this example, the `token` input is generated using a GitHub App. This will sign commits as `<application-name>[bot]`.
|
||||
```yaml
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/create-github-app-token@v1
|
||||
id: generate-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
sign-commits: true
|
||||
```
|
||||
|
||||
#### GPG commit signature verification
|
||||
|
||||
The action can use GPG to sign commits with a GPG key that you generate yourself.
|
||||
|
||||
1. Follow GitHub's guide to [generate a new GPG key](https://docs.github.com/en/github/authenticating-to-github/generating-a-new-gpg-key).
|
||||
|
||||
2. [Add the public key](https://docs.github.com/en/github/authenticating-to-github/adding-a-new-gpg-key-to-your-github-account) to the user account associated with the [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) that you will use with the action.
|
||||
|
||||
3. Copy the private key to your clipboard, replacing `email@example.com` with the email address of your GPG key.
|
||||
```
|
||||
# macOS
|
||||
gpg --armor --export-secret-key email@example.com | pbcopy
|
||||
```
|
||||
|
||||
4. Paste the private key into a repository secret where the workflow will run. e.g. `GPG_PRIVATE_KEY`
|
||||
|
||||
5. Create another repository secret for the key's passphrase, if applicable. e.g. `GPG_PASSPHRASE`
|
||||
|
||||
6. The following example workflow shows how to use [crazy-max/ghaction-import-gpg](https://github.com/crazy-max/ghaction-import-gpg) to import your GPG key and allow the action to sign commits.
|
||||
|
||||
Note that the `committer` email address *MUST* match the email address used to create your GPG key.
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: crazy-max/ghaction-import-gpg@v5
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
git_user_signingkey: true
|
||||
git_commit_gpgsign: true
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
committer: example <email@example.com>
|
||||
```
|
||||
|
||||
### 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@v4
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
```
|
||||
|
||||
**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@v4
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
```
|
||||
|
333
docs/examples.md
333
docs/examples.md
@ -2,9 +2,15 @@
|
||||
|
||||
- [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 on release](#use-case-create-a-pull-request-to-update-x-on-release)
|
||||
- [Update changelog](#update-changelog)
|
||||
- [Use case: Create a pull request to update X periodically](#use-case-create-a-pull-request-to-update-x-periodically)
|
||||
- [Update NPM dependencies](#update-npm-dependencies)
|
||||
- [Update Gradle dependencies](#update-gradle-dependencies)
|
||||
- [Update Cargo dependencies](#update-cargo-dependencies)
|
||||
- [Update SwaggerUI for GitHub Pages](#update-swaggerui-for-github-pages)
|
||||
- [Keep a fork up-to-date with its upstream](#keep-a-fork-up-to-date-with-its-upstream)
|
||||
- [Spider and download a website](#spider-and-download-a-website)
|
||||
- [Use case: Create a pull request to update X by calling the GitHub API](#use-case-create-a-pull-request-to-update-x-by-calling-the-github-api)
|
||||
- [Call the GitHub API from an external service](#call-the-github-api-from-an-external-service)
|
||||
@ -14,6 +20,7 @@
|
||||
- [Misc workflow tips](#misc-workflow-tips)
|
||||
- [Filtering push events](#filtering-push-events)
|
||||
- [Dynamic configuration using variables](#dynamic-configuration-using-variables)
|
||||
- [Using a markdown template](#using-a-markdown-template)
|
||||
- [Debugging GitHub Actions](#debugging-github-actions)
|
||||
|
||||
|
||||
@ -30,60 +37,230 @@ name: Update AUTHORS
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
jobs:
|
||||
updateAuthors:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
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@v2
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
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 `main`. Merging the pull request is effectively promoting those changes to production.
|
||||
|
||||
```yml
|
||||
name: Create production promotion pull request
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
productionPromotion:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: production
|
||||
- name: Reset promotion branch
|
||||
run: |
|
||||
git fetch origin main:main
|
||||
git reset --hard main
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
branch: production-promotion
|
||||
```
|
||||
|
||||
## Use case: Create a pull request to update X on release
|
||||
|
||||
This pattern will work well for updating any kind of static content based on the tagged commit of a release. Note that because `release` is one of the [events which checkout a commit](concepts-guidelines.md#events-which-checkout-a-commit) it is necessary to supply the `base` input to the action.
|
||||
|
||||
### Update changelog
|
||||
|
||||
Raises a pull request to update the `CHANGELOG.md` file based on the tagged commit of the release.
|
||||
Note that [git-chglog](https://github.com/git-chglog/git-chglog/) requires some configuration files to exist in the repository before this workflow will work.
|
||||
|
||||
This workflow assumes the tagged release was made on a default branch called `main`.
|
||||
|
||||
```yml
|
||||
name: Update Changelog
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
jobs:
|
||||
updateChangelog:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Update Changelog
|
||||
run: |
|
||||
curl -o git-chglog -L https://github.com/git-chglog/git-chglog/releases/download/0.9.1/git-chglog_linux_amd64
|
||||
chmod u+x git-chglog
|
||||
./git-chglog -o CHANGELOG.md
|
||||
rm git-chglog
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
commit-message: update changelog
|
||||
title: Update Changelog
|
||||
body: Update changelog to reflect release changes
|
||||
branch: update-changelog
|
||||
base: main
|
||||
```
|
||||
|
||||
## 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://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) can be used in order for the creation of the pull request to trigger further workflows. See the [documentation here](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
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '10.x'
|
||||
node-version: '16.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@v6
|
||||
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
|
||||
```
|
||||
|
||||
The above workflow works best in combination with a build workflow triggered on `push` and `pull_request`.
|
||||
|
||||
```yml
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.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@v4
|
||||
- uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
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@v6
|
||||
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 Cargo dependencies
|
||||
|
||||
The following workflow will create a pull request for Cargo dependencies.
|
||||
It optionally uses [`cargo-edit`](https://github.com/killercup/cargo-edit) to update `Cargo.toml` and keep it in sync with `Cargo.lock`.
|
||||
|
||||
```yml
|
||||
name: Update Dependencies
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 1 * * 1'
|
||||
jobs:
|
||||
update-dep:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Update dependencies
|
||||
run: |
|
||||
cargo install cargo-edit
|
||||
cargo update
|
||||
cargo upgrade --to-lockfile
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
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
|
||||
@ -100,12 +277,14 @@ jobs:
|
||||
updateSwagger:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Get Latest Swagger UI Release
|
||||
id: swagger-ui
|
||||
run: |
|
||||
echo ::set-output name=release_tag::$(curl -sL https://api.github.com/repos/swagger-api/swagger-ui/releases/latest | jq -r ".tag_name")
|
||||
echo ::set-output name=current_tag::$(<swagger-ui.version)
|
||||
release_tag=$(curl -sL https://api.github.com/repos/swagger-api/swagger-ui/releases/latest | jq -r ".tag_name")
|
||||
echo "release_tag=$release_tag" >> $GITHUB_OUTPUT
|
||||
current_tag=$(<swagger-ui.version)
|
||||
echo "current_tag=$current_tag" >> $GITHUB_OUTPUT
|
||||
- name: Update Swagger UI
|
||||
if: steps.swagger-ui.outputs.current_tag != steps.swagger-ui.outputs.release_tag
|
||||
env:
|
||||
@ -128,9 +307,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@v6
|
||||
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: |
|
||||
@ -144,6 +322,41 @@ jobs:
|
||||
branch: swagger-ui-updates
|
||||
```
|
||||
|
||||
### Keep a fork up-to-date with its upstream
|
||||
|
||||
This example is designed to be run in a seperate repository from the fork repository itself.
|
||||
The aim of this is to prevent committing anything to the fork's default branch would cause it to differ from the upstream.
|
||||
|
||||
In the following example workflow, `owner/repo` is the upstream repository and `fork-owner/repo` is the fork. It assumes the default branch of the upstream repository is called `main`.
|
||||
|
||||
The [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) should have `repo` scope. Additionally, if the upstream makes changes to the `.github/workflows` directory, the action will be unable to push the changes to a branch and throw the error "_(refusing to allow a GitHub App to create or update workflow `.github/workflows/xxx.yml` without `workflows` permission)_". To allow these changes to be pushed to the fork, add the `workflow` scope to the PAT. Of course, allowing this comes with the risk that the workflow changes from the upstream could run and do something unexpected. Disabling GitHub Actions in the fork is highly recommended to prevent this.
|
||||
|
||||
When you merge the pull request make sure to choose the [`Rebase and merge`](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-request-merges#rebase-and-merge-your-pull-request-commits) option. This will make the fork's commits match the commits on the upstream.
|
||||
|
||||
```yml
|
||||
name: Update fork
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
jobs:
|
||||
updateFork:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: fork-owner/repo
|
||||
- name: Reset the default branch with upstream changes
|
||||
run: |
|
||||
git remote add upstream https://github.com/owner/repo.git
|
||||
git fetch upstream main:upstream-main
|
||||
git reset --hard upstream-main
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
branch: upstream-changes
|
||||
```
|
||||
|
||||
### Spider and download a website
|
||||
|
||||
This workflow spiders a website and downloads the content. Any changes to the website will be raised in a pull request.
|
||||
@ -157,7 +370,7 @@ jobs:
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Download website
|
||||
run: |
|
||||
wget \
|
||||
@ -171,9 +384,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@v6
|
||||
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.
|
||||
@ -182,7 +394,7 @@ jobs:
|
||||
|
||||
## Use case: Create a pull request to update X by calling the GitHub API
|
||||
|
||||
You can use the GitHub API to trigger a webhook event called [`repository_dispatch`](https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows#external-events-repository_dispatch) when you want to trigger a workflow for activity that happens outside of GitHub.
|
||||
You can use the GitHub API to trigger a webhook event called [`repository_dispatch`](https://docs.github.com/en/actions/reference/events-that-trigger-workflows#repository_dispatch) when you want to trigger a workflow for any activity that happens outside of GitHub.
|
||||
This pattern will work well for updating any kind of static content from an external source.
|
||||
|
||||
You can modify any of the examples in the previous section to work in this fashion.
|
||||
@ -200,7 +412,7 @@ on:
|
||||
An `on: repository_dispatch` workflow can be triggered by a call to the GitHub API as follows.
|
||||
|
||||
- `[username]` is a GitHub username
|
||||
- `[token]` is a `repo` scoped [Personal Access Token](https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line)
|
||||
- `[token]` is a `repo` scoped [Personal Access Token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token)
|
||||
- `[repository]` is the name of the repository the workflow resides in.
|
||||
|
||||
```
|
||||
@ -217,7 +429,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@v2
|
||||
with:
|
||||
token: ${{ secrets.REPO_ACCESS_TOKEN }}
|
||||
repository: username/my-repo
|
||||
@ -231,7 +443,8 @@ An `on: repository_dispatch` workflow can be triggered from another workflow wit
|
||||
|
||||
This is a pattern that lends itself to automated code linting and fixing. A pull request can be created to fix or modify something during an `on: pull_request` workflow. The pull request containing the fix will be raised with the original pull request as the base. This can be then be merged to update the original pull request and pass any required tests.
|
||||
|
||||
Note that due to [limitations on forked repositories](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/authenticating-with-the-github_token#permissions-for-the-github_token) workflows for this use case do not work for pull requests raised from forks.
|
||||
Note that due to [token restrictions on public repository forks](https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#permissions-for-the-github_token), workflows for this use case do not work for pull requests raised from forks.
|
||||
Private repositories can be configured to [enable workflows](https://docs.github.com/en/github/administering-a-repository/disabling-or-limiting-github-actions-for-a-repository#enabling-workflows-for-private-repository-forks) from forks to run without restriction.
|
||||
|
||||
### autopep8
|
||||
|
||||
@ -253,22 +466,23 @@ jobs:
|
||||
if: startsWith(github.head_ref, 'autopep8-patches') == false && github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
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
|
||||
id: vars
|
||||
run: echo ::set-output name=branch-name::"autopep8-patches/${{ github.head_ref }}"
|
||||
run: |
|
||||
branch-name="autopep8-patches/${{ github.head_ref }}"
|
||||
echo "branch-name=$branch-name" >> $GITHUB_OUTPUT
|
||||
- name: Create Pull Request
|
||||
if: steps.autopep8.outputs.exit-code == 2
|
||||
uses: peter-evans/create-pull-request@v2
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
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.
|
||||
@ -301,63 +515,72 @@ jobs:
|
||||
if: startsWith(github.ref, 'refs/heads/')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
...
|
||||
|
||||
someOtherJob:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
...
|
||||
```
|
||||
|
||||
### Dynamic configuration using variables
|
||||
|
||||
The following examples show how configuration for the action can be dynamically defined in a previous workflow step.
|
||||
|
||||
The recommended method is to use [`set-output`](https://help.github.com/en/github/automating-your-workflow-with-github-actions/development-tools-for-github-actions#set-an-output-parameter-set-output). Note that the step where output variables are defined must have an id.
|
||||
Note that the step where output variables are defined must have an id.
|
||||
|
||||
```yml
|
||||
- name: Set output variables
|
||||
id: vars
|
||||
run: |
|
||||
echo ::set-output name=pr_title::"[Test] Add report file $(date +%d-%m-%Y)"
|
||||
echo ::set-output name=pr_body::"This PR was auto-generated on $(date +%d-%m-%Y) \
|
||||
pr_title="[Test] Add report file $(date +%d-%m-%Y)"
|
||||
pr_body="This PR was auto-generated on $(date +%d-%m-%Y) \
|
||||
by [create-pull-request](https://github.com/peter-evans/create-pull-request)."
|
||||
echo "pr_title=$pr_title" >> $GITHUB_OUTPUT
|
||||
echo "pr_body=$pr_body" >> $GITHUB_OUTPUT
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v2
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
title: ${{ steps.vars.outputs.pr_title }}
|
||||
body: ${{ steps.vars.outputs.pr_body }}
|
||||
```
|
||||
|
||||
Alternatively, [`set-env`](https://help.github.com/en/github/automating-your-workflow-with-github-actions/development-tools-for-github-actions#set-an-environment-variable-set-env) can be used to create environment variables.
|
||||
### Using a markdown template
|
||||
|
||||
In this example, a markdown template file is added to the repository at `.github/pull-request-template.md` with the following content.
|
||||
```
|
||||
This is a test pull request template
|
||||
Render template variables such as {{ .foo }} and {{ .bar }}.
|
||||
```
|
||||
|
||||
The template is rendered using the [render-template](https://github.com/chuhlomin/render-template) action and the result is used to create the pull request.
|
||||
```yml
|
||||
- name: Set environment variables
|
||||
run: |
|
||||
echo ::set-env name=PULL_REQUEST_TITLE::"[Test] Add report file $(date +%d-%m-%Y)"
|
||||
echo ::set-env name=PULL_REQUEST_BODY::"This PR was auto-generated on $(date +%d-%m-%Y) \
|
||||
by [create-pull-request](https://github.com/peter-evans/create-pull-request)."
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v2
|
||||
- name: Render template
|
||||
id: template
|
||||
uses: chuhlomin/render-template@v1.4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
title: ${{ env.PULL_REQUEST_TITLE }}
|
||||
body: ${{ env.PULL_REQUEST_BODY }}
|
||||
template: .github/pull-request-template.md
|
||||
vars: |
|
||||
foo: this
|
||||
bar: that
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
body: ${{ steps.template.outputs.result }}
|
||||
```
|
||||
|
||||
### Debugging GitHub Actions
|
||||
|
||||
#### Runner Diagnostic Logging
|
||||
|
||||
[Runner diagnostic logging](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/managing-a-workflow-run#enabling-runner-diagnostic-logging) provides additional log files that contain information about how a runner is executing an action.
|
||||
[Runner diagnostic logging](https://docs.github.com/en/actions/configuring-and-managing-workflows/managing-a-workflow-run#enabling-runner-diagnostic-logging) provides additional log files that contain information about how a runner is executing an action.
|
||||
To enable runner diagnostic logging, set the secret `ACTIONS_RUNNER_DEBUG` to `true` in the repository that contains the workflow.
|
||||
|
||||
#### Step Debug Logging
|
||||
|
||||
[Step debug logging](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/managing-a-workflow-run#enabling-step-debug-logging) increases the verbosity of a job's logs during and after a job's execution.
|
||||
[Step debug logging](https://docs.github.com/en/actions/configuring-and-managing-workflows/managing-a-workflow-run#enabling-step-debug-logging) increases the verbosity of a job's logs during and after a job's execution.
|
||||
To enable step debug logging set the secret `ACTIONS_STEP_DEBUG` to `true` in the repository that contains the workflow.
|
||||
|
||||
#### Output Various Contexts
|
||||
|
111
docs/updating.md
111
docs/updating.md
@ -1,25 +1,124 @@
|
||||
# Updating from `v1` to `v2`
|
||||
## Updating from `v5` to `v6`
|
||||
|
||||
## Breaking changes
|
||||
### Behaviour changes
|
||||
|
||||
- The default values for `author` and `committer` have changed. See "What's new" below for details. If you are overriding the default values you will not be affected by this change.
|
||||
- On completion, the action now removes the temporary git remote configuration it adds when using `push-to-fork`. This should not affect you unless you were using the temporary configuration for some other purpose after the action completes.
|
||||
|
||||
### What's new
|
||||
|
||||
- Updated runtime to Node.js 20
|
||||
- The action now requires a minimum version of [v2.308.0](https://github.com/actions/runner/releases/tag/v2.308.0) for the Actions runner. Update self-hosted runners to v2.308.0 or later to ensure compatibility.
|
||||
- The default value for `author` has been changed to `${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>`. The change adds the `${{ github.actor_id }}+` prefix to the email address to align with GitHub's standard format for the author email address.
|
||||
- The default value for `committer` has been changed to `github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>`. This is to align with the default GitHub Actions bot user account.
|
||||
- Adds input `git-token`, the [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) that the action will use for git operations. This input defaults to the value of `token`. Use this input if you would like the action to use a different token for git operations than the one used for the GitHub API.
|
||||
- `push-to-fork` now supports pushing to sibling repositories in the same network.
|
||||
- Previously, when using `push-to-fork`, the action did not remove temporary git remote configuration it adds during execution. This has been fixed and the configuration is now removed when the action completes.
|
||||
- If the pull request body is truncated due to exceeding the maximum length, the action will now suffix the body with the message "...*[Pull request body truncated]*" to indicate that the body has been truncated.
|
||||
- The action now uses `--unshallow` only when necessary, rather than as a default argument of `git fetch`. This should improve performance, particularly for large git repositories with extensive commit history.
|
||||
- The action can now be executed on one GitHub server and create pull requests on a *different* GitHub server. Server products include GitHub hosted (github.com), GitHub Enterprise Server (GHES), and GitHub Enterprise Cloud (GHEC). For example, the action can be executed on GitHub hosted and create pull requests on a GHES or GHEC instance.
|
||||
|
||||
## Updating from `v4` to `v5`
|
||||
|
||||
### Behaviour changes
|
||||
|
||||
- The action will no longer leave the local repository checked out on the pull request `branch`. Instead, it will leave the repository checked out on the branch or commit that it was when the action started.
|
||||
- When using `add-paths`, uncommitted changes will no longer be destroyed. They will be stashed and restored at the end of the action run.
|
||||
|
||||
### What's new
|
||||
|
||||
- Adds input `body-path`, the path to a file containing the pull request body.
|
||||
- At the end of the action run the local repository is now checked out on the branch or commit that it was when the action started.
|
||||
- Any uncommitted tracked or untracked changes are now stashed and restored at the end of the action run. Currently, this can only occur when using the `add-paths` input, which allows for changes to not be committed. Previously, any uncommitted changes would be destroyed.
|
||||
- The proxy implementation has been revised but is not expected to have any change in behaviour. It continues to support the standard environment variables `http_proxy`, `https_proxy` and `no_proxy`.
|
||||
- Now sets the git `safe.directory` configuration for the local repository path. The configuration is removed when the action completes. Fixes issue https://github.com/peter-evans/create-pull-request/issues/1170.
|
||||
- Now determines the git directory path using the `git rev-parse --git-dir` command. This allows users with custom repository configurations to use the action.
|
||||
- Improved handling of the `team-reviewers` input and associated errors.
|
||||
|
||||
## Updating from `v3` to `v4`
|
||||
|
||||
### Behaviour changes
|
||||
|
||||
- The `add-paths` input no longer accepts `-A` as a valid value. When committing all new and modified files the `add-paths` input should be omitted.
|
||||
|
||||
- If using self-hosted runners or GitHub Enterprise Server, there are minimum requirements for `v4` to run. See "What's new" below for details.
|
||||
|
||||
### What's new
|
||||
|
||||
- Updated runtime to Node.js 16
|
||||
- The action now requires a minimum version of v2.285.0 for the [Actions Runner](https://github.com/actions/runner/releases/tag/v2.285.0).
|
||||
- If using GitHub Enterprise Server, the action requires [GHES 3.4](https://docs.github.com/en/enterprise-server@3.4/admin/release-notes) or later.
|
||||
|
||||
## Updating from `v2` to `v3`
|
||||
|
||||
### Behaviour 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
|
||||
```
|
||||
|
||||
### What's new
|
||||
|
||||
- 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`
|
||||
|
||||
### Behaviour changes
|
||||
|
||||
- `v2` now expects repositories to be checked out with `actions/checkout@v2`
|
||||
|
||||
To use `actions/checkout@v1` the following step to checkout the branch is necessary.
|
||||
```
|
||||
```yml
|
||||
- uses: actions/checkout@v1
|
||||
- name: Checkout branch
|
||||
run: git checkout "${GITHUB_REF:11}"
|
||||
```
|
||||
|
||||
- The two branch naming strategies have been swapped. Fixed branch naming strategy is now the default. i.e. `branch-suffix: none` is now the default and should be removed from configuration if set.
|
||||
- The two branch naming strategies have been swapped. Fixed-branch naming strategy is now the default. i.e. `branch-suffix: none` is now the default and should be removed from configuration if set.
|
||||
|
||||
- `author-name`, `author-email`, `committer-name`, `committer-email` have been removed in favour of `author` and `committer`.
|
||||
They can both be set in the format `Display Name <email@address.com>`
|
||||
|
||||
If neither `author` or `committer` are set the action will default to making commits as the GitHub Actions bot user.
|
||||
|
||||
## New features
|
||||
### What's new
|
||||
|
||||
- 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.
|
||||
- Unpushed commits made during the workflow before the action runs will now be considered as changes to be raised in the pull request. See [Create your own commits](https://github.com/peter-evans/create-pull-request#create-your-own-commits) for details.
|
||||
- New commits made to the pull request base will now be taken into account when pull requests are updated.
|
||||
- If an updated pull request no longer differs from its base it will automatically be closed and the pull request branch deleted.
|
||||
|
70
index.js
70
index.js
@ -1,70 +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"),
|
||||
path: core.getInput("path"),
|
||||
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.path) process.env.CPR_PATH = inputs.path;
|
||||
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
11
jest.config.js
Normal file
@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
clearMocks: true,
|
||||
moduleFileExtensions: ['js', 'ts'],
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/*.test.ts'],
|
||||
testRunner: 'jest-circus/runner',
|
||||
transform: {
|
||||
'^.+\\.ts$': 'ts-jest'
|
||||
},
|
||||
verbose: true
|
||||
}
|
8191
package-lock.json
generated
8191
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
51
package.json
51
package.json
@ -1,16 +1,27 @@
|
||||
{
|
||||
"name": "create-pull-request",
|
||||
"version": "2.0.0",
|
||||
"version": "6.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,35 @@
|
||||
},
|
||||
"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.10.1",
|
||||
"@actions/exec": "^1.1.1",
|
||||
"@octokit/core": "^6.1.2",
|
||||
"@octokit/plugin-paginate-rest": "^11.3.3",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^13.2.4",
|
||||
"@octokit/plugin-throttling": "^9.3.1",
|
||||
"p-limit": "^6.1.0",
|
||||
"proxy-from-env": "^1.1.0",
|
||||
"undici": "^6.19.7",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@zeit/ncc": "0.21.0"
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^18.19.44",
|
||||
"@typescript-eslint/eslint-plugin": "^7.17.0",
|
||||
"@typescript-eslint/parser": "^7.17.0",
|
||||
"@vercel/ncc": "^0.38.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-github": "^4.10.2",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-jest": "^27.9.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"jest": "^29.7.0",
|
||||
"jest-circus": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"prettier": "^3.3.3",
|
||||
"ts-jest": "^29.2.4",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base"
|
||||
]
|
||||
}
|
@ -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
|
337
src/create-or-update-branch.ts
Normal file
337
src/create-or-update-branch.ts
Normal file
@ -0,0 +1,337 @@
|
||||
import * as core from '@actions/core'
|
||||
import {GitCommandManager, Commit} 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.'
|
||||
const NOTHING_TO_COMMIT = 'nothing to commit, working tree clean'
|
||||
|
||||
const FETCH_DEPTH_MARGIN = 10
|
||||
|
||||
export enum WorkingBaseType {
|
||||
Branch = 'branch',
|
||||
Commit = 'commit'
|
||||
}
|
||||
|
||||
export async function getWorkingBaseAndType(
|
||||
git: GitCommandManager
|
||||
): Promise<[string, WorkingBaseType]> {
|
||||
const symbolicRefResult = await git.exec(
|
||||
['symbolic-ref', 'HEAD', '--short'],
|
||||
true
|
||||
)
|
||||
if (symbolicRefResult.exitCode == 0) {
|
||||
// A ref is checked out
|
||||
return [symbolicRefResult.stdout.trim(), WorkingBaseType.Branch]
|
||||
} else {
|
||||
// A commit is checked out (detached HEAD)
|
||||
const headSha = await git.revParse('HEAD')
|
||||
return [headSha, WorkingBaseType.Commit]
|
||||
}
|
||||
}
|
||||
|
||||
export async function tryFetch(
|
||||
git: GitCommandManager,
|
||||
remote: string,
|
||||
branch: string,
|
||||
depth: number
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await git.fetch([`${branch}:refs/remotes/${remote}/${branch}`], remote, [
|
||||
'--force',
|
||||
`--depth=${depth}`
|
||||
])
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function buildBranchCommits(
|
||||
git: GitCommandManager,
|
||||
base: string,
|
||||
branch: string
|
||||
): Promise<Commit[]> {
|
||||
const output = await git.exec(['log', '--format=%H', `${base}..${branch}`])
|
||||
const shas = output.stdout
|
||||
.split('\n')
|
||||
.filter(x => x !== '')
|
||||
.reverse()
|
||||
const commits: Commit[] = []
|
||||
for (const sha of shas) {
|
||||
const commit = await git.getCommit(sha)
|
||||
commits.push(commit)
|
||||
}
|
||||
return commits
|
||||
}
|
||||
|
||||
// Return the number of commits that branch2 is ahead of branch1
|
||||
async function commitsAhead(
|
||||
git: GitCommandManager,
|
||||
branch1: string,
|
||||
branch2: string
|
||||
): Promise<number> {
|
||||
const result = await git.revList(
|
||||
[`${branch1}...${branch2}`],
|
||||
['--right-only', '--count']
|
||||
)
|
||||
return Number(result)
|
||||
}
|
||||
|
||||
// Return true if branch2 is ahead of branch1
|
||||
async function isAhead(
|
||||
git: GitCommandManager,
|
||||
branch1: string,
|
||||
branch2: string
|
||||
): Promise<boolean> {
|
||||
return (await commitsAhead(git, branch1, branch2)) > 0
|
||||
}
|
||||
|
||||
// Return the number of commits that branch2 is behind branch1
|
||||
async function commitsBehind(
|
||||
git: GitCommandManager,
|
||||
branch1: string,
|
||||
branch2: string
|
||||
): Promise<number> {
|
||||
const result = await git.revList(
|
||||
[`${branch1}...${branch2}`],
|
||||
['--left-only', '--count']
|
||||
)
|
||||
return Number(result)
|
||||
}
|
||||
|
||||
// Return true if branch2 is behind branch1
|
||||
async function isBehind(
|
||||
git: GitCommandManager,
|
||||
branch1: string,
|
||||
branch2: string
|
||||
): Promise<boolean> {
|
||||
return (await commitsBehind(git, branch1, branch2)) > 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))
|
||||
)
|
||||
}
|
||||
|
||||
function splitLines(multilineString: string): string[] {
|
||||
return multilineString
|
||||
.split('\n')
|
||||
.map(s => s.trim())
|
||||
.filter(x => x !== '')
|
||||
}
|
||||
|
||||
interface CreateOrUpdateBranchResult {
|
||||
action: string
|
||||
base: string
|
||||
hasDiffWithBase: boolean
|
||||
baseSha: string
|
||||
headSha: string
|
||||
branchCommits: Commit[]
|
||||
}
|
||||
|
||||
export async function createOrUpdateBranch(
|
||||
git: GitCommandManager,
|
||||
commitMessage: string,
|
||||
base: string,
|
||||
branch: string,
|
||||
branchRemoteName: string,
|
||||
signoff: boolean,
|
||||
addPaths: string[],
|
||||
signCommits: boolean = false
|
||||
): Promise<CreateOrUpdateBranchResult> {
|
||||
// Get the working base.
|
||||
// When a ref, it may or may not be the actual base.
|
||||
// When a commit, we must rebase onto the actual base.
|
||||
const [workingBase, workingBaseType] = await getWorkingBaseAndType(git)
|
||||
core.info(`Working base is ${workingBaseType} '${workingBase}'`)
|
||||
if (workingBaseType == WorkingBaseType.Commit && !base) {
|
||||
throw new Error(`When in 'detached HEAD' state, 'base' must be supplied.`)
|
||||
}
|
||||
|
||||
// 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,
|
||||
baseSha: '',
|
||||
headSha: '',
|
||||
branchCommits: []
|
||||
}
|
||||
|
||||
// Save the working base changes to a temporary branch
|
||||
const tempBranch = uuidv4()
|
||||
await git.checkout(tempBranch, 'HEAD')
|
||||
// Commit any uncommitted changes
|
||||
if (await git.isDirty(true, addPaths)) {
|
||||
core.info('Uncommitted changes found. Adding a commit.')
|
||||
const aopts = ['add']
|
||||
if (addPaths.length > 0) {
|
||||
aopts.push(...['--', ...addPaths])
|
||||
} else {
|
||||
aopts.push('-A')
|
||||
}
|
||||
await git.exec(aopts, true)
|
||||
const popts = ['-m', commitMessage]
|
||||
if (signoff) {
|
||||
popts.push('--signoff')
|
||||
}
|
||||
const commitResult = await git.commit(popts, true)
|
||||
// 'nothing to commit' can occur when core.autocrlf is set to true
|
||||
if (
|
||||
commitResult.exitCode != 0 &&
|
||||
!commitResult.stdout.includes(NOTHING_TO_COMMIT)
|
||||
) {
|
||||
throw new Error(`Unexpected error: ${commitResult.stderr}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Stash any uncommitted tracked and untracked changes
|
||||
const stashed = await git.stashPush(['--include-untracked'])
|
||||
|
||||
// Reset the working base
|
||||
// Commits made during the workflow will be removed
|
||||
if (workingBaseType == WorkingBaseType.Branch) {
|
||||
core.info(`Resetting working base branch '${workingBase}'`)
|
||||
await git.checkout(workingBase)
|
||||
await git.exec(['reset', '--hard', `${baseRemote}/${workingBase}`])
|
||||
}
|
||||
|
||||
// If the working base is not the base, rebase the temp branch commits
|
||||
// This will also be true if the working base type is a commit
|
||||
if (workingBase != base) {
|
||||
core.info(
|
||||
`Rebasing commits made to ${workingBaseType} '${workingBase}' on to base branch '${base}'`
|
||||
)
|
||||
const fetchArgs = ['--force']
|
||||
if (branchRemoteName != 'fork') {
|
||||
// If pushing to a fork we cannot shallow fetch otherwise the 'shallow update not allowed' error occurs
|
||||
fetchArgs.push('--depth=1')
|
||||
}
|
||||
// Checkout the actual base
|
||||
await git.fetch([`${base}:${base}`], baseRemote, fetchArgs)
|
||||
await git.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, fetchArgs)
|
||||
}
|
||||
|
||||
// Determine the fetch depth for the pull request branch (best effort)
|
||||
const tempBranchCommitsAhead = await commitsAhead(git, base, tempBranch)
|
||||
const fetchDepth =
|
||||
tempBranchCommitsAhead > 0
|
||||
? tempBranchCommitsAhead + FETCH_DEPTH_MARGIN
|
||||
: FETCH_DEPTH_MARGIN
|
||||
|
||||
// Try to fetch the pull request branch
|
||||
if (!(await tryFetch(git, branchRemoteName, branch, fetchDepth))) {
|
||||
// The pull request branch does not exist
|
||||
core.info(`Pull request branch '${branch}' does not exist yet.`)
|
||||
// Create the pull request branch
|
||||
await git.checkout(branch, tempBranch)
|
||||
// Check if the pull request branch is ahead of the base
|
||||
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)
|
||||
|
||||
// Reset the branch if one of the following conditions is true.
|
||||
// - If the branch differs from the recreated temp branch.
|
||||
// - If the recreated temp branch is not ahead of the base. This means there will be
|
||||
// no pull request diff after the branch is reset. This will reset any undeleted
|
||||
// branches after merging. In particular, it catches a case where the branch was
|
||||
// squash merged but not deleted. We need to reset to make sure it doesn't appear
|
||||
// to have a diff with the base due to different commits for the same changes.
|
||||
// - If the number of commits ahead of the base branch differs between the branch and
|
||||
// temp branch. This catches a case where the base branch has been force pushed to
|
||||
// a new commit.
|
||||
// For changes on base this reset is equivalent to a rebase of the pull request branch.
|
||||
const branchCommitsAhead = await commitsAhead(git, base, branch)
|
||||
if (
|
||||
(await git.hasDiff([`${branch}..${tempBranch}`])) ||
|
||||
branchCommitsAhead != tempBranchCommitsAhead ||
|
||||
!(tempBranchCommitsAhead > 0) // !isAhead
|
||||
) {
|
||||
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 {
|
||||
result.action = 'not-updated'
|
||||
core.info(
|
||||
`Branch '${branch}' is even with its remote and will not be updated`
|
||||
)
|
||||
}
|
||||
|
||||
// Check if the pull request branch is ahead of the base
|
||||
result.hasDiffWithBase = await isAhead(git, base, branch)
|
||||
}
|
||||
|
||||
// Get the base and head SHAs
|
||||
result.baseSha = await git.revParse(base)
|
||||
result.headSha = await git.revParse(branch)
|
||||
|
||||
// NOTE: This could always be built and returned. Maybe remove when there is confidence in buildBranchCommits.
|
||||
if (signCommits) {
|
||||
// Build the branch commits
|
||||
result.branchCommits = await buildBranchCommits(git, base, branch)
|
||||
}
|
||||
|
||||
// Delete the temporary branch
|
||||
await git.exec(['branch', '--delete', '--force', tempBranch])
|
||||
|
||||
// Checkout the working base to leave the local repository as it was found
|
||||
await git.checkout(workingBase)
|
||||
|
||||
// Restore any stashed changes
|
||||
if (stashed) {
|
||||
await git.stashPop()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
292
src/create-pull-request.ts
Normal file
292
src/create-pull-request.ts
Normal file
@ -0,0 +1,292 @@
|
||||
import * as core from '@actions/core'
|
||||
import {
|
||||
createOrUpdateBranch,
|
||||
getWorkingBaseAndType,
|
||||
WorkingBaseType
|
||||
} from './create-or-update-branch'
|
||||
import {GitHubHelper} from './github-helper'
|
||||
import {GitCommandManager} from './git-command-manager'
|
||||
import {GitConfigHelper} from './git-config-helper'
|
||||
import * as utils from './utils'
|
||||
|
||||
export interface Inputs {
|
||||
token: string
|
||||
gitToken: string
|
||||
path: string
|
||||
addPaths: string[]
|
||||
commitMessage: string
|
||||
committer: string
|
||||
author: string
|
||||
signoff: boolean
|
||||
branch: string
|
||||
deleteBranch: boolean
|
||||
branchSuffix: string
|
||||
base: string
|
||||
pushToFork: string
|
||||
signCommits: boolean
|
||||
title: string
|
||||
body: string
|
||||
bodyPath: string
|
||||
labels: string[]
|
||||
assignees: string[]
|
||||
reviewers: string[]
|
||||
teamReviewers: string[]
|
||||
milestone: number
|
||||
draft: boolean
|
||||
maintainerCanModify: boolean
|
||||
}
|
||||
|
||||
export async function createPullRequest(inputs: Inputs): Promise<void> {
|
||||
let gitConfigHelper, git
|
||||
try {
|
||||
core.startGroup('Prepare git configuration')
|
||||
const repoPath = utils.getRepoPath(inputs.path)
|
||||
git = await GitCommandManager.create(repoPath)
|
||||
gitConfigHelper = await GitConfigHelper.create(git)
|
||||
core.endGroup()
|
||||
|
||||
core.startGroup('Determining the base and head repositories')
|
||||
const baseRemote = gitConfigHelper.getGitRemote()
|
||||
// Init the GitHub clients
|
||||
const ghBranch = new GitHubHelper(baseRemote.hostname, inputs.gitToken)
|
||||
const ghPull = new GitHubHelper(baseRemote.hostname, inputs.token)
|
||||
// Determine the head repository; the target for the pull request branch
|
||||
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
|
||||
core.info(
|
||||
`Checking if '${branchRepository}' is a fork of '${baseRemote.repository}'`
|
||||
)
|
||||
const baseParentRepository = await ghBranch.getRepositoryParent(
|
||||
baseRemote.repository
|
||||
)
|
||||
const branchParentRepository =
|
||||
await ghBranch.getRepositoryParent(branchRepository)
|
||||
if (branchParentRepository == null) {
|
||||
throw new Error(
|
||||
`Repository '${branchRepository}' is not a fork. Unable to continue.`
|
||||
)
|
||||
}
|
||||
if (
|
||||
branchParentRepository != baseRemote.repository &&
|
||||
baseParentRepository != branchParentRepository
|
||||
) {
|
||||
throw new Error(
|
||||
`Repository '${branchRepository}' is not a fork of '${baseRemote.repository}', nor are they siblings. Unable to continue.`
|
||||
)
|
||||
}
|
||||
// Add a remote for the fork
|
||||
const remoteUrl = utils.getRemoteUrl(
|
||||
baseRemote.protocol,
|
||||
baseRemote.hostname,
|
||||
branchRepository
|
||||
)
|
||||
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 gitConfigHelper.configureToken(inputs.gitToken)
|
||||
core.endGroup()
|
||||
}
|
||||
|
||||
core.startGroup('Checking the base repository state')
|
||||
const [workingBase, workingBaseType] = await getWorkingBaseAndType(git)
|
||||
core.info(`Working base is ${workingBaseType} '${workingBase}'`)
|
||||
// When in detached HEAD state (checked out on a commit), we need to
|
||||
// know the 'base' branch in order to rebase changes.
|
||||
if (workingBaseType == WorkingBaseType.Commit && !inputs.base) {
|
||||
throw new Error(
|
||||
`When the repository is checked out on a commit instead of a branch, the 'base' input must be supplied.`
|
||||
)
|
||||
}
|
||||
// If the base is not specified it is assumed to be the working base.
|
||||
const base = inputs.base ? inputs.base : workingBase
|
||||
// Throw an error if the base and branch are not different branches
|
||||
// of the 'origin' remote. An identically named branch in the `fork`
|
||||
// remote is perfectly fine.
|
||||
if (branchRemoteName == 'origin' && base == inputs.branch) {
|
||||
throw new Error(
|
||||
`The 'base' and 'branch' for a pull request must be different branches. Unable to continue.`
|
||||
)
|
||||
}
|
||||
// For self-hosted runners the repository state persists between runs.
|
||||
// This command prunes the stale remote ref when the pull request branch was
|
||||
// deleted after being merged or closed. Without this the push using
|
||||
// '--force-with-lease' fails due to "stale info."
|
||||
// https://github.com/peter-evans/create-pull-request/issues/633
|
||||
await git.exec(['remote', 'prune', branchRemoteName])
|
||||
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()
|
||||
|
||||
// Action outputs
|
||||
const outputs = new Map<string, string>()
|
||||
outputs.set('pull-request-branch', inputs.branch)
|
||||
outputs.set('pull-request-operation', 'none')
|
||||
outputs.set('pull-request-commits-verified', 'false')
|
||||
|
||||
// Create or update the pull request branch
|
||||
core.startGroup('Create or update the pull request branch')
|
||||
const result = await createOrUpdateBranch(
|
||||
git,
|
||||
inputs.commitMessage,
|
||||
inputs.base,
|
||||
inputs.branch,
|
||||
branchRemoteName,
|
||||
inputs.signoff,
|
||||
inputs.addPaths,
|
||||
inputs.signCommits
|
||||
)
|
||||
outputs.set('pull-request-head-sha', result.headSha)
|
||||
// Set the base. It would have been '' if not specified as an input
|
||||
inputs.base = result.base
|
||||
core.endGroup()
|
||||
|
||||
if (['created', 'updated'].includes(result.action)) {
|
||||
// The branch was created or updated
|
||||
core.startGroup(
|
||||
`Pushing pull request branch to '${branchRemoteName}/${inputs.branch}'`
|
||||
)
|
||||
if (inputs.signCommits) {
|
||||
// Create signed commits via the GitHub API
|
||||
const stashed = await git.stashPush(['--include-untracked'])
|
||||
await git.checkout(inputs.branch)
|
||||
const pushSignedCommitsResult = await ghBranch.pushSignedCommits(
|
||||
result.branchCommits,
|
||||
result.baseSha,
|
||||
repoPath,
|
||||
branchRepository,
|
||||
inputs.branch
|
||||
)
|
||||
outputs.set('pull-request-head-sha', pushSignedCommitsResult.sha)
|
||||
outputs.set(
|
||||
'pull-request-commits-verified',
|
||||
pushSignedCommitsResult.verified.toString()
|
||||
)
|
||||
await git.checkout('-')
|
||||
if (stashed) {
|
||||
await git.stashPop()
|
||||
}
|
||||
} else {
|
||||
await git.push([
|
||||
'--force-with-lease',
|
||||
branchRemoteName,
|
||||
`${inputs.branch}:refs/heads/${inputs.branch}`
|
||||
])
|
||||
}
|
||||
core.endGroup()
|
||||
}
|
||||
|
||||
if (result.hasDiffWithBase) {
|
||||
core.startGroup('Create or update the pull request')
|
||||
const pull = await ghPull.createOrUpdatePullRequest(
|
||||
inputs,
|
||||
baseRemote.repository,
|
||||
branchRepository
|
||||
)
|
||||
outputs.set('pull-request-number', pull.number.toString())
|
||||
outputs.set('pull-request-url', pull.html_url)
|
||||
if (pull.created) {
|
||||
outputs.set('pull-request-operation', 'created')
|
||||
} else if (result.action == 'updated') {
|
||||
outputs.set('pull-request-operation', 'updated')
|
||||
}
|
||||
// Deprecated
|
||||
core.exportVariable('PULL_REQUEST_NUMBER', pull.number)
|
||||
core.endGroup()
|
||||
} else {
|
||||
// There is no longer a diff with the base
|
||||
// Check we are in a state where a branch exists
|
||||
if (['updated', 'not-updated'].includes(result.action)) {
|
||||
core.info(
|
||||
`Branch '${inputs.branch}' no longer differs from base branch '${inputs.base}'`
|
||||
)
|
||||
if (inputs.deleteBranch) {
|
||||
core.info(`Deleting branch '${inputs.branch}'`)
|
||||
await git.push([
|
||||
'--delete',
|
||||
'--force',
|
||||
branchRemoteName,
|
||||
`refs/heads/${inputs.branch}`
|
||||
])
|
||||
outputs.set('pull-request-operation', 'closed')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set outputs
|
||||
core.startGroup('Setting outputs')
|
||||
for (const [key, value] of outputs) {
|
||||
core.info(`${key} = ${value}`)
|
||||
core.setOutput(key, value)
|
||||
}
|
||||
core.endGroup()
|
||||
} catch (error) {
|
||||
core.setFailed(utils.getErrorMessage(error))
|
||||
} finally {
|
||||
core.startGroup('Restore git configuration')
|
||||
if (inputs.pushToFork) {
|
||||
await git.exec(['remote', 'rm', 'fork'])
|
||||
}
|
||||
await gitConfigHelper.close()
|
||||
core.endGroup()
|
||||
}
|
||||
}
|
@ -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}
|
@ -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"]
|
||||
)
|
||||
)
|
@ -1,205 +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
|
||||
path = os.getenv("CPR_PATH", os.getcwd())
|
||||
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 path
|
||||
repo = Repo(path)
|
||||
|
||||
# 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"),
|
||||
)
|
378
src/git-command-manager.ts
Normal file
378
src/git-command-manager.ts
Normal file
@ -0,0 +1,378 @@
|
||||
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 type Commit = {
|
||||
sha: string
|
||||
tree: string
|
||||
parents: string[]
|
||||
subject: string
|
||||
body: string
|
||||
changes: {
|
||||
mode: string
|
||||
status: 'A' | 'M' | 'D'
|
||||
path: string
|
||||
}[]
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
// https://github.com/git/git/commit/a047fafc7866cc4087201e284dc1f53e8f9a32d5
|
||||
args.push('--')
|
||||
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[],
|
||||
allowAllExitCodes = false
|
||||
): Promise<GitOutput> {
|
||||
const args = ['commit']
|
||||
if (this.identityGitOptions) {
|
||||
args.unshift(...this.identityGitOptions)
|
||||
}
|
||||
|
||||
if (options) {
|
||||
args.push(...options)
|
||||
}
|
||||
|
||||
return await this.exec(args, allowAllExitCodes)
|
||||
}
|
||||
|
||||
async config(
|
||||
configKey: string,
|
||||
configValue: string,
|
||||
globalConfig?: boolean,
|
||||
add?: boolean
|
||||
): Promise<void> {
|
||||
const args: string[] = ['config', globalConfig ? '--global' : '--local']
|
||||
if (add) {
|
||||
args.push('--add')
|
||||
}
|
||||
args.push(...[configKey, configValue])
|
||||
await this.exec(args)
|
||||
}
|
||||
|
||||
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 fetch(
|
||||
refSpec: string[],
|
||||
remoteName?: string,
|
||||
options?: string[],
|
||||
unshallow = false
|
||||
): 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 (
|
||||
unshallow &&
|
||||
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 getCommit(ref: string): Promise<Commit> {
|
||||
const endOfBody = '###EOB###'
|
||||
const output = await this.exec([
|
||||
'show',
|
||||
'--raw',
|
||||
'--cc',
|
||||
'--diff-filter=AMD',
|
||||
`--format=%H%n%T%n%P%n%s%n%b%n${endOfBody}`,
|
||||
ref
|
||||
])
|
||||
const lines = output.stdout.split('\n')
|
||||
const endOfBodyIndex = lines.lastIndexOf(endOfBody)
|
||||
const detailLines = lines.slice(0, endOfBodyIndex)
|
||||
|
||||
return <Commit>{
|
||||
sha: detailLines[0],
|
||||
tree: detailLines[1],
|
||||
parents: detailLines[2].split(' '),
|
||||
subject: detailLines[3],
|
||||
body: detailLines.slice(4, endOfBodyIndex).join('\n'),
|
||||
changes: lines.slice(endOfBodyIndex + 2, -1).map(line => {
|
||||
const change = line.match(
|
||||
/^:(\d{6}) (\d{6}) \w{7} \w{7} ([AMD])\s+(.*)$/
|
||||
)
|
||||
if (change) {
|
||||
return {
|
||||
mode: change[3] === 'D' ? change[1] : change[2],
|
||||
status: change[3],
|
||||
path: change[4]
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unexpected line format: ${line}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async getConfigValue(configKey: string, configValue = '.'): Promise<string> {
|
||||
const output = await this.exec([
|
||||
'config',
|
||||
'--local',
|
||||
'--get-regexp',
|
||||
configKey,
|
||||
configValue
|
||||
])
|
||||
return output.stdout.trim().split(`${configKey} `)[1]
|
||||
}
|
||||
|
||||
getGitDirectory(): Promise<string> {
|
||||
return this.revParse('--git-dir')
|
||||
}
|
||||
|
||||
getWorkingDirectory(): string {
|
||||
return this.workingDirectory
|
||||
}
|
||||
|
||||
async hasDiff(options?: string[]): Promise<boolean> {
|
||||
const args = ['diff', '--quiet']
|
||||
if (options) {
|
||||
args.push(...options)
|
||||
}
|
||||
const output = await this.exec(args, true)
|
||||
return output.exitCode === 1
|
||||
}
|
||||
|
||||
async isDirty(untracked: boolean, pathspec?: string[]): Promise<boolean> {
|
||||
const pathspecArgs = pathspec ? ['--', ...pathspec] : []
|
||||
// Check untracked changes
|
||||
const sargs = ['--porcelain', '-unormal']
|
||||
sargs.push(...pathspecArgs)
|
||||
if (untracked && (await this.status(sargs))) {
|
||||
return true
|
||||
}
|
||||
// Check working index changes
|
||||
if (await this.hasDiff(pathspecArgs)) {
|
||||
return true
|
||||
}
|
||||
// Check staged changes
|
||||
const dargs = ['--staged']
|
||||
dargs.push(...pathspecArgs)
|
||||
if (await this.hasDiff(dargs)) {
|
||||
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 stashPush(options?: string[]): Promise<boolean> {
|
||||
const args = ['stash', 'push']
|
||||
if (options) {
|
||||
args.push(...options)
|
||||
}
|
||||
const output = await this.exec(args)
|
||||
return output.stdout.trim() !== 'No local changes to save'
|
||||
}
|
||||
|
||||
async stashPop(options?: string[]): Promise<void> {
|
||||
const args = ['stash', 'pop']
|
||||
if (options) {
|
||||
args.push(...options)
|
||||
}
|
||||
await this.exec(args)
|
||||
}
|
||||
|
||||
async status(options?: string[]): Promise<string> {
|
||||
const args = ['status']
|
||||
if (options) {
|
||||
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
|
||||
}
|
217
src/git-config-helper.ts
Normal file
217
src/git-config-helper.ts
Normal file
@ -0,0 +1,217 @@
|
||||
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'
|
||||
import * as utils from './utils'
|
||||
|
||||
interface GitRemote {
|
||||
hostname: string
|
||||
protocol: string
|
||||
repository: string
|
||||
}
|
||||
|
||||
export class GitConfigHelper {
|
||||
private git: GitCommandManager
|
||||
private gitConfigPath = ''
|
||||
private workingDirectory: string
|
||||
private safeDirectoryConfigKey = 'safe.directory'
|
||||
private safeDirectoryAdded = false
|
||||
private remoteUrl = ''
|
||||
private extraheaderConfigKey = ''
|
||||
private extraheaderConfigPlaceholderValue = 'AUTHORIZATION: basic ***'
|
||||
private extraheaderConfigValueRegex = '^AUTHORIZATION:'
|
||||
private persistedExtraheaderConfigValue = ''
|
||||
|
||||
private constructor(git: GitCommandManager) {
|
||||
this.git = git
|
||||
this.workingDirectory = this.git.getWorkingDirectory()
|
||||
}
|
||||
|
||||
static async create(git: GitCommandManager): Promise<GitConfigHelper> {
|
||||
const gitConfigHelper = new GitConfigHelper(git)
|
||||
await gitConfigHelper.addSafeDirectory()
|
||||
await gitConfigHelper.fetchRemoteDetail()
|
||||
await gitConfigHelper.savePersistedAuth()
|
||||
return gitConfigHelper
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
// Remove auth and restore persisted auth config if it existed
|
||||
await this.removeAuth()
|
||||
await this.restorePersistedAuth()
|
||||
await this.removeSafeDirectory()
|
||||
}
|
||||
|
||||
async addSafeDirectory(): Promise<void> {
|
||||
const exists = await this.git.configExists(
|
||||
this.safeDirectoryConfigKey,
|
||||
this.workingDirectory,
|
||||
true
|
||||
)
|
||||
if (!exists) {
|
||||
await this.git.config(
|
||||
this.safeDirectoryConfigKey,
|
||||
this.workingDirectory,
|
||||
true,
|
||||
true
|
||||
)
|
||||
this.safeDirectoryAdded = true
|
||||
}
|
||||
}
|
||||
|
||||
async removeSafeDirectory(): Promise<void> {
|
||||
if (this.safeDirectoryAdded) {
|
||||
await this.git.tryConfigUnset(
|
||||
this.safeDirectoryConfigKey,
|
||||
this.workingDirectory,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async fetchRemoteDetail(): Promise<void> {
|
||||
this.remoteUrl = await this.git.tryGetRemoteUrl()
|
||||
}
|
||||
|
||||
getGitRemote(): GitRemote {
|
||||
return GitConfigHelper.parseGitRemote(this.remoteUrl)
|
||||
}
|
||||
|
||||
static parseGitRemote(remoteUrl: string): GitRemote {
|
||||
const httpsUrlPattern = new RegExp(
|
||||
'^(https?)://(?:.+@)?(.+?)/(.+/.+?)(\\.git)?$',
|
||||
'i'
|
||||
)
|
||||
const httpsMatch = remoteUrl.match(httpsUrlPattern)
|
||||
if (httpsMatch) {
|
||||
return {
|
||||
hostname: httpsMatch[2],
|
||||
protocol: 'HTTPS',
|
||||
repository: httpsMatch[3]
|
||||
}
|
||||
}
|
||||
|
||||
const sshUrlPattern = new RegExp('^git@(.+?):(.+/.+)\\.git$', 'i')
|
||||
const sshMatch = remoteUrl.match(sshUrlPattern)
|
||||
if (sshMatch) {
|
||||
return {
|
||||
hostname: sshMatch[1],
|
||||
protocol: 'SSH',
|
||||
repository: sshMatch[2]
|
||||
}
|
||||
}
|
||||
|
||||
// Unauthenticated git protocol for integration tests only
|
||||
const gitUrlPattern = new RegExp('^git://(.+?)/(.+/.+)\\.git$', 'i')
|
||||
const gitMatch = remoteUrl.match(gitUrlPattern)
|
||||
if (gitMatch) {
|
||||
return {
|
||||
hostname: gitMatch[1],
|
||||
protocol: 'GIT',
|
||||
repository: gitMatch[2]
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`The format of '${remoteUrl}' is not a valid GitHub repository URL`
|
||||
)
|
||||
}
|
||||
|
||||
async savePersistedAuth(): Promise<void> {
|
||||
const serverUrl = new URL(`https://${this.getGitRemote().hostname}`)
|
||||
this.extraheaderConfigKey = `http.${serverUrl.origin}/.extraheader`
|
||||
// Save and unset persisted extraheader credential in git config if it exists
|
||||
this.persistedExtraheaderConfigValue = await this.getAndUnset()
|
||||
}
|
||||
|
||||
async restorePersistedAuth(): Promise<void> {
|
||||
if (this.persistedExtraheaderConfigValue) {
|
||||
try {
|
||||
await this.setExtraheaderConfig(this.persistedExtraheaderConfigValue)
|
||||
core.info('Persisted git credentials restored')
|
||||
} catch (e) {
|
||||
core.warning(utils.getErrorMessage(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> {
|
||||
if (this.gitConfigPath.length === 0) {
|
||||
const gitDir = await this.git.getGitDirectory()
|
||||
this.gitConfigPath = path.join(this.workingDirectory, gitDir, 'config')
|
||||
}
|
||||
let content = (await fs.promises.readFile(this.gitConfigPath)).toString()
|
||||
const index = content.indexOf(find)
|
||||
if (index < 0 || index != content.lastIndexOf(find)) {
|
||||
throw new Error(`Unable to replace '${find}' in ${this.gitConfigPath}`)
|
||||
}
|
||||
content = content.replace(find, replace)
|
||||
await fs.promises.writeFile(this.gitConfigPath, content)
|
||||
}
|
||||
}
|
331
src/github-helper.ts
Normal file
331
src/github-helper.ts
Normal file
@ -0,0 +1,331 @@
|
||||
import * as core from '@actions/core'
|
||||
import {Inputs} from './create-pull-request'
|
||||
import {Commit} from './git-command-manager'
|
||||
import {Octokit, OctokitOptions, throttleOptions} from './octokit-client'
|
||||
import pLimit from 'p-limit'
|
||||
import * as utils from './utils'
|
||||
|
||||
const ERROR_PR_ALREADY_EXISTS = 'A pull request already exists for'
|
||||
const ERROR_PR_REVIEW_TOKEN_SCOPE =
|
||||
'Validation Failed: "Could not resolve to a node with the global id of'
|
||||
const ERROR_PR_FORK_COLLAB = `Fork collab can't be granted by someone without permission`
|
||||
|
||||
const blobCreationLimit = pLimit(8)
|
||||
|
||||
interface Repository {
|
||||
owner: string
|
||||
repo: string
|
||||
}
|
||||
|
||||
interface Pull {
|
||||
number: number
|
||||
html_url: string
|
||||
created: boolean
|
||||
}
|
||||
|
||||
interface CommitResponse {
|
||||
sha: string
|
||||
verified: boolean
|
||||
}
|
||||
|
||||
type TreeObject = {
|
||||
path: string
|
||||
mode: '100644' | '100755' | '040000' | '160000' | '120000'
|
||||
sha: string | null
|
||||
type: 'blob'
|
||||
}
|
||||
|
||||
export class GitHubHelper {
|
||||
private octokit: InstanceType<typeof Octokit>
|
||||
|
||||
constructor(githubServerHostname: string, token: string) {
|
||||
const options: OctokitOptions = {}
|
||||
if (token) {
|
||||
options.auth = `${token}`
|
||||
}
|
||||
if (githubServerHostname !== 'github.com') {
|
||||
options.baseUrl = `https://${githubServerHostname}/api/v3`
|
||||
} else {
|
||||
options.baseUrl = 'https://api.github.com'
|
||||
}
|
||||
options.throttle = throttleOptions
|
||||
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,
|
||||
headRepository: string
|
||||
): Promise<Pull> {
|
||||
const [headOwner] = headRepository.split('/')
|
||||
const headBranch = `${headOwner}:${inputs.branch}`
|
||||
|
||||
// Try to create the pull request
|
||||
try {
|
||||
core.info(`Attempting creation of pull request`)
|
||||
const {data: pull} = await this.octokit.rest.pulls.create({
|
||||
...this.parseRepository(baseRepository),
|
||||
title: inputs.title,
|
||||
head: headBranch,
|
||||
head_repo: headRepository,
|
||||
base: inputs.base,
|
||||
body: inputs.body,
|
||||
draft: inputs.draft,
|
||||
maintainer_can_modify: inputs.maintainerCanModify
|
||||
})
|
||||
core.info(
|
||||
`Created pull request #${pull.number} (${headBranch} => ${inputs.base})`
|
||||
)
|
||||
return {
|
||||
number: pull.number,
|
||||
html_url: pull.html_url,
|
||||
created: true
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMessage = utils.getErrorMessage(e)
|
||||
if (errorMessage.includes(ERROR_PR_ALREADY_EXISTS)) {
|
||||
core.info(`A pull request already exists for ${headBranch}`)
|
||||
} else if (errorMessage.includes(ERROR_PR_FORK_COLLAB)) {
|
||||
core.warning(
|
||||
'An attempt was made to create a pull request using a token that does not have write access to the head branch.'
|
||||
)
|
||||
core.warning(
|
||||
`For this case, set input 'maintainer-can-modify' to 'false' to allow pull request creation.`
|
||||
)
|
||||
throw e
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// Update the pull request that exists for this branch and base
|
||||
core.info(`Fetching existing pull request`)
|
||||
const {data: pulls} = await this.octokit.rest.pulls.list({
|
||||
...this.parseRepository(baseRepository),
|
||||
state: 'open',
|
||||
head: headBranch,
|
||||
base: inputs.base
|
||||
})
|
||||
core.info(`Attempting update of pull request`)
|
||||
const {data: pull} = await this.octokit.rest.pulls.update({
|
||||
...this.parseRepository(baseRepository),
|
||||
pull_number: pulls[0].number,
|
||||
title: inputs.title,
|
||||
body: inputs.body
|
||||
})
|
||||
core.info(
|
||||
`Updated pull request #${pull.number} (${headBranch} => ${inputs.base})`
|
||||
)
|
||||
return {
|
||||
number: pull.number,
|
||||
html_url: pull.html_url,
|
||||
created: false
|
||||
}
|
||||
}
|
||||
|
||||
async getRepositoryParent(headRepository: string): Promise<string | null> {
|
||||
const {data: headRepo} = await this.octokit.rest.repos.get({
|
||||
...this.parseRepository(headRepository)
|
||||
})
|
||||
if (!headRepo.parent) {
|
||||
return null
|
||||
}
|
||||
return headRepo.parent.full_name
|
||||
}
|
||||
|
||||
async createOrUpdatePullRequest(
|
||||
inputs: Inputs,
|
||||
baseRepository: string,
|
||||
headRepository: string
|
||||
): Promise<Pull> {
|
||||
// Create or update the pull request
|
||||
const pull = await this.createOrUpdate(
|
||||
inputs,
|
||||
baseRepository,
|
||||
headRepository
|
||||
)
|
||||
|
||||
// Apply milestone
|
||||
if (inputs.milestone) {
|
||||
core.info(`Applying milestone '${inputs.milestone}'`)
|
||||
await this.octokit.rest.issues.update({
|
||||
...this.parseRepository(baseRepository),
|
||||
issue_number: pull.number,
|
||||
milestone: inputs.milestone
|
||||
})
|
||||
}
|
||||
// Apply labels
|
||||
if (inputs.labels.length > 0) {
|
||||
core.info(`Applying labels '${inputs.labels}'`)
|
||||
await this.octokit.rest.issues.addLabels({
|
||||
...this.parseRepository(baseRepository),
|
||||
issue_number: pull.number,
|
||||
labels: inputs.labels
|
||||
})
|
||||
}
|
||||
// Apply assignees
|
||||
if (inputs.assignees.length > 0) {
|
||||
core.info(`Applying assignees '${inputs.assignees}'`)
|
||||
await this.octokit.rest.issues.addAssignees({
|
||||
...this.parseRepository(baseRepository),
|
||||
issue_number: pull.number,
|
||||
assignees: inputs.assignees
|
||||
})
|
||||
}
|
||||
|
||||
// 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) {
|
||||
const teams = utils.stripOrgPrefixFromTeams(inputs.teamReviewers)
|
||||
requestReviewersParams['team_reviewers'] = teams
|
||||
core.info(`Requesting team reviewers '${teams}'`)
|
||||
}
|
||||
if (Object.keys(requestReviewersParams).length > 0) {
|
||||
try {
|
||||
await this.octokit.rest.pulls.requestReviewers({
|
||||
...this.parseRepository(baseRepository),
|
||||
pull_number: pull.number,
|
||||
...requestReviewersParams
|
||||
})
|
||||
} catch (e) {
|
||||
if (utils.getErrorMessage(e).includes(ERROR_PR_REVIEW_TOKEN_SCOPE)) {
|
||||
core.error(
|
||||
`Unable to request reviewers. If requesting team reviewers a 'repo' scoped PAT is required.`
|
||||
)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
return pull
|
||||
}
|
||||
|
||||
async pushSignedCommits(
|
||||
branchCommits: Commit[],
|
||||
baseSha: string,
|
||||
repoPath: string,
|
||||
branchRepository: string,
|
||||
branch: string
|
||||
): Promise<CommitResponse> {
|
||||
let headCommit: CommitResponse = {
|
||||
sha: baseSha,
|
||||
verified: false
|
||||
}
|
||||
for (const commit of branchCommits) {
|
||||
headCommit = await this.createCommit(
|
||||
commit,
|
||||
[headCommit.sha],
|
||||
repoPath,
|
||||
branchRepository
|
||||
)
|
||||
}
|
||||
await this.createOrUpdateRef(branchRepository, branch, headCommit.sha)
|
||||
return headCommit
|
||||
}
|
||||
|
||||
private async createCommit(
|
||||
commit: Commit,
|
||||
parents: string[],
|
||||
repoPath: string,
|
||||
branchRepository: string
|
||||
): Promise<CommitResponse> {
|
||||
const repository = this.parseRepository(branchRepository)
|
||||
let treeSha = commit.tree
|
||||
if (commit.changes.length > 0) {
|
||||
core.info(`Creating tree objects for local commit ${commit.sha}`)
|
||||
const treeObjects = await Promise.all(
|
||||
commit.changes.map(async ({path, mode, status}) => {
|
||||
let sha: string | null = null
|
||||
if (status === 'A' || status === 'M') {
|
||||
core.info(`Creating blob for file '${path}'`)
|
||||
const {data: blob} = await blobCreationLimit(() =>
|
||||
this.octokit.rest.git.createBlob({
|
||||
...repository,
|
||||
content: utils.readFileBase64([repoPath, path]),
|
||||
encoding: 'base64'
|
||||
})
|
||||
)
|
||||
sha = blob.sha
|
||||
}
|
||||
return <TreeObject>{
|
||||
path,
|
||||
mode,
|
||||
sha,
|
||||
type: 'blob'
|
||||
}
|
||||
})
|
||||
)
|
||||
core.info(`Creating tree for local commit ${commit.sha}`)
|
||||
const {data: tree} = await this.octokit.rest.git.createTree({
|
||||
...repository,
|
||||
base_tree: parents[0],
|
||||
tree: treeObjects
|
||||
})
|
||||
treeSha = tree.sha
|
||||
core.info(`Created tree ${treeSha} for local commit ${commit.sha}`)
|
||||
}
|
||||
|
||||
const {data: remoteCommit} = await this.octokit.rest.git.createCommit({
|
||||
...repository,
|
||||
parents: parents,
|
||||
tree: treeSha,
|
||||
message: `${commit.subject}\n\n${commit.body}`
|
||||
})
|
||||
core.info(
|
||||
`Created commit ${remoteCommit.sha} for local commit ${commit.sha}`
|
||||
)
|
||||
core.info(
|
||||
`Commit verified: ${remoteCommit.verification.verified}; reason: ${remoteCommit.verification.reason}`
|
||||
)
|
||||
return {
|
||||
sha: remoteCommit.sha,
|
||||
verified: remoteCommit.verification.verified
|
||||
}
|
||||
}
|
||||
|
||||
private async createOrUpdateRef(
|
||||
branchRepository: string,
|
||||
branch: string,
|
||||
newHead: string
|
||||
) {
|
||||
const repository = this.parseRepository(branchRepository)
|
||||
const branchExists = await this.octokit.rest.repos
|
||||
.getBranch({
|
||||
...repository,
|
||||
branch: branch
|
||||
})
|
||||
.then(
|
||||
() => true,
|
||||
() => false
|
||||
)
|
||||
|
||||
if (branchExists) {
|
||||
core.info(`Branch ${branch} exists; Updating ref`)
|
||||
await this.octokit.rest.git.updateRef({
|
||||
...repository,
|
||||
sha: newHead,
|
||||
ref: `heads/${branch}`,
|
||||
force: true
|
||||
})
|
||||
} else {
|
||||
core.info(`Branch ${branch} does not exist; Creating ref`)
|
||||
await this.octokit.rest.git.createRef({
|
||||
...repository,
|
||||
sha: newHead,
|
||||
ref: `refs/heads/${branch}`
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
63
src/main.ts
Normal file
63
src/main.ts
Normal file
@ -0,0 +1,63 @@
|
||||
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'),
|
||||
gitToken: core.getInput('git-token'),
|
||||
path: core.getInput('path'),
|
||||
addPaths: utils.getInputAsArray('add-paths'),
|
||||
commitMessage: core.getInput('commit-message'),
|
||||
committer: core.getInput('committer'),
|
||||
author: core.getInput('author'),
|
||||
signoff: core.getBooleanInput('signoff'),
|
||||
branch: core.getInput('branch'),
|
||||
deleteBranch: core.getBooleanInput('delete-branch'),
|
||||
branchSuffix: core.getInput('branch-suffix'),
|
||||
base: core.getInput('base'),
|
||||
pushToFork: core.getInput('push-to-fork'),
|
||||
signCommits: core.getBooleanInput('sign-commits'),
|
||||
title: core.getInput('title'),
|
||||
body: core.getInput('body'),
|
||||
bodyPath: core.getInput('body-path'),
|
||||
labels: utils.getInputAsArray('labels'),
|
||||
assignees: utils.getInputAsArray('assignees'),
|
||||
reviewers: utils.getInputAsArray('reviewers'),
|
||||
teamReviewers: utils.getInputAsArray('team-reviewers'),
|
||||
milestone: Number(core.getInput('milestone')),
|
||||
draft: core.getBooleanInput('draft'),
|
||||
maintainerCanModify: core.getBooleanInput('maintainer-can-modify')
|
||||
}
|
||||
core.debug(`Inputs: ${inspect(inputs)}`)
|
||||
|
||||
if (!inputs.token) {
|
||||
throw new Error(`Input 'token' not supplied. Unable to continue.`)
|
||||
}
|
||||
if (!inputs.gitToken) {
|
||||
inputs.gitToken = inputs.token
|
||||
}
|
||||
if (inputs.bodyPath) {
|
||||
if (!utils.fileExistsSync(inputs.bodyPath)) {
|
||||
throw new Error(`File '${inputs.bodyPath}' does not exist.`)
|
||||
}
|
||||
// Update the body input with the contents of the file
|
||||
inputs.body = utils.readFile(inputs.bodyPath)
|
||||
}
|
||||
// 65536 characters is the maximum allowed for the pull request body.
|
||||
if (inputs.body.length > 65536) {
|
||||
core.warning(
|
||||
`Pull request body is too long. Truncating to 65536 characters.`
|
||||
)
|
||||
inputs.body = inputs.body.substring(0, 65536)
|
||||
}
|
||||
|
||||
await createPullRequest(inputs)
|
||||
} catch (error) {
|
||||
core.setFailed(utils.getErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
run()
|
55
src/octokit-client.ts
Normal file
55
src/octokit-client.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import * as core from '@actions/core'
|
||||
import {Octokit as OctokitCore} from '@octokit/core'
|
||||
import {paginateRest} from '@octokit/plugin-paginate-rest'
|
||||
import {restEndpointMethods} from '@octokit/plugin-rest-endpoint-methods'
|
||||
import {throttling} from '@octokit/plugin-throttling'
|
||||
import {getProxyForUrl} from 'proxy-from-env'
|
||||
import {ProxyAgent, fetch as undiciFetch} from 'undici'
|
||||
export {RestEndpointMethodTypes} from '@octokit/plugin-rest-endpoint-methods'
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
export {OctokitOptions} from '@octokit/core/dist-types/types'
|
||||
|
||||
export const Octokit = OctokitCore.plugin(
|
||||
paginateRest,
|
||||
restEndpointMethods,
|
||||
throttling,
|
||||
autoProxyAgent
|
||||
)
|
||||
|
||||
export const throttleOptions = {
|
||||
onRateLimit: (retryAfter, options, _, retryCount) => {
|
||||
core.debug(`Hit rate limit for request ${options.method} ${options.url}`)
|
||||
// Retries twice for a total of three attempts
|
||||
if (retryCount < 2) {
|
||||
core.debug(`Retrying after ${retryAfter} seconds!`)
|
||||
return true
|
||||
}
|
||||
},
|
||||
onSecondaryRateLimit: (retryAfter, options) => {
|
||||
core.warning(
|
||||
`Hit secondary rate limit for request ${options.method} ${options.url}`
|
||||
)
|
||||
core.warning(`Requests may be retried after ${retryAfter} seconds.`)
|
||||
}
|
||||
}
|
||||
|
||||
const proxyFetch =
|
||||
(proxyUrl: string): typeof undiciFetch =>
|
||||
(url, opts) => {
|
||||
return undiciFetch(url, {
|
||||
...opts,
|
||||
dispatcher: new ProxyAgent({
|
||||
uri: proxyUrl
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Octokit plugin to support the standard environment variables http_proxy, https_proxy and no_proxy
|
||||
function autoProxyAgent(octokit: OctokitCore) {
|
||||
octokit.hook.before('request', options => {
|
||||
const proxy = getProxyForUrl(options.baseUrl)
|
||||
if (proxy) {
|
||||
options.request.fetch = proxyFetch(proxy)
|
||||
}
|
||||
})
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
GitPython==3.0.5
|
||||
PyGithub==1.45
|
@ -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;
|
@ -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"
|
||||
)
|
@ -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
|
||||
)
|
141
src/utils.ts
Normal file
141
src/utils.ts
Normal file
@ -0,0 +1,141 @@
|
||||
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 stripOrgPrefixFromTeams(teams: string[]): string[] {
|
||||
return teams.map(team => {
|
||||
const slashIndex = team.lastIndexOf('/')
|
||||
if (slashIndex > 0) {
|
||||
return team.substring(slashIndex + 1)
|
||||
}
|
||||
return team
|
||||
})
|
||||
}
|
||||
|
||||
export function getRepoPath(relativePath?: string): string {
|
||||
let githubWorkspacePath = process.env['GITHUB_WORKSPACE']
|
||||
if (!githubWorkspacePath) {
|
||||
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
|
||||
}
|
||||
|
||||
export function getRemoteUrl(
|
||||
protocol: string,
|
||||
hostname: string,
|
||||
repository: string
|
||||
): string {
|
||||
return protocol == 'HTTPS'
|
||||
? `https://${hostname}/${repository}`
|
||||
: `git@${hostname}:${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 (hasErrorCode(error) && error.code === 'ENOENT') {
|
||||
return false
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Encountered an error when checking whether path '${path}' exists: ${getErrorMessage(
|
||||
error
|
||||
)}`
|
||||
)
|
||||
}
|
||||
|
||||
if (!stats.isDirectory()) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function readFile(path: string): string {
|
||||
return fs.readFileSync(path, 'utf-8')
|
||||
}
|
||||
|
||||
export function readFileBase64(pathParts: string[]): string {
|
||||
return fs.readFileSync(path.resolve(...pathParts)).toString('base64')
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
function hasErrorCode(error: any): error is {code: string} {
|
||||
return typeof (error && error.code) === 'string'
|
||||
}
|
||||
|
||||
export function getErrorMessage(error: unknown) {
|
||||
if (error instanceof Error) return error.message
|
||||
return String(error)
|
||||
}
|
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"lib": [
|
||||
"es6"
|
||||
],
|
||||
"outDir": "./lib",
|
||||
"rootDir": "./src",
|
||||
"declaration": true,
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"exclude": ["__test__", "lib", "node_modules"]
|
||||
}
|
Reference in New Issue
Block a user