Compare commits
1034 commits
Author | SHA1 | Date | |
---|---|---|---|
cf18b4ebc5 | |||
3d64030895 | |||
7174d368c2 | |||
1b757dbf52 | |||
9791a4f146 | |||
|
16e0059bfd | ||
|
5bffd5ae80 | ||
|
a22a0ddc21 | ||
|
b27ce378c8 | ||
|
4e0cc19e22 | ||
|
25b6871a4e | ||
|
507d0a9edf | ||
|
5474387748 | ||
|
fd327677a6 | ||
|
2d6e2b85ac | ||
|
059edeb68d | ||
|
c8c4196500 | ||
|
28b3f122d6 | ||
|
fb9d3b16d6 | ||
|
ce71ba0ccb | ||
|
48e05473b3 | ||
|
56c2d957df | ||
|
0c478ad1a0 | ||
|
6d751ce675 | ||
|
2aadff01a4 | ||
|
5da4e80e7b | ||
|
f3328ea675 | ||
|
2b17eaf819 | ||
|
dcbbee9996 | ||
|
36234c3f4b | ||
|
7f04a931a2 | ||
|
41fe70dee0 | ||
|
fecb38a4ba | ||
|
31f0a785ee | ||
|
6a95549605 | ||
|
91d9b1abea | ||
|
e60c9f9016 | ||
|
5fb55cc7c4 | ||
|
4a018ca361 | ||
|
f495c14b47 | ||
|
6d225c24a0 | ||
|
7920c48d6a | ||
|
d24ced9c1f | ||
|
5e914681df | ||
|
2f38cd26bf | ||
|
7a8aeac749 | ||
|
d39d596a77 | ||
|
f6f978fd3d | ||
|
6cd32fd936 | ||
|
d121e62763 | ||
|
f4d66f4d5a | ||
|
488c869d17 | ||
|
5354f85616 | ||
|
8867c4aba1 | ||
|
6073f5434b | ||
|
6d01b5601c | ||
|
25cf8451c3 | ||
|
d87b980a0e | ||
|
119d131ea9 | ||
|
73e6230af4 | ||
|
c0348e860f | ||
|
4320041ed3 | ||
|
0c2a66fe4a | ||
|
17121bc5b5 | ||
|
87b5d6d564 | ||
|
00897e0bc2 | ||
|
4cfc1fc717 | ||
|
cb4ed6491a | ||
|
4eb90b7bc7 | ||
|
5308ecb864 | ||
|
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 | ||
|
8246a6aea9 | ||
|
1e09ec2f22 | ||
|
4c018f4174 | ||
|
080aaa90aa | ||
|
6ac64298f6 | ||
|
76e932f3c9 | ||
|
06078e295b | ||
|
004434a414 | ||
|
4661d6d7af | ||
|
a972260284 | ||
|
f1d6c2dca3 | ||
|
aa1cba4c18 | ||
|
9b33a4edd8 | ||
|
938e6aea6f | ||
|
699d304845 | ||
|
2ce0b003ee | ||
|
10653669e8 | ||
|
c39f04a86a | ||
|
430f28afbf | ||
|
8e07307f04 | ||
|
c8fed30c65 | ||
|
11c388252c | ||
|
e41956c7e7 | ||
|
f3583f71f8 | ||
|
e8e53ed431 | ||
|
cff82c7ec9 | ||
|
c388aba95a | ||
|
02f0ffd558 | ||
|
b616fd026e | ||
|
ba79823749 | ||
|
0af5090b86 | ||
|
b750589747 | ||
|
894d4b1412 | ||
|
95615643f0 | ||
|
fb48683668 | ||
|
0fb7b7c32a | ||
|
3e4ab24cbb | ||
|
8c86d0f83f | ||
|
673922cfd2 | ||
|
b11cb71e96 | ||
|
bdadb3b4a0 | ||
|
9bdc8b271f | ||
|
c208033c6a | ||
|
01aec28fd9 | ||
|
484de7fc89 | ||
|
71f4fe31a8 | ||
|
883f800b96 | ||
|
b3805d65e3 | ||
|
4f8d5b9d3e | ||
|
efbd4fa1ef | ||
|
b7565b81a7 | ||
|
ec6352b0a0 | ||
|
ce4df54075 | ||
|
c855a500d1 | ||
|
baf52378ae | ||
|
e6ebd7ca5c | ||
|
83fef4e8a1 | ||
|
d3dc225920 | ||
|
6342438c4e | ||
|
324fe6cdde | ||
|
4faa5d5a97 | ||
|
381bc30cef | ||
|
7531167f24 | ||
|
66a1436815 | ||
|
1d1fedd99c | ||
|
823751817d | ||
|
1edcf07a19 | ||
|
4c95214a9b | ||
|
b37c0a038c | ||
|
d9841567d1 | ||
|
2cce94bfb0 | ||
|
d968e8b11b | ||
|
6c73093f9b | ||
|
d17c882ef3 | ||
|
d5f4e48a66 | ||
|
484d8bd85d | ||
|
74b6a9337d | ||
|
2dd62d446e | ||
|
b6a98c049d | ||
|
67e8822279 | ||
|
956a240181 | ||
|
179aca65d7 | ||
|
e4c8b68e33 | ||
|
731211fd81 | ||
|
006313b45a | ||
|
3c86e9a421 | ||
|
e49ebca896 | ||
|
d9652cf06a | ||
|
4517bf7ce3 | ||
|
04aa84a2af | ||
|
11baa5dc07 | ||
|
1281ebd51a | ||
|
46dc4f23d5 | ||
|
d469aaf0ef | ||
|
5692e041c6 | ||
|
55e7b1ec28 | ||
|
d99b58f9d5 | ||
|
b8ce455e56 | ||
|
c9e477ec05 | ||
|
979d417e9d | ||
|
b74a77cefc | ||
|
2af7d9797a | ||
|
b72489d629 | ||
|
1461de7c9d | ||
|
3061b7bc6e | ||
|
98ee7d63d3 | ||
|
066b4398c0 | ||
|
028e1bbdd5 | ||
|
8b8594e5e4 | ||
|
cefb562076 | ||
|
9eccdd1603 | ||
|
f429dfef59 | ||
|
963e38fd7d | ||
|
f411b5902d | ||
|
97dd1f3ef4 | ||
|
3413a77ac7 | ||
|
206756cd93 | ||
|
263b5927d1 | ||
|
a0ad81c6c7 | ||
|
23846c7f83 | ||
|
96ac64ddca | ||
|
a2ed0052e5 | ||
|
6e7e889e01 | ||
|
7e18a60b72 | ||
|
0be4e396cf | ||
|
b86bbce924 | ||
|
e1e9dd3f1c | ||
|
8c2a43987b | ||
|
c5ff844b40 | ||
|
1f47cf3861 | ||
|
bcdc7ae6f9 | ||
|
91ca84eab5 | ||
|
875d189c30 | ||
|
7c3f5f4833 | ||
|
7a7e797068 | ||
|
a41f37fc96 | ||
|
10554ac6c3 | ||
|
2439726cfe | ||
|
4f04f4efd9 | ||
|
32d0abbc7c | ||
|
ed5b8b1eea | ||
|
ea241dfc79 | ||
|
0e70fec054 | ||
|
5077e8d6bd | ||
|
901e854f30 | ||
|
f3b1bd6331 | ||
|
51ade9f54b | ||
|
3c86dbf9e6 | ||
|
b2d426ea4e | ||
|
fbe0cd3d1a | ||
|
5f9821866a | ||
|
ee153ec8cf | ||
|
e138d84114 | ||
|
90843d3c39 | ||
|
90ad96bd01 | ||
|
f8cc05d28d | ||
|
fe78afb6d7 | ||
|
af7dbce93d | ||
|
2e283eddc6 | ||
|
3e0a17d097 | ||
|
7ebddff547 | ||
|
1397aded03 | ||
|
40fd4990fc | ||
|
5784478398 | ||
|
6e9abd8a8d | ||
|
ef76e27efb | ||
|
6be23859c3 | ||
|
aa09d77c75 | ||
|
b86b066e8c | ||
|
7556cef424 | ||
|
ff7ec1c208 | ||
|
6f333b4ebc | ||
|
65682597a6 | ||
|
fc9f1a6380 | ||
|
4bb662aed6 | ||
|
a5c1d5b56d | ||
|
dc7361b29a | ||
|
3c9fba7af9 | ||
|
ae3acffc2b | ||
|
f3f5770a0e | ||
|
64178c87e0 | ||
|
b1a0d5fc24 | ||
|
7beba58e42 | ||
|
062968931c | ||
|
76116b7f39 | ||
|
e635c55400 | ||
|
9e25487be4 | ||
|
39529236f7 | ||
|
f0728e6543 | ||
|
188c998f8f | ||
|
1fc2947eb6 | ||
|
4e47f5e151 | ||
|
e543bbd98a | ||
|
4fb3c76ad7 | ||
|
fcc56736a6 | ||
|
014d447f0c | ||
|
9d998cbb4c | ||
|
baab1666cc | ||
|
f639d94f5e | ||
|
fd4e62ed7d | ||
|
21aff96eee | ||
|
f4703cdc23 | ||
|
7980880191 | ||
|
4efcea62da | ||
|
580fc69c02 | ||
|
facb42d776 | ||
|
37fd4d3558 | ||
|
bc78d4cf02 | ||
|
91ff2766bf | ||
|
c216905beb |
51 changed files with 51330 additions and 239 deletions
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
|
7
.github/ISSUE_TEMPLATE.md
vendored
Normal file
7
.github/ISSUE_TEMPLATE.md
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
### Subject of the issue
|
||||
|
||||
Describe your issue here.
|
||||
|
||||
### Steps to reproduce
|
||||
|
||||
If this issue is describing a possible bug please provide (or link to) your GitHub Actions workflow.
|
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
|
140
.github/workflows/ci.yml
vendored
Normal file
140
.github/workflows/ci.yml
vendored
Normal file
|
@ -0,0 +1,140 @@
|
|||
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
|
||||
- name: Install Docker
|
||||
run: apt update && apt install docker.io -y
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npm run format-check
|
||||
- run: npm run lint
|
||||
- run: npm run test
|
||||
- uses: actions/upload-artifact@v4
|
||||
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
|
||||
```
|
||||
```
|
||||
/test repository=${{ github.event.pull_request.head.repo.full_name }} ref=${{ github.event.pull_request.head.ref }} build=true sign-commits=true
|
||||
```
|
||||
|
||||
package:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs: [test]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: dist
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
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
|
49
.github/workflows/cpr-example-command.yml
vendored
Normal file
49
.github/workflows/cpr-example-command.yml
vendored
Normal file
|
@ -0,0 +1,49 @@
|
|||
name: Create Pull Request Example Command
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [cpr-example-command]
|
||||
jobs:
|
||||
createPullRequest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Make changes to pull request
|
||||
run: date +%s > report.txt
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: ./
|
||||
with:
|
||||
commit-message: Update report
|
||||
committer: github-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: |
|
||||
Update report
|
||||
- Updated with *today's* date
|
||||
- Auto-generated by [create-pull-request][1]
|
||||
|
||||
[1]: https://github.com/peter-evans/create-pull-request
|
||||
labels: |
|
||||
report
|
||||
automated pr
|
||||
assignees: peter-evans
|
||||
reviewers: peter-evans
|
||||
milestone: 1
|
||||
draft: false
|
||||
branch: example-patches
|
||||
delete-branch: true
|
||||
|
||||
- name: Check output
|
||||
run: |
|
||||
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@v4
|
||||
with:
|
||||
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
|
||||
comment-id: ${{ github.event.client_payload.github.payload.comment.id }}
|
||||
reaction-type: hooray
|
21
.github/workflows/push.yml
vendored
21
.github/workflows/push.yml
vendored
|
@ -1,21 +0,0 @@
|
|||
on: push
|
||||
name: create-pull-request action testing workflow
|
||||
jobs:
|
||||
createPullRequest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Create report file
|
||||
uses: finnp/create-file-action@1.0.0
|
||||
env:
|
||||
FILE_DATA: This is created to test create-pull-request action.
|
||||
FILE_NAME: report.txt
|
||||
- name: Create Pull Request
|
||||
uses: ./
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REPO_ACCESS_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
|
||||
COMMIT_MESSAGE: Add report file
|
||||
PULL_REQUEST_BODY: This PR is auto-generated by [create-pull-request](https://github.com/peter-evans/create-pull-request).
|
||||
PULL_REQUEST_BRANCH: test-patches
|
||||
PULL_REQUEST_TITLE: '[Test] Add report file'
|
43
.github/workflows/slash-command-dispatch.yml
vendored
Normal file
43
.github/workflows/slash-command-dispatch.yml
vendored
Normal file
|
@ -0,0 +1,43 @@
|
|||
name: Slash Command Dispatch
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
jobs:
|
||||
slashCommandDispatch:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Slash Command Dispatch
|
||||
uses: peter-evans/slash-command-dispatch@v4
|
||||
with:
|
||||
token: ${{ secrets.ACTIONS_BOT_TOKEN }}
|
||||
config: >
|
||||
[
|
||||
{
|
||||
"command": "test",
|
||||
"permission": "admin",
|
||||
"repository": "peter-evans/create-pull-request-tests",
|
||||
"named_args": true
|
||||
},
|
||||
{
|
||||
"command": "testv5",
|
||||
"permission": "admin",
|
||||
"repository": "peter-evans/create-pull-request-tests",
|
||||
"named_args": true
|
||||
},
|
||||
{
|
||||
"command": "clean",
|
||||
"permission": "admin",
|
||||
"repository": "peter-evans/create-pull-request-tests"
|
||||
},
|
||||
{
|
||||
"command": "cpr-example",
|
||||
"permission": "admin",
|
||||
"issue_type": "issue"
|
||||
},
|
||||
{
|
||||
"command": "rebase",
|
||||
"permission": "admin",
|
||||
"repository": "peter-evans/slash-command-dispatch-processor",
|
||||
"issue_type": "pull-request"
|
||||
}
|
||||
]
|
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:
|
||||
- v6
|
||||
- v7
|
||||
|
||||
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
|
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
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"
|
||||
}
|
18
Dockerfile
18
Dockerfile
|
@ -1,18 +0,0 @@
|
|||
FROM python:3.7.3
|
||||
|
||||
LABEL maintainer="Peter Evans <mail@peterevans.dev>"
|
||||
LABEL repository="https://github.com/peter-evans/create-pull-request"
|
||||
LABEL homepage="https://github.com/peter-evans/create-pull-request"
|
||||
|
||||
LABEL com.github.actions.name="Create Pull Request"
|
||||
LABEL com.github.actions.description="Creates a pull request for changes to your repository in the actions workspace"
|
||||
LABEL com.github.actions.icon="git-pull-request"
|
||||
LABEL com.github.actions.color="gray-dark"
|
||||
|
||||
COPY LICENSE README.md /
|
||||
|
||||
COPY requirements.txt /tmp/
|
||||
RUN pip install --requirement /tmp/requirements.txt
|
||||
|
||||
COPY create-pull-request.py /create-pull-request.py
|
||||
ENTRYPOINT [ "/create-pull-request.py" ]
|
318
README.md
318
README.md
|
@ -1,79 +1,307 @@
|
|||
# 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.
|
||||
|
||||
Changes to a repository in the actions workspace persist between actions in a workflow.
|
||||
This action is designed to be used in conjunction with other actions that modify or add files to your repository.
|
||||
Changes to a repository in the Actions workspace persist between steps in a workflow.
|
||||
This action is designed to be used in conjunction with other steps that modify or add files to your repository.
|
||||
The changes will be automatically committed to a new branch and a pull request created.
|
||||
|
||||
Create Pull Request action will:
|
||||
|
||||
1. Check for repository changes in the actions workspace. This includes untracked (new) files as well as modified files.
|
||||
2. Commit all changes to a new branch. The commit will be made using the name and email of the `HEAD` commit author.
|
||||
3. Create a pull request to merge the new branch into the currently active branch executing the workflow.
|
||||
1. Check for repository changes in the Actions workspace. This includes:
|
||||
- untracked (new) files
|
||||
- tracked (modified) files
|
||||
- commits made during the workflow that have not been pushed
|
||||
2. Commit all changes to a new branch, or update an existing pull request branch.
|
||||
3. Create or update a pull request to merge the branch into the base—the branch checked out in the workflow.
|
||||
|
||||
Note: Modifying a repository during workflows is not good practice in general.
|
||||
However, this action opens up some interesting possibilities when used carefully.
|
||||
This action is experimental and may not work well for repositories that have a very high frequency of commits.
|
||||
## Documentation
|
||||
|
||||
- [Concepts, guidelines and advanced usage](docs/concepts-guidelines.md)
|
||||
- [Examples](docs/examples.md)
|
||||
- [Updating to v7](docs/updating.md)
|
||||
- [Common issues](docs/common-issues.md)
|
||||
|
||||
## Usage
|
||||
|
||||
In addition to the default `GITHUB_TOKEN`, the action requires a `repo` scoped token in order to commit.
|
||||
Create one [here](https://github.com/settings/tokens) and pass that as a secret to the `REPO_ACCESS_TOKEN` environment variable.
|
||||
```yml
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
```
|
||||
|
||||
You can also pin to a [specific release](https://github.com/peter-evans/create-pull-request/releases) version in the format `@v7.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
|
||||
|
||||
All inputs are **optional**. If not set, sensible defaults will be used.
|
||||
|
||||
| Name | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `token` | The token that the action will use to create and update the pull request. See [token](#token). | `GITHUB_TOKEN` |
|
||||
| `branch-token` | The token that the action will use to create and update the branch. See [branch-token](#branch-token). | Defaults to the value of `token` |
|
||||
| `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` |
|
||||
| `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. | |
|
||||
| `draft` | Create a [draft pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests#draft-pull-requests). Valid values are `true` (only on create), `always-true` (on create and update), and `false`. | `false` |
|
||||
| `maintainer-can-modify` | Indicates whether [maintainers can modify](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork) the pull request. | `true` |
|
||||
|
||||
#### token
|
||||
|
||||
The token input defaults to the repository's `GITHUB_TOKEN`.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> - If you want pull requests created by this action to trigger an `on: push` or `on: pull_request` workflow then you cannot use the default `GITHUB_TOKEN`. See the [documentation here](docs/concepts-guidelines.md#triggering-further-workflow-runs) for further details.
|
||||
> - If using the repository's `GITHUB_TOKEN` and your repository was created after 2nd February 2023, the [default permission is read-only](https://github.blog/changelog/2023-02-02-github-actions-updating-the-default-github_token-permissions-to-read-only/). Elevate the [permissions](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token#defining-access-for-the-github_token-permissions) in your workflow.
|
||||
> ```yml
|
||||
> permissions:
|
||||
> contents: write
|
||||
> pull-requests: write
|
||||
> ```
|
||||
|
||||
Other token options:
|
||||
- Classic [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) with `repo` scope.
|
||||
- Fine-grained [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) with `contents: write` and `pull-requests: write` scopes.
|
||||
- [GitHub App tokens](docs/concepts-guidelines.md#authenticating-with-github-app-generated-tokens) with `contents: write` and `pull-requests: write` scopes.
|
||||
|
||||
> [!TIP]
|
||||
> If pull requests could contain changes to Actions workflows you may also need the `workflows` scope.
|
||||
|
||||
#### branch-token
|
||||
|
||||
The action first creates a branch, and then creates a pull request for the branch.
|
||||
For some rare use cases it can be useful, or even necessary, to use different tokens for these operations.
|
||||
It is not advisable to use this input unless you know you need to.
|
||||
|
||||
#### commit-message
|
||||
|
||||
In addition to a message, the `commit-message` input can also be used to populate the commit description. Leave a single blank line between the message and description.
|
||||
|
||||
```yml
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v1.1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REPO_ACCESS_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
|
||||
commit-message: |
|
||||
the first line is the commit message
|
||||
|
||||
the commit description starts
|
||||
after a blank line and can be
|
||||
multiple lines
|
||||
```
|
||||
|
||||
#### Environment variables
|
||||
#### delete-branch
|
||||
|
||||
These variables are all optional. If not set, a default value will be used.
|
||||
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.
|
||||
|
||||
- `PULL_REQUEST_BRANCH` - The branch name. See **Branch naming** below for details.
|
||||
- `COMMIT_MESSAGE` - The message to use when committing changes.
|
||||
- `PULL_REQUEST_TITLE` - The title of the pull request.
|
||||
- `PULL_REQUEST_BODY` - The body of the pull request.
|
||||
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.
|
||||
|
||||
#### Branch naming
|
||||
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.
|
||||
|
||||
The variable `PULL_REQUEST_BRANCH` defaults to `create-pull-request/patch`.
|
||||
Commits will be made to a branch with this name and suffixed with the short SHA1 commit hash.
|
||||
#### Proxy support
|
||||
|
||||
e.g.
|
||||
```
|
||||
create-pull-request/patch-fcdfb59
|
||||
create-pull-request/patch-394710b
|
||||
For self-hosted runners behind a corporate proxy set the `https_proxy` environment variable.
|
||||
```yml
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
env:
|
||||
https_proxy: http://<proxy_address>:<port>
|
||||
```
|
||||
|
||||
#### Ignoring files
|
||||
### 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@v7
|
||||
- name: Check outputs
|
||||
if: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
run: |
|
||||
echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
|
||||
echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}"
|
||||
```
|
||||
|
||||
### Action behaviour
|
||||
|
||||
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.
|
||||
|
||||
How the action behaves:
|
||||
|
||||
- If there are changes (i.e. a diff exists with the checked-out base branch), the changes will be pushed to a new `branch` and a pull request created.
|
||||
- If there are no changes (i.e. no diff exists with the checked-out base branch), no pull request will be created and the action exits silently.
|
||||
- If a pull request already exists it will be updated if necessary. Local changes in the Actions workspace, or changes on the base branch, can cause an update. If no update is required the action exits silently.
|
||||
- If a pull request exists and new changes on the base branch make the pull request unnecessary (i.e. there is no longer a diff between the pull request branch and the base), the pull request is automatically closed. Additionally, if [`delete-branch`](#delete-branch) is set to `true` the `branch` will be deleted.
|
||||
|
||||
For further details about how the action works and usage guidelines, see [Concepts, guidelines and advanced usage](docs/concepts-guidelines.md).
|
||||
|
||||
#### Alternative strategy - Always create a new pull request branch
|
||||
|
||||
For some use cases it may be desirable to always create a new unique branch each time there are changes to be committed.
|
||||
This strategy is *not recommended* because if not used carefully it could result in multiple pull requests being created unnecessarily. If in doubt, use the [default strategy](#action-behaviour) of creating an updating a fixed-name branch.
|
||||
|
||||
To use this strategy, set input `branch-suffix` with one of the following options.
|
||||
|
||||
- `random` - Commits will be made to a branch suffixed with a random alpha-numeric string. e.g. `create-pull-request/patch-6qj97jr`, `create-pull-request/patch-5jrjhvd`
|
||||
|
||||
- `timestamp` - Commits will be made to a branch suffixed by a timestamp. e.g. `create-pull-request/patch-1569322532`, `create-pull-request/patch-1569322552`
|
||||
|
||||
- `short-commit-hash` - Commits will be made to a branch suffixed with the short SHA1 commit hash. e.g. `create-pull-request/patch-fcdfb59`, `create-pull-request/patch-394710b`
|
||||
|
||||
### 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.
|
||||
|
||||
## Example
|
||||
#### Add specific paths
|
||||
|
||||
Here is an example that sets all the environment variables.
|
||||
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.
|
||||
|
||||
```yml
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v1.1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REPO_ACCESS_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
|
||||
PULL_REQUEST_BRANCH: my-patches
|
||||
COMMIT_MESSAGE: Auto-modify files by my-file-modifier-action
|
||||
PULL_REQUEST_TITLE: Changes from my-file-modifier-action
|
||||
PULL_REQUEST_BODY: This is an auto-generated PR with changes from my-file-modifier-action
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
add-paths: |
|
||||
*.java
|
||||
docs/*.md
|
||||
```
|
||||
|
||||
This configuration will create pull requests that look like this:
|
||||
#### 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@v4
|
||||
- name: Create commits
|
||||
run: |
|
||||
git config user.name 'Peter Evans'
|
||||
git config user.email 'peter-evans@users.noreply.github.com'
|
||||
date +%s > report.txt
|
||||
git commit -am "Modify tracked file during workflow"
|
||||
date +%s > new-report.txt
|
||||
git add -A
|
||||
git commit -m "Add untracked file during workflow"
|
||||
- name: Uncommitted change
|
||||
run: date +%s > report.txt
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
```
|
||||
|
||||
### 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 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
|
||||
jobs:
|
||||
createPullRequest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- 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@v7
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
commit-message: Update report
|
||||
committer: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
||||
author: ${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>
|
||||
signoff: false
|
||||
branch: example-patches
|
||||
delete-branch: true
|
||||
title: '[Example] Update report'
|
||||
body: |
|
||||
Update report
|
||||
- Updated with *today's* date
|
||||
- Auto-generated by [create-pull-request][1]
|
||||
|
||||
[1]: https://github.com/peter-evans/create-pull-request
|
||||
labels: |
|
||||
report
|
||||
automated pr
|
||||
assignees: peter-evans
|
||||
reviewers: peter-evans
|
||||
team-reviewers: |
|
||||
developers
|
||||
qa-team
|
||||
milestone: 1
|
||||
draft: false
|
||||
```
|
||||
|
||||
An example based on the above reference configuration creates pull requests that look like this:
|
||||
|
||||

|
||||
|
||||
## License
|
||||
|
||||
MIT License - see the [LICENSE](LICENSE) file for details
|
||||
[MIT](LICENSE)
|
||||
|
|
2468
__test__/create-or-update-branch.int.test.ts
Normal file
2468
__test__/create-or-update-branch.int.test.ts
Normal file
File diff suppressed because it is too large
Load diff
49
__test__/entrypoint.sh
Executable file
49
__test__/entrypoint.sh
Executable file
|
@ -0,0 +1,49 @@
|
|||
#!/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_TEMP.md
|
||||
git add .
|
||||
git commit -m "initial commit"
|
||||
git commit --allow-empty -m "empty commit for tests"
|
||||
echo "#test-base :sparkles:" > README_TEMP.md
|
||||
git add .
|
||||
git commit -m "add sparkles" -m "Change description:
|
||||
- updates README_TEMP.md to add sparkles to the title"
|
||||
mv README_TEMP.md README.md
|
||||
git add .
|
||||
git commit -m "rename readme"
|
||||
git push -u
|
||||
git log -1 --pretty=oneline
|
||||
git config --global --unset user.email
|
||||
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
|
48
__test__/git-command-manager.int.test.ts
Normal file
48
__test__/git-command-manager.int.test.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import {GitCommandManager} from '../lib/git-command-manager'
|
||||
|
||||
const REPO_PATH = '/git/local/repos/test-base'
|
||||
|
||||
describe('git-command-manager integration tests', () => {
|
||||
let git: GitCommandManager
|
||||
|
||||
beforeAll(async () => {
|
||||
git = await GitCommandManager.create(REPO_PATH)
|
||||
await git.checkout('main')
|
||||
})
|
||||
|
||||
it('tests getCommit', async () => {
|
||||
const initialCommit = await git.getCommit('HEAD^^^')
|
||||
const emptyCommit = await git.getCommit('HEAD^^')
|
||||
const modifiedCommit = await git.getCommit('HEAD^')
|
||||
const headCommit = await git.getCommit('HEAD')
|
||||
|
||||
expect(initialCommit.subject).toEqual('initial commit')
|
||||
expect(initialCommit.signed).toBeFalsy()
|
||||
expect(initialCommit.changes[0].mode).toEqual('100644')
|
||||
expect(initialCommit.changes[0].status).toEqual('A')
|
||||
expect(initialCommit.changes[0].path).toEqual('README_TEMP.md')
|
||||
|
||||
expect(emptyCommit.subject).toEqual('empty commit for tests')
|
||||
expect(emptyCommit.tree).toEqual(initialCommit.tree) // empty commits have no tree and reference the parent's
|
||||
expect(emptyCommit.parents[0]).toEqual(initialCommit.sha)
|
||||
expect(emptyCommit.signed).toBeFalsy()
|
||||
expect(emptyCommit.changes).toEqual([])
|
||||
|
||||
expect(modifiedCommit.subject).toEqual('add sparkles')
|
||||
expect(modifiedCommit.parents[0]).toEqual(emptyCommit.sha)
|
||||
expect(modifiedCommit.signed).toBeFalsy()
|
||||
expect(modifiedCommit.changes[0].mode).toEqual('100644')
|
||||
expect(modifiedCommit.changes[0].status).toEqual('M')
|
||||
expect(modifiedCommit.changes[0].path).toEqual('README_TEMP.md')
|
||||
|
||||
expect(headCommit.subject).toEqual('rename readme')
|
||||
expect(headCommit.parents[0]).toEqual(modifiedCommit.sha)
|
||||
expect(headCommit.signed).toBeFalsy()
|
||||
expect(headCommit.changes[0].mode).toEqual('100644')
|
||||
expect(headCommit.changes[0].status).toEqual('A')
|
||||
expect(headCommit.changes[0].path).toEqual('README.md')
|
||||
expect(headCommit.changes[1].mode).toEqual('100644')
|
||||
expect(headCommit.changes[1].status).toEqual('D')
|
||||
expect(headCommit.changes[1].path).toEqual('README_TEMP.md')
|
||||
})
|
||||
})
|
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`
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
101
action.yml
Normal file
101
action.yml
Normal file
|
@ -0,0 +1,101 @@
|
|||
name: 'Create Pull Request'
|
||||
description: 'Creates a pull request for changes to your repository in the actions workspace'
|
||||
inputs:
|
||||
token:
|
||||
description: 'The token that the action will use to create and update the pull request.'
|
||||
default: ${{ github.token }}
|
||||
branch-token:
|
||||
description: >
|
||||
The token that the action will use to create and update the branch.
|
||||
Defaults to the value of `token`.
|
||||
path:
|
||||
description: >
|
||||
Relative path under $GITHUB_WORKSPACE to the repository.
|
||||
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 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 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.'
|
||||
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 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.
|
||||
Valid values are `true` (only on create), `always-true` (on create and update), and `false`.
|
||||
default: false
|
||||
maintainer-can-modify:
|
||||
description: 'Indicates whether maintainers can modify the pull request.'
|
||||
default: true
|
||||
outputs:
|
||||
pull-request-number:
|
||||
description: 'The pull request number'
|
||||
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: 'node20'
|
||||
main: 'dist/index.js'
|
||||
branding:
|
||||
icon: 'git-pull-request'
|
||||
color: 'gray-dark'
|
|
@ -1,147 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
''' Create Pull Request '''
|
||||
import json
|
||||
import os
|
||||
from git import Repo
|
||||
from github import Github
|
||||
|
||||
|
||||
def get_github_event(github_event_path):
|
||||
with open(github_event_path) as f:
|
||||
github_event = json.load(f)
|
||||
if os.environ.get('DEBUG_EVENT') is not None:
|
||||
print(json.dumps(github_event, sort_keys=True, indent=2))
|
||||
return github_event
|
||||
|
||||
|
||||
def ignore_event(github_event):
|
||||
# Ignore push events on deleted branches
|
||||
# The event we want to ignore occurs when a PR is created but the repository owner decides
|
||||
# not to commit the changes. They close the PR and delete the branch. This creates a
|
||||
# "push" event that we want to ignore, otherwise it will create another branch and PR on
|
||||
# the same commit.
|
||||
deleted = "{deleted}".format(**github_event)
|
||||
if deleted == "True":
|
||||
print("Ignoring delete branch event.")
|
||||
return True
|
||||
ref = "{ref}".format(**github_event)
|
||||
if not ref.startswith('refs/heads/'):
|
||||
print("Ignoring events for tags and remotes.")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def pr_branch_exists(repo, branch):
|
||||
for ref in repo.remotes.origin.refs:
|
||||
if ref.name == ("origin/%s" % branch):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_head_author(github_event):
|
||||
email = "{head_commit[author][email]}".format(**github_event)
|
||||
name = "{head_commit[author][name]}".format(**github_event)
|
||||
return email, name
|
||||
|
||||
|
||||
def get_head_short_sha1(repo):
|
||||
return repo.git.rev_parse('--short', 'HEAD')
|
||||
|
||||
|
||||
def set_git_config(git, email, name):
|
||||
git.config('--global', 'user.email', '"%s"' % email)
|
||||
git.config('--global', 'user.name', '"%s"' % name)
|
||||
|
||||
|
||||
def set_git_remote_url(git, token, github_repository):
|
||||
git.remote('set-url', 'origin', "https://%s:x-oauth-basic@github.com/%s" % (token, github_repository))
|
||||
|
||||
|
||||
def commit_changes(git, branch, commit_message):
|
||||
git.checkout('HEAD', b=branch)
|
||||
git.add('-A')
|
||||
git.commit(m=commit_message)
|
||||
return git.push('--set-upstream', 'origin', branch)
|
||||
|
||||
|
||||
def create_pull_request(token, repo, head, base, title, body):
|
||||
return Github(token).get_repo(repo).create_pull(
|
||||
title=title,
|
||||
body=body,
|
||||
base=base,
|
||||
head=head)
|
||||
|
||||
|
||||
def process_event(github_event, repo, branch, base):
|
||||
# Fetch required environment variables
|
||||
github_token = os.environ['GITHUB_TOKEN']
|
||||
github_repository = os.environ['GITHUB_REPOSITORY']
|
||||
repo_access_token = os.environ['REPO_ACCESS_TOKEN']
|
||||
# Fetch remaining optional environment variables
|
||||
commit_message = os.getenv(
|
||||
'COMMIT_MESSAGE',
|
||||
"Auto-committed changes by create-pull-request action")
|
||||
title = os.getenv(
|
||||
'PULL_REQUEST_TITLE',
|
||||
"Auto-generated by create-pull-request action")
|
||||
body = os.getenv(
|
||||
'PULL_REQUEST_BODY', "Auto-generated pull request by "
|
||||
"[create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub Action")
|
||||
|
||||
# Get the HEAD committer's email and name
|
||||
author_email, author_name = get_head_author(github_event)
|
||||
# Set git configuration
|
||||
set_git_config(repo.git, author_email, author_name)
|
||||
# Update URL for the 'origin' remote
|
||||
set_git_remote_url(repo.git, repo_access_token, github_repository)
|
||||
|
||||
# Commit the repository changes
|
||||
print("Committing changes.")
|
||||
commit_result = commit_changes(repo.git, branch, commit_message)
|
||||
print(commit_result)
|
||||
|
||||
# Create the pull request
|
||||
print("Creating a request to pull %s into %s." % (branch, base))
|
||||
pull_request = create_pull_request(
|
||||
github_token,
|
||||
github_repository,
|
||||
branch,
|
||||
base,
|
||||
title,
|
||||
body
|
||||
)
|
||||
print("Created pull request %d." % pull_request.number)
|
||||
|
||||
|
||||
# Get the JSON event data
|
||||
github_event = get_github_event(os.environ['GITHUB_EVENT_PATH'])
|
||||
# Check if this event should be ignored
|
||||
if not ignore_event(github_event):
|
||||
# Set the repo to the working directory
|
||||
repo = Repo(os.getcwd())
|
||||
|
||||
# Fetch/Set the branch name
|
||||
branch = os.getenv('PULL_REQUEST_BRANCH', 'create-pull-request/patch')
|
||||
# Set the current branch as the target base branch
|
||||
base = os.environ['GITHUB_REF'][11:]
|
||||
|
||||
# Skip if the current branch is a PR branch created by this action
|
||||
if not base.startswith(branch):
|
||||
# Suffix with the short SHA1 hash
|
||||
branch = "%s-%s" % (branch, get_head_short_sha1(repo))
|
||||
|
||||
# Check if a PR branch already exists for this HEAD commit
|
||||
if not pr_branch_exists(repo, branch):
|
||||
# Check if there are changes to pull request
|
||||
if repo.is_dirty() or len(repo.untracked_files) > 0:
|
||||
print("Repository has modified or untracked files.")
|
||||
process_event(github_event, repo, branch, base)
|
||||
else:
|
||||
print("Repository has no modified or untracked files. Skipping.")
|
||||
else:
|
||||
print(
|
||||
"Pull request branch '%s' already exists for this commit. Skipping." %
|
||||
branch)
|
||||
else:
|
||||
print(
|
||||
"Branch '%s' was created by this action. Skipping." % base)
|
16
dist/790.index.js
vendored
Normal file
16
dist/790.index.js
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
"use strict";
|
||||
exports.id = 790;
|
||||
exports.ids = [790];
|
||||
exports.modules = {
|
||||
|
||||
/***/ 790:
|
||||
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
|
||||
|
||||
var y=Object.defineProperty;var c=(R,o)=>y(R,"name",{value:o,configurable:!0});__webpack_require__(3024),__webpack_require__(6760);const node=__webpack_require__(117);__webpack_require__(7067),__webpack_require__(4708),__webpack_require__(8522),__webpack_require__(7075),__webpack_require__(4573),__webpack_require__(7975),__webpack_require__(7713),__webpack_require__(3136),__webpack_require__(7030);let s=0;const S={START_BOUNDARY:s++,HEADER_FIELD_START:s++,HEADER_FIELD:s++,HEADER_VALUE_START:s++,HEADER_VALUE:s++,HEADER_VALUE_ALMOST_DONE:s++,HEADERS_ALMOST_DONE:s++,PART_DATA_START:s++,PART_DATA:s++,END:s++};let f=1;const F={PART_BOUNDARY:f,LAST_BOUNDARY:f*=2},LF=10,CR=13,SPACE=32,HYPHEN=45,COLON=58,A=97,Z=122,lower=c(R=>R|32,"lower"),noop=c(()=>{},"noop"),g=class g{constructor(o){this.index=0,this.flags=0,this.onHeaderEnd=noop,this.onHeaderField=noop,this.onHeadersEnd=noop,this.onHeaderValue=noop,this.onPartBegin=noop,this.onPartData=noop,this.onPartEnd=noop,this.boundaryChars={},o=`\r
|
||||
--`+o;const t=new Uint8Array(o.length);for(let n=0;n<o.length;n++)t[n]=o.charCodeAt(n),this.boundaryChars[t[n]]=!0;this.boundary=t,this.lookbehind=new Uint8Array(this.boundary.length+8),this.state=S.START_BOUNDARY}write(o){let t=0;const n=o.length;let E=this.index,{lookbehind:l,boundary:h,boundaryChars:H,index:e,state:a,flags:d}=this;const b=this.boundary.length,m=b-1,O=o.length;let r,P;const u=c(D=>{this[D+"Mark"]=t},"mark"),i=c(D=>{delete this[D+"Mark"]},"clear"),T=c((D,p,_,N)=>{(p===void 0||p!==_)&&this[D](N&&N.subarray(p,_))},"callback"),L=c((D,p)=>{const _=D+"Mark";_ in this&&(p?(T(D,this[_],t,o),delete this[_]):(T(D,this[_],o.length,o),this[_]=0))},"dataCallback");for(t=0;t<n;t++)switch(r=o[t],a){case S.START_BOUNDARY:if(e===h.length-2){if(r===HYPHEN)d|=F.LAST_BOUNDARY;else if(r!==CR)return;e++;break}else if(e-1===h.length-2){if(d&F.LAST_BOUNDARY&&r===HYPHEN)a=S.END,d=0;else if(!(d&F.LAST_BOUNDARY)&&r===LF)e=0,T("onPartBegin"),a=S.HEADER_FIELD_START;else return;break}r!==h[e+2]&&(e=-2),r===h[e+2]&&e++;break;case S.HEADER_FIELD_START:a=S.HEADER_FIELD,u("onHeaderField"),e=0;case S.HEADER_FIELD:if(r===CR){i("onHeaderField"),a=S.HEADERS_ALMOST_DONE;break}if(e++,r===HYPHEN)break;if(r===COLON){if(e===1)return;L("onHeaderField",!0),a=S.HEADER_VALUE_START;break}if(P=lower(r),P<A||P>Z)return;break;case S.HEADER_VALUE_START:if(r===SPACE)break;u("onHeaderValue"),a=S.HEADER_VALUE;case S.HEADER_VALUE:r===CR&&(L("onHeaderValue",!0),T("onHeaderEnd"),a=S.HEADER_VALUE_ALMOST_DONE);break;case S.HEADER_VALUE_ALMOST_DONE:if(r!==LF)return;a=S.HEADER_FIELD_START;break;case S.HEADERS_ALMOST_DONE:if(r!==LF)return;T("onHeadersEnd"),a=S.PART_DATA_START;break;case S.PART_DATA_START:a=S.PART_DATA,u("onPartData");case S.PART_DATA:if(E=e,e===0){for(t+=m;t<O&&!(o[t]in H);)t+=b;t-=m,r=o[t]}if(e<h.length)h[e]===r?(e===0&&L("onPartData",!0),e++):e=0;else if(e===h.length)e++,r===CR?d|=F.PART_BOUNDARY:r===HYPHEN?d|=F.LAST_BOUNDARY:e=0;else if(e-1===h.length)if(d&F.PART_BOUNDARY){if(e=0,r===LF){d&=~F.PART_BOUNDARY,T("onPartEnd"),T("onPartBegin"),a=S.HEADER_FIELD_START;break}}else d&F.LAST_BOUNDARY&&r===HYPHEN?(T("onPartEnd"),a=S.END,d=0):e=0;if(e>0)l[e-1]=r;else if(E>0){const D=new Uint8Array(l.buffer,l.byteOffset,l.byteLength);T("onPartData",0,E,D),E=0,u("onPartData"),t--}break;case S.END:break;default:throw new Error(`Unexpected state entered: ${a}`)}L("onHeaderField"),L("onHeaderValue"),L("onPartData"),this.index=e,this.state=a,this.flags=d}end(){if(this.state===S.HEADER_FIELD_START&&this.index===0||this.state===S.PART_DATA&&this.index===this.boundary.length)this.onPartEnd();else if(this.state!==S.END)throw new Error("MultipartParser.end(): stream ended unexpectedly")}};c(g,"MultipartParser");let MultipartParser=g;function _fileName(R){const o=R.match(/\bfilename=("(.*?)"|([^()<>@,;:\\"/[\]?={}\s\t]+))($|;\s)/i);if(!o)return;const t=o[2]||o[3]||"";let n=t.slice(t.lastIndexOf("\\")+1);return n=n.replace(/%22/g,'"'),n=n.replace(/&#(\d{4});/g,(E,l)=>String.fromCharCode(l)),n}c(_fileName,"_fileName");async function toFormData(R,o){if(!/multipart/i.test(o))throw new TypeError("Failed to fetch");const t=o.match(/boundary=(?:"([^"]+)"|([^;]+))/i);if(!t)throw new TypeError("no or bad content-type header, no multipart boundary");const n=new MultipartParser(t[1]||t[2]);let E,l,h,H,e,a;const d=[],b=new node.FormData,m=c(i=>{h+=u.decode(i,{stream:!0})},"onPartData"),O=c(i=>{d.push(i)},"appendToFile"),r=c(()=>{const i=new node.File(d,a,{type:e});b.append(H,i)},"appendFileToFormData"),P=c(()=>{b.append(H,h)},"appendEntryToFormData"),u=new TextDecoder("utf-8");u.decode(),n.onPartBegin=function(){n.onPartData=m,n.onPartEnd=P,E="",l="",h="",H="",e="",a=null,d.length=0},n.onHeaderField=function(i){E+=u.decode(i,{stream:!0})},n.onHeaderValue=function(i){l+=u.decode(i,{stream:!0})},n.onHeaderEnd=function(){if(l+=u.decode(),E=E.toLowerCase(),E==="content-disposition"){const i=l.match(/\bname=("([^"]*)"|([^()<>@,;:\\"/[\]?={}\s\t]+))/i);i&&(H=i[2]||i[3]||""),a=_fileName(l),a&&(n.onPartData=O,n.onPartEnd=r)}else E==="content-type"&&(e=l);l="",E=""};for await(const i of R)n.write(i);return n.end(),b}c(toFormData,"toFormData"),exports.toFormData=toFormData;
|
||||
|
||||
|
||||
/***/ })
|
||||
|
||||
};
|
||||
;
|
36048
dist/index.js
vendored
Normal file
36048
dist/index.js
vendored
Normal file
File diff suppressed because one or more lines are too long
68
docs/assets/cpr-gitgraph.htm
Normal file
68
docs/assets/cpr-gitgraph.htm
Normal file
|
@ -0,0 +1,68 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>create-pull-request GitHub action</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- partial:index.partial.html -->
|
||||
<div id="graph-container"></div>
|
||||
<!-- partial -->
|
||||
<script src='https://cdn.jsdelivr.net/npm/@gitgraph/js'></script>
|
||||
<script>
|
||||
const graphContainer = document.getElementById("graph-container");
|
||||
|
||||
const customTemplate = GitgraphJS.templateExtend(GitgraphJS.TemplateName.Metro, {
|
||||
commit: {
|
||||
message: {
|
||||
displayAuthor: false,
|
||||
displayHash: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Instantiate the graph.
|
||||
const gitgraph = GitgraphJS.createGitgraph(graphContainer, {
|
||||
template: customTemplate,
|
||||
orientation: "vertical-reverse"
|
||||
});
|
||||
|
||||
const 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: localMain,
|
||||
commitOptions: {
|
||||
subject: "[create-pull-request] automated change",
|
||||
body: "Changes pushed to create the remote branch",
|
||||
},
|
||||
});
|
||||
main.commit("New commit on base");
|
||||
|
||||
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: localMain2,
|
||||
commitOptions: {
|
||||
subject: "[create-pull-request] automated change",
|
||||
body: "Changes force pushed to update the remote branch",
|
||||
},
|
||||
});
|
||||
|
||||
main.merge(remotePatch);
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
BIN
docs/assets/cpr-gitgraph.png
Normal file
BIN
docs/assets/cpr-gitgraph.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 108 KiB |
6
docs/assets/logo.svg
Normal file
6
docs/assets/logo.svg
Normal file
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="43%" height="43%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: #000000;"><title>git-pull-request</title>
|
||||
<circle cx="18" cy="18" r="3"></circle>
|
||||
<circle cx="6" cy="6" r="3"></circle>
|
||||
<path d="M13 6h3a2 2 0 0 1 2 2v7"></path>
|
||||
<line x1="6" y1="9" x2="6" y2="21"></line>
|
||||
</svg>
|
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@v7
|
||||
env:
|
||||
HUSKY: '0'
|
||||
```
|
||||
- If hooks are installed in a script, then add a condition checking if the `CI` environment variable exists.
|
||||
```sh
|
||||
#!/bin/sh
|
||||
|
||||
[ -n "$CI" ] && exit 0
|
||||
```
|
||||
- If preventing the hooks installing is problematic, just delete them in a workflow step before the action runs.
|
||||
```yml
|
||||
- run: rm .git/hooks -rf
|
||||
```
|
505
docs/concepts-guidelines.md
Normal file
505
docs/concepts-guidelines.md
Normal file
|
@ -0,0 +1,505 @@
|
|||
# Concepts, guidelines and advanced usage
|
||||
|
||||
This document covers terminology, how the action works, general usage guidelines, and advanced usage.
|
||||
|
||||
- [Terminology](#terminology)
|
||||
- [Events and checkout](#events-and-checkout)
|
||||
- [How the action works](#how-the-action-works)
|
||||
- [Guidelines](#guidelines)
|
||||
- [Providing a consistent base](#providing-a-consistent-base)
|
||||
- [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)
|
||||
- [Pushing to a fork with fine-grained permissions](#pushing-to-a-fork-with-fine-grained-permissions)
|
||||
- [Authenticating with GitHub App generated tokens](#authenticating-with-github-app-generated-tokens)
|
||||
- [Creating pull requests in a remote repository using GitHub App generated tokens](#creating-pull-requests-in-a-remote-repository-using-github-app-generated-tokens)
|
||||
- [Commit signing](#commit-signing)
|
||||
- [Commit signature verification for bots](#commit-signature-verification-for-bots)
|
||||
- [GPG commit signature verification](#gpg-commit-signature-verification)
|
||||
- [Running in a container or on self-hosted runners](#running-in-a-container-or-on-self-hosted-runners)
|
||||
|
||||
## Terminology
|
||||
|
||||
[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:
|
||||
|
||||
- The `base` of a pull request is the branch you intend to change once the proposed changes are merged.
|
||||
- The `branch` of a pull request represents what you intend the `base` to look like when merged. It is the `base` branch *plus* changes that have been made to it.
|
||||
|
||||
## Events and checkout
|
||||
|
||||
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@v4
|
||||
with:
|
||||
ref: develop
|
||||
```
|
||||
|
||||
## How the action works
|
||||
|
||||
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:
|
||||
|
||||
1. Checkout the `base` branch
|
||||
2. Make changes
|
||||
3. Execute `create-pull-request` action
|
||||
|
||||
The following git diagram shows how the action creates and updates a pull request branch.
|
||||
|
||||

|
||||
|
||||
## Guidelines
|
||||
|
||||
### Providing a consistent base
|
||||
|
||||
For the action to work correctly it should be executed in a workflow that checks out a *consistent* base branch. This will be the base of the pull request unless overridden with the `base` input.
|
||||
|
||||
This means your workflow should be consistently checking out the branch that you intend to modify once the PR is merged.
|
||||
|
||||
In the following example, the [`push`](https://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:
|
||||
push:
|
||||
create:
|
||||
jobs:
|
||||
example:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
### Events which checkout a commit
|
||||
|
||||
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: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
base: ${{ github.head_ref }}
|
||||
```
|
||||
|
||||
Workflows triggered by [`release`](https://docs.github.com/en/actions/reference/events-that-trigger-workflows#release) events will by default check out a tag. For most use cases, you will need to set the `base` input to the branch name of the tagged commit.
|
||||
|
||||
```yml
|
||||
- uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
base: main
|
||||
```
|
||||
|
||||
### 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://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://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, 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.
|
||||
|
||||
```yml
|
||||
on: pull_request
|
||||
jobs:
|
||||
example:
|
||||
runs-on: ubuntu-latest
|
||||
# Check if the event is not triggered by a fork
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
- Create draft pull requests by setting the `draft: always-true` input, and configure your workflow to trigger `on: ready_for_review`. The workflow will run when users manually click the "Ready for review" button on the draft pull requests. If the pull request is updated by the action, the `always-true` mode ensures that the pull request will be converted back to a draft.
|
||||
|
||||
- Use a [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) created on an account that has write access to the repository that pull requests are being created in. This is the standard workaround and [recommended by GitHub](https://docs.github.com/en/actions/using-workflows/triggering-a-workflow#triggering-a-workflow-from-a-workflow). It's advisable to use a dedicated [machine account](https://docs.github.com/en/github/site-policy/github-terms-of-service#3-account-requirements) that has collaborator access to the repository, rather than creating a PAT on a personal user account. Also note that because the account that owns the PAT will be the creator of pull requests, that user account will be unable to perform actions such as request changes or approve the pull request.
|
||||
|
||||
- Use [SSH (deploy keys)](#push-using-ssh-deploy-keys) to push the pull request branch. This is arguably more secure than using a PAT because deploy keys can be set per repository. However, this method will only trigger `on: push` workflows.
|
||||
|
||||
- Use a [machine account that creates pull requests from its own fork](#push-pull-request-branches-to-a-fork). This is the most secure because the PAT created only grants access to the machine account's fork, not the main repository. This method will trigger `on: pull_request` workflows to run. Workflows triggered `on: push` will not run because the push event is in the fork.
|
||||
|
||||
- Use a [GitHub App to generate a token](#authenticating-with-github-app-generated-tokens) that can be used with this action. GitHub App generated tokens are more secure than using a Classic PAT because access permissions can be set with finer granularity and are scoped to only repositories where the App is installed. This method will trigger both `on: push` and `on: pull_request` workflows.
|
||||
|
||||
### 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, the `GITHUB_TOKEN` will not work and one of the other [token options](../README.md#token) must be used.
|
||||
|
||||
```yml
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
repository: owner/repo
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
```
|
||||
|
||||
### Push using SSH (deploy keys)
|
||||
|
||||
[Deploy keys](https://developer.github.com/v3/guides/managing-deploy-keys/#deploy-keys) can be set per repository and so are arguably more secure than using a Classic [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token).
|
||||
Allowing the action to push with a configured deploy key will trigger `on: push` workflows. This makes it an alternative to using a PAT to trigger checks for pull requests.
|
||||
|
||||
> [!NOTE]
|
||||
> You cannot use deploy keys alone to [create a pull request in a remote repository](#creating-pull-requests-in-a-remote-repository) because then using a PAT would become a requirement.
|
||||
> This method only makes sense if creating a pull request in the repository where the workflow is running.
|
||||
|
||||
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@v7
|
||||
```
|
||||
|
||||
### 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]
|
||||
> If you choose to not give the machine account `write` access to the parent repository, the following inputs cannot be used: `labels`, `assignees`, `reviewers`, `team-reviewers` and `milestone`.
|
||||
|
||||
1. Create a new GitHub user and login.
|
||||
2. Fork the repository that you will be creating pull requests in.
|
||||
3. Create a Classic [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) with `repo` and `workflow` scopes.
|
||||
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@v7
|
||||
with:
|
||||
token: ${{ secrets.MACHINE_USER_PAT }}
|
||||
push-to-fork: machine-user/fork-of-repository
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> You can also combine `push-to-fork` with [creating pull requests in a remote repository](#creating-pull-requests-in-a-remote-repository).
|
||||
|
||||
#### Pushing to a fork with fine-grained permissions
|
||||
|
||||
Using a fine-grained [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) or [GitHub App](#authenticating-with-github-app-generated-tokens) with `push-to-fork` can be achieved, but comes with some caveats.
|
||||
|
||||
When using `push-to-fork`, the action needs permissions for two different repositories.
|
||||
It needs `contents: write` for the fork to push the branch, and `pull-requests: write` for the parent repository to create the pull request.
|
||||
|
||||
There are two main scenarios:
|
||||
1. The parent and fork have different owners. In this case, it's not possible to create a token that is scoped to both repositories so different tokens must be used for each.
|
||||
2. The parent and fork both have the same owner (i.e. they exist in the same org). In this case, a single token can be scoped to both repositories, but the permissions granted cannot be different. So it would defeat the purpose of using `push-to-fork`, and you might as well just create the pull request directly on the parent repository.
|
||||
|
||||
For the first scenario, the solution is to scope the token for the fork, and use the `branch-token` input to push the branch.
|
||||
The `token` input will then default to the repository's `GITHUB_TOKEN`, which will be used to create the pull request.
|
||||
|
||||
> [!NOTE]
|
||||
> Solution limitations:
|
||||
> - Since `GITHUB_TOKEN` will be used to create the pull request, the workflow *must* be executing in the parent repository where the pull request should be created.
|
||||
> - `maintainer-can-modify` *must* be set to `false`, because the `GITHUB_TOKEN` will not have `write` access to the head branch in the fork.
|
||||
|
||||
The following is an example of pushing to a fork using GitHub App tokens.
|
||||
```yaml
|
||||
- uses: actions/create-github-app-token@v1
|
||||
id: generate-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
owner: owner
|
||||
repositories: fork-of-repo
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
branch-token: ${{ steps.generate-token.outputs.token }}
|
||||
push-to-fork: owner/fork-of-repo
|
||||
maintainer-can-modify: false
|
||||
```
|
||||
|
||||
### Authenticating with GitHub App generated tokens
|
||||
|
||||
A GitHub App can be created for the sole purpose of generating tokens for use with GitHub actions.
|
||||
GitHub App generated tokens can be configured with fine-grained permissions and are scoped to only repositories where the App is installed.
|
||||
|
||||
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 `Repository permissions: Workflows` select `Access: Read & write`.
|
||||
- **NOTE**: Only needed if pull requests could contain changes to Actions workflows.
|
||||
- Under `Organization permissions: Members` select `Access: Read-only`.
|
||||
- **NOTE**: Only needed if you would like add teams as reviewers to PRs.
|
||||
|
||||
2. Create a Private key from the App settings page and store it securely.
|
||||
|
||||
3. Install the App on repositories that the action will require access to in order to create pull requests.
|
||||
|
||||
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/create-github-app-token@v1
|
||||
id: generate-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
```
|
||||
|
||||
#### Creating pull requests in a remote repository using GitHub App generated tokens
|
||||
|
||||
For this case a token must be generated from the GitHub App installation of the remote repository.
|
||||
|
||||
In the following example, a pull request is being created in remote repo `owner/repo`.
|
||||
```yaml
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@v1
|
||||
id: generate-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
owner: owner
|
||||
repositories: repo
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }} # necessary if the repo is private
|
||||
repository: owner/repo
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
```
|
||||
|
||||
### Commit signing
|
||||
|
||||
[Commit signature verification](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification) is a feature where GitHub will mark signed commits as "verified" to give confidence that changes are from a trusted source.
|
||||
Some organizations require commit signing, and enforce it with branch protection rules.
|
||||
|
||||
The action supports two methods to sign commits, [commit signature verification for bots](#commit-signature-verification-for-bots), and [GPG commit signature verification](#gpg-commit-signature-verification).
|
||||
|
||||
#### Commit signature verification for bots
|
||||
|
||||
The action can sign commits as `github-actions[bot]` when using the repository's default `GITHUB_TOKEN`, or your own bot when using [GitHub App tokens](#authenticating-with-github-app-generated-tokens).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> - When setting `sign-commits: true` the action will ignore the `committer` and `author` inputs.
|
||||
> - If you attempt to use a [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) the action will create the pull request, but commits will *not* be signed. Commit signing is only supported with bot generated tokens.
|
||||
> - The GitHub API has a 40MiB limit when creating git blobs. An error will be raised if there are files in the pull request larger than this. If you hit this limit, use [GPG commit signature verification](#gpg-commit-signature-verification) instead.
|
||||
|
||||
In this example the `token` input is not supplied, so the action will use the repository's default `GITHUB_TOKEN`. This will sign commits as `github-actions[bot]`.
|
||||
```yaml
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
sign-commits: true
|
||||
```
|
||||
|
||||
In this example, the `token` input is generated using a GitHub App. This will sign commits as `<application-name>[bot]`.
|
||||
```yaml
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/create-github-app-token@v1
|
||||
id: generate-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
# Make changes to pull request here
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
sign-commits: true
|
||||
```
|
||||
|
||||
#### GPG commit signature verification
|
||||
|
||||
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.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 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@v7
|
||||
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@v7
|
||||
```
|
||||
|
||||
**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@v7
|
||||
```
|
614
docs/examples.md
Normal file
614
docs/examples.md
Normal file
|
@ -0,0 +1,614 @@
|
|||
# Examples
|
||||
|
||||
- [Use case: Create a pull request to update X on push](#use-case-create-a-pull-request-to-update-x-on-push)
|
||||
- [Update project authors](#update-project-authors)
|
||||
- [Keep a branch up-to-date with another](#keep-a-branch-up-to-date-with-another)
|
||||
- [Use case: Create a pull request to update X 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)
|
||||
- [Call the GitHub API from another GitHub Actions workflow](#call-the-github-api-from-another-github-actions-workflow)
|
||||
- [Use case: Create a pull request to modify/fix pull requests](#use-case-create-a-pull-request-to-modifyfix-pull-requests)
|
||||
- [autopep8](#autopep8)
|
||||
- [Misc workflow tips](#misc-workflow-tips)
|
||||
- [Filtering push events](#filtering-push-events)
|
||||
- [Dynamic configuration using variables](#dynamic-configuration-using-variables)
|
||||
- [Using a markdown template](#using-a-markdown-template)
|
||||
- [Debugging GitHub Actions](#debugging-github-actions)
|
||||
|
||||
|
||||
## Use case: Create a pull request to update X on push
|
||||
|
||||
This pattern will work well for updating any kind of static content based on pushed changes. Care should be taken when using this pattern in repositories with a high frequency of commits.
|
||||
|
||||
### Update project authors
|
||||
|
||||
Raises a pull request to update a file called `AUTHORS` with the git user names and email addresses of contributors.
|
||||
|
||||
```yml
|
||||
name: Update AUTHORS
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
updateAuthors:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- 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@v7
|
||||
with:
|
||||
commit-message: update authors
|
||||
title: Update AUTHORS
|
||||
body: Credit new contributors by updating AUTHORS
|
||||
branch: update-authors
|
||||
```
|
||||
|
||||
### Keep a branch up-to-date with another
|
||||
|
||||
This is a use case where a branch should be kept up to date with another by opening a pull request to update it. The pull request should then be updated with new changes until it is merged or closed.
|
||||
|
||||
In this example scenario, a branch called `production` should be updated via pull request to keep it in sync with `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@v7
|
||||
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@v7
|
||||
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-dep:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16.x'
|
||||
- name: Update dependencies
|
||||
run: |
|
||||
npx -p npm-check-updates ncu -u
|
||||
npm install
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
commit-message: Update dependencies
|
||||
title: Update dependencies
|
||||
body: |
|
||||
- Dependency updates
|
||||
|
||||
Auto-generated by [create-pull-request][1]
|
||||
|
||||
[1]: https://github.com/peter-evans/create-pull-request
|
||||
branch: update-dependencies
|
||||
```
|
||||
|
||||
The above workflow works best in combination with a build workflow triggered on `push` and `pull_request`.
|
||||
|
||||
```yml
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches: [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@v7
|
||||
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@v7
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
commit-message: Update dependencies
|
||||
title: Update dependencies
|
||||
body: |
|
||||
- Dependency updates
|
||||
|
||||
Auto-generated by [create-pull-request][1]
|
||||
|
||||
[1]: https://github.com/peter-evans/create-pull-request
|
||||
branch: update-dependencies
|
||||
```
|
||||
|
||||
### Update SwaggerUI for GitHub Pages
|
||||
|
||||
When using [GitHub Pages to host Swagger documentation](https://github.com/peter-evans/swagger-github-pages), this workflow updates the repository with the latest distribution of [SwaggerUI](https://github.com/swagger-api/swagger-ui).
|
||||
|
||||
You must create a file called `swagger-ui.version` at the root of your repository before running.
|
||||
```yml
|
||||
name: Update Swagger UI
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 10 * * *'
|
||||
jobs:
|
||||
updateSwagger:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Get Latest Swagger UI Release
|
||||
id: swagger-ui
|
||||
run: |
|
||||
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:
|
||||
RELEASE_TAG: ${{ steps.swagger-ui.outputs.release_tag }}
|
||||
SWAGGER_YAML: "swagger.yaml"
|
||||
run: |
|
||||
# Delete the dist directory and index.html
|
||||
rm -fr dist index.html
|
||||
# Download the release
|
||||
curl -sL -o $RELEASE_TAG https://api.github.com/repos/swagger-api/swagger-ui/tarball/$RELEASE_TAG
|
||||
# Extract the dist directory
|
||||
tar -xzf $RELEASE_TAG --strip-components=1 $(tar -tzf $RELEASE_TAG | head -1 | cut -f1 -d"/")/dist
|
||||
rm $RELEASE_TAG
|
||||
# Move index.html to the root
|
||||
mv dist/index.html .
|
||||
# Fix references in index.html
|
||||
sed -i "s|https://petstore.swagger.io/v2/swagger.json|$SWAGGER_YAML|g" index.html
|
||||
sed -i "s|href=\"./|href=\"dist/|g" index.html
|
||||
sed -i "s|src=\"./|src=\"dist/|g" index.html
|
||||
# Update current release
|
||||
echo ${{ steps.swagger-ui.outputs.release_tag }} > swagger-ui.version
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
commit-message: Update swagger-ui to ${{ steps.swagger-ui.outputs.release_tag }}
|
||||
title: Update SwaggerUI to ${{ steps.swagger-ui.outputs.release_tag }}
|
||||
body: |
|
||||
Updates [swagger-ui][1] to ${{ steps.swagger-ui.outputs.release_tag }}
|
||||
|
||||
Auto-generated by [create-pull-request][2]
|
||||
|
||||
[1]: https://github.com/swagger-api/swagger-ui
|
||||
[2]: https://github.com/peter-evans/create-pull-request
|
||||
labels: dependencies, automated pr
|
||||
branch: swagger-ui-updates
|
||||
```
|
||||
|
||||
### Keep a fork up-to-date with its upstream
|
||||
|
||||
This example is designed to be run in a separate repository from the fork repository itself.
|
||||
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@v7
|
||||
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.
|
||||
|
||||
```yml
|
||||
name: Download Website
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 10 * * *'
|
||||
jobs:
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Download website
|
||||
run: |
|
||||
wget \
|
||||
--recursive \
|
||||
--level=2 \
|
||||
--wait=1 \
|
||||
--no-clobber \
|
||||
--page-requisites \
|
||||
--html-extension \
|
||||
--convert-links \
|
||||
--domains quotes.toscrape.com \
|
||||
http://quotes.toscrape.com/
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
commit-message: update local website copy
|
||||
title: Automated Updates to Local Website Copy
|
||||
body: This is an auto-generated PR with website updates.
|
||||
branch: website-updates
|
||||
```
|
||||
|
||||
## Use case: Create a pull request to update X by calling the GitHub API
|
||||
|
||||
You can use the GitHub API to trigger a webhook event called [`repository_dispatch`](https://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.
|
||||
|
||||
Set the workflow to execute `on: repository_dispatch`.
|
||||
|
||||
```yml
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [create-pull-request]
|
||||
```
|
||||
|
||||
### Call the GitHub API from an external service
|
||||
|
||||
An `on: repository_dispatch` workflow can be triggered by a call to the GitHub API as follows.
|
||||
|
||||
- `[username]` is a GitHub username
|
||||
- `[token]` is a `repo` scoped [Personal Access Token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token)
|
||||
- `[repository]` is the name of the repository the workflow resides in.
|
||||
|
||||
```
|
||||
curl -XPOST -u "[username]:[token]" \
|
||||
-H "Accept: application/vnd.github.everest-preview+json" \
|
||||
-H "Content-Type: application/json" \
|
||||
https://api.github.com/repos/[username]/[repository]/dispatches \
|
||||
--data '{"event_type": "create-pull-request"}'
|
||||
```
|
||||
|
||||
### Call the GitHub API from another GitHub Actions workflow
|
||||
|
||||
An `on: repository_dispatch` workflow can be triggered from another workflow with [repository-dispatch](https://github.com/peter-evans/repository-dispatch) action.
|
||||
|
||||
```yml
|
||||
- name: Repository Dispatch
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.REPO_ACCESS_TOKEN }}
|
||||
repository: username/my-repo
|
||||
event-type: create-pull-request
|
||||
client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}"}'
|
||||
```
|
||||
|
||||
## Use case: Create a pull request to modify/fix pull requests
|
||||
|
||||
**Note**: While the following approach does work, my strong recommendation would be to use a slash command style "ChatOps" solution for operations on pull requests. See [slash-command-dispatch](https://github.com/peter-evans/slash-command-dispatch) for such a solution.
|
||||
|
||||
This is a pattern that lends itself to automated code linting and fixing. A pull request can be created to fix or modify something during an `on: pull_request` workflow. The pull request containing the fix will be raised with the original pull request as the base. This can be then be merged to update the original pull request and pass any required tests.
|
||||
|
||||
Note that due to [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
|
||||
|
||||
The following is an example workflow for a use case where [autopep8 action](https://github.com/peter-evans/autopep8) runs as both a check on pull requests and raises a further pull request to apply code fixes.
|
||||
|
||||
How it works:
|
||||
|
||||
1. When a pull request is raised the workflow executes as a check
|
||||
2. If autopep8 makes any fixes a pull request will be raised for those fixes to be merged into the current pull request branch. The workflow then deliberately causes the check to fail.
|
||||
3. When the pull request containing the fixes is merged the workflow runs again. This time autopep8 makes no changes and the check passes.
|
||||
4. The original pull request can now be merged.
|
||||
|
||||
```yml
|
||||
name: autopep8
|
||||
on: pull_request
|
||||
jobs:
|
||||
autopep8:
|
||||
# Check if the PR is not raised by this workflow and is not from a fork
|
||||
if: startsWith(github.head_ref, 'autopep8-patches') == false && github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
- name: autopep8
|
||||
id: autopep8
|
||||
uses: peter-evans/autopep8@v1
|
||||
with:
|
||||
args: --exit-code --recursive --in-place --aggressive --aggressive .
|
||||
- name: Set autopep8 branch name
|
||||
id: vars
|
||||
run: |
|
||||
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@v7
|
||||
with:
|
||||
commit-message: autopep8 action fixes
|
||||
title: Fixes by autopep8 action
|
||||
body: This is an auto-generated PR with fixes by autopep8.
|
||||
labels: autopep8, automated pr
|
||||
branch: ${{ steps.vars.outputs.branch-name }}
|
||||
- name: Fail if autopep8 made changes
|
||||
if: steps.autopep8.outputs.exit-code == 2
|
||||
run: exit 1
|
||||
```
|
||||
|
||||
## Misc workflow tips
|
||||
|
||||
### Filtering push events
|
||||
|
||||
For workflows using `on: push` you may want to ignore push events for tags and only execute for branches. Specifying `branches` causes only events on branches to trigger the workflow. The `'**'` wildcard will match any branch name.
|
||||
|
||||
```yml
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
```
|
||||
|
||||
If you have a workflow that contains jobs to handle push events on branches as well as tags, you can make sure that the job where you use `create-pull-request` action only executes when `github.ref` is a branch by using an `if` condition as follows.
|
||||
|
||||
```yml
|
||||
on: push
|
||||
jobs:
|
||||
createPullRequest:
|
||||
if: startsWith(github.ref, 'refs/heads/')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
...
|
||||
|
||||
someOtherJob:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- 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.
|
||||
Note that the step where output variables are defined must have an id.
|
||||
|
||||
```yml
|
||||
- name: Set output variables
|
||||
id: vars
|
||||
run: |
|
||||
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@v7
|
||||
with:
|
||||
title: ${{ steps.vars.outputs.pr_title }}
|
||||
body: ${{ steps.vars.outputs.pr_body }}
|
||||
```
|
||||
|
||||
### Using a markdown template
|
||||
|
||||
In this example, a markdown template file is added to the repository at `.github/pull-request-template.md` with the following content.
|
||||
```
|
||||
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: Render template
|
||||
id: template
|
||||
uses: chuhlomin/render-template@v1.4
|
||||
with:
|
||||
template: .github/pull-request-template.md
|
||||
vars: |
|
||||
foo: this
|
||||
bar: that
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
body: ${{ steps.template.outputs.result }}
|
||||
```
|
||||
|
||||
### Debugging GitHub Actions
|
||||
|
||||
#### Runner Diagnostic Logging
|
||||
|
||||
[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://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
|
||||
|
||||
```yml
|
||||
steps:
|
||||
- name: Dump GitHub context
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- name: Dump job context
|
||||
env:
|
||||
JOB_CONTEXT: ${{ toJson(job) }}
|
||||
run: echo "$JOB_CONTEXT"
|
||||
- name: Dump steps context
|
||||
env:
|
||||
STEPS_CONTEXT: ${{ toJson(steps) }}
|
||||
run: echo "$STEPS_CONTEXT"
|
||||
- name: Dump runner context
|
||||
env:
|
||||
RUNNER_CONTEXT: ${{ toJson(runner) }}
|
||||
run: echo "$RUNNER_CONTEXT"
|
||||
- name: Dump strategy context
|
||||
env:
|
||||
STRATEGY_CONTEXT: ${{ toJson(strategy) }}
|
||||
run: echo "$STRATEGY_CONTEXT"
|
||||
- name: Dump matrix context
|
||||
env:
|
||||
MATRIX_CONTEXT: ${{ toJson(matrix) }}
|
||||
run: echo "$MATRIX_CONTEXT"
|
||||
```
|
140
docs/updating.md
Normal file
140
docs/updating.md
Normal file
|
@ -0,0 +1,140 @@
|
|||
## Updating from `v6` to `v7`
|
||||
|
||||
### Behaviour changes
|
||||
|
||||
- Action input `git-token` has been renamed `branch-token`, to be more clear about its purpose. The `branch-token` is the token that the action will use to create and update the branch.
|
||||
- The action now handles requests that have been rate-limited by GitHub. Requests hitting a primary rate limit will retry twice, for a total of three attempts. Requests hitting a secondary rate limit will not be retried.
|
||||
- The `pull-request-operation` output now returns `none` when no operation was executed.
|
||||
- Removed deprecated output environment variable `PULL_REQUEST_NUMBER`. Please use the `pull-request-number` action output instead.
|
||||
|
||||
### What's new
|
||||
|
||||
- The action can now sign commits as `github-actions[bot]` when using `GITHUB_TOKEN`, or your own bot when using [GitHub App tokens](concepts-guidelines.md#authenticating-with-github-app-generated-tokens). See [commit signing](concepts-guidelines.md#commit-signature-verification-for-bots) for details.
|
||||
- Action input `draft` now accepts a new value `always-true`. This will set the pull request to draft status when the pull request is updated, as well as on creation.
|
||||
- A new action input `maintainer-can-modify` indicates whether [maintainers can modify](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork) the pull request. The default is `true`, which retains the existing behaviour of the action.
|
||||
- A new output `pull-request-commits-verified` returns `true` or `false`, indicating whether GitHub considers the signature of the branch's commits to be verified.
|
||||
|
||||
## Updating from `v5` to `v6`
|
||||
|
||||
### Behaviour changes
|
||||
|
||||
- The default values for `author` and `committer` have changed. See "What's new" below for details. If you are overriding the default values you will not be affected by this change.
|
||||
- On completion, the action now removes the temporary git remote configuration it adds when using `push-to-fork`. This should not affect you unless you were using the temporary configuration for some other purpose after the action completes.
|
||||
|
||||
### What's new
|
||||
|
||||
- Updated runtime to Node.js 20
|
||||
- The action now requires a minimum version of [v2.308.0](https://github.com/actions/runner/releases/tag/v2.308.0) for the Actions runner. Update self-hosted runners to v2.308.0 or later to ensure compatibility.
|
||||
- The default value for `author` has been changed to `${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>`. The change adds the `${{ github.actor_id }}+` prefix to the email address to align with GitHub's standard format for the author email address.
|
||||
- The default value for `committer` has been changed to `github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>`. This is to align with the default GitHub Actions bot user account.
|
||||
- Adds input `git-token`, the [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) that the action will use for git operations. This input defaults to the value of `token`. Use this input if you would like the action to use a different token for git operations than the one used for the GitHub API.
|
||||
- `push-to-fork` now supports pushing to sibling repositories in the same network.
|
||||
- Previously, when using `push-to-fork`, the action did not remove temporary git remote configuration it adds during execution. This has been fixed and the configuration is now removed when the action completes.
|
||||
- If the pull request body is truncated due to exceeding the maximum length, the action will now suffix the body with the message "...*[Pull request body truncated]*" to indicate that the body has been truncated.
|
||||
- The action now uses `--unshallow` only when necessary, rather than as a default argument of `git fetch`. This should improve performance, particularly for large git repositories with extensive commit history.
|
||||
- The action can now be executed on one GitHub server and create pull requests on a *different* GitHub server. Server products include GitHub hosted (github.com), GitHub Enterprise Server (GHES), and GitHub Enterprise Cloud (GHEC). For example, the action can be executed on GitHub hosted and create pull requests on a GHES or GHEC instance.
|
||||
|
||||
## Updating from `v4` to `v5`
|
||||
|
||||
### Behaviour changes
|
||||
|
||||
- The action will no longer leave the local repository checked out on the pull request `branch`. Instead, it will leave the repository checked out on the branch or commit that it was when the action started.
|
||||
- When using `add-paths`, uncommitted changes will no longer be destroyed. They will be stashed and restored at the end of the action run.
|
||||
|
||||
### What's new
|
||||
|
||||
- Adds input `body-path`, the path to a file containing the pull request body.
|
||||
- At the end of the action run the local repository is now checked out on the branch or commit that it was when the action started.
|
||||
- Any uncommitted tracked or untracked changes are now stashed and restored at the end of the action run. Currently, this can only occur when using the `add-paths` input, which allows for changes to not be committed. Previously, any uncommitted changes would be destroyed.
|
||||
- The proxy implementation has been revised but is not expected to have any change in behaviour. It continues to support the standard environment variables `http_proxy`, `https_proxy` and `no_proxy`.
|
||||
- Now sets the git `safe.directory` configuration for the local repository path. The configuration is removed when the action completes. Fixes issue https://github.com/peter-evans/create-pull-request/issues/1170.
|
||||
- Now determines the git directory path using the `git rev-parse --git-dir` command. This allows users with custom repository configurations to use the action.
|
||||
- Improved handling of the `team-reviewers` input and associated errors.
|
||||
|
||||
## Updating from `v3` to `v4`
|
||||
|
||||
### Behaviour changes
|
||||
|
||||
- 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.
|
||||
|
||||
- `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.
|
||||
|
||||
### 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 [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.
|
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
|
||||
}
|
8183
package-lock.json
generated
Normal file
8183
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
63
package.json
Normal file
63
package.json
Normal file
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"name": "create-pull-request",
|
||||
"version": "7.0.0",
|
||||
"private": true,
|
||||
"description": "Creates a pull request for changes to your repository in the actions workspace",
|
||||
"main": "lib/main.js",
|
||||
"scripts": {
|
||||
"build": "tsc && ncc build",
|
||||
"format": "prettier --write '**/*.ts'",
|
||||
"format-check": "prettier --check '**/*.ts'",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"test:unit": "jest unit",
|
||||
"test:int": "__test__/integration-tests.sh",
|
||||
"test": "npm run test:unit && npm run test:int"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/peter-evans/create-pull-request.git"
|
||||
},
|
||||
"keywords": [
|
||||
"actions",
|
||||
"pull",
|
||||
"request"
|
||||
],
|
||||
"author": "Peter Evans",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/peter-evans/create-pull-request/issues"
|
||||
},
|
||||
"homepage": "https://github.com/peter-evans/create-pull-request",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.11.1",
|
||||
"@actions/exec": "^1.1.1",
|
||||
"@octokit/core": "^6.1.2",
|
||||
"@octokit/plugin-paginate-rest": "^11.3.6",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^13.2.6",
|
||||
"@octokit/plugin-throttling": "^9.3.2",
|
||||
"node-fetch-native": "^1.6.4",
|
||||
"p-limit": "^6.1.0",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^18.19.67",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@vercel/ncc": "^0.38.3",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-import-resolver-typescript": "^3.7.0",
|
||||
"eslint-plugin-github": "^4.10.2",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jest": "^27.9.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"jest": "^29.7.0",
|
||||
"jest-circus": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"prettier": "^3.4.2",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typescript": "^5.7.2",
|
||||
"undici": "^6.21.0"
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 260 KiB |
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"extends": [
|
||||
"config:base"
|
||||
]
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
GitPython==3.0.0
|
||||
PyGithub==1.43.8
|
372
src/create-or-update-branch.ts
Normal file
372
src/create-or-update-branch.ts
Normal file
|
@ -0,0 +1,372 @@
|
|||
import * as core from '@actions/core'
|
||||
import {GitCommandManager, Commit} from './git-command-manager'
|
||||
import {v4 as uuidv4} from 'uuid'
|
||||
import * as utils from './utils'
|
||||
|
||||
const CHERRYPICK_EMPTY =
|
||||
'The previous cherry-pick is now empty, possibly due to conflict resolution.'
|
||||
const NOTHING_TO_COMMIT = 'nothing to commit, working tree clean'
|
||||
|
||||
const FETCH_DEPTH_MARGIN = 10
|
||||
|
||||
export enum WorkingBaseType {
|
||||
Branch = 'branch',
|
||||
Commit = 'commit'
|
||||
}
|
||||
|
||||
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)
|
||||
for (const unparsedChange of commit.unparsedChanges) {
|
||||
core.warning(`Skipping unexpected diff entry: ${unparsedChange}`)
|
||||
}
|
||||
}
|
||||
return commits
|
||||
}
|
||||
|
||||
// Return the number of commits that branch2 is ahead of branch1
|
||||
async function commitsAhead(
|
||||
git: GitCommandManager,
|
||||
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))
|
||||
)
|
||||
}
|
||||
|
||||
// Return true if the specified number of commits on branch1 and branch2 have a diff
|
||||
async function commitsHaveDiff(
|
||||
git: GitCommandManager,
|
||||
branch1: string,
|
||||
branch2: string,
|
||||
depth: number
|
||||
): Promise<boolean> {
|
||||
// Some action use cases lead to the depth being a very large number and the diff fails.
|
||||
// I've made this check optional for now because it was a fix for an edge case that is
|
||||
// very rare, anyway.
|
||||
try {
|
||||
const diff1 = (
|
||||
await git.exec(['diff', '--stat', `${branch1}..${branch1}~${depth}`])
|
||||
).stdout.trim()
|
||||
const diff2 = (
|
||||
await git.exec(['diff', '--stat', `${branch2}..${branch2}~${depth}`])
|
||||
).stdout.trim()
|
||||
return diff1 !== diff2
|
||||
} catch (error) {
|
||||
core.info('Failed optional check of commits diff; Skipping.')
|
||||
core.debug(utils.getErrorMessage(error))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function splitLines(multilineString: string): string[] {
|
||||
return multilineString
|
||||
.split('\n')
|
||||
.map(s => s.trim())
|
||||
.filter(x => x !== '')
|
||||
}
|
||||
|
||||
interface CreateOrUpdateBranchResult {
|
||||
action: string
|
||||
base: string
|
||||
hasDiffWithBase: boolean
|
||||
baseCommit: Commit
|
||||
headSha: string
|
||||
branchCommits: Commit[]
|
||||
}
|
||||
|
||||
export async function createOrUpdateBranch(
|
||||
git: GitCommandManager,
|
||||
commitMessage: string,
|
||||
base: string,
|
||||
branch: string,
|
||||
branchRemoteName: string,
|
||||
signoff: boolean,
|
||||
addPaths: string[]
|
||||
): 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'
|
||||
|
||||
// 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
|
||||
|
||||
let action = 'none'
|
||||
let hasDiffWithBase = false
|
||||
|
||||
// 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
|
||||
hasDiffWithBase = await isAhead(git, base, branch)
|
||||
if (hasDiffWithBase) {
|
||||
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 number of commits ahead of the base branch differs between the branch and
|
||||
// temp branch. This catches a case where the base branch has been force pushed to
|
||||
// a new commit.
|
||||
// - If the recreated temp branch is not ahead of the base. This means there will be
|
||||
// no pull request diff after the branch is reset. This will reset any undeleted
|
||||
// branches after merging. In particular, it catches a case where the branch was
|
||||
// squash merged but not deleted. We need to reset to make sure it doesn't appear
|
||||
// to have a diff with the base due to different commits for the same changes.
|
||||
// - If the diff of the commits ahead of the base branch differs between the branch and
|
||||
// temp branch. This catches a case where changes have been partially merged to the
|
||||
// base. The overall diff is the same, but the branch needs to be rebased to show
|
||||
// the correct diff.
|
||||
//
|
||||
// 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
|
||||
(await commitsHaveDiff(git, branch, tempBranch, tempBranchCommitsAhead))
|
||||
) {
|
||||
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))) {
|
||||
action = 'updated'
|
||||
core.info(`Updated branch '${branch}'`)
|
||||
} else {
|
||||
action = 'not-updated'
|
||||
core.info(
|
||||
`Branch '${branch}' is even with its remote and will not be updated`
|
||||
)
|
||||
}
|
||||
|
||||
// Check if the pull request branch is ahead of the base
|
||||
hasDiffWithBase = await isAhead(git, base, branch)
|
||||
}
|
||||
|
||||
// Get the base and head SHAs
|
||||
const baseSha = await git.revParse(base)
|
||||
const baseCommit = await git.getCommit(baseSha)
|
||||
const headSha = await git.revParse(branch)
|
||||
|
||||
let branchCommits: Commit[] = []
|
||||
if (hasDiffWithBase) {
|
||||
// Build the branch commits
|
||||
branchCommits = await buildBranchCommits(git, base, branch)
|
||||
}
|
||||
|
||||
// Delete the temporary branch
|
||||
await git.exec(['branch', '--delete', '--force', tempBranch])
|
||||
|
||||
// Checkout the working base to leave the local repository as it was found
|
||||
await git.checkout(workingBase)
|
||||
|
||||
// Restore any stashed changes
|
||||
if (stashed) {
|
||||
await git.stashPop()
|
||||
}
|
||||
|
||||
return {
|
||||
action: action,
|
||||
base: base,
|
||||
hasDiffWithBase: hasDiffWithBase,
|
||||
baseCommit: baseCommit,
|
||||
headSha: headSha,
|
||||
branchCommits: branchCommits
|
||||
}
|
||||
}
|
325
src/create-pull-request.ts
Normal file
325
src/create-pull-request.ts
Normal file
|
@ -0,0 +1,325 @@
|
|||
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
|
||||
branchToken: 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: {
|
||||
value: boolean
|
||||
always: 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 apiUrl = await GitHubHelper.determineApiUrl(baseRemote.hostname)
|
||||
core.info(`Using API base URL: ${apiUrl}`)
|
||||
const ghBranch = new GitHubHelper(apiUrl, inputs.branchToken)
|
||||
const ghPull = new GitHubHelper(apiUrl, inputs.token)
|
||||
// Determine the 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.branchToken)
|
||||
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')
|
||||
|
||||
// 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
|
||||
)
|
||||
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.baseCommit,
|
||||
repoPath,
|
||||
branchRepository,
|
||||
inputs.branch
|
||||
)
|
||||
outputs.set('pull-request-head-sha', pushSignedCommitsResult.sha)
|
||||
outputs.set(
|
||||
'pull-request-commits-verified',
|
||||
pushSignedCommitsResult.verified.toString()
|
||||
)
|
||||
await git.checkout('-')
|
||||
if (stashed) {
|
||||
await git.stashPop()
|
||||
}
|
||||
} else {
|
||||
await git.push([
|
||||
'--force-with-lease',
|
||||
branchRemoteName,
|
||||
`${inputs.branch}:refs/heads/${inputs.branch}`
|
||||
])
|
||||
}
|
||||
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')
|
||||
// The pull request was updated AND the branch was updated.
|
||||
// Convert back to draft if 'draft: always-true' is set.
|
||||
if (inputs.draft.always && pull.draft !== undefined && !pull.draft) {
|
||||
await ghPull.convertToDraft(pull.node_id)
|
||||
}
|
||||
}
|
||||
core.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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
core.startGroup('Setting outputs')
|
||||
// If the head commit is signed, get its verification status if we don't already know it.
|
||||
// This can happen if the branch wasn't updated (action = 'not-updated'), or GPG commit signing is in use.
|
||||
if (
|
||||
!outputs.has('pull-request-commits-verified') &&
|
||||
result.branchCommits.length > 0 &&
|
||||
result.branchCommits[result.branchCommits.length - 1].signed
|
||||
) {
|
||||
// Using the local head commit SHA because in this case commits have not been pushed via the API.
|
||||
core.info(`Checking verification status of head commit ${result.headSha}`)
|
||||
try {
|
||||
const headCommit = await ghBranch.getCommit(
|
||||
result.headSha,
|
||||
branchRepository
|
||||
)
|
||||
outputs.set(
|
||||
'pull-request-commits-verified',
|
||||
headCommit.verified.toString()
|
||||
)
|
||||
} catch (error) {
|
||||
core.warning('Failed to check verification status of head commit.')
|
||||
core.debug(utils.getErrorMessage(error))
|
||||
}
|
||||
}
|
||||
if (!outputs.has('pull-request-commits-verified')) {
|
||||
outputs.set('pull-request-commits-verified', 'false')
|
||||
}
|
||||
|
||||
// Set outputs
|
||||
for (const [key, value] of outputs) {
|
||||
core.info(`${key} = ${value}`)
|
||||
core.setOutput(key, value)
|
||||
}
|
||||
core.endGroup()
|
||||
} catch (error) {
|
||||
core.setFailed(utils.getErrorMessage(error))
|
||||
} finally {
|
||||
core.startGroup('Restore git configuration')
|
||||
if (inputs.pushToFork) {
|
||||
await git.exec(['remote', 'rm', 'fork'])
|
||||
}
|
||||
await gitConfigHelper.close()
|
||||
core.endGroup()
|
||||
}
|
||||
}
|
386
src/git-command-manager.ts
Normal file
386
src/git-command-manager.ts
Normal file
|
@ -0,0 +1,386 @@
|
|||
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[]
|
||||
signed: boolean
|
||||
subject: string
|
||||
body: string
|
||||
changes: {
|
||||
mode: string
|
||||
dstSha: string
|
||||
status: 'A' | 'M' | 'D'
|
||||
path: string
|
||||
}[]
|
||||
unparsedChanges: string[]
|
||||
}
|
||||
|
||||
export class GitCommandManager {
|
||||
private gitPath: string
|
||||
private workingDirectory: string
|
||||
// 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',
|
||||
'--no-renames',
|
||||
'--no-abbrev',
|
||||
`--format=%H%n%T%n%P%n%G?%n%s%n%b%n${endOfBody}`,
|
||||
ref
|
||||
])
|
||||
const lines = output.stdout.split('\n')
|
||||
const endOfBodyIndex = lines.lastIndexOf(endOfBody)
|
||||
const detailLines = lines.slice(0, endOfBodyIndex)
|
||||
|
||||
const unparsedChanges: string[] = []
|
||||
return <Commit>{
|
||||
sha: detailLines[0],
|
||||
tree: detailLines[1],
|
||||
parents: detailLines[2].split(' '),
|
||||
signed: detailLines[3] !== 'N',
|
||||
subject: detailLines[4],
|
||||
body: detailLines.slice(5, endOfBodyIndex).join('\n'),
|
||||
changes: lines.slice(endOfBodyIndex + 2, -1).map(line => {
|
||||
const change = line.match(
|
||||
/^:(\d{6}) (\d{6}) \w{40} (\w{40}) ([AMD])\s+(.*)$/
|
||||
)
|
||||
if (change) {
|
||||
return {
|
||||
mode: change[4] === 'D' ? change[1] : change[2],
|
||||
dstSha: change[3],
|
||||
status: change[4],
|
||||
path: change[5]
|
||||
}
|
||||
} else {
|
||||
unparsedChanges.push(line)
|
||||
}
|
||||
}),
|
||||
unparsedChanges: unparsedChanges
|
||||
}
|
||||
}
|
||||
|
||||
async getConfigValue(configKey: string, configValue = '.'): Promise<string> {
|
||||
const output = await this.exec([
|
||||
'config',
|
||||
'--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)
|
||||
}
|
||||
}
|
439
src/github-helper.ts
Normal file
439
src/github-helper.ts
Normal file
|
@ -0,0 +1,439 @@
|
|||
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
|
||||
node_id: string
|
||||
draft?: boolean
|
||||
created: boolean
|
||||
}
|
||||
|
||||
interface CommitResponse {
|
||||
sha: string
|
||||
tree: string
|
||||
verified: boolean
|
||||
}
|
||||
|
||||
type TreeObject = {
|
||||
path: string
|
||||
mode: '100644' | '100755' | '040000' | '160000' | '120000'
|
||||
sha: string | null
|
||||
type: 'blob' | 'commit'
|
||||
}
|
||||
|
||||
export class GitHubHelper {
|
||||
private octokit: InstanceType<typeof Octokit>
|
||||
|
||||
constructor(apiUrl: string, token: string) {
|
||||
const options: OctokitOptions = {}
|
||||
if (token) {
|
||||
options.auth = `${token}`
|
||||
}
|
||||
options.baseUrl = apiUrl
|
||||
options.throttle = throttleOptions
|
||||
this.octokit = new Octokit(options)
|
||||
}
|
||||
|
||||
static async determineApiUrl(hostname: string): Promise<string> {
|
||||
if (hostname === 'github.com') {
|
||||
return 'https://api.github.com'
|
||||
}
|
||||
|
||||
const baseUrl = `https://${hostname}`
|
||||
const possiblePaths = [
|
||||
'/api/v4/version',
|
||||
'/api/forgejo/v1/version',
|
||||
'/api/v1/version'
|
||||
]
|
||||
|
||||
for (const path of possiblePaths) {
|
||||
try {
|
||||
const url = `${baseUrl}${path}`
|
||||
const response = await fetch(url, {method: 'GET', redirect: 'manual'}) // GitLab redirects
|
||||
// invalid API paths
|
||||
// to login prompt
|
||||
// which returns 200
|
||||
|
||||
const contentType = response.headers.get('Content-Type') || ''
|
||||
if (
|
||||
(response.ok || [401, 403].includes(response.status)) && // We might get 401, 403
|
||||
// as we're unauthorised
|
||||
contentType.includes('application/json')
|
||||
) {
|
||||
return path.includes('/version') ? url.replace('/version', '') : url
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors and try the next path
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unable to determine API base URL for hostname: ${hostname}`
|
||||
)
|
||||
}
|
||||
|
||||
private parseRepository(repository: string): Repository {
|
||||
const [owner, repo] = repository.split('/')
|
||||
return {
|
||||
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.value,
|
||||
maintainer_can_modify: inputs.maintainerCanModify
|
||||
})
|
||||
core.info(
|
||||
`Created pull request #${pull.number} (${headBranch} => ${inputs.base})`
|
||||
)
|
||||
return {
|
||||
number: pull.number,
|
||||
html_url: pull.html_url,
|
||||
node_id: pull.node_id,
|
||||
draft: pull.draft,
|
||||
created: true
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMessage = utils.getErrorMessage(e)
|
||||
if (errorMessage.includes(ERROR_PR_ALREADY_EXISTS)) {
|
||||
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,
|
||||
node_id: pull.node_id,
|
||||
draft: pull.draft,
|
||||
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[],
|
||||
baseCommit: Commit,
|
||||
repoPath: string,
|
||||
branchRepository: string,
|
||||
branch: string
|
||||
): Promise<CommitResponse> {
|
||||
let headCommit: CommitResponse = {
|
||||
sha: baseCommit.sha,
|
||||
tree: baseCommit.tree,
|
||||
verified: false
|
||||
}
|
||||
for (const commit of branchCommits) {
|
||||
headCommit = await this.createCommit(
|
||||
commit,
|
||||
headCommit,
|
||||
repoPath,
|
||||
branchRepository
|
||||
)
|
||||
}
|
||||
await this.createOrUpdateRef(branchRepository, branch, headCommit.sha)
|
||||
return headCommit
|
||||
}
|
||||
|
||||
private async createCommit(
|
||||
commit: Commit,
|
||||
parentCommit: CommitResponse,
|
||||
repoPath: string,
|
||||
branchRepository: string
|
||||
): Promise<CommitResponse> {
|
||||
const repository = this.parseRepository(branchRepository)
|
||||
// In the case of an empty commit, the tree references the parent's tree
|
||||
let treeSha = parentCommit.tree
|
||||
if (commit.changes.length > 0) {
|
||||
core.info(`Creating tree objects for local commit ${commit.sha}`)
|
||||
const treeObjects = await Promise.all(
|
||||
commit.changes.map(async ({path, mode, status, dstSha}) => {
|
||||
if (mode === '160000') {
|
||||
// submodule
|
||||
core.info(`Creating tree object for submodule commit at '${path}'`)
|
||||
return <TreeObject>{
|
||||
path,
|
||||
mode,
|
||||
sha: dstSha,
|
||||
type: 'commit'
|
||||
}
|
||||
} else {
|
||||
let sha: string | null = null
|
||||
if (status === 'A' || status === 'M') {
|
||||
try {
|
||||
const {data: blob} = await blobCreationLimit(() =>
|
||||
this.octokit.rest.git.createBlob({
|
||||
...repository,
|
||||
content: utils.readFileBase64([repoPath, path]),
|
||||
encoding: 'base64'
|
||||
})
|
||||
)
|
||||
sha = blob.sha
|
||||
} catch (error) {
|
||||
core.error(
|
||||
`Error creating blob for file '${path}': ${utils.getErrorMessage(error)}`
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
core.info(
|
||||
`Creating tree object for blob at '${path}' with status '${status}'`
|
||||
)
|
||||
return <TreeObject>{
|
||||
path,
|
||||
mode,
|
||||
sha,
|
||||
type: 'blob'
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const chunkSize = 100
|
||||
const chunkedTreeObjects: TreeObject[][] = Array.from(
|
||||
{length: Math.ceil(treeObjects.length / chunkSize)},
|
||||
(_, i) => treeObjects.slice(i * chunkSize, i * chunkSize + chunkSize)
|
||||
)
|
||||
|
||||
core.info(`Creating tree for local commit ${commit.sha}`)
|
||||
for (let i = 0; i < chunkedTreeObjects.length; i++) {
|
||||
const {data: tree} = await this.octokit.rest.git.createTree({
|
||||
...repository,
|
||||
base_tree: treeSha,
|
||||
tree: chunkedTreeObjects[i]
|
||||
})
|
||||
treeSha = tree.sha
|
||||
if (chunkedTreeObjects.length > 1) {
|
||||
core.info(
|
||||
`Created tree ${treeSha} of multipart tree (${i + 1} of ${chunkedTreeObjects.length})`
|
||||
)
|
||||
}
|
||||
}
|
||||
core.info(`Created tree ${treeSha} for local commit ${commit.sha}`)
|
||||
}
|
||||
|
||||
const {data: remoteCommit} = await this.octokit.rest.git.createCommit({
|
||||
...repository,
|
||||
parents: [parentCommit.sha],
|
||||
tree: treeSha,
|
||||
message: `${commit.subject}\n\n${commit.body}`
|
||||
})
|
||||
core.info(
|
||||
`Created commit ${remoteCommit.sha} for local commit ${commit.sha}`
|
||||
)
|
||||
core.info(
|
||||
`Commit verified: ${remoteCommit.verification.verified}; reason: ${remoteCommit.verification.reason}`
|
||||
)
|
||||
return {
|
||||
sha: remoteCommit.sha,
|
||||
tree: remoteCommit.tree.sha,
|
||||
verified: remoteCommit.verification.verified
|
||||
}
|
||||
}
|
||||
|
||||
async getCommit(
|
||||
sha: string,
|
||||
branchRepository: string
|
||||
): Promise<CommitResponse> {
|
||||
const repository = this.parseRepository(branchRepository)
|
||||
const {data: remoteCommit} = await this.octokit.rest.git.getCommit({
|
||||
...repository,
|
||||
commit_sha: sha
|
||||
})
|
||||
return {
|
||||
sha: remoteCommit.sha,
|
||||
tree: remoteCommit.tree.sha,
|
||||
verified: remoteCommit.verification.verified
|
||||
}
|
||||
}
|
||||
|
||||
private async createOrUpdateRef(
|
||||
branchRepository: string,
|
||||
branch: string,
|
||||
newHead: string
|
||||
) {
|
||||
const repository = this.parseRepository(branchRepository)
|
||||
const branchExists = await this.octokit.rest.repos
|
||||
.getBranch({
|
||||
...repository,
|
||||
branch: branch
|
||||
})
|
||||
.then(
|
||||
() => true,
|
||||
() => false
|
||||
)
|
||||
|
||||
if (branchExists) {
|
||||
core.info(`Branch ${branch} exists; Updating ref`)
|
||||
await this.octokit.rest.git.updateRef({
|
||||
...repository,
|
||||
sha: newHead,
|
||||
ref: `heads/${branch}`,
|
||||
force: true
|
||||
})
|
||||
} else {
|
||||
core.info(`Branch ${branch} does not exist; Creating ref`)
|
||||
await this.octokit.rest.git.createRef({
|
||||
...repository,
|
||||
sha: newHead,
|
||||
ref: `refs/heads/${branch}`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async convertToDraft(id: string): Promise<void> {
|
||||
core.info(`Converting pull request to draft`)
|
||||
await this.octokit.graphql({
|
||||
query: `mutation($pullRequestId: ID!) {
|
||||
convertPullRequestToDraft(input: {pullRequestId: $pullRequestId}) {
|
||||
pullRequest {
|
||||
isDraft
|
||||
}
|
||||
}
|
||||
}`,
|
||||
pullRequestId: id
|
||||
})
|
||||
}
|
||||
}
|
71
src/main.ts
Normal file
71
src/main.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
import * as core from '@actions/core'
|
||||
import {Inputs, createPullRequest} from './create-pull-request'
|
||||
import {inspect} from 'util'
|
||||
import * as utils from './utils'
|
||||
|
||||
function getDraftInput(): {value: boolean; always: boolean} {
|
||||
if (core.getInput('draft') === 'always-true') {
|
||||
return {value: true, always: true}
|
||||
} else {
|
||||
return {value: core.getBooleanInput('draft'), always: false}
|
||||
}
|
||||
}
|
||||
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
const inputs: Inputs = {
|
||||
token: core.getInput('token'),
|
||||
branchToken: core.getInput('branch-token'),
|
||||
path: core.getInput('path'),
|
||||
addPaths: utils.getInputAsArray('add-paths'),
|
||||
commitMessage: core.getInput('commit-message'),
|
||||
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: getDraftInput(),
|
||||
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.branchToken) {
|
||||
inputs.branchToken = inputs.token
|
||||
}
|
||||
if (inputs.bodyPath) {
|
||||
if (!utils.fileExistsSync(inputs.bodyPath)) {
|
||||
throw new Error(`File '${inputs.bodyPath}' does not exist.`)
|
||||
}
|
||||
// Update the body input with the contents of the file
|
||||
inputs.body = utils.readFile(inputs.bodyPath)
|
||||
}
|
||||
// 65536 characters is the maximum allowed for the pull request body.
|
||||
if (inputs.body.length > 65536) {
|
||||
core.warning(
|
||||
`Pull request body is too long. Truncating to 65536 characters.`
|
||||
)
|
||||
inputs.body = inputs.body.substring(0, 65536)
|
||||
}
|
||||
|
||||
await createPullRequest(inputs)
|
||||
} catch (error) {
|
||||
core.setFailed(utils.getErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
run()
|
40
src/octokit-client.ts
Normal file
40
src/octokit-client.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
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 {fetch} from 'node-fetch-native/proxy'
|
||||
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.`)
|
||||
}
|
||||
}
|
||||
|
||||
// Octokit plugin to support the standard environment variables http_proxy, https_proxy and no_proxy
|
||||
function autoProxyAgent(octokit: OctokitCore) {
|
||||
octokit.hook.before('request', options => {
|
||||
options.request.fetch = fetch
|
||||
})
|
||||
}
|
147
src/utils.ts
Normal file
147
src/utils.ts
Normal file
|
@ -0,0 +1,147 @@
|
|||
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 {
|
||||
const resolvedPath = path.resolve(...pathParts)
|
||||
if (fs.lstatSync(resolvedPath).isSymbolicLink()) {
|
||||
return fs
|
||||
.readlinkSync(resolvedPath, {encoding: 'buffer'})
|
||||
.toString('base64')
|
||||
}
|
||||
return fs.readFileSync(resolvedPath).toString('base64')
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
function hasErrorCode(error: any): error is {code: string} {
|
||||
return typeof (error && error.code) === 'string'
|
||||
}
|
||||
|
||||
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"]
|
||||
}
|
Loading…
Reference in a new issue