forked from mirrors/probot
Compare commits
633 Commits
ts-extract
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
e952c65fda | |
|
|
d92f72bc35 | |
|
|
ef7b9df5e4 | |
|
|
93007b63c4 | |
|
|
dbeda67e4a | |
|
|
de39bb672e | |
|
|
6c55f0a96f | |
|
|
5ef04a10a9 | |
|
|
55f56032e0 | |
|
|
26665e66fe | |
|
|
1f21a63c7d | |
|
|
3f2032077b | |
|
|
8e9dd47e69 | |
|
|
e64f48519f | |
|
|
b1d3ac3c2e | |
|
|
3c655e8292 | |
|
|
052a8c63a1 | |
|
|
52de532918 | |
|
|
2549886e3c | |
|
|
d1952641f2 | |
|
|
c62652e368 | |
|
|
7583bdf293 | |
|
|
b93e73aab6 | |
|
|
8ade04032f | |
|
|
2d10214781 | |
|
|
eb39500dd7 | |
|
|
eaa2d7cc25 | |
|
|
2c12d3dd93 | |
|
|
737835fb8a | |
|
|
1eb03896e9 | |
|
|
f7e4588b16 | |
|
|
f97fc64dac | |
|
|
5d95b7386e | |
|
|
380d24dfc5 | |
|
|
c065c15483 | |
|
|
39ced7e3ba | |
|
|
33776dd2ad | |
|
|
8a403e8e50 | |
|
|
d7ae857632 | |
|
|
6ed90dac20 | |
|
|
6bca1bdb79 | |
|
|
cd0c4a7bfa | |
|
|
948a1b7147 | |
|
|
02d81f886c | |
|
|
fc6886d3b3 | |
|
|
76f3376070 | |
|
|
c1f0767427 | |
|
|
7b09369f4b | |
|
|
6c5840d2df | |
|
|
6e3070f978 | |
|
|
229a01c7bf | |
|
|
776f6a782f | |
|
|
85bd6e5d55 | |
|
|
99c7b129cd | |
|
|
ff524af791 | |
|
|
a03417e6a5 | |
|
|
f89d27175f | |
|
|
fac5b43584 | |
|
|
4d6ee86700 | |
|
|
75a82ad2c5 | |
|
|
0a863acb74 | |
|
|
0fcf5ded94 | |
|
|
d668f24c00 | |
|
|
5be04fd99e | |
|
|
f81a134dd2 | |
|
|
98f4b302c9 | |
|
|
eff5553253 | |
|
|
78cb0197a6 | |
|
|
3c6dca6c1d | |
|
|
68c9b91b89 | |
|
|
be45120fab | |
|
|
d5e22e3979 | |
|
|
05297fbd50 | |
|
|
529197ed60 | |
|
|
3d30acc854 | |
|
|
4b1ccf860b | |
|
|
646b6a9276 | |
|
|
7fd06d6975 | |
|
|
559f4374d4 | |
|
|
3a7bbb2e1a | |
|
|
12d9b05d4e | |
|
|
1a73ff40a6 | |
|
|
420f88612d | |
|
|
07fe1a709b | |
|
|
ce9c3d13fb | |
|
|
9cf9be235f | |
|
|
c34f64d2f4 | |
|
|
5d232e2d86 | |
|
|
dd9f5ae98e | |
|
|
a85c7a6307 | |
|
|
ec92db2c0c | |
|
|
992b480073 | |
|
|
c83edcbbfc | |
|
|
073f08793d | |
|
|
bb068d9ae2 | |
|
|
25a4eca970 | |
|
|
02a57d1bd2 | |
|
|
638a3b2255 | |
|
|
718025d161 | |
|
|
2641ab5b60 | |
|
|
33d5fac956 | |
|
|
0a170e8e8b | |
|
|
279af98d77 | |
|
|
6b7e684235 | |
|
|
d6b0d4ebbd | |
|
|
f02c5498fd | |
|
|
624b934155 | |
|
|
803fa2f00d | |
|
|
41f2bb1bbb | |
|
|
bdbb396581 | |
|
|
ef70c58148 | |
|
|
f58a88e1b2 | |
|
|
758fb5fdb5 | |
|
|
7a43e9e5d4 | |
|
|
4b3ae0eee5 | |
|
|
be1711ee6d | |
|
|
9d12a1eb23 | |
|
|
23e2af28c7 | |
|
|
453ddd29bb | |
|
|
01c2006700 | |
|
|
d8a3186154 | |
|
|
acd47a6ea0 | |
|
|
fdd14486b5 | |
|
|
4d70d69c92 | |
|
|
6dec1fa5f0 | |
|
|
474b7735bd | |
|
|
8cc15900ca | |
|
|
72b0531d60 | |
|
|
e11b91e838 | |
|
|
9342caf1ab | |
|
|
ce96884930 | |
|
|
9eb3e973a8 | |
|
|
0e37427938 | |
|
|
fa4a95c69b | |
|
|
d70c4af748 | |
|
|
c956d25c98 | |
|
|
5b79ce2f89 | |
|
|
57ad735d1c | |
|
|
f053bf84ec | |
|
|
b2fe1f595f | |
|
|
3f57c70185 | |
|
|
9a24f9dcb5 | |
|
|
2c97c99ea8 | |
|
|
c0710a20b2 | |
|
|
c612fc903d | |
|
|
cd14dd472d | |
|
|
5f1831b99a | |
|
|
a5779ff835 | |
|
|
2275698633 | |
|
|
01bb6788f3 | |
|
|
dc7dc324c2 | |
|
|
59f07c9bfc | |
|
|
335684509e | |
|
|
15e4b00702 | |
|
|
42b043e588 | |
|
|
41820c3358 | |
|
|
a028c3d793 | |
|
|
3aa0d5fd53 | |
|
|
f6a7970292 | |
|
|
60b702bd05 | |
|
|
8c01e90ecc | |
|
|
d315f0c2b6 | |
|
|
d1a66fc59e | |
|
|
0343d5119e | |
|
|
bdbe94ee42 | |
|
|
a771d25303 | |
|
|
3d4b363261 | |
|
|
7a8f268561 | |
|
|
a5c78b18e4 | |
|
|
a94fdca9fd | |
|
|
2ff5d21e2a | |
|
|
e0d34c7f80 | |
|
|
3d90806c95 | |
|
|
5c2d3c39d5 | |
|
|
9d767e191e | |
|
|
8a3599db82 | |
|
|
966ea5aabe | |
|
|
6af93d0d3d | |
|
|
d673f3b8fd | |
|
|
5e8dcc6798 | |
|
|
f41af1827e | |
|
|
e2c35c20d6 | |
|
|
d203219707 | |
|
|
0ec5f23098 | |
|
|
49153b8dd4 | |
|
|
7abbef776c | |
|
|
cea4318106 | |
|
|
e67b78511f | |
|
|
1dbd999982 | |
|
|
514c7645e8 | |
|
|
f50f68f485 | |
|
|
7c46218da8 | |
|
|
8ee18cdd4a | |
|
|
f35b58a54c | |
|
|
3f0974b4d2 | |
|
|
74f1f16bcd | |
|
|
f829f9505a | |
|
|
d96c65f09f | |
|
|
dfc59fc20c | |
|
|
39675d8c7e | |
|
|
47d9f3a2e1 | |
|
|
0527b98039 | |
|
|
9351df4b5e | |
|
|
2fcc0b5c5c | |
|
|
0e52e05319 | |
|
|
1812cfeb2f | |
|
|
de3adc1169 | |
|
|
4bfae5a54f | |
|
|
ab6fcb1dc4 | |
|
|
133b332b0c | |
|
|
3a02fa3d19 | |
|
|
58b2e1ddfd | |
|
|
2b30518fbd | |
|
|
8ba3a8e8a4 | |
|
|
506a5ed08b | |
|
|
a5b36b3344 | |
|
|
41ee70cbf1 | |
|
|
c7ead98f78 | |
|
|
8ddac1f6bb | |
|
|
05abeef921 | |
|
|
630d78e347 | |
|
|
9483546c74 | |
|
|
f537204987 | |
|
|
c758898b8a | |
|
|
a235671fe9 | |
|
|
89538b073e | |
|
|
c8f7f7374d | |
|
|
a03c610a49 | |
|
|
dc4cd53198 | |
|
|
d907bbda0c | |
|
|
31fdd619ac | |
|
|
86b0408f13 | |
|
|
235e308e35 | |
|
|
7c1b4880a0 | |
|
|
ed7a5134be | |
|
|
240e77ca3c | |
|
|
b9904882f4 | |
|
|
4cf7de9902 | |
|
|
e0760b0ef9 | |
|
|
86c19733ca | |
|
|
d671d82642 | |
|
|
1f4a038400 | |
|
|
890483da26 | |
|
|
4c88da9b81 | |
|
|
f97c6ea619 | |
|
|
db03885d39 | |
|
|
5dcb8ae895 | |
|
|
8b07e385c2 | |
|
|
ca07116bf1 | |
|
|
c43910364f | |
|
|
ad3216497c | |
|
|
072b8c26b5 | |
|
|
e3f6cf583b | |
|
|
ee65669677 | |
|
|
83b625b782 | |
|
|
4e132c5710 | |
|
|
589f891feb | |
|
|
2714ebd06c | |
|
|
ac86ffb01c | |
|
|
1c31415759 | |
|
|
7d1abafd58 | |
|
|
1445abe6f2 | |
|
|
8f626590cb | |
|
|
3f0d529d1d | |
|
|
48a5b089c9 | |
|
|
f603499a22 | |
|
|
a1aa894d0c | |
|
|
e2a87f84eb | |
|
|
a95f0ef364 | |
|
|
8a057d2b01 | |
|
|
69d5cf6737 | |
|
|
948670faa5 | |
|
|
3dc2e34877 | |
|
|
5100488ef3 | |
|
|
3647867bc5 | |
|
|
87163def8e | |
|
|
b838b26786 | |
|
|
2fdb253d85 | |
|
|
78f61e03ac | |
|
|
6aeeabceb5 | |
|
|
0bb1d19b8f | |
|
|
982e411360 | |
|
|
15468b1b9e | |
|
|
459404a8cd | |
|
|
6e45ad7aa0 | |
|
|
694f4fdd89 | |
|
|
a174158c71 | |
|
|
45a21fcc3f | |
|
|
05c9240792 | |
|
|
a3fc9182cd | |
|
|
787932b977 | |
|
|
75551d2288 | |
|
|
c6551d0a89 | |
|
|
1a81280d82 | |
|
|
fbf6031ba3 | |
|
|
52b590a7e1 | |
|
|
d05c8c5f3c | |
|
|
81a9fad0ac | |
|
|
c5c8158d84 | |
|
|
9dadedd7ff | |
|
|
9e2f381637 | |
|
|
22d8215a63 | |
|
|
0490ea463b | |
|
|
6719615ee3 | |
|
|
44701f81b2 | |
|
|
8e8700d6bc | |
|
|
f5e8407486 | |
|
|
db1d270cbd | |
|
|
1be85410bd | |
|
|
c4dc7e129b | |
|
|
4a4a97df92 | |
|
|
1e6473c497 | |
|
|
37c878efe3 | |
|
|
653868e74b | |
|
|
bb7cc86d81 | |
|
|
a0ea1de803 | |
|
|
b0af073bbb | |
|
|
2cf107949e | |
|
|
86af7614a4 | |
|
|
7140cac92c | |
|
|
4a14d15496 | |
|
|
de228a354f | |
|
|
189d9c9521 | |
|
|
11e9de5b0a | |
|
|
c5687fff38 | |
|
|
a2e1358208 | |
|
|
256bee273d | |
|
|
837459bb99 | |
|
|
ec2b353725 | |
|
|
ee4c43e495 | |
|
|
015c743664 | |
|
|
7bddf159b5 | |
|
|
54a61a25b8 | |
|
|
85b33ff6bd | |
|
|
1a19bdd16c | |
|
|
bb07be821a | |
|
|
de0aa01a2c | |
|
|
9da282d2a2 | |
|
|
419855b45d | |
|
|
d006c1035e | |
|
|
0b53898946 | |
|
|
35ae977a8d | |
|
|
1c1f098249 | |
|
|
9e14526180 | |
|
|
59af701f3e | |
|
|
05d500c467 | |
|
|
bf5a28dda1 | |
|
|
8254889675 | |
|
|
804f62be0e | |
|
|
3a4e661519 | |
|
|
4fdcde33a1 | |
|
|
30b33021d9 | |
|
|
e0d3072751 | |
|
|
f90057e9b0 | |
|
|
28a4d43185 | |
|
|
f368013729 | |
|
|
5b86e1bb6a | |
|
|
5c2070de51 | |
|
|
b8410d3dcf | |
|
|
b35379450b | |
|
|
d8aa4e05ca | |
|
|
64254159c5 | |
|
|
467eb67f18 | |
|
|
df27ed7180 | |
|
|
95994a0927 | |
|
|
0f777079a2 | |
|
|
61c05ea76e | |
|
|
96c32db837 | |
|
|
48200a940c | |
|
|
c2f477c5b9 | |
|
|
d35e3b730e | |
|
|
24bdbb1f41 | |
|
|
1ea4a750fb | |
|
|
e9d1d34a59 | |
|
|
9ac0ee1ca9 | |
|
|
af2be40db4 | |
|
|
7768a970f9 | |
|
|
677e8f58d1 | |
|
|
a0e90f4d3b | |
|
|
39c0d83f72 | |
|
|
e22571703b | |
|
|
9cf0f98a82 | |
|
|
1d87b2dc32 | |
|
|
15e28d59a5 | |
|
|
159c215219 | |
|
|
b9bb41280a | |
|
|
edc9f8059a | |
|
|
30532782cc | |
|
|
56415e919e | |
|
|
79f8809e37 | |
|
|
ca3d2f8a58 | |
|
|
65382de391 | |
|
|
e2599d64dd | |
|
|
ed1cdf7aba | |
|
|
c5f7a32e12 | |
|
|
956e260614 | |
|
|
422f7da5fe | |
|
|
bf5275d41f | |
|
|
796a118c9b | |
|
|
61be0fd496 | |
|
|
31c3602e96 | |
|
|
62645eb08e | |
|
|
2af6673932 | |
|
|
bf354ffc80 | |
|
|
e4623840e6 | |
|
|
a8c97599ff | |
|
|
5d22f261cd | |
|
|
9574d66765 | |
|
|
42342a58b4 | |
|
|
98de360352 | |
|
|
27cecb0e7e | |
|
|
1328ac4c1d | |
|
|
1011bb56e4 | |
|
|
f457477e50 | |
|
|
95978f5bd8 | |
|
|
618d73885f | |
|
|
be11adc313 | |
|
|
b6d1ee09fa | |
|
|
ef33656bfe | |
|
|
e20d3ce8f1 | |
|
|
fd24686721 | |
|
|
b656ef668e | |
|
|
1867b3986d | |
|
|
3f0491f63b | |
|
|
de79aab554 | |
|
|
7ceb3b68aa | |
|
|
56d4e2f12a | |
|
|
e59aba5d0c | |
|
|
b73ae3cb2a | |
|
|
a3ee78a055 | |
|
|
41fb455cd5 | |
|
|
9d1878d8e1 | |
|
|
0fa713ce0c | |
|
|
07bfe79643 | |
|
|
17be86a111 | |
|
|
b2d7eadc82 | |
|
|
a632bdc176 | |
|
|
3d909b4ba0 | |
|
|
a29e131803 | |
|
|
05af801c90 | |
|
|
460e5787c8 | |
|
|
fc282f3801 | |
|
|
ade14ae1dc | |
|
|
fd6d6febf1 | |
|
|
23257cb485 | |
|
|
232ce07ff1 | |
|
|
8f86e8d1c8 | |
|
|
aa16435fd1 | |
|
|
9567c991f9 | |
|
|
c7aed320f6 | |
|
|
90b54ac05e | |
|
|
c3357483d3 | |
|
|
6f7559996f | |
|
|
397d6325f4 | |
|
|
3352bad383 | |
|
|
93d6899c8e | |
|
|
087d9674d2 | |
|
|
def87c05c0 | |
|
|
66a3b14242 | |
|
|
e831d3e38f | |
|
|
9cc6131ed2 | |
|
|
f33ab54847 | |
|
|
8dffb996ae | |
|
|
ecae3fc1da | |
|
|
de871e1800 | |
|
|
ee2589a776 | |
|
|
4b137a2414 | |
|
|
89a27c6bf0 | |
|
|
def3703ed7 | |
|
|
172a0b3968 | |
|
|
23e8eabf4a | |
|
|
363d9a4255 | |
|
|
1d485c067e | |
|
|
2d6b3fc6d5 | |
|
|
bd0a3cf91e | |
|
|
61e99ab8a8 | |
|
|
fd63724fd5 | |
|
|
036ee3be83 | |
|
|
7824645f8f | |
|
|
e584124c7f | |
|
|
f768901c6a | |
|
|
12ebbd5bf5 | |
|
|
52e7223959 | |
|
|
28ead7e0ec | |
|
|
b2f3858c7c | |
|
|
620e40b172 | |
|
|
9265609c91 | |
|
|
707a25dd44 | |
|
|
3c40cd92c8 | |
|
|
0e871fd0e8 | |
|
|
596a42d0a0 | |
|
|
1581315234 | |
|
|
7582227266 | |
|
|
fb42aec2af | |
|
|
b77e06c621 | |
|
|
fb4c06950c | |
|
|
d76c70ba23 | |
|
|
2501243a32 | |
|
|
19fb9d2b53 | |
|
|
8c47d1f9f8 | |
|
|
bd42feba23 | |
|
|
f331ff014d | |
|
|
6d5347b034 | |
|
|
9b11084404 | |
|
|
9aa2812263 | |
|
|
597ba9d525 | |
|
|
65014ebcb4 | |
|
|
30ac1a4200 | |
|
|
067e26b4e6 | |
|
|
4bbf29c245 | |
|
|
eab6851c53 | |
|
|
9e9b808a08 | |
|
|
1f76ec618c | |
|
|
f46c41cb89 | |
|
|
57587b83b8 | |
|
|
6638331030 | |
|
|
2a0c1de266 | |
|
|
0a3c828ff9 | |
|
|
7e59638bb5 | |
|
|
08047e25ca | |
|
|
f86f362e59 | |
|
|
86f5312ad5 | |
|
|
b0f0f50602 | |
|
|
7ce6902b71 | |
|
|
328d9f519b | |
|
|
c4845298bc | |
|
|
189ff9ba33 | |
|
|
cb21ca7a22 | |
|
|
20e7c65a50 | |
|
|
d3ea6114e7 | |
|
|
328ea0c640 | |
|
|
a3da7b231c | |
|
|
64f5928356 | |
|
|
3f8a50a993 | |
|
|
0821b969f3 | |
|
|
d6dcaebcdd | |
|
|
74e040ea43 | |
|
|
e87187c3ba | |
|
|
f85a223cf1 | |
|
|
a9fafdd56d | |
|
|
4b71bca6de | |
|
|
cf24e4c773 | |
|
|
c1141ca6e6 | |
|
|
70a58f6947 | |
|
|
e5974b6489 | |
|
|
aa1c57072d | |
|
|
c6cbc97bf0 | |
|
|
39f1fc087a | |
|
|
6a6f456620 | |
|
|
e0868f10b4 | |
|
|
fa9a228197 | |
|
|
83b42b930f | |
|
|
624bae2d61 | |
|
|
6dc8dccdf8 | |
|
|
291824960c | |
|
|
f06d98479f | |
|
|
3f83dae4e7 | |
|
|
a486f7c03b | |
|
|
c8e72aeeed | |
|
|
5567e97b32 | |
|
|
20e5914b3b | |
|
|
ab557bb8a4 | |
|
|
0f464c00c9 | |
|
|
22ce38c86a | |
|
|
c95f5edb9b | |
|
|
b42f83db11 | |
|
|
d32ce60588 | |
|
|
e975bbda00 | |
|
|
beb84661b1 | |
|
|
0dbb499826 | |
|
|
d742bc7bdd | |
|
|
d835553926 | |
|
|
c8ed774382 | |
|
|
7d938d67fe | |
|
|
ca853910cd | |
|
|
cc5b80b31e | |
|
|
301fc4775a | |
|
|
222256ab92 | |
|
|
33f321a92b | |
|
|
6ad29f86af | |
|
|
f6d7bdcdfb | |
|
|
2b78263e82 | |
|
|
fe99d0b2d3 | |
|
|
8d8292bca8 | |
|
|
d23ebba0e2 | |
|
|
6b73d5a746 | |
|
|
dc558dcef5 | |
|
|
6620ba18da | |
|
|
444fcac73d | |
|
|
15fbadca2d | |
|
|
bb06c51485 | |
|
|
e85341c637 | |
|
|
2c0a342c89 | |
|
|
53564a9119 | |
|
|
5063300606 | |
|
|
e1d80a96f3 | |
|
|
fee5451482 | |
|
|
cc87051c0c | |
|
|
a96b06ab25 | |
|
|
213b12f5fb | |
|
|
4a5b8619c0 | |
|
|
3b3e555f8b | |
|
|
58559ff22d | |
|
|
59bf271254 | |
|
|
9d159250cd | |
|
|
5c622e4ee9 | |
|
|
107c4e6749 | |
|
|
7831ef0c4d | |
|
|
5443ce03c8 | |
|
|
9e60d34710 | |
|
|
e1e1b32db1 | |
|
|
286f2d4916 | |
|
|
35dd36da69 | |
|
|
787e36f2ad | |
|
|
f82d647f51 | |
|
|
85502cf3da | |
|
|
494fd3cbb3 | |
|
|
06d2767607 | |
|
|
1fc1e905b1 | |
|
|
76b0417745 | |
|
|
133496bc88 | |
|
|
43acbbc843 | |
|
|
5dc37f6bdf | |
|
|
76b5edd13d | |
|
|
ad06bd5e32 | |
|
|
aef4d72d95 | |
|
|
8964e7c613 | |
|
|
6b5e9a2a72 | |
|
|
914d968045 | |
|
|
00f0e192a5 | |
|
|
0036fee389 | |
|
|
f657a9733f | |
|
|
7cb1167dc9 |
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"sandboxes": ["new", "vanilla"]
|
||||
}
|
||||
|
|
@ -9,21 +9,23 @@ about: If something isn't working as expected 🤔.
|
|||
A clear and concise description of the behavior.
|
||||
|
||||
```js
|
||||
// Your code here
|
||||
module.exports = robot => {
|
||||
robot.log('There is a bug')
|
||||
}
|
||||
// Your code here
|
||||
export default (app) => {
|
||||
app.log.info("There is a bug");
|
||||
};
|
||||
```
|
||||
|
||||
**Expected behavior/code**
|
||||
A clear and concise description of what you expected to happen (or code).
|
||||
|
||||
**Environment**
|
||||
|
||||
- Probot version(s): [e.g. v6.0.0]
|
||||
- Node/npm version: [e.g. Node 8/npm 5]
|
||||
- OS: [e.g. OSX 10.13.4, Windows 10]
|
||||
|
||||
**Possible Solution**
|
||||
|
||||
<!--- Only if you have suggestions on a fix for the bug -->
|
||||
|
||||
**Additional context/Screenshots**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
label-alias:
|
||||
bug: 'bug 🐞'
|
||||
feature_request: 'enhancement ➕'
|
||||
question: 'question ❓'
|
||||
|
|
@ -3,7 +3,7 @@ template: >
|
|||
- [ ] Sort changes into relevant sections below
|
||||
- [ ] Replace `master` with next tag in "See full changelog" link
|
||||
- [ ] Delete this TODO list
|
||||
- [ ] Hit pubish
|
||||
- [ ] Hit publish
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"extends": [
|
||||
"github>probot/.github"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master", "*.x", "beta", "next" ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ "master", "beta" ]
|
||||
schedule:
|
||||
- cron: '24 7 * * 2'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript-typescript' ]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: security-and-quality
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
name: Docs
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
docs:
|
||||
name: docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
- run: git fetch --depth=20 origin +refs/tags/*:refs/tags/*
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: npm
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Build probot
|
||||
run: npm run build
|
||||
- name: Publish documentation
|
||||
run: ./script/publish-docs
|
||||
env:
|
||||
OCTOKITBOT_PAT: ${{ secrets.OCTOKITBOT_PAT }}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
name: Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- next
|
||||
- beta
|
||||
- "*.x"
|
||||
|
||||
# These are recommended by the semantic-release docs: https://github.com/semantic-release/npm#npm-provenance
|
||||
permissions:
|
||||
contents: write # to be able to publish a GitHub release
|
||||
issues: write # to be able to comment on released issues
|
||||
pull-requests: write # to be able to comment on released pull requests
|
||||
id-token: write # to enable use of OIDC for npm provenance
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Build
|
||||
run: npm run build
|
||||
- name: Release
|
||||
run: npx semantic-release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
name: Test
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
name: Dependency Review
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Dependency review
|
||||
uses: actions/dependency-review-action@v4
|
||||
|
||||
license-check:
|
||||
name: Check Licenses
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Check Licenses
|
||||
run: npx license-checker --production --summary --onlyAllow="0BSD;Apache-2.0;Apache 2.0;Python-2.0;BSD-2-Clause;BSD-3-Clause;ISC;MIT"
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: npm
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
test-unit:
|
||||
name: Test on Node ${{ matrix.node-version }} and ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
node-version:
|
||||
- 18
|
||||
- 20
|
||||
- 21
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- macos-latest
|
||||
- windows-latest
|
||||
fail-fast: false
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Test
|
||||
run: npm run test
|
||||
|
||||
test-redis:
|
||||
name: Test on Node LTS and Redis 7
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
redis:
|
||||
image: 'redis:7'
|
||||
ports:
|
||||
- '6379:6379'
|
||||
options: '--entrypoint redis-server'
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Use Node.js LTS
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: npm
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
- name: Test
|
||||
run: npm run test
|
||||
env:
|
||||
REDIS_HOST: 127.0.0.1
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- test-unit
|
||||
- test-redis
|
||||
|
||||
steps:
|
||||
- run: exit 1
|
||||
if: ${{ needs.test-unit.result != 'success' || needs.test-redis.result != 'success' }}
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Use Node.js LTS
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: npm
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
- name: Build
|
||||
run: npm run build
|
||||
- name: Test
|
||||
run: npm run test:coverage
|
||||
- name: Update Codecov
|
||||
run: npx codecov
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
if: ${{ always() }}
|
||||
|
|
@ -2,10 +2,10 @@ npm-debug.log
|
|||
.DS_Store
|
||||
node_modules/
|
||||
*.pem
|
||||
!test/**/*.pem
|
||||
.env
|
||||
out
|
||||
tmp
|
||||
package-lock.json
|
||||
coverage
|
||||
lib/
|
||||
.idea
|
||||
|
|
|
|||
32
.travis.yml
32
.travis.yml
|
|
@ -1,32 +0,0 @@
|
|||
sudo: required
|
||||
|
||||
language: node_js
|
||||
|
||||
node_js:
|
||||
- "8"
|
||||
|
||||
env:
|
||||
matrix:
|
||||
include:
|
||||
- node_js: "10"
|
||||
allow_failures:
|
||||
- node_js: "10"
|
||||
|
||||
|
||||
notifications:
|
||||
disabled: true
|
||||
|
||||
before_install:
|
||||
- npm install -g codecov
|
||||
|
||||
install:
|
||||
- npm install
|
||||
- npm run build
|
||||
|
||||
branches:
|
||||
except:
|
||||
- /^v\d+\.\d+\.\d+$/
|
||||
|
||||
script:
|
||||
- npm test
|
||||
- codecov
|
||||
|
|
@ -1,17 +1,12 @@
|
|||
{
|
||||
"mode": "file",
|
||||
"entryPoints": ["src"],
|
||||
"out": "out",
|
||||
"exclude": "test",
|
||||
"theme": "minimal",
|
||||
"theme": "default",
|
||||
"excludePrivate": true,
|
||||
"excludeNotExported": true,
|
||||
"excludeExternals": true,
|
||||
"listInvalidSymbolLinks": true,
|
||||
"target": "ES5",
|
||||
"moduleResolution": "node",
|
||||
"preserveConstEnums": "true",
|
||||
"stripInternal": "true",
|
||||
"module": "commonjs",
|
||||
"excludeInternal": true,
|
||||
"entryPointStrategy": "expand",
|
||||
"readme": "none",
|
||||
"gaID": "UA-102577034-2"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,45 +2,131 @@
|
|||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
- The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at opensource+probot@github.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
opensource+probot@github.com.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][mozilla coc].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][faq]. Translations are available
|
||||
at [https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
|
||||
[mozilla coc]: https://github.com/mozilla/diversity
|
||||
[faq]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
|
|
|
|||
|
|
@ -1,13 +1,4 @@
|
|||
## Contributing
|
||||
|
||||
[](https://travis-ci.org/probot/probot) [](https://ci.appveyor.com/api/projects/status/32r7s2skrgm9ubva?svg=true) [](https://codecov.io/gh/probot/probot/) [](https://greenkeeper.io/)
|
||||
|
||||
[fork]: https://github.com/probot/probot/fork
|
||||
[pr]: https://github.com/probot/probot/compare
|
||||
[style]: https://standardjs.com/
|
||||
[code-of-conduct]: CODE_OF_CONDUCT.md
|
||||
[good-first-issue-search]: https://github.com/search?utf8=%E2%9C%93&q=topic%3Aprobot+topic%3Aprobot-app+good-first-issues%3A%3E0&type=
|
||||
[linter]: https://github.com/probot/probot/blob/ts-readme/tslint.json
|
||||
# Contributing
|
||||
|
||||
Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great.
|
||||
|
||||
|
|
@ -17,7 +8,7 @@ Please note that this project is released with a [Contributor Code of Conduct][c
|
|||
|
||||
1. [Fork][fork] and clone the repository
|
||||
1. Configure and install the dependencies: `npm install`
|
||||
1. Make sure the tests pass on your machine: `npm test`, note: these tests also apply the [linter][linter] and run the TypeScript compiler (`tsc`) to check for type errors, so there's no need to run these commands separately.
|
||||
1. Make sure the tests pass on your machine: `npm test`, note: these tests also run the TypeScript compiler (`tsc`) to check for type errors, so there's no need to run these commands separately.
|
||||
1. Create a new branch: `git checkout -b my-branch-name`
|
||||
1. Make your change, add tests, and make sure the tests still pass
|
||||
1. Push to your fork and [submit a pull request][pr]
|
||||
|
|
@ -25,13 +16,35 @@ Please note that this project is released with a [Contributor Code of Conduct][c
|
|||
|
||||
Here are a few things you can do that will increase the likelihood of your pull request being accepted:
|
||||
|
||||
- Follow the [style guide][style] which is using standard. Any linting errors should be shown when running `npm test`
|
||||
- Follow the [style guide][style] which is using Prettier. Any linting errors should be shown when running `npm test`
|
||||
- Write and update tests.
|
||||
- Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests.
|
||||
- Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
|
||||
|
||||
Work in Progress pull requests are also welcome to get feedback early on, or if there is something that blocked you.
|
||||
|
||||
To submit a pull request for a previous version of Probot, checkout the maintenance branch before creating a new branch, e.g. `git checkout 7.x && git checkout -b my-branch-name`. When submitting the pull request, set the base branch to the same maintenance branch.
|
||||
|
||||
## Merging the Pull Request & releasing a new version
|
||||
|
||||
Releases are automated using [semantic-release](https://github.com/semantic-release/semantic-release).
|
||||
The following three commit message conventions determine which version is released:
|
||||
|
||||
1. `fix: ...` or `fix(scope name): ...` prefix in subject: bumps fix version, e.g. `1.2.3` → `1.2.4`
|
||||
2. `feat: ...` or `feat(scope name): ...` prefix in subject: bumps feature version, e.g. `1.2.3` → `1.3.0`
|
||||
3. `BREAKING CHANGE:` in body: bumps breaking version, e.g. `1.2.3` → `2.0.0`
|
||||
|
||||
Only one version number is bumped at a time, the highest version change trumps the others. Besides publishing a new version to npm, semantic-release also creates a git tag and release on GitHub, generates changelogs from the commit messages and puts them into the release notes.
|
||||
|
||||
New versions are released from the following branches
|
||||
|
||||
1. `master`: new versions are published to npm using the `@latest` dist-tag
|
||||
1. `next`: new versions are published to npm using the `@next` dist-tag. When merging the `next` branch into `master`, the `@latest` dist-tag will be bumped automatically.
|
||||
1. `beta`: a new `@beta` version will be released. For example, if the current version is `7.5.0`, then the first version will be published as `8.0.0-beta` using the `@beta` dist-tag. The next version will be `8.0.0-beta.1`, etc. When merged into master, the `8.0.0` version will be released automatically and all release notes will be combined into the `8.0.0` release. See also [Publishing pre-releases](https://github.com/semantic-release/semantic-release/blob/beta/docs/recipes/pre-releases.md).
|
||||
1. `<version>.x`: For example `7.x`. New bug fix & feature releases are published the given version. No breaking changes are allowed. See also [Publishing maintenance releases](https://github.com/semantic-release/semantic-release/blob/beta/docs/recipes/maintenance-releases.md).
|
||||
|
||||
When in doubt, ping [@gr2m](https://github.com/gr2m).
|
||||
|
||||
## Just starting out? Looking for how to help?
|
||||
|
||||
Use [this search][good-first-issue-search] to find Probot apps that have issues marked with the `good-first-issue` label.
|
||||
|
|
@ -41,3 +54,10 @@ Use [this search][good-first-issue-search] to find Probot apps that have issues
|
|||
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
|
||||
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
|
||||
- [GitHub Help](https://help.github.com)
|
||||
|
||||
[fork]: https://github.com/probot/probot/fork
|
||||
[pr]: https://github.com/probot/probot/compare
|
||||
[style]: https://prettier.io/
|
||||
[code-of-conduct]: CODE_OF_CONDUCT.md
|
||||
[good-first-issue-search]: https://github.com/search?utf8=%E2%9C%93&q=topic%3Aprobot+topic%3Aprobot-app+good-first-issues%3A%3E0&type=
|
||||
[linter]: https://github.com/probot/probot/blob/ts-readme/tslint.json
|
||||
|
|
|
|||
2
LICENSE
2
LICENSE
|
|
@ -1,6 +1,6 @@
|
|||
ISC License
|
||||
|
||||
Copyright (c) 2016-2018 Brandon Keepers
|
||||
Copyright (c) 2016-2020 Brandon Keepers and Probot Contributors
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
|
|||
39
README.md
39
README.md
|
|
@ -1,22 +1,35 @@
|
|||
# Probot
|
||||
<p align="center">
|
||||
<a href="https://probot.github.io"><img src="/static/robot.svg" width="160" alt="Probot's logo, a cartoon robot" /></a>
|
||||
</p>
|
||||
<h3 align="center"><a href="https://probot.github.io">Probot</a></h3>
|
||||
<p align="center">A framework for building GitHub Apps to automate and improve your workflow<p>
|
||||
<p align="center"><a href="https://npmjs.com/package/probot"><img src="https://badgen.net/npm/v/probot" alt="npm"></a> <a href="https://github.com/probot/probot/actions?query=workflow%3ACI"><img src="https://github.com/probot/probot/workflows/CI/badge.svg" alt="Build Status"></a> <a href="https://codecov.io/gh/probot/probot/"><img src="https://badgen.now.sh/codecov/c/github/probot/probot" alt="Codecov"></a> <a href="https://twitter.com/ProbotTheRobot"><img src="https://img.shields.io/twitter/follow/ProbotTheRobot.svg?style=social&logo=twitter&label=Follow" alt="@ProbotTheRobot on Twitter"></a>
|
||||
|
||||
[](https://www.npmjs.com/package/probot) [](https://twitter.com/ProbotTheRobot)
|
||||
---
|
||||
|
||||
> 🤖 A framework for building GitHub Apps to automate and improve your workflow
|
||||
|
||||
If you've ever thought, "wouldn't it be cool if GitHub could…"; I'm going to stop you right there. Most features can actually be added via [GitHub Apps](https://developer.github.com/apps/), which extend GitHub and can be installed directly on organizations and user accounts and granted access to specific repositories. They come with granular permissions and built-in webhooks. Apps are first class actors within GitHub.
|
||||
If you've ever thought, "wouldn't it be cool if GitHub could…"; I'm going to stop you right there. Most features can actually be added via [GitHub Apps](https://docs.github.com/en/developers/apps), which extend GitHub and can be installed directly on organizations and user accounts and granted access to specific repositories. They come with granular permissions and built-in webhooks. Apps are first class actors within GitHub.
|
||||
|
||||
## How it works
|
||||
|
||||
**Probot is a framework for building [GitHub Apps](http://developer.github.com/apps) in [Node.js](https://nodejs.org/)**, written in [TypeScript](https://www.typescriptlang.org/). GitHub Apps can listen to webhook events sent by a repository or organization. Probot uses its internal event emitter to perform actions based on those events. A simple Probot App might look like this:
|
||||
**Probot is a framework for building [GitHub Apps](https://docs.github.com/en/developers/apps) in [Node.js](https://nodejs.org/)**, written in [TypeScript](https://www.typescriptlang.org/). GitHub Apps can listen to webhook events sent by a repository or organization. Probot uses its internal event emitter to perform actions based on those events. A simple Probot App might look like this:
|
||||
|
||||
```js
|
||||
module.exports = (app) => {
|
||||
app.on('issues.opened', async context => {
|
||||
const issueComment = context.issue({ body: 'Thanks for opening this issue!' })
|
||||
return context.github.issues.createComment(issueComment)
|
||||
})
|
||||
}
|
||||
export default (app) => {
|
||||
app.on("issues.opened", async (context) => {
|
||||
const issueComment = context.issue({
|
||||
body: "Thanks for opening this issue!",
|
||||
});
|
||||
return context.octokit.issues.createComment(issueComment);
|
||||
});
|
||||
|
||||
app.onAny(async (context) => {
|
||||
context.log.info({ event: context.name, action: context.payload.action });
|
||||
});
|
||||
|
||||
app.onError(async (error) => {
|
||||
app.log.error(error);
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
## Building a Probot App
|
||||
|
|
@ -31,7 +44,7 @@ Probot is built by people just like you! Most of the interesting things are buil
|
|||
|
||||
If you're interested in contributing to Probot itself, check out our [contributing docs](CONTRIBUTING.md) to get started.
|
||||
|
||||
Want to chat with Probot users and contributors? [Join us in Slack](https://probot-slackin.herokuapp.com/)!
|
||||
Want to discuss with Probot users and contributors? [Discuss on GitHub](https://github.com/probot/probot/discussions)!
|
||||
|
||||
## Ideas
|
||||
|
||||
|
|
|
|||
23
app.json
23
app.json
|
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"name": "Probot",
|
||||
"description": "a trainable robot that responds to activity on GitHub",
|
||||
"keywords": ["Probot", "node", "github"],
|
||||
"repository": "https://github.com/probot/probot",
|
||||
"logo": "https://github.com/probot.png",
|
||||
"env": {
|
||||
"PRIVATE_KEY": {
|
||||
"description": "the private key you downloaded when creating the GitHub App"
|
||||
},
|
||||
"APP_ID": {
|
||||
"description": "the ID of your GitHub App"
|
||||
},
|
||||
"WEBHOOK_SECRET": {
|
||||
"description": "the secret configured for your GitHub App"
|
||||
},
|
||||
"LOG_LEVEL": {
|
||||
"required": false,
|
||||
"value": "info",
|
||||
"description": "trace, debug, info, warn, error, or fatal; default: info"
|
||||
}
|
||||
}
|
||||
}
|
||||
21
appveyor.yml
21
appveyor.yml
|
|
@ -1,21 +0,0 @@
|
|||
# Test against the latest version of this Node.js version
|
||||
environment:
|
||||
nodejs_version: "8"
|
||||
|
||||
# Install scripts. (runs after repo cloning)
|
||||
install:
|
||||
# Get the latest stable version of Node.js or io.js
|
||||
- ps: Install-Product node $env:nodejs_version
|
||||
# install modules
|
||||
- npm install
|
||||
|
||||
# Post-install test scripts.
|
||||
test_script:
|
||||
# Output useful info for debugging.
|
||||
- node --version
|
||||
- npm --version
|
||||
# run tests
|
||||
- npm test
|
||||
|
||||
# Don't actually build.
|
||||
build: off
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
require("../lib/bin/probot-receive");
|
||||
|
|
@ -1,44 +1,3 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
require('dotenv').config()
|
||||
|
||||
const pkgConf = require('pkg-conf')
|
||||
const program = require('commander')
|
||||
|
||||
const {findPrivateKey} = require('../lib/private-key')
|
||||
|
||||
program
|
||||
.usage('[options] <apps...>')
|
||||
.option('-p, --port <n>', 'Port to start the server on', process.env.PORT || 3000)
|
||||
.option('-W, --webhook-proxy <url>', 'URL of the webhook proxy service.`', process.env.WEBHOOK_PROXY_URL)
|
||||
.option('-w, --webhook-path <path>', 'URL path which receives webhooks. Ex: `/webhook`', process.env.WEBHOOK_PATH)
|
||||
.option('-a, --app <id>', 'ID of the GitHub App', process.env.APP_ID)
|
||||
.option('-s, --secret <secret>', 'Webhook secret of the GitHub App', process.env.WEBHOOK_SECRET)
|
||||
.option('-P, --private-key <file>', 'Path to certificate of the GitHub App', findPrivateKey)
|
||||
.parse(process.argv)
|
||||
|
||||
program.privateKey = findPrivateKey()
|
||||
|
||||
const {createProbot} = require('../')
|
||||
|
||||
const probot = createProbot({
|
||||
id: program.app,
|
||||
secret: program.secret,
|
||||
cert: program.privateKey,
|
||||
port: program.port,
|
||||
webhookPath: program.webhookPath,
|
||||
webhookProxy: program.webhookProxy
|
||||
})
|
||||
|
||||
const setupMode = !program.app || !program.privateKey
|
||||
|
||||
if (setupMode) {
|
||||
const setupApp = require('../lib/apps/setup')
|
||||
probot.load(setupApp)
|
||||
probot.start()
|
||||
} else {
|
||||
pkgConf('probot').then(pkg => {
|
||||
probot.setup(program.args.concat(pkg.apps || pkg.plugins || []))
|
||||
probot.start()
|
||||
})
|
||||
}
|
||||
require("../lib/bin/probot-run");
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
// Usage: bin/simulate issues path/to/payload app.js
|
||||
|
||||
require('dotenv').config({silent: true})
|
||||
|
||||
const path = require('path')
|
||||
const program = require('commander')
|
||||
const {findPrivateKey} = require('../lib/private-key')
|
||||
|
||||
program
|
||||
.usage('[options] <event-name> <path/to/payload.json> [path/to/app.js...]')
|
||||
.option('-a, --app <id>', 'ID of the GitHub App', process.env.APP_ID)
|
||||
.option('-P, --private-key <file>', 'Path to certificate of the GitHub App', findPrivateKey)
|
||||
.parse(process.argv)
|
||||
|
||||
const eventName = program.args[0]
|
||||
const payloadPath = program.args[1]
|
||||
|
||||
if (!eventName || !payloadPath) {
|
||||
program.help()
|
||||
}
|
||||
|
||||
const payload = require(path.join(process.cwd(), payloadPath))
|
||||
|
||||
const {createProbot} = require('../')
|
||||
|
||||
const probot = createProbot({
|
||||
id: program.app,
|
||||
cert: findPrivateKey()
|
||||
})
|
||||
|
||||
probot.setup(program.args.slice(2))
|
||||
|
||||
probot.logger.debug('Simulating event', eventName)
|
||||
probot.receive({name: eventName, payload})
|
||||
|
|
@ -1,16 +1,3 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const semver = require('semver')
|
||||
const version = require('../package').engines.node
|
||||
|
||||
if (!semver.satisfies(process.version, version)) {
|
||||
console.log(`Node.js version ${version} is required. You have ${process.version}.`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
require('commander')
|
||||
.version(require('../package').version)
|
||||
.usage('<command> [options]')
|
||||
.command('run', 'run the bot')
|
||||
.command('simulate', 'simulate a webhook being delivered')
|
||||
.parse(process.argv)
|
||||
require("../lib/bin/probot");
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
---
|
||||
next: docs/hello-world.md
|
||||
permalink: /docs/
|
||||
next: hello-world
|
||||
title: Introduction
|
||||
layout: docs.liquid
|
||||
---
|
||||
|
||||
# Introduction
|
||||
|
||||
If you've ever thought, "wouldn't it be cool if GitHub could…"; imma stop you right there. Most features can actually be added via [GitHub Apps](https://developer.github.com/apps/), which extend GitHub and can be installed directly on organizations and user accounts and granted access to specific repositories. They come with granular permissions and built-in webhooks. Apps are first class actors within GitHub.
|
||||
If you've ever thought, "wouldn't it be cool if GitHub could…"; imma stop you right there. Most features can actually be added via [GitHub Apps](https://docs.github.com/apps/), which extend GitHub and can be installed directly on organizations and user accounts and granted access to specific repositories. They come with granular permissions and built-in webhooks. Apps are first class actors within GitHub.
|
||||
|
||||
**Probot is a framework for building [GitHub Apps](http://developer.github.com/apps) in [Node.js](https://nodejs.org/)**. It aims to eliminate all the drudgery–like receiving and validating webhooks, and doing authentication handstands–so you can focus on the features you want to build.
|
||||
**Probot is a framework for building [GitHub Apps](http://docs.github.com/apps) in [Node.js](https://nodejs.org/)**. It aims to eliminate all the drudgery–like receiving and validating webhooks, and doing authentication handstands–so you can focus on the features you want to build.
|
||||
|
||||
Probot apps are easy to write, deploy, and share. Many of the most popular Probot apps are hosted, so there's nothing for you to deploy and manage. Here are just a few examples of things that have been built with Probot:
|
||||
|
||||
{% include docs/examples.html %}
|
||||
{% include "docs/examples" %}
|
||||
|
||||
Check out the [featured apps](https://probot.github.io/apps/) or [browse more examples on GitHub](https://github.com/search?q=topic%3Aprobot-app&type=Repositories)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
# Best Practices
|
||||
---
|
||||
title: Best practices
|
||||
---
|
||||
|
||||
# Best practices
|
||||
|
||||
First and foremost, your app must obey the [The Three Laws of Robotics](https://en.wikipedia.org/wiki/Three_Laws_of_Robotics):
|
||||
|
||||
|
|
@ -11,9 +15,19 @@ Now that we agree that nobody will get hurt, here are some tips to make your app
|
|||
|
||||
**Contents:**
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
- [Empathy](#empathy)
|
||||
- [Avoid the uncanny valley](#avoid-the-uncanny-valley)
|
||||
- [Autonomy](#autonomy)
|
||||
- [Never take bulk actions without explicit permission](#never-take-bulk-actions-without-explicit-permission)
|
||||
- [Include "dry run" functionality](#include-dry-run-functionality)
|
||||
- [Configuration](#configuration)
|
||||
- [Require minimal configuration](#require-minimal-configuration)
|
||||
- [Provide full configuration](#provide-full-configuration)
|
||||
- [Store configuration in the repository](#store-configuration-in-the-repository)
|
||||
|
||||
<!-- tocstop -->
|
||||
|
||||
## Empathy
|
||||
|
||||
|
|
@ -29,8 +43,8 @@ The [uncanny valley](https://en.wikipedia.org/wiki/Uncanny_valley) is the hypoth
|
|||
|
||||
Your app should be empathetic, but it shouldn't pretend to be human. It is an app and everyone that interacts with it knows that.
|
||||
|
||||
> - :smile: "Latest build failures: _{listing of build failures}_…"
|
||||
> - :cry: "Hey there! You asked for the build failures, so I went and dug them up for you: _{listing of build failures}_ … Have a fantastic day!"
|
||||
> - 😄 "Latest build failures: _{listing of build failures}_…"
|
||||
> - 😢 "Hey there! You asked for the build failures, so I went and dug them up for you: _{listing of build failures}_ … Have a fantastic day!"
|
||||
|
||||
## Autonomy
|
||||
|
||||
|
|
@ -58,4 +72,52 @@ Apps _should_ allow all settings to be customized for each installation.
|
|||
|
||||
### Store configuration in the repository
|
||||
|
||||
Any configuration _should_ be stored in the target repository. Unless the app is using files from an established convention, the configuration _should_ be stored in the `.github` directory. See the [API docs for `context.config`](https://probot.github.io/api/latest/classes/context.html#config).
|
||||
Any configuration _should_ be stored in the target repository. Unless the app is using files from an established convention, the configuration _should_ be stored in the `.github` directory. See the [API docs for `context.config`](https://probot.github.io/api/latest/classes/context.Context.html#config).
|
||||
|
||||
`context.config` supports sharing configs between repositories. If configuration for your app is not available in the target repository, it will be loaded from the `.github` directory of the target organization's `.github` repository.
|
||||
|
||||
User's can also choose their own shared location. Use the `_extends` option in the configuration file to extend settings from another repository.
|
||||
|
||||
For example, given `.github/test.yml`:
|
||||
|
||||
```yaml
|
||||
_extends: github-settings
|
||||
# Override values from the extended config or define new values
|
||||
name: myrepo
|
||||
```
|
||||
|
||||
This configuration will be merged with the `.github/test.yml` file from the `github-settings` repository, which might look like this:
|
||||
|
||||
```yaml
|
||||
shared1: will be merged
|
||||
shared2: will also be merged
|
||||
```
|
||||
|
||||
Just put common configuration keys in a repository within your organization. Then reference this repository from config files with the same name.
|
||||
|
||||
You can also reference configurations from other organizations:
|
||||
|
||||
```yaml
|
||||
_extends: other/probot-settings
|
||||
other: DDD
|
||||
```
|
||||
|
||||
Additionally, you can specify a specific path for the configuration by
|
||||
appending a colon after the project.
|
||||
|
||||
```yaml
|
||||
_extends: probot-settings:.github/other_test.yaml
|
||||
other: FFF
|
||||
```
|
||||
|
||||
Inherited configurations are in the **exact same location** within the
|
||||
repositories.
|
||||
|
||||
```yaml
|
||||
# octocat/repo1:.github/test.yaml
|
||||
_extends: .github
|
||||
other: GGG
|
||||
|
||||
# octocat/.github:test.yaml
|
||||
other: HHH
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,35 +1,37 @@
|
|||
---
|
||||
next: docs/deployment.md
|
||||
next: testing
|
||||
title: Configuration
|
||||
---
|
||||
|
||||
# Environment Configuration
|
||||
# Configuration
|
||||
|
||||
When developing a Probot App, you will need to have several different fields in a `.env` file which specify environment variables. Here are some common use cases:
|
||||
|
||||
Variable | Description
|
||||
---|---
|
||||
`APP_ID` | The App ID assigned to your GitHub App. **Required** <p>_(Example: `1234`)_</p>
|
||||
**Private Key Options** | One of the following is **Required** if there is no `.pem` file in your project's root directory
|
||||
`PRIVATE_KEY_PATH` | The path to the `.pem` file for your GitHub App. <p>_(Example: `path/to/key.pem`)_</p>
|
||||
`PRIVATE_KEY` | The contents of the private key for your GitHub App. If you're unable to use multiline environment variables, use base64 encoding to convert the key to a single line string. See the [Deployment](deployment.md) docs for provider specific usage. |
|
||||
**Webhook Options** |
|
||||
`WEBHOOK_PROXY_URL` | Allows your local development environment to receive GitHub webhook events. Go to https://smee.io/new to get started. <p>_(Example: `https://smee.io/your-custom-url`)_</p>
|
||||
`WEBHOOK_SECRET` | The webhook secret used when creating a GitHub App. 'development' is used as a default, but the value in `.env` needs to match the value configured in your App settings on GitHub. Note: GitHub marks this value as optional, but for optimal security it's required for Probot apps. **Required** <p>_(Example: `development`)_</p>
|
||||
| Environment Variable | [Programmatic Argument](/docs/development/#run-probot-programmatically) | Description |
|
||||
| ----------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `APP_ID` | `new Probot({ appId })` | The App ID assigned to your GitHub App. **Required** <p>_(Example: `1234`)_</p> |
|
||||
| `PRIVATE_KEY` or `PRIVATE_KEY_PATH` | `new Probot({ privateKey })` | The contents of the private key for your GitHub App. If you're unable to use multiline environment variables, use base64 encoding to convert the key to a single line string. See the [Deployment](/docs/deployment) docs for provider specific usage. When using the `PRIVATE_KEY_PATH` environment variable, set it to the path of the `.pem` file that you downloaded from your GitHub App registration. <p>_(Example: `path/to/key.pem`)_</p> |
|
||||
| **Webhook options** | |
|
||||
| `WEBHOOK_PROXY_URL` | `new Server({ webhookProxy })` | Allows your local development environment to receive GitHub webhook events. Go to https://smee.io/new to get started. <p>_(Example: `https://smee.io/your-custom-url`)_</p> |
|
||||
| `WEBHOOK_SECRET` | `new Probot({ secret })` | The webhook secret used when creating a GitHub App. 'development' is used as a default, but the value in `.env` needs to match the value configured in your App settings on GitHub. Note: GitHub marks this value as optional, but for optimal security it's required for Probot apps. **Required** <p>_(Example: `development`)_</p> |
|
||||
|
||||
For more on the set up of these items, check out [Configuring a GitHub App](https://probot.github.io/docs/development/#configuring-a-github-app).
|
||||
For more on the set up of these items, check out [Configuring a GitHub App](/docs/development/#configuring-a-github-app).
|
||||
|
||||
Some less common environment variables are:
|
||||
|
||||
Variable | Description
|
||||
---|---
|
||||
`DISABLE_STATS` | Set to `true` to disable the the `/probot/stats` endpoint, which gathers data about each app. Recommend for apps with a lot of installations. <p>_(Example: `true`)_</p>
|
||||
`GHE_HOST` | The hostname of your GitHub Enterprise instance. <p>_(Example: `github.mycompany.com`)_</p>
|
||||
`IGNORED_ACCOUNTS` | A comma-separated list of GitHub account names to ignore. This is currently used by the `/probot/stats`. By marking an account as ignored, that account will not be included in data collected on the website. The primary use case for this is spammy or abusive users that the GitHub API sends us but who 404. <p>_(Example: `spammyPerson,abusiveAccount`)_</p>
|
||||
`LOG_FORMAT` | By default, logs are formatted for readability in development. You can set this to `short`, `long`, `simple`, `json`, `bunyan`. Default: `short`
|
||||
`LOG_LEVEL` | The verbosity of logs to show when running your app, which can be `trace`, `debug`, `info`, `warn`, `error`, or `fatal`. Default: `info`
|
||||
`PORT` | The port to start the local server on. Default: `3000`.
|
||||
`SENTRY_DSN` | Set to a [Sentry](https://sentry.io/) DSN to report all errors thrown by your app. <p>_(Example: `https://1234abcd@sentry.io/12345`)_</p>
|
||||
`WEBHOOK_PATH` | The URL path which will recieve webhooks. Default: `/`.
|
||||
`INSTALLATION_TOKEN_TTL` | The length of time installation access tokens are cached by Probot. This may be useful if your app is running long processes before accessing `context.github`. Default: `3540` (59 minutes) <p>_(Example: `3300` or 55 minutes)_</p>
|
||||
| Environment Variable | [Programmatic Argument](/docs/development/#run-probot-programmatically) | Description |
|
||||
| --------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `GHE_HOST` | `new Probot({ Octokit: ProbotOctokit.defaults({ baseUrl }) })` | The hostname of your GitHub Enterprise instance. <p>_(Example: `github.mycompany.com`)_</p> |
|
||||
| `GHE_PROTOCOL` | `new Probot({ Octokit: ProbotOctokit.defaults({ baseUrl }) })` | The protocol of your GitHub Enterprise instance. Defaults to HTTPS. Do not change unless you are certain. <p>_(Example: `https`)_</p> |
|
||||
| `GH_ORG` | - | The organization where you want to register the app in the app creation manifest flow. If set, the app is registered for an organization (https://github.com/organizations/ORGANIZATION/settings/apps/new), if not set, the GitHub app would be registered for the user account (https://github.com/settings/apps/new). |
|
||||
| `LOG_FORMAT` | - | By default, logs are formatted for readability in development. You can set this to `json` in order to disable the formatting |
|
||||
| `LOG_LEVEL` | `const log = require('pino')({ level })` | The verbosity of logs to show when running your app, which can be `fatal`, `error`, `warn`, `info`, `debug`, `trace` or `silent`. Default: `info` |
|
||||
| `LOG_LEVEL_IN_STRING` | - | By default, when using the `json` format, the level printed in the log records is an int (`10`, `20`, ..). This option tells the logger to print level as a string: `{"level": "info"}`. Default `false` |
|
||||
| `LOG_MESSAGE_KEY` | `const log = require('pino')({ messageKey: 'msg' })` | Only relevant when `LOG_FORMAT` is set to `json`. Sets the json key for the log message. Default: `msg` |
|
||||
| `SENTRY_DSN` | - | Set to a [Sentry](https://sentry.io/) DSN to report all errors thrown by your app. <p>_(Example: `https://1234abcd@sentry.io/12345`)_</p> |
|
||||
| `PORT` | `new Server({ port })` | The port to start the local server on. Default: `3000` |
|
||||
| `HOST` | `new Server({ host })` | The host to start the local server on. |
|
||||
| `WEBHOOK_PATH` | `new Server({ webhookPath })` | The URL path which will receive webhooks. Default: `/api/github/webhooks` |
|
||||
| `REDIS_URL` | `new Probot({ redisConfig })` | Set to a `redis://` url as connection option for [ioredis](https://github.com/luin/ioredis#connect-to-redis) in order to enable [cluster support for request throttling](https://github.com/octokit/plugin-throttling.js#clustering). <p>_(Example: `redis://:secret@redis-123.redislabs.com:12345/0`)_</p> |
|
||||
|
||||
For more information on the formatting conventions and rules of `.env` files, check out [the npm `dotenv` module's documentation](https://www.npmjs.com/package/dotenv#rules).
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
next: docs/persistence.md
|
||||
next: http
|
||||
title: Deployment
|
||||
---
|
||||
|
||||
# Deployment
|
||||
|
|
@ -10,23 +11,36 @@ Every app can either be deployed stand-alone, or combined with other apps in one
|
|||
|
||||
**Contents:**
|
||||
|
||||
1. [Create the GitHub App](#create-the-github-app)
|
||||
1. [Deploy the app](#deploy-the-app)
|
||||
1. [Glitch](#glitch)
|
||||
1. [Heroku](#heroku)
|
||||
1. [Now](#now)
|
||||
1. [Share the app](#share-the-app)
|
||||
1. [Combining apps](#combining-apps)
|
||||
1. [Error tracking](#error-tracking)
|
||||
<!-- toc -->
|
||||
|
||||
## Create the GitHub App
|
||||
- [Register the GitHub App](#register-the-github-app)
|
||||
- [Deploy the app](#deploy-the-app)
|
||||
- [As node app](#as-node-app)
|
||||
- [Glitch](#glitch)
|
||||
- [Heroku](#heroku)
|
||||
- [As serverless function](#as-serverless-function)
|
||||
- [AWS Lambda](#aws-lambda)
|
||||
- [Azure Functions](#azure-functions)
|
||||
- [Google Cloud Functions](#google-cloud-functions)
|
||||
- [GitHub Actions](#github-actions)
|
||||
- [Begin](#begin)
|
||||
- [Vercel](#vercel)
|
||||
- [Netlify Functions](#netlify-functions)
|
||||
- [Share the app](#share-the-app)
|
||||
- [Combining apps](#combining-apps)
|
||||
- [Error tracking](#error-tracking)
|
||||
|
||||
Every deployment will need an [App](https://developer.github.com/apps/).
|
||||
<!-- tocstop -->
|
||||
|
||||
1. [Create a new GitHub App](https://github.com/settings/apps/new) with:
|
||||
- **Homepage URL**: the URL to the GitHub repository for your app
|
||||
- **Webhook URL**: Use `https://example.com/` for now, we'll come back in a minute to update this with the URL of your deployed app.
|
||||
- **Webhook Secret**: Generate a unique secret with `openssl rand -base64 32` and save it because you'll need it in a minute to configure your deployed app.
|
||||
## Register the GitHub App
|
||||
|
||||
Every deployment will need a [GitHub App registration](https://docs.github.com/apps).
|
||||
|
||||
1. [Register a new GitHub App](https://github.com/settings/apps/new) with:
|
||||
|
||||
- **Homepage URL**: the URL to the GitHub repository for your app
|
||||
- **Webhook URL**: Use `https://example.com/` for now, we'll come back in a minute to update this with the URL of your deployed app.
|
||||
- **Webhook Secret**: Generate a unique secret with (e.g. with `openssl rand -base64 32`) and save it because you'll need it in a minute to configure your Probot app.
|
||||
|
||||
1. Download the private key from the app.
|
||||
|
||||
|
|
@ -46,7 +60,11 @@ And one of:
|
|||
|
||||
`PRIVATE_KEY` takes precedence over `PRIVATE_KEY_PATH`.
|
||||
|
||||
### Glitch
|
||||
### As node app
|
||||
|
||||
Probot can run your app function using the `probot` binary. If your app function lives in `./app.js`, you can start it as node process using `probot run ./app.js`
|
||||
|
||||
#### Glitch
|
||||
|
||||
Glitch lets you host node applications for free and edit them directly in your browser. It’s great for experimentation and entirely sufficient for simple apps.
|
||||
|
||||
|
|
@ -65,77 +83,246 @@ Glitch lets you host node applications for free and edit them directly in your b
|
|||
|
||||
Enjoy!
|
||||
|
||||
**Bonus:** You can deploy your app using [glitch-deploy](https://github.com/gr2m/glitch-deploy) directly from your terminal or as [continuous deployment](https://github.com/gr2m/glitch-deploy#deploy-from-ci).
|
||||
#### Heroku
|
||||
|
||||
### Heroku
|
||||
Probot runs like [any other Node app](https://devcenter.heroku.com/articles/deploying-nodejs) on Heroku. After [creating the GitHub App](#register-the-github-app):
|
||||
|
||||
Probot runs like [any other Node app](https://devcenter.heroku.com/articles/deploying-nodejs) on Heroku. After [creating the GitHub App](#create-the-github-app):
|
||||
1. Make sure you have the [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli) client installed.
|
||||
|
||||
1. Make sure you have the [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli) client installed.
|
||||
1. Clone the app that you want to deploy. e.g. `git clone https://github.com/probot/stale`
|
||||
|
||||
1. Clone the app that you want to deploy. e.g. `git clone https://github.com/probot/stale`
|
||||
|
||||
1. Create the Heroku app with the `heroku create` command:
|
||||
1. Create the Heroku app with the `heroku create` command:
|
||||
|
||||
$ heroku create
|
||||
Creating arcane-lowlands-8408... done, stack is cedar
|
||||
http://arcane-lowlands-8408.herokuapp.com/ | git@heroku.com:arcane-lowlands-8408.git
|
||||
Git remote heroku added
|
||||
|
||||
1. Go back to your [app settings page](https://github.com/settings/apps) and update the **Webhook URL** to the URL of your deployment, e.g. `http://arcane-lowlands-8408.herokuapp.com/`.
|
||||
1. Go back to your [app settings page](https://github.com/settings/apps) and update the **Webhook URL** to the URL of your deployment, e.g. `http://arcane-lowlands-8408.herokuapp.com/`.
|
||||
|
||||
1. Configure the Heroku app, replacing the `APP_ID` and `WEBHOOK_SECRET` with the values for those variables, and setting the path for the `PRIVATE_KEY`:
|
||||
1. Configure the Heroku app, replacing the `APP_ID` and `WEBHOOK_SECRET` with the values for those variables, and setting the path for the `PRIVATE_KEY`:
|
||||
|
||||
$ heroku config:set APP_ID=aaa \
|
||||
WEBHOOK_SECRET=bbb \
|
||||
PRIVATE_KEY="$(cat ~/Downloads/*.private-key.pem)"
|
||||
|
||||
1. Deploy the app to heroku with `git push`:
|
||||
1. Deploy the app to heroku with `git push`:
|
||||
|
||||
$ git push heroku master
|
||||
$ git push heroku main
|
||||
...
|
||||
-----> Node.js app detected
|
||||
...
|
||||
-----> Launching... done
|
||||
http://arcane-lowlands-8408.herokuapp.com deployed to Heroku
|
||||
|
||||
1. Your app should be up and running! To verify that your app
|
||||
is receiving webhook data, you can tail your app's logs:
|
||||
1. Your app should be up and running! To verify that your app
|
||||
is receiving webhook data, you can tail your app's logs:
|
||||
|
||||
$ heroku config:set LOG_LEVEL=trace
|
||||
$ heroku logs --tail
|
||||
$ heroku config:set LOG_LEVEL=trace
|
||||
$ heroku logs --tail
|
||||
|
||||
### Now
|
||||
### As serverless function
|
||||
|
||||
Zeit [Now](http://zeit.co/now) is a great service for running Probot apps. After [creating the GitHub App](#create-the-github-app):
|
||||
When deploying your Probot app to a serverless/function environment, you don't need to worry about handling the http webhook requests coming from GitHub, the platform takes care of that. In many cases you can use [`createNodeMiddleware`](/docs/development/#use-createNodeMiddleware) directly, e.g. for Vercel or Google Cloud Function.
|
||||
|
||||
1. Install the now CLI with `npm i -g now`
|
||||
```js
|
||||
import { Probot, createProbot } from "probot";
|
||||
import { createMyMiddleware } from "my-probot-middleware";
|
||||
import myApp from "./my-app.js";
|
||||
|
||||
1. Clone the app that you want to deploy. e.g. `git clone https://github.com/probot/stale`
|
||||
export default createMyMiddleware(myApp, { probot: createProbot() });
|
||||
```
|
||||
|
||||
1. Run `now` to deploy, replacing the `APP_ID` and `WEBHOOK_SECRET` with the values for those variables, and setting the `PRIVATE_KEY`:
|
||||
For other environments such as AWS Lambda, Netlify Functions or GitHub Actions, you can use one of [Probot's adapters](https://github.com/probot/?q=adapter).
|
||||
|
||||
$ now -e APP_ID=aaa \
|
||||
-e WEBHOOK_SECRET=bbb \
|
||||
-e NODE_ENV=production \
|
||||
-e PRIVATE_KEY="$(cat ~/Downloads/*.private-key.pem | base64)"
|
||||
#### AWS Lambda
|
||||
|
||||
**NOTE**: Add `-e LOG_LEVEL=trace` to get verbose logging, or add `-e LOG_LEVEL=info` instead to show less details.
|
||||
```js
|
||||
// handler.js
|
||||
import {
|
||||
createLambdaFunction,
|
||||
createProbot,
|
||||
} from "@probot/adapter-aws-lambda-serverless";
|
||||
import appFn from "./app.js";
|
||||
|
||||
1. Once the deploy is started, go back to your [app settings page](https://github.com/settings/apps) and update the **Webhook URL** to the URL of your deployment (which `now` has kindly copied to your clipboard).
|
||||
export const webhooks = createLambdaFunction(appFn, {
|
||||
probot: createProbot(),
|
||||
});
|
||||
```
|
||||
|
||||
1. Your app should be up and running! For long term use, create an alias for your app. After making an alias, you can swap to new deploy URLs with no downtime.
|
||||
Learn more
|
||||
|
||||
$ now alias set https://your-generated-url.now.sh https://a-fancier-url.now.sh
|
||||
- Probot's official adapter for AWS Lambda using the Serverless framework: [@probot/adapter-aws-lambda-serverless](https://github.com/probot/adapter-aws-lambda-serverless#readme)
|
||||
|
||||
1. You can also keep your app running forever, with instant response to webhooks with:
|
||||
Examples
|
||||
|
||||
$ now scale https://a-fancier-url.now.sh 1
|
||||
- Probot's "Hello, world!" example deployed to AWS Lambda: [probot/example-aws-lambda-serverless](https://github.com/probot/example-aws-lambda-serverless/#readme)
|
||||
- Issue labeler bot deployed to AWS Lambda: [riyadhalnur/issuelabeler](https://github.com/riyadhalnur/issuelabeler#issuelabeler)
|
||||
- Auto-Me-Bot is deployed to AWS Lambda without using the _serverless_ framework and adapter: [TomerFi/auto-me-bot](https://github.com/TomerFi/auto-me-bot)
|
||||
|
||||
Please add yours!
|
||||
|
||||
#### Azure Functions
|
||||
|
||||
```js
|
||||
// ProbotFunction/index.js
|
||||
import {
|
||||
createProbot,
|
||||
createAzureFunction,
|
||||
} from "@probot/adapter-azure-functions";
|
||||
import app from "../app.js";
|
||||
|
||||
export default createAzureFunction(app, { probot: createProbot() });
|
||||
```
|
||||
|
||||
Learn more
|
||||
|
||||
- Probot's official adapter for Azure functions: [@probot/adapter-azure-functions](https://github.com/probot/adapter-azure-functions#readme)
|
||||
|
||||
Examples
|
||||
|
||||
- Probot's "Hello, world!" example deployed to Azure functions: [probot/example-azure-function](https://github.com/probot/example-azure-function/#readme)
|
||||
|
||||
Please add yours!
|
||||
|
||||
#### Google Cloud Functions
|
||||
|
||||
```js
|
||||
// function.js
|
||||
import { createNodeMiddleware, createProbot } from "probot";
|
||||
import app from "./app.js";
|
||||
|
||||
exports.probotApp = createNodeMiddleware(app, { probot: createProbot() });
|
||||
```
|
||||
|
||||
Examples
|
||||
|
||||
- Probot's "Hello, world!" example deployed to Google Cloud Functions: [probot/example-google-cloud-function](https://github.com/probot/example-google-cloud-function#readme)
|
||||
|
||||
Please add yours!
|
||||
|
||||
#### GitHub Actions
|
||||
|
||||
```js
|
||||
import { run } from "@probot/adapter-github-actions";
|
||||
import app from "./app.js";
|
||||
|
||||
run(app);
|
||||
```
|
||||
|
||||
Learn more
|
||||
|
||||
- Probot's official adapter for GitHub Actions: [@probot/adapter-github-actions](https://github.com/probot/adapter-github-actions#readme)
|
||||
|
||||
Examples
|
||||
|
||||
- Probot's "Hello, world!" example deployed as a GitHub Action: [probot/example-github-action](https://github.com/probot/example-github-action/#readme)
|
||||
|
||||
Please add yours!
|
||||
|
||||
#### Begin
|
||||
|
||||
[Begin](https://begin.com/) is a service to deploy serverless applications build using the [Architect](https://arc.codes/) to AWS.
|
||||
|
||||
1. Add the `@http` pragma to your `app.arc` file
|
||||
|
||||
```
|
||||
@app
|
||||
my-app-name
|
||||
|
||||
@http
|
||||
post /api/github/webhooks
|
||||
```
|
||||
|
||||
2. Make sure to [configure your app](../confinguration) using environment variables
|
||||
|
||||
3. Create the `src/http/post-api-github-webhooks` folder with the following files
|
||||
|
||||
```js
|
||||
{
|
||||
"name": "http-post-api-github-webhooks",
|
||||
"dependencies": {}
|
||||
}
|
||||
```
|
||||
|
||||
in the new directory, install the `probot` and `@architect/functions`
|
||||
|
||||
```
|
||||
cd src/http/post-api-github-webhooks
|
||||
npm install probot @architect/functions
|
||||
```
|
||||
|
||||
4. Create `src/http/post-api-github-webhooks/app.js` with your Probot application function, e.g.
|
||||
|
||||
```
|
||||
/**
|
||||
* @param {import('probot').Probot} app
|
||||
*/
|
||||
export default (app) => {
|
||||
app.log("Yay! The app was loaded!");
|
||||
|
||||
app.on("issues.opened", async (context) => {
|
||||
return context.octokit.issues.createComment(
|
||||
context.issue({ body: "Hello, World!" })
|
||||
);
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
5. Create `src/http/post-api-github-webhooks/index.js` with the request handler. See [/probot/example-begin/src/http/post-api-github-webhooks/index.js](https://github.com/probot/example-begin/blob/main/src/http/post-api-github-webhooks/index.js) for an example.
|
||||
|
||||
Examples
|
||||
|
||||
- [probot/example-begin](https://github.com/probot/example-begin#readme)
|
||||
|
||||
Please add yours!
|
||||
|
||||
#### Vercel
|
||||
|
||||
```js
|
||||
// api/github/webhooks/index.js
|
||||
import { createNodeMiddleware, createProbot } from "probot";
|
||||
|
||||
import app from "../../../app.js";
|
||||
|
||||
export default createNodeMiddleware(app, {
|
||||
probot: createProbot(),
|
||||
webhooksPath: "/api/github/webhooks",
|
||||
});
|
||||
```
|
||||
|
||||
**Important:** Set `NODEJS_HELPERS` environment variable to `0` in order to prevent Vercel from parsing the response body.
|
||||
See [Disable Helpers](https://vercel.com/docs/functions/runtimes/node-js#disabling-helpers-for-node.js) for detail.
|
||||
|
||||
Examples
|
||||
|
||||
- [probot/example-vercel](https://github.com/probot/example-vercel#readme)
|
||||
- [wip/app](https://github.com/wip/app#readme)
|
||||
- [all-contributors/app](https://github.com/all-contributors/app#readme)
|
||||
- [probot-nextjs-starter](https://github.com/maximousblk/probot-nextjs-starter#readme)
|
||||
|
||||
Please add yours!
|
||||
|
||||
#### Netlify Functions
|
||||
|
||||
[Netlify Functions](https://www.netlify.com/products/functions/) are deployed on AWS by Netlify itself. So we can use `@probot/adapter-aws-lambda-serverless` adapter for Netlify Functions as well.
|
||||
|
||||
```js
|
||||
// functions/index.js
|
||||
import {
|
||||
createLambdaFunction,
|
||||
createProbot,
|
||||
} from "@probot/adapter-aws-lambda-serverless";
|
||||
import appFn from "../src/app";
|
||||
|
||||
export const handler = createLambdaFunction(appFn, {
|
||||
probot: createProbot(),
|
||||
});
|
||||
```
|
||||
|
||||
## Share the app
|
||||
|
||||
The Probot website includes a list of [featured apps](https://probot.github.io/apps). Consider [adding your app to the website](https://github.com/probot/probot.github.io/blob/master/CONTRIBUTING.md#adding-your-app
|
||||
) so others can discover and use it.
|
||||
The Probot website includes a list of [featured apps](https://probot.github.io/apps). Consider [adding your app to the website](https://github.com/probot/probot.github.io/blob/master/CONTRIBUTING.md#adding-your-app) so others can discover and use it.
|
||||
|
||||
## Combining apps
|
||||
|
||||
|
|
@ -151,20 +338,28 @@ To deploy multiple apps in one instance, create a new app that has the existing
|
|||
},
|
||||
"scripts": {
|
||||
"start": "probot run"
|
||||
},
|
||||
"probot": {
|
||||
"apps": [
|
||||
"probot-autoresponder",
|
||||
"probot-settings"
|
||||
]
|
||||
}
|
||||
},
|
||||
"probot": {
|
||||
"apps": ["probot-autoresponder", "probot-settings"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that this feature is only supported when [run as Node app](#as-node-app). For serverless/function deployments, create a new Probot app that combines others programmatically
|
||||
|
||||
```js
|
||||
// app.js
|
||||
import autoresponder from "probot-autoresponder";
|
||||
import settings from "probot-settings";
|
||||
|
||||
export default async (app, options) => {
|
||||
await autoresponder(app, options);
|
||||
await settings(app, options);
|
||||
};
|
||||
```
|
||||
|
||||
## Error tracking
|
||||
|
||||
Probot comes bundled with a client for the [Sentry](https://github.com/getsentry/sentry) exception tracking platform. To enable Sentry:
|
||||
Probot logs messages using [pino](https://getpino.io/). There is a growing number of tools that consume these logs and send them to error tracking services: https://getpino.io/#/docs/transports.
|
||||
|
||||
1. [Install Sentry from Marketplace](https://github.com/marketplace/sentry) (with [10k events/month free](https://github.com/marketplace/sentry/plan/MDIyOk1hcmtldHBsYWNlTGlzdGluZ1BsYW40Nw==#pricing-and-setup)) or [host your own instance](https://github.com/getsentry/sentry) (Students can get [extra Sentry credit](https://education.github.com/pack))
|
||||
2. Follow the setup instructions to find your DSN.
|
||||
3. Set the `SENTRY_DSN` environment variable with the DSN you retrieved.
|
||||
By default, Probot can send errors to [Sentry](https://sentry.io/) using its own transport [`@probot/pino`](https://github.com/probot/pino/#readme). Set the `SENTRY_DSN` environment variable to enable it.
|
||||
|
|
|
|||
|
|
@ -1,44 +1,86 @@
|
|||
---
|
||||
next: docs/webhooks.md
|
||||
next: webhooks
|
||||
title: Developing an app
|
||||
---
|
||||
|
||||
# Developing an app
|
||||
|
||||
To develop a Probot app, you will first need a recent version of [Node.js](https://nodejs.org/) installed. Open a terminal and run `node -v` to verify that it is installed and is at least 8.3.0 or later. Otherwise, [install the latest version](https://nodejs.org/).
|
||||
To develop a Probot app, you will first need a recent version of [Node.js](https://nodejs.org/) installed. Open a terminal and run `node -v` to verify that it is installed and is at least 18.0.0 or later. Otherwise, [install the latest version](https://nodejs.org/).
|
||||
|
||||
**Contents:**
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
- [Generating a new app](#generating-a-new-app)
|
||||
- [Running the app locally](#running-the-app-locally)
|
||||
- [Configuring a GitHub App](#configuring-a-github-app)
|
||||
- [Manually Configuring a GitHub App](#manually-configuring-a-github-app)
|
||||
- [Installing the app on a repository](#installing-the-app-on-a-repository)
|
||||
- [Debugging](#debugging)
|
||||
- [Run Probot programmatically](#run-probot-programmatically)
|
||||
- [Use run](#use-run)
|
||||
- [Use server](#use-server)
|
||||
- [Use createNodeMiddleware](#use-createnodemiddleware)
|
||||
- [Use probot](#use-probot)
|
||||
- [Use Probot's Octokit](#use-probots-octokit)
|
||||
|
||||
<!-- tocstop -->
|
||||
|
||||
## Generating a new app
|
||||
|
||||
[create-probot-app](https://github.com/probot/create-probot-app) is the best way to start building a new app. It will generate a new app with everything you need to get started and run your app in production.
|
||||
|
||||
To get started, run one of these commands:
|
||||
To get started, run:
|
||||
|
||||
- If you're using npm: `$ npx create-probot-app [--typescript] my-first-app`
|
||||
- or, if you're using Yarn: `$ yarn create probot-app [--typescript] my-first-app`
|
||||
```
|
||||
npx create-probot-app my-first-app
|
||||
```
|
||||
|
||||
This will ask you a series of questions about your app, which should look something like this:
|
||||
|
||||
```
|
||||
Let's create a Probot app!
|
||||
? App name: my-first-app
|
||||
? Description of app: A "Hello World" GitHub App built with Probot
|
||||
? Description of app: A 'Hello World' GitHub App built with Probot.
|
||||
? Author's full name: Katie Horne
|
||||
? Author's email address: katie@auth0.com
|
||||
? Homepage:
|
||||
? GitHub user or org name: khorne3
|
||||
? Repository name: my-first-app
|
||||
created files...
|
||||
? Which template would you like to use? (Use arrow keys)
|
||||
❯ basic-js
|
||||
basic-ts (use this one for TypeScript support)
|
||||
checks-js
|
||||
git-data-js
|
||||
deploy-js
|
||||
|
||||
Finished scaffolding files!
|
||||
|
||||
Installing Node dependencies!
|
||||
Installing dependencies. This may take a few minutes...
|
||||
|
||||
Done! Enjoy building your Probot app!
|
||||
Successfully created my-first-app.
|
||||
|
||||
Begin using your app with:
|
||||
cd my-first-app
|
||||
npm start
|
||||
|
||||
View your app's README for more usage instructions.
|
||||
|
||||
Visit the Probot docs:
|
||||
https://probot.github.io/docs/
|
||||
|
||||
Get help from the community:
|
||||
https://probot.github.io/community/
|
||||
|
||||
Enjoy building your Probot app!
|
||||
```
|
||||
|
||||
The most important files note here are `index.js`, which is where the code for your app will go, and `package.json`, which makes this a standard [npm module](https://docs.npmjs.com/files/package.json).
|
||||
The most important files created are `index.js`, which is where the code for your app will go, and `package.json`, which makes the app a standard [npm module](https://docs.npmjs.com/files/package.json).
|
||||
|
||||
## Running the app locally
|
||||
|
||||
Now you're ready to run the app on your local machine. Run `npm run dev` to start the server:
|
||||
Now you're ready to run the app on your local machine. Run `npm start` to start the server:
|
||||
|
||||
> Note: If you're building a TypeScript app, be sure to run `npm run build` first!
|
||||
|
||||
```
|
||||
> testerino@1.0.0 dev /Users/hiimbex/Desktop/testerino
|
||||
|
|
@ -50,58 +92,204 @@ Now you're ready to run the app on your local machine. Run `npm run dev` to star
|
|||
[nodemon] starting `npm start`
|
||||
|
||||
> testerino@1.0.0 start /Users/hiimbex/Desktop/testerino
|
||||
> my-first-app@1.0.0 start /private/var/folders/hs/x9qtfmvn1lz1sgml9q21h7k80000gn/T/tmp.bgzYr6j1/my-first-app
|
||||
> probot run ./index.js
|
||||
|
||||
01:57:22.365Z INFO probot:
|
||||
|
||||
Welcome to Probot! Go to http://localhost:3000 to get started.
|
||||
|
||||
01:57:22.428Z INFO probot: Forwarding https://smee.io/1S10MsoRSjZKWKMt to http://localhost:3000/
|
||||
01:57:22.431Z INFO probot: Listening on http://localhost:3000
|
||||
01:57:22.564Z INFO probot: Connected https://smee.io/1S10MsoRSjZKWKMt
|
||||
INFO (probot):
|
||||
INFO (probot): Welcome to Probot!
|
||||
INFO (probot): Probot is in setup mode, webhooks cannot be received and
|
||||
INFO (probot): custom routes will not work until APP_ID and PRIVATE_KEY
|
||||
INFO (probot): are configured in .env.
|
||||
INFO (probot): Please follow the instructions at http://localhost:3000 to configure .env.
|
||||
INFO (probot): Once you are done, restart the server.
|
||||
INFO (probot):
|
||||
INFO (server): Running Probot v11.0.5 (Node.js: v15.5.1)
|
||||
INFO (server): Listening on http://localhost:3000
|
||||
```
|
||||
|
||||
The `dev` script will start your app using [nodemon](https://github.com/remy/nodemon#nodemon), which will watch for any files changes in your local development environment and automatically restart the server.
|
||||
|
||||
## Configuring a GitHub App
|
||||
|
||||
To automatically configure your GitHub App, follow these steps:
|
||||
|
||||
1. Run the app locally by running `npm run dev`.
|
||||
1. Next follow instructions to visit localhost:3000 (or your custom Glitch URL).
|
||||
1. You should see something like this: <img width="625" alt="screen shot 2018-09-25 at 10 01 28 pm" src="https://user-images.githubusercontent.com/13410355/46052950-a19e2900-c10e-11e8-9e7e-0c803b8ca35c.png">
|
||||
1. Run the app locally by running `npm start` in your terminal.
|
||||
1. Next follow instructions to visit [http://localhost:3000](http://localhost:3000) (or your custom Glitch URL).
|
||||
1. You should see something like this: 
|
||||
1. Go ahead and click the **Register a GitHub App** button.
|
||||
1. Next you'll get to decide on an app name that isn't already taken.
|
||||
1. After registering your GitHub App, you'll be redirected to install the app on any repos. At the same time, you can check your local `.env` and notice it will be populated with values GitHub sends us in the course of that redirect.
|
||||
1. Install the app on a test repo and try triggering a webhook to activate the bot!
|
||||
1. Next, you'll get to decide on an app name that isn't already taken. Note: if you see a message "Name is already in use" although no such app exists, it means that a GitHub organization with that name exists and cannot be used as an app name.
|
||||
1. After registering your GitHub App, you'll be redirected to install the app on any repositories. At the same time, you can check your local `.env` and notice it will be populated with values GitHub sends us in the course of that redirect.
|
||||
1. Restart the server in your terminal (press <kbd>ctrl</kbd> + <kbd>c</kbd> to stop the server)
|
||||
1. Install the app on a test repository.
|
||||
1. Try triggering a webhook to activate the bot!
|
||||
1. You're all set! Head down to [Debugging](#debugging) to learn more about developing your Probot App.
|
||||
|
||||
GitHub App Manifests--otherwise known as easy app creation--make it simple to generate all the settings necessary for a GitHub App. This process abstracts the [Configuring a GitHub App](#configuring-a-github-app) section. You can learn more about how GitHub App Manifests work and how to change your settings for one via the [GitHub Developer Docs](https://developer.github.com/apps/building-github-apps/creating-github-apps-from-a-manifest/).
|
||||
GitHub App Manifests--otherwise known as easy app creation--make it simple to generate all the settings necessary for a GitHub App. This process abstracts the [Configuring a GitHub App](#configuring-a-github-app) section. You can learn more about how GitHub App Manifests work and how to change your settings for one via the [GitHub Developer Docs](https://docs.github.com/en/developers/apps/creating-a-github-app-from-a-manifest/).
|
||||
|
||||
## Manually Configuring a GitHub App
|
||||
|
||||
> If you created an App with a manifest, you can skip this section; your app is already configured! If you ever need to edit those settings, you can visit `https://github.com/settings/apps/your-app-name`
|
||||
> If you created an App with a manifest, you can skip this section; your app is already configured! If you ever need to edit those settings, you can visit `https://github.com/settings/apps/[your-app-name]`
|
||||
|
||||
To run your app in development, you will need to configure a GitHub App to deliver webhooks to your local machine.
|
||||
To run your app in development, you will need to configure a GitHub App's `APP_ID`, `PRIVATE_KEY`, `WEBHOOK_SECRET`, and `WEBHOOK_PROXY_URL` in order to receive webhooks to your local machine.
|
||||
|
||||
1. On your local machine, copy `.env.example` to `.env`.
|
||||
1. Go to [smee.io](https://smee.io) and click **Start a new channel**. Set `WEBHOOK_PROXY_URL` in `.env` to the URL that you are redirected to.
|
||||
1. On your local machine, copy `.env.example` to `.env` in the same directory. We're going to be changing a few things in this new file.
|
||||
1. Go to [smee.io](https://smee.io) and click **Start a new channel**. Set `WEBHOOK_PROXY_URL` to the URL that you are redirected to.<br/>
|
||||
E.g. `https://smee.io/AbCd1234EfGh5678`
|
||||
1. [Create a new GitHub App](https://github.com/settings/apps/new) with:
|
||||
- **Webhook URL**: Use your `WEBHOOK_PROXY_URL` from the previous step.
|
||||
- **Webhook Secret:** `development` (Note: For optimal security, Probot apps **require** this secret be set, even though it's optional on GitHub.).
|
||||
- **Permissions & events** is located lower down the page and will depend on what data you want your app to have access to. Note: if, for example, you only enable issue events, you will not be able to listen on pull request webhooks with your app. However, for development we recommend enabling everything.
|
||||
1. Download the private key and move it to your project's directory. As long as it's in the root of your project, Probot will find it automatically regardless of the filename.
|
||||
1. Edit `.env` and set `APP_ID` to the ID of the app you just created. The App ID can be found in your app settings page here <img width="1048" alt="screen shot 2017-08-20 at 8 31 31 am" src="https://user-images.githubusercontent.com/5713670/42248717-f6bf4f10-7edb-11e8-8dd5-387181c771bc.png">
|
||||
- **Webhook URL**: Use the same `WEBHOOK_PROXY_URL` from the previous step.
|
||||
- **Webhook Secret:** `development`, or whatever you set for this in your `.env` file. (Note: For optimal security, Probot apps **require** this secret be set, even though it's optional on GitHub.).
|
||||
- **Permissions & events** is located lower down the page and will depend on what data you want your app to have access to. Note: if, for example, you only enable issue events, you will not be able to listen on pull request webhooks with your app. However, for development, we recommend enabling everything.
|
||||
1. You must now set `APP_ID` in your `.env` to the ID of the app you just created. The App ID can be found in on top of your apps settings page.
|
||||
1. Finally, generate and download a private key file (using the button seen in the image above), then move it to your project's directory. As long as it's in the root of your project, Probot will find it automatically regardless of the filename.
|
||||
|
||||
## Installing the app on a repo
|
||||
For more information about these and other available keys, head over to the [environmental configuration documentation](https://probot.github.io/docs/configuration/).
|
||||
|
||||
## Installing the app on a repository
|
||||
|
||||
You'll need to create a test repository and install your app by clicking the "Install" button on the settings page of your app, e.g. `https://github.com/apps/your-app`
|
||||
|
||||
**Other available scripts**
|
||||
* `$ npm start` to start your app without watching files.
|
||||
* `$ npm run lint` to lint your code using [standard](https://www.npmjs.com/package/standard).
|
||||
|
||||
## Debugging
|
||||
|
||||
1. Always run `$ npm install` and restart the server if `package.json` has changed.
|
||||
1. To turn on verbose logging, start server by running: `$ LOG_LEVEL=trace npm start`
|
||||
1. To turn on verbose logging, start the server by running: `$ LOG_LEVEL=trace npm start`
|
||||
|
||||
## Run Probot programmatically
|
||||
|
||||
### Use run
|
||||
|
||||
If you take a look to the `npm start` script, this is what it runs: `probot run ./index.js`. This is nice, but you sometimes need more control over how your Node.js application is executed. For example, if you want to use custom V8 flags, `ts-node`, etc. you need more flexibility. In those cases there's a simple way of executing your probot application programmatically:
|
||||
|
||||
```js
|
||||
// main.js
|
||||
import { run } from "probot";
|
||||
import app from "./index.js";
|
||||
|
||||
// pass a probot app function
|
||||
run(app);
|
||||
```
|
||||
|
||||
You can also set your own [`Octokit` constructor](https://github.com/octokit/core.js) with custom plugins and a custom logger while still reading default options from `.env`:
|
||||
|
||||
```js
|
||||
// main.js
|
||||
import { run } from "probot";
|
||||
import app from "./index.js";
|
||||
|
||||
// pass a probot app function by overriding Probot options
|
||||
run(app, {
|
||||
Octokit: ProbotOctokit.plugin(myPlugin),
|
||||
log: pino(),
|
||||
});
|
||||
```
|
||||
|
||||
Now you can run `main.js` however you want.
|
||||
|
||||
### Use server
|
||||
|
||||
The [`run`](https://github.com/probot/probot/blob/master/src/run.ts) function that gets executed when running `probot run ./index.js` does two main things
|
||||
|
||||
1. Read configuration from environment variables and local files
|
||||
2. Start a [`Server`](https://probot.github.io/api/latest/classes/server_server.Server.html) instance
|
||||
|
||||
If you need more control over the Server instance, you can use the `Server` class directly:
|
||||
|
||||
```js
|
||||
import { Server, Probot } from "probot";
|
||||
import app from "./index.js";
|
||||
|
||||
async function startServer() {
|
||||
const server = new Server({
|
||||
Probot: Probot.defaults({
|
||||
appId: 123,
|
||||
privateKey: "content of your *.pem file here",
|
||||
secret: "webhooksecret123",
|
||||
}),
|
||||
});
|
||||
|
||||
await server.load(app);
|
||||
|
||||
server.start();
|
||||
}
|
||||
```
|
||||
|
||||
The `server` instance gives you access to the express app instance (`server.expressApp`) as well as the [`Probot`](https://probot.github.io/api/latest/classes/probot.Probot.html) instance (`server.probotApp`).
|
||||
|
||||
### Use createNodeMiddleware
|
||||
|
||||
If you have your own server or deploy to a serverless environment that supports loading [Express-style middleware](https://expressjs.com/en/guide/using-middleware.html) or Node's http middleware (`(request, response) => { ... }`), you can use `createNodeMiddleware`.
|
||||
|
||||
```js
|
||||
import { createNodeMiddleware, Probot } from "probot";
|
||||
import app from "./index.js";
|
||||
|
||||
const probot = new Probot({
|
||||
appId: 123,
|
||||
privateKey: "content of your *.pem file here",
|
||||
secret: "webhooksecret123",
|
||||
});
|
||||
|
||||
export default createNodeMiddleware(app, { probot });
|
||||
```
|
||||
|
||||
If you want to read probot's configuration from the same environment variables as [`run`](#run), use the [`createProbot`](https://probot.github.io/api/latest/index.html#createprobot) export
|
||||
|
||||
```js
|
||||
import { createNodeMiddleware, createProbot } from "probot";
|
||||
import app from "./index.js";
|
||||
|
||||
export default createNodeMiddleware(app, { probot: createProbot() });
|
||||
```
|
||||
|
||||
By default, `createNodeMiddleware()` uses `/api/github/webhooks` as the webhook endpoint. To customize this behaviour, you can use the `webhooksPath` option.
|
||||
|
||||
```js
|
||||
export default createNodeMiddleware(app, {
|
||||
probot: createProbot(),
|
||||
webhooksPath: "/path/to/webhook/endpoint",
|
||||
});
|
||||
```
|
||||
|
||||
### Use probot
|
||||
|
||||
If you don't use Probot's http handling in order to receive and verify events from GitHub via webhook requests, you can use the [`Probot`](https://probot.github.io/api/latest/classes/probot.Probot.html) class directly.
|
||||
|
||||
```js
|
||||
import { Probot } from "probot";
|
||||
import app from "./index.js";
|
||||
|
||||
async function example() {
|
||||
const probot = new Probot({
|
||||
appId: 123,
|
||||
privateKey: "content of your *.pem file here",
|
||||
secret: "webhooksecret123",
|
||||
});
|
||||
|
||||
await probot.load(app);
|
||||
|
||||
// https://github.com/octokit/webhooks.js/#webhooksreceive
|
||||
probot.webhooks.receive({
|
||||
id: '123',
|
||||
name: 'issues',
|
||||
payload: { ... }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Using the `Probot` class directly is great for [writing tests](/docs/testing) for your Probot app function. It permits you to pass a custom logger to test for log output, disable throttling, request retries, and much more.
|
||||
|
||||
### Use Probot's Octokit
|
||||
|
||||
Sometimes you may need to create your own instance of Probot's internally used [Octokit](https://github.com/octokit/rest.js/#readme) class, for example when using the [OAuth user authorization flow](https://docs.github.com/en/developers/apps/identifying-and-authorizing-users-for-github-apps). You may access the class by importing `ProbotOctokit`:
|
||||
|
||||
```js
|
||||
import { ProbotOctokit } from "probot";
|
||||
|
||||
function myProbotApp(app) {
|
||||
const octokit = new ProbotOctokit({
|
||||
// any options you'd pass to Octokit
|
||||
auth: {
|
||||
token: "yourToken",
|
||||
},
|
||||
// and a logger
|
||||
log: app.log.child({ name: "my-octokit" }),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,47 +1,19 @@
|
|||
---
|
||||
next: docs/configuration.md
|
||||
next: persistence
|
||||
title: Extensions
|
||||
---
|
||||
|
||||
# Extensions
|
||||
|
||||
While Probot doesn't have an official extension API (yet), there are a handful of reusable utilities that have been extracted from existing apps.
|
||||
While Probot doesn't have an official extension API, there are a handful of reusable utilities that have been extracted from existing apps.
|
||||
|
||||
## Config
|
||||
<!-- toc -->
|
||||
|
||||
[probot-config](https://github.com/getsentry/probot-config) is an extension for sharing configs between repositories.
|
||||
- [Commands](#commands)
|
||||
- [Metadata](#metadata)
|
||||
- [Attachments](#attachments)
|
||||
|
||||
|
||||
```js
|
||||
const getConfig = require('probot-config')
|
||||
|
||||
module.exports = app => {
|
||||
app.on('push', async context => {
|
||||
// Will look for 'test.yml' inside the '.github' folder
|
||||
const config = await getConfig(context, 'test.yml')
|
||||
|
||||
context.log(config, 'Loaded config')
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Use the `_extends` option in your configuration file to extend settings from another repository.
|
||||
|
||||
For example, given `.github/test.yml`:
|
||||
|
||||
```yaml
|
||||
_extends: github-settings
|
||||
# Override values from the extended config or define new values
|
||||
name: myrepo
|
||||
```
|
||||
|
||||
This configuration will be merged with the `.github/test.yml` file from the `github-settings` repository, which might look like this:
|
||||
|
||||
```yaml
|
||||
shared1: will be merged
|
||||
shared2: will also be merged
|
||||
```
|
||||
|
||||
Just put common configuration keys in a repository within your organization. Then reference this repository from config files with the same name.
|
||||
<!-- tocstop -->
|
||||
|
||||
## Commands
|
||||
|
||||
|
|
@ -50,15 +22,15 @@ Just put common configuration keys in a repository within your organization. The
|
|||
For example, users could add labels from comments by typing `/label in-progress`.
|
||||
|
||||
```js
|
||||
const commands = require('probot-commands')
|
||||
import commands from "probot-commands";
|
||||
|
||||
module.exports = app => {
|
||||
export default (app) => {
|
||||
// Type `/label foo, bar` in a comment box for an Issue or Pull Request
|
||||
commands(app, 'label', (context, command) => {
|
||||
const labels = command.arguments.split(/, */)
|
||||
return context.github.issues.addLabels(context.issue({labels}))
|
||||
})
|
||||
}
|
||||
commands(app, "label", (context, command) => {
|
||||
const labels = command.arguments.split(/, */);
|
||||
return context.octokit.issues.addLabels(context.issue({ labels }));
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
## Metadata
|
||||
|
|
@ -68,60 +40,40 @@ module.exports = app => {
|
|||
For example, here is a contrived app that stores the number of times that comments were edited in a discussion and comments with the edit count when the issue is closed.
|
||||
|
||||
```js
|
||||
const metadata = require('probot-metadata')
|
||||
import metadata from "probot-metadata";
|
||||
|
||||
module.exports = app => {
|
||||
app.on(['issues.edited', 'issue_comment.edited'], async context => {
|
||||
const kv = await metadata(context)
|
||||
await kv.set('edits', await kv.get('edits') || 1)
|
||||
})
|
||||
export default (app) => {
|
||||
app.on(["issues.edited", "issue_comment.edited"], async (context) => {
|
||||
const kv = await metadata(context);
|
||||
await kv.set("edits", (await kv.get("edits")) || 1);
|
||||
});
|
||||
|
||||
app.on('issues.closed', async context => {
|
||||
const edits = await metadata(context).get('edits')
|
||||
context.github.issues.createComment(context.issue({
|
||||
body: `There were ${edits} edits to issues in this thread.`
|
||||
}))
|
||||
})
|
||||
}
|
||||
app.on("issues.closed", async (context) => {
|
||||
const edits = await metadata(context).get("edits");
|
||||
context.octokit.issues.createComment(
|
||||
context.issue({
|
||||
body: `There were ${edits} edits to issues in this thread.`,
|
||||
}),
|
||||
);
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
## Scheduler
|
||||
|
||||
[probot-scheduler](https://github.com/probot/scheduler) is an extension to trigger events on a periodic schedule. It triggers a `schedule.repository` event every hour for each repository it has access to.
|
||||
|
||||
```js
|
||||
const createScheduler = require('probot-scheduler')
|
||||
|
||||
module.exports = app => {
|
||||
createScheduler(app)
|
||||
|
||||
app.on('schedule.repository', context => {
|
||||
// this event is triggered on an interval, which is 1 hr by default
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Check out [stale](https://github.com/probot/stale) to see it in action.
|
||||
|
||||
## Attachments
|
||||
|
||||
[probot-attachments](https://github.com/probot/attachments) adds message attachments to comments on GitHub. This extension should be used any time an app is appending content to user comments.
|
||||
|
||||
```js
|
||||
const attachments = require('probot-attachments')
|
||||
import attachments from "probot-attachments";
|
||||
|
||||
module.exports = app => {
|
||||
app.on('issue_comment.created', context => {
|
||||
export default (app) => {
|
||||
app.on("issue_comment.created", (context) => {
|
||||
return attachments(context).add({
|
||||
'title': 'Hello World',
|
||||
'title_link': 'https://example.com/hello'
|
||||
})
|
||||
})
|
||||
}
|
||||
title: "Hello World",
|
||||
title_link: "https://example.com/hello",
|
||||
});
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
Check out [probot/unfurl](https://github.com/probot/unfurl) to see it in action.
|
||||
|
||||
## Community Created Extensions
|
||||
|
||||
[probot-messages](https://github.com/dessant/probot-messages) was created by [@dessant](https://github.com/dessant) to deliver messages that require user action to ensure the correct operation of the app.
|
||||
|
|
|
|||
|
|
@ -1,40 +1,39 @@
|
|||
---
|
||||
next: docs/http.md
|
||||
next: configuration
|
||||
title: Interacting with GitHub
|
||||
---
|
||||
|
||||
# Interacting with GitHub
|
||||
|
||||
Probot uses [GitHub Apps](https://developer.github.com/apps/). An app is a first-class actor on GitHub, like a user (e.g. [@defunkt](https://github.com/defunkt)) or an organization (e.g. [@github](https://github.com/github)). The app is given access to a repository or repositories by being "installed" on a user or organization account and can perform actions through the API like [commenting on an issue](https://developer.github.com/v3/issues/comments/#create-a-comment) or [creating a status](https://developer.github.com/v3/repos/statuses/#create-a-status).
|
||||
Probot uses [GitHub Apps](https://docs.github.com/developers/apps/) for authorizing requests to GitHub's APIs. A registered GitHub App is a first-class actor on GitHub, like a user (e.g. [@bkeepers](https://github.com/bkeepers)) or an organization (e.g. [@github](https://github.com/github)). The GitHub App is granted access to all or selected repositories by being "installed" on a user or organization account and can perform actions through the API like [commenting on an issue](https://docs.github.com/rest/reference/issues#create-an-issue-comment) or [creating a status](https://docs.github.com/rest/reference/repos#create-a-commit-status).
|
||||
|
||||
Your app has access to an authenticated GitHub client that can be used to make API calls. It supports both the [GitHub REST API](https://developer.github.com/v3/), and the [GitHub GraphQL API](https://developer.github.com/v4/).
|
||||
Your Probot app has access to an authenticated [Octokit client](https://octokit.github.io/rest.js/) that can be used to make API calls. It supports both the [GitHub REST API](https://docs.github.com/rest), and the [GitHub GraphQL API](https://docs.github.com/graphql).
|
||||
|
||||
## REST API
|
||||
|
||||
`context.github` is an instance of the [`@octokit/rest` Node.js module](https://github.com/octokit/rest.js), which wraps the [GitHub REST API](https://developer.github.com/v3/) and allows you to do almost anything programmatically that you can do through a web browser.
|
||||
`context.octokit` is an instance of the [`@octokit/rest` Node.js module](https://github.com/octokit/rest.js#readme), and allows you to do almost anything programmatically that you can do through a web browser.
|
||||
|
||||
Here is an example of an autoresponder app that comments on opened issues:
|
||||
|
||||
```js
|
||||
module.exports = app => {
|
||||
app.on('issues.opened', async context => {
|
||||
export default (app) => {
|
||||
app.on("issues.opened", async (context) => {
|
||||
// `context` extracts information from the event, which can be passed to
|
||||
// GitHub API calls. This will return:
|
||||
// {owner: 'yourname', repo: 'yourrepo', number: 123, body: 'Hello World!}
|
||||
const params = context.issue({body: 'Hello World!'})
|
||||
// { owner: 'yourname', repo: 'yourrepo', number: 123, body: 'Hello World! }
|
||||
const params = context.issue({ body: "Hello World!" });
|
||||
|
||||
// Post a comment on the issue
|
||||
return context.github.issues.createComment(params)
|
||||
})
|
||||
}
|
||||
return context.octokit.issues.createComment(params);
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
See the [full API docs](https://octokit.github.io/rest.js/) to see all the ways you can interact with GitHub. Some API endpoints are not available on GitHub Apps yet, so check [which ones are available](https://developer.github.com/v3/apps/available-endpoints/) first.
|
||||
See the [full API docs](https://octokit.github.io/rest.js/) to see all the ways you can interact with GitHub. Some API endpoints are not available on GitHub Apps yet, so check [which ones are available](https://docs.github.com/en/rest/overview/endpoints-available-for-github-apps) first.
|
||||
|
||||
## GraphQL API
|
||||
|
||||
> **Heads Up!** GraphQL support in Probot is currently a preview feature and this interface could change in a future releases without notice. [Let us know](https://github.com/probot/probot/issues/476) if you have any feedback or ideas to improve GraphQL support.
|
||||
|
||||
Use `context.github.query` to make requests to the [GitHub GraphQL API](https://developer.github.com/v4/).
|
||||
Use `context.octokit.graphql` to make requests to the [GitHub GraphQL API](https://docs.github.com/en/graphql).
|
||||
|
||||
Here is an example of the same autoresponder app from above that comments on opened issues, but this time with GraphQL:
|
||||
|
||||
|
|
@ -46,30 +45,54 @@ const addComment = `
|
|||
clientMutationId
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
module.exports = app => {
|
||||
app.on('issues.opened', async context => {
|
||||
export default (app) => {
|
||||
app.on("issues.opened", async (context) => {
|
||||
// Post a comment on the issue
|
||||
context.github.query(addComment, {
|
||||
context.octokit.graphql(addComment, {
|
||||
id: context.payload.issue.node_id,
|
||||
body: 'Hello World'
|
||||
})
|
||||
})
|
||||
}
|
||||
body: "Hello World",
|
||||
});
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
Check out the [GitHub GraphQL API docs](https://developer.github.com/v4/) to learn more.
|
||||
The options in the 2nd argument will be passed as variables to the query. You can pass custom headers by using the `headers` key:
|
||||
|
||||
```js
|
||||
// GraphQL query to pin an issue
|
||||
const pinIssue = `
|
||||
mutation comment($id: ID!) {
|
||||
pinIssue(input: {subjectId: $id}) {
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default (app) => {
|
||||
app.on("issues.opened", async (context) => {
|
||||
context.octokit.graphql(pinIssue, {
|
||||
id: context.payload.issue.node_id,
|
||||
headers: {
|
||||
accept: "application/vnd.github.elektra-preview+json",
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
Check out the [GitHub GraphQL API docs](https://docs.github.com/en/graphql) to learn more.
|
||||
|
||||
## Unauthenticated Events
|
||||
|
||||
When [receiving webhook events](./webhooks.md), `context.github` is _usually_ an authenticated client, but there are a few events that are exceptions:
|
||||
When [receiving webhook events](/docs/webhooks), `context.octokit` is _usually_ an authenticated client, but there are a few events that are exceptions:
|
||||
|
||||
- [`installation.deleted`](https://developer.github.com/v3/activity/events/types/#installationevent) - The installation was _just_ deleted, so we can't authenticate as the installation.
|
||||
- [`installation.deleted` & `installation.suspend`](https://docs.github.com/en/developers/webhooks-and-events/webhook-events-and-payloads#installation) - The installation was _just_ deleted or suspended, so we can't authenticate as the installation.
|
||||
|
||||
- [`marketplace_purchase`](https://developer.github.com/v3/activity/events/types/#marketplacepurchaseevent) - The purchase happens before the app is installed on an account.
|
||||
- [`marketplace_purchase`](https://docs.github.com/en/developers/webhooks-and-events/webhook-events-and-payloads#marketplace_purchase) - The purchase happens before the app is installed on an account.
|
||||
|
||||
For these events, `context.github` will be [authenticated as the GitHub App](https://developer.github.com/apps/building-github-apps/authenticating-with-github-apps/#authenticating-as-a-github-app) instead of as a specific installation.
|
||||
For these events, `context.octokit` will be unauthenticated. Attemts to send any requests will fail with an error explaining the circumstances.
|
||||
|
||||
## GitHub Enterprise
|
||||
|
||||
|
|
@ -79,4 +102,10 @@ If you want to run a Probot App against a GitHub Enterprise instance, you'll nee
|
|||
GHE_HOST=fake.github-enterprise.com
|
||||
```
|
||||
|
||||
> GitHub Apps are enabled in GitHub Enterprise 2.12 as an [early access technical preview](https://developer.github.com/enterprise/2.12/apps/) but are generally available in GitHub Enterprise 2.13 and above.
|
||||
When [using Probot programmatically](/docs/development/#run-probot-programmatically), set the `baseUrl` option for the [`Probot`](https://probot.github.io/api/latest/classes/probot.Probot.html) constructor to the full base Url of the REST API
|
||||
|
||||
```js
|
||||
const MyProbot = Probot.defaults({
|
||||
baseUrl: "https://fake.github-enterprise.com/api/v3",
|
||||
});
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,49 +1,50 @@
|
|||
---
|
||||
next: docs/development.md
|
||||
next: development
|
||||
title: Hello World
|
||||
---
|
||||
|
||||
# Hello World
|
||||
# Hello world
|
||||
|
||||
A Probot app is just a [Node.js module](https://nodejs.org/api/modules.html) that exports a function:
|
||||
|
||||
```js
|
||||
module.exports = app => {
|
||||
export default (app) => {
|
||||
// your code here
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
The `app` parameter is an instance of [`Application`](https://probot.github.io/api/latest/classes/application.html) and gives you access to all of the GitHub goodness.
|
||||
The `app` parameter is an instance of [`Probot`](https://probot.github.io/api/latest/classes/probot.Probot.html) and gives you access to all of the GitHub goodness.
|
||||
|
||||
`app.on` will listen for any [webhook events triggered by GitHub](./webhooks.md), which will notify you when anything interesting happens on GitHub that your app wants to know about.
|
||||
`app.on` will listen for any [webhook events triggered by GitHub](/docs/webhooks/), which will notify you when anything interesting happens on GitHub that your app wants to know about.
|
||||
|
||||
```js
|
||||
module.exports = app => {
|
||||
app.on('issues.opened', async context => {
|
||||
export default (app) => {
|
||||
app.on("issues.opened", async (context) => {
|
||||
// A new issue was opened, what should we do with it?
|
||||
context.log(context.payload)
|
||||
})
|
||||
}
|
||||
context.log.info(context.payload);
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
The `context` passed to the event handler includes everything about the event that was triggered, as well as some helpful properties for doing something useful in response to the event. `context.github` is an authenticated GitHub client that can be used to [make API calls](./github-api.md), and allows you to do almost anything programmatically that you can do through a web browser on GitHub.
|
||||
The [`context`](https://probot.github.io/api/latest/classes/context.Context.html) passed to the event handler includes everything about the event that was triggered, as well as some helpful properties for doing something useful in response to the event. `context.octokit` is an authenticated GitHub client that can be used to [make REST API and GraphQL calls](/docs/github-api), and allows you to do almost anything programmatically that you can do through a web browser on GitHub.
|
||||
|
||||
Here is an example of an autoresponder app that comments on opened issues:
|
||||
|
||||
```js
|
||||
module.exports = app => {
|
||||
app.on('issues.opened', async context => {
|
||||
export default (app) => {
|
||||
app.on("issues.opened", async (context) => {
|
||||
// `context` extracts information from the event, which can be passed to
|
||||
// GitHub API calls. This will return:
|
||||
// {owner: 'yourname', repo: 'yourrepo', number: 123, body: 'Hello World!}
|
||||
const params = context.issue({body: 'Hello World!'})
|
||||
// { owner: 'yourname', repo: 'yourrepo', number: 123, body: 'Hello World !}
|
||||
const params = context.issue({ body: "Hello World!" });
|
||||
|
||||
// Post a comment on the issue
|
||||
return context.github.issues.createComment(params)
|
||||
})
|
||||
}
|
||||
return context.octokit.issues.createComment(params);
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
To get started, you can use the instructions for [Developing an App](https://probot.github.io/docs/development/) or remix this 'Hello World' project on Glitch:
|
||||
To get started, you can use the instructions for [Developing an App](/docs/development/) or remix this 'Hello World' project on Glitch:
|
||||
|
||||
[](https://glitch.com/edit/#!/remix/probot-hello-world)
|
||||
|
||||
|
|
|
|||
29
docs/http.md
29
docs/http.md
|
|
@ -1,28 +1,33 @@
|
|||
---
|
||||
next: docs/simulating-webhooks.md
|
||||
next: pagination
|
||||
title: HTTP routes
|
||||
---
|
||||
|
||||
# HTTP Routes
|
||||
# HTTP routes
|
||||
|
||||
Calling `app.route('/my-app')` will return an [express](http://expressjs.com/) router that you can use to expose HTTP endpoints from your app.
|
||||
When starting your app using `probot run ./app.js` or using the [`Server`](/docs/development/#use-server) class, your Probot app function will receive the `options.getRouter` function as its 2nd argument.
|
||||
|
||||
Calling `getRouter('/my-app')` will return an [express](http://expressjs.com/) router that you can use to expose custom HTTP endpoints from your app.
|
||||
|
||||
```js
|
||||
module.exports = app => {
|
||||
import * as express from "express";
|
||||
|
||||
export default (app, { getRouter }) => {
|
||||
// Get an express router to expose new HTTP endpoints
|
||||
const router = app.route('/my-app')
|
||||
const router = getRouter("/my-app");
|
||||
|
||||
// Use any middleware
|
||||
router.use(require('express').static('public'))
|
||||
router.use(express.static("public"));
|
||||
|
||||
// Add a new route
|
||||
router.get('/hello-world', (req, res) => {
|
||||
res.send('Hello World')
|
||||
})
|
||||
}
|
||||
router.get("/hello-world", (req, res) => {
|
||||
res.send("Hello World");
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
Visit https://localhost:3000/my-app/hello-world to access the endpoint.
|
||||
Visit http://localhost:3000/my-app/hello-world to access the endpoint.
|
||||
|
||||
It is strongly encouraged to use the name of your package as the prefix so none of your routes or middleware conflict with other apps. For example, if [`probot/owners`](https://github.com/probot/owners) exposed an endpoint, the app would call `app.route('/owners')` to prefix all endpoints with `/owners`.
|
||||
It is strongly encouraged to use the name of your package as the prefix so none of your routes or middleware conflict with other apps. For example, if [`probot/owners`](https://github.com/probot/owners) exposed an endpoint, the app would call `getRouter('/owners')` to prefix all endpoints with `/owners`.
|
||||
|
||||
See the [express documentation](http://expressjs.com/en/guide/routing.html) for more information.
|
||||
|
|
|
|||
|
|
@ -1,78 +1,79 @@
|
|||
---
|
||||
next: docs/pagination.md
|
||||
next: deployment
|
||||
title: Logging
|
||||
---
|
||||
|
||||
# Logging
|
||||
|
||||
A good logger is a good developer's secret weapon. Probot comes with [bunyan](https://github.com/trentm/node-bunyan), which is a simple and fast logging library that supports some pretty sophisticated logging if you need it (hint: you will).
|
||||
Probot comes with [`pino`](https://getpino.io), a minimal logging library that outputs [newline delimited JSON](http://ndjson.org/). Probot uses [`pino-pretty`](https://github.com/pinojs/pino-pretty) for more readable formatting during development and [`@probot/pino`](https://github.com/probot/pino/) which supports error reporting to Sentry by configuring the `SENTRY_DSN` environment variable.
|
||||
|
||||
`app.log`, `context.log` in an event handler, and `req.log` in an HTTP request are all loggers that you can use to get more information about what your app is doing.
|
||||
|
||||
```js
|
||||
module.exports = app => {
|
||||
app.log('Yay, my app is loaded')
|
||||
export default (app, { getRouter }) => {
|
||||
app.log.info("Yay, my app is loaded");
|
||||
|
||||
app.on('issues.opened', context => {
|
||||
app.on("issues.opened", (context) => {
|
||||
if (context.payload.issue.body.match(/bacon/)) {
|
||||
context.log('This issue is about bacon')
|
||||
context.log.info("This issue is about bacon");
|
||||
} else {
|
||||
context.log('Sadly, this issue is not about bacon')
|
||||
context.log.info("Sadly, this issue is not about bacon");
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
app.route().get('/hello-world', (req, res) => {
|
||||
req.log('Someone is saying hello')
|
||||
})
|
||||
}
|
||||
const router = getRouter("/my-app");
|
||||
router.get("/hello-world", (req, res) => {
|
||||
req.log.info("Someone is saying hello");
|
||||
res.send("Hello World");
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
When you start up your app with `npm start`, You should see your log message appear in your terminal.
|
||||
When you start up your Probot app you should see your log message appear in your terminal.
|
||||
|
||||
<img width="753" alt="" src="https://user-images.githubusercontent.com/173/33234904-d43e7f14-d1f3-11e7-8dcb-6c47e58bd56b.png">
|
||||
|
||||
`app.log` will log messages at the `info` level, which is what your app should use for most relevant messages. Occasionally you will want to log more detailed information that is useful for debugging, but you might not want to see it all the time.
|
||||
Occasionally you will want to log more detailed information that is useful for debugging, but you might not want to see it all the time.
|
||||
|
||||
```js
|
||||
module.exports = app => {
|
||||
export default (app) => {
|
||||
// …
|
||||
app.log.trace('Really low-level logging')
|
||||
app.log.debug({data: 'here'}, 'End-line specs on the rotary girder')
|
||||
app.log.info('Same as using `app.log`')
|
||||
app.log.trace("Really low-level logging");
|
||||
app.log.debug({ data: "here" }, "End-line specs on the rotary girder");
|
||||
app.log.info("Same as using `app.log`");
|
||||
|
||||
const err = new Error('Some error')
|
||||
app.log.warn(err, 'Uh-oh, this may not be good')
|
||||
app.log.error(err, 'Yeah, it was bad')
|
||||
app.log.fatal(err, 'Goodbye, cruel world!')
|
||||
}
|
||||
const err = new Error("Some error");
|
||||
app.log.warn(err, "Uh-oh, this may not be good");
|
||||
app.log.error(err, "Yeah, it was bad");
|
||||
app.log.fatal(err, "Goodbye, cruel world!");
|
||||
};
|
||||
```
|
||||
|
||||
By default, messages that are `info` and above will show in your logs, but you can change it by setting the
|
||||
`LOG_LEVEL` environment variable to `trace`, `debug`, `info`, `warn`, `error`, or `fatal` in `.env` or on the comand line.
|
||||
`LOG_LEVEL` environment variable to `trace`, `debug`, `info`, `warn`, `error`, or `fatal` in `.env` or on the command line.
|
||||
|
||||
```
|
||||
$ LOG_LEVEL=debug npm start
|
||||
```
|
||||
|
||||
### Log formats
|
||||
## Log formats
|
||||
|
||||
In development, it's nice to see simple, colorized, pretty log messages. But those pretty messages don't do you any good when you have 2TB of log files and you're trying to track down why that one-in-a-million bug is happening in production.
|
||||
|
||||
Set `LOG_FORMAT=json` to show log messages as structured JSON, which can then be drained to a logging service that allows querying by various attributes.
|
||||
When `NODE_ENV` is set (as it should be in production), the log output is structured JSON, which can then be drained to a logging service that allows querying by various attributes.
|
||||
|
||||
For example, given this log:
|
||||
|
||||
```js
|
||||
module.exports = app => {
|
||||
app.on('issue_comment.created', context => {
|
||||
context.log('Comment created')
|
||||
})
|
||||
}
|
||||
export default (app) => {
|
||||
app.on("issue_comment.created", (context) => {
|
||||
context.log.info("Comment created");
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
You'll see this output:
|
||||
|
||||
```
|
||||
{"name":"Probot","hostname":"Brandons-MacBook-Pro-3.local","pid":96993,"event":{"id":"afdcb370-c57d-11e7-9b26-0f31120e45b8","event":"issue_comment","action":"created","repository":"robotland/test","installation":13055},"level":20,"msg":"Comment created","time":"2017-11-09T18:42:07.312Z","v":0}
|
||||
{"name":"Probot","hostname":"Brandons-MacBook-Pro-3.local","pid":96993,"event":{"id":"afdcb370-c57d-11e7-9b26-0f31120e45b8","event":"issue_comment","action":"created","repository":"robotland/test","installation":13055},"level":30,"msg":"Comment created","time":"2017-11-09T18:42:07.312Z","v":0}
|
||||
```
|
||||
|
||||
For more about bunyan, check out [this talk about logging in production](http://trentm.com/talk-bunyan-in-prod/).
|
||||
The output can then be piped to one of [pino's transport tools](https://getpino.io/#/docs/transports), or you can build your own.
|
||||
|
|
|
|||
|
|
@ -1,61 +1,86 @@
|
|||
---
|
||||
next: docs/extensions.md
|
||||
next: extensions
|
||||
title: Pagination
|
||||
---
|
||||
|
||||
# Pagination
|
||||
|
||||
Many GitHub API endpoints are paginated. The `github.paginate` method can be used to get each page of the results.
|
||||
Many GitHub API endpoints are paginated. The [`octokit.paginate` method](https://github.com/octokit/plugin-paginate-rest.js/#readme) can be used to get each page of the results.
|
||||
|
||||
```js
|
||||
module.exports = app => {
|
||||
app.on('issues.opened', context => {
|
||||
context.github.paginate(
|
||||
context.github.issues.getAll(context.repo()),
|
||||
res => {
|
||||
res.data.issues.forEach(issue => {
|
||||
context.log('Issue: %s', issue.title)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
export default (app) => {
|
||||
app.on("issues.opened", (context) => {
|
||||
context.octokit.paginate(
|
||||
context.octokit.issues.list,
|
||||
context.repo(),
|
||||
(response) => {
|
||||
response.data.issues.forEach((issue) => {
|
||||
context.log.info("Issue: %s", issue.title);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
## Accumulating pages
|
||||
|
||||
The return value of the `github.paginate` callback will be used to accumulate results.
|
||||
The return value of the `octokit.paginate` callback will be used to accumulate results.
|
||||
|
||||
```js
|
||||
module.exports = app => {
|
||||
app.on('issues.opened', async context => {
|
||||
const allIssues = await context.github.paginate(
|
||||
context.github.issues.getAll(context.repo()),
|
||||
res => res.data
|
||||
)
|
||||
console.log(allIssues)
|
||||
})
|
||||
}
|
||||
export default (app) => {
|
||||
app.on("issues.opened", async (context) => {
|
||||
const allIssues = await context.octokit.paginate(
|
||||
context.octokit.issues.list,
|
||||
context.repo(),
|
||||
(response) => response.data,
|
||||
);
|
||||
console.log(allIssues);
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
## Early exit
|
||||
|
||||
Sometimes it is desirable to stop fetching pages after a certain condition has been satisfied. A second argument, `done`, is provided to the callback and can be used to stop pagination. After `done` is invoked, no additional pages will be fetched.
|
||||
Sometimes it is desirable to stop fetching pages after a certain condition has been satisfied. A second argument, `done`, is provided to the callback and can be used to stop pagination. After `done` is invoked, no additional pages will be fetched, but you still need to return the mapped value for the current page request.
|
||||
|
||||
```js
|
||||
module.exports = app => {
|
||||
app.on('issues.opened', context => {
|
||||
context.github.paginate(
|
||||
context.github.issues.getAll(context.repo()),
|
||||
export default (app) => {
|
||||
app.on("issues.opened", (context) => {
|
||||
context.octokit.paginate(
|
||||
context.octokit.issues.list,
|
||||
context.repo(),
|
||||
(res, done) => {
|
||||
for (let issue of res.data) {
|
||||
if (issue.body.includes('something')) {
|
||||
console.log('found it:', issue)
|
||||
done()
|
||||
break
|
||||
for (const issue of res.data) {
|
||||
if (issue.body.includes("something")) {
|
||||
console.log("found it:", issue);
|
||||
done();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
## Async iterators
|
||||
|
||||
Using async iterators you can iterate through each response
|
||||
|
||||
```js
|
||||
export default (app) => {
|
||||
app.on("issues.opened", async (context) => {
|
||||
for await (const response of octokit.paginate.iterator(
|
||||
context.octokit.issues.list,
|
||||
context.repo(),
|
||||
)) {
|
||||
for (const issue of response.data) {
|
||||
if (issue.body.includes("something")) {
|
||||
return console.log("found it:", issue);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,22 +1,36 @@
|
|||
---
|
||||
next: docs/best-practices.md
|
||||
next: best-practices
|
||||
title: Persistence
|
||||
---
|
||||
|
||||
# Persistence
|
||||
|
||||
Generally speaking, adding database storage or persistence to your Probot App will greatly complicate your life. It's perfectly doable, but for most use-cases you'll be able to manage relevant data within GitHub issues, pull requests and your app's configuration file.
|
||||
|
||||
**Contents:**
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
- [Using GitHub for persistent data](#using-github-for-persistent-data)
|
||||
- [Using a Database](#using-a-database)
|
||||
- [MongoDB (with Mongoose)](#mongodb-with-mongoose)
|
||||
- [MySQL](#mysql)
|
||||
- [Postgres](#postgres)
|
||||
- [Firebase](#firebase)
|
||||
|
||||
<!-- tocstop -->
|
||||
|
||||
## Using GitHub for persistent data
|
||||
|
||||
Probot includes a wrapper for the GitHub API which can enable you to store and manipulate data in the GitHub environment. Since your Probot App will usually be running to supplement work done on GitHub, it makes sense to try to keep everything in one place and avoid extra complications.
|
||||
|
||||
- *Comments:* The API can read, write and delete comments on issues and pull requests.
|
||||
- *Status:* The API can read and change the status of an issue or pull request.
|
||||
- *Search:* GitHub has a powerful search API that can be used
|
||||
- *Repository:* Built-in `context.config()` allows storing configuration in the repository.
|
||||
- *Labels:* The API can read labels, and add or remove them from issues and pull requests.
|
||||
- _Comments:_ The API can read, write and delete comments on issues and pull requests.
|
||||
- _Status:_ The API can read and change the status of an issue or pull request.
|
||||
- _Search:_ GitHub has a powerful search API that can be used
|
||||
- _Repository:_ Built-in `context.config()` allows storing configuration in the repository or the organization's `.github` repository.
|
||||
- _Labels:_ The API can read labels, and add or remove them from issues and pull requests.
|
||||
|
||||
If your Probot App needs to store more data than Issues and Pull Requests store normally, you can use the [`probot-metadata` extension](https://probot.github.io/docs/extensions/#metadata) to hide data in comments. It isn't meant to be super secure or scalable, but it's an easy way to manage some data without a full database.
|
||||
If your Probot App needs to store more data than Issues and Pull Requests store normally, you can use the [`probot-metadata` extension](/docs/extensions#metadata) to hide data in comments. It isn't meant to be super secure or scalable, but it's an easy way to manage some data without a full database.
|
||||
|
||||
There are even more APIs that you can use to increase the functionality of your Probot App. You can read about all of the ones available in Probot on the [`@octokit/rest` documentation](http://octokit.github.io/rest.js/).
|
||||
|
||||
|
|
@ -31,147 +45,186 @@ For when you absolutely do need external data storage, here are some examples us
|
|||
```js
|
||||
// PeopleSchema.js
|
||||
|
||||
const mongoose = require('mongoose')
|
||||
import * as mongoose from "mongoose";
|
||||
|
||||
const Schema = mongoose.Schema
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const PeopleSchema = new Schema({
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('People', PeopleSchema)
|
||||
export default mongoose.model("People", PeopleSchema);
|
||||
```
|
||||
|
||||
```js
|
||||
// index.js
|
||||
|
||||
const mongoose = require('mongoose')
|
||||
import * as mongoose from "mongoose";
|
||||
|
||||
// Connect to the Mongo database using credentials
|
||||
// in your environment variables
|
||||
const mongoUri = `mongodb://${process.env.DB_HOST}`
|
||||
const mongoUri = `mongodb://${process.env.DB_HOST}`;
|
||||
|
||||
mongoose.connect(mongoUri, {
|
||||
user: process.env.DB_USER,
|
||||
pass: process.env.DB_PASS,
|
||||
useMongoClient: true
|
||||
})
|
||||
useMongoClient: true,
|
||||
});
|
||||
|
||||
// Register the mongoose model
|
||||
const People = require('./PeopleSchema')
|
||||
import People from "./PeopleSchema.js";
|
||||
|
||||
module.exports = app => {
|
||||
app.on('issues.opened', async context => {
|
||||
export default (app) => {
|
||||
app.on("issues.opened", async (context) => {
|
||||
// Find all the people in the database
|
||||
const people = await People.find().exec()
|
||||
const people = await People.find().exec();
|
||||
|
||||
// Generate a string using all the peoples' names.
|
||||
// It would look like: 'Jason, Jane, James, Jennifer'
|
||||
const peoplesNames = people.map(person => person.name).join(', ')
|
||||
const peoplesNames = people.map((person) => person.name).join(", ");
|
||||
|
||||
// `context` extracts information from the event, which can be passed to
|
||||
// GitHub API calls. This will return:
|
||||
// {owner: 'yourname', repo: 'yourrepo', number: 123, body: 'The following people are in the database: Jason, Jane, James, Jennifer'}
|
||||
const params = context.issue({body: `The following people are in the database: ${peoplesNames}`})
|
||||
// { owner: 'yourname', repo: 'yourrepo', number: 123, body: 'The following people are in the database: Jason, Jane, James, Jennifer' }
|
||||
const params = context.issue({
|
||||
body: `The following people are in the database: ${peoplesNames}`,
|
||||
});
|
||||
|
||||
// Post a comment on the issue
|
||||
return context.github.issues.createComment(params)
|
||||
})
|
||||
}
|
||||
return context.octokit.issues.createComment(params);
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### MySQL
|
||||
|
||||
Using the [`mysql`](https://github.com/mysqljs/mysql) module, we can connect to our MySQL database and perform queries.
|
||||
Using the [`@databases/mysql`](https://www.atdatabases.org/docs/mysql.html) module, we can connect to our MySQL database and perform queries.
|
||||
|
||||
```js
|
||||
// connection.js
|
||||
const mysql = require('mysql')
|
||||
import { connect } from "@databases/mysql";
|
||||
|
||||
const connection = mysql.createConnection({
|
||||
host: process.env.DB_HOST,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASS,
|
||||
database: process.env.DB_DATABASE
|
||||
})
|
||||
// DATABASE_URL = mysql://my-user:my-password@localhost/my-db
|
||||
const connection = connect(process.env.DATABASE_URL);
|
||||
|
||||
connection.connect()
|
||||
module.exports = connection
|
||||
export default connection;
|
||||
```
|
||||
|
||||
```js
|
||||
// index.js
|
||||
const connection = require('./connection')
|
||||
import { sql } from "@databases/mysql";
|
||||
import connection from "./connection.js";
|
||||
|
||||
function performQuery (query) {
|
||||
return new Promise((resolve, reject) => {
|
||||
connection.query(query, function (error, results, fields) {
|
||||
if (error) reject(new Error(error))
|
||||
resolve(results)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = app => {
|
||||
app.on('issues.opened', async context => {
|
||||
export default (app) => {
|
||||
app.on("issues.opened", async (context) => {
|
||||
// Find all the people in the database
|
||||
const people = await performQuery('SELECT * FROM `people`')
|
||||
const people = await connection.query(sql`SELECT * FROM people`);
|
||||
|
||||
// Generate a string using all the peoples' names.
|
||||
// It would look like: 'Jason, Jane, James, Jennifer'
|
||||
const peoplesNames = people.map(key => people[key].name).join(', ')
|
||||
const peoplesNames = people.map((key) => people[key].name).join(", ");
|
||||
|
||||
// `context` extracts information from the event, which can be passed to
|
||||
// GitHub API calls. This will return:
|
||||
// {owner: 'yourname', repo: 'yourrepo', number: 123, body: 'The following people are in the database: Jason, Jane, James, Jennifer'}
|
||||
const params = context.issue({body: `The following people are in the database: ${peoplesNames}`})
|
||||
// { owner: 'yourname', repo: 'yourrepo', number: 123, body: 'The following people are in the database: Jason, Jane, James, Jennifer' }
|
||||
const params = context.issue({
|
||||
body: `The following people are in the database: ${peoplesNames}`,
|
||||
});
|
||||
|
||||
// Post a comment on the issue
|
||||
return context.github.issues.createComment(params)
|
||||
})
|
||||
}
|
||||
return context.octokit.issues.createComment(params);
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### Postgres
|
||||
|
||||
Using the [`@databases/pg`](https://www.atdatabases.org/docs/pg.html) module, we can connect to our Postgres database and perform queries.
|
||||
|
||||
```js
|
||||
// connection.js
|
||||
import { connect } from "@databases/pg";
|
||||
|
||||
// DATABASE_URL = postgresql://my-user:my-password@localhost/my-db
|
||||
const connection = connect(process.env.DATABASE_URL);
|
||||
|
||||
export default connection;
|
||||
```
|
||||
|
||||
```js
|
||||
// index.js
|
||||
import { sql } from "@databases/pg";
|
||||
import connection from "./connection.js";
|
||||
|
||||
export default (app) => {
|
||||
app.on("issues.opened", async (context) => {
|
||||
// Find all the people in the database
|
||||
const people = await connection.query(sql`SELECT * FROM people`);
|
||||
|
||||
// Generate a string using all the peoples' names.
|
||||
// It would look like: 'Jason, Jane, James, Jennifer'
|
||||
const peoplesNames = people.map((key) => people[key].name).join(", ");
|
||||
|
||||
// `context` extracts information from the event, which can be passed to
|
||||
// GitHub API calls. This will return:
|
||||
// { owner: 'yourname', repo: 'yourrepo', number: 123, body: 'The following people are in the database: Jason, Jane, James, Jennifer' }
|
||||
const params = context.issue({
|
||||
body: `The following people are in the database: ${peoplesNames}`,
|
||||
});
|
||||
|
||||
// Post a comment on the issue
|
||||
return context.octokit.issues.createComment(params);
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### Firebase
|
||||
|
||||
[Firebase](https://firebase.google.com/) is Google's services-as-a-service that includes a simple JSON database. You can learn more about dealing with the Javascript API [here](https://firebase.google.com/docs/database/web/start). Note that for security purposes, you may also want to look into the [Admin API](https://firebase.google.com/docs/database/admin/start).
|
||||
[Firebase](https://firebase.google.com/) is Google's services-as-a-service that includes a simple JSON database. You can learn more about dealing with the JavaScript API [here](https://firebase.google.com/docs/database/web/start). Note that for security purposes, you may also want to look into the [Admin API](https://firebase.google.com/docs/database/admin/start).
|
||||
|
||||
```js
|
||||
// index.js
|
||||
|
||||
const firebase = require('firebase')
|
||||
import * as firebase from "firebase";
|
||||
// Set the configuration for your app
|
||||
// TODO: Replace with your project's config object
|
||||
const config = {
|
||||
apiKey: 'apiKey',
|
||||
authDomain: 'projectId.firebaseapp.com',
|
||||
databaseURL: 'https://databaseName.firebaseio.com'
|
||||
}
|
||||
firebase.initializeApp(config)
|
||||
apiKey: "apiKey",
|
||||
authDomain: "projectId.firebaseapp.com",
|
||||
databaseURL: "https://databaseName.firebaseio.com",
|
||||
};
|
||||
firebase.initializeApp(config);
|
||||
|
||||
const database = firebase.database()
|
||||
const database = firebase.database();
|
||||
|
||||
module.exports = app => {
|
||||
app.on('issues.opened', async context => {
|
||||
export default (app) => {
|
||||
app.on("issues.opened", async (context) => {
|
||||
// Find all the people in the database
|
||||
const people = await database.ref('/people').once('value').then((snapshot) => {
|
||||
return snapshot.val()
|
||||
})
|
||||
const people = await database
|
||||
.ref("/people")
|
||||
.once("value")
|
||||
.then((snapshot) => {
|
||||
return snapshot.val();
|
||||
});
|
||||
|
||||
// Generate a string using all the peoples' names.
|
||||
// It would look like: 'Jason, Jane, James, Jennifer'
|
||||
const peoplesNames = Object.keys(people).map(key => people[key].name).join(', ')
|
||||
const peoplesNames = Object.keys(people)
|
||||
.map((key) => people[key].name)
|
||||
.join(", ");
|
||||
|
||||
// `context` extracts information from the event, which can be passed to
|
||||
// GitHub API calls. This will return:
|
||||
// {owner: 'yourname', repo: 'yourrepo', number: 123, body: 'The following people are in the database: Jason, Jane, James, Jennifer'}
|
||||
const params = context.issue({body: `The following people are in the database: ${peoplesNames}`})
|
||||
// { owner: 'yourname', repo: 'yourrepo', number: 123, body: 'The following people are in the database: Jason, Jane, James, Jennifer' }
|
||||
const params = context.issue({
|
||||
body: `The following people are in the database: ${peoplesNames}`,
|
||||
});
|
||||
|
||||
// Post a comment on the issue
|
||||
return context.github.issues.createComment(params)
|
||||
})
|
||||
}
|
||||
return context.octokit.issues.createComment(params);
|
||||
});
|
||||
};
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,24 +1,28 @@
|
|||
---
|
||||
next: docs/testing.md
|
||||
next: logging
|
||||
title: Simulate receiving webhooks
|
||||
---
|
||||
|
||||
# Simulating Webhooks
|
||||
# Simulate receiving webhooks
|
||||
|
||||
As you are developing your app, you will likely want to test it by repeatedly triggering the same webhook. You can simulate a webhook being delivered by saving the payload to a file, and then calling `probot simulate` from the command line.
|
||||
As you are developing your app, you will likely want to test it by repeatedly triggering the same webhook. You can simulate a webhook being delivered by saving the payload to a file, and then calling `probot receive` from the command line.
|
||||
|
||||
To save a copy of the payload, go to the [settings](https://github.com/settings/apps) page for your App, and go to the **Advanced** tab. Click on one of the **Recent Deliveries** to expand it and see the details of the webhook event. Copy the JSON from the the **Payload** and save it to a new file. (`test/fixtures/issues.labeled.json` in this example).
|
||||
To save a copy of the payload, go to the [settings](https://github.com/settings/apps) page for your App, and go to the **Advanced** tab. Click on one of the **Recent Deliveries** to expand it and see the details of the webhook event. Copy the JSON from the **Payload** and save it to a new file. (`test/fixtures/issues.labeled.json` in this example).
|
||||
|
||||
**Note**: This will only simulate the JSON payload, not the headers that are also sent by GitHub webhooks.
|
||||
**Note**: This will only receive the JSON payload, not the headers that are also sent by GitHub webhooks.
|
||||
|
||||

|
||||
|
||||
Next, simulate this event being delivered by running:
|
||||
Next, simulate receiving this event being delivered by running:
|
||||
|
||||
$ node_modules/.bin/probot simulate <event-name> <path-to-fixture> <path-to-app>
|
||||
```bash
|
||||
$ node_modules/.bin/probot receive -e <event-name> -p <path-to-fixture> <path-to-app>
|
||||
```
|
||||
|
||||
Note that `event-name` here is just the name of the event (like pull_request or issues) and not the action (like labeled). You can find it in the GitHub deliveries history under the `X-GitHub-Event` header.
|
||||
Note that `event-name` here is just the name of the event (like pull_request or issues) and not the action (like labeled). You can find it in the GitHub deliveries history under the `X-GitHub-Event` header.
|
||||
|
||||
For example, to simulate the pull_request.labeled event, run:
|
||||
For example, to simulate receiving the `pull_request.labeled` event, run:
|
||||
|
||||
$ node_modules/.bin/probot simulate pull_request test/fixtures/pull_request.labeled.json ./index.js
|
||||
|
||||
```bash
|
||||
$ node_modules/.bin/probot receive -e pull_request -p test/fixtures/pull_request.labeled.json ./index.js
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
# Welcome to Probot's Summer of Code!
|
||||
|
||||
In 2018, Probot is participating in Google's Summer of Code and Rails Girls Summer of Code, two amazing programs that help students connect with open source projects.
|
||||
|
||||
## FAQ
|
||||
|
||||
#### How do I get started with Probot?
|
||||
|
||||
We suggest that you build our ["Hello World" app](https://probot.github.io/docs/hello-world/) to get the hang of registering a GitHub App and the base setup of a Probot App. In your application, include a link to a comment that your Hello World app created on a test repository.
|
||||
|
||||
You can also [join our Slack](https://probot-slackin.herokuapp.com/) and join the **#summer-of-code** channel.
|
||||
|
||||
#### Can I claim this cool idea?
|
||||
|
||||
We're excited that you're excited, but the Summer of Code ideas in our [ideas repo](https://github.com/probot/ideas) are for the students that are accepted to work on Probot over the summer. Claims to those ideas will be ignored until April (when the submission deadline has passed).
|
||||
|
||||
#### Someone has claimed this idea, can I still have it in my application?
|
||||
|
||||
Absolutely! See the above answer.
|
||||
|
||||
#### I have another cool idea for an app!
|
||||
|
||||
Amazing! Be sure to include it in your application.
|
||||
|
||||
## Mentors
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="top">
|
||||
<img width="150" height="150" src="https://github.com/bkeepers.png?s=150">
|
||||
<br>
|
||||
<a href="https://github.com/bkeepers">Brandon Keepers</a>
|
||||
</td>
|
||||
<td align="center" valign="top">
|
||||
<img width="150" height="150" src="https://github.com/gr2m.png?s=150">
|
||||
<br>
|
||||
<a href="https://github.com/gr2m">Gregor Martynus</a>
|
||||
</td>
|
||||
<td align="center" valign="top">
|
||||
<img width="150" height="150" src="https://github.com/JasonEtco.png?s=150">
|
||||
<br>
|
||||
<a href="https://github.com/JasonEtco">Jason Etcovitch</a>
|
||||
</td>
|
||||
<td align="center" valign="top">
|
||||
<img width="150" height="150" src="https://github.com/tcbyrd.png?s=150">
|
||||
<br>
|
||||
<a href="https://github.com/tcbyrd">Tommy Byrd</a>
|
||||
<br>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Application Requirements
|
||||
|
||||
* You must have created the ["Hello World" app](https://probot.github.io/docs/hello-world/), and provided a link to a comment that it has posted.
|
||||
121
docs/testing.md
121
docs/testing.md
|
|
@ -1,48 +1,105 @@
|
|||
---
|
||||
next: docs/logging.md
|
||||
next: simulating-webhooks
|
||||
title: Testing
|
||||
---
|
||||
|
||||
# Testing
|
||||
|
||||
We highly recommend working in the style of [test-driven development](http://agiledata.org/essays/tdd.html) when creating probot apps. It is frustrating to constantly create real GitHub events in order to test an app. Redelivering webhooks is possible and can be accessed in your app's [settings](https://github.com/settings/apps) page under the **Advanced** tab. We do offer the above documented `simulate` method to help make this easier; however, by writing your tests first, you can avoid repeatedly recreating actual events from GitHub to check if your code is working.
|
||||
We highly recommend working in the style of [test-driven development](http://agiledata.org/essays/tdd.html) when creating probot apps. It is frustrating to constantly create real GitHub events in order to test an app. Redelivering webhooks is possible and can be accessed in your app's [settings](https://github.com/settings/apps) page under the **Advanced** tab. We do offer the above documented `receive` method to help make this easier; however, by writing your tests first, you can avoid repeatedly recreating actual events from GitHub to check if your code is working.
|
||||
|
||||
For our testing examples, we use [jest](https://facebook.github.io/jest/), but there are other options that can perform similar operations. Here's an example of creating an app instance and mocking out the GitHub API:
|
||||
For our testing examples, we use [jest](https://facebook.github.io/jest/), but there are other options that can perform similar operations. We also recommend using [nock](https://github.com/nock/nock), a tool for mocking HTTP requests, which is often crucial to testing in Probot, considering how much of Probot depends on GitHub's APIs. Here's an example of creating an app instance and using nock to test that we correctly hit the GitHub API:
|
||||
|
||||
```js
|
||||
const { Application } = require('probot')
|
||||
import nock from "nock";
|
||||
// Requiring our app implementation
|
||||
const myProbotApp = require('..')
|
||||
import myProbotApp from "../index.js";
|
||||
import { Probot, ProbotOctokit } from "probot";
|
||||
// Requiring our fixtures
|
||||
import payload from "./fixtures/issues.opened.json" with { type: "json" };
|
||||
const issueCreatedBody = { body: "Thanks for opening this issue!" };
|
||||
|
||||
const issuesOpenedPayload = require('./fixtures/issues.opened.json')
|
||||
|
||||
describe('My Probot app', () => {
|
||||
let app, github
|
||||
describe("My Probot app", () => {
|
||||
let probot;
|
||||
|
||||
beforeEach(() => {
|
||||
app = new Application()
|
||||
// Initialize the app based on the code from index.js
|
||||
app.load(myProbotApp)
|
||||
// This is an easy way to mock out the GitHub API
|
||||
github = {
|
||||
issues: {
|
||||
createComment: jest.fn().mockReturnValue(Promise.resolve({}))
|
||||
}
|
||||
}
|
||||
// Passes the mocked out GitHub API into out app instance
|
||||
app.auth = () => Promise.resolve(github)
|
||||
})
|
||||
nock.disableNetConnect();
|
||||
probot = new Probot({
|
||||
githubToken: "test",
|
||||
// Disable throttling & retrying requests for easier testing
|
||||
Octokit: ProbotOctokit.defaults({
|
||||
retry: { enabled: false },
|
||||
throttle: { enabled: false },
|
||||
}),
|
||||
});
|
||||
myProbotApp(probot);
|
||||
});
|
||||
|
||||
test('creates a comment when an issue is opened', async () => {
|
||||
// Simulates delivery of an issues.opened webhook
|
||||
await app.receive({
|
||||
name: 'issues.opened',
|
||||
payload: issuesOpenedPayload
|
||||
})
|
||||
test("creates a passing check", async () => {
|
||||
// Test that we correctly return a test token
|
||||
nock("https://api.github.com")
|
||||
.post("/app/installations/2/access_tokens")
|
||||
.reply(200, { token: "test" });
|
||||
|
||||
// This test passes if the code in your index.js file calls `context.github.issues.createComment`
|
||||
expect(github.issues.createComment).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
// Test that a comment is posted
|
||||
nock("https://api.github.com")
|
||||
.post("/repos/hiimbex/testing-things/issues/1/comments", (body) => {
|
||||
expect(body).toMatchObject(issueCreatedBody);
|
||||
return true;
|
||||
})
|
||||
.reply(200);
|
||||
|
||||
// Receive a webhook event
|
||||
await probot.receive({ name: "issues", payload });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
nock.enableNetConnect();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
A good testing example from [update-docs](https://github.com/behaviorbot/update-docs) can be found [here](https://github.com/behaviorbot/update-docs/blob/master/test/index.js), and another one from [owners](https://github.com/probot/owners) can be found [here](https://github.com/probot/owners/blob/master/test/owner-notifier.js).
|
||||
## Testing log output
|
||||
|
||||
Probot is using [pino](https://getpino.io/) for logging. A custom `pino` instance can be passed to the `Probot` constructor. You can write all log output into an array by passing a custom transport function:
|
||||
|
||||
```js
|
||||
import pino from "pino";
|
||||
import Stream from "stream";
|
||||
|
||||
const streamLogsToOutput = new Stream.Writable({ objectMode: true });
|
||||
streamLogsToOutput._write = (object, encoding, done) => {
|
||||
output.push(JSON.parse(object));
|
||||
done();
|
||||
};
|
||||
|
||||
const probot = new Probot({
|
||||
id: 1,
|
||||
githubToken: "test",
|
||||
// Disable throttling & retrying requests for easier testing
|
||||
Octokit: ProbotOctokit.defaults({
|
||||
retry: { enabled: false },
|
||||
throttle: { enabled: false },
|
||||
}),
|
||||
log: pino(streamLogsToOutput),
|
||||
});
|
||||
|
||||
probot.log.info("test");
|
||||
// output is now:
|
||||
// [ { level: 30, time: 1600619283012, pid: 44071, hostname: 'Gregors-MacBook-Pro.local', msg: 'test' } ]
|
||||
```
|
||||
|
||||
## Examples:
|
||||
|
||||
Using Jest
|
||||
|
||||
- [Settings](https://github.com/probot/settings): [probot/settings/test/integration/plugins/repository.test.js](https://github.com/probot/settings/blob/master/test/integration/plugins/repository.test.js)
|
||||
- [DCO](https://github.com/probot/dco): [probot/dco/test/index.test.js](https://github.com/probot/dco/blob/master/test/index.test.js)
|
||||
|
||||
Using Tap
|
||||
|
||||
- [WIP](https://github.com/apps/wip/): [wip/app/test/integration](https://github.com/wip/app/tree/master/test/integration)
|
||||
|
||||
Using Mocha and Sinon
|
||||
|
||||
- [Auto-Me-Bot](https://github.com/TomerFi/auto-me-bot): [tests](https://github.com/TomerFi/auto-me-bot/tree/main/tests)
|
||||
|
|
|
|||
|
|
@ -1,53 +1,54 @@
|
|||
---
|
||||
next: docs/github-api.md
|
||||
next: github-api
|
||||
title: Receiving webhooks
|
||||
---
|
||||
|
||||
# Receiving Webhooks
|
||||
# Receiving webhooks
|
||||
|
||||
[GitHub webhooks](https://developer.github.com/webhooks/) are fired for almost every significant action that users take on GitHub, whether it's pushes to code, opening or closing issues, opening or merging pull requests, or commenting on a discussion.
|
||||
[GitHub webhooks](https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/about-webhooks) are fired for almost every significant action that users or other apps take on GitHub, whether it's pushing to code, opening or closing issues, opening or merging pull requests, or commenting on a discussion.
|
||||
|
||||
Many apps will spend their entire day responding to these actions. `app.on` will listen for any GitHub webhook events:
|
||||
|
||||
```js
|
||||
module.exports = app => {
|
||||
app.on('push', async context => {
|
||||
export default (app) => {
|
||||
app.on("push", async (context) => {
|
||||
// Code was pushed to the repo, what should we do with it?
|
||||
app.log(context)
|
||||
})
|
||||
}
|
||||
app.log.info(context);
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
The app can listen to any of the [GitHub webhook events](https://developer.github.com/webhooks/#events). The `context` object includes everything about the event that was triggered, and `context.payload` has the payload delivered by GitHub.
|
||||
The app can listen to any of the [GitHub webhook events](https://docs.github.com/en/developers/webhooks-and-events). The `context` object includes everything about the event that was triggered, and `context.payload` has the payload delivered by GitHub.
|
||||
|
||||
Most events also include an "action". For example, the [`issues`](https://developer.github.com/v3/activity/events/types/#issuesevent) event has actions of `assigned`, `unassigned`, `labeled`, `unlabeled`, `opened`, `edited`, `milestoned`, `demilestoned`, `closed`, and `reopened`. Often, your app will only care about one type of action, so you can append it to the event name with a `.`:
|
||||
Most events also include an "action". For example, the [`issues`](https://docs.github.com/en/developers/webhooks-and-events/webhook-events-and-payloads#issues) event has actions of `assigned`, `unassigned`, `labeled`, `unlabeled`, `opened`, `edited`, `milestoned`, `demilestoned`, `closed`, and `reopened`. Often, your app will only care about one type of action, so you can append it to the event name with a `.`:
|
||||
|
||||
```js
|
||||
module.exports = app => {
|
||||
app.on('issues.opened', async context => {
|
||||
export default (app) => {
|
||||
app.on("issues.opened", async (context) => {
|
||||
// An issue was just opened.
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
Sometimes you want to handle multiple webhook events the same way. `app.on` can listen to a list of events and run the same callback:
|
||||
|
||||
```js
|
||||
module.exports = app => {
|
||||
app.on(['issues.opened', 'issues.edited'], async context => {
|
||||
export default (app) => {
|
||||
app.on(["issues.opened", "issues.edited"], async (context) => {
|
||||
// An issue was opened or edited, what should we do with it?
|
||||
app.log(context)
|
||||
})
|
||||
}
|
||||
app.log.info(context);
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
You can also use the wildcard event (`*`) to listen for any event that your app is subscribed to:
|
||||
You can also use `app.onAny()` to listen for any event that your app is subscribed to:
|
||||
|
||||
```js
|
||||
module.exports = app => {
|
||||
app.on(`*`, async context => {
|
||||
context.log({event: context.event, action: context.payload.action})
|
||||
})
|
||||
}
|
||||
export default (app) => {
|
||||
app.onAny(async (context) => {
|
||||
app.log.info({ event: context.name, action: context.payload.action });
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
Explore the [GitHub webhook documentation](https://developer.github.com/webhooks/#events) to see what events are available to use in your app.
|
||||
For more details, explore the [GitHub webhook documentation](https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/about-webhooks#events) or see a list of all the named events in the [@octokit/webhooks.js](https://github.com/octokit/webhooks.js/#webhook-events) npm module.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
'use strict';
|
||||
|
||||
const { createServer } = require("http");
|
||||
const { createNodeMiddleware } = require('../lib/create-node-middleware');
|
||||
const { createProbot } = require('../lib/create-probot');
|
||||
const { sign } = require("@octokit/webhooks-methods");
|
||||
const WebhookExamples = require("@octokit/webhooks-examples");
|
||||
|
||||
process.env.APP_ID = "123";
|
||||
process.env.PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEA1c7+9z5Pad7OejecsQ0bu3aozN3tihPmljnnudb9G3HECdnH
|
||||
lWu2/a1gB9JW5TBQ+AVpum9Okx7KfqkfBKL9mcHgSL0yWMdjMfNOqNtrQqKlN4kE
|
||||
p6RD++7sGbzbfZ9arwrlD/HSDAWGdGGJTSOBM6pHehyLmSC3DJoR/CTu0vTGTWXQ
|
||||
rO64Z8tyXQPtVPb/YXrcUhbBp8i72b9Xky0fD6PkEebOy0Ip58XVAn2UPNlNOSPS
|
||||
ye+Qjtius0Md4Nie4+X8kwVI2Qjk3dSm0sw/720KJkdVDmrayeljtKBx6AtNQsSX
|
||||
gzQbeMmiqFFkwrG1+zx6E7H7jqIQ9B6bvWKXGwIDAQABAoIBAD8kBBPL6PPhAqUB
|
||||
K1r1/gycfDkUCQRP4DbZHt+458JlFHm8QL6VstKzkrp8mYDRhffY0WJnYJL98tr4
|
||||
4tohsDbqFGwmw2mIaHjl24LuWXyyP4xpAGDpl9IcusjXBxLQLp2m4AKXbWpzb0OL
|
||||
Ulrfc1ZooPck2uz7xlMIZOtLlOPjLz2DuejVe24JcwwHzrQWKOfA11R/9e50DVse
|
||||
hnSH/w46Q763y4I0E3BIoUMsolEKzh2ydAAyzkgabGQBUuamZotNfvJoDXeCi1LD
|
||||
8yNCWyTlYpJZJDDXooBU5EAsCvhN1sSRoaXWrlMSDB7r/E+aQyKua4KONqvmoJuC
|
||||
21vSKeECgYEA7yW6wBkVoNhgXnk8XSZv3W+Q0xtdVpidJeNGBWnczlZrummt4xw3
|
||||
xs6zV+rGUDy59yDkKwBKjMMa42Mni7T9Fx8+EKUuhVK3PVQyajoyQqFwT1GORJNz
|
||||
c/eYQ6VYOCSC8OyZmsBM2p+0D4FF2/abwSPMmy0NgyFLCUFVc3OECpkCgYEA5OAm
|
||||
I3wt5s+clg18qS7BKR2DuOFWrzNVcHYXhjx8vOSWV033Oy3yvdUBAhu9A1LUqpwy
|
||||
Ma+unIgxmvmUMQEdyHQMcgBsVs10dR/g2xGjMLcwj6kn+xr3JVIZnbRT50YuPhf+
|
||||
ns1ScdhP6upo9I0/sRsIuN96Gb65JJx94gQ4k9MCgYBO5V6gA2aMQvZAFLUicgzT
|
||||
u/vGea+oYv7tQfaW0J8E/6PYwwaX93Y7Q3QNXCoCzJX5fsNnoFf36mIThGHGiHY6
|
||||
y5bZPPWFDI3hUMa1Hu/35XS85kYOP6sGJjf4kTLyirEcNKJUWH7CXY+00cwvTkOC
|
||||
S4Iz64Aas8AilIhRZ1m3eQKBgQCUW1s9azQRxgeZGFrzC3R340LL530aCeta/6FW
|
||||
CQVOJ9nv84DLYohTVqvVowdNDTb+9Epw/JDxtDJ7Y0YU0cVtdxPOHcocJgdUGHrX
|
||||
ZcJjRIt8w8g/s4X6MhKasBYm9s3owALzCuJjGzUKcDHiO2DKu1xXAb0SzRcTzUCn
|
||||
7daCswKBgQDOYPZ2JGmhibqKjjLFm0qzpcQ6RPvPK1/7g0NInmjPMebP0K6eSPx0
|
||||
9/49J6WTD++EajN7FhktUSYxukdWaCocAQJTDNYP0K88G4rtC2IYy5JFn9SWz5oh
|
||||
x//0u+zd/R/QRUzLOw4N72/Hu+UG6MNt5iDZFCtapRaKt6OvSBwy8w==
|
||||
-----END RSA PRIVATE KEY-----`;
|
||||
process.env.WEBHOOK_SECRET = "secret";
|
||||
process.env.WEBHOOK_PATH = "/";
|
||||
|
||||
const pushEvent = JSON.stringify((
|
||||
WebhookExamples.filter(
|
||||
(event) => event.name === "push",
|
||||
)[0]
|
||||
).examples[0]);
|
||||
|
||||
const appFn = (app) => {
|
||||
app.on("issues.opened", async (context) => {
|
||||
const issueComment = context.issue({
|
||||
body: "Thanks for opening this issue!",
|
||||
});
|
||||
return context.octokit.issues.createComment(issueComment);
|
||||
});
|
||||
|
||||
app.onAny(async (context) => {
|
||||
context.log.info({ event: context.name, action: context.payload.action });
|
||||
});
|
||||
|
||||
app.onError(async (error) => {
|
||||
app.log.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
const middleware = createNodeMiddleware(appFn, { probot: createProbot() });
|
||||
|
||||
const server = createServer(middleware);
|
||||
|
||||
server.listen(3000, async () => {
|
||||
console.log("Probot started http://localhost:3000/")
|
||||
console.log(`autocannon -m POST -b '${pushEvent}' -H content-type=application/json -H x-github-event=push -H x-github-delivery=1 -H x-hub-signature-256=${await sign("secret", pushEvent)} http://127.0.0.1:3000/`)
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
187
package.json
187
package.json
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "probot",
|
||||
"version": "7.2.0",
|
||||
"description": "🤖 A framework for building GitHub Apps to automate and improve your workflow",
|
||||
"repository": "https://github.com/probot/probot",
|
||||
"version": "0.0.0-development",
|
||||
"description": "A framework for building GitHub Apps to automate and improve your workflow",
|
||||
"repository": "github:probot/probot",
|
||||
"main": "lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"bin": {
|
||||
|
|
@ -10,14 +10,13 @@
|
|||
},
|
||||
"scripts": {
|
||||
"build": "rimraf lib && tsc -p tsconfig.json",
|
||||
"start": "node ./bin/probot run",
|
||||
"lint": "tslint --project test",
|
||||
"test": "tsc --noEmit -p test && jest --coverage && npm run lint && npm run doc-lint",
|
||||
"doc-lint": "standard-markdown docs/",
|
||||
"doc": "typedoc --options .typedoc.json",
|
||||
"semantic-release": "semantic-release pre && npm publish && semantic-release post",
|
||||
"postsemantic-release": "script/publish-docs",
|
||||
"prepare": "npm run build"
|
||||
"lint": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\" \"docs/*.md\" *.md tsconfig.json package.json --end-of-line auto",
|
||||
"lint:fix": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"docs/*.md\" *.md tsconfig.json package.json --end-of-line auto",
|
||||
"pretest": "npm run build && tsc --noEmit -p test",
|
||||
"test": "vitest run",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:dev": "vitest --ui --coverage",
|
||||
"doc": "typedoc --options .typedoc.json"
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
|
|
@ -25,32 +24,6 @@
|
|||
"static",
|
||||
"views"
|
||||
],
|
||||
"jest": {
|
||||
"setupFiles": [
|
||||
"<rootDir>/test/setup.js"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
"<rootDir>/lib/"
|
||||
],
|
||||
"moduleFileExtensions": [
|
||||
"ts",
|
||||
"js",
|
||||
"json",
|
||||
"node"
|
||||
],
|
||||
"transform": {
|
||||
".+\\.tsx?$": "ts-jest"
|
||||
},
|
||||
"testMatch": [
|
||||
"<rootDir>/test/**/*.test.(ts|js)"
|
||||
],
|
||||
"globals": {
|
||||
"ts-jest": {
|
||||
"skipBabel": true
|
||||
}
|
||||
},
|
||||
"testURL": "http://localhost/"
|
||||
},
|
||||
"keywords": [
|
||||
"probot",
|
||||
"github-apps",
|
||||
|
|
@ -59,81 +32,87 @@
|
|||
"robots",
|
||||
"workflow"
|
||||
],
|
||||
"bugs": "https://github.com/probot/probot/issues",
|
||||
"homepage": "https://probot.github.io",
|
||||
"author": "Brandon Keepers",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@octokit/rest": "^15.13.0",
|
||||
"@octokit/webhooks": "^5.0.2",
|
||||
"@types/supports-color": "^5.3.0",
|
||||
"bottleneck": "^2.8.0",
|
||||
"bunyan": "^1.8.12",
|
||||
"bunyan-format": "^0.2.1",
|
||||
"bunyan-sentry-stream": "^1.1.0",
|
||||
"cache-manager": "^2.4.0",
|
||||
"commander": "^2.11.0",
|
||||
"dotenv": "~6.0.0",
|
||||
"express": "^4.16.2",
|
||||
"express-async-errors": "^3.0.0",
|
||||
"hbs": "^4.0.1",
|
||||
"is-base64": "0.0.5",
|
||||
"js-yaml": "^3.9.1",
|
||||
"jsonwebtoken": "^8.1.0",
|
||||
"pkg-conf": "^2.0.0",
|
||||
"promise-events": "^0.1.3",
|
||||
"qs": "^6.5.2",
|
||||
"raven": "^2.4.2",
|
||||
"resolve": "^1.4.0",
|
||||
"semver": "^5.5.0",
|
||||
"supports-color": "^5.5.0",
|
||||
"update-dotenv": "^1.1.0",
|
||||
"uuid": "^3.2.1"
|
||||
"@octokit/core": "^5.0.2",
|
||||
"@octokit/plugin-enterprise-compatibility": "^4.0.1",
|
||||
"@octokit/plugin-paginate-rest": "^9.1.4",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^10.1.5",
|
||||
"@octokit/plugin-retry": "^6.0.1",
|
||||
"@octokit/plugin-throttling": "^8.1.3",
|
||||
"@octokit/request": "^8.1.6",
|
||||
"@octokit/types": "^12.3.0",
|
||||
"@octokit/webhooks": "^12.0.10",
|
||||
"@probot/get-private-key": "^1.1.2",
|
||||
"@probot/octokit-plugin-config": "^2.0.1",
|
||||
"@probot/pino": "^2.3.5",
|
||||
"@types/express": "^4.17.21",
|
||||
"bottleneck": "^2.19.5",
|
||||
"commander": "^12.0.0",
|
||||
"deepmerge": "^4.3.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"eventsource": "^2.0.2",
|
||||
"express": "^4.18.2",
|
||||
"ioredis": "^5.3.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lru-cache": "^10.0.3",
|
||||
"octokit-auth-probot": "^2.0.0",
|
||||
"pino": "^9.0.0",
|
||||
"pino-http": "^10.0.0",
|
||||
"pkg-conf": "^3.1.0",
|
||||
"resolve": "^1.22.8",
|
||||
"update-dotenv": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bunyan": "^1.8.4",
|
||||
"@types/bunyan-format": "^0.2.0",
|
||||
"@types/cache-manager": "^1.2.6",
|
||||
"@types/dotenv": "^4.0.2",
|
||||
"@types/express": "^4.0.39",
|
||||
"@types/jest": "^23.1.3",
|
||||
"@types/js-yaml": "^3.10.1",
|
||||
"@types/jsonwebtoken": "^7.2.5",
|
||||
"@types/nock": "^9.1.0",
|
||||
"@types/node": "^10.7.0",
|
||||
"@types/qs": "^6.5.1",
|
||||
"@types/raven": "^2.1.5",
|
||||
"@types/resolve": "^0.0.4",
|
||||
"@types/semver": "^5.4.0",
|
||||
"@types/supertest": "^2.0.4",
|
||||
"@types/uuid": "^3.4.3",
|
||||
"@octokit/tsconfig": "^3.0.0",
|
||||
"@octokit/webhooks-examples": "^7.3.1",
|
||||
"@octokit/webhooks-methods": "^4.0.0",
|
||||
"@octokit/webhooks-types": "^7.3.1",
|
||||
"@types/eventsource": "^1.1.15",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/resolve": "^1.20.5",
|
||||
"@types/supertest": "^6.0.0",
|
||||
"@vitest/coverage-v8": "^1.0.0",
|
||||
"@vitest/ui": "^1.0.0",
|
||||
"connect-sse": "^1.2.0",
|
||||
"eslint": "^5.0.1",
|
||||
"eslint-plugin-markdown": "^1.0.0-beta.8",
|
||||
"jest": "^23.6.0",
|
||||
"minami": "^1.1.1",
|
||||
"nock": "^10.0.0",
|
||||
"semantic-release": "^15.0.0",
|
||||
"smee-client": "^1.0.1",
|
||||
"standard": "^12.0.0",
|
||||
"standard-markdown": "^4.0.2",
|
||||
"supertest": "^3.0.0",
|
||||
"ts-jest": "^23.1.4",
|
||||
"tslint": "^5.9.1",
|
||||
"tslint-config-prettier": "^1.10.0",
|
||||
"tslint-config-standard": "^8.0.0",
|
||||
"typedoc": "^0.12.0",
|
||||
"typescript": "^2.7.2"
|
||||
},
|
||||
"standard": {
|
||||
"env": [
|
||||
"jest"
|
||||
],
|
||||
"plugins": [
|
||||
"markdown"
|
||||
]
|
||||
"execa": "^5.0.0",
|
||||
"fetch-mock": "npm:@gr2m/fetch-mock@9.11.0-pull-request-644.1",
|
||||
"get-port": "^5.1.1",
|
||||
"prettier": "^3.0.3",
|
||||
"rimraf": "^5.0.5",
|
||||
"semantic-release": "^23.0.0",
|
||||
"semantic-release-plugin-update-version-in-files": "^1.1.0",
|
||||
"smee-client": "^2.0.0",
|
||||
"sonic-boom": "^4.0.0",
|
||||
"supertest": "^7.0.0",
|
||||
"tsd": "^0.31.0",
|
||||
"typedoc": "^0.25.3",
|
||||
"typescript": "^5.2.2",
|
||||
"vitest": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.7"
|
||||
"node": ">=18"
|
||||
},
|
||||
"release": {
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
"@semantic-release/release-notes-generator",
|
||||
"@semantic-release/github",
|
||||
"@semantic-release/npm",
|
||||
[
|
||||
"semantic-release-plugin-update-version-in-files",
|
||||
{
|
||||
"files": [
|
||||
"lib/version.*"
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"publishConfig": {
|
||||
"provenance": true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,23 @@
|
|||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
# Exit if a command returns a non-zero code
|
||||
set -e
|
||||
|
||||
# Store the versions
|
||||
VERSION=$(node -e "process.stdout.write(require('./package.json').version)")
|
||||
VERSION=$(git describe --tags $(git rev-list --tags --max-count=1) | sed 's/^v//')
|
||||
HEAD=$(git rev-parse HEAD)
|
||||
REPO=https://${OCTOKITBOT_PAT}@github.com/probot/probot.github.io.git
|
||||
|
||||
echo "Version is $VERSION"
|
||||
echo "Head is $HEAD"
|
||||
|
||||
# Generate docs
|
||||
npm run doc
|
||||
|
||||
# Checkout the website into the tmp directory
|
||||
[ -d tmp/website ] || {
|
||||
REPO=https://github.com/probot/probot.github.io
|
||||
|
||||
# If GH_TOKEN environment variable is present, use that to push.
|
||||
if [[ -n "$GH_TOKEN" ]]; then
|
||||
REPO=${REPO/https:\/\//https://${GH_TOKEN}@}
|
||||
fi
|
||||
|
||||
mkdir -p tmp
|
||||
git clone $REPO tmp/website
|
||||
git clone "$REPO" tmp/website
|
||||
}
|
||||
|
||||
# Update the website checkout
|
||||
|
|
@ -27,23 +25,24 @@ cd tmp/website
|
|||
git pull origin master
|
||||
|
||||
# Delete previously generated docs for this version
|
||||
rm -rf api/$VERSION
|
||||
rm -rf "api/$VERSION"
|
||||
|
||||
# Copy over the new docs
|
||||
mv ../../out api/$VERSION
|
||||
mv ../../out "api/$VERSION"
|
||||
|
||||
# Update the /api/latest link to point to this version
|
||||
ln -sfn $VERSION api/latest
|
||||
ln -sfn "$VERSION" api/latest
|
||||
|
||||
# Update the submodule
|
||||
git submodule update --init
|
||||
cd _submodules/probot
|
||||
git fetch
|
||||
git checkout $HEAD
|
||||
git checkout "$HEAD"
|
||||
cd ../..
|
||||
|
||||
# Commit and publish
|
||||
# Configure Git and commit, and publish
|
||||
git config --global user.email "action@github.com"
|
||||
git config --global user.name "GitHub Action"
|
||||
git add .
|
||||
git commit -m "Update docs for v$VERSION"
|
||||
git tag -f v$VERSION
|
||||
git push --tags origin master
|
||||
git commit -m "Update docs from Probot v$VERSION" -m "via $GITHUB_REPOSITORY@$HEAD"
|
||||
git push "$REPO" master
|
||||
|
|
|
|||
|
|
@ -1,68 +0,0 @@
|
|||
declare module '@octokit/webhooks' {
|
||||
import { Application } from 'express'
|
||||
|
||||
interface Options {
|
||||
path: string,
|
||||
secret: string
|
||||
}
|
||||
|
||||
declare class Webhooks {
|
||||
public middleware: Application
|
||||
|
||||
constructor (Options)
|
||||
|
||||
public on (event: string, callback: (event: WebhookEvent | Error) => void)
|
||||
public sign (data: WebhookPayloadWithRepository)
|
||||
}
|
||||
|
||||
declare namespace Webhooks {
|
||||
export interface WebhookEvent {
|
||||
id: string
|
||||
name: string
|
||||
payload: WebhookPayloadWithRepository
|
||||
protocol?: 'http' | 'https'
|
||||
host?: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
export interface PayloadRepository {
|
||||
[key: string]: any
|
||||
full_name?: string
|
||||
name: string
|
||||
owner: {
|
||||
[key: string]: any
|
||||
login: string
|
||||
name?: string
|
||||
}
|
||||
html_url?: string
|
||||
}
|
||||
|
||||
export interface WebhookPayloadWithRepository {
|
||||
[key: string]: any
|
||||
repository?: PayloadRepository
|
||||
issue?: {
|
||||
[key: string]: any
|
||||
number: number
|
||||
html_url?: string
|
||||
body?: string
|
||||
}
|
||||
pull_request?: {
|
||||
[key: string]: any
|
||||
number: number
|
||||
html_url?: string
|
||||
body?: string
|
||||
}
|
||||
sender?: {
|
||||
[key: string]: any
|
||||
type: string
|
||||
}
|
||||
action?: string
|
||||
installation?: {
|
||||
id: number
|
||||
[key: string]: any
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export = Webhooks
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
/** Declaration file generated by dts-gen */
|
||||
|
||||
declare module 'bunyan-sentry-stream' {
|
||||
export = bunyan_sentry_stream;
|
||||
|
||||
declare function bunyan_sentry_stream(client: any): any;
|
||||
|
||||
declare namespace bunyan_sentry_stream {
|
||||
class SentryStream {
|
||||
constructor(...args: any[]);
|
||||
|
||||
deserializeError(...args: any[]): void;
|
||||
|
||||
getSentryLevel(...args: any[]): void;
|
||||
|
||||
write(...args: any[]): void;
|
||||
|
||||
}
|
||||
|
||||
const prototype: {
|
||||
};
|
||||
|
||||
namespace SentryStream {
|
||||
namespace prototype {
|
||||
function deserializeError(...args: any[]): void;
|
||||
|
||||
function getSentryLevel(...args: any[]): void;
|
||||
|
||||
function write(...args: any[]): void;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
declare module "promise-events" {
|
||||
class EventEmitter {
|
||||
emit<T = void>(event: string | symbol, ...args: any[]): Promise<T>;
|
||||
on<T = void>(event: string | symbol, handler: (...args: any[]) => Promise<T>): void;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,227 +0,0 @@
|
|||
import { WebhookEvent } from '@octokit/webhooks'
|
||||
import express from 'express'
|
||||
import { EventEmitter } from 'promise-events'
|
||||
import { ApplicationFunction } from '.'
|
||||
import { Context } from './context'
|
||||
import { GitHubAPI } from './github'
|
||||
import { logger } from './logger'
|
||||
import { LoggerWithTarget, wrapLogger } from './wrap-logger'
|
||||
|
||||
// Some events can't get an authenticated client (#382):
|
||||
function isUnauthenticatedEvent (event: WebhookEvent) {
|
||||
return !event.payload.installation ||
|
||||
(event.name === 'installation' && event.payload.action === 'deleted')
|
||||
}
|
||||
|
||||
/**
|
||||
* The `app` parameter available to `ApplicationFunction`s
|
||||
*
|
||||
* @property {logger} log - A logger
|
||||
*/
|
||||
export class Application {
|
||||
public events: EventEmitter
|
||||
public app: () => string
|
||||
public cache: Cache
|
||||
public router: express.Router
|
||||
public catchErrors: boolean
|
||||
public log: LoggerWithTarget
|
||||
|
||||
constructor (options?: Options) {
|
||||
const opts = options || {} as any
|
||||
this.events = new EventEmitter()
|
||||
this.log = wrapLogger(logger, logger)
|
||||
this.app = opts.app
|
||||
this.cache = opts.cache
|
||||
this.catchErrors = opts.catchErrors || false
|
||||
this.router = opts.router || express.Router() // you can do this?
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an ApplicationFunction into the current Application
|
||||
* @param appFn - Probot application function to load
|
||||
*/
|
||||
public load (appFn: ApplicationFunction | ApplicationFunction[]): Application {
|
||||
if (Array.isArray(appFn)) {
|
||||
appFn.forEach(a => this.load(a))
|
||||
} else {
|
||||
appFn(this)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public async receive (event: WebhookEvent) {
|
||||
if ((event as any).event) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.warn(new Error('Propery `event` is deprecated, use `name`'))
|
||||
event = { name: (event as any).event, ...event }
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
this.events.emit('*', event),
|
||||
this.events.emit(event.name, event),
|
||||
this.events.emit(`${ event.name }.${ event.payload.action }`, event)
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an {@link http://expressjs.com|express} router that can be used to
|
||||
* expose HTTP endpoints
|
||||
*
|
||||
* ```
|
||||
* module.exports = app => {
|
||||
* // Get an express router to expose new HTTP endpoints
|
||||
* const route = app.route('/my-app');
|
||||
*
|
||||
* // Use any middleware
|
||||
* route.use(require('express').static(__dirname + '/public'));
|
||||
*
|
||||
* // Add a new route
|
||||
* route.get('/hello-world', (req, res) => {
|
||||
* res.end('Hello World');
|
||||
* });
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @param path - the prefix for the routes
|
||||
* @returns an [express.Router](http://expressjs.com/en/4x/api.html#router)
|
||||
*/
|
||||
public route (path?: string): express.Router {
|
||||
if (path) {
|
||||
const router = express.Router()
|
||||
this.router.use(path, router)
|
||||
return router
|
||||
} else {
|
||||
return this.router
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for [GitHub webhooks](https://developer.github.com/webhooks/),
|
||||
* which are fired for almost every significant action that users take on
|
||||
* GitHub.
|
||||
*
|
||||
* @param event - the name of the [GitHub webhook
|
||||
* event](https://developer.github.com/webhooks/#events). Most events also
|
||||
* include an "action". For example, the * [`issues`](
|
||||
* https://developer.github.com/v3/activity/events/types/#issuesevent)
|
||||
* event has actions of `assigned`, `unassigned`, `labeled`, `unlabeled`,
|
||||
* `opened`, `edited`, `milestoned`, `demilestoned`, `closed`, and `reopened`.
|
||||
* Often, your bot will only care about one type of action, so you can append
|
||||
* it to the event name with a `.`, like `issues.closed`.
|
||||
*
|
||||
* ```js
|
||||
* app.on('push', context => {
|
||||
* // Code was just pushed.
|
||||
* });
|
||||
*
|
||||
* app.on('issues.opened', context => {
|
||||
* // An issue was just opened.
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param callback - a function to call when the
|
||||
* webhook is received.
|
||||
*/
|
||||
public on (eventName: string | string[], callback: (context: Context) => Promise<void>) {
|
||||
if (typeof eventName === 'string') {
|
||||
|
||||
return this.events.on(eventName, async (event: WebhookEvent) => {
|
||||
const log = this.log.child({ name: 'event', id: event.id })
|
||||
|
||||
try {
|
||||
let github
|
||||
|
||||
if (isUnauthenticatedEvent(event)) {
|
||||
github = await this.auth()
|
||||
log.debug('`context.github` is unauthenticated. See https://probot.github.io/docs/github-api/#unauthenticated-events')
|
||||
} else {
|
||||
github = await this.auth(event.payload.installation!.id, log)
|
||||
}
|
||||
|
||||
const context = new Context(event, github, log)
|
||||
|
||||
await callback(context)
|
||||
} catch (err) {
|
||||
log.error({ err, event })
|
||||
if (!this.catchErrors) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
eventName.forEach(e => this.on(e, callback))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate and get a GitHub client that can be used to make API calls.
|
||||
*
|
||||
* You'll probably want to use `context.github` instead.
|
||||
*
|
||||
* **Note**: `app.auth` is asynchronous, so it needs to be prefixed with a
|
||||
* [`await`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await)
|
||||
* to wait for the magic to happen.
|
||||
*
|
||||
* ```js
|
||||
* module.exports = (app) => {
|
||||
* app.on('issues.opened', async context => {
|
||||
* const github = await app.auth();
|
||||
* });
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @param id - ID of the installation, which can be extracted from
|
||||
* `context.payload.installation.id`. If called without this parameter, the
|
||||
* client wil authenticate [as the app](https://developer.github.com/apps/building-integrations/setting-up-and-registering-github-apps/about-authentication-options-for-github-apps/#authenticating-as-a-github-app)
|
||||
* instead of as a specific installation, which means it can only be used for
|
||||
* [app APIs](https://developer.github.com/v3/apps/).
|
||||
*
|
||||
* @returns An authenticated GitHub API client
|
||||
* @private
|
||||
*/
|
||||
public async auth (id?: number, log = this.log): Promise<GitHubAPI> {
|
||||
if (process.env.GHE_HOST && /^https?:\/\//.test(process.env.GHE_HOST)) {
|
||||
throw new Error('Your \`GHE_HOST\` environment variable should not begin with https:// or http://')
|
||||
}
|
||||
|
||||
const github = GitHubAPI({
|
||||
baseUrl: process.env.GHE_HOST && `https://${process.env.GHE_HOST}/api/v3`,
|
||||
debug: process.env.LOG_LEVEL === 'trace',
|
||||
logger: log.child({ name: 'github', installation: String(id) })
|
||||
})
|
||||
|
||||
// Cache for 1 minute less than GitHub expiry
|
||||
const installationTokenTTL = parseInt(process.env.INSTALLATION_TOKEN_TTL || '3540', 10)
|
||||
|
||||
if (id) {
|
||||
const res = await this.cache.wrap(`app:${id}:token`, () => {
|
||||
log.trace(`creating token for installation`)
|
||||
github.authenticate({ type: 'app', token: this.app() })
|
||||
|
||||
return github.apps.createInstallationToken({ installation_id: id })
|
||||
}, { ttl: installationTokenTTL })
|
||||
|
||||
github.authenticate({ type: 'token', token: res.data.token })
|
||||
} else {
|
||||
github.authenticate({ type: 'app', token: this.app() })
|
||||
}
|
||||
|
||||
return github
|
||||
}
|
||||
}
|
||||
|
||||
// The TypeScript definition for cache-manager does not export the Cache interface so we recreate it here
|
||||
export interface Cache {
|
||||
wrap<T> (key: string, wrapper: (callback: (error: any, result: T) => void) => any, options: CacheConfig): Promise<any>
|
||||
}
|
||||
export interface CacheConfig {
|
||||
ttl: number
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
app: () => string
|
||||
cache: Cache
|
||||
router?: express.Router
|
||||
catchErrors?: boolean
|
||||
}
|
||||
|
|
@ -1,18 +1,28 @@
|
|||
import path from 'path'
|
||||
import { Application } from '../application'
|
||||
import { resolve } from "node:path";
|
||||
|
||||
export = (app: Application) => {
|
||||
const route = app.route()
|
||||
import type { ApplicationFunctionOptions, Probot } from "../index.js";
|
||||
import { loadPackageJson } from "../helpers/load-package-json.js";
|
||||
import { probotView } from "../views/probot.js";
|
||||
|
||||
route.get('/probot', (req, res) => {
|
||||
let pkg
|
||||
try {
|
||||
pkg = require(path.join(process.cwd(), 'package.json'))
|
||||
} catch (e) {
|
||||
pkg = {}
|
||||
}
|
||||
export function defaultApp(
|
||||
_app: Probot,
|
||||
{ getRouter, cwd = process.cwd() }: ApplicationFunctionOptions,
|
||||
) {
|
||||
if (!getRouter) {
|
||||
throw new Error("getRouter() is required for defaultApp");
|
||||
}
|
||||
|
||||
res.render('probot.hbs', pkg)
|
||||
})
|
||||
route.get('/', (req, res, next) => res.redirect('/probot'))
|
||||
const pkg = loadPackageJson(resolve(cwd, "package.json"));
|
||||
const probotViewRendered = probotView({
|
||||
name: pkg.name,
|
||||
version: pkg.version,
|
||||
description: pkg.description,
|
||||
});
|
||||
const router = getRouter();
|
||||
|
||||
router.get("/probot", (_req, res) => {
|
||||
res.send(probotViewRendered);
|
||||
});
|
||||
|
||||
router.get("/", (_req, res) => res.redirect("/probot"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
import sentryStream from 'bunyan-sentry-stream'
|
||||
import Raven from 'raven'
|
||||
import { Application } from '../application'
|
||||
|
||||
export = (app: Application) => {
|
||||
// If sentry is configured, report all logged errors
|
||||
if (process.env.SENTRY_DSN) {
|
||||
app.log.debug(process.env.SENTRY_DSN, 'Errors will be reported to Sentry')
|
||||
Raven.disableConsoleAlerts()
|
||||
Raven.config(process.env.SENTRY_DSN, {
|
||||
autoBreadcrumbs: true
|
||||
}).install()
|
||||
|
||||
app.log.target.addStream(sentryStream(Raven))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,55 +1,210 @@
|
|||
import { exec } from 'child_process'
|
||||
import { Request, Response } from 'express'
|
||||
import { Application } from '../application'
|
||||
import { ManifestCreation } from '../manifest-creation'
|
||||
import { exec } from "node:child_process";
|
||||
|
||||
// use glitch env to get correct domain welcome message
|
||||
// https://glitch.com/help/project/
|
||||
const domain = process.env.PROJECT_DOMAIN || `http://localhost:${process.env.PORT || 3000}`
|
||||
const welcomeMessage = `\nWelcome to Probot! Go to ${domain} to get started.\n`
|
||||
import type { IncomingMessage, ServerResponse } from "http";
|
||||
import { parse as parseQuery } from "querystring";
|
||||
import express from "express";
|
||||
import updateDotenv from "update-dotenv";
|
||||
|
||||
export = async (app: Application, setup: ManifestCreation = new ManifestCreation()) => {
|
||||
// If not on Glitch or Production, create a smee URL
|
||||
if (process.env.NODE_ENV !== 'production' && !(process.env.PROJECT_DOMAIN || process.env.WEBHOOK_PROXY_URL)) {
|
||||
await setup.createWebhookChannel()
|
||||
}
|
||||
import { Probot } from "../probot.js";
|
||||
import { ManifestCreation } from "../manifest-creation.js";
|
||||
import { getLoggingMiddleware } from "../server/logging-middleware.js";
|
||||
import type { ApplicationFunctionOptions } from "../types.js";
|
||||
import { isProduction } from "../helpers/is-production.js";
|
||||
|
||||
const route = app.route()
|
||||
import { importView } from "../views/import.js";
|
||||
import { setupView } from "../views/setup.js";
|
||||
import { successView } from "../views/success.js";
|
||||
|
||||
app.log.info(welcomeMessage)
|
||||
|
||||
route.get('/probot', async (req, res) => {
|
||||
const protocols = req.headers['x-forwarded-proto'] || req.protocol
|
||||
const protocol = typeof protocols === 'string' ? protocols.split(',')[0] : protocols[0]
|
||||
const host = req.headers['x-forwarded-host'] || req.get('host')
|
||||
const baseUrl = `${protocol}://${host}`
|
||||
|
||||
const pkg = setup.pkg
|
||||
const manifest = setup.getManifest(pkg, baseUrl)
|
||||
const createAppUrl = setup.createAppUrl
|
||||
// Pass the manifest to be POST'd
|
||||
res.render('setup.hbs', { pkg, createAppUrl, manifest })
|
||||
})
|
||||
|
||||
route.get('/probot/setup', async (req: Request, res: Response) => {
|
||||
const { code } = req.query
|
||||
const response = await setup.createAppFromCode(code)
|
||||
|
||||
// If using glitch, restart the app
|
||||
if (process.env.PROJECT_DOMAIN) {
|
||||
exec('refresh', (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
app.log.error(err, stderr)
|
||||
}
|
||||
})
|
||||
export const setupAppFactory = (
|
||||
host: string | undefined,
|
||||
port: number | undefined,
|
||||
) =>
|
||||
async function setupApp(
|
||||
app: Probot,
|
||||
{ getRouter }: ApplicationFunctionOptions,
|
||||
) {
|
||||
if (!getRouter) {
|
||||
throw new Error("getRouter is required to use the setup app");
|
||||
}
|
||||
|
||||
res.redirect(`${response}/installations/new`)
|
||||
})
|
||||
const setup: ManifestCreation = new ManifestCreation();
|
||||
const pkg = setup.pkg;
|
||||
|
||||
route.get('/probot/success', async (req, res) => {
|
||||
res.render('success.hbs')
|
||||
})
|
||||
// If not on Glitch or Production, create a smee URL
|
||||
if (
|
||||
!isProduction() &&
|
||||
!(
|
||||
process.env.PROJECT_DOMAIN ||
|
||||
process.env.WEBHOOK_PROXY_URL ||
|
||||
process.env.NO_SMEE_SETUP === "true"
|
||||
)
|
||||
) {
|
||||
await setup.createWebhookChannel();
|
||||
}
|
||||
|
||||
route.get('/', (req, res, next) => res.redirect('/probot'))
|
||||
const route = getRouter();
|
||||
|
||||
route.use(getLoggingMiddleware(app.log));
|
||||
|
||||
printWelcomeMessage(app, host, port);
|
||||
|
||||
route.get("/probot", async (req: IncomingMessage, res: ServerResponse) => {
|
||||
const baseUrl = getBaseUrl(req);
|
||||
const manifest = setup.getManifest(pkg, baseUrl);
|
||||
const createAppUrl = setup.createAppUrl;
|
||||
// Pass the manifest to be POST'd
|
||||
res.writeHead(200, { "content-type": "text/html" }).end(
|
||||
setupView({
|
||||
name: pkg.name,
|
||||
version: pkg.version,
|
||||
description: pkg.description,
|
||||
createAppUrl,
|
||||
manifest,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
route.get(
|
||||
"/probot/setup",
|
||||
async (req: IncomingMessage, res: ServerResponse) => {
|
||||
// @ts-expect-error query could be set by a framework, e.g. express
|
||||
const { code } = req.query || parseQuery(req.url?.split("?")[1] || "");
|
||||
|
||||
if (!code || typeof code !== "string" || code.length === 0) {
|
||||
res
|
||||
.writeHead(400, { "content-type": "text/plain" })
|
||||
.end("code missing or invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await setup.createAppFromCode(code, {
|
||||
// @ts-expect-error
|
||||
request: app.state.request,
|
||||
});
|
||||
|
||||
// If using glitch, restart the app
|
||||
if (process.env.PROJECT_DOMAIN) {
|
||||
exec("refresh", (error) => {
|
||||
if (error) {
|
||||
app.log.error(error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
printRestartMessage(app);
|
||||
}
|
||||
|
||||
res
|
||||
.writeHead(302, {
|
||||
"content-type": "text/plain",
|
||||
location: `${response}/installations/new`,
|
||||
})
|
||||
.end(`Found. Redirecting to ${response}/installations/new`);
|
||||
},
|
||||
);
|
||||
|
||||
const { WEBHOOK_PROXY_URL, GHE_HOST } = process.env;
|
||||
const GH_HOST = `https://${GHE_HOST ?? "github.com"}`;
|
||||
|
||||
const importViewRendered = importView({
|
||||
name: pkg.name,
|
||||
WEBHOOK_PROXY_URL,
|
||||
GH_HOST,
|
||||
});
|
||||
|
||||
route.get(
|
||||
"/probot/import",
|
||||
(_req: IncomingMessage, res: ServerResponse) => {
|
||||
res
|
||||
.writeHead(200, {
|
||||
"content-type": "text/html",
|
||||
})
|
||||
.end(importViewRendered);
|
||||
},
|
||||
);
|
||||
|
||||
route.post(
|
||||
"/probot/import",
|
||||
express.json(),
|
||||
(req: IncomingMessage, res: ServerResponse) => {
|
||||
const { appId, pem, webhook_secret } = (req as unknown as { body: any })
|
||||
.body;
|
||||
if (!appId || !pem || !webhook_secret) {
|
||||
res
|
||||
.writeHead(400, {
|
||||
"content-type": "text/plain",
|
||||
})
|
||||
.end("appId and/or pem and/or webhook_secret missing");
|
||||
return;
|
||||
}
|
||||
updateDotenv({
|
||||
APP_ID: appId,
|
||||
PRIVATE_KEY: `"${pem}"`,
|
||||
WEBHOOK_SECRET: webhook_secret,
|
||||
});
|
||||
res.end();
|
||||
printRestartMessage(app);
|
||||
},
|
||||
);
|
||||
|
||||
const successViewRendered = successView({ name: pkg.name });
|
||||
|
||||
route.get(
|
||||
"/probot/success",
|
||||
(_req: IncomingMessage, res: ServerResponse) => {
|
||||
res
|
||||
.writeHead(200, { "content-type": "text/html" })
|
||||
.end(successViewRendered);
|
||||
},
|
||||
);
|
||||
|
||||
route.get("/", (_req, res: ServerResponse) =>
|
||||
res
|
||||
.writeHead(302, { "content-type": "text/plain", location: `/probot` })
|
||||
.end(`Found. Redirecting to /probot`),
|
||||
);
|
||||
};
|
||||
|
||||
function printWelcomeMessage(
|
||||
app: Probot,
|
||||
host: string | undefined,
|
||||
port: number | undefined,
|
||||
) {
|
||||
// use glitch env to get correct domain welcome message
|
||||
// https://glitch.com/help/project/
|
||||
const domain =
|
||||
process.env.PROJECT_DOMAIN ||
|
||||
`http://${host ?? "localhost"}:${port || 3000}`;
|
||||
|
||||
[
|
||||
``,
|
||||
`Welcome to Probot!`,
|
||||
`Probot is in setup mode, webhooks cannot be received and`,
|
||||
`custom routes will not work until APP_ID and PRIVATE_KEY`,
|
||||
`are configured in .env.`,
|
||||
`Please follow the instructions at ${domain} to configure .env.`,
|
||||
`Once you are done, restart the server.`,
|
||||
``,
|
||||
].forEach((line) => {
|
||||
app.log.info(line);
|
||||
});
|
||||
}
|
||||
|
||||
function printRestartMessage(app: Probot) {
|
||||
app.log.info("");
|
||||
app.log.info("Probot has been set up, please restart the server!");
|
||||
app.log.info("");
|
||||
}
|
||||
|
||||
function getBaseUrl(req: IncomingMessage): string {
|
||||
const protocols =
|
||||
req.headers["x-forwarded-proto"] ||
|
||||
// @ts-expect-error based on the functionality of express
|
||||
req.socket?.encrypted
|
||||
? "https"
|
||||
: "http";
|
||||
const protocol =
|
||||
typeof protocols === "string" ? protocols.split(",")[0] : protocols[0];
|
||||
const host = req.headers["x-forwarded-host"] || req.headers.host;
|
||||
const baseUrl = `${protocol}://${host}`;
|
||||
return baseUrl;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,86 +0,0 @@
|
|||
import { AnyResponse } from '@octokit/rest'
|
||||
import { Request,Response } from 'express'
|
||||
|
||||
// Built-in app to expose stats about the deployment
|
||||
module.exports = async (app: any): Promise<void> => {
|
||||
if (process.env.DISABLE_STATS) {
|
||||
return
|
||||
}
|
||||
|
||||
const REFRESH_INTERVAL = 60 * 60 * 1000
|
||||
|
||||
// Cache of stats that get reported
|
||||
const stats = { installations: 0, popular: [{}] }
|
||||
|
||||
// Refresh the stats when the ApplicationFunction is loaded
|
||||
const initializing = refresh()
|
||||
|
||||
// Refresh the stats on an interval
|
||||
setInterval(refresh, REFRESH_INTERVAL)
|
||||
|
||||
// Check for accounts (typically spammy or abusive) to ignore
|
||||
const ignoredAccounts = (process.env.IGNORED_ACCOUNTS || '').toLowerCase().split(',')
|
||||
|
||||
// Setup /probot/stats endpoint to return cached stats
|
||||
app.router.get('/probot/stats', async (req: Request, res: Response) => {
|
||||
// ensure stats are loaded
|
||||
await initializing
|
||||
res.json(stats)
|
||||
})
|
||||
|
||||
async function refresh () {
|
||||
const installations = await getInstallations()
|
||||
|
||||
stats.installations = installations.length
|
||||
stats.popular = await popularInstallations(installations)
|
||||
}
|
||||
|
||||
async function getInstallations (): Promise<Installation[]> {
|
||||
const github = await app.auth()
|
||||
const req = github.apps.getInstallations({ per_page: 100 })
|
||||
return github.paginate(req, (res: AnyResponse) => res.data)
|
||||
}
|
||||
|
||||
async function popularInstallations (installations: Installation[]): Promise<Account[]> {
|
||||
let popular = await Promise.all(installations.map(async (installation) => {
|
||||
const { account } = installation
|
||||
|
||||
if (ignoredAccounts.includes(account.login.toLowerCase())) {
|
||||
account.stars = 0
|
||||
app.log.debug({ installation }, 'Installation is ignored')
|
||||
return account
|
||||
}
|
||||
|
||||
const github = await app.auth(installation.id)
|
||||
|
||||
const req = github.apps.getInstallationRepositories({ per_page: 100 })
|
||||
const repositories: Repository[] = await github.paginate(req, (res: AnyResponse) => {
|
||||
return res.data.repositories.filter((repository: Repository) => !repository.private)
|
||||
})
|
||||
|
||||
account.stars = repositories.reduce((stars, repository) => {
|
||||
return stars + repository.stargazers_count
|
||||
}, 0)
|
||||
|
||||
return account
|
||||
}))
|
||||
|
||||
popular = popular.filter(installation => installation.stars > 0)
|
||||
return popular.sort((a, b) => b.stars - a.stars).slice(0, 10)
|
||||
}
|
||||
}
|
||||
|
||||
interface Installation {
|
||||
id: number
|
||||
account: Account
|
||||
}
|
||||
|
||||
interface Account {
|
||||
stars: number
|
||||
login: string
|
||||
}
|
||||
|
||||
interface Repository {
|
||||
private: boolean
|
||||
stargazers_count: number
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { getAuthenticatedOctokit } from "./octokit/get-authenticated-octokit.js";
|
||||
import { ProbotOctokit } from "./octokit/probot-octokit.js";
|
||||
import type { State } from "./types.js";
|
||||
|
||||
/**
|
||||
* Authenticate and get a GitHub client that can be used to make API calls.
|
||||
*
|
||||
* You'll probably want to use `context.octokit` instead.
|
||||
*
|
||||
* **Note**: `app.auth` is asynchronous, so it needs to be prefixed with a
|
||||
* [`await`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await)
|
||||
* to wait for the magic to happen.
|
||||
*
|
||||
* ```js
|
||||
* export default (app) => {
|
||||
* app.on('issues.opened', async context => {
|
||||
* const octokit = await app.auth();
|
||||
* });
|
||||
* };
|
||||
* ```
|
||||
* @param state - Probot application instance state, which is used to persist
|
||||
*
|
||||
* @param id - ID of the installation, which can be extracted from
|
||||
* `context.payload.installation.id`. If called without this parameter, the
|
||||
* client wil authenticate [as the app](https://docs.github.com/en/developers/apps/authenticating-with-github-apps#authenticating-as-a-github-app)
|
||||
* instead of as a specific installation, which means it can only be used for
|
||||
* [app APIs](https://docs.github.com/apps/).
|
||||
*
|
||||
* @returns An authenticated GitHub API client
|
||||
*/
|
||||
export async function auth(
|
||||
state: State,
|
||||
installationId?: number,
|
||||
): Promise<InstanceType<typeof ProbotOctokit>> {
|
||||
return getAuthenticatedOctokit(Object.assign({}, state), installationId);
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
// Usage: probot receive -e push -p path/to/payload app.js
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { randomUUID as uuidv4 } from "node:crypto";
|
||||
|
||||
import express, { Router } from "express";
|
||||
import { config as dotenvConfig } from "dotenv";
|
||||
dotenvConfig();
|
||||
|
||||
import { program } from "commander";
|
||||
import { getPrivateKey } from "@probot/get-private-key";
|
||||
import { getLog } from "../helpers/get-log.js";
|
||||
|
||||
import { Probot, type ApplicationFunctionOptions } from "../index.js";
|
||||
import { resolveAppFunction } from "../helpers/resolve-app-function.js";
|
||||
|
||||
async function main() {
|
||||
program
|
||||
.usage("[options] [path/to/app.js...]")
|
||||
.option(
|
||||
"-e, --event <event-name>",
|
||||
"Event name",
|
||||
process.env.GITHUB_EVENT_NAME,
|
||||
)
|
||||
.option(
|
||||
"-p, --payload-path <payload-path>",
|
||||
"Path to the event payload",
|
||||
process.env.GITHUB_EVENT_PATH,
|
||||
)
|
||||
.option(
|
||||
"-t, --token <access-token>",
|
||||
"Access token",
|
||||
process.env.GITHUB_TOKEN,
|
||||
)
|
||||
.option("-a, --app <id>", "ID of the GitHub App", process.env.APP_ID)
|
||||
.option(
|
||||
"-P, --private-key <file>",
|
||||
"Path to private key file (.pem) for the GitHub App",
|
||||
process.env.PRIVATE_KEY_PATH,
|
||||
)
|
||||
.option(
|
||||
"-L, --log-level <level>",
|
||||
'One of: "trace" | "debug" | "info" | "warn" | "error" | "fatal"',
|
||||
process.env.LOG_LEVEL,
|
||||
)
|
||||
.option(
|
||||
"--log-format <format>",
|
||||
'One of: "pretty", "json"',
|
||||
process.env.LOG_LEVEL || "pretty",
|
||||
)
|
||||
.option(
|
||||
"--log-level-in-string",
|
||||
"Set to log levels (trace, debug, info, ...) as words instead of numbers (10, 20, 30, ...)",
|
||||
process.env.LOG_LEVEL_IN_STRING === "true",
|
||||
)
|
||||
.option(
|
||||
"--log-message-key",
|
||||
"Set to the string key for the 'message' in the log JSON object",
|
||||
process.env.LOG_MESSAGE_KEY || "msg",
|
||||
)
|
||||
.option(
|
||||
"--sentry-dsn <dsn>",
|
||||
'Set to your Sentry DSN, e.g. "https://1234abcd@sentry.io/12345"',
|
||||
process.env.SENTRY_DSN,
|
||||
)
|
||||
.option(
|
||||
"--base-url <url>",
|
||||
'GitHub API base URL. If you use GitHub Enterprise Server, and your hostname is "https://github.acme-inc.com", then the root URL is "https://github.acme-inc.com/api/v3"',
|
||||
process.env.GHE_HOST
|
||||
? `${process.env.GHE_PROTOCOL || "https"}://${
|
||||
process.env.GHE_HOST
|
||||
}/api/v3`
|
||||
: "https://api.github.com",
|
||||
)
|
||||
.parse(process.argv);
|
||||
|
||||
const {
|
||||
app: appId,
|
||||
baseUrl,
|
||||
token: githubToken,
|
||||
event,
|
||||
payloadPath,
|
||||
logLevel,
|
||||
logFormat,
|
||||
logLevelInString,
|
||||
logMessageKey,
|
||||
sentryDsn,
|
||||
} = program.opts();
|
||||
|
||||
if (!event || !payloadPath) {
|
||||
program.help();
|
||||
}
|
||||
|
||||
const privateKey = getPrivateKey();
|
||||
if (!githubToken && (!appId || !privateKey)) {
|
||||
console.warn(
|
||||
"No token specified and no certificate found, which means you will not be able to do authenticated requests to GitHub",
|
||||
);
|
||||
}
|
||||
|
||||
const payload = JSON.parse(
|
||||
fs.readFileSync(path.resolve(payloadPath), "utf8"),
|
||||
);
|
||||
const log = getLog({
|
||||
level: logLevel,
|
||||
logFormat,
|
||||
logLevelInString,
|
||||
logMessageKey,
|
||||
sentryDsn,
|
||||
});
|
||||
|
||||
const probot = new Probot({
|
||||
appId,
|
||||
privateKey: String(privateKey),
|
||||
githubToken: githubToken,
|
||||
log,
|
||||
baseUrl: baseUrl,
|
||||
});
|
||||
|
||||
const expressApp = express();
|
||||
const options: ApplicationFunctionOptions = {
|
||||
getRouter: (path: string = "/") => {
|
||||
const newRouter = Router();
|
||||
expressApp.use(path, newRouter);
|
||||
return newRouter;
|
||||
},
|
||||
};
|
||||
|
||||
const appFn = await resolveAppFunction(
|
||||
path.resolve(process.cwd(), program.args[0]),
|
||||
);
|
||||
await probot.load(appFn, options);
|
||||
|
||||
probot.log.debug("Receiving event", event);
|
||||
probot.receive({ name: event, payload, id: uuidv4() }).catch(() => {
|
||||
// Process must exist non-zero to indicate that the action failed to run
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import { run } from "../index.js";
|
||||
|
||||
run(process.argv);
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { resolve } from "node:path";
|
||||
|
||||
import { program } from "commander";
|
||||
import { config as dotenvConfig } from "dotenv";
|
||||
import { isSupportedNodeVersion } from "../helpers/is-supported-node-version.js";
|
||||
import { loadPackageJson } from "../helpers/load-package-json.js";
|
||||
|
||||
/*import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';*/
|
||||
|
||||
dotenvConfig();
|
||||
|
||||
//const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const pkg = loadPackageJson(resolve(__dirname, "package.json"));
|
||||
|
||||
if (!isSupportedNodeVersion()) {
|
||||
console.log(`Node.js version 18 is required. You have ${process.version}.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
program
|
||||
.version(pkg.version || "0.0.0-dev")
|
||||
.usage("<command> [options]")
|
||||
.command("run", "run the bot")
|
||||
.command("receive", "Receive a single event and payload")
|
||||
.on("command:*", (cmd) => {
|
||||
if (!program.commands.find((c) => c.name() == cmd[0])) {
|
||||
console.error(`Invalid command: ${program.args.join(" ")}\n`);
|
||||
program.outputHelp();
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program.parse(process.argv);
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
import { program } from "commander";
|
||||
import { getPrivateKey } from "@probot/get-private-key";
|
||||
import type { Options as PinoOptions } from "@probot/pino";
|
||||
|
||||
import type { Options } from "../types.js";
|
||||
|
||||
export function readCliOptions(
|
||||
argv: string[],
|
||||
): Options & PinoOptions & { args: string[] } {
|
||||
program
|
||||
.usage("[options] <apps...>")
|
||||
.option(
|
||||
"-p, --port <n>",
|
||||
"Port to start the server on",
|
||||
String(process.env.PORT || 3000),
|
||||
)
|
||||
.option("-H --host <host>", "Host to start the server on", process.env.HOST)
|
||||
.option(
|
||||
"-W, --webhook-proxy <url>",
|
||||
"URL of the webhook proxy service.`",
|
||||
process.env.WEBHOOK_PROXY_URL,
|
||||
)
|
||||
.option(
|
||||
"-w, --webhook-path <path>",
|
||||
"URL path which receives webhooks. Ex: `/webhook`",
|
||||
process.env.WEBHOOK_PATH,
|
||||
)
|
||||
.option("-a, --app <id>", "ID of the GitHub App", process.env.APP_ID)
|
||||
.option(
|
||||
"-s, --secret <secret>",
|
||||
"Webhook secret of the GitHub App",
|
||||
process.env.WEBHOOK_SECRET,
|
||||
)
|
||||
.option(
|
||||
"-P, --private-key <file>",
|
||||
"Path to private key file (.pem) for the GitHub App",
|
||||
process.env.PRIVATE_KEY_PATH,
|
||||
)
|
||||
.option(
|
||||
"-L, --log-level <level>",
|
||||
'One of: "trace" | "debug" | "info" | "warn" | "error" | "fatal"',
|
||||
process.env.LOG_LEVEL || "info",
|
||||
)
|
||||
.option(
|
||||
"--log-format <format>",
|
||||
'One of: "pretty", "json"',
|
||||
process.env.LOG_FORMAT,
|
||||
)
|
||||
.option(
|
||||
"--log-level-in-string",
|
||||
"Set to log levels (trace, debug, info, ...) as words instead of numbers (10, 20, 30, ...)",
|
||||
process.env.LOG_LEVEL_IN_STRING === "true",
|
||||
)
|
||||
.option(
|
||||
"--sentry-dsn <dsn>",
|
||||
'Set to your Sentry DSN, e.g. "https://1234abcd@sentry.io/12345"',
|
||||
process.env.SENTRY_DSN,
|
||||
)
|
||||
.option(
|
||||
"--redis-url <url>",
|
||||
'Set to a "redis://" url in order to enable cluster support for request throttling. Example: "redis://:secret@redis-123.redislabs.com:12345/0"',
|
||||
process.env.REDIS_URL,
|
||||
)
|
||||
.option(
|
||||
"--base-url <url>",
|
||||
'GitHub API base URL. If you use GitHub Enterprise Server, and your hostname is "https://github.acme-inc.com", then the root URL is "https://github.acme-inc.com/api/v3"',
|
||||
process.env.GHE_HOST
|
||||
? `${process.env.GHE_PROTOCOL || "https"}://${
|
||||
process.env.GHE_HOST
|
||||
}/api/v3`
|
||||
: "https://api.github.com",
|
||||
)
|
||||
.parse(argv);
|
||||
|
||||
const {
|
||||
app: appId,
|
||||
privateKey: privateKeyPath,
|
||||
redisUrl,
|
||||
...options
|
||||
} = program.opts();
|
||||
|
||||
return {
|
||||
privateKey: getPrivateKey({ filepath: privateKeyPath }) || undefined,
|
||||
appId,
|
||||
redisConfig: redisUrl,
|
||||
args: program.args,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import { getPrivateKey } from "@probot/get-private-key";
|
||||
import type { Options as PinoOptions, LogLevel } from "@probot/pino";
|
||||
|
||||
export function readEnvOptions(env = process.env) {
|
||||
const privateKey = getPrivateKey({ env });
|
||||
|
||||
const logFormat: PinoOptions["logFormat"] =
|
||||
env.LOG_FORMAT && env.LOG_FORMAT.length !== 0
|
||||
? env.LOG_FORMAT === "pretty"
|
||||
? "pretty"
|
||||
: "json"
|
||||
: env.NODE_ENV === "production"
|
||||
? "json"
|
||||
: "pretty";
|
||||
|
||||
return {
|
||||
args: [],
|
||||
privateKey: (privateKey && privateKey.toString()) || undefined,
|
||||
appId: Number(env.APP_ID),
|
||||
port: Number(env.PORT) || 3000,
|
||||
host: env.HOST,
|
||||
secret: env.WEBHOOK_SECRET,
|
||||
webhookPath: env.WEBHOOK_PATH,
|
||||
webhookProxy: env.WEBHOOK_PROXY_URL,
|
||||
logLevel: env.LOG_LEVEL as LogLevel,
|
||||
logFormat: logFormat,
|
||||
logLevelInString: env.LOG_LEVEL_IN_STRING === "true",
|
||||
logMessageKey: env.LOG_MESSAGE_KEY,
|
||||
sentryDsn: env.SENTRY_DSN,
|
||||
redisConfig: env.REDIS_URL,
|
||||
baseUrl: env.GHE_HOST
|
||||
? `${env.GHE_PROTOCOL || "https"}://${env.GHE_HOST}/api/v3`
|
||||
: "https://api.github.com",
|
||||
};
|
||||
}
|
||||
228
src/context.ts
228
src/context.ts
|
|
@ -1,51 +1,78 @@
|
|||
import { WebhookEvent, WebhookPayloadWithRepository } from '@octokit/webhooks'
|
||||
import yaml from 'js-yaml'
|
||||
import path from 'path'
|
||||
import { GitHubAPI } from './github'
|
||||
import { LoggerWithTarget } from './wrap-logger'
|
||||
import path from "node:path";
|
||||
import merge from "deepmerge";
|
||||
|
||||
import type {
|
||||
EmitterWebhookEvent as WebhookEvent,
|
||||
EmitterWebhookEventName as WebhookEvents,
|
||||
} from "@octokit/webhooks";
|
||||
import type { Logger } from "pino";
|
||||
import type { ProbotOctokit } from "./octokit/probot-octokit.js";
|
||||
|
||||
export type MergeOptions = merge.Options;
|
||||
|
||||
/** Repo owner type, either string or never depending on the context */
|
||||
type RepoOwnerType<T extends WebhookEvents> =
|
||||
WebhookEvent<T>["payload"] extends {
|
||||
repository: { owner: { login: string } };
|
||||
}
|
||||
? string
|
||||
: never;
|
||||
|
||||
/** Repo name type, either string or never depending on the context */
|
||||
type RepoNameType<T extends WebhookEvents> =
|
||||
WebhookEvent<T>["payload"] extends { repository: { name: string } }
|
||||
? string
|
||||
: never;
|
||||
|
||||
/** Issue type (also pull request number), either number or never depending on the context */
|
||||
type RepoIssueNumberType<T extends WebhookEvents> =
|
||||
WebhookEvent<T>["payload"] extends { issue: { number: number } }
|
||||
? number
|
||||
: never | WebhookEvent<T>["payload"] extends {
|
||||
pull_request: { number: number };
|
||||
}
|
||||
? number
|
||||
: never | WebhookEvent<T>["payload"] extends { number: number }
|
||||
? number
|
||||
: never;
|
||||
|
||||
/** Context.repo return type */
|
||||
type RepoResultType<E extends WebhookEvents> = {
|
||||
owner: RepoOwnerType<E>;
|
||||
repo: RepoNameType<E>;
|
||||
};
|
||||
|
||||
/**
|
||||
* The context of the event that was triggered, including the payload and
|
||||
* helpers for extracting information can be passed to GitHub API calls.
|
||||
*
|
||||
* ```js
|
||||
* module.exports = app => {
|
||||
* export default app => {
|
||||
* app.on('push', context => {
|
||||
* context.log('Code was pushed to the repo, what should we do with it?');
|
||||
* context.log.info('Code was pushed to the repo, what should we do with it?');
|
||||
* });
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @property {github} github - A GitHub API client
|
||||
* @property {octokit} octokit - An Octokit instance
|
||||
* @property {payload} payload - The webhook event payload
|
||||
* @property {logger} log - A logger
|
||||
* @property {log} log - A pino instance
|
||||
*/
|
||||
export class Context<E extends WebhookEvents = WebhookEvents> {
|
||||
public name: WebhookEvents;
|
||||
public id: string;
|
||||
public payload: WebhookEvent<E>["payload"];
|
||||
|
||||
export class Context implements WebhookEvent {
|
||||
public name: string
|
||||
public id: string
|
||||
public payload: WebhookPayloadWithRepository
|
||||
public protocol?: 'http' | 'https'
|
||||
public host?: string
|
||||
public url?: string
|
||||
public octokit: ProbotOctokit;
|
||||
public log: Logger;
|
||||
|
||||
public github: GitHubAPI
|
||||
public log: LoggerWithTarget
|
||||
constructor(event: WebhookEvent<E>, octokit: ProbotOctokit, log: Logger) {
|
||||
this.name = event.name;
|
||||
this.id = event.id;
|
||||
this.payload = event.payload;
|
||||
|
||||
constructor (event: WebhookEvent, github: GitHubAPI, log: LoggerWithTarget) {
|
||||
this.name = event.name
|
||||
this.id = event.id
|
||||
this.payload = event.payload
|
||||
this.protocol = event.protocol
|
||||
this.host = event.host
|
||||
this.url = event.url
|
||||
|
||||
this.github = github
|
||||
this.log = log
|
||||
}
|
||||
|
||||
// Maintain backward compatability
|
||||
public get event (): string {
|
||||
return this.name
|
||||
this.octokit = octokit;
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -60,44 +87,84 @@ export class Context implements WebhookEvent {
|
|||
* @param object - Params to be merged with the repo params.
|
||||
*
|
||||
*/
|
||||
public repo<T> (object?: T) {
|
||||
const repo = this.payload.repository
|
||||
public repo<T>(object?: T): RepoResultType<E> & T {
|
||||
// @ts-expect-error `repository` is not always present in this.payload
|
||||
const repo = this.payload.repository;
|
||||
|
||||
if (!repo) {
|
||||
throw new Error('context.repo() is not supported for this webhook event.')
|
||||
throw new Error(
|
||||
"context.repo() is not supported for this webhook event.",
|
||||
);
|
||||
}
|
||||
|
||||
return Object.assign({
|
||||
owner: repo.owner.login || repo.owner.name!,
|
||||
repo: repo.name
|
||||
}, object)
|
||||
return Object.assign(
|
||||
{
|
||||
owner: repo.owner.login,
|
||||
repo: repo.name,
|
||||
},
|
||||
object,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the `owner`, `repo`, and `number` params for making API requests
|
||||
* against an issue or pull request. The object passed in will be merged with
|
||||
* the repo params.
|
||||
* Return the `owner`, `repo`, and `issue_number` params for making API requests
|
||||
* against an issue. The object passed in will be merged with the repo params.
|
||||
*
|
||||
*
|
||||
* ```js
|
||||
* const params = context.issue({body: 'Hello World!'})
|
||||
* // Returns: {owner: 'username', repo: 'reponame', number: 123, body: 'Hello World!'}
|
||||
* // Returns: {owner: 'username', repo: 'reponame', issue_number: 123, body: 'Hello World!'}
|
||||
* ```
|
||||
*
|
||||
* @param object - Params to be merged with the issue params.
|
||||
*/
|
||||
public issue<T> (object?: T) {
|
||||
const payload = this.payload
|
||||
return Object.assign({
|
||||
number: (payload.issue || payload.pull_request || payload).number
|
||||
}, this.repo(object))
|
||||
public issue<T>(
|
||||
object?: T,
|
||||
): RepoResultType<E> & { issue_number: RepoIssueNumberType<E> } & T {
|
||||
return Object.assign(
|
||||
{
|
||||
issue_number:
|
||||
// @ts-expect-error - this.payload may not have `issue` or `pull_request` keys
|
||||
(this.payload.issue || this.payload.pull_request || this.payload)
|
||||
.number,
|
||||
},
|
||||
this.repo(object),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the `owner`, `repo`, and `pull_number` params for making API requests
|
||||
* against a pull request. The object passed in will be merged with the repo params.
|
||||
*
|
||||
*
|
||||
* ```js
|
||||
* const params = context.pullRequest({body: 'Hello World!'})
|
||||
* // Returns: {owner: 'username', repo: 'reponame', pull_number: 123, body: 'Hello World!'}
|
||||
* ```
|
||||
*
|
||||
* @param object - Params to be merged with the pull request params.
|
||||
*/
|
||||
public pullRequest<T>(
|
||||
object?: T,
|
||||
): RepoResultType<E> & { pull_number: RepoIssueNumberType<E> } & T {
|
||||
const payload = this.payload;
|
||||
return Object.assign(
|
||||
{
|
||||
// @ts-expect-error - this.payload may not have `issue` or `pull_request` keys
|
||||
pull_number: (payload.issue || payload.pull_request || payload).number,
|
||||
},
|
||||
this.repo(object),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean if the actor on the event was a bot.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isBot () {
|
||||
return this.payload.sender!.type === 'Bot'
|
||||
get isBot() {
|
||||
// @ts-expect-error - `sender` key is currently not present in all events
|
||||
// see https://github.com/octokit/webhooks/issues/510
|
||||
return this.payload.sender.type === "Bot";
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -111,15 +178,15 @@ export class Context implements WebhookEvent {
|
|||
* comment: Check the specs on the rotary girder.
|
||||
* ```
|
||||
*
|
||||
* You app can read that file from the target repository:
|
||||
* Your app can read that file from the target repository:
|
||||
*
|
||||
* ```js
|
||||
* // Load config from .github/config.yml in the repository
|
||||
* const config = await context.config('config.yml')
|
||||
*
|
||||
* if (config.close) {
|
||||
* context.github.issues.comment(context.issue({body: config.comment}))
|
||||
* context.github.issues.edit(context.issue({state: 'closed'}))
|
||||
* context.octokit.issues.comment(context.issue({body: config.comment}))
|
||||
* context.octokit.issues.edit(context.issue({state: 'closed'}))
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
|
|
@ -130,31 +197,50 @@ export class Context implements WebhookEvent {
|
|||
* const config = await context.config('config.yml', {comment: 'Make sure to check all the specs.'})
|
||||
*
|
||||
* if (config.close) {
|
||||
* context.github.issues.comment(context.issue({body: config.comment}));
|
||||
* context.github.issues.edit(context.issue({state: 'closed'}))
|
||||
* context.octokit.issues.comment(context.issue({body: config.comment}));
|
||||
* context.octokit.issues.edit(context.issue({state: 'closed'}))
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Config files can also specify a base that they extend. `deepMergeOptions` can be used
|
||||
* to configure how the target config, extended base, and default configs are merged.
|
||||
*
|
||||
* For security reasons, configuration is only loaded from the repository's default branch,
|
||||
* changes made in pull requests from different branches or forks are ignored.
|
||||
*
|
||||
* If you need more lower-level control over reading and merging configuration files,
|
||||
* you can `context.octokit.config.get(options)`, see https://github.com/probot/octokit-plugin-config.
|
||||
*
|
||||
* @param fileName - Name of the YAML file in the `.github` directory
|
||||
* @param defaultConfig - An object of default config options
|
||||
* @param deepMergeOptions - Controls merging configs (from the [deepmerge](https://github.com/TehShrike/deepmerge) module)
|
||||
* @return Configuration object read from the file
|
||||
*/
|
||||
public async config<T> (fileName: string, defaultConfig?: T) {
|
||||
const params = this.repo({ path: path.posix.join('.github', fileName) })
|
||||
public async config<T>(
|
||||
fileName: string,
|
||||
defaultConfig?: T,
|
||||
deepMergeOptions?: MergeOptions,
|
||||
): Promise<T | null> {
|
||||
const params = this.repo({
|
||||
path: path.posix.join(".github", fileName),
|
||||
defaults(configs: object[]) {
|
||||
const result = merge.all(
|
||||
[defaultConfig || {}, ...configs],
|
||||
deepMergeOptions,
|
||||
);
|
||||
|
||||
try {
|
||||
const res = await this.github.repos.getContent(params)
|
||||
const config = yaml.safeLoad(Buffer.from(res.data.content, 'base64').toString()) || {}
|
||||
return Object.assign({}, defaultConfig, config)
|
||||
} catch (err) {
|
||||
if (err.code === 404) {
|
||||
if (defaultConfig) {
|
||||
return defaultConfig
|
||||
}
|
||||
return null
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-expect-error
|
||||
const { config, files } = await this.octokit.config.get(params);
|
||||
|
||||
// if no default config is set, and no config files are found, return null
|
||||
if (!defaultConfig && !files.find((file) => file.config !== null)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return config as T;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
import type { RequestListener } from "http";
|
||||
import { createNodeMiddleware as createWebhooksMiddleware } from "@octokit/webhooks";
|
||||
|
||||
import type { ApplicationFunction, MiddlewareOptions } from "./types.js";
|
||||
import { defaultWebhooksPath } from "./server/server.js";
|
||||
import { createProbot } from "./create-probot.js";
|
||||
|
||||
export function createNodeMiddleware(
|
||||
appFn: ApplicationFunction,
|
||||
{ probot = createProbot(), webhooksPath } = {} as MiddlewareOptions,
|
||||
): RequestListener {
|
||||
probot.load(appFn);
|
||||
|
||||
return createWebhooksMiddleware(probot.webhooks, {
|
||||
path: webhooksPath || probot.webhookPath || defaultWebhooksPath,
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
import type { LogLevel, Options as PinoOptions } from "@probot/pino";
|
||||
import { getPrivateKey } from "@probot/get-private-key";
|
||||
|
||||
import { getLog } from "./helpers/get-log.js";
|
||||
import type { Options } from "./types.js";
|
||||
import { Probot } from "./probot.js";
|
||||
import { defaultWebhooksPath } from "./server/server.js";
|
||||
|
||||
type CreateProbotOptions = {
|
||||
overrides?: Options;
|
||||
defaults?: Options;
|
||||
env?: Partial<NodeJS.ProcessEnv>;
|
||||
};
|
||||
|
||||
const DEFAULTS: Partial<NodeJS.ProcessEnv> = {
|
||||
APP_ID: "",
|
||||
WEBHOOK_SECRET: "",
|
||||
WEBHOOK_PATH: defaultWebhooksPath,
|
||||
GHE_HOST: "",
|
||||
GHE_PROTOCOL: "https",
|
||||
LOG_FORMAT: undefined,
|
||||
LOG_LEVEL: "warn",
|
||||
LOG_LEVEL_IN_STRING: "false",
|
||||
LOG_MESSAGE_KEY: "msg",
|
||||
REDIS_URL: "",
|
||||
SENTRY_DSN: "",
|
||||
};
|
||||
|
||||
/**
|
||||
* Merges configuration from defaults/environment variables/overrides and returns
|
||||
* a Probot instance. Finds private key using [`@probot/get-private-key`](https://github.com/probot/get-private-key).
|
||||
*
|
||||
* @see https://probot.github.io/docs/configuration/
|
||||
* @param defaults default Options, will be overwritten if according environment variable is set
|
||||
* @param overrides overwrites defaults and according environment variables
|
||||
* @param env defaults to process.env
|
||||
*/
|
||||
export function createProbot({
|
||||
overrides = {},
|
||||
defaults = {},
|
||||
env = process.env,
|
||||
}: CreateProbotOptions = {}) {
|
||||
const privateKey = getPrivateKey({ env });
|
||||
const envWithDefaults = { ...DEFAULTS, ...env };
|
||||
|
||||
const envOptions: Options = {
|
||||
logLevel: envWithDefaults.LOG_LEVEL as LogLevel,
|
||||
appId: Number(envWithDefaults.APP_ID),
|
||||
privateKey: (privateKey && privateKey.toString()) || undefined,
|
||||
secret: envWithDefaults.WEBHOOK_SECRET,
|
||||
redisConfig: envWithDefaults.REDIS_URL,
|
||||
webhookPath: envWithDefaults.WEBHOOK_PATH,
|
||||
baseUrl: envWithDefaults.GHE_HOST
|
||||
? `${envWithDefaults.GHE_PROTOCOL || "https"}://${
|
||||
envWithDefaults.GHE_HOST
|
||||
}/api/v3`
|
||||
: "https://api.github.com",
|
||||
};
|
||||
|
||||
const probotOptions = {
|
||||
...defaults,
|
||||
...envOptions,
|
||||
...overrides,
|
||||
};
|
||||
|
||||
const log = getLog({
|
||||
level: probotOptions.logLevel,
|
||||
logFormat: envWithDefaults.LOG_FORMAT as PinoOptions["logFormat"],
|
||||
logLevelInString: envWithDefaults.LOG_LEVEL_IN_STRING === "true",
|
||||
logMessageKey: envWithDefaults.LOG_MESSAGE_KEY,
|
||||
sentryDsn: envWithDefaults.SENTRY_DSN,
|
||||
}).child({ name: "server" });
|
||||
|
||||
return new Probot({
|
||||
log: log.child({ name: "probot" }),
|
||||
...probotOptions,
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { Router } from "express";
|
||||
|
||||
/**
|
||||
* Get an {@link http://expressjs.com|express} router that can be used to
|
||||
* expose HTTP endpoints
|
||||
*
|
||||
* @param path - the prefix for the routes
|
||||
* @returns an [express.Router](http://expressjs.com/en/4x/api.html#router)
|
||||
*/
|
||||
export function getRouter(router: Router, path?: string): Router {
|
||||
if (path) {
|
||||
const newRouter = Router();
|
||||
router.use(path, newRouter);
|
||||
return newRouter;
|
||||
}
|
||||
|
||||
return router;
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import jwt from 'jsonwebtoken'
|
||||
|
||||
export const createApp = (options: AppOptions) => {
|
||||
return () => {
|
||||
const payload = {
|
||||
exp: Math.floor(Date.now() / 1000) + 60, // JWT expiration time
|
||||
iat: Math.floor(Date.now() / 1000), // Issued at time
|
||||
iss: options.id // GitHub App ID
|
||||
}
|
||||
|
||||
// Sign with RSA SHA256
|
||||
return jwt.sign(payload, options.cert, { algorithm: 'RS256' })
|
||||
}
|
||||
}
|
||||
|
||||
export interface AppOptions {
|
||||
id: number
|
||||
cert: string
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
import { GitHubAPI, Headers, Variables } from './'
|
||||
|
||||
class GraphQLError extends Error {
|
||||
public query: string
|
||||
public variables: Variables
|
||||
|
||||
constructor (errors: Error[], query: string, variables: Variables) {
|
||||
super(JSON.stringify(errors))
|
||||
this.name = 'GraphQLError'
|
||||
this.query = query
|
||||
this.variables = variables
|
||||
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, GraphQLError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function addGraphQL (client: GitHubAPI) {
|
||||
client.query = graphql.bind(null, client)
|
||||
}
|
||||
|
||||
async function graphql (client: GitHubAPI, query: string, variables: Variables, headers: Headers = {}) {
|
||||
const res = await client.request({
|
||||
baseUrl: process.env.GHE_HOST && `https://${process.env.GHE_HOST}/api`,
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
'content-type': 'application/json',
|
||||
...headers
|
||||
},
|
||||
method: 'POST',
|
||||
url: '/graphql',
|
||||
|
||||
query,
|
||||
variables
|
||||
})
|
||||
|
||||
if (res.data.errors) {
|
||||
throw new GraphQLError(res.data.errors, query, variables)
|
||||
}
|
||||
|
||||
return res.data.data
|
||||
}
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
import Octokit from '@octokit/rest'
|
||||
import { addGraphQL } from './graphql'
|
||||
import { addLogging, Logger } from './logging'
|
||||
import { addPagination } from './pagination'
|
||||
import { addRateLimiting } from './rate-limiting'
|
||||
|
||||
/**
|
||||
* the [@octokit/rest Node.js module](https://github.com/octokit/rest.js),
|
||||
* which wraps the [GitHub API](https://developer.github.com/v3/) and allows
|
||||
* you to do almost anything programmatically that you can do through a web
|
||||
* browser.
|
||||
* @see {@link https://github.com/octokit/rest.js}
|
||||
*/
|
||||
export function GitHubAPI (options: Options = {} as any) {
|
||||
const octokit = new Octokit(options) as GitHubAPI
|
||||
|
||||
addRateLimiting(octokit, options.limiter)
|
||||
addLogging(octokit, options.logger)
|
||||
addPagination(octokit)
|
||||
addGraphQL(octokit)
|
||||
|
||||
return octokit
|
||||
}
|
||||
|
||||
export interface Options extends Octokit.Options {
|
||||
debug?: boolean
|
||||
logger: Logger
|
||||
limiter?: any
|
||||
}
|
||||
|
||||
export interface RequestOptions {
|
||||
baseUrl?: string
|
||||
method: string
|
||||
url: string
|
||||
headers: any
|
||||
query?: string
|
||||
variables?: Variables
|
||||
}
|
||||
|
||||
export interface Result {
|
||||
headers: {
|
||||
status: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface OctokitError extends Error {
|
||||
code: number
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface GitHubAPI extends Octokit {
|
||||
paginate: (res: Promise<Octokit.AnyResponse>, callback: (results: Octokit.AnyResponse) => void) => Promise<any[]>
|
||||
// The following are added because Octokit does not expose the hook.error, hook.before, and hook.after methods
|
||||
hook: {
|
||||
error: (when: 'request', callback: (error: OctokitError, options: RequestOptions) => void) => void
|
||||
before: (when: 'request', callback: (result: Result, options: RequestOptions) => void) => void
|
||||
after: (when: 'request', callback: (result: Result, options: RequestOptions) => void) => void
|
||||
}
|
||||
|
||||
request: (RequestOptions: RequestOptions) => Promise<Octokit.AnyResponse>
|
||||
query: (query: string, variables?: Variables, headers?: Headers) => Promise<GraphQLResponse>
|
||||
}
|
||||
|
||||
export interface Headers {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
export interface Variables { [key: string]: any }
|
||||
|
||||
export interface GraphQLResponse {
|
||||
data: any
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import Logger from 'bunyan'
|
||||
import { GitHubAPI } from './'
|
||||
|
||||
export function addLogging (client: GitHubAPI, logger: Logger) {
|
||||
if (!logger) {
|
||||
return
|
||||
}
|
||||
|
||||
client.hook.error('request', (error, options) => {
|
||||
const { method, url, headers, ...params } = options
|
||||
const msg = `GitHub request: ${method} ${url} - ${error.code} ${error.status}`
|
||||
logger.debug({ params }, msg)
|
||||
throw error
|
||||
})
|
||||
client.hook.after('request', (result, options) => {
|
||||
const { method, url, headers, ...params } = options
|
||||
const msg = `GitHub request: ${method} ${url} - ${result.headers.status}`
|
||||
logger.debug({ params }, msg)
|
||||
})
|
||||
}
|
||||
|
||||
export { Logger }
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { AnyResponse } from '@octokit/rest'
|
||||
import { GitHubAPI } from './'
|
||||
|
||||
export function addPagination (octokit: GitHubAPI) {
|
||||
octokit.paginate = paginate.bind(null, octokit)
|
||||
}
|
||||
|
||||
const defaultCallback = (response: Promise<AnyResponse>, done?: () => void) => response
|
||||
|
||||
async function paginate (octokit: GitHubAPI, responsePromise: any, callback = defaultCallback) {
|
||||
let collection: any[] = []
|
||||
let getNextPage = true
|
||||
|
||||
const done = () => {
|
||||
getNextPage = false
|
||||
}
|
||||
|
||||
let response = await responsePromise
|
||||
|
||||
collection = collection.concat(await callback(response, done))
|
||||
|
||||
// eslint-disable-next-line no-unmodified-loop-condition
|
||||
while (getNextPage && octokit.hasNextPage(response)) {
|
||||
response = await octokit.getNextPage(response)
|
||||
collection = collection.concat(await callback(response, done))
|
||||
}
|
||||
return collection
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import Bottleneck from 'bottleneck'
|
||||
import { GitHubAPI } from './'
|
||||
|
||||
export function addRateLimiting (octokit: GitHubAPI, limiter: Bottleneck) {
|
||||
if (!limiter) {
|
||||
limiter = new Bottleneck({
|
||||
maxConcurrent: 1,
|
||||
minTime: 1000
|
||||
})
|
||||
}
|
||||
|
||||
const noop = () => Promise.resolve()
|
||||
octokit.hook.before('request', limiter.schedule.bind(limiter, noop))
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import type { Logger } from "pino";
|
||||
import type {
|
||||
WebhookError,
|
||||
EmitterWebhookEvent as WebhookEvent,
|
||||
} from "@octokit/webhooks";
|
||||
|
||||
export function getErrorHandler(log: Logger) {
|
||||
return (error: Error & { event?: WebhookEvent }) => {
|
||||
const errors = (
|
||||
error.name === "AggregateError" ? error : [error]
|
||||
) as WebhookError[];
|
||||
|
||||
const event = error.event;
|
||||
|
||||
for (const error of errors) {
|
||||
const errMessage = (error.message || "").toLowerCase();
|
||||
|
||||
if (errMessage.includes("x-hub-signature-256")) {
|
||||
log.error(
|
||||
error,
|
||||
"Go to https://github.com/settings/apps/YOUR_APP and verify that the Webhook secret matches the value of the WEBHOOK_SECRET environment variable.",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (errMessage.includes("pem") || errMessage.includes("json web token")) {
|
||||
log.error(
|
||||
error,
|
||||
"Your private key (a .pem file or PRIVATE_KEY environment variable) or APP_ID is incorrect. Go to https://github.com/settings/apps/YOUR_APP, verify that APP_ID is set correctly, and generate a new PEM file if necessary.",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
log
|
||||
.child({
|
||||
name: "event",
|
||||
id: event?.id,
|
||||
})
|
||||
.error(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* A logger backed by [pino](https://getpino.io/)
|
||||
*
|
||||
* The default log level is `info`, but you can change it passing a level
|
||||
* string set to one of: `"trace"`, `"debug"`, `"info"`, `"warn"`,
|
||||
* `"error"`, or `"fatal"`.
|
||||
*
|
||||
* ```js
|
||||
* app.log.debug("…so is this");
|
||||
* app.log.trace("Now we're talking");
|
||||
* app.log.info("I thought you should know…");
|
||||
* app.log.warn("Woah there");
|
||||
* app.log.error("ETOOMANYLOGS");
|
||||
* app.log.fatal("Goodbye, cruel world!");
|
||||
* ```
|
||||
*/
|
||||
import { pino } from "pino";
|
||||
import type { Logger, LoggerOptions } from "pino";
|
||||
import { getTransformStream, type Options, type LogLevel } from "@probot/pino";
|
||||
import { rebindLog } from "./rebind-log";
|
||||
|
||||
export type GetLogOptions = {
|
||||
level?: LogLevel;
|
||||
logMessageKey?: string;
|
||||
} & Options;
|
||||
|
||||
export function getLog(options: GetLogOptions = {}): Logger {
|
||||
const { level, logMessageKey, ...getTransformStreamOptions } = options;
|
||||
|
||||
const pinoOptions: LoggerOptions = {
|
||||
level: level || "info",
|
||||
name: "probot",
|
||||
messageKey: logMessageKey || "msg",
|
||||
};
|
||||
const transform = getTransformStream(getTransformStreamOptions);
|
||||
transform.pipe(pino.destination(1));
|
||||
|
||||
return rebindLog(pino(pinoOptions, transform));
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export function isProduction() {
|
||||
return process.env.NODE_ENV === "production";
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export function isSupportedNodeVersion(nodeVersion = process.versions.node) {
|
||||
return Number(nodeVersion.split(".", 10)[0]) >= 18;
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import type { PackageJson } from "../types.js";
|
||||
|
||||
export function loadPackageJson(
|
||||
filepath = path.join(process.cwd(), "package.json"),
|
||||
): PackageJson {
|
||||
let pkgContent;
|
||||
try {
|
||||
pkgContent = fs.readFileSync(filepath, "utf8");
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
const pkg = pkgContent && JSON.parse(pkgContent);
|
||||
if (pkg && typeof pkg === "object") {
|
||||
return pkg;
|
||||
}
|
||||
return {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import type { Logger } from "pino";
|
||||
|
||||
const kIsBound = Symbol("is-bound");
|
||||
|
||||
export function rebindLog(log: Logger): Logger {
|
||||
// @ts-ignore
|
||||
if (log[kIsBound]) return log;
|
||||
for (const key in log) {
|
||||
// @ts-ignore
|
||||
if (typeof log[key] !== "function") continue;
|
||||
// @ts-ignore
|
||||
log[key] = log[key].bind(log);
|
||||
}
|
||||
// @ts-ignore
|
||||
log[kIsBound] = true;
|
||||
return log;
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import resolveModule from "resolve";
|
||||
|
||||
const defaultOptions: ResolveOptions = {};
|
||||
|
||||
export const resolveAppFunction = async (
|
||||
appFnId: string,
|
||||
opts?: ResolveOptions,
|
||||
) => {
|
||||
opts = opts || defaultOptions;
|
||||
// These are mostly to ease testing
|
||||
const basedir = opts.basedir || process.cwd();
|
||||
const resolver: Resolver = opts.resolver || resolveModule.sync;
|
||||
const appFnPath = resolver(appFnId, { basedir });
|
||||
// On windows, an absolute path may start with a drive letter, e.g. C:/path/to/file.js
|
||||
// This can be interpreted as a protocol, so ensure it's prefixed with file://
|
||||
const appFnPathWithFileProtocol = appFnPath.replace(
|
||||
/^([a-zA-Z]:)/,
|
||||
"file://$1",
|
||||
);
|
||||
const { default: mod } = await import(appFnPathWithFileProtocol);
|
||||
// mod.default gets exported by transpiled TypeScript code
|
||||
return mod.__esModule && mod.default ? mod.default : mod;
|
||||
};
|
||||
|
||||
export type Resolver = (appFnId: string, opts: { basedir: string }) => string;
|
||||
|
||||
export interface ResolveOptions {
|
||||
basedir?: string;
|
||||
resolver?: Resolver;
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import EventSource from "eventsource";
|
||||
|
||||
import type { Logger } from "pino";
|
||||
|
||||
export const createWebhookProxy = async (
|
||||
opts: WebhookProxyOptions,
|
||||
): Promise<EventSource | undefined> => {
|
||||
try {
|
||||
const SmeeClient = (await import("smee-client")).default;
|
||||
const smee = new SmeeClient({
|
||||
logger: opts.logger,
|
||||
source: opts.url,
|
||||
target: `http://localhost:${opts.port}${opts.path}`,
|
||||
fetch: opts.fetch,
|
||||
});
|
||||
return smee.start();
|
||||
} catch (error) {
|
||||
opts.logger.warn(
|
||||
"Run `npm install --save-dev smee-client` to proxy webhooks to localhost.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export interface WebhookProxyOptions {
|
||||
url: string;
|
||||
port?: number;
|
||||
path?: string;
|
||||
logger: Logger;
|
||||
fetch?: Function;
|
||||
}
|
||||
259
src/index.ts
259
src/index.ts
|
|
@ -1,129 +1,154 @@
|
|||
import Webhooks, { WebhookEvent } from '@octokit/webhooks'
|
||||
import Logger from 'bunyan'
|
||||
import cacheManager from 'cache-manager'
|
||||
import express from 'express'
|
||||
import { Application } from './application'
|
||||
import { Context } from './context'
|
||||
import { createApp } from './github-app'
|
||||
import { logger } from './logger'
|
||||
import { resolve } from './resolver'
|
||||
import { createServer } from './server'
|
||||
import { createWebhookProxy } from './webhook-proxy'
|
||||
export type { Logger } from "pino";
|
||||
|
||||
// tslint:disable:no-var-requires
|
||||
// These needs types
|
||||
const logRequestErrors = require('./middleware/log-request-errors')
|
||||
export { Context } from "./context.js";
|
||||
|
||||
const cache = cacheManager.caching({
|
||||
store: 'memory',
|
||||
ttl: 60 * 60 // 1 hour
|
||||
})
|
||||
export { Probot } from "./probot.js";
|
||||
export { Server } from "./server/server.js";
|
||||
export { ProbotOctokit } from "./octokit/probot-octokit.js";
|
||||
export { run } from "./run.js";
|
||||
export { createNodeMiddleware } from "./create-node-middleware.js";
|
||||
export { createProbot } from "./create-probot.js";
|
||||
|
||||
const defaultAppFns: ApplicationFunction[] = [
|
||||
require('./apps/default'),
|
||||
require('./apps/sentry'),
|
||||
require('./apps/stats')
|
||||
]
|
||||
// tslint:enable:no-var-requires
|
||||
/** NOTE: exported types might change at any point in time */
|
||||
export type {
|
||||
Options,
|
||||
ApplicationFunction,
|
||||
ApplicationFunctionOptions,
|
||||
} from "./types.js";
|
||||
|
||||
export class Probot {
|
||||
public server: express.Application
|
||||
public webhook: Webhooks
|
||||
public logger: Logger
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
/**
|
||||
* The App ID assigned to your GitHub App.
|
||||
* @example '1234'
|
||||
*/
|
||||
APP_ID?: string;
|
||||
|
||||
private options: Options
|
||||
private apps: Application[]
|
||||
private app: () => string
|
||||
/**
|
||||
* By default, logs are formatted for readability in development. You can
|
||||
* set this to `json` in order to disable the formatting.
|
||||
*/
|
||||
LOG_FORMAT?: "json" | "pretty";
|
||||
|
||||
constructor (options: Options) {
|
||||
options.webhookPath = options.webhookPath || '/'
|
||||
options.secret = options.secret || 'development'
|
||||
this.options = options
|
||||
this.logger = logger
|
||||
this.apps = []
|
||||
this.webhook = new Webhooks({ path: options.webhookPath, secret: options.secret })
|
||||
this.app = createApp({ id: options.id, cert: options.cert })
|
||||
this.server = createServer({ webhook: this.webhook.middleware, logger })
|
||||
/**
|
||||
* The verbosity of logs to show when running your app, which can be
|
||||
* `fatal`, `error`, `warn`, `info`, `debug`, `trace` or `silent`.
|
||||
* @default 'info'
|
||||
*/
|
||||
LOG_LEVEL?:
|
||||
| "trace"
|
||||
| "debug"
|
||||
| "info"
|
||||
| "warn"
|
||||
| "error"
|
||||
| "fatal"
|
||||
| "silent";
|
||||
|
||||
// Log all received webhooks
|
||||
this.webhook.on('*', (event: WebhookEvent) => {
|
||||
return this.receive(event)
|
||||
})
|
||||
/**
|
||||
* By default, when using the `json` format, the level printed in the log
|
||||
* records is an int (`10`, `20`, ..). This option tells the logger to
|
||||
* print level as a string: `{"level": "info"}`. Default `false`
|
||||
*/
|
||||
LOG_LEVEL_IN_STRING?: "true" | "false";
|
||||
|
||||
// Log all webhook errors
|
||||
this.webhook.on('error', this.errorHandler)
|
||||
}
|
||||
/**
|
||||
* Only relevant when `LOG_FORMAT` is set to `json`. Sets the json key for the log message.
|
||||
* @default 'msg'
|
||||
*/
|
||||
LOG_MESSAGE_KEY?: string;
|
||||
|
||||
public errorHandler (err: Error) {
|
||||
const errMessage = err.message.toLowerCase()
|
||||
if (errMessage.includes('x-hub-signature')) {
|
||||
logger.error({ err }, 'Go to https://github.com/settings/apps/YOUR_APP and verify that the Webhook secret matches the value of the WEBHOOK_SECRET environment variable.')
|
||||
} else if (errMessage.includes('pem') || errMessage.includes('json web token')) {
|
||||
logger.error({ err }, 'Your private key (usually a .pem file) is not correct. Go to https://github.com/settings/apps/YOUR_APP and generate a new PEM file. If you\'re deploying to Now, visit https://probot.github.io/docs/deployment/#now.')
|
||||
} else {
|
||||
logger.error(err)
|
||||
/**
|
||||
* The organization where you want to register the app in the app
|
||||
* creation manifest flow. If set, the app is registered for an
|
||||
* organization
|
||||
* (https://github.com/organizations/ORGANIZATION/settings/apps/new), if
|
||||
* not set, the GitHub app would be registered for the user account
|
||||
* (https://github.com/settings/apps/new).
|
||||
*/
|
||||
GH_ORG?: string;
|
||||
|
||||
/**
|
||||
* The hostname of your GitHub Enterprise instance.
|
||||
* @example github.mycompany.com
|
||||
*/
|
||||
GHE_HOST?: string;
|
||||
|
||||
/**
|
||||
* The protocol of your GitHub Enterprise instance. Defaults to HTTPS.
|
||||
* Do not change unless you are certain.
|
||||
* @default 'https'
|
||||
*/
|
||||
GHE_PROTOCOL?: string;
|
||||
|
||||
/**
|
||||
* The contents of the private key for your GitHub App. If you're unable
|
||||
* to use multiline environment variables, use base64 encoding to
|
||||
* convert the key to a single line string. See the Deployment docs for
|
||||
* provider specific usage.
|
||||
*/
|
||||
PRIVATE_KEY?: string;
|
||||
|
||||
/**
|
||||
* When using the `PRIVATE_KEY_PATH` environment variable, set it to the
|
||||
* path of the `.pem` file that you downloaded from your GitHub App registration.
|
||||
* @example 'path/to/key.pem'
|
||||
*/
|
||||
PRIVATE_KEY_PATH?: string;
|
||||
/**
|
||||
* The port to start the local server on.
|
||||
* @default '3000'
|
||||
*/
|
||||
PORT?: string;
|
||||
|
||||
/**
|
||||
* The host to start the local server on.
|
||||
*/
|
||||
HOST?: string;
|
||||
|
||||
/**
|
||||
* Set to a `redis://` url as connection option for
|
||||
* [ioredis](https://github.com/luin/ioredis#connect-to-redis) in order
|
||||
* to enable
|
||||
* [cluster support for request throttling](https://github.com/octokit/plugin-throttling.js#clustering).
|
||||
* @example 'redis://:secret@redis-123.redislabs.com:12345/0'
|
||||
*/
|
||||
REDIS_URL?: string;
|
||||
|
||||
/**
|
||||
* Set to a [Sentry](https://sentry.io/) DSN to report all errors thrown
|
||||
* by your app.
|
||||
* @example 'https://1234abcd@sentry.io/12345'
|
||||
*/
|
||||
SENTRY_DSN?: string;
|
||||
|
||||
/**
|
||||
* The URL path which will receive webhooks.
|
||||
* @default '/api/github/webhooks'
|
||||
*/
|
||||
WEBHOOK_PATH?: string;
|
||||
|
||||
/**
|
||||
* Allows your local development environment to receive GitHub webhook
|
||||
* events. Go to https://smee.io/new to get started.
|
||||
* @example 'https://smee.io/your-custom-url'
|
||||
*/
|
||||
WEBHOOK_PROXY_URL?: string;
|
||||
|
||||
/**
|
||||
* **Required**
|
||||
* The webhook secret used when creating a GitHub App. 'development' is
|
||||
* used as a default, but the value in `.env` needs to match the value
|
||||
* configured in your App settings on GitHub. Note: GitHub marks this
|
||||
* value as optional, but for optimal security it's required for Probot
|
||||
* apps.
|
||||
*
|
||||
* @example 'development'
|
||||
* @default 'development'
|
||||
*/
|
||||
WEBHOOK_SECRET?: string;
|
||||
|
||||
NODE_ENV?: string;
|
||||
}
|
||||
}
|
||||
|
||||
public receive (event: WebhookEvent) {
|
||||
this.logger.debug({ event }, 'Webhook received')
|
||||
return Promise.all(this.apps.map(app => app.receive(event)))
|
||||
}
|
||||
|
||||
public load (appFn: string | ApplicationFunction) {
|
||||
if (typeof appFn === 'string') {
|
||||
appFn = resolve(appFn) as ApplicationFunction
|
||||
}
|
||||
|
||||
const app = new Application({ app: this.app, cache, catchErrors: true })
|
||||
|
||||
// Connect the router from the app to the server
|
||||
this.server.use(app.router)
|
||||
|
||||
// Initialize the ApplicationFunction
|
||||
app.load(appFn)
|
||||
this.apps.push(app)
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
public setup (appFns: Array<string | ApplicationFunction>) {
|
||||
// Log all unhandled rejections
|
||||
process.on('unhandledRejection', this.errorHandler)
|
||||
|
||||
// Load the given appFns along with the default ones
|
||||
appFns.concat(defaultAppFns).forEach(appFn => this.load(appFn))
|
||||
|
||||
// Register error handler as the last middleware
|
||||
this.server.use(logRequestErrors)
|
||||
}
|
||||
|
||||
public start () {
|
||||
if (this.options.webhookProxy) {
|
||||
createWebhookProxy({
|
||||
logger,
|
||||
path: this.options.webhookPath,
|
||||
port: this.options.port,
|
||||
url: this.options.webhookProxy
|
||||
})
|
||||
}
|
||||
|
||||
this.server.listen(this.options.port)
|
||||
logger.info('Listening on http://localhost:' + this.options.port)
|
||||
}
|
||||
}
|
||||
|
||||
export const createProbot = (options: Options) => new Probot(options)
|
||||
|
||||
export type ApplicationFunction = (app: Application) => void
|
||||
|
||||
export interface Options {
|
||||
webhookPath?: string
|
||||
secret?: string,
|
||||
id: number,
|
||||
cert: string,
|
||||
webhookProxy?: string,
|
||||
port?: number
|
||||
}
|
||||
|
||||
export { Logger, Context, Application }
|
||||
|
|
|
|||
|
|
@ -1,65 +0,0 @@
|
|||
/**
|
||||
* A logger backed by [bunyan](https://github.com/trentm/node-bunyan)
|
||||
*
|
||||
* The default log level is `info`, but you can change it by setting the
|
||||
* `LOG_LEVEL` environment variable to `trace`, `debug`, `info`, `warn`,
|
||||
* `error`, or `fatal`.
|
||||
*
|
||||
* By default, logs are formatted for readability in development. If you intend
|
||||
* to drain logs to a logging service, set `LOG_FORMAT=json`.
|
||||
*
|
||||
* **Note**: All execptions reported with `logger.error` will be forwarded to
|
||||
* [sentry](https://github.com/getsentry/sentry) if the `SENTRY_DSN` environment
|
||||
* variable is set.
|
||||
*
|
||||
* ```js
|
||||
* app.log("This is an info message");
|
||||
* app.log.debug("…so is this");
|
||||
* app.log.trace("Now we're talking");
|
||||
* app.log.info("I thought you should know…");
|
||||
* app.log.warn("Woah there");
|
||||
* app.log.error("ETOOMANYLOGS");
|
||||
* app.log.fatal("Goodbye, cruel world!");
|
||||
* ```
|
||||
*/
|
||||
|
||||
import Logger from 'bunyan'
|
||||
import bunyanFormat from 'bunyan-format'
|
||||
import supportsColor from 'supports-color'
|
||||
import { serializers } from './serializers'
|
||||
|
||||
function toBunyanLogLevel (level: string) {
|
||||
switch (level) {
|
||||
case 'info':
|
||||
case 'trace':
|
||||
case 'debug':
|
||||
case 'warn':
|
||||
case 'error':
|
||||
case 'fatal':
|
||||
case undefined:
|
||||
return level
|
||||
default:
|
||||
throw new Error('Invalid log level')
|
||||
}
|
||||
}
|
||||
|
||||
function toBunyanFormat (format: string) {
|
||||
switch (format) {
|
||||
case 'short':
|
||||
case 'long':
|
||||
case 'simple':
|
||||
case 'json':
|
||||
case 'bunyan':
|
||||
case undefined:
|
||||
return format
|
||||
default:
|
||||
throw new Error('Invalid log format')
|
||||
}
|
||||
}
|
||||
|
||||
export const logger = new Logger({
|
||||
level: toBunyanLogLevel(process.env.LOG_LEVEL || 'info'),
|
||||
name: 'probot',
|
||||
serializers,
|
||||
stream: new bunyanFormat({ outputMode: toBunyanFormat(process.env.LOG_FORMAT || 'short'), color: supportsColor.stdout })
|
||||
})
|
||||
|
|
@ -1,84 +1,105 @@
|
|||
import fs from 'fs'
|
||||
import yaml from 'js-yaml'
|
||||
import path from 'path'
|
||||
import updateDotenv from 'update-dotenv'
|
||||
import { GitHubAPI } from './github'
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import yaml from "js-yaml";
|
||||
import updateDotenv from "update-dotenv";
|
||||
import type { RequestParameters } from "@octokit/types";
|
||||
|
||||
import { ProbotOctokit } from "./octokit/probot-octokit.js";
|
||||
import { loadPackageJson } from "./helpers/load-package-json.js";
|
||||
import type { Env, Manifest, OctokitOptions, PackageJson } from "./types.js";
|
||||
|
||||
export class ManifestCreation {
|
||||
get pkg () {
|
||||
let pkg: any
|
||||
try {
|
||||
pkg = require(path.join(process.cwd(), 'package.json'))
|
||||
} catch (e) {
|
||||
pkg = {}
|
||||
}
|
||||
return pkg
|
||||
get pkg() {
|
||||
return loadPackageJson();
|
||||
}
|
||||
|
||||
public async createWebhookChannel () {
|
||||
public async createWebhookChannel(): Promise<string | undefined> {
|
||||
try {
|
||||
// tslint:disable:no-var-requires
|
||||
const SmeeClient = require('smee-client')
|
||||
await this.updateEnv({ WEBHOOK_PROXY_URL: await SmeeClient.createChannel() })
|
||||
} catch (err) {
|
||||
const SmeeClient = (await import("smee-client")).default;
|
||||
|
||||
const WEBHOOK_PROXY_URL = await SmeeClient.createChannel();
|
||||
await this.updateEnv({
|
||||
WEBHOOK_PROXY_URL,
|
||||
});
|
||||
return WEBHOOK_PROXY_URL;
|
||||
} catch (error) {
|
||||
// Smee is not available, so we'll just move on
|
||||
// tslint:disable:no-console
|
||||
console.warn('Unable to connect to smee.io, try restarting your server.')
|
||||
console.warn("Unable to connect to smee.io, try restarting your server.");
|
||||
return void 0;
|
||||
}
|
||||
}
|
||||
|
||||
public getManifest (pkg: any, baseUrl: any) {
|
||||
let manifest: any = {}
|
||||
public getManifest(pkg: PackageJson, baseUrl: string) {
|
||||
let manifest: Partial<Manifest> = {};
|
||||
try {
|
||||
const file = fs.readFileSync(path.join(process.cwd(), 'app.yml'), 'utf8')
|
||||
manifest = yaml.safeLoad(file)
|
||||
} catch (err) {
|
||||
const file = fs.readFileSync(path.join(process.cwd(), "app.yml"), "utf8");
|
||||
manifest = yaml.load(file) as Manifest;
|
||||
} catch (error) {
|
||||
// App config does not exist, which is ok.
|
||||
if (err.code !== 'ENOENT') {
|
||||
throw err
|
||||
if ((error as Error & { code?: string }).code !== "ENOENT") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const generatedManifest = JSON.stringify(Object.assign({
|
||||
const generatedManifest = JSON.stringify({
|
||||
description: manifest.description || pkg.description,
|
||||
hook_attributes: {
|
||||
url: process.env.WEBHOOK_PROXY_URL || `${baseUrl}/`
|
||||
url: process.env.WEBHOOK_PROXY_URL || `${baseUrl}/`,
|
||||
},
|
||||
name: manifest.name || pkg.name,
|
||||
name: process.env.PROJECT_DOMAIN || manifest.name || pkg.name,
|
||||
public: manifest.public || true,
|
||||
redirect_url: `${baseUrl}/probot/setup`,
|
||||
// TODO: add setup url
|
||||
// setup_url:`${baseUrl}/probot/success`,
|
||||
url: manifest.url || pkg.homepage || pkg.repository
|
||||
}, manifest))
|
||||
url: manifest.url || pkg.homepage || pkg.repository,
|
||||
version: "v1",
|
||||
...manifest,
|
||||
});
|
||||
|
||||
return generatedManifest
|
||||
return generatedManifest;
|
||||
}
|
||||
|
||||
public async createAppFromCode (code: any) {
|
||||
const github = GitHubAPI()
|
||||
const response = await github.request({
|
||||
headers: { accept: 'application/vnd.github.fury-preview+json' },
|
||||
method: 'POST',
|
||||
url: `/app-manifests/${code}/conversions`
|
||||
})
|
||||
public async createAppFromCode(code: string, probotOptions?: OctokitOptions) {
|
||||
const octokit = new ProbotOctokit(probotOptions);
|
||||
const options: RequestParameters = {
|
||||
...probotOptions,
|
||||
code,
|
||||
mediaType: {
|
||||
previews: ["fury"], // needed for GHES 2.20 and older
|
||||
},
|
||||
...(process.env.GHE_HOST && {
|
||||
baseUrl: `${process.env.GHE_PROTOCOL || "https"}://${
|
||||
process.env.GHE_HOST
|
||||
}/api/v3`,
|
||||
}),
|
||||
};
|
||||
const response = await octokit.request(
|
||||
"POST /app-manifests/:code/conversions",
|
||||
options,
|
||||
);
|
||||
|
||||
const { id, webhook_secret, pem } = response.data
|
||||
const { id, client_id, client_secret, webhook_secret, pem } = response.data;
|
||||
await this.updateEnv({
|
||||
APP_ID: id.toString(),
|
||||
PRIVATE_KEY: pem,
|
||||
WEBHOOK_SECRET: webhook_secret
|
||||
})
|
||||
PRIVATE_KEY: `"${pem}"`,
|
||||
WEBHOOK_SECRET: webhook_secret,
|
||||
GITHUB_CLIENT_ID: client_id,
|
||||
GITHUB_CLIENT_SECRET: client_secret,
|
||||
});
|
||||
|
||||
return response.data.html_url
|
||||
return response.data.html_url;
|
||||
}
|
||||
|
||||
private async updateEnv (env: any) {
|
||||
return updateDotenv(env)
|
||||
public async updateEnv(env: Env) {
|
||||
// Needs to be public due to tests
|
||||
return updateDotenv(env);
|
||||
}
|
||||
|
||||
get createAppUrl () {
|
||||
const githubHost = process.env.GHE_HOST || `github.com`
|
||||
return `https://${githubHost}/settings/apps/new`
|
||||
get createAppUrl() {
|
||||
const githubHost = process.env.GHE_HOST || `github.com`;
|
||||
return `${process.env.GHE_PROTOCOL || "https"}://${githubHost}${
|
||||
process.env.GH_ORG ? `/organizations/${process.env.GH_ORG}` : ""
|
||||
}/settings/apps/new`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
import { NextFunction, Request, Response } from './logging'
|
||||
|
||||
module.exports = (err: Error, req: Request, res: Response, next: NextFunction) => {
|
||||
if (req.log) {
|
||||
req.log.error(err)
|
||||
}
|
||||
next()
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
// Borrowed from https://github.com/vvo/bunyan-request
|
||||
// Copyright (c) Christian Tellnes <christian@tellnes.no>
|
||||
// tslint:disable
|
||||
import {wrapLogger} from '../wrap-logger'
|
||||
import uuid from 'uuid'
|
||||
import express from 'express'
|
||||
import Logger from 'bunyan'
|
||||
|
||||
export const logRequest = function ({logger}: any): express.RequestHandler {
|
||||
return function (req: Request, res: Response, next: NextFunction) {
|
||||
// Use X-Request-ID from request if it is set, otherwise generate a uuid
|
||||
req.id = req.headers['x-request-id'] ||
|
||||
req.headers['x-github-delivery'] ||
|
||||
uuid.v4()
|
||||
res.setHeader('x-request-id', req.id)
|
||||
|
||||
// Make a logger available on the request
|
||||
req.log = wrapLogger(logger, logger.target).child({name: 'http', id: req.id})
|
||||
|
||||
// Request started
|
||||
req.log.trace({req}, `${req.method} ${req.url}`)
|
||||
|
||||
// Start the request timer
|
||||
const time = process.hrtime()
|
||||
|
||||
res.on('finish', () => {
|
||||
// Calculate how long the request took
|
||||
const [seconds, nanoseconds] = process.hrtime(time)
|
||||
res.duration = (seconds * 1e3 + nanoseconds * 1e-6).toFixed(2)
|
||||
|
||||
const message = `${req.method} ${req.url} ${res.statusCode} - ${res.duration} ms`
|
||||
|
||||
if (req.log) {
|
||||
req.log.info(message)
|
||||
req.log.trace({res})
|
||||
}
|
||||
})
|
||||
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
export interface Request extends express.Request {
|
||||
id?: string | number | string[]
|
||||
log?: Logger
|
||||
}
|
||||
|
||||
export interface Response extends express.Response {
|
||||
duration?: string
|
||||
log?: Logger
|
||||
}
|
||||
|
||||
export interface NextFunction extends express.NextFunction { }
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import type { State } from "../types.js";
|
||||
import type { ProbotOctokit } from "./probot-octokit.js";
|
||||
import type { OctokitOptions } from "../types.js";
|
||||
import type { LogFn, Level } from "pino";
|
||||
|
||||
type FactoryOptions = {
|
||||
octokit: ProbotOctokit;
|
||||
octokitOptions: OctokitOptions;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export async function getAuthenticatedOctokit(
|
||||
state: State,
|
||||
installationId?: number,
|
||||
) {
|
||||
const { log, octokit } = state;
|
||||
|
||||
if (!installationId) return octokit;
|
||||
|
||||
return octokit.auth({
|
||||
type: "installation",
|
||||
installationId,
|
||||
factory: ({ octokit, octokitOptions, ...otherOptions }: FactoryOptions) => {
|
||||
const pinoLog = log.child({ name: "github" });
|
||||
|
||||
const options: ConstructorParameters<typeof ProbotOctokit>[0] & {
|
||||
log: Record<Level, LogFn>;
|
||||
} = {
|
||||
...octokitOptions,
|
||||
log: {
|
||||
fatal: pinoLog.fatal.bind(pinoLog),
|
||||
error: pinoLog.error.bind(pinoLog),
|
||||
warn: pinoLog.warn.bind(pinoLog),
|
||||
info: pinoLog.info.bind(pinoLog),
|
||||
debug: pinoLog.debug.bind(pinoLog),
|
||||
trace: pinoLog.trace.bind(pinoLog),
|
||||
},
|
||||
throttle: octokitOptions.throttle?.enabled
|
||||
? {
|
||||
...octokitOptions.throttle,
|
||||
id: String(installationId),
|
||||
}
|
||||
: { enabled: false },
|
||||
auth: {
|
||||
...octokitOptions.auth,
|
||||
otherOptions,
|
||||
installationId,
|
||||
},
|
||||
};
|
||||
|
||||
const Octokit = octokit.constructor as typeof ProbotOctokit;
|
||||
|
||||
return new Octokit(options);
|
||||
},
|
||||
}) as Promise<ProbotOctokit>;
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import Bottleneck from "bottleneck";
|
||||
import { Redis, type RedisOptions } from "ioredis";
|
||||
import type { Logger } from "pino";
|
||||
import type { ThrottlingOptions } from "@octokit/plugin-throttling";
|
||||
|
||||
type Options = {
|
||||
log: Logger;
|
||||
redisConfig?: RedisOptions | string;
|
||||
};
|
||||
|
||||
export function getOctokitThrottleOptions(options: Options) {
|
||||
let { log, redisConfig } = options;
|
||||
|
||||
const throttlingOptions: ThrottlingOptions = {
|
||||
onRateLimit: (retryAfter, options: { [key: string]: any }) => {
|
||||
log.warn(
|
||||
`Request quota exhausted for request ${options.method} ${options.url}`,
|
||||
);
|
||||
|
||||
// Retry twice after hitting a rate limit error, then give up
|
||||
if (options.request.retryCount <= 2) {
|
||||
log.info(`Retrying after ${retryAfter} seconds!`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
onSecondaryRateLimit: (_retryAfter, options: { [key: string]: any }) => {
|
||||
// does not retry, only logs a warning
|
||||
log.warn(
|
||||
`Secondary quota detected for request ${options.method} ${options.url}`,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
if (!redisConfig) return throttlingOptions;
|
||||
|
||||
const connection = new Bottleneck.IORedisConnection({
|
||||
client: getRedisClient(options),
|
||||
});
|
||||
connection.on("error", (error) => {
|
||||
log.error(Object.assign(error, { source: "bottleneck" }));
|
||||
});
|
||||
|
||||
throttlingOptions.Bottleneck = Bottleneck;
|
||||
throttlingOptions.connection = connection;
|
||||
|
||||
return throttlingOptions;
|
||||
}
|
||||
|
||||
function getRedisClient({ redisConfig }: Options): Redis | void {
|
||||
if (redisConfig) return new Redis(redisConfig as RedisOptions);
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
import type { LRUCache } from "lru-cache";
|
||||
import { ProbotOctokit } from "./probot-octokit.js";
|
||||
import type { RedisOptions } from "ioredis";
|
||||
import { request } from "@octokit/request";
|
||||
|
||||
import { getOctokitThrottleOptions } from "./get-octokit-throttle-options.js";
|
||||
|
||||
import type { Logger } from "pino";
|
||||
import type { RequestRequestOptions } from "@octokit/types";
|
||||
import type { OctokitOptions } from "../types.js";
|
||||
|
||||
type Options = {
|
||||
cache: LRUCache<number, string>;
|
||||
Octokit: typeof ProbotOctokit;
|
||||
log: Logger;
|
||||
githubToken?: string;
|
||||
appId?: number;
|
||||
privateKey?: string;
|
||||
redisConfig?: RedisOptions | string;
|
||||
webhookPath?: string;
|
||||
baseUrl?: string;
|
||||
request?: RequestRequestOptions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an Octokit instance with default settings for authentication. If
|
||||
* a `githubToken` is passed explicitly, the Octokit instance will be
|
||||
* pre-authenticated with that token when instantiated. Otherwise Octokit's
|
||||
* app authentication strategy is used, and `options.auth` options are merged
|
||||
* deeply when instantiated.
|
||||
*
|
||||
* Besides the authentication, the Octokit's baseUrl is set as well when run
|
||||
* against a GitHub Enterprise Server with a custom domain.
|
||||
*/
|
||||
export function getProbotOctokitWithDefaults(options: Options) {
|
||||
const authOptions = options.githubToken
|
||||
? {
|
||||
token: options.githubToken,
|
||||
request: request.defaults({
|
||||
request: {
|
||||
fetch: options.request?.fetch,
|
||||
},
|
||||
}),
|
||||
}
|
||||
: {
|
||||
cache: options.cache,
|
||||
appId: options.appId,
|
||||
privateKey: options.privateKey,
|
||||
request: request.defaults({
|
||||
request: {
|
||||
fetch: options.request?.fetch,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const octokitThrottleOptions = getOctokitThrottleOptions({
|
||||
log: options.log,
|
||||
redisConfig: options.redisConfig,
|
||||
});
|
||||
|
||||
let defaultOptions: Partial<OctokitOptions> = {
|
||||
auth: authOptions,
|
||||
log: options.log.child
|
||||
? options.log.child({ name: "octokit" })
|
||||
: options.log,
|
||||
};
|
||||
|
||||
if (options.baseUrl) {
|
||||
defaultOptions.baseUrl = options.baseUrl;
|
||||
}
|
||||
|
||||
if (octokitThrottleOptions) {
|
||||
defaultOptions.throttle = octokitThrottleOptions;
|
||||
}
|
||||
|
||||
return options.Octokit.defaults((instanceOptions: OctokitOptions) => {
|
||||
const options = Object.assign({}, defaultOptions, instanceOptions, {
|
||||
auth: instanceOptions.auth
|
||||
? Object.assign({}, defaultOptions.auth, instanceOptions.auth)
|
||||
: defaultOptions.auth,
|
||||
});
|
||||
|
||||
if (instanceOptions.throttle) {
|
||||
options.throttle = Object.assign(
|
||||
{},
|
||||
defaultOptions.throttle,
|
||||
instanceOptions.throttle,
|
||||
);
|
||||
}
|
||||
|
||||
return options;
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { Webhooks } from "@octokit/webhooks";
|
||||
|
||||
import type { State } from "../types.js";
|
||||
import { getErrorHandler } from "../helpers/get-error-handler.js";
|
||||
import { webhookTransform } from "./octokit-webhooks-transform.js";
|
||||
|
||||
export function getWebhooks(state: State) {
|
||||
const webhooks = new Webhooks({
|
||||
log: state.log,
|
||||
secret: state.webhooks.secret!,
|
||||
transform: (hook) => webhookTransform(state, hook),
|
||||
});
|
||||
webhooks.onError(getErrorHandler(state.log));
|
||||
return webhooks;
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import type { Octokit } from "@octokit/core";
|
||||
|
||||
export function probotRequestLogging(octokit: Octokit) {
|
||||
octokit.hook.error("request", (error, options) => {
|
||||
if ("status" in error) {
|
||||
const { method, url, request, ...params } =
|
||||
octokit.request.endpoint.parse(options);
|
||||
const msg = `GitHub request: ${method} ${url} - ${error.status}`;
|
||||
|
||||
// @ts-expect-error log.debug is a pino log method and accepts a fields object
|
||||
octokit.log.debug(params.body || {}, msg);
|
||||
}
|
||||
|
||||
throw error;
|
||||
});
|
||||
|
||||
octokit.hook.after("request", (result, options) => {
|
||||
const { method, url, request, ...params } =
|
||||
octokit.request.endpoint.parse(options);
|
||||
const msg = `GitHub request: ${method} ${url} - ${result.status}`;
|
||||
|
||||
// @ts-expect-error log.debug is a pino log method and accepts a fields object
|
||||
octokit.log.debug(params.body || {}, msg);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import type { EmitterWebhookEvent as WebhookEvent } from "@octokit/webhooks";
|
||||
|
||||
import { Context } from "../context.js";
|
||||
import type { State } from "../types.js";
|
||||
|
||||
/**
|
||||
* Probot's transform option, which extends the `event` object that is passed
|
||||
* to webhook event handlers by `@octokit/webhooks`
|
||||
* @see https://github.com/octokit/webhooks.js/#constructor
|
||||
*/
|
||||
export async function webhookTransform(state: State, event: WebhookEvent) {
|
||||
const log = state.log.child({ name: "event", id: event.id });
|
||||
const octokit = (await state.octokit.auth({
|
||||
type: "event-octokit",
|
||||
event,
|
||||
})) as typeof state.octokit;
|
||||
return new Context(event, octokit, log);
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import { Octokit } from "@octokit/core";
|
||||
import { enterpriseCompatibility } from "@octokit/plugin-enterprise-compatibility";
|
||||
import type { RequestOptions } from "@octokit/types";
|
||||
import { paginateRest } from "@octokit/plugin-paginate-rest";
|
||||
import { legacyRestEndpointMethods } from "@octokit/plugin-rest-endpoint-methods";
|
||||
import { retry } from "@octokit/plugin-retry";
|
||||
import { throttling } from "@octokit/plugin-throttling";
|
||||
import { config } from "@probot/octokit-plugin-config";
|
||||
import { createProbotAuth } from "octokit-auth-probot";
|
||||
|
||||
import { probotRequestLogging } from "./octokit-plugin-probot-request-logging.js";
|
||||
import { VERSION } from "../version.js";
|
||||
|
||||
const defaultOptions = {
|
||||
authStrategy: createProbotAuth,
|
||||
throttle: {
|
||||
enabled: true,
|
||||
onSecondaryRateLimit: (
|
||||
retryAfter: number,
|
||||
options: RequestOptions,
|
||||
octokit: Octokit,
|
||||
) => {
|
||||
octokit.log.warn(
|
||||
`Secondary Rate limit hit with "${options.method} ${options.url}", retrying in ${retryAfter} seconds.`,
|
||||
);
|
||||
return true;
|
||||
},
|
||||
onRateLimit: (
|
||||
retryAfter: number,
|
||||
options: RequestOptions,
|
||||
octokit: Octokit,
|
||||
) => {
|
||||
octokit.log.warn(
|
||||
`Rate limit hit with "${options.method} ${options.url}", retrying in ${retryAfter} seconds.`,
|
||||
);
|
||||
return true;
|
||||
},
|
||||
},
|
||||
userAgent: `probot/${VERSION}`,
|
||||
};
|
||||
|
||||
export const ProbotOctokit = Octokit.plugin(
|
||||
throttling,
|
||||
retry,
|
||||
paginateRest,
|
||||
legacyRestEndpointMethods,
|
||||
enterpriseCompatibility,
|
||||
probotRequestLogging,
|
||||
config,
|
||||
).defaults((instanceOptions: any) => {
|
||||
// merge throttle options deeply
|
||||
const options = {
|
||||
...defaultOptions,
|
||||
...instanceOptions,
|
||||
...{
|
||||
throttle: { ...defaultOptions.throttle, ...instanceOptions?.throttle },
|
||||
},
|
||||
};
|
||||
|
||||
return options;
|
||||
});
|
||||
|
||||
export type ProbotOctokit = InstanceType<typeof ProbotOctokit>;
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
import fs from 'fs'
|
||||
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
const isBase64 = require('is-base64')
|
||||
|
||||
const hint = `please use:
|
||||
* \`--private-key=/path/to/private-key\` flag, or
|
||||
* \`PRIVATE_KEY\` environment variable, or
|
||||
* \`PRIVATE_KEY_PATH\` environment variable
|
||||
`
|
||||
|
||||
/**
|
||||
* Finds a private key through various user-(un)specified methods.
|
||||
* Order of precedence:
|
||||
* 1. Explicit path (CLI option)
|
||||
* 2. `PRIVATE_KEY` env var
|
||||
* 3. `PRIVATE_KEY_PATH` env var
|
||||
* 4. Any file w/ `.pem` extension in current working dir
|
||||
* @param filepath - Explicit, user-defined path to keyfile
|
||||
* @returns Private key
|
||||
* @private
|
||||
*/
|
||||
export function findPrivateKey (filepath?: string): Buffer | string | null {
|
||||
if (filepath) {
|
||||
return fs.readFileSync(filepath)
|
||||
}
|
||||
if (process.env.PRIVATE_KEY) {
|
||||
let cert = process.env.PRIVATE_KEY
|
||||
|
||||
if (isBase64(cert)) {
|
||||
// Decode base64-encoded certificate
|
||||
cert = Buffer.from(cert, 'base64').toString()
|
||||
}
|
||||
|
||||
const begin = '-----BEGIN RSA PRIVATE KEY-----'
|
||||
const end = '-----END RSA PRIVATE KEY-----'
|
||||
if (cert.includes(begin) && cert.includes(end)) {
|
||||
// Full key with new lines
|
||||
return cert.replace(/\\n/g, '\n')
|
||||
}
|
||||
|
||||
throw new Error('The contents of \`PRIVATE_KEY\` could not be validated. Please check to ensure you have copied the contents of the .pem file correctly.')
|
||||
}
|
||||
if (process.env.PRIVATE_KEY_PATH) {
|
||||
return fs.readFileSync(process.env.PRIVATE_KEY_PATH)
|
||||
}
|
||||
const pemFiles = fs.readdirSync(process.cwd())
|
||||
.filter(path => path.endsWith('.pem'))
|
||||
if (pemFiles.length > 1) {
|
||||
throw new Error(
|
||||
`Found several private keys: ${pemFiles.join(', ')}. ` +
|
||||
`To avoid ambiguity ${hint}`
|
||||
)
|
||||
} else if (pemFiles[0]) {
|
||||
return findPrivateKey(pemFiles[0])
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
import { LRUCache } from "lru-cache";
|
||||
import type { Logger } from "pino";
|
||||
import type { EmitterWebhookEvent as WebhookEvent } from "@octokit/webhooks";
|
||||
|
||||
import { auth } from "./auth.js";
|
||||
import { getLog } from "./helpers/get-log.js";
|
||||
import { getProbotOctokitWithDefaults } from "./octokit/get-probot-octokit-with-defaults.js";
|
||||
import { getWebhooks } from "./octokit/get-webhooks.js";
|
||||
import { ProbotOctokit } from "./octokit/probot-octokit.js";
|
||||
import { VERSION } from "./version.js";
|
||||
import type {
|
||||
ApplicationFunction,
|
||||
ApplicationFunctionOptions,
|
||||
Options,
|
||||
ProbotWebhooks,
|
||||
State,
|
||||
} from "./types.js";
|
||||
import { defaultWebhooksPath } from "./server/server.js";
|
||||
import { rebindLog } from "./helpers/rebind-log.js";
|
||||
|
||||
export type Constructor<T = any> = new (...args: any[]) => T;
|
||||
|
||||
export class Probot {
|
||||
static version = VERSION;
|
||||
static defaults<S extends Constructor>(this: S, defaults: Options) {
|
||||
const ProbotWithDefaults = class extends this {
|
||||
constructor(...args: any[]) {
|
||||
const options = args[0] || {};
|
||||
super(Object.assign({}, defaults, options));
|
||||
}
|
||||
};
|
||||
|
||||
return ProbotWithDefaults;
|
||||
}
|
||||
|
||||
public webhooks: ProbotWebhooks;
|
||||
public webhookPath: string;
|
||||
public log: Logger;
|
||||
public version: String;
|
||||
public on: ProbotWebhooks["on"];
|
||||
public onAny: ProbotWebhooks["onAny"];
|
||||
public onError: ProbotWebhooks["onError"];
|
||||
public auth: (
|
||||
installationId?: number,
|
||||
log?: Logger,
|
||||
) => Promise<ProbotOctokit>;
|
||||
|
||||
private state: State;
|
||||
|
||||
constructor(options: Options = {}) {
|
||||
options.secret = options.secret || "development";
|
||||
|
||||
let level = options.logLevel;
|
||||
const logMessageKey = options.logMessageKey;
|
||||
|
||||
this.log = options.log
|
||||
? rebindLog(options.log)
|
||||
: getLog({ level, logMessageKey });
|
||||
|
||||
// TODO: support redis backend for access token cache if `options.redisConfig`
|
||||
const cache = new LRUCache<number, string>({
|
||||
// cache max. 15000 tokens, that will use less than 10mb memory
|
||||
max: 15000,
|
||||
// Cache for 1 minute less than GitHub expiry
|
||||
ttl: 1000 * 60 * 59,
|
||||
});
|
||||
|
||||
const Octokit = getProbotOctokitWithDefaults({
|
||||
githubToken: options.githubToken,
|
||||
Octokit: options.Octokit || ProbotOctokit,
|
||||
appId: Number(options.appId),
|
||||
privateKey: options.privateKey,
|
||||
cache,
|
||||
log: rebindLog(this.log),
|
||||
redisConfig: options.redisConfig,
|
||||
baseUrl: options.baseUrl,
|
||||
});
|
||||
const octokitLogger = rebindLog(this.log.child({ name: "octokit" }));
|
||||
const octokit = new Octokit({
|
||||
request: options.request,
|
||||
log: {
|
||||
debug: octokitLogger.debug.bind(octokitLogger),
|
||||
info: octokitLogger.info.bind(octokitLogger),
|
||||
warn: octokitLogger.warn.bind(octokitLogger),
|
||||
error: octokitLogger.error.bind(octokitLogger),
|
||||
},
|
||||
});
|
||||
|
||||
this.state = {
|
||||
cache,
|
||||
githubToken: options.githubToken,
|
||||
log: rebindLog(this.log),
|
||||
Octokit,
|
||||
octokit,
|
||||
webhooks: {
|
||||
secret: options.secret,
|
||||
},
|
||||
appId: Number(options.appId),
|
||||
privateKey: options.privateKey,
|
||||
host: options.host,
|
||||
port: options.port,
|
||||
webhookPath: options.webhookPath || defaultWebhooksPath,
|
||||
request: options.request,
|
||||
};
|
||||
|
||||
this.auth = auth.bind(null, this.state);
|
||||
|
||||
this.webhooks = getWebhooks(this.state);
|
||||
this.webhookPath = this.state.webhookPath;
|
||||
|
||||
this.on = this.webhooks.on;
|
||||
this.onAny = this.webhooks.onAny;
|
||||
this.onError = this.webhooks.onError;
|
||||
|
||||
this.version = VERSION;
|
||||
}
|
||||
|
||||
public receive(event: WebhookEvent) {
|
||||
this.log.debug({ event }, "Webhook received");
|
||||
return this.webhooks.receive(event);
|
||||
}
|
||||
|
||||
public async load(
|
||||
appFn: ApplicationFunction | ApplicationFunction[],
|
||||
options: ApplicationFunctionOptions = {},
|
||||
) {
|
||||
if (Array.isArray(appFn)) {
|
||||
for (const fn of appFn) {
|
||||
await this.load(fn);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
return appFn(this, options);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import { sync } from 'resolve'
|
||||
|
||||
const defaultOptions: ResolveOptions = {}
|
||||
|
||||
export const resolve = (appFnId: string, opts?: ResolveOptions) => {
|
||||
opts = opts || defaultOptions
|
||||
// These are mostly to ease testing
|
||||
const basedir = opts.basedir || process.cwd()
|
||||
const resolver: Resolver = opts.resolver || sync
|
||||
return require(resolver(appFnId, { basedir }))
|
||||
}
|
||||
|
||||
export type Resolver = (appFnId: string, opts: {basedir: string}) => string
|
||||
|
||||
export interface ResolveOptions {
|
||||
basedir?: string
|
||||
resolver?: Resolver
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
import pkgConf from "pkg-conf";
|
||||
|
||||
import type { ApplicationFunction, Options, ServerOptions } from "./types.js";
|
||||
import { Logger, Probot, ProbotOctokit } from "./index.js";
|
||||
import { setupAppFactory } from "./apps/setup.js";
|
||||
import { getLog } from "./helpers/get-log.js";
|
||||
import { readCliOptions } from "./bin/read-cli-options.js";
|
||||
import { readEnvOptions } from "./bin/read-env-options.js";
|
||||
import { Server } from "./server/server.js";
|
||||
import { defaultApp } from "./apps/default.js";
|
||||
import { resolveAppFunction } from "./helpers/resolve-app-function.js";
|
||||
import { isProduction } from "./helpers/is-production.js";
|
||||
import { config as dotenvConfig } from "dotenv";
|
||||
|
||||
type AdditionalOptions = {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
Octokit?: typeof ProbotOctokit;
|
||||
log?: Logger;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param appFnOrArgv set to either a probot application function: `(app) => { ... }` or to process.argv
|
||||
*/
|
||||
export async function run(
|
||||
appFnOrArgv: ApplicationFunction | string[],
|
||||
additionalOptions?: AdditionalOptions,
|
||||
) {
|
||||
dotenvConfig();
|
||||
|
||||
const envOptions = readEnvOptions(additionalOptions?.env);
|
||||
const cliOptions = Array.isArray(appFnOrArgv)
|
||||
? readCliOptions(appFnOrArgv)
|
||||
: {};
|
||||
|
||||
const {
|
||||
// log options
|
||||
logLevel: level,
|
||||
logFormat,
|
||||
logLevelInString,
|
||||
logMessageKey,
|
||||
sentryDsn,
|
||||
|
||||
// server options
|
||||
host,
|
||||
port,
|
||||
webhookPath,
|
||||
webhookProxy,
|
||||
|
||||
// probot options
|
||||
appId,
|
||||
privateKey,
|
||||
redisConfig,
|
||||
secret,
|
||||
baseUrl,
|
||||
|
||||
// others
|
||||
args,
|
||||
} = { ...envOptions, ...cliOptions };
|
||||
|
||||
const log = getLog({
|
||||
level,
|
||||
logFormat,
|
||||
logLevelInString,
|
||||
logMessageKey,
|
||||
sentryDsn,
|
||||
});
|
||||
|
||||
const probotOptions: Options = {
|
||||
appId,
|
||||
privateKey,
|
||||
redisConfig,
|
||||
secret,
|
||||
baseUrl,
|
||||
log: additionalOptions?.log || log.child({ name: "probot" }),
|
||||
Octokit: additionalOptions?.Octokit || undefined,
|
||||
};
|
||||
|
||||
const serverOptions: ServerOptions = {
|
||||
host,
|
||||
port,
|
||||
webhookPath,
|
||||
webhookProxy,
|
||||
log: log.child({ name: "server" }),
|
||||
Probot: Probot.defaults(probotOptions),
|
||||
};
|
||||
|
||||
let server: Server;
|
||||
|
||||
if (!appId || !privateKey) {
|
||||
if (isProduction()) {
|
||||
if (!appId) {
|
||||
throw new Error(
|
||||
"App ID is missing, and is required to run in production mode. " +
|
||||
"To resolve, ensure the APP_ID environment variable is set.",
|
||||
);
|
||||
} else if (!privateKey) {
|
||||
throw new Error(
|
||||
"Certificate is missing, and is required to run in production mode. " +
|
||||
"To resolve, ensure either the PRIVATE_KEY or PRIVATE_KEY_PATH environment variable is set and contains a valid certificate",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround for setup (#1512)
|
||||
// When probot is started for the first time, it gets into a setup mode
|
||||
// where `appId` and `privateKey` are not present. The setup mode gets
|
||||
// these credentials. In order to not throw an error, we set the values
|
||||
// to anything, as the Probot instance is not used in setup it makes no
|
||||
// difference anyway.
|
||||
server = new Server({
|
||||
...serverOptions,
|
||||
Probot: Probot.defaults({
|
||||
...probotOptions,
|
||||
appId: 1,
|
||||
privateKey: "dummy value for setup, see #1512",
|
||||
}),
|
||||
});
|
||||
await server.load(setupAppFactory(host, port));
|
||||
await server.start();
|
||||
return server;
|
||||
}
|
||||
|
||||
if (Array.isArray(appFnOrArgv)) {
|
||||
const pkg = await pkgConf("probot");
|
||||
|
||||
const combinedApps: ApplicationFunction = async (_app) => {
|
||||
await server.load(defaultApp);
|
||||
|
||||
if (Array.isArray(pkg.apps)) {
|
||||
for (const appPath of pkg.apps) {
|
||||
const appFn = await resolveAppFunction(appPath);
|
||||
await server.load(appFn);
|
||||
}
|
||||
}
|
||||
|
||||
const [appPath] = args;
|
||||
const appFn = await resolveAppFunction(appPath);
|
||||
await server.load(appFn);
|
||||
};
|
||||
|
||||
server = new Server(serverOptions);
|
||||
await server.load(combinedApps);
|
||||
await server.start();
|
||||
return server;
|
||||
}
|
||||
|
||||
server = new Server(serverOptions);
|
||||
await server.load(appFnOrArgv);
|
||||
await server.start();
|
||||
|
||||
return server;
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
import { PayloadRepository, WebhookEvent } from '@octokit/webhooks'
|
||||
import bunyan from 'bunyan'
|
||||
import express from 'express'
|
||||
|
||||
export const serializers: bunyan.StdSerializers = {
|
||||
|
||||
event: (event: WebhookEvent | any) => {
|
||||
if (typeof event !== 'object' || !event.payload) {
|
||||
return event
|
||||
} else {
|
||||
let name = event.name
|
||||
if (event.payload && event.payload.action) {
|
||||
name = `${name}.${event.payload.action}`
|
||||
}
|
||||
|
||||
return {
|
||||
event: name,
|
||||
id: event.id,
|
||||
installation: event.payload.installation && event.payload.installation.id,
|
||||
repository: event.payload.repository && event.payload.repository.full_name
|
||||
}
|
||||
}
|
||||
},
|
||||
installation: (installation: Installation) => {
|
||||
if (installation.account) {
|
||||
return installation.account.login
|
||||
} else {
|
||||
return installation
|
||||
}
|
||||
},
|
||||
|
||||
err: bunyan.stdSerializers.err,
|
||||
|
||||
repository: (repository: PayloadRepository) => repository.full_name,
|
||||
|
||||
req: bunyan.stdSerializers.req,
|
||||
|
||||
// Same as buyan's standard serializers, but gets headers as an object
|
||||
// instead of a string.
|
||||
// https://github.com/trentm/node-bunyan/blob/fe31b83e42d9c7f784e83fdcc528a7c76e0dacae/lib/bunyan.js#L1105-L1113
|
||||
res: (res: ExpressResponseWithDuration) => {
|
||||
if (!res || !res.statusCode) {
|
||||
return res
|
||||
} else {
|
||||
return {
|
||||
duration: res.duration,
|
||||
headers: res.getHeaders(),
|
||||
statusCode: res.statusCode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface Installation {
|
||||
account: {
|
||||
login: string
|
||||
}
|
||||
}
|
||||
|
||||
interface ExpressResponseWithDuration extends express.Response {
|
||||
duration: any
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import Logger from 'bunyan'
|
||||
import express from 'express'
|
||||
import path from 'path'
|
||||
|
||||
// Teach express to properly handle async errors
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
require('express-async-errors')
|
||||
|
||||
import { logRequest } from './middleware/logging'
|
||||
|
||||
export const createServer = (args: ServerArgs) => {
|
||||
const app: express.Application = express()
|
||||
|
||||
app.use(logRequest({ logger: args.logger }))
|
||||
app.use('/probot/static/', express.static(path.join(__dirname, '..', 'static')))
|
||||
app.use(args.webhook)
|
||||
app.set('view engine', 'hbs')
|
||||
app.set('views', path.join(__dirname, '..', 'views'))
|
||||
app.get('/ping', (req, res) => res.end('PONG'))
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
export interface ServerArgs {
|
||||
webhook: express.Application
|
||||
logger: Logger
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { randomUUID as uuidv4 } from "node:crypto";
|
||||
|
||||
import { pinoHttp, startTime, type Options, type HttpLogger } from "pino-http";
|
||||
import type { Logger } from "pino";
|
||||
|
||||
export function getLoggingMiddleware(
|
||||
logger: Logger,
|
||||
options?: Options,
|
||||
): HttpLogger {
|
||||
return pinoHttp({
|
||||
...options,
|
||||
logger: logger.child({ name: "http" }),
|
||||
customSuccessMessage(_req, res) {
|
||||
const responseTime = Date.now() - res[startTime];
|
||||
return `${res.req.method} ${res.req.url} ${res.statusCode} - ${responseTime}ms`;
|
||||
},
|
||||
customErrorMessage(_err, res) {
|
||||
const responseTime = Date.now() - res[startTime];
|
||||
return `${res.req.method} ${res.req.url} ${res.statusCode} - ${responseTime}ms`;
|
||||
},
|
||||
genReqId: (req) =>
|
||||
req.headers["x-request-id"] ||
|
||||
req.headers["x-github-delivery"] ||
|
||||
uuidv4(),
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
import type { Server as HttpServer } from "node:http";
|
||||
import { join } from "node:path";
|
||||
|
||||
import express, { Router, type Application } from "express";
|
||||
import type { Logger } from "pino";
|
||||
import { createNodeMiddleware as createWebhooksMiddleware } from "@octokit/webhooks";
|
||||
|
||||
import { getLoggingMiddleware } from "./logging-middleware.js";
|
||||
import { createWebhookProxy } from "../helpers/webhook-proxy.js";
|
||||
import { VERSION } from "../version.js";
|
||||
import type { ApplicationFunction, ServerOptions } from "../types.js";
|
||||
import type { Probot } from "../index.js";
|
||||
import type EventSource from "eventsource";
|
||||
import { rebindLog } from "../helpers/rebind-log.js";
|
||||
|
||||
// the default path as defined in @octokit/webhooks
|
||||
export const defaultWebhooksPath = "/api/github/webhooks";
|
||||
|
||||
type State = {
|
||||
cwd?: string;
|
||||
httpServer?: HttpServer;
|
||||
port?: number;
|
||||
host?: string;
|
||||
webhookPath: string;
|
||||
webhookProxy?: string;
|
||||
eventSource?: EventSource;
|
||||
};
|
||||
|
||||
export class Server {
|
||||
static version = VERSION;
|
||||
|
||||
public expressApp: Application;
|
||||
public log: Logger;
|
||||
public version = VERSION;
|
||||
public probotApp: Probot;
|
||||
|
||||
private state: State;
|
||||
|
||||
constructor(options: ServerOptions = {} as ServerOptions) {
|
||||
this.expressApp = express();
|
||||
this.probotApp = new options.Probot({
|
||||
request: options.request,
|
||||
});
|
||||
this.log = options.log
|
||||
? rebindLog(options.log)
|
||||
: rebindLog(this.probotApp.log.child({ name: "server" }));
|
||||
|
||||
this.state = {
|
||||
cwd: options.cwd || process.cwd(),
|
||||
port: options.port,
|
||||
host: options.host,
|
||||
webhookPath: options.webhookPath || defaultWebhooksPath,
|
||||
webhookProxy: options.webhookProxy,
|
||||
};
|
||||
|
||||
this.expressApp.use(getLoggingMiddleware(this.log, options.loggingOptions));
|
||||
this.expressApp.use(
|
||||
"/probot/static/",
|
||||
express.static(join(__dirname, "..", "..", "static")),
|
||||
);
|
||||
this.expressApp.use(
|
||||
createWebhooksMiddleware(this.probotApp.webhooks, {
|
||||
path: this.state.webhookPath,
|
||||
}),
|
||||
);
|
||||
|
||||
this.expressApp.get("/ping", (_req, res) => res.end("PONG"));
|
||||
}
|
||||
|
||||
public async load(appFn: ApplicationFunction) {
|
||||
await appFn(this.probotApp, {
|
||||
cwd: this.state.cwd,
|
||||
getRouter: (path) => this.router(path),
|
||||
});
|
||||
}
|
||||
|
||||
public async start() {
|
||||
this.log.info(
|
||||
`Running Probot v${this.version} (Node.js: ${process.version})`,
|
||||
);
|
||||
const port = this.state.port || 3000;
|
||||
const { host, webhookPath, webhookProxy } = this.state;
|
||||
const printableHost = host ?? "localhost";
|
||||
|
||||
this.state.httpServer = await new Promise((resolve, reject) => {
|
||||
const server = this.expressApp.listen(
|
||||
port,
|
||||
...((host ? [host] : []) as any),
|
||||
async () => {
|
||||
if (webhookProxy) {
|
||||
this.state.eventSource = await createWebhookProxy({
|
||||
logger: this.log,
|
||||
path: webhookPath,
|
||||
port: port,
|
||||
url: webhookProxy,
|
||||
});
|
||||
}
|
||||
this.log.info(`Listening on http://${printableHost}:${port}`);
|
||||
resolve(server);
|
||||
},
|
||||
);
|
||||
|
||||
server.on("error", (error: NodeJS.ErrnoException) => {
|
||||
if (error.code === "EADDRINUSE") {
|
||||
error = Object.assign(error, {
|
||||
message: `Port ${port} is already in use. You can define the PORT environment variable to use a different port.`,
|
||||
});
|
||||
}
|
||||
|
||||
this.log.error(error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
return this.state.httpServer;
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
if (this.state.eventSource) this.state.eventSource.close();
|
||||
if (!this.state.httpServer) return;
|
||||
const server = this.state.httpServer;
|
||||
return new Promise((resolve) => server.close(resolve));
|
||||
}
|
||||
|
||||
public router(path: string = "/") {
|
||||
const newRouter = Router();
|
||||
this.expressApp.use(path, newRouter);
|
||||
return newRouter;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
import express from "express";
|
||||
import type {
|
||||
EmitterWebhookEvent as WebhookEvent,
|
||||
Webhooks,
|
||||
} from "@octokit/webhooks";
|
||||
import type { LRUCache } from "lru-cache";
|
||||
import type { RedisOptions } from "ioredis";
|
||||
import type { Options as LoggingOptions } from "pino-http";
|
||||
|
||||
import { Probot } from "./index.js";
|
||||
import { Context } from "./context.js";
|
||||
import { ProbotOctokit } from "./octokit/probot-octokit.js";
|
||||
|
||||
import type { Logger } from "pino";
|
||||
import type { RequestRequestOptions } from "@octokit/types";
|
||||
|
||||
export interface Options {
|
||||
privateKey?: string;
|
||||
githubToken?: string;
|
||||
appId?: number | string;
|
||||
|
||||
Octokit?: typeof ProbotOctokit;
|
||||
log?: Logger;
|
||||
redisConfig?: RedisOptions | string;
|
||||
secret?: string;
|
||||
logLevel?: "trace" | "debug" | "info" | "warn" | "error" | "fatal";
|
||||
logMessageKey?: string;
|
||||
port?: number;
|
||||
host?: string;
|
||||
baseUrl?: string;
|
||||
request?: RequestRequestOptions;
|
||||
webhookPath?: string;
|
||||
}
|
||||
|
||||
export type State = {
|
||||
appId?: number;
|
||||
privateKey?: string;
|
||||
githubToken?: string;
|
||||
log: Logger;
|
||||
Octokit: typeof ProbotOctokit;
|
||||
octokit: ProbotOctokit;
|
||||
cache?: LRUCache<number, string>;
|
||||
webhooks: {
|
||||
secret?: string;
|
||||
};
|
||||
port?: number;
|
||||
host?: string;
|
||||
baseUrl?: string;
|
||||
webhookPath: string;
|
||||
request?: RequestRequestOptions;
|
||||
};
|
||||
|
||||
// Omit the `payload`, `id`,`name` properties from the `Context` class as they are already present in the types of `WebhookEvent`
|
||||
// The `Webhooks` class accepts a type parameter (`TTransformed`) that is used to transform the event payload in the form of
|
||||
// WebhookEvent["payload"] & T
|
||||
// Simply passing `Context` as `TTransformed` would result in the payload types being too complex for TypeScript to infer
|
||||
// See https://github.com/probot/probot/issues/1388
|
||||
// See https://github.com/probot/probot/issues/1815 as for why this is in a seperate type, and not directly passed to `Webhooks`
|
||||
type SimplifiedObject = Omit<Context, keyof WebhookEvent>;
|
||||
export type ProbotWebhooks = Webhooks<SimplifiedObject>;
|
||||
|
||||
export type ApplicationFunctionOptions = {
|
||||
getRouter?: (path?: string) => express.Router;
|
||||
cwd?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
export type ApplicationFunction = (
|
||||
app: Probot,
|
||||
options: ApplicationFunctionOptions,
|
||||
) => void | Promise<void>;
|
||||
|
||||
export type ServerOptions = {
|
||||
cwd?: string;
|
||||
log?: Logger;
|
||||
port?: number;
|
||||
host?: string;
|
||||
webhookPath?: string;
|
||||
webhookProxy?: string;
|
||||
Probot: typeof Probot;
|
||||
loggingOptions?: LoggingOptions;
|
||||
request?: RequestRequestOptions;
|
||||
};
|
||||
|
||||
export type MiddlewareOptions = {
|
||||
probot: Probot;
|
||||
webhooksPath?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type OctokitOptions = NonNullable<
|
||||
ConstructorParameters<typeof ProbotOctokit>[0]
|
||||
>;
|
||||
|
||||
export type PackageJson = {
|
||||
name?: string;
|
||||
version?: string;
|
||||
description?: string;
|
||||
homepage?: string;
|
||||
repository?: string;
|
||||
engines?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type Env = Record<Uppercase<string>, string>;
|
||||
|
||||
type ManifestPermissionValue = "read" | "write" | "none";
|
||||
type ManifestPermissionScope =
|
||||
| "actions"
|
||||
| "checks"
|
||||
| "contents"
|
||||
| "deployments"
|
||||
| "id-token"
|
||||
| "issues"
|
||||
| "discussions"
|
||||
| "packages"
|
||||
| "pages"
|
||||
| "pull-requests"
|
||||
| "repository-projects"
|
||||
| "security-events"
|
||||
| "statuses";
|
||||
|
||||
export type Manifest = {
|
||||
/**
|
||||
* The name of the GitHub App.
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* __Required.__ The homepage of your GitHub App.
|
||||
*/
|
||||
url: string;
|
||||
/**
|
||||
* The configuration of the GitHub App's webhook.
|
||||
*/
|
||||
hook_attributes?: {
|
||||
/*
|
||||
* __Required.__ The URL of the server that will receive the webhook POST requests.
|
||||
*/
|
||||
url: string;
|
||||
/*
|
||||
* Deliver event details when this hook is triggered, defaults to true.
|
||||
*/
|
||||
active?: boolean;
|
||||
};
|
||||
/**
|
||||
* The full URL to redirect to after a user initiates the registration of a GitHub App from a manifest.
|
||||
*/
|
||||
redirect_url?: string;
|
||||
/**
|
||||
* A full URL to redirect to after someone authorizes an installation. You can provide up to 10 callback URLs.
|
||||
*/
|
||||
callback_urls?: string[];
|
||||
/**
|
||||
* A full URL to redirect users to after they install your GitHub App if additional setup is required.
|
||||
*/
|
||||
setup_url?: string;
|
||||
/**
|
||||
* A description of the GitHub App.
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* Set to `true` when your GitHub App is available to the public or `false` when it is only accessible to the owner of the app.
|
||||
*/
|
||||
public?: boolean;
|
||||
/**
|
||||
* The list of events the GitHub App subscribes to.
|
||||
*/
|
||||
default_events?: WebhookEvent[];
|
||||
/**
|
||||
* The set of permissions needed by the GitHub App. The format of the object uses the permission name for the key (for example, `issues`) and the access type for the value (for example, `write`).
|
||||
*/
|
||||
default_permissions?:
|
||||
| "read-all"
|
||||
| "write-all"
|
||||
| Record<ManifestPermissionScope, ManifestPermissionValue>;
|
||||
/**
|
||||
* Set to `true` to request the user to authorize the GitHub App, after the GitHub App is installed.
|
||||
*/
|
||||
request_oauth_on_install?: boolean;
|
||||
/**
|
||||
* Set to `true` to redirect users to the setup_url after they update your GitHub App installation.
|
||||
*/
|
||||
setup_on_update?: boolean;
|
||||
};
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
// The version is set automatically before publish to npm
|
||||
export const VERSION = "0.0.0-development";
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
export function importView({
|
||||
name,
|
||||
GH_HOST,
|
||||
WEBHOOK_PROXY_URL = "",
|
||||
}: {
|
||||
name?: string;
|
||||
GH_HOST: string;
|
||||
WEBHOOK_PROXY_URL?: string;
|
||||
}): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en" class="height-full" data-color-mode="auto" data-light-theme="light" data-dark-theme="dark">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Import ${name || "Your App"} | built with Probot</title>
|
||||
<link rel="icon" href="/probot/static/probot-head.png">
|
||||
<link rel="stylesheet" href="/probot/static/primer.css">
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-light">
|
||||
<div class="d-flex flex-column flex-justify-center flex-items-center text-center height-full py-6">
|
||||
<a href="/probot"><img src="/probot/static/robot.svg" alt="Probot Logo" width="100" class="mb-6"></a>
|
||||
<div class="box-shadow rounded-2 border p-6 bg-white">
|
||||
<h2>Use existing Github App</h2>
|
||||
<br>
|
||||
|
||||
<h3>Step 1:</h3>
|
||||
<p class="d-block mt-2">
|
||||
Replace your app's Webhook URL with <br>
|
||||
<b>${WEBHOOK_PROXY_URL}</b>
|
||||
</p>
|
||||
<a class="d-block mt-2" href="${GH_HOST}/settings/apps" target="__blank" rel="noreferrer">
|
||||
You can do it here
|
||||
</a>
|
||||
|
||||
<br>
|
||||
<h3>Step 2:</h3>
|
||||
<p class="mt-2">Fill out this form</p>
|
||||
<form onsubmit="return onSubmit(event) || false">
|
||||
<label class="d-block mt-2" for="appId">App Id</label>
|
||||
<input class="form-control width-full" type="text" required="true" id="appId" name="appId"><br>
|
||||
|
||||
<label class="d-block mt-3" for="whs">Webhook secret (required!)</label>
|
||||
<input class="form-control width-full" type="password" required="true" id="whs" name="whs"><br>
|
||||
|
||||
<label class="d-block mt-3" for="pem">Private Key</label>
|
||||
<input class="form-control width-full m-2" type="file" accept=".pem" required="true" id="pem"
|
||||
name="pem">
|
||||
<br>
|
||||
|
||||
<button class="btn btn-outline m-2" type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<h4 class="alt-h4 text-gray-light">Need help?</h4>
|
||||
<div class="d-flex flex-justify-center mt-2">
|
||||
<a href="https://probot.github.io/docs/" class="btn btn-outline mr-2">Documentation</a>
|
||||
<a href="https://github.com/probot/probot/discussions" class="btn btn-outline">Discuss on GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function onSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const idEl = document.getElementById('appId');
|
||||
const appId = idEl.value;
|
||||
|
||||
|
||||
const secretEl = document.getElementById('whs');
|
||||
const webhook_secret = secretEl.value;
|
||||
|
||||
const fileEl = document.getElementById('pem');
|
||||
const file = fileEl.files[0];
|
||||
|
||||
file.text().then((text) => fetch('', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ appId, pem: text, webhook_secret })
|
||||
})).then((r) => {
|
||||
if (r.ok) {
|
||||
location.replace('/probot/success');
|
||||
}
|
||||
}).catch((e) => alert(e));
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>`;
|
||||
}
|
||||
|
|
@ -1,10 +1,19 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" class="height-full">
|
||||
export function probotView({
|
||||
name,
|
||||
description,
|
||||
version,
|
||||
}: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
version?: string;
|
||||
}): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en" class="height-full" data-color-mode="auto" data-light-theme="light" data-dark-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>{{#if name }}{{ name }}{{else}}Your App{{/if}} | built with Probot</title>
|
||||
<title>${name || "Your App"} | built with Probot</title>
|
||||
<link rel="icon" href="/probot/static/probot-head.png">
|
||||
<link rel="stylesheet" href="/probot/static/primer.css">
|
||||
</head>
|
||||
|
|
@ -13,26 +22,28 @@
|
|||
<img src="/probot/static/robot.svg" alt="Probot Logo" width="100" class="mb-6">
|
||||
<div class="box-shadow rounded-2 border p-6 bg-white">
|
||||
<h1>
|
||||
Welcome to {{#if name }}{{ name }}{{else}}your Probot App{{/if}}
|
||||
{{#if version}}
|
||||
<span class="Label Label--outline v-align-middle ml-2 text-gray-light">v{{ version }}</span>
|
||||
{{/if}}
|
||||
</h1>
|
||||
Welcome to ${name || "your Probot App"}
|
||||
${
|
||||
version
|
||||
? ` <span class="Label Label--outline v-align-middle ml-2 text-gray-light">v${version}</span>\n`
|
||||
: ""
|
||||
} </h1>
|
||||
|
||||
{{#if description }}
|
||||
<p>{{ description }}</p>
|
||||
{{else}}
|
||||
<p>This bot was built using <a href="https://github.com/probot/probot">Probot</a>, a framework for building GitHub Apps.</p>
|
||||
{{/if}}
|
||||
<p>${
|
||||
description
|
||||
? description
|
||||
: 'This bot was built using <a href="https://github.com/probot/probot">Probot</a>, a framework for building GitHub Apps.'
|
||||
}</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<h4 class="alt-h4 text-gray-light">Need help?</h4>
|
||||
<div class="d-flex flex-justify-center mt-2">
|
||||
<a href="https://probot.github.io/docs/" class="btn btn-outline mr-2">Documentation</a>
|
||||
<a href="https://probot-slackin.herokuapp.com/" class="btn btn-outline">Chat on Slack</a>
|
||||
<a href="https://github.com/probot/probot/discussions" class="btn btn-outline">Discuss on GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>`;
|
||||
}
|
||||
|
|
@ -1,10 +1,24 @@
|
|||
export function setupView({
|
||||
name,
|
||||
description,
|
||||
version,
|
||||
createAppUrl,
|
||||
manifest,
|
||||
}: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
version?: string;
|
||||
createAppUrl: string;
|
||||
manifest: string;
|
||||
}): string {
|
||||
return `<!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="height-full">
|
||||
<html lang="en" class="height-full" data-color-mode="auto" data-light-theme="light" data-dark-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Setup {{#if pkg.name }}{{ pkg.name }}{{else}}Your App{{/if}} | built with Probot</title>
|
||||
<title>Setup ${name || "Your App"} | built with Probot</title>
|
||||
<link rel="icon" href="/probot/static/probot-head.png">
|
||||
<link rel="stylesheet" href="/probot/static/primer.css">
|
||||
</head>
|
||||
|
|
@ -13,17 +27,19 @@
|
|||
<img src="/probot/static/robot.svg" alt="Probot Logo" width="100" class="mb-6">
|
||||
<div class="box-shadow rounded-2 border p-6 bg-white">
|
||||
<h1>
|
||||
Welcome to {{#if pkg.name }}{{ pkg.name }}{{else}}your Probot App{{/if}}
|
||||
{{#if version}}
|
||||
<span class="Label Label--outline v-align-middle ml-2 text-gray-light">v{{ pkg.version }}</span>
|
||||
{{/if}}
|
||||
Welcome to ${name || "your Probot App"}
|
||||
${
|
||||
version
|
||||
? `<span class="Label Label--outline v-align-middle ml-2 text-gray-light">v${version}</span>`
|
||||
: ""
|
||||
}
|
||||
</h1>
|
||||
|
||||
{{#if pkg.description }}
|
||||
<p>{{ pkg.description }}</p>
|
||||
{{else}}
|
||||
<p>This app was built using <a href="https://github.com/probot/probot">Probot</a>, a framework for building GitHub Apps.</p>
|
||||
{{/if}}
|
||||
<p>${
|
||||
description
|
||||
? description
|
||||
: 'This app was built using <a href="https://github.com/probot/probot">Probot</a>, a framework for building GitHub Apps.'
|
||||
}</p>
|
||||
|
||||
<div class="text-left mt-6">
|
||||
<h2 class="alt-h3 mb-2">Getting Started</h2>
|
||||
|
|
@ -31,8 +47,9 @@
|
|||
<p>To start building a GitHub App, you'll need to register a new app on GitHub.</p>
|
||||
<br>
|
||||
|
||||
<form action="{{ createAppUrl }}" method="post" target="_blank">
|
||||
<button class="btn btn-outline" name="manifest" id="manifest" value='{{{ manifest }}}' >Register GitHub App</button>
|
||||
<form action="${createAppUrl}" method="post" target="_blank" class="d-flex flex-items-center">
|
||||
<button class="btn btn-outline" name="manifest" id="manifest" value='${manifest}' >Register GitHub App</button>
|
||||
<a href="/probot/import" class="ml-2">or use an existing Github App</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -41,9 +58,10 @@
|
|||
<h4 class="alt-h4 text-gray-light">Need help?</h4>
|
||||
<div class="d-flex flex-justify-center mt-2">
|
||||
<a href="https://probot.github.io/docs/" class="btn btn-outline mr-2">Documentation</a>
|
||||
<a href="https://probot-slackin.herokuapp.com/" class="btn btn-outline">Chat on Slack</a>
|
||||
<a href="https://github.com/probot/probot/discussions" class="btn btn-outline">Discuss on GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>`;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue