Compare commits
947 Commits
Author | SHA1 | Date | |
---|---|---|---|
926367150a | |||
14ff340e93 | |||
c8e750745a | |||
6b8f2da93e | |||
352219eafb | |||
e8a8770eee | |||
f536261639 | |||
2d06693560 | |||
315d95c747 | |||
5c5b62cc86 | |||
479f773bb3 | |||
629c83588d | |||
d96ba0ffd9 | |||
30700035b3 | |||
8c6d6b85db | |||
0c10b57749 | |||
fe1c0d7a0c | |||
dc053859ee | |||
4d532df52b | |||
5eb540cf34 | |||
7697650edc | |||
4517b8e618 | |||
3abc4c9c54 | |||
1d6333891c | |||
4d4e85b708 | |||
28104828d5 | |||
77f531b6f7 | |||
3041240cbc | |||
257ac83095 | |||
bc93d47129 | |||
3f61071638 | |||
ea571aa9d1 | |||
40cd0371d2 | |||
8356577670 | |||
45d3c8093b | |||
e7eec2e44b | |||
c44b93253c | |||
cd444bc2ba | |||
e9ae26a272 | |||
f9856974e3 | |||
b3c6409763 | |||
4c3e9a32d2 | |||
2700952040 | |||
6d92c99c49 | |||
7cefb3efd4 | |||
1c0ab3468d | |||
b5ede01c09 | |||
98b1a9dcfe | |||
c277c198d2 | |||
b7d7609872 | |||
415e3e06ed | |||
0f7a850699 | |||
1e2525b8a7 | |||
bc9f534bf4 | |||
ea2a3ca705 | |||
802e996e64 | |||
aa7dd1267b | |||
91916e7113 | |||
0e92f1eee5 | |||
4ac3b2e2bd | |||
20ce134961 | |||
c01f3409f3 | |||
71580c8e37 | |||
e58f8b7be0 | |||
046c96a3f1 | |||
00e6569e0f | |||
5dbf9f721b | |||
c63bc02846 | |||
2f3308e4f0 | |||
cc5e4fb93f | |||
3ab9947c7f | |||
b1f92db22b | |||
bda1e52b50 | |||
1084ebc091 | |||
5a3af00398 | |||
7a088af3ff | |||
3e8d8ec2f5 | |||
d4ea7f1f7e | |||
23096501e2 | |||
023874426e | |||
aeffcdd417 | |||
6d7d7c77e3 | |||
8da6c369e5 | |||
d0f2bae62b | |||
062af7a725 | |||
9d1d70a563 | |||
5f908eedbf | |||
a3eac19831 | |||
cace4dea64 | |||
a012ffbd5b | |||
2065149d73 | |||
44e604b131 | |||
4503b28295 | |||
f8fcea333f | |||
a09782b8ac | |||
bfeceb3d3e | |||
1a5735d5c0 | |||
43d4446ec0 | |||
91edfb8be0 | |||
9d0a6554a3 | |||
8fff438767 | |||
b2ae08f272 | |||
c78542f124 | |||
b52c97cf70 | |||
929739c736 | |||
85bde4ff90 | |||
5b5b8254b1 | |||
ac8871fb7f | |||
6b2b1eae9d | |||
b3f096219f | |||
224d9dcd4a | |||
31a0c2a6c2 | |||
c251a5a4d1 | |||
81e571b908 | |||
80a9de0a57 | |||
d1e2ac86fd | |||
9a0d326f4f | |||
ce3024017a | |||
c8adef2dab | |||
778e1c2602 | |||
35bfa1437f | |||
4d83078cd2 | |||
64d005de05 | |||
0c822b75f9 | |||
e0a9cfb5e2 | |||
f8dff83d4b | |||
e411cd124a | |||
df4e85907c | |||
1498dc2dca | |||
abfde44afb | |||
eb9d431487 | |||
9060fb7031 | |||
7fbe547f78 | |||
cda6a215f2 | |||
a8430db45d | |||
5e2bf292e5 | |||
efded24527 | |||
30ac643d1d | |||
d9725016ad | |||
3f94759b14 | |||
9a46420aaf | |||
6a11d7e2a0 | |||
4fd9988251 | |||
a4fcd07792 | |||
078a928fab | |||
5d90aede4e | |||
658a18cdab | |||
6d240cf687 | |||
dca176e232 | |||
6c4845515b | |||
c2ce832f65 | |||
c726d27331 | |||
c4b7862e1b | |||
39472e102b | |||
1fafc5ca18 | |||
07a26647ac | |||
817821e553 | |||
398db3e9f0 | |||
d5fa4dc146 | |||
0ae3518fa9 | |||
e0f9a1b3f3 | |||
2604f429c7 | |||
ecada17ad4 | |||
fa3751ec9e | |||
f85a969219 | |||
d0a1e42e50 | |||
4f14a09895 | |||
8d28fe9df9 | |||
6715f056b0 | |||
32c018cbfe | |||
a403c4f7f5 | |||
bf656d64b8 | |||
3b183ce9b5 | |||
f635c3ecec | |||
dd6e8424aa | |||
1035c6a3ee | |||
b49b274a16 | |||
a4ebf3fb6d | |||
9997ab5ef9 | |||
c109aada2c | |||
57ce13aa1c | |||
2fd127d000 | |||
2f28e6a62d | |||
bf1a13e5e1 | |||
ab9e114fee | |||
448ab9c465 | |||
d769b441c0 | |||
50ccbe744d | |||
b29e709208 | |||
017a087f9b | |||
4b522572e6 | |||
3808434eec | |||
34aff0b3a1 | |||
7cb4183f58 | |||
960d32dd4f | |||
be0382b50c | |||
7ac4b76336 | |||
00b5c7b49b | |||
96f38562bd | |||
94b98aa819 | |||
1c9b06504b | |||
dd8a85158e | |||
164f79a7b0 | |||
327c614799 | |||
1aa8cfbf74 | |||
95d0626a1e | |||
5183bbffbe | |||
e76a570908 | |||
9afc9a7464 | |||
931e603f80 | |||
45732e5b91 | |||
b2db32b715 | |||
9e32dc7c95 | |||
a19b690338 | |||
bab5b68910 | |||
3daeadd235 | |||
ff15043e48 | |||
ea20ae63d0 | |||
7777a99fe5 | |||
9ebb4c02a2 | |||
7fbeb04b7c | |||
42ee50fc22 | |||
163b7c94a9 | |||
071934e92a | |||
735dfd3b1a | |||
70cd112872 | |||
989555352d | |||
99736750fc | |||
0a3f8173f0 | |||
6a64ac4151 | |||
062fe5c2cf | |||
4b494f23f5 | |||
bd186c7ef9 | |||
1657c997cd | |||
1e69d601fb | |||
34b6d5fff9 | |||
632f66a461 | |||
f7b17a4784 | |||
9562324ea4 | |||
880c0add56 | |||
64c96186da | |||
26209fca49 | |||
7f03528dbc | |||
d17602f31d | |||
7d0e17530b | |||
053bf27fb3 | |||
39f42bad1c | |||
3f8ac238f1 | |||
e0e2038718 | |||
67608a907e | |||
7acdad6921 | |||
7466a99dda | |||
912f3d186f | |||
e26cb21e4e | |||
be4edf15ee | |||
d5e9405d4f | |||
d1b7bb52e7 | |||
1312693f88 | |||
72ff9c880c | |||
3060b3e29b | |||
ee28b64d74 | |||
1246ba53c7 | |||
c29ff722a0 | |||
67ad9468d3 | |||
f922808b8d | |||
67435d456c | |||
fbfce79b93 | |||
2a14dfa4ba | |||
8e71ad6027 | |||
25289664ea | |||
e5644204dc | |||
69b9758ab8 | |||
7ea5161d4d | |||
b0879046b7 | |||
456f23f76a | |||
9623e7c639 | |||
83302d193e | |||
807070fe83 | |||
3ac8a63499 | |||
6a24db2bc6 | |||
50d1cba174 | |||
44c05c05af | |||
7d08722e80 | |||
13cdd13511 | |||
9320ec0f43 | |||
5dd225cb43 | |||
1b1c8ee545 | |||
10e414f617 | |||
b46fa92ae5 | |||
0bdea1f69c | |||
5a31bde649 | |||
decc0b840d | |||
55d54c7e97 | |||
ccceff5ecc | |||
6c6bc95ac0 | |||
c9cfcfa728 | |||
f543d71cc3 | |||
3a5cb1cb11 | |||
ab6f055479 | |||
3683c6a188 | |||
4006c9b6e6 | |||
245b85f72a | |||
bfeed842ee | |||
77942cc690 | |||
7aed64d3a1 | |||
9953fe7c00 | |||
3cce4afa0d | |||
8f25321787 | |||
51dfdd5dd1 | |||
fdaf573073 | |||
35bf95281f | |||
7a78889994 | |||
19c4e705ff | |||
36d6e6076e | |||
868047e87f | |||
5f1273ba2e | |||
355a7cae3c | |||
4c615f7de7 | |||
79466baef8 | |||
b0070dfb9a | |||
9ed4e3df60 | |||
a2da485d90 | |||
9cb17ecc39 | |||
35936864bc | |||
532e53678d | |||
f859d83298 | |||
ac68c75e26 | |||
fe45b9cebd | |||
aaaa34021c | |||
730679964f | |||
cb59d87489 | |||
d216a46412 | |||
a2878b0b1d | |||
5977b72e9c | |||
7373da9b11 | |||
783a682a7d | |||
d22418d417 | |||
4b1fd98093 | |||
935ef13096 | |||
f4b60588fb | |||
15dadb92ef | |||
ce06a75ebf | |||
9889276b15 | |||
d0f7eadc09 | |||
4b132c9848 | |||
46729c76a0 | |||
f22deb2e2d | |||
57de9fc41a | |||
31034f5146 | |||
c5899f4cd4 | |||
ab379ab72a | |||
32e479ffec | |||
391c708d7e | |||
c51331689f | |||
68fadd9b97 | |||
2ad1bb4eb9 | |||
794c3595d4 | |||
b807106f54 | |||
86e6a2099a | |||
9993c72335 | |||
f455518d80 | |||
7cf5807100 | |||
9523991a9b | |||
9acd04c192 | |||
c091d40fb0 | |||
b7baf632c0 | |||
4c0d4fc649 | |||
5b3c08b237 | |||
68f2e0c391 | |||
9c1c945489 | |||
ef5338663d | |||
380b3d7653 | |||
4decc8521d | |||
4d544bcb46 | |||
4c819f79b2 | |||
ac3252a73b | |||
a08af77b70 | |||
aac08e0438 | |||
63b795ae4a | |||
5f6900ecc0 | |||
325e8010e9 | |||
632b19d5c2 | |||
add1198b88 | |||
0ed2df2a36 | |||
bc1f2d6411 | |||
d7326d81ba | |||
c683f74225 | |||
b286abeabe | |||
eeebe28c0f | |||
ffc6e199bf | |||
a01acec7fe | |||
021f4344b1 | |||
f113b49909 | |||
d8d276c245 | |||
e42bd012f9 | |||
6d6b0ff1ad | |||
f378454c92 | |||
c8c8436e58 | |||
b31c8b6063 | |||
897261efdc | |||
35d70ff265 | |||
fc0a7959a4 | |||
182c08bee1 | |||
e2bc0ad6c2 | |||
73333ee3e5 | |||
c819859598 | |||
79aefa7659 | |||
e1990a5a80 | |||
4cff5b2964 | |||
459758231b | |||
f29b218060 | |||
39a67548ac | |||
bc88f318f6 | |||
a5b7008c8e | |||
0aafbac99b | |||
ac5aa8f46d | |||
1fb3c4ffee | |||
3c8aa0b301 | |||
15a2b8f622 | |||
d19108531c | |||
354d1944bb | |||
eaccd03ed7 | |||
343df337f4 | |||
9b14483824 | |||
bd42caf1c7 | |||
7db8111973 | |||
74fef157e6 | |||
9661bed3ba | |||
95168e4de0 | |||
13c3e241c8 | |||
8d098d389a | |||
79a2567aa3 | |||
5649acd03f | |||
ebd01e8e79 | |||
e08955b557 | |||
04dfca41f4 | |||
129f69c3bc | |||
c7e2930f25 | |||
6a62ed2245 | |||
482e12c940 | |||
0c344715e5 | |||
cf095d982d | |||
23ec88ef23 | |||
2bd767c4a6 | |||
1e02cd9961 | |||
bc02e19831 | |||
47eb2122c0 | |||
b8422b41bb | |||
8ac4dd6447 | |||
206ae7a233 | |||
79b6256789 | |||
72dce34f42 | |||
7d39bc68fb | |||
32ad2438ca | |||
fc4b993d98 | |||
ff028f0b39 | |||
fef9cebed0 | |||
c08549ae38 | |||
3808416479 | |||
cf8ad24dcf | |||
cee7448efc | |||
7f1cace2a2 | |||
56c86c7e79 | |||
82a14dc107 | |||
a880686081 | |||
026b60cd70 | |||
cea2e0477c | |||
96f9f03d25 | |||
9931bd7576 | |||
48094835bf | |||
e42c1b0da8 | |||
e7ade38731 | |||
d5f47d6b71 | |||
64aa6701f6 | |||
12ccf57340 | |||
c634176035 | |||
4bb10d224c | |||
5d689469f6 | |||
855ad8804e | |||
29d3f3f6dd | |||
33101359c6 | |||
44eef5c343 | |||
68b7847b4c | |||
2b2e841e5b | |||
549de1e21a | |||
48e73c1558 | |||
41ac58ab7d | |||
f37cf52b4c | |||
927323f24e | |||
b94436d86c | |||
bc5cb8153e | |||
34b848ad51 | |||
d7e5bbf2d0 | |||
a9a81f91cf | |||
07c10e2844 | |||
df5999a739 | |||
3fb0da2de5 | |||
8f0fcc3f71 | |||
ca1e56dc8b | |||
d0e710d472 | |||
bc7f962039 | |||
78d42a9503 | |||
dd5e35ee67 | |||
f91b0455c0 | |||
e8bab1349f | |||
e952c65759 | |||
5241ea086d | |||
cbbad1b791 | |||
96ee898cee | |||
2c40a86b61 | |||
a53a559f5a | |||
6de393b2b8 | |||
56283ed594 | |||
ddd3bf83c7 | |||
9b1bb370a3 | |||
976389836e | |||
f76a9ad156 | |||
6f1100a7e9 | |||
b99d7ed5bf | |||
f47f2628e1 | |||
5653874683 | |||
21e566d9bc | |||
bdbb2f9bfa | |||
2e32d4ee17 | |||
8f81dba367 | |||
aedebaf025 | |||
47f4412650 | |||
a09c3923db | |||
10a656fc38 | |||
8dc2b119fb | |||
f2ba55f2fb | |||
00d3666d95 | |||
21009b06a1 | |||
379e8c5e19 | |||
f3b552f51f | |||
c5b594e351 | |||
86a3be8610 | |||
d5bd86ae5d | |||
a9099e8f70 | |||
96d6b79ada | |||
13ccdfd89d | |||
a0c4b2d8f0 | |||
7ba0cb7c93 | |||
d83f9d432a | |||
e3633888ed | |||
ed266daf2c | |||
89af5291de | |||
91d79939be | |||
96eb79b1c7 | |||
83a1334876 | |||
2a21ca09d2 | |||
ddc13352e9 | |||
62be8c2e2f | |||
d2dfd48be0 | |||
d6cd041cbd | |||
694b8ae779 | |||
152de20774 | |||
34ec9244a6 | |||
268e9772d5 | |||
e2c67ba155 | |||
dcad0544d7 | |||
1bc6f64eb5 | |||
397b047eff | |||
d96e962da3 | |||
b0cb134815 | |||
2a672a97ab | |||
71007ef9b2 | |||
51c26b8afb | |||
1e7a873cf4 | |||
13b8399d0c | |||
010e35d995 | |||
234661b3d6 | |||
4a04ab8823 | |||
a417b2b419 | |||
2c66523222 | |||
25c1f331d6 | |||
59aab14394 | |||
c1ae3c16e8 | |||
51c0d9cae9 | |||
08dfbc5475 | |||
2f1bc1aa1a | |||
cc29b9cf93 | |||
bd0eb0d1d4 | |||
e84da1981d | |||
abd29f5049 | |||
6def18a95e | |||
34be51898d | |||
1e3460be0b | |||
31349fde90 | |||
910381ddbd | |||
20b9c61d4c | |||
e964319fe9 | |||
26cd9f5433 | |||
e73e864f87 | |||
73047483a1 | |||
6f168b7a0f | |||
a469c2c412 | |||
38f624d7e3 | |||
b424b3187e | |||
00f13110be | |||
ccb4a396f0 | |||
d539122466 | |||
b89a7dd4a2 | |||
9533cc9809 | |||
06d04002fd | |||
6143da66b2 | |||
a080ffc743 | |||
a8210d010b | |||
c9844a2f01 | |||
4815b92495 | |||
d76a7d6f7c | |||
6e6489a408 | |||
1d8e821276 | |||
6e828bba88 | |||
1f59f2f04d | |||
371df35624 | |||
3809e0fcae | |||
b06f1c0087 | |||
2379ad1a4b | |||
dd2a650c34 | |||
72b3b27348 | |||
f394ba0e27 | |||
268cf7989c | |||
9ee29ecdd4 | |||
42072c4d0d | |||
29761ea5f8 | |||
a22fb91e1a | |||
0386c44acc | |||
0fe708ff82 | |||
b069514818 | |||
0024d68add | |||
317d40d879 | |||
ab32ac6bb7 | |||
5653fada32 | |||
c230173716 | |||
366195e182 | |||
b1902db0cb | |||
a081d207f8 | |||
6ed79934c4 | |||
6a0f78fabf | |||
3634575d89 | |||
f596930c8c | |||
b3c56f021f | |||
864c22fb79 | |||
4d83a0a789 | |||
2569fd4d75 | |||
c73ebe79b3 | |||
110ab2230b | |||
4a61cb3fab | |||
4dc5afb5c6 | |||
8c2c6b5d1a | |||
f71cce7f9b | |||
53c1efb50a | |||
b6ccd9f7bd | |||
8b614d4e1b | |||
7f905da335 | |||
be24f9f0cb | |||
66ffa360df | |||
9bcd8c2425 | |||
8fa099158e | |||
b00038c847 | |||
18f129f536 | |||
efb453cb73 | |||
658f49f650 | |||
a37bcc3bfe | |||
a59d4da304 | |||
22e7f7e99f | |||
3d41739021 | |||
668bfcec9d | |||
eb1fe19088 | |||
22d58fc89b | |||
27e2039630 | |||
5c95b4b3a3 | |||
61218f5f0b | |||
7500f0eafb | |||
68acc5b355 | |||
7d3b70c2af | |||
7ce291c72a | |||
6ae1e63c89 | |||
c8baace554 | |||
d33e0091df | |||
b97d770e60 | |||
a3158bff27 | |||
d6e91ba545 | |||
cdb0215d0b | |||
396766104b | |||
e77f0fd6e6 | |||
cdd4c9be63 | |||
29705dd8f2 | |||
ea68ba048a | |||
9081efa961 | |||
9e179cb311 | |||
3211432d2a | |||
a7134dbc37 | |||
a528636f56 | |||
f87b499dde | |||
a45f2bfb8f | |||
94332affd3 | |||
52605aa2d8 | |||
4e36f0cd68 | |||
d5b70e0c66 | |||
f33dbf42fd | |||
6176974832 | |||
69b57b2dca | |||
831e71ea3c | |||
fc89479044 | |||
f54f3856cb | |||
c8b70ae8e4 | |||
9ed3bd6b4f | |||
11e2d9da1a | |||
b05d4a5007 | |||
469d5e0448 | |||
21a14407f6 | |||
d2be3d5775 | |||
f2aa9c6a7f | |||
4708cb91ef | |||
21d22ce4ad | |||
e9026a5201 | |||
f053a3f274 | |||
31f0f5b3c3 | |||
07d8d3994c | |||
116946fb11 | |||
503905c807 | |||
cc55d609ce | |||
de03abbd34 | |||
6482f6f0fe | |||
abcc430310 | |||
9605456b66 | |||
9ee6702fa9 | |||
92c8752d0a | |||
68bfe686d8 | |||
d604ef7cf0 | |||
36c4c8daa9 | |||
cc6f36a9d7 | |||
643766637e | |||
a800a5118a | |||
3b8b7f4087 | |||
9b820555a3 | |||
364459c576 | |||
8347bb0d2d | |||
4ce70b9edf | |||
1f1103913a | |||
01ec5fd6b0 | |||
be2cf4dfd6 | |||
eeb81b9370 | |||
b5f354f2fb | |||
a0a29fdd27 | |||
26066f282e | |||
b40c437379 | |||
82e2725154 | |||
c13901f4c8 | |||
6a2130117f | |||
4e45f2c481 | |||
78f477652e | |||
98f336c0fb | |||
9117fa199c | |||
0c4209f4b9 | |||
14ac7ad6b4 | |||
85106375ac | |||
ecb5dc03f9 | |||
bbb3f8fa60 | |||
09711507f9 | |||
3ac7070009 | |||
c869b143c6 | |||
e0314b5d90 | |||
1bb30147d3 | |||
fb2c5241fc | |||
97d8b5ed88 | |||
4a4d6fb0e6 | |||
2016afdbff | |||
c8c1aa7fc0 | |||
409860a4da | |||
2b128a47b9 | |||
209cc7e1b0 | |||
2d759927d4 | |||
7058072ff6 | |||
33fd7e0784 | |||
2befc65777 | |||
6f085f8610 | |||
5be186035f | |||
fba276d3d1 | |||
9c92a6fc7a | |||
6c4da9dcd3 | |||
b64fed1ba3 | |||
8bbce3feff | |||
6c359afce6 | |||
a3f1e2cb42 | |||
090824526b | |||
bfdbdc2ee6 | |||
638ff760a5 | |||
74518c4b2e | |||
ebf508fcd0 | |||
1039bea53b | |||
a2593cbfb1 | |||
70a3deb8a5 | |||
843479449d | |||
732026c3f5 | |||
ec6d6175d2 | |||
af9ced9026 | |||
c6e5b971d6 | |||
dbdbbdbe86 | |||
fefc860f35 | |||
02e201ab1a | |||
7bf5a43385 | |||
2fe05abbc4 | |||
2505c077d7 | |||
066fc6a0ca | |||
07ab98bbb0 | |||
16c03c0f38 | |||
3355502f2f | |||
02c15a2448 | |||
6861bc5b06 | |||
b8887ddf16 | |||
4933e103d3 | |||
7d006c5005 | |||
67ad59c245 | |||
397530ab24 | |||
a9881bb18e | |||
c1587029db | |||
2b906f652f | |||
c67f1bb38e | |||
637ae135c5 | |||
4eb8ac6de9 | |||
ba1e25f53f | |||
97b5cb2e3b | |||
795e1e8a38 | |||
aea8832243 | |||
1e7ca22078 | |||
26a15cc534 | |||
b0d86c1c2f | |||
dc0715142f | |||
4e264781ee | |||
79a9c71422 | |||
15cc85c54a | |||
725bae1921 | |||
eb999300d9 | |||
afa6b9e794 | |||
0822dc70f2 | |||
728d98d3a9 | |||
2f4abbf5a1 | |||
1000fb8406 | |||
b38931b484 | |||
1fb7111da1 | |||
c2c12e52fe | |||
28c7a4efbc | |||
4f741e74e1 | |||
6bacd32fbd | |||
183757daa2 | |||
adf510f986 | |||
74bce18190 | |||
3ba5220839 | |||
5982425436 | |||
140248ade0 | |||
e60737f63c | |||
7b89711402 | |||
f1223628a6 | |||
1b4269ad85 | |||
a224df43af | |||
d46a961509 | |||
20b453008f | |||
06a1974a48 | |||
3d7f555044 | |||
06af7943a4 | |||
fa70a2a650 | |||
bde4402675 | |||
7d6b258778 | |||
5342aeaafd | |||
1dd2eaa7d2 | |||
af07ffc2ad | |||
2b6e1f0f4b | |||
7a4fb44f8d | |||
88da8f3d52 | |||
01e6dab544 | |||
a9ecf4b929 | |||
166ddaadca | |||
1e5327872d | |||
367841d237 | |||
d5b73832bf | |||
7075c418f9 | |||
466e026f6f | |||
4976a58780 | |||
bafe1a0d2a | |||
c8a4fb1faf | |||
64516da6b0 | |||
6e2a1877ab | |||
aafd502bcb | |||
4cb1074850 | |||
76d8eb021c | |||
3f6fc00d73 | |||
5254d3447d | |||
4ee9db959a | |||
3f20a2fb5a | |||
e3834b7001 | |||
36648293a8 | |||
cd89eb8404 | |||
e99d860393 | |||
24789e9ad9 | |||
f94f9640d0 | |||
4d5167ec83 | |||
efc6684cd3 | |||
2ef777b0b2 | |||
fe14f180a6 | |||
87419097da | |||
9a6d26e05b | |||
6a797d5401 | |||
89e8b6fc0e | |||
f82b6b2ed7 | |||
a87d44c187 | |||
43d0e3dd72 | |||
5b32aa4486 | |||
844d510d3f | |||
2f70e90493 | |||
45cf5b5dad | |||
4ad2f11919 | |||
d7aa20d912 | |||
a673494412 | |||
07e6de5788 | |||
6f1685ab98 | |||
67588ec606 | |||
ee2c050521 | |||
185b932138 | |||
5e98421d33 | |||
8e65891985 | |||
7f59170f77 | |||
9ea112473b | |||
16f0ac38b8 | |||
15df853622 | |||
d3c0915598 | |||
ce98634dfd | |||
342678486d | |||
e8d4211d5c | |||
6a4d66d432 | |||
a3cf61b7cf | |||
a1b185b723 | |||
601064e41d | |||
e265ccd82c | |||
dd44f63c73 | |||
1d051c5841 | |||
323faf954b | |||
3169edd77a | |||
8de304c15a | |||
0d1d5898e3 | |||
6fe865b080 | |||
e0c0c44d99 | |||
13a0d527f6 | |||
ed7aa1c3e5 | |||
f902b5ec59 |
3
.bazelignore
Normal file
3
.bazelignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
aio/node_modules
|
@ -13,7 +13,7 @@ a GitHub token that enables publishing snapshots.
|
|||||||
|
|
||||||
To create the github_token file, we take this approach:
|
To create the github_token file, we take this approach:
|
||||||
- Find the angular-builds:token in http://valentine
|
- Find the angular-builds:token in http://valentine
|
||||||
- Go inside the ngcontainer docker image so you use the same version of openssl as we will at runtime: `docker run --rm -it angular/ngcontainer`
|
- Go inside the CircleCI default docker image so you use the same version of openssl as we will at runtime: `docker run --rm -it circleci/node:10.12`
|
||||||
- echo "https://[token]:@github.com" > credentials
|
- echo "https://[token]:@github.com" > credentials
|
||||||
- openssl aes-256-cbc -e -in credentials -out .circleci/github_token -k $KEY
|
- openssl aes-256-cbc -e -in credentials -out .circleci/github_token -k $KEY
|
||||||
- If needed, base64-encode the result so you can copy-paste it out of docker: `base64 github_token`
|
- If needed, base64-encode the result so you can copy-paste it out of docker: `base64 github_token`
|
@ -20,18 +20,6 @@ build --announce_rc
|
|||||||
# We use this when uploading artifacts after the build finishes
|
# We use this when uploading artifacts after the build finishes
|
||||||
build --symlink_prefix=dist/
|
build --symlink_prefix=dist/
|
||||||
|
|
||||||
# Enable experimental CircleCI bazel remote cache proxy
|
|
||||||
# See remote cache documentation in /docs/BAZEL.md
|
|
||||||
build --experimental_remote_spawn_cache --remote_rest_cache=http://localhost:7643
|
|
||||||
|
|
||||||
# Prevent unstable environment variables from tainting cache keys
|
|
||||||
build --experimental_strict_action_env
|
|
||||||
|
|
||||||
# Save downloaded repositories such as the go toolchain
|
|
||||||
# This directory can then be included in the CircleCI cache
|
|
||||||
# It should save time running the first build
|
|
||||||
build --experimental_repository_cache=/home/circleci/bazel_repository_cache
|
|
||||||
|
|
||||||
# Workaround https://github.com/bazelbuild/bazel/issues/3645
|
# Workaround https://github.com/bazelbuild/bazel/issues/3645
|
||||||
# Bazel doesn't calculate the memory ceiling correctly when running under Docker.
|
# Bazel doesn't calculate the memory ceiling correctly when running under Docker.
|
||||||
# Limit Bazel to consuming resources that fit in CircleCI "xlarge" class
|
# Limit Bazel to consuming resources that fit in CircleCI "xlarge" class
|
||||||
@ -40,3 +28,6 @@ build --local_resources=14336,8.0,1.0
|
|||||||
|
|
||||||
# Retry in the event of flakes, eg. https://circleci.com/gh/angular/angular/31309
|
# Retry in the event of flakes, eg. https://circleci.com/gh/angular/angular/31309
|
||||||
test --flaky_test_attempts=2
|
test --flaky_test_attempts=2
|
||||||
|
|
||||||
|
# More details on failures
|
||||||
|
build --verbose_failures=true
|
||||||
|
@ -7,85 +7,88 @@
|
|||||||
# To validate changes, use an online parser, eg.
|
# To validate changes, use an online parser, eg.
|
||||||
# http://yaml-online-parser.appspot.com/
|
# http://yaml-online-parser.appspot.com/
|
||||||
|
|
||||||
# Variables
|
# Note that the browser docker image comes with Chrome and Firefox preinstalled. This is just
|
||||||
|
# needed for jobs that run tests without Bazel. Bazel runs tests with browsers that will be
|
||||||
## IMPORTANT
|
# fetched by the Webtesting rules. Therefore for jobs that run tests with Bazel, we don't need a
|
||||||
# If you change the `docker_image` version, also change the `cache_key` suffix and the version of
|
# docker image with browsers pre-installed.
|
||||||
# `com_github_bazelbuild_buildtools` in the `/WORKSPACE` file.
|
# **NOTE**: If you change the version of the docker images, also change the `cache_key` suffix.
|
||||||
var_1: &docker_image angular/ngcontainer:0.3.3
|
var_1: &default_docker_image circleci/node:10.12
|
||||||
var_2: &cache_key v2-angular-{{ .Branch }}-{{ checksum "yarn.lock" }}-0.3.3
|
var_2: &browsers_docker_image circleci/node:10.12-browsers
|
||||||
|
var_3: &cache_key v2-angular-{{ .Branch }}-{{ checksum "yarn.lock" }}-node-10.12
|
||||||
|
|
||||||
# Define common ENV vars
|
# Define common ENV vars
|
||||||
var_3: &define_env_vars
|
var_4: &define_env_vars
|
||||||
run: echo "export PROJECT_ROOT=$(pwd)" >> $BASH_ENV
|
|
||||||
|
|
||||||
# See remote cache documentation in /docs/BAZEL.md
|
|
||||||
var_4: &setup-bazel-remote-cache
|
|
||||||
run:
|
run:
|
||||||
name: Start up bazel remote cache proxy
|
name: Define environment variables
|
||||||
command: ~/bazel-remote-proxy -backend circleci://
|
command: ./.circleci/env.sh
|
||||||
background: true
|
|
||||||
|
var_5: &setup_bazel_remote_execution
|
||||||
|
run:
|
||||||
|
name: "Setup bazel RBE remote execution"
|
||||||
|
command: openssl aes-256-cbc -d -in .circleci/gcp_token -k "$CI_REPO_NAME" -out /home/circleci/.gcp_credentials && echo "export GOOGLE_APPLICATION_CREDENTIALS=/home/circleci/.gcp_credentials" >> $BASH_ENV && sudo bash -c "cat .circleci/rbe-bazel.rc >> /etc/bazel.bazelrc"
|
||||||
|
|
||||||
# Settings common to each job
|
# Settings common to each job
|
||||||
anchor_1: &job_defaults
|
var_6: &job_defaults
|
||||||
working_directory: ~/ng
|
working_directory: ~/ng
|
||||||
docker:
|
docker:
|
||||||
- image: *docker_image
|
- image: *default_docker_image
|
||||||
|
|
||||||
# After checkout, rebase on top of master.
|
# After checkout, rebase on top of master.
|
||||||
# Similar to travis behavior, but not quite the same.
|
# Similar to travis behavior, but not quite the same.
|
||||||
# See https://discuss.circleci.com/t/1662
|
# See https://discuss.circleci.com/t/1662
|
||||||
anchor_2: &post_checkout
|
var_7: &post_checkout
|
||||||
post: git pull --ff-only origin "refs/pull/${CIRCLE_PULL_REQUEST//*pull\//}/merge"
|
post: git pull --ff-only origin "refs/pull/${CI_PULL_REQUEST//*pull\//}/merge"
|
||||||
|
|
||||||
|
var_8: &yarn_install
|
||||||
|
run:
|
||||||
|
name: Running Yarn install
|
||||||
|
command: yarn install --frozen-lockfile --non-interactive
|
||||||
|
|
||||||
|
var_9: &setup_circleci_bazel_config
|
||||||
|
run:
|
||||||
|
name: Setting up CircleCI bazel configuration
|
||||||
|
command: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
|
||||||
|
|
||||||
version: 2
|
version: 2
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
<<: *job_defaults
|
<<: *job_defaults
|
||||||
|
resource_class: xlarge
|
||||||
steps:
|
steps:
|
||||||
- checkout:
|
- checkout:
|
||||||
<<: *post_checkout
|
<<: *post_checkout
|
||||||
|
|
||||||
# Check BUILD.bazel formatting before we have a node_modules directory
|
|
||||||
# Then we don't need any exclude pattern to avoid checking those files
|
|
||||||
- run: 'buildifier -mode=check $(find . -type f \( -name BUILD.bazel -or -name BUILD \)) ||
|
|
||||||
(echo "BUILD files not formatted. Please run ''yarn buildifier''" ; exit 1)'
|
|
||||||
# Run the skylark linter to check our Bazel rules
|
|
||||||
# deprecated-api is disabled because we use actions.new_file(genfiles_dir)
|
|
||||||
# which has no replacement, see https://github.com/bazelbuild/bazel/issues/4858
|
|
||||||
- run: 'find . -type f -name "*.bzl" |
|
|
||||||
xargs java -jar /usr/local/bin/Skylint_deploy.jar --disable-checks=deprecated-api ||
|
|
||||||
(echo -e "\n.bzl files have lint errors. Please run ''yarn skylint''"; exit 1)'
|
|
||||||
|
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
key: *cache_key
|
key: *cache_key
|
||||||
|
- *define_env_vars
|
||||||
|
- *setup_circleci_bazel_config
|
||||||
|
- *yarn_install
|
||||||
|
|
||||||
|
- run: 'yarn buildifier -mode=check ||
|
||||||
|
(echo "BUILD files not formatted. Please run ''yarn buildifier''" ; exit 1)'
|
||||||
|
# Run the skylark linter to check our Bazel rules
|
||||||
|
- run: 'yarn skylint ||
|
||||||
|
(echo -e "\n.bzl files have lint errors. Please run ''yarn skylint''"; exit 1)'
|
||||||
|
|
||||||
- run: yarn install --frozen-lockfile --non-interactive
|
|
||||||
- run: ./node_modules/.bin/gulp lint
|
- run: ./node_modules/.bin/gulp lint
|
||||||
|
|
||||||
test:
|
test:
|
||||||
<<: *job_defaults
|
<<: *job_defaults
|
||||||
resource_class: xlarge
|
resource_class: xlarge
|
||||||
steps:
|
steps:
|
||||||
- *define_env_vars
|
|
||||||
- checkout:
|
- checkout:
|
||||||
<<: *post_checkout
|
<<: *post_checkout
|
||||||
# See remote cache documentation in /docs/BAZEL.md
|
|
||||||
- run: .circleci/setup_cache.sh
|
|
||||||
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
|
|
||||||
- *setup-bazel-remote-cache
|
|
||||||
|
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
key: *cache_key
|
key: *cache_key
|
||||||
|
- *define_env_vars
|
||||||
|
- *setup_circleci_bazel_config
|
||||||
|
- *yarn_install
|
||||||
|
|
||||||
- run: ls /home/circleci/bazel_repository_cache || true
|
# Setup remote execution and run RBE-compatible tests.
|
||||||
- run: bazel info release
|
- *setup_bazel_remote_execution
|
||||||
- run: bazel run @nodejs//:yarn
|
- run: yarn bazel test //... --build_tag_filters=-ivy-only --test_tag_filters=-ivy-only,-local
|
||||||
# Use bazel query so that we explicitly ask for all buildable targets to be built as well
|
# Now run RBE incompatible tests locally.
|
||||||
# This avoids waiting for the slowest build target to finish before running the first test
|
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
|
||||||
# See https://github.com/bazelbuild/bazel/issues/4257
|
- run: yarn bazel test //... --build_tag_filters=-ivy-only,local --test_tag_filters=-ivy-only,local
|
||||||
# NOTE: Angular developers should typically just bazel build //packages/... or bazel test //packages/...
|
|
||||||
- run: bazel query --output=label //... | xargs bazel test --build_tag_filters=-ivy-only --test_tag_filters=-manual,-ivy-only
|
|
||||||
|
|
||||||
# CircleCI will allow us to go back and view/download these artifacts from past builds.
|
# CircleCI will allow us to go back and view/download these artifacts from past builds.
|
||||||
# Also we can use a service like https://buildsize.org/ to automatically track binary size of these artifacts.
|
# Also we can use a service like https://buildsize.org/ to automatically track binary size of these artifacts.
|
||||||
@ -111,42 +114,200 @@ jobs:
|
|||||||
paths:
|
paths:
|
||||||
- "node_modules"
|
- "node_modules"
|
||||||
- "~/bazel_repository_cache"
|
- "~/bazel_repository_cache"
|
||||||
|
|
||||||
# Temporary job to test what will happen when we flip the Ivy flag to true
|
# Temporary job to test what will happen when we flip the Ivy flag to true
|
||||||
test_ivy_jit:
|
test_ivy_jit:
|
||||||
<<: *job_defaults
|
<<: *job_defaults
|
||||||
resource_class: xlarge
|
resource_class: xlarge
|
||||||
steps:
|
steps:
|
||||||
- *define_env_vars
|
# don't run this job on the patch branch (to preserve resources)
|
||||||
|
- run: circleci step halt
|
||||||
- checkout:
|
- checkout:
|
||||||
<<: *post_checkout
|
<<: *post_checkout
|
||||||
# See remote cache documentation in /docs/BAZEL.md
|
|
||||||
- run: .circleci/setup_cache.sh
|
|
||||||
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
|
|
||||||
- *setup-bazel-remote-cache
|
|
||||||
|
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
key: *cache_key
|
key: *cache_key
|
||||||
|
- *define_env_vars
|
||||||
|
- *setup_circleci_bazel_config
|
||||||
|
- *yarn_install
|
||||||
|
- *setup_bazel_remote_execution
|
||||||
|
|
||||||
- run: bazel run @yarn//:yarn
|
- run: yarn test-ivy-jit //...
|
||||||
- run: bazel query --output=label //... | xargs bazel test --define=compile=jit --build_tag_filters=ivy-jit --test_tag_filters=-manual,ivy-jit
|
|
||||||
|
|
||||||
test_ivy_aot:
|
test_ivy_aot:
|
||||||
<<: *job_defaults
|
<<: *job_defaults
|
||||||
resource_class: xlarge
|
resource_class: xlarge
|
||||||
steps:
|
steps:
|
||||||
- *define_env_vars
|
# don't run this job on the patch branch (to preserve resources)
|
||||||
|
- run: circleci step halt
|
||||||
- checkout:
|
- checkout:
|
||||||
<<: *post_checkout
|
<<: *post_checkout
|
||||||
# See remote cache documentation in /docs/BAZEL.md
|
|
||||||
- run: .circleci/setup_cache.sh
|
|
||||||
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
|
|
||||||
- *setup-bazel-remote-cache
|
|
||||||
|
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
key: *cache_key
|
key: *cache_key
|
||||||
|
- *define_env_vars
|
||||||
|
- *setup_circleci_bazel_config
|
||||||
|
- *yarn_install
|
||||||
|
- *setup_bazel_remote_execution
|
||||||
|
|
||||||
- run: bazel run @yarn//:yarn
|
- run: yarn test-ivy-aot //...
|
||||||
- run: bazel query --output=label //... | xargs bazel test --define=compile=local --build_tag_filters=ivy-local --test_tag_filters=-manual,ivy-local
|
|
||||||
|
test_aio:
|
||||||
|
<<: *job_defaults
|
||||||
|
docker:
|
||||||
|
# Needed because the AIO tests and the PWA score test depend on Chrome being available.
|
||||||
|
- image: *browsers_docker_image
|
||||||
|
steps:
|
||||||
|
- checkout:
|
||||||
|
<<: *post_checkout
|
||||||
|
- restore_cache:
|
||||||
|
key: *cache_key
|
||||||
|
- *define_env_vars
|
||||||
|
# Build aio
|
||||||
|
- run: yarn --cwd aio build --progress=false
|
||||||
|
# Lint the code
|
||||||
|
- run: yarn --cwd aio lint
|
||||||
|
# Run PWA-score tests
|
||||||
|
# (Run before unit and e2e tests, which destroy the `dist/` directory.)
|
||||||
|
- run: yarn --cwd aio test-pwa-score-localhost $CI_AIO_MIN_PWA_SCORE
|
||||||
|
# Check the bundle sizes.
|
||||||
|
# (Run before unit and e2e tests, which destroy the `dist/` directory.)
|
||||||
|
- run: yarn --cwd aio payload-size
|
||||||
|
# Run unit tests
|
||||||
|
- run: yarn --cwd aio test --watch=false
|
||||||
|
# Run e2e tests
|
||||||
|
- run: yarn --cwd aio e2e
|
||||||
|
# Run unit tests for Firebase redirects
|
||||||
|
- run: yarn --cwd aio redirects-test
|
||||||
|
|
||||||
|
deploy_aio:
|
||||||
|
<<: *job_defaults
|
||||||
|
docker:
|
||||||
|
# Needed because before deploying the deploy-production script runs the PWA score tests.
|
||||||
|
- image: *browsers_docker_image
|
||||||
|
steps:
|
||||||
|
- checkout:
|
||||||
|
<<: *post_checkout
|
||||||
|
- restore_cache:
|
||||||
|
key: *cache_key
|
||||||
|
- *define_env_vars
|
||||||
|
# Deploy angular.io to production (if necessary)
|
||||||
|
- run: setPublicVar CI_STABLE_BRANCH "$(npm info @angular/core dist-tags.latest | sed -r 's/^\s*([0-9]+\.[0-9]+)\.[0-9]+.*$/\1.x/')"
|
||||||
|
- run: yarn --cwd aio deploy-production
|
||||||
|
|
||||||
|
test_aio_local:
|
||||||
|
<<: *job_defaults
|
||||||
|
docker:
|
||||||
|
# Needed because the AIO tests and the PWA score test depend on Chrome being available.
|
||||||
|
- image: *browsers_docker_image
|
||||||
|
steps:
|
||||||
|
- checkout:
|
||||||
|
<<: *post_checkout
|
||||||
|
- restore_cache:
|
||||||
|
key: *cache_key
|
||||||
|
- attach_workspace:
|
||||||
|
at: dist
|
||||||
|
- *define_env_vars
|
||||||
|
# Build aio (with local Angular packages)
|
||||||
|
- run: yarn --cwd aio build-local --progress=false
|
||||||
|
# Run PWA-score tests
|
||||||
|
# (Run before unit and e2e tests, which destroy the `dist/` directory.)
|
||||||
|
- run: yarn --cwd aio test-pwa-score-localhost $CI_AIO_MIN_PWA_SCORE
|
||||||
|
# Run unit tests
|
||||||
|
- run: yarn --cwd aio test --watch=false
|
||||||
|
# Run e2e tests
|
||||||
|
- run: yarn --cwd aio e2e
|
||||||
|
|
||||||
|
test_aio_tools:
|
||||||
|
<<: *job_defaults
|
||||||
|
steps:
|
||||||
|
- checkout:
|
||||||
|
<<: *post_checkout
|
||||||
|
- restore_cache:
|
||||||
|
key: *cache_key
|
||||||
|
- attach_workspace:
|
||||||
|
at: dist
|
||||||
|
- *define_env_vars
|
||||||
|
# Install
|
||||||
|
- run: yarn --cwd aio install --frozen-lockfile --non-interactive
|
||||||
|
- run: yarn --cwd aio extract-cli-command-docs
|
||||||
|
# Run tools tests
|
||||||
|
- run: yarn --cwd aio tools-test
|
||||||
|
- run: ./aio/aio-builds-setup/scripts/test.sh
|
||||||
|
|
||||||
|
test_docs_examples_0:
|
||||||
|
<<: *job_defaults
|
||||||
|
docker:
|
||||||
|
# Needed because the example e2e tests depend on Chrome.
|
||||||
|
- image: *browsers_docker_image
|
||||||
|
steps:
|
||||||
|
- checkout:
|
||||||
|
<<: *post_checkout
|
||||||
|
- restore_cache:
|
||||||
|
key: *cache_key
|
||||||
|
- attach_workspace:
|
||||||
|
at: dist
|
||||||
|
- *define_env_vars
|
||||||
|
# Install root
|
||||||
|
- *yarn_install
|
||||||
|
# Install aio
|
||||||
|
- run: yarn --cwd aio install --frozen-lockfile --non-interactive
|
||||||
|
# Run examples tests
|
||||||
|
- run: yarn --cwd aio example-e2e --setup --local --shard=0/2
|
||||||
|
|
||||||
|
test_docs_examples_1:
|
||||||
|
<<: *job_defaults
|
||||||
|
docker:
|
||||||
|
# Needed because the example e2e tests depend on Chrome.
|
||||||
|
- image: *browsers_docker_image
|
||||||
|
steps:
|
||||||
|
- checkout:
|
||||||
|
<<: *post_checkout
|
||||||
|
- restore_cache:
|
||||||
|
key: *cache_key
|
||||||
|
- attach_workspace:
|
||||||
|
at: dist
|
||||||
|
- *define_env_vars
|
||||||
|
# Install root
|
||||||
|
- *yarn_install
|
||||||
|
# Install aio
|
||||||
|
- run: yarn --cwd aio install --frozen-lockfile --non-interactive
|
||||||
|
# Run examples tests
|
||||||
|
- run: yarn --cwd aio example-e2e --setup --local --shard=1/2
|
||||||
|
|
||||||
|
# This job should only be run on PR builds, where `CI_PULL_REQUEST` is not `false`.
|
||||||
|
aio_preview:
|
||||||
|
<<: *job_defaults
|
||||||
|
environment:
|
||||||
|
AIO_SNAPSHOT_ARTIFACT_PATH: &aio_preview_artifact_path 'aio/tmp/snapshot.tgz'
|
||||||
|
steps:
|
||||||
|
- checkout:
|
||||||
|
<<: *post_checkout
|
||||||
|
- restore_cache:
|
||||||
|
key: *cache_key
|
||||||
|
- *define_env_vars
|
||||||
|
- *yarn_install
|
||||||
|
- run: ./aio/scripts/build-artifacts.sh $AIO_SNAPSHOT_ARTIFACT_PATH $CI_PULL_REQUEST $CI_COMMIT
|
||||||
|
- store_artifacts:
|
||||||
|
path: *aio_preview_artifact_path
|
||||||
|
# The `destination` needs to be kept in synch with the value of
|
||||||
|
# `AIO_ARTIFACT_PATH` in `aio/aio-builds-setup/Dockerfile`
|
||||||
|
destination: aio/dist/aio-snapshot.tgz
|
||||||
|
|
||||||
|
# This job should only be run on PR builds, where `CI_PULL_REQUEST` is not `false`.
|
||||||
|
test_aio_preview:
|
||||||
|
<<: *job_defaults
|
||||||
|
docker:
|
||||||
|
# Needed because the test-preview script runs e2e tests and the PWA score test with Chrome.
|
||||||
|
- image: *browsers_docker_image
|
||||||
|
steps:
|
||||||
|
- checkout:
|
||||||
|
<<: *post_checkout
|
||||||
|
- restore_cache:
|
||||||
|
key: *cache_key
|
||||||
|
- *define_env_vars
|
||||||
|
- run: yarn install --cwd aio --frozen-lockfile --non-interactive
|
||||||
|
- run:
|
||||||
|
name: Wait for preview and run tests
|
||||||
|
command: node aio/scripts/test-preview.js $CI_PULL_REQUEST $CI_COMMIT $CI_AIO_MIN_PWA_SCORE
|
||||||
|
|
||||||
# This job exists only for backwards-compatibility with old scripts and tests
|
# This job exists only for backwards-compatibility with old scripts and tests
|
||||||
# that rely on the pre-Bazel dist/packages-dist layout.
|
# that rely on the pre-Bazel dist/packages-dist layout.
|
||||||
@ -159,15 +320,15 @@ jobs:
|
|||||||
<<: *job_defaults
|
<<: *job_defaults
|
||||||
resource_class: xlarge
|
resource_class: xlarge
|
||||||
steps:
|
steps:
|
||||||
- *define_env_vars
|
|
||||||
- checkout:
|
- checkout:
|
||||||
<<: *post_checkout
|
<<: *post_checkout
|
||||||
# See remote cache documentation in /docs/BAZEL.md
|
- restore_cache:
|
||||||
- run: .circleci/setup_cache.sh
|
key: *cache_key
|
||||||
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
|
- *define_env_vars
|
||||||
- *setup-bazel-remote-cache
|
- *setup_circleci_bazel_config
|
||||||
|
- *yarn_install
|
||||||
|
- *setup_bazel_remote_execution
|
||||||
|
|
||||||
- run: bazel run @nodejs//:yarn
|
|
||||||
- run: scripts/build-packages-dist.sh
|
- run: scripts/build-packages-dist.sh
|
||||||
|
|
||||||
# Save the npm packages from //packages/... for other workflow jobs to read
|
# Save the npm packages from //packages/... for other workflow jobs to read
|
||||||
@ -177,7 +338,7 @@ jobs:
|
|||||||
paths:
|
paths:
|
||||||
- packages-dist
|
- packages-dist
|
||||||
- packages-dist-ivy-jit
|
- packages-dist-ivy-jit
|
||||||
- packages-dist-ivy-local
|
- packages-dist-ivy-aot
|
||||||
|
|
||||||
# We run the integration tests outside of Bazel for now.
|
# We run the integration tests outside of Bazel for now.
|
||||||
# They are a separate workflow job so that they can be easily re-run.
|
# They are a separate workflow job so that they can be easily re-run.
|
||||||
@ -187,35 +348,41 @@ jobs:
|
|||||||
# See comments inside the integration/run_tests.sh script.
|
# See comments inside the integration/run_tests.sh script.
|
||||||
integration_test:
|
integration_test:
|
||||||
<<: *job_defaults
|
<<: *job_defaults
|
||||||
|
docker:
|
||||||
|
# Needed because the integration tests expect Chrome to be installed (e.g cli-hello-world)
|
||||||
|
- image: *browsers_docker_image
|
||||||
# Note: we run Bazel in one of the integration tests, and it can consume >2G
|
# Note: we run Bazel in one of the integration tests, and it can consume >2G
|
||||||
# of memory. Together with the system under test, this can exhaust the RAM
|
# of memory. Together with the system under test, this can exhaust the RAM
|
||||||
# on a 4G worker so we use a larger machine here too.
|
# on a 4G worker so we use a larger machine here too.
|
||||||
resource_class: xlarge
|
resource_class: xlarge
|
||||||
steps:
|
steps:
|
||||||
- *define_env_vars
|
|
||||||
- checkout:
|
- checkout:
|
||||||
<<: *post_checkout
|
<<: *post_checkout
|
||||||
|
- restore_cache:
|
||||||
|
key: *cache_key
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: dist
|
at: dist
|
||||||
- run: xvfb-run --auto-servernum ./integration/run_tests.sh
|
- *define_env_vars
|
||||||
|
- run: ./integration/run_tests.sh
|
||||||
|
|
||||||
# This job updates the content of repos like github.com/angular/core-builds
|
# This job updates the content of repos like github.com/angular/core-builds
|
||||||
# for every green build on angular/angular.
|
# for every green build on angular/angular.
|
||||||
publish_snapshot:
|
publish_snapshot:
|
||||||
<<: *job_defaults
|
<<: *job_defaults
|
||||||
steps:
|
steps:
|
||||||
|
- checkout:
|
||||||
|
<<: *post_checkout
|
||||||
|
- *define_env_vars
|
||||||
# See below - ideally this job should not trigger for non-upstream builds.
|
# See below - ideally this job should not trigger for non-upstream builds.
|
||||||
# But since it does, we have to check this condition.
|
# But since it does, we have to check this condition.
|
||||||
- run:
|
- run:
|
||||||
name: Skip this job for Pull Requests and Fork builds
|
name: Skip this job for Pull Requests and Fork builds
|
||||||
# Note, `|| true` on the end makes this step always exit 0
|
# Note, `|| true` on the end makes this step always exit 0
|
||||||
command: '[[
|
command: '[[
|
||||||
-v CIRCLE_PR_NUMBER
|
"$CI_PULL_REQUEST" != "false"
|
||||||
|| "$CIRCLE_PROJECT_USERNAME" != "angular"
|
|| "$CI_REPO_OWNER" != "angular"
|
||||||
|| "$CIRCLE_PROJECT_REPONAME" != "angular"
|
|| "$CI_REPO_NAME" != "angular"
|
||||||
]] && circleci step halt || true'
|
]] && circleci step halt || true'
|
||||||
- checkout:
|
|
||||||
<<: *post_checkout
|
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: dist
|
at: dist
|
||||||
# CircleCI has a config setting to force SSH for all github connections
|
# CircleCI has a config setting to force SSH for all github connections
|
||||||
@ -229,12 +396,23 @@ jobs:
|
|||||||
|
|
||||||
aio_monitoring:
|
aio_monitoring:
|
||||||
<<: *job_defaults
|
<<: *job_defaults
|
||||||
|
docker:
|
||||||
|
# This job needs Chrome to be globally installed because the tests run with Protractor
|
||||||
|
# which does not load the browser through the Bazel webtesting rules.
|
||||||
|
- image: *browsers_docker_image
|
||||||
steps:
|
steps:
|
||||||
- checkout:
|
- checkout:
|
||||||
<<: *post_checkout
|
<<: *post_checkout
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
key: *cache_key
|
key: *cache_key
|
||||||
- run: xvfb-run --auto-servernum ./aio/scripts/test-production.sh
|
- *define_env_vars
|
||||||
|
- run:
|
||||||
|
name: Run tests against the deployed apps
|
||||||
|
command: ./aio/scripts/test-production.sh $CI_AIO_MIN_PWA_SCORE
|
||||||
|
- run:
|
||||||
|
name: Notify caretaker about failure
|
||||||
|
command: 'curl --request POST --header "Content-Type: application/json" --data "{\"text\":\":x: \`$CIRCLE_JOB\` job failed on build $CIRCLE_BUILD_NUM: $CIRCLE_BUILD_URL :scream:\"}" $CI_SECRET_SLACK_CARETAKER_WEBHOOK_URL'
|
||||||
|
when: on_fail
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
version: 2
|
version: 2
|
||||||
@ -245,6 +423,30 @@ workflows:
|
|||||||
- test_ivy_jit
|
- test_ivy_jit
|
||||||
- test_ivy_aot
|
- test_ivy_aot
|
||||||
- build-packages-dist
|
- build-packages-dist
|
||||||
|
- test_aio
|
||||||
|
- deploy_aio:
|
||||||
|
requires:
|
||||||
|
- test_aio
|
||||||
|
- test_aio_local:
|
||||||
|
requires:
|
||||||
|
- build-packages-dist
|
||||||
|
- test_aio_tools:
|
||||||
|
requires:
|
||||||
|
- build-packages-dist
|
||||||
|
- test_docs_examples_0:
|
||||||
|
requires:
|
||||||
|
- build-packages-dist
|
||||||
|
- test_docs_examples_1:
|
||||||
|
requires:
|
||||||
|
- build-packages-dist
|
||||||
|
- aio_preview:
|
||||||
|
# Only run on PR builds. (There can be no previews for non-PR builds.)
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only: /pull\/\d+/
|
||||||
|
- test_aio_preview:
|
||||||
|
requires:
|
||||||
|
- aio_preview
|
||||||
- integration_test:
|
- integration_test:
|
||||||
requires:
|
requires:
|
||||||
- build-packages-dist
|
- build-packages-dist
|
||||||
@ -259,6 +461,10 @@ workflows:
|
|||||||
- test_ivy_jit
|
- test_ivy_jit
|
||||||
- test_ivy_aot
|
- test_ivy_aot
|
||||||
- integration_test
|
- integration_test
|
||||||
|
# Only publish if `aio`/`docs` tests using the locally built Angular packages pass
|
||||||
|
- test_aio_local
|
||||||
|
- test_docs_examples_0
|
||||||
|
- test_docs_examples_1
|
||||||
# Get the artifacts to publish from the build-packages-dist job
|
# Get the artifacts to publish from the build-packages-dist job
|
||||||
# since the publishing script expects the legacy outputs layout.
|
# since the publishing script expects the legacy outputs layout.
|
||||||
- build-packages-dist
|
- build-packages-dist
|
||||||
@ -273,3 +479,7 @@ workflows:
|
|||||||
branches:
|
branches:
|
||||||
only:
|
only:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
|
notify:
|
||||||
|
webhooks:
|
||||||
|
- url: https://ngbuilds.io/circle-build
|
||||||
|
38
.circleci/env-helpers.inc.sh
Normal file
38
.circleci/env-helpers.inc.sh
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
####################################################################################################
|
||||||
|
# Helpers for defining environment variables for CircleCI.
|
||||||
|
#
|
||||||
|
# In CircleCI, each step runs in a new shell. The way to share ENV variables across steps is to
|
||||||
|
# export them from `$BASH_ENV`, which is automatically sourced at the beginning of every step (for
|
||||||
|
# the default `bash` shell).
|
||||||
|
#
|
||||||
|
# See also https://circleci.com/docs/2.0/env-vars/#using-bash_env-to-set-environment-variables.
|
||||||
|
####################################################################################################
|
||||||
|
|
||||||
|
# Set and print an environment variable.
|
||||||
|
#
|
||||||
|
# Use this function for setting environment variables that are public, i.e. it is OK for them to be
|
||||||
|
# visible to anyone through the CI logs.
|
||||||
|
#
|
||||||
|
# Usage: `setPublicVar <name> <value>`
|
||||||
|
function setPublicVar() {
|
||||||
|
setSecretVar $1 $2;
|
||||||
|
echo "$1=$2";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set (without printing) an environment variable.
|
||||||
|
#
|
||||||
|
# Use this function for setting environment variables that are secret, i.e. should not be visible to
|
||||||
|
# everyone through the CI logs.
|
||||||
|
#
|
||||||
|
# Usage: `setSecretVar <name> <value>`
|
||||||
|
function setSecretVar() {
|
||||||
|
# WARNING: Secrets (e.g. passwords, access tokens) should NOT be printed.
|
||||||
|
# (Keep original shell options to restore at the end.)
|
||||||
|
local -r originalShellOptions=$(set +o);
|
||||||
|
set +x -eu -o pipefail;
|
||||||
|
|
||||||
|
echo "export $1=\"${2:-}\";" >> $BASH_ENV;
|
||||||
|
|
||||||
|
# Restore original shell options.
|
||||||
|
eval "$originalShellOptions";
|
||||||
|
}
|
35
.circleci/env.sh
Executable file
35
.circleci/env.sh
Executable file
@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Load helpers and make them available everywhere (through `$BASH_ENV`).
|
||||||
|
readonly envHelpersPath="`dirname $0`/env-helpers.inc.sh";
|
||||||
|
source $envHelpersPath;
|
||||||
|
echo "source $envHelpersPath;" >> $BASH_ENV;
|
||||||
|
|
||||||
|
|
||||||
|
####################################################################################################
|
||||||
|
# Define PUBLIC environment variables for CircleCI.
|
||||||
|
####################################################################################################
|
||||||
|
setPublicVar PROJECT_ROOT "$(pwd)";
|
||||||
|
setPublicVar CI_AIO_MIN_PWA_SCORE "95";
|
||||||
|
# This is the branch being built; e.g. `pull/12345` for PR builds.
|
||||||
|
setPublicVar CI_BRANCH "$CIRCLE_BRANCH";
|
||||||
|
setPublicVar CI_COMMIT "$CIRCLE_SHA1";
|
||||||
|
# `CI_COMMIT_RANGE` will only be available when `CIRCLE_COMPARE_URL` is also available,
|
||||||
|
# i.e. on push builds (a.k.a. non-PR builds). That is fine, since we only need it in push builds.
|
||||||
|
setPublicVar CI_COMMIT_RANGE "$(sed -r 's|^.*/([0-9a-f]+\.\.\.[0-9a-f]+)$|\1|i' <<< ${CIRCLE_COMPARE_URL:-})";
|
||||||
|
setPublicVar CI_PULL_REQUEST "${CIRCLE_PR_NUMBER:-false}";
|
||||||
|
setPublicVar CI_REPO_NAME "$CIRCLE_PROJECT_REPONAME";
|
||||||
|
setPublicVar CI_REPO_OWNER "$CIRCLE_PROJECT_USERNAME";
|
||||||
|
|
||||||
|
|
||||||
|
####################################################################################################
|
||||||
|
# Define SECRET environment variables for CircleCI.
|
||||||
|
####################################################################################################
|
||||||
|
setSecretVar CI_SECRET_AIO_DEPLOY_FIREBASE_TOKEN "$AIO_DEPLOY_TOKEN";
|
||||||
|
setSecretVar CI_SECRET_PAYLOAD_FIREBASE_TOKEN "$ANGULAR_PAYLOAD_TOKEN";
|
||||||
|
# Defined in https://angular-team.slack.com/apps/A0F7VRE7N-circleci.
|
||||||
|
setSecretVar CI_SECRET_SLACK_CARETAKER_WEBHOOK_URL "$SLACK_CARETAKER_WEBHOOK_URL";
|
||||||
|
|
||||||
|
|
||||||
|
# Source `$BASH_ENV` to make the variables available immediately.
|
||||||
|
source $BASH_ENV;
|
BIN
.circleci/gcp_token
Normal file
BIN
.circleci/gcp_token
Normal file
Binary file not shown.
Binary file not shown.
77
.circleci/rbe-bazel.rc
Normal file
77
.circleci/rbe-bazel.rc
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
# These options are enabled when running on CI with Remote Build Execution.
|
||||||
|
|
||||||
|
################################################################
|
||||||
|
# Toolchain related flags for remote build execution. #
|
||||||
|
################################################################
|
||||||
|
# Remote Build Execution requires a strong hash function, such as SHA256.
|
||||||
|
startup --host_jvm_args=-Dbazel.DigestFunction=SHA256
|
||||||
|
|
||||||
|
# Depending on how many machines are in the remote execution instance, setting
|
||||||
|
# this higher can make builds faster by allowing more jobs to run in parallel.
|
||||||
|
# Setting it too high can result in jobs that timeout, however, while waiting
|
||||||
|
# for a remote machine to execute them.
|
||||||
|
build --jobs=150
|
||||||
|
|
||||||
|
# Set several flags related to specifying the platform, toolchain and java
|
||||||
|
# properties.
|
||||||
|
# These flags are duplicated rather than imported from (for example)
|
||||||
|
# %workspace%/configs/ubuntu16_04_clang/1.0/toolchain.bazelrc to make this
|
||||||
|
# bazelrc a standalone file that can be copied more easily.
|
||||||
|
# These flags should only be used as is for the rbe-ubuntu16-04 container
|
||||||
|
# and need to be adapted to work with other toolchain containers.
|
||||||
|
build --host_javabase=@bazel_toolchains//configs/ubuntu16_04_clang/1.0:jdk8
|
||||||
|
build --javabase=@bazel_toolchains//configs/ubuntu16_04_clang/1.0:jdk8
|
||||||
|
build --host_java_toolchain=@bazel_tools//tools/jdk:toolchain_hostjdk8
|
||||||
|
build --java_toolchain=@bazel_tools//tools/jdk:toolchain_hostjdk8
|
||||||
|
build --crosstool_top=@bazel_toolchains//configs/ubuntu16_04_clang/1.0/bazel_0.15.0/default:toolchain
|
||||||
|
build --action_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1
|
||||||
|
# Platform flags:
|
||||||
|
# The toolchain container used for execution is defined in the target indicated
|
||||||
|
# by "extra_execution_platforms", "host_platform" and "platforms".
|
||||||
|
# If you are using your own toolchain container, you need to create a platform
|
||||||
|
# target with "constraint_values" that allow for the toolchain specified with
|
||||||
|
# "extra_toolchains" to be selected (given constraints defined in
|
||||||
|
# "exec_compatible_with").
|
||||||
|
# More about platforms: https://docs.bazel.build/versions/master/platforms.html
|
||||||
|
build --extra_toolchains=@bazel_toolchains//configs/ubuntu16_04_clang/1.0/bazel_0.15.0/cpp:cc-toolchain-clang-x86_64-default
|
||||||
|
build --extra_execution_platforms=//tools:rbe_ubuntu1604-angular
|
||||||
|
build --host_platform=//tools:rbe_ubuntu1604-angular
|
||||||
|
build --platforms=//tools:rbe_ubuntu1604-angular
|
||||||
|
|
||||||
|
# Set various strategies so that all actions execute remotely. Mixing remote
|
||||||
|
# and local execution will lead to errors unless the toolchain and remote
|
||||||
|
# machine exactly match the host machine.
|
||||||
|
build --spawn_strategy=remote
|
||||||
|
build --strategy=Javac=remote
|
||||||
|
build --strategy=Closure=remote
|
||||||
|
build --genrule_strategy=remote
|
||||||
|
build --define=EXECUTOR=remote
|
||||||
|
|
||||||
|
# Enable the remote cache so action results can be shared across machines,
|
||||||
|
# developers, and workspaces.
|
||||||
|
build --remote_cache=remotebuildexecution.googleapis.com
|
||||||
|
|
||||||
|
# Enable remote execution so actions are performed on the remote systems.
|
||||||
|
build --remote_executor=remotebuildexecution.googleapis.com
|
||||||
|
|
||||||
|
# Remote instance.
|
||||||
|
build --remote_instance_name=projects/internal-200822/instances/default_instance
|
||||||
|
|
||||||
|
# Enable encryption.
|
||||||
|
build --tls_enabled=true
|
||||||
|
|
||||||
|
# Enforce stricter environment rules, which eliminates some non-hermetic
|
||||||
|
# behavior and therefore improves both the remote cache hit rate and the
|
||||||
|
# correctness and repeatability of the build.
|
||||||
|
build --experimental_strict_action_env=true
|
||||||
|
|
||||||
|
# Set a higher timeout value, just in case.
|
||||||
|
build --remote_timeout=3600
|
||||||
|
|
||||||
|
# Enable authentication. This will pick up application default credentials by
|
||||||
|
# default. You can use --auth_credentials=some_file.json to use a service
|
||||||
|
# account credential instead.
|
||||||
|
build --auth_enabled=true
|
||||||
|
|
||||||
|
# Do not accept remote cache.
|
||||||
|
build --remote_accept_cached=false
|
61
.github/ISSUE_TEMPLATE.md
vendored
61
.github/ISSUE_TEMPLATE.md
vendored
@ -1,59 +1,10 @@
|
|||||||
<!--
|
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
||||||
PLEASE HELP US PROCESS GITHUB ISSUES FASTER BY PROVIDING THE FOLLOWING INFORMATION.
|
|
||||||
|
|
||||||
ISSUES MISSING IMPORTANT INFORMATION MAY BE CLOSED WITHOUT INVESTIGATION.
|
Please help us process issues more efficiently by filing an
|
||||||
-->
|
issue using one of the following templates:
|
||||||
|
|
||||||
## I'm submitting a...
|
https://github.com/angular/angular/issues/new/choose
|
||||||
<!-- Check one of the following options with "x" -->
|
|
||||||
<pre><code>
|
|
||||||
[ ] Regression (a behavior that used to work and stopped working in a new release)
|
|
||||||
[ ] Bug report <!-- Please search GitHub for a similar issue or PR before submitting -->
|
|
||||||
[ ] Performance issue
|
|
||||||
[ ] Feature request
|
|
||||||
[ ] Documentation issue or request
|
|
||||||
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question
|
|
||||||
[ ] Other... Please describe:
|
|
||||||
</code></pre>
|
|
||||||
|
|
||||||
## Current behavior
|
Thank you!
|
||||||
<!-- Describe how the issue manifests. -->
|
|
||||||
|
|
||||||
|
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
||||||
## Expected behavior
|
|
||||||
<!-- Describe what the desired behavior would be. -->
|
|
||||||
|
|
||||||
|
|
||||||
## Minimal reproduction of the problem with instructions
|
|
||||||
<!--
|
|
||||||
For bug reports please provide the *STEPS TO REPRODUCE* and if possible a *MINIMAL DEMO* of the problem via
|
|
||||||
https://stackblitz.com or similar (you can use this template as a starting point: https://stackblitz.com/fork/angular-gitter).
|
|
||||||
-->
|
|
||||||
|
|
||||||
## What is the motivation / use case for changing the behavior?
|
|
||||||
<!-- Describe the motivation or the concrete use case. -->
|
|
||||||
|
|
||||||
|
|
||||||
## Environment
|
|
||||||
|
|
||||||
<pre><code>
|
|
||||||
Angular version: X.Y.Z
|
|
||||||
<!-- Check whether this is still an issue in the most recent Angular version -->
|
|
||||||
|
|
||||||
Browser:
|
|
||||||
- [ ] Chrome (desktop) version XX
|
|
||||||
- [ ] Chrome (Android) version XX
|
|
||||||
- [ ] Chrome (iOS) version XX
|
|
||||||
- [ ] Firefox version XX
|
|
||||||
- [ ] Safari (desktop) version XX
|
|
||||||
- [ ] Safari (iOS) version XX
|
|
||||||
- [ ] IE version XX
|
|
||||||
- [ ] Edge version XX
|
|
||||||
|
|
||||||
For Tooling issues:
|
|
||||||
- Node version: XX <!-- run `node --version` -->
|
|
||||||
- Platform: <!-- Mac, Linux, Windows -->
|
|
||||||
|
|
||||||
Others:
|
|
||||||
<!-- Anything else relevant? Operating system version, IDE, package manager, HTTP server, ... -->
|
|
||||||
</code></pre>
|
|
||||||
|
63
.github/ISSUE_TEMPLATE/1-bug-report.md
vendored
Normal file
63
.github/ISSUE_TEMPLATE/1-bug-report.md
vendored
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
name: "\U0001F41EBug report"
|
||||||
|
about: Report a bug in the Angular Framework
|
||||||
|
---
|
||||||
|
<!--🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅
|
||||||
|
|
||||||
|
Oh hi there! 😄
|
||||||
|
|
||||||
|
To expedite issue processing please search open and closed issues before submitting a new one.
|
||||||
|
Existing issues often contain information about workarounds, resolution, or progress updates.
|
||||||
|
|
||||||
|
🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅-->
|
||||||
|
|
||||||
|
|
||||||
|
# 🐞 bug report
|
||||||
|
|
||||||
|
### Affected Package
|
||||||
|
<!-- Can you pin-point one or more @angular/* packages as the source of the bug? -->
|
||||||
|
<!-- ✍️edit: --> The issue is caused by package @angular/....
|
||||||
|
|
||||||
|
|
||||||
|
### Is this a regression?
|
||||||
|
|
||||||
|
<!-- Did this behavior use to work in the previous version? -->
|
||||||
|
<!-- ✍️--> Yes, the previous version in which this bug was not present was: ....
|
||||||
|
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
<!-- ✍️--> A clear and concise description of the problem...
|
||||||
|
|
||||||
|
|
||||||
|
## 🔬 Minimal Reproduction
|
||||||
|
<!--
|
||||||
|
Please create and share minimal reproduction of the issue starting with this template: https://stackblitz.com/fork/angular-issue-repro2
|
||||||
|
-->
|
||||||
|
<!-- ✍️--> https://stackblitz.com/...
|
||||||
|
|
||||||
|
<!--
|
||||||
|
If StackBlitz is not suitable for reproduction of your issue, please create a minimal GitHub repository with the reproduction of the issue. Share the link to the repo below along with step-by-step instructions to reproduce the problem, as well as expected and actual behavior.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## 🔥 Exception or Error
|
||||||
|
<pre><code>
|
||||||
|
<!-- If the issue is accompanied by an exception or an error, please share it below: -->
|
||||||
|
<!-- ✍️-->
|
||||||
|
|
||||||
|
</code></pre>
|
||||||
|
|
||||||
|
|
||||||
|
## 🌍 Your Environment
|
||||||
|
|
||||||
|
**Angular Version:**
|
||||||
|
<pre><code>
|
||||||
|
<!-- run `ng version` and paste output below -->
|
||||||
|
<!-- ✍️-->
|
||||||
|
|
||||||
|
</code></pre>
|
||||||
|
|
||||||
|
**Anything else relevant?**
|
||||||
|
<!-- ✍️Is this a browser specific issue? If so, please specify the browser and version. -->
|
||||||
|
|
||||||
|
<!-- ✍️Do any of these matter: operating system, IDE, package manager, HTTP server, ...? If so, please mention it below. -->
|
32
.github/ISSUE_TEMPLATE/2-feature-request.md
vendored
Normal file
32
.github/ISSUE_TEMPLATE/2-feature-request.md
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
name: "\U0001F680Feature request"
|
||||||
|
about: Suggest a feature for Angular Framework
|
||||||
|
|
||||||
|
---
|
||||||
|
<!--🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅
|
||||||
|
|
||||||
|
Oh hi there! 😄
|
||||||
|
|
||||||
|
To expedite issue processing please search open and closed issues before submitting a new one.
|
||||||
|
Existing issues often contain information about workarounds, resolution, or progress updates.
|
||||||
|
|
||||||
|
🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅-->
|
||||||
|
|
||||||
|
|
||||||
|
# 🚀 feature request
|
||||||
|
|
||||||
|
### Releavant Package
|
||||||
|
<!-- Can you pin-point one or more @angular/* packages the are relevant for this feature request? -->
|
||||||
|
<!-- ✍️edit: --> This feature request is for @angular/....
|
||||||
|
|
||||||
|
|
||||||
|
### Description
|
||||||
|
<!-- ✍️--> A clear and concise description of the problem or missing capability...
|
||||||
|
|
||||||
|
|
||||||
|
### Describe the solution you'd like
|
||||||
|
<!-- ✍️--> If you have a solution in mind, please describe it.
|
||||||
|
|
||||||
|
|
||||||
|
### Describe alternatives you've considered
|
||||||
|
<!-- ✍️--> Have you considered any alternative solutions or workarounds?
|
55
.github/ISSUE_TEMPLATE/3-docs-bug.md
vendored
Normal file
55
.github/ISSUE_TEMPLATE/3-docs-bug.md
vendored
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
name: "📚 Docs or angular.io issue report"
|
||||||
|
about: Report an issue in Angular's documentation or angular.io application
|
||||||
|
|
||||||
|
---
|
||||||
|
<!--🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅
|
||||||
|
|
||||||
|
Oh hi there! 😄
|
||||||
|
|
||||||
|
To expedite issue processing please search open and closed issues before submitting a new one.
|
||||||
|
Existing issues often contain information about workarounds, resolution, or progress updates.
|
||||||
|
|
||||||
|
🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅-->
|
||||||
|
|
||||||
|
# 📚 Docs or angular.io bug report
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
<!-- ✍️edit:--> A clear and concise description of the problem...
|
||||||
|
|
||||||
|
|
||||||
|
## 🔬 Minimal Reproduction
|
||||||
|
|
||||||
|
### What's the affected URL?**
|
||||||
|
<!-- ✍️edit:--> https://angular.io/...
|
||||||
|
|
||||||
|
### Reproduction Steps**
|
||||||
|
<!-- If applicable please list the steps to take to reproduce the issue -->
|
||||||
|
<!-- ✍️edit:-->
|
||||||
|
|
||||||
|
### Expected vs Actual Behavior**
|
||||||
|
<!-- If applicable please describe the difference between the expected and actual behavior after following the repro steps. -->
|
||||||
|
<!-- ✍️edit:-->
|
||||||
|
|
||||||
|
|
||||||
|
## 📷Screenshot
|
||||||
|
<!-- Often a screenshot can help to capture the issue better than a long description. -->
|
||||||
|
<!-- ✍️upload a screenshot:-->
|
||||||
|
|
||||||
|
|
||||||
|
## 🔥 Exception or Error
|
||||||
|
<pre><code>
|
||||||
|
<!-- If the issue is accompanied by an exception or an error, please share it below: -->
|
||||||
|
<!-- ✍️-->
|
||||||
|
|
||||||
|
</code></pre>
|
||||||
|
|
||||||
|
|
||||||
|
## 🌍 Your Environment
|
||||||
|
|
||||||
|
### Browser info
|
||||||
|
<!-- ✍️Is this a browser specific issue? If so, please specify the device, browser, and version. -->
|
||||||
|
|
||||||
|
### Anything else relevant?
|
||||||
|
<!-- ✍️Please provide additional info if necessary. -->
|
11
.github/ISSUE_TEMPLATE/4-security-issue-disclosure.md
vendored
Normal file
11
.github/ISSUE_TEMPLATE/4-security-issue-disclosure.md
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
name: ⚠️ Security issue disclosure
|
||||||
|
about: Report a security issue in Angular Framework, Material, or CLI
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
||||||
|
|
||||||
|
Please read https://angular.io/guide/security#report-issues on how to disclose security related issues.
|
||||||
|
|
||||||
|
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
16
.github/ISSUE_TEMPLATE/5-support-request.md
vendored
Normal file
16
.github/ISSUE_TEMPLATE/5-support-request.md
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
name: "❓Support request"
|
||||||
|
about: Questions and requests for support
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
||||||
|
|
||||||
|
Please do not file questions or support requests on the GitHub issues tracker.
|
||||||
|
|
||||||
|
You can get your questions answered using other communication channels. Please see:
|
||||||
|
https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question
|
||||||
|
|
||||||
|
Thank you!
|
||||||
|
|
||||||
|
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
13
.github/ISSUE_TEMPLATE/6-angular-cli.md
vendored
Normal file
13
.github/ISSUE_TEMPLATE/6-angular-cli.md
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
name: "\U0001F6E0️Angular CLI"
|
||||||
|
about: Issues and feature requests for Angular CLI
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
||||||
|
|
||||||
|
Please file any Angular CLI issues at: https://github.com/angular/angular-cli/issues/new
|
||||||
|
|
||||||
|
For the time being, we keep Angular CLI issues in a separate repository.
|
||||||
|
|
||||||
|
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
13
.github/ISSUE_TEMPLATE/7-angular-material.md
vendored
Normal file
13
.github/ISSUE_TEMPLATE/7-angular-material.md
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
name: "\U0001F48EAngular Material"
|
||||||
|
about: Issues and feature requests for Angular Material
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
||||||
|
|
||||||
|
Please file any Angular Material issues at: https://github.com/angular/material2/issues/new
|
||||||
|
|
||||||
|
For the time being, we keep Angular Material issues in a separate repository.
|
||||||
|
|
||||||
|
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
30
.github/PULL_REQUEST_TEMPLATE.md
vendored
30
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -10,17 +10,17 @@ Please check if your PR fulfills the following requirements:
|
|||||||
What kind of change does this PR introduce?
|
What kind of change does this PR introduce?
|
||||||
|
|
||||||
<!-- Please check the one that applies to this PR using "x". -->
|
<!-- Please check the one that applies to this PR using "x". -->
|
||||||
```
|
|
||||||
[ ] Bugfix
|
- [ ] Bugfix
|
||||||
[ ] Feature
|
- [ ] Feature
|
||||||
[ ] Code style update (formatting, local variables)
|
- [ ] Code style update (formatting, local variables)
|
||||||
[ ] Refactoring (no functional changes, no api changes)
|
- [ ] Refactoring (no functional changes, no api changes)
|
||||||
[ ] Build related changes
|
- [ ] Build related changes
|
||||||
[ ] CI related changes
|
- [ ] CI related changes
|
||||||
[ ] Documentation content changes
|
- [ ] Documentation content changes
|
||||||
[ ] angular.io application / infrastructure changes
|
- [ ] angular.io application / infrastructure changes
|
||||||
[ ] Other... Please describe:
|
- [ ] Other... Please describe:
|
||||||
```
|
|
||||||
|
|
||||||
## What is the current behavior?
|
## What is the current behavior?
|
||||||
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->
|
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->
|
||||||
@ -32,10 +32,10 @@ Issue Number: N/A
|
|||||||
|
|
||||||
|
|
||||||
## Does this PR introduce a breaking change?
|
## Does this PR introduce a breaking change?
|
||||||
```
|
|
||||||
[ ] Yes
|
- [ ] Yes
|
||||||
[ ] No
|
- [ ] No
|
||||||
```
|
|
||||||
|
|
||||||
<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->
|
<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->
|
||||||
|
|
||||||
|
28
.github/angular-robot.yml
vendored
28
.github/angular-robot.yml
vendored
@ -3,11 +3,8 @@
|
|||||||
#options for the size plugin
|
#options for the size plugin
|
||||||
size:
|
size:
|
||||||
disabled: false
|
disabled: false
|
||||||
maxSizeIncrease: 1000
|
maxSizeIncrease: 2000
|
||||||
circleCiStatusName: "ci/circleci: build-packages-dist"
|
circleCiStatusName: "ci/circleci: test"
|
||||||
status:
|
|
||||||
disabled: false
|
|
||||||
context: "ci/angular: size"
|
|
||||||
|
|
||||||
# options for the merge plugin
|
# options for the merge plugin
|
||||||
merge:
|
merge:
|
||||||
@ -42,6 +39,7 @@ merge:
|
|||||||
- "packages/**"
|
- "packages/**"
|
||||||
# list of patterns to ignore for the files changed by the PR
|
# list of patterns to ignore for the files changed by the PR
|
||||||
exclude:
|
exclude:
|
||||||
|
- "packages/bazel/*.bzl"
|
||||||
- "packages/language-service/**"
|
- "packages/language-service/**"
|
||||||
- "**/.gitignore"
|
- "**/.gitignore"
|
||||||
- "**/.gitkeep"
|
- "**/.gitkeep"
|
||||||
@ -127,3 +125,23 @@ triage:
|
|||||||
-
|
-
|
||||||
- "type: RFC / Discussion / question"
|
- "type: RFC / Discussion / question"
|
||||||
- "comp: *"
|
- "comp: *"
|
||||||
|
|
||||||
|
# options for the triage PR plugin
|
||||||
|
triagePR:
|
||||||
|
# set to true to disable
|
||||||
|
disabled: false
|
||||||
|
# number of the milestone to apply when the PR has not been triaged yet
|
||||||
|
needsTriageMilestone: 83,
|
||||||
|
# number of the milestone to apply when the PR is triaged
|
||||||
|
defaultMilestone: 82,
|
||||||
|
# arrays of labels that determine if a PR has been triaged by the caretaker
|
||||||
|
l1TriageLabels:
|
||||||
|
-
|
||||||
|
- "comp: *"
|
||||||
|
# arrays of labels that determine if a PR has been fully triaged
|
||||||
|
l2TriageLabels:
|
||||||
|
-
|
||||||
|
- "type: *"
|
||||||
|
- "effort*"
|
||||||
|
- "risk*"
|
||||||
|
- "comp: *"
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -14,7 +14,6 @@ pubspec.lock
|
|||||||
.settings/
|
.settings/
|
||||||
*.swo
|
*.swo
|
||||||
modules/.settings
|
modules/.settings
|
||||||
.bazelrc
|
|
||||||
.vscode
|
.vscode
|
||||||
modules/.vscode
|
modules/.vscode
|
||||||
|
|
||||||
|
@ -24,7 +24,6 @@
|
|||||||
# petebacondarwin - Pete Bacon Darwin
|
# petebacondarwin - Pete Bacon Darwin
|
||||||
# pkozlowski-opensource - Pawel Kozlowski
|
# pkozlowski-opensource - Pawel Kozlowski
|
||||||
# robwormald - Rob Wormald
|
# robwormald - Rob Wormald
|
||||||
# vicb - Victor Berchet
|
|
||||||
# vikerman - Vikram Subramanian
|
# vikerman - Vikram Subramanian
|
||||||
|
|
||||||
|
|
||||||
@ -88,10 +87,10 @@ groups:
|
|||||||
files:
|
files:
|
||||||
include:
|
include:
|
||||||
- "WORKSPACE"
|
- "WORKSPACE"
|
||||||
|
- ".bazel*"
|
||||||
- "*.bazel"
|
- "*.bazel"
|
||||||
- "*.bzl"
|
- "*.bzl"
|
||||||
- "packages/bazel/*"
|
- "packages/bazel/*"
|
||||||
- "tools/bazel.rc"
|
|
||||||
- "/docs/BAZEL.md"
|
- "/docs/BAZEL.md"
|
||||||
users:
|
users:
|
||||||
- alexeagle #primary
|
- alexeagle #primary
|
||||||
@ -109,7 +108,6 @@ groups:
|
|||||||
- "*.lock"
|
- "*.lock"
|
||||||
- "tools/*"
|
- "tools/*"
|
||||||
exclude:
|
exclude:
|
||||||
- "tools/bazel.rc"
|
|
||||||
- "tools/public_api_guard/*"
|
- "tools/public_api_guard/*"
|
||||||
- "aio/*"
|
- "aio/*"
|
||||||
users:
|
users:
|
||||||
@ -125,7 +123,6 @@ groups:
|
|||||||
users:
|
users:
|
||||||
- alexeagle
|
- alexeagle
|
||||||
- mhevery
|
- mhevery
|
||||||
- vicb
|
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
|
|
||||||
core:
|
core:
|
||||||
@ -197,7 +194,6 @@ groups:
|
|||||||
- mhevery #primary
|
- mhevery #primary
|
||||||
- jasonaden
|
- jasonaden
|
||||||
- kara
|
- kara
|
||||||
- vicb
|
|
||||||
- IgorMinar
|
- IgorMinar
|
||||||
- jenniferfell #docs only
|
- jenniferfell #docs only
|
||||||
|
|
||||||
@ -222,8 +218,7 @@ groups:
|
|||||||
- "aio/content/guide/i18n.md"
|
- "aio/content/guide/i18n.md"
|
||||||
- "aio/content/examples/i18n/*"
|
- "aio/content/examples/i18n/*"
|
||||||
users:
|
users:
|
||||||
- vicb #primary
|
- alxhub #primary
|
||||||
- alxhub
|
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
- mhevery #fallback
|
- mhevery #fallback
|
||||||
- jenniferfell #docs only
|
- jenniferfell #docs only
|
||||||
@ -235,7 +230,6 @@ groups:
|
|||||||
- "aio/content/guide/aot-compiler.md"
|
- "aio/content/guide/aot-compiler.md"
|
||||||
users:
|
users:
|
||||||
- alxhub #primary
|
- alxhub #primary
|
||||||
- vicb
|
|
||||||
- mhevery
|
- mhevery
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
- jenniferfell #docs only
|
- jenniferfell #docs only
|
||||||
@ -260,7 +254,6 @@ groups:
|
|||||||
users:
|
users:
|
||||||
- alexeagle
|
- alexeagle
|
||||||
- alxhub
|
- alxhub
|
||||||
- vicb
|
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
- mhevery #fallback
|
- mhevery #fallback
|
||||||
|
|
||||||
@ -273,7 +266,6 @@ groups:
|
|||||||
- "packages/common/http/*"
|
- "packages/common/http/*"
|
||||||
users:
|
users:
|
||||||
- pkozlowski-opensource #primary
|
- pkozlowski-opensource #primary
|
||||||
- vicb
|
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
- mhevery #fallback
|
- mhevery #fallback
|
||||||
|
|
||||||
@ -284,6 +276,9 @@ groups:
|
|||||||
- "aio/content/guide/forms.md"
|
- "aio/content/guide/forms.md"
|
||||||
- "aio/content/examples/forms/*"
|
- "aio/content/examples/forms/*"
|
||||||
- "aio/content/images/guide/forms/*"
|
- "aio/content/images/guide/forms/*"
|
||||||
|
- "aio/content/guide/forms-overview.md"
|
||||||
|
- "aio/content/examples/forms-overview/*"
|
||||||
|
- "aio/content/images/guide/forms-overview/*"
|
||||||
- "aio/content/guide/form-validation.md"
|
- "aio/content/guide/form-validation.md"
|
||||||
- "aio/content/examples/form-validation/*"
|
- "aio/content/examples/form-validation/*"
|
||||||
- "aio/content/images/guide/form-validation/*"
|
- "aio/content/images/guide/form-validation/*"
|
||||||
@ -322,7 +317,6 @@ groups:
|
|||||||
users:
|
users:
|
||||||
- kyliau #primary
|
- kyliau #primary
|
||||||
# needs secondary
|
# needs secondary
|
||||||
- vicb
|
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
- mhevery #fallback
|
- mhevery #fallback
|
||||||
- jenniferfell #docs only
|
- jenniferfell #docs only
|
||||||
@ -336,7 +330,6 @@ groups:
|
|||||||
- "aio/content/images/guide/router/*"
|
- "aio/content/images/guide/router/*"
|
||||||
users:
|
users:
|
||||||
- jasonaden #primary
|
- jasonaden #primary
|
||||||
- vicb
|
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
- mhevery #fallback
|
- mhevery #fallback
|
||||||
- jenniferfell #docs only
|
- jenniferfell #docs only
|
||||||
@ -379,10 +372,9 @@ groups:
|
|||||||
files:
|
files:
|
||||||
- "packages/platform-browser/*"
|
- "packages/platform-browser/*"
|
||||||
users:
|
users:
|
||||||
- vicb #primary
|
- mhevery #primary
|
||||||
# needs secondary
|
# needs secondary
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
- mhevery #fallback
|
|
||||||
|
|
||||||
platform-server:
|
platform-server:
|
||||||
conditions:
|
conditions:
|
||||||
@ -393,7 +385,6 @@ groups:
|
|||||||
users:
|
users:
|
||||||
- vikerman #primary
|
- vikerman #primary
|
||||||
- alxhub #secondary
|
- alxhub #secondary
|
||||||
- vicb
|
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
- mhevery #fallback
|
- mhevery #fallback
|
||||||
- jenniferfell #docs only
|
- jenniferfell #docs only
|
||||||
@ -403,10 +394,9 @@ groups:
|
|||||||
files:
|
files:
|
||||||
- "packages/platform-webworker/*"
|
- "packages/platform-webworker/*"
|
||||||
users:
|
users:
|
||||||
- vicb #primary
|
- mhevery #primary
|
||||||
# needs secondary
|
# needs secondary
|
||||||
- IgorMinar #fallback
|
- IgorMinar #fallback
|
||||||
- mhevery #fallback
|
|
||||||
|
|
||||||
service-worker:
|
service-worker:
|
||||||
conditions:
|
conditions:
|
||||||
|
22
.travis.yml
22
.travis.yml
@ -2,7 +2,7 @@ language: node_js
|
|||||||
sudo: false
|
sudo: false
|
||||||
dist: trusty
|
dist: trusty
|
||||||
node_js:
|
node_js:
|
||||||
- '8.9.1'
|
- '10.9.0'
|
||||||
|
|
||||||
addons:
|
addons:
|
||||||
# firefox: "38.0"
|
# firefox: "38.0"
|
||||||
@ -13,11 +13,7 @@ addons:
|
|||||||
packages:
|
packages:
|
||||||
# needed to install g++ that is used by npms's native modules
|
# needed to install g++ that is used by npms's native modules
|
||||||
- g++-4.8
|
- g++-4.8
|
||||||
# https://docs.travis-ci.com/user/jwt
|
|
||||||
jwt:
|
|
||||||
# SAUCE_ACCESS_KEY<=secret for NGBUILDS_IO_KEY to work around travis-ci/travis-ci#7223, unencrypted value in valentine as NGBUILDS_IO_KEY>
|
|
||||||
# we alias NGBUILDS_IO_KEY to $SAUCE_ACCESS_KEY in env.sh and set the SAUCE_ACCESS_KEY there
|
|
||||||
- secure: "L7nrZwkAtFtYrP2DykPXgZvEKjkv0J/TwQ/r2QGxFTaBq4VZn+2Dw0YS7uCxoMqYzDwH0aAOqxoutibVpk8Z/16nE3tNmU5RzltMd6Xmt3qU2f/JDQLMo6PSlBodnjOUsDHJgmtrcbjhqrx/znA237BkNUu6UZRT7mxhXIZpn0U="
|
|
||||||
branches:
|
branches:
|
||||||
except:
|
except:
|
||||||
- g3
|
- g3
|
||||||
@ -34,14 +30,6 @@ env:
|
|||||||
# GITHUB_TOKEN_ANGULAR=<github token, a personal access token of the angular-builds account, account access in valentine>
|
# GITHUB_TOKEN_ANGULAR=<github token, a personal access token of the angular-builds account, account access in valentine>
|
||||||
# This is needed for the e2e Travis matrix task to publish packages to github for continuous packages delivery.
|
# This is needed for the e2e Travis matrix task to publish packages to github for continuous packages delivery.
|
||||||
- secure: "aCdHveZuY8AT4Jr1JoJB4LxZsnGWRe/KseZh1YXYe5UtufFCtTVHvUcLn0j2aLBF0KpdyS+hWf0i4np9jthKu2xPKriefoPgCMpisYeC0MFkwbmv+XlgkUbgkgVZMGiVyX7DCYXVahxIoOUjVMEDCbNiHTIrfEuyq24U3ok2tHc="
|
- secure: "aCdHveZuY8AT4Jr1JoJB4LxZsnGWRe/KseZh1YXYe5UtufFCtTVHvUcLn0j2aLBF0KpdyS+hWf0i4np9jthKu2xPKriefoPgCMpisYeC0MFkwbmv+XlgkUbgkgVZMGiVyX7DCYXVahxIoOUjVMEDCbNiHTIrfEuyq24U3ok2tHc="
|
||||||
# FIREBASE_TOKEN
|
|
||||||
# This is needed for publishing builds to the "aio-staging" and "angular-io" firebase projects.
|
|
||||||
# This token was generated using the aio-deploy@angular.io account using `firebase login:ci` and password from valentine
|
|
||||||
- secure: "L5CyQmpwWtoR4Qi4xlWQh/cL1M6ZeJL4W4QAr4HdKFMgYt9h+Whqkymyh2NxwmCbPvWa7yUd+OiLQUDCY7L2VIg16hTwoe2CgYDyQA0BEwLzxtRrJXl93TfwMlrUx5JSIzAccD6D4sjtz8kSFMomK2Nls33xOXOukwyhVMjd0Cg="
|
|
||||||
# ANGULAR_PAYLOAD_FIREBASE_TOKEN
|
|
||||||
# This is for payload size data to "angular-payload-size" firebase project
|
|
||||||
# This token was generated using the payload@angular.io account using `firebase login:ci` and password from valentine
|
|
||||||
- secure: "SxotP/ymNy6uWAVbfwM9BlwETPEBpkRvU/F7fCtQDDic99WfQHzzUSQqHTk8eKk3GrGAOSL09vT0WfStQYEIGEoS5UHWNgOnelxhw+d5EnaoB8vQ0dKQBTK092hQg4feFprr+B/tCasyMV6mVwpUzZMbIJNn/Rx7H5g1bp+Gkfg="
|
|
||||||
matrix:
|
matrix:
|
||||||
# Order: a slower build first, so that we don't occupy an idle travis worker waiting for others to complete.
|
# Order: a slower build first, so that we don't occupy an idle travis worker waiting for others to complete.
|
||||||
- CI_MODE=e2e
|
- CI_MODE=e2e
|
||||||
@ -51,10 +39,6 @@ env:
|
|||||||
# - CI_MODE=browserstack_required
|
# - CI_MODE=browserstack_required
|
||||||
- CI_MODE=saucelabs_optional
|
- CI_MODE=saucelabs_optional
|
||||||
- CI_MODE=browserstack_optional
|
- CI_MODE=browserstack_optional
|
||||||
- CI_MODE=aio_tools_test
|
|
||||||
- CI_MODE=aio
|
|
||||||
- CI_MODE=aio_e2e AIO_SHARD=0
|
|
||||||
- CI_MODE=aio_e2e AIO_SHARD=1
|
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
fast_finish: true
|
fast_finish: true
|
||||||
@ -72,8 +56,6 @@ install:
|
|||||||
script:
|
script:
|
||||||
- ./scripts/ci/build.sh
|
- ./scripts/ci/build.sh
|
||||||
- ./scripts/ci/test.sh
|
- ./scripts/ci/test.sh
|
||||||
# deploy is part of 'script' and not 'after_success' so that we fail the build if the deployment fails
|
|
||||||
- ./scripts/ci/deploy.sh
|
|
||||||
- ./scripts/ci/angular.sh
|
- ./scripts/ci/angular.sh
|
||||||
# all the scripts under this line will not quickly abort in case ${TRAVIS_TEST_RESULT} is 1 (job failure)
|
# all the scripts under this line will not quickly abort in case ${TRAVIS_TEST_RESULT} is 1 (job failure)
|
||||||
- ./scripts/ci/cleanup.sh
|
- ./scripts/ci/cleanup.sh
|
||||||
|
50
BUILD.bazel
50
BUILD.bazel
@ -8,26 +8,14 @@ exports_files([
|
|||||||
"protractor-perf.conf.js",
|
"protractor-perf.conf.js",
|
||||||
])
|
])
|
||||||
|
|
||||||
# Developers should always run `bazel run :install`
|
|
||||||
# This ensures that package.json in subdirectories get installed as well.
|
|
||||||
alias(
|
|
||||||
name = "install",
|
|
||||||
actual = "@nodejs//:yarn",
|
|
||||||
)
|
|
||||||
|
|
||||||
alias(
|
|
||||||
name = "node_modules",
|
|
||||||
actual = "@angular_deps//:node_modules",
|
|
||||||
)
|
|
||||||
|
|
||||||
filegroup(
|
filegroup(
|
||||||
name = "web_test_bootstrap_scripts",
|
name = "web_test_bootstrap_scripts",
|
||||||
# do not sort
|
# do not sort
|
||||||
srcs = [
|
srcs = [
|
||||||
"@angular_deps//:node_modules/reflect-metadata/Reflect.js",
|
"@ngdeps//node_modules/reflect-metadata:Reflect.js",
|
||||||
"@angular_deps//:node_modules/zone.js/dist/zone.js",
|
"@ngdeps//node_modules/zone.js:dist/zone.js",
|
||||||
"@angular_deps//:node_modules/zone.js/dist/zone-testing.js",
|
"@ngdeps//node_modules/zone.js:dist/zone-testing.js",
|
||||||
"@angular_deps//:node_modules/zone.js/dist/task-tracking.js",
|
"@ngdeps//node_modules/zone.js:dist/task-tracking.js",
|
||||||
"//:test-events.js",
|
"//:test-events.js",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -35,11 +23,29 @@ filegroup(
|
|||||||
filegroup(
|
filegroup(
|
||||||
name = "angularjs_scripts",
|
name = "angularjs_scripts",
|
||||||
srcs = [
|
srcs = [
|
||||||
"@angular_deps//:node_modules/angular-1.5/angular.js",
|
"@ngdeps//node_modules/angular:angular.js",
|
||||||
"@angular_deps//:node_modules/angular-1.6/angular.js",
|
"@ngdeps//node_modules/angular-1.5:angular.js",
|
||||||
"@angular_deps//:node_modules/angular-mocks-1.5/angular-mocks.js",
|
"@ngdeps//node_modules/angular-1.6:angular.js",
|
||||||
"@angular_deps//:node_modules/angular-mocks-1.6/angular-mocks.js",
|
"@ngdeps//node_modules/angular-mocks:angular-mocks.js",
|
||||||
"@angular_deps//:node_modules/angular-mocks/angular-mocks.js",
|
"@ngdeps//node_modules/angular-mocks-1.5:angular-mocks.js",
|
||||||
"@angular_deps//:node_modules/angular/angular.js",
|
"@ngdeps//node_modules/angular-mocks-1.6:angular-mocks.js",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
load("@build_bazel_rules_nodejs//:defs.bzl", "nodejs_binary")
|
||||||
|
|
||||||
|
# A nodejs_binary for @angular/bazel/ngc-wrapped to use by default in
|
||||||
|
# ng_module that depends on @npm//@angular/bazel instead of the
|
||||||
|
# output of the //packages/bazel/src/ngc-wrapped ts_library rule. This
|
||||||
|
# default is for downstream users that depend on the @angular/bazel npm
|
||||||
|
# package. The generated @npm//@angular/bazel/ngc-wrapped target
|
||||||
|
# does not work because it does not have the node `--expose-gc` flag
|
||||||
|
# set which is required to support the call to `global.gc()`.
|
||||||
|
nodejs_binary(
|
||||||
|
name = "@angular/bazel/ngc-wrapped",
|
||||||
|
configuration_env_vars = ["compile"],
|
||||||
|
data = ["@npm//@angular/bazel"],
|
||||||
|
entry_point = "@angular/bazel/src/ngc-wrapped/index.js",
|
||||||
|
install_source_map_support = False,
|
||||||
|
templated_args = ["--node_options=--expose-gc"],
|
||||||
|
)
|
||||||
|
156
CHANGELOG.md
156
CHANGELOG.md
@ -1,3 +1,156 @@
|
|||||||
|
<a name="7.0.3"></a>
|
||||||
|
## [7.0.3](https://github.com/angular/angular/compare/7.0.2...7.0.3) (2018-11-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **bazel:** unknown replay compiler error in windows ([#26711](https://github.com/angular/angular/issues/26711)) ([4d532df](https://github.com/angular/angular/commit/4d532df))
|
||||||
|
* **router:** remove type bludgeoning of context and outlet when running CanDeactivate ([#26496](https://github.com/angular/angular/issues/26496)) ([dc05385](https://github.com/angular/angular/commit/dc05385)), closes [#18253](https://github.com/angular/angular/issues/18253)
|
||||||
|
* **upgrade:** make typings compatible with older AngularJS typings ([#26880](https://github.com/angular/angular/issues/26880)) ([315d95c](https://github.com/angular/angular/commit/315d95c)), closes [#26420](https://github.com/angular/angular/issues/26420)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<a name="7.0.2"></a>
|
||||||
|
## [7.0.2](https://github.com/angular/angular/compare/7.0.1...7.0.2) (2018-10-31)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **compiler:** generate relative paths only in summary file errors ([#26759](https://github.com/angular/angular/issues/26759)) ([c01f340](https://github.com/angular/angular/commit/c01f340))
|
||||||
|
* **core:** Remove static dependency from [@angular](https://github.com/angular)/core to [@angular](https://github.com/angular)/compiler ([#26734](https://github.com/angular/angular/issues/26734)) ([#26879](https://github.com/angular/angular/issues/26879)) ([257ac83](https://github.com/angular/angular/commit/257ac83))
|
||||||
|
* **core:** support computed base class in metadata inheritance ([#24014](https://github.com/angular/angular/issues/24014)) ([b3c6409](https://github.com/angular/angular/commit/b3c6409))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<a name="7.0.1"></a>
|
||||||
|
## [7.0.1](https://github.com/angular/angular/compare/7.0.0...7.0.1) (2018-10-24)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<a name="7.0.0"></a>
|
||||||
|
# [7.0.0](https://github.com/angular/angular/compare/7.0.0-rc.1...7.0.0) (2018-10-18)
|
||||||
|
|
||||||
|
|
||||||
|
### Release Highlights & Update instructions
|
||||||
|
|
||||||
|
To learn about the release highlights and our new CLI-powered update workflow for your projects please check out the [v7 release announcement](https://blog.angular.io/version-7-of-angular-cli-prompts-virtual-scroll-drag-and-drop-and-more-c594e22e7b8c).
|
||||||
|
|
||||||
|
|
||||||
|
### Dependency updates
|
||||||
|
|
||||||
|
* @angular/core now depends on
|
||||||
|
* TypeScript 3.1
|
||||||
|
* RxJS 6.3
|
||||||
|
* @angular/platform-server now depends on Domino 2.1
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **core:** add DoBootstrap interface. ([#24558](https://github.com/angular/angular/issues/24558)) ([732026c](https://github.com/angular/angular/commit/732026c)), closes [#24557](https://github.com/angular/angular/issues/24557)
|
||||||
|
* **compiler:** add "original" placeholder value on extracted XMB ([#25079](https://github.com/angular/angular/issues/25079)) ([e99d860](https://github.com/angular/angular/commit/e99d860))
|
||||||
|
* **compiler-cli:** add support to extend `angularCompilerOptions` ([#22717](https://github.com/angular/angular/issues/22717)) ([d7e5bbf](https://github.com/angular/angular/commit/d7e5bbf)), closes [#22684](https://github.com/angular/angular/issues/22684)
|
||||||
|
* **bazel:** add additional parameters to `ts_api_guardian_test` def ([#25694](https://github.com/angular/angular/issues/25694)) ([2a21ca0](https://github.com/angular/angular/commit/2a21ca0))
|
||||||
|
* **elements:** enable Shadow DOM v1 and slots ([#24861](https://github.com/angular/angular/issues/24861)) ([c9844a2](https://github.com/angular/angular/commit/c9844a2))
|
||||||
|
* **platform-server:** update domino to v2.1.0 ([#25564](https://github.com/angular/angular/issues/25564)) ([3fb0da2](https://github.com/angular/angular/commit/3fb0da2))
|
||||||
|
* **router:** warn if navigation triggered outside Angular zone ([#24959](https://github.com/angular/angular/issues/24959)) ([010e35d](https://github.com/angular/angular/commit/010e35d)), closes [#15770](https://github.com/angular/angular/issues/15770) [#15946](https://github.com/angular/angular/issues/15946) [#24728](https://github.com/angular/angular/issues/24728)
|
||||||
|
* **router:** add UrlSegment[] to CanLoad interface ([#13127](https://github.com/angular/angular/issues/13127)) ([07d8d39](https://github.com/angular/angular/commit/07d8d39)), closes [#12411](https://github.com/angular/angular/issues/12411)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add mappings for ngfactory & ngsummary files to their module names in aot summary resolver ([#25335](https://github.com/angular/angular/issues/25335)) ([02e201a](https://github.com/angular/angular/commit/02e201a))
|
||||||
|
* **bazel:** Cache fileNameToModuleName lookups ([#25731](https://github.com/angular/angular/issues/25731)) ([f394ba0](https://github.com/angular/angular/commit/f394ba0))
|
||||||
|
* **bazel:** allow compile_strategy to be (privately) imported ([#25080](https://github.com/angular/angular/issues/25080)) ([0d1d589](https://github.com/angular/angular/commit/0d1d589))
|
||||||
|
* **bazel:** correct type concatenated to devmode_js ([#25467](https://github.com/angular/angular/issues/25467)) ([fb2c524](https://github.com/angular/angular/commit/fb2c524))
|
||||||
|
* **bazel:** move bazel managed runtime deps for downstream usage ([#25690](https://github.com/angular/angular/issues/25690)) ([6ed7993](https://github.com/angular/angular/commit/6ed7993))
|
||||||
|
* **bazel:** only lookup amd module-name tags in .d.ts files ([#25710](https://github.com/angular/angular/issues/25710)) ([42072c4](https://github.com/angular/angular/commit/42072c4))
|
||||||
|
* **bazel:** protractor rule should include *.e2e-spec.js ([#25701](https://github.com/angular/angular/issues/25701)) ([3809e0f](https://github.com/angular/angular/commit/3809e0f))
|
||||||
|
* **bazel:** specify the package and lock files using the workspace ([#25694](https://github.com/angular/angular/issues/25694)) ([ddc1335](https://github.com/angular/angular/commit/ddc1335))
|
||||||
|
* **benchpress:** Use performance.mark() instead of console.time() ([#24114](https://github.com/angular/angular/issues/24114)) ([06d0400](https://github.com/angular/angular/commit/06d0400))
|
||||||
|
* **common:** register locale data for all equivalent closure locales ([#25867](https://github.com/angular/angular/issues/25867)) ([d83f9d4](https://github.com/angular/angular/commit/d83f9d4))
|
||||||
|
* **compiler-cli:** correct realPath to realpath. ([#25023](https://github.com/angular/angular/issues/25023)) ([01e6dab](https://github.com/angular/angular/commit/01e6dab))
|
||||||
|
* **compiler-cli:** use the oldProgram option in watch mode ([#21364](https://github.com/angular/angular/issues/21364)) ([c6e5b97](https://github.com/angular/angular/commit/c6e5b97)), closes [#21361](https://github.com/angular/angular/issues/21361)
|
||||||
|
* **compiler:** Fix look up of entryComponents in AOT Summaries ([#24892](https://github.com/angular/angular/issues/24892)) ([00d3666](https://github.com/angular/angular/commit/00d3666))
|
||||||
|
* **compiler:** add hostVars and support pure functions in host bindings ([#25626](https://github.com/angular/angular/issues/25626)) ([b424b31](https://github.com/angular/angular/commit/b424b31))
|
||||||
|
* **compiler:** update compiler to flatten nested template fns ([#24943](https://github.com/angular/angular/issues/24943)) ([fe14f18](https://github.com/angular/angular/commit/fe14f18))
|
||||||
|
* **compiler:** update compiler to generate new slot allocations ([#25607](https://github.com/angular/angular/issues/25607)) ([27e2039](https://github.com/angular/angular/commit/27e2039))
|
||||||
|
* **core:** In Testability.whenStable update callback, pass more complete ([#25010](https://github.com/angular/angular/issues/25010)) ([16c03c0](https://github.com/angular/angular/commit/16c03c0))
|
||||||
|
* **core:** add missing `peerDependency ` to `[@angular](https://github.com/angular)/compiler` ([#26033](https://github.com/angular/angular/issues/26033)) ([549de1e](https://github.com/angular/angular/commit/549de1e)), closes [/github.com/angular/angular/commit/919f42fea1df4b9e38b7d688aef5f2de668e9d3e#diff-58563046c4439699f2e6a89187099a54](https://github.com//github.com/angular/angular/commit/919f42fea1df4b9e38b7d688aef5f2de668e9d3e/issues/diff-58563046c4439699f2e6a89187099a54)
|
||||||
|
* **core:** allow null value for renderer setElement(…) ([#17065](https://github.com/angular/angular/issues/17065)) ([ff15043](https://github.com/angular/angular/commit/ff15043)), closes [#13686](https://github.com/angular/angular/issues/13686)
|
||||||
|
* **core:** do not clear element content when using shadow dom ([#24861](https://github.com/angular/angular/issues/24861)) ([6e828bb](https://github.com/angular/angular/commit/6e828bb))
|
||||||
|
* **core:** size regression with closure compiler ([#25531](https://github.com/angular/angular/issues/25531)) ([1f59f2f](https://github.com/angular/angular/commit/1f59f2f))
|
||||||
|
* **core:** throw error message when @Output not initialized ([#19116](https://github.com/angular/angular/issues/19116)) ([adf510f](https://github.com/angular/angular/commit/adf510f)), closes [#3664](https://github.com/angular/angular/issues/3664)
|
||||||
|
* **elements:** add compiler dependency ([#24861](https://github.com/angular/angular/issues/24861)) ([6143da6](https://github.com/angular/angular/commit/6143da6))
|
||||||
|
* **elements:** add compiler to integration ([#24861](https://github.com/angular/angular/issues/24861)) ([a080ffc](https://github.com/angular/angular/commit/a080ffc))
|
||||||
|
* **elements:** strict null checks ([#24861](https://github.com/angular/angular/issues/24861)) ([a8210d0](https://github.com/angular/angular/commit/a8210d0))
|
||||||
|
* **router:** fix regression where navigateByUrl promise didn't resolve on CanLoad failure ([#26455](https://github.com/angular/angular/issues/26455)) ([1c9b065](https://github.com/angular/angular/commit/1c9b065)), closes [#26284](https://github.com/angular/angular/issues/26284)
|
||||||
|
* **router:** mount correct component if router outlet was not instantiated and if using a route reuse strategy ([#25313](https://github.com/angular/angular/issues/25313)) ([#25314](https://github.com/angular/angular/issues/25314)) ([8dc2b11](https://github.com/angular/angular/commit/8dc2b11))
|
||||||
|
* **router:** take base uri into account in `setUpLocationSync()` ([#20244](https://github.com/angular/angular/issues/20244)) ([ba1e25f](https://github.com/angular/angular/commit/ba1e25f)), closes [#20061](https://github.com/angular/angular/issues/20061)
|
||||||
|
* **service-worker:** clean up caches from old SW versions ([#26319](https://github.com/angular/angular/issues/26319)) ([00b5c7b](https://github.com/angular/angular/commit/00b5c7b))
|
||||||
|
* **service-worker:** do not blow up when caches are unwritable ([#26042](https://github.com/angular/angular/issues/26042)) ([2bd767c](https://github.com/angular/angular/commit/2bd767c))
|
||||||
|
* **upgrade:** properly destroy upgraded component elements and descendants ([#26209](https://github.com/angular/angular/issues/26209)) ([071934e](https://github.com/angular/angular/commit/071934e)), closes [#26208](https://github.com/angular/angular/issues/26208)
|
||||||
|
* **upgrade:** trigger `$destroy` event on upgraded component element ([#25357](https://github.com/angular/angular/issues/25357)) ([2a672a9](https://github.com/angular/angular/commit/2a672a9)), closes [#25334](https://github.com/angular/angular/issues/25334)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<a name="6.1.10"></a>
|
||||||
|
## [6.1.10](https://github.com/angular/angular/compare/6.1.9...6.1.10) (2018-10-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **platform-browser:** fix [#22155](https://github.com/angular/angular/issues/22155), destroy hammer manager when `HammerInstance.off()` is run ([#22156](https://github.com/angular/angular/issues/22156)) ([3b4d9dc](https://github.com/angular/angular/commit/3b4d9dc))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<a name="6.1.9"></a>
|
||||||
|
## [6.1.9](https://github.com/angular/angular/compare/6.1.8...6.1.9) (2018-09-26)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<a name="6.1.7"></a>
|
||||||
|
## [6.1.7](https://github.com/angular/angular/compare/6.1.6...6.1.7) (2018-09-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **bazel:** protractor rule should include *.e2e-spec.js ([#25701](https://github.com/angular/angular/issues/25701)) ([ed6b68b](https://github.com/angular/angular/commit/ed6b68b))
|
||||||
|
* **core:** size regression with closure compiler ([#25531](https://github.com/angular/angular/issues/25531)) ([ebcf762](https://github.com/angular/angular/commit/ebcf762))
|
||||||
|
* **docs-infra:** show "suggest edits" only for /guide and /tutorial dirs ([#24378](https://github.com/angular/angular/issues/24378)) ([66b7870](https://github.com/angular/angular/commit/66b7870))
|
||||||
|
* **upgrade:** trigger `$destroy` event on upgraded component element ([#25357](https://github.com/angular/angular/issues/25357)) ([82e0676](https://github.com/angular/angular/commit/82e0676)), closes [#25334](https://github.com/angular/angular/issues/25334)
|
||||||
|
* **router:** warn if navigation triggered outside Angular zone ([#24959](https://github.com/angular/angular/issues/24959)) ([23a96dc](https://github.com/angular/angular/commit/23a96dc)), closes [#15770](https://github.com/angular/angular/issues/15770) [#15946](https://github.com/angular/angular/issues/15946) [#24728](https://github.com/angular/angular/issues/24728)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<a name="6.1.6"></a>
|
||||||
|
## [6.1.6](https://github.com/angular/angular/compare/6.1.5...6.1.6) (2018-08-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **bazel:** Cache fileNameToModuleName lookups ([#25731](https://github.com/angular/angular/issues/25731)) ([3e690e0](https://github.com/angular/angular/commit/3e690e0))
|
||||||
|
* **bazel:** only lookup amd module-name tags in .d.ts files ([#25710](https://github.com/angular/angular/issues/25710)) ([7aff364](https://github.com/angular/angular/commit/7aff364))
|
||||||
|
|
||||||
|
|
||||||
|
Note: the 6.1.5 release on npm accidentally glitched-out midway, so we cut 6.1.6 instead. sorry! :-)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<a name="6.1.4"></a>
|
||||||
|
## [6.1.4](https://github.com/angular/angular/compare/6.1.3...6.1.4) (2018-08-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **router:** default scroll position restoration to disabled ([#25586](https://github.com/angular/angular/issues/25586)) ([7e61645](https://github.com/angular/angular/commit/7e61645)), closes [#25145](https://github.com/angular/angular/issues/25145)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a name="6.1.3"></a>
|
<a name="6.1.3"></a>
|
||||||
## [6.1.3](https://github.com/angular/angular/compare/6.1.2...6.1.3) (2018-08-15)
|
## [6.1.3](https://github.com/angular/angular/compare/6.1.2...6.1.3) (2018-08-15)
|
||||||
|
|
||||||
@ -22,9 +175,6 @@
|
|||||||
<a name="6.1.1"></a>
|
<a name="6.1.1"></a>
|
||||||
## [6.1.1](https://github.com/angular/angular/compare/6.1.0...6.1.1) (2018-08-02)
|
## [6.1.1](https://github.com/angular/angular/compare/6.1.0...6.1.1) (2018-08-02)
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* **compiler-cli:** correct tsickle dependency version to fix typescript 2.9 compatibility ([fec29fa](https://github.com/angular/angular/commit/317c7087c56b72aa74cd6d6a8f719e6e7fec29fa))
|
* **compiler-cli:** correct tsickle dependency version to fix typescript 2.9 compatibility ([fec29fa](https://github.com/angular/angular/commit/317c7087c56b72aa74cd6d6a8f719e6e7fec29fa))
|
||||||
|
|
||||||
|
|
||||||
|
@ -71,6 +71,8 @@ Before you submit your Pull Request (PR) consider the following guidelines:
|
|||||||
|
|
||||||
1. Search [GitHub](https://github.com/angular/angular/pulls) for an open or closed PR
|
1. Search [GitHub](https://github.com/angular/angular/pulls) for an open or closed PR
|
||||||
that relates to your submission. You don't want to duplicate effort.
|
that relates to your submission. You don't want to duplicate effort.
|
||||||
|
1. Be sure that an issue describes the problem you're fixing, or documents the design for the feature you'd like to add.
|
||||||
|
Discussing the design up front helps to ensure that we're ready to accept your work.
|
||||||
1. Please sign our [Contributor License Agreement (CLA)](#cla) before sending PRs.
|
1. Please sign our [Contributor License Agreement (CLA)](#cla) before sending PRs.
|
||||||
We cannot accept code without this. Make sure you sign with the primary email address of the Git identity that has been granted access to the Angular repository.
|
We cannot accept code without this. Make sure you sign with the primary email address of the Git identity that has been granted access to the Angular repository.
|
||||||
1. Fork the angular/angular repo.
|
1. Fork the angular/angular repo.
|
||||||
|
@ -13,12 +13,10 @@ Angular is a development platform for building mobile and desktop web applicatio
|
|||||||
|
|
||||||
[Get started in 5 minutes][quickstart].
|
[Get started in 5 minutes][quickstart].
|
||||||
|
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
[Learn about the latest improvements][changelog].
|
[Learn about the latest improvements][changelog].
|
||||||
|
|
||||||
|
|
||||||
## Want to help?
|
## Want to help?
|
||||||
|
|
||||||
Want to file a bug, contribute some code, or improve documentation? Excellent! Read up on our
|
Want to file a bug, contribute some code, or improve documentation? Excellent! Read up on our
|
||||||
|
139
WORKSPACE
139
WORKSPACE
@ -1,90 +1,34 @@
|
|||||||
workspace(name = "angular")
|
workspace(name = "angular")
|
||||||
|
|
||||||
#
|
load(
|
||||||
# Download Bazel toolchain dependencies as needed by build actions
|
"//packages/bazel:package.bzl",
|
||||||
#
|
"rules_angular_dependencies",
|
||||||
|
"rules_angular_dev_dependencies",
|
||||||
http_archive(
|
|
||||||
name = "build_bazel_rules_nodejs",
|
|
||||||
urls = ["https://github.com/bazelbuild/rules_nodejs/archive/0.11.4.zip"],
|
|
||||||
strip_prefix = "rules_nodejs-0.11.4",
|
|
||||||
sha256 = "c31c4ead696944a50fad2b3ee9dfbbeffe31a8dcca0b21b9bf5b3e6c6b069801",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
http_archive(
|
# Uncomment for local bazel rules development
|
||||||
name = "bazel_skylib",
|
#local_repository(
|
||||||
urls = ["https://github.com/bazelbuild/bazel-skylib/archive/0.3.1.zip"],
|
# name = "build_bazel_rules_nodejs",
|
||||||
strip_prefix = "bazel-skylib-0.3.1",
|
# path = "../rules_nodejs",
|
||||||
sha256 = "95518adafc9a2b656667bbf517a952e54ce7f350779d0dd95133db4eb5c27fb1",
|
#)
|
||||||
)
|
#local_repository(
|
||||||
|
# name = "build_bazel_rules_typescript",
|
||||||
|
# path = "../rules_typescript",
|
||||||
|
#)
|
||||||
|
|
||||||
http_archive(
|
# Angular Bazel users will call this function
|
||||||
name = "io_bazel_rules_webtesting",
|
rules_angular_dependencies()
|
||||||
url = "https://github.com/bazelbuild/rules_webtesting/archive/0.2.1.zip",
|
# These are the dependencies only for us
|
||||||
strip_prefix = "rules_webtesting-0.2.1",
|
rules_angular_dev_dependencies()
|
||||||
sha256 = "7d490aadff9b5262e5251fa69427ab2ffd1548422467cb9f9e1d110e2c36f0fa",
|
|
||||||
)
|
|
||||||
|
|
||||||
http_archive(
|
|
||||||
name = "build_bazel_rules_typescript",
|
|
||||||
url = "https://github.com/bazelbuild/rules_typescript/archive/0.16.0.zip",
|
|
||||||
strip_prefix = "rules_typescript-0.16.0",
|
|
||||||
sha256 = "e65c5639a42e2f6d3f9d2bda62487d6b42734830dda45be1620c3e2b1115070c",
|
|
||||||
)
|
|
||||||
|
|
||||||
http_archive(
|
|
||||||
name = "io_bazel_rules_go",
|
|
||||||
url = "https://github.com/bazelbuild/rules_go/releases/download/0.10.3/rules_go-0.10.3.tar.gz",
|
|
||||||
sha256 = "feba3278c13cde8d67e341a837f69a029f698d7a27ddbb2a202be7a10b22142a",
|
|
||||||
)
|
|
||||||
|
|
||||||
# This commit matches the version of buildifier in angular/ngcontainer
|
|
||||||
# If you change this, also check if it matches the version in the angular/ngcontainer
|
|
||||||
# version in /.circleci/config.yml
|
|
||||||
BAZEL_BUILDTOOLS_VERSION = "82b21607e00913b16fe1c51bec80232d9d6de31c"
|
|
||||||
|
|
||||||
http_archive(
|
|
||||||
name = "com_github_bazelbuild_buildtools",
|
|
||||||
url = "https://github.com/bazelbuild/buildtools/archive/%s.zip" % BAZEL_BUILDTOOLS_VERSION,
|
|
||||||
strip_prefix = "buildtools-%s" % BAZEL_BUILDTOOLS_VERSION,
|
|
||||||
sha256 = "edb24c2f9c55b10a820ec74db0564415c0cf553fa55e9fc709a6332fb6685eff",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Fetching the Bazel source code allows us to compile the Skylark linter
|
|
||||||
http_archive(
|
|
||||||
name = "io_bazel",
|
|
||||||
url = "https://github.com/bazelbuild/bazel/archive/968f87900dce45a7af749a965b72dbac51b176b3.zip",
|
|
||||||
strip_prefix = "bazel-968f87900dce45a7af749a965b72dbac51b176b3",
|
|
||||||
sha256 = "e373d2ae24955c1254c495c9c421c009d88966565c35e4e8444c082cb1f0f48f",
|
|
||||||
)
|
|
||||||
|
|
||||||
# We have a source dependency on the Devkit repository, because it's built with
|
|
||||||
# Bazel.
|
|
||||||
# This allows us to edit sources and have the effect appear immediately without
|
|
||||||
# re-packaging or "npm link"ing.
|
|
||||||
# Even better, things like aspects will visit the entire graph including
|
|
||||||
# ts_library rules in the devkit repository.
|
|
||||||
http_archive(
|
|
||||||
name = "angular_cli",
|
|
||||||
url = "https://github.com/angular/angular-cli/archive/v6.1.0-rc.0.zip",
|
|
||||||
strip_prefix = "angular-cli-6.1.0-rc.0",
|
|
||||||
sha256 = "8cf320ea58c321e103f39087376feea502f20eaf79c61a4fdb05c7286c8684fd",
|
|
||||||
)
|
|
||||||
|
|
||||||
http_archive(
|
|
||||||
name = "org_brotli",
|
|
||||||
url = "https://github.com/google/brotli/archive/f9b8c02673c576a3e807edbf3a9328e9e7af6d7c.zip",
|
|
||||||
strip_prefix = "brotli-f9b8c02673c576a3e807edbf3a9328e9e7af6d7c",
|
|
||||||
sha256 = "8a517806d2b7c8505ba5c53934e7d7c70d341b68ffd268e9044d35b564a48828",
|
|
||||||
)
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Point Bazel to WORKSPACEs that live in subdirectories
|
# Point Bazel to WORKSPACEs that live in subdirectories
|
||||||
#
|
#
|
||||||
|
http_archive(
|
||||||
local_repository(
|
|
||||||
name = "rxjs",
|
name = "rxjs",
|
||||||
path = "node_modules/rxjs/src",
|
url = "https://registry.yarnpkg.com/rxjs/-/rxjs-6.3.3.tgz",
|
||||||
|
strip_prefix = "package/src",
|
||||||
|
sha256 = "72b0b4e517f43358f554c125e40e39f67688cd2738a8998b4a266981ed32f403",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Point to the integration test workspace just so that Bazel doesn't descend into it
|
# Point to the integration test workspace just so that Bazel doesn't descend into it
|
||||||
@ -97,23 +41,37 @@ local_repository(
|
|||||||
#
|
#
|
||||||
# Load and install our dependencies downloaded above.
|
# Load and install our dependencies downloaded above.
|
||||||
#
|
#
|
||||||
|
|
||||||
load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories", "yarn_install")
|
load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories", "yarn_install")
|
||||||
|
|
||||||
check_bazel_version("0.15.0")
|
check_bazel_version("0.18.0", """
|
||||||
|
If you are on a Mac and using Homebrew, there is a breaking change to the installation in Bazel 0.16
|
||||||
|
See https://blog.bazel.build/2018/08/22/bazel-homebrew.html
|
||||||
|
|
||||||
|
""")
|
||||||
|
|
||||||
node_repositories(
|
node_repositories(
|
||||||
|
node_version = "10.9.0",
|
||||||
package_json = ["//:package.json"],
|
package_json = ["//:package.json"],
|
||||||
preserve_symlinks = True,
|
preserve_symlinks = True,
|
||||||
|
yarn_version = "1.9.2",
|
||||||
|
)
|
||||||
|
|
||||||
|
yarn_install(
|
||||||
|
name = "npm",
|
||||||
|
package_json = "//tools:npm/package.json",
|
||||||
|
yarn_lock = "//tools:npm/yarn.lock",
|
||||||
)
|
)
|
||||||
|
|
||||||
load("@io_bazel_rules_go//go:def.bzl", "go_rules_dependencies", "go_register_toolchains")
|
load("@io_bazel_rules_go//go:def.bzl", "go_rules_dependencies", "go_register_toolchains")
|
||||||
|
|
||||||
go_rules_dependencies()
|
go_rules_dependencies()
|
||||||
|
|
||||||
go_register_toolchains()
|
go_register_toolchains()
|
||||||
|
|
||||||
load("@io_bazel_rules_webtesting//web:repositories.bzl", "browser_repositories", "web_test_repositories")
|
load("@io_bazel_rules_webtesting//web:repositories.bzl", "browser_repositories", "web_test_repositories")
|
||||||
|
|
||||||
web_test_repositories()
|
web_test_repositories()
|
||||||
|
|
||||||
browser_repositories(
|
browser_repositories(
|
||||||
chromium = True,
|
chromium = True,
|
||||||
firefox = True,
|
firefox = True,
|
||||||
@ -127,20 +85,13 @@ load("@angular//:index.bzl", "ng_setup_workspace")
|
|||||||
|
|
||||||
ng_setup_workspace()
|
ng_setup_workspace()
|
||||||
|
|
||||||
#
|
##################################
|
||||||
# Ask Bazel to manage these toolchain dependencies for us.
|
# Skylark documentation generation
|
||||||
# Bazel will run `yarn install` when one of these toolchains is requested during
|
|
||||||
# a build.
|
|
||||||
#
|
|
||||||
|
|
||||||
yarn_install(
|
load("@io_bazel_rules_sass//sass:sass_repositories.bzl", "sass_repositories")
|
||||||
name = "ts-api-guardian_runtime_deps",
|
|
||||||
package_json = "//tools/ts-api-guardian:package.json",
|
|
||||||
yarn_lock = "//tools/ts-api-guardian:yarn.lock",
|
|
||||||
)
|
|
||||||
|
|
||||||
yarn_install(
|
sass_repositories()
|
||||||
name = "http-server_runtime_deps",
|
|
||||||
package_json = "//tools/http-server:package.json",
|
load("@io_bazel_skydoc//skylark:skylark.bzl", "skydoc_repositories")
|
||||||
yarn_lock = "//tools/http-server:yarn.lock",
|
|
||||||
)
|
skydoc_repositories()
|
||||||
|
@ -22,8 +22,8 @@ Here are the most important tasks you might need to use:
|
|||||||
* `yarn start` - run a development web server that watches the files; then builds the doc-viewer and reloads the page, as necessary.
|
* `yarn start` - run a development web server that watches the files; then builds the doc-viewer and reloads the page, as necessary.
|
||||||
* `yarn serve-and-sync` - run both the `docs-watch` and `start` in the same console.
|
* `yarn serve-and-sync` - run both the `docs-watch` and `start` in the same console.
|
||||||
* `yarn lint` - check that the doc-viewer code follows our style rules.
|
* `yarn lint` - check that the doc-viewer code follows our style rules.
|
||||||
* `yarn test` - run all the unit tests once.
|
* `yarn test` - watch all the source files, for the doc-viewer, and run all the unit tests when any change.
|
||||||
* `yarn test --watch` - watch all the source files, for the doc-viewer, and run all the unit tests when any change.
|
* `yarn test --watch=false` - run all the unit tests once.
|
||||||
* `yarn e2e` - run all the e2e tests for the doc-viewer.
|
* `yarn e2e` - run all the e2e tests for the doc-viewer.
|
||||||
|
|
||||||
* `yarn docs` - generate all the docs from the source files.
|
* `yarn docs` - generate all the docs from the source files.
|
||||||
@ -56,14 +56,9 @@ It's necessary to remove the temporary files, because otherwise they're displaye
|
|||||||
|
|
||||||
## Using ServiceWorker locally
|
## Using ServiceWorker locally
|
||||||
|
|
||||||
Since abb36e3cb, running `yarn start --prod` will no longer set up the ServiceWorker, which
|
Running `yarn start` (even when explicitly targeting production mode) does not set up the
|
||||||
would require manually running `yarn sw-manifest` and `yarn sw-copy` (something that is not possible
|
ServiceWorker. If you want to test the ServiceWorker locally, you can use `yarn build` and then
|
||||||
with webpack serving the files from memory).
|
serve the files in `dist/` with `yarn http-server dist -p 4200`.
|
||||||
|
|
||||||
If you want to test ServiceWorker locally, you can use `yarn build` and serve the files in `dist/`
|
|
||||||
with `yarn http-server dist -p 4200`.
|
|
||||||
|
|
||||||
For more details see #16745.
|
|
||||||
|
|
||||||
|
|
||||||
## Guide to authoring
|
## Guide to authoring
|
||||||
|
@ -8,17 +8,24 @@ LABEL name="angular.io PR preview" \
|
|||||||
|
|
||||||
VOLUME /aio-secrets
|
VOLUME /aio-secrets
|
||||||
VOLUME /var/www/aio-builds
|
VOLUME /var/www/aio-builds
|
||||||
|
VOLUME /dockerbuild
|
||||||
|
|
||||||
EXPOSE 80 443
|
EXPOSE 80 443
|
||||||
|
|
||||||
|
|
||||||
# Build-time args and env vars
|
# Build-time args and env vars
|
||||||
|
# The AIO_ARTIFACT_PATH path needs to be kept in synch with the value of
|
||||||
|
# `aio_preview->steps->store_artifacts->destination` property in `.circleci/config.yml`
|
||||||
|
ARG AIO_ARTIFACT_PATH=aio/dist/aio-snapshot.tgz
|
||||||
|
ARG TEST_AIO_ARTIFACT_PATH=$AIO_ARTIFACT_PATH
|
||||||
ARG AIO_BUILDS_DIR=/var/www/aio-builds
|
ARG AIO_BUILDS_DIR=/var/www/aio-builds
|
||||||
ARG TEST_AIO_BUILDS_DIR=/tmp/aio-builds
|
ARG TEST_AIO_BUILDS_DIR=/tmp/aio-builds
|
||||||
ARG AIO_DOMAIN_NAME=ngbuilds.io
|
ARG AIO_DOMAIN_NAME=ngbuilds.io
|
||||||
ARG TEST_AIO_DOMAIN_NAME=$AIO_DOMAIN_NAME.localhost
|
ARG TEST_AIO_DOMAIN_NAME=$AIO_DOMAIN_NAME.localhost
|
||||||
ARG AIO_GITHUB_ORGANIZATION=angular
|
ARG AIO_GITHUB_ORGANIZATION=angular
|
||||||
ARG TEST_AIO_GITHUB_ORGANIZATION=angular
|
ARG TEST_AIO_GITHUB_ORGANIZATION=test-org
|
||||||
|
ARG AIO_GITHUB_REPO=angular
|
||||||
|
ARG TEST_AIO_GITHUB_REPO=test-repo
|
||||||
ARG AIO_GITHUB_TEAM_SLUGS=team,aio-contributors
|
ARG AIO_GITHUB_TEAM_SLUGS=team,aio-contributors
|
||||||
ARG TEST_AIO_GITHUB_TEAM_SLUGS=team,aio-contributors
|
ARG TEST_AIO_GITHUB_TEAM_SLUGS=team,aio-contributors
|
||||||
ARG AIO_NGINX_HOSTNAME=$AIO_DOMAIN_NAME
|
ARG AIO_NGINX_HOSTNAME=$AIO_DOMAIN_NAME
|
||||||
@ -27,33 +34,35 @@ ARG AIO_NGINX_PORT_HTTP=80
|
|||||||
ARG TEST_AIO_NGINX_PORT_HTTP=8080
|
ARG TEST_AIO_NGINX_PORT_HTTP=8080
|
||||||
ARG AIO_NGINX_PORT_HTTPS=443
|
ARG AIO_NGINX_PORT_HTTPS=443
|
||||||
ARG TEST_AIO_NGINX_PORT_HTTPS=4433
|
ARG TEST_AIO_NGINX_PORT_HTTPS=4433
|
||||||
ARG AIO_REPO_SLUG=angular/angular
|
ARG AIO_SIGNIFICANT_FILES_PATTERN='^(?:aio|packages)/(?!.*[._]spec\\.[jt]s$)'
|
||||||
ARG TEST_AIO_REPO_SLUG=test-repo/test-slug
|
ARG TEST_AIO_SIGNIFICANT_FILES_PATTERN=$AIO_SIGNIFICANT_FILES_PATTERN
|
||||||
ARG AIO_TRUSTED_PR_LABEL="aio: preview"
|
ARG AIO_TRUSTED_PR_LABEL="aio: preview"
|
||||||
ARG TEST_AIO_TRUSTED_PR_LABEL="aio: preview"
|
ARG TEST_AIO_TRUSTED_PR_LABEL="aio: preview"
|
||||||
ARG AIO_UPLOAD_HOSTNAME=upload.localhost
|
ARG AIO_PREVIEW_SERVER_HOSTNAME=preview.localhost
|
||||||
ARG TEST_AIO_UPLOAD_HOSTNAME=upload.localhost
|
ARG TEST_AIO_PREVIEW_SERVER_HOSTNAME=preview.localhost
|
||||||
ARG AIO_UPLOAD_MAX_SIZE=20971520
|
ARG AIO_ARTIFACT_MAX_SIZE=20971520
|
||||||
ARG TEST_AIO_UPLOAD_MAX_SIZE=20971520
|
ARG TEST_AIO_ARTIFACT_MAX_SIZE=200
|
||||||
ARG AIO_UPLOAD_PORT=3000
|
ARG AIO_PREVIEW_SERVER_PORT=3000
|
||||||
ARG TEST_AIO_UPLOAD_PORT=3001
|
ARG TEST_AIO_PREVIEW_SERVER_PORT=3001
|
||||||
|
|
||||||
ENV AIO_BUILDS_DIR=$AIO_BUILDS_DIR TEST_AIO_BUILDS_DIR=$TEST_AIO_BUILDS_DIR \
|
ENV AIO_ARTIFACT_PATH=$AIO_ARTIFACT_PATH TEST_AIO_ARTIFACT_PATH=$TEST_AIO_ARTIFACT_PATH \
|
||||||
|
AIO_BUILDS_DIR=$AIO_BUILDS_DIR TEST_AIO_BUILDS_DIR=$TEST_AIO_BUILDS_DIR \
|
||||||
AIO_DOMAIN_NAME=$AIO_DOMAIN_NAME TEST_AIO_DOMAIN_NAME=$TEST_AIO_DOMAIN_NAME \
|
AIO_DOMAIN_NAME=$AIO_DOMAIN_NAME TEST_AIO_DOMAIN_NAME=$TEST_AIO_DOMAIN_NAME \
|
||||||
AIO_GITHUB_ORGANIZATION=$AIO_GITHUB_ORGANIZATION TEST_AIO_GITHUB_ORGANIZATION=$TEST_AIO_GITHUB_ORGANIZATION \
|
AIO_GITHUB_ORGANIZATION=$AIO_GITHUB_ORGANIZATION TEST_AIO_GITHUB_ORGANIZATION=$TEST_AIO_GITHUB_ORGANIZATION \
|
||||||
|
AIO_GITHUB_REPO=$AIO_GITHUB_REPO TEST_AIO_GITHUB_REPO=$TEST_AIO_GITHUB_REPO \
|
||||||
AIO_GITHUB_TEAM_SLUGS=$AIO_GITHUB_TEAM_SLUGS TEST_AIO_GITHUB_TEAM_SLUGS=$TEST_AIO_GITHUB_TEAM_SLUGS \
|
AIO_GITHUB_TEAM_SLUGS=$AIO_GITHUB_TEAM_SLUGS TEST_AIO_GITHUB_TEAM_SLUGS=$TEST_AIO_GITHUB_TEAM_SLUGS \
|
||||||
AIO_LOCALCERTS_DIR=/etc/ssl/localcerts TEST_AIO_LOCALCERTS_DIR=/etc/ssl/localcerts-test \
|
AIO_LOCALCERTS_DIR=/etc/ssl/localcerts TEST_AIO_LOCALCERTS_DIR=/etc/ssl/localcerts-test \
|
||||||
AIO_NGINX_HOSTNAME=$AIO_NGINX_HOSTNAME TEST_AIO_NGINX_HOSTNAME=$TEST_AIO_NGINX_HOSTNAME \
|
AIO_NGINX_HOSTNAME=$AIO_NGINX_HOSTNAME TEST_AIO_NGINX_HOSTNAME=$TEST_AIO_NGINX_HOSTNAME \
|
||||||
AIO_NGINX_LOGS_DIR=/var/log/aio/nginx TEST_AIO_NGINX_LOGS_DIR=/var/log/aio/nginx-test \
|
AIO_NGINX_LOGS_DIR=/var/log/aio/nginx TEST_AIO_NGINX_LOGS_DIR=/var/log/aio/nginx-test \
|
||||||
AIO_NGINX_PORT_HTTP=$AIO_NGINX_PORT_HTTP TEST_AIO_NGINX_PORT_HTTP=$TEST_AIO_NGINX_PORT_HTTP \
|
AIO_NGINX_PORT_HTTP=$AIO_NGINX_PORT_HTTP TEST_AIO_NGINX_PORT_HTTP=$TEST_AIO_NGINX_PORT_HTTP \
|
||||||
AIO_NGINX_PORT_HTTPS=$AIO_NGINX_PORT_HTTPS TEST_AIO_NGINX_PORT_HTTPS=$TEST_AIO_NGINX_PORT_HTTPS \
|
AIO_NGINX_PORT_HTTPS=$AIO_NGINX_PORT_HTTPS TEST_AIO_NGINX_PORT_HTTPS=$TEST_AIO_NGINX_PORT_HTTPS \
|
||||||
AIO_REPO_SLUG=$AIO_REPO_SLUG TEST_AIO_REPO_SLUG=$TEST_AIO_REPO_SLUG \
|
|
||||||
AIO_SCRIPTS_JS_DIR=/usr/share/aio-scripts-js \
|
AIO_SCRIPTS_JS_DIR=/usr/share/aio-scripts-js \
|
||||||
AIO_SCRIPTS_SH_DIR=/usr/share/aio-scripts-sh \
|
AIO_SCRIPTS_SH_DIR=/usr/share/aio-scripts-sh \
|
||||||
|
AIO_SIGNIFICANT_FILES_PATTERN=$AIO_SIGNIFICANT_FILES_PATTERN TEST_AIO_SIGNIFICANT_FILES_PATTERN=$TEST_AIO_SIGNIFICANT_FILES_PATTERN \
|
||||||
AIO_TRUSTED_PR_LABEL=$AIO_TRUSTED_PR_LABEL TEST_AIO_TRUSTED_PR_LABEL=$TEST_AIO_TRUSTED_PR_LABEL \
|
AIO_TRUSTED_PR_LABEL=$AIO_TRUSTED_PR_LABEL TEST_AIO_TRUSTED_PR_LABEL=$TEST_AIO_TRUSTED_PR_LABEL \
|
||||||
AIO_UPLOAD_HOSTNAME=$AIO_UPLOAD_HOSTNAME TEST_AIO_UPLOAD_HOSTNAME=$TEST_AIO_UPLOAD_HOSTNAME \
|
AIO_PREVIEW_SERVER_HOSTNAME=$AIO_PREVIEW_SERVER_HOSTNAME TEST_AIO_PREVIEW_SERVER_HOSTNAME=$TEST_AIO_PREVIEW_SERVER_HOSTNAME \
|
||||||
AIO_UPLOAD_MAX_SIZE=$AIO_UPLOAD_MAX_SIZE TEST_AIO_UPLOAD_MAX_SIZE=$TEST_AIO_UPLOAD_MAX_SIZE \
|
AIO_ARTIFACT_MAX_SIZE=$AIO_ARTIFACT_MAX_SIZE TEST_AIO_ARTIFACT_MAX_SIZE=$TEST_AIO_ARTIFACT_MAX_SIZE \
|
||||||
AIO_UPLOAD_PORT=$AIO_UPLOAD_PORT TEST_AIO_UPLOAD_PORT=$TEST_AIO_UPLOAD_PORT \
|
AIO_PREVIEW_SERVER_PORT=$AIO_PREVIEW_SERVER_PORT TEST_AIO_PREVIEW_SERVER_PORT=$TEST_AIO_PREVIEW_SERVER_PORT \
|
||||||
AIO_WWW_USER=www-data \
|
AIO_WWW_USER=www-data \
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
|
|
||||||
@ -64,7 +73,7 @@ RUN mkdir /var/log/aio
|
|||||||
|
|
||||||
# Add extra package sources
|
# Add extra package sources
|
||||||
RUN apt-get update -y && apt-get install -y curl
|
RUN apt-get update -y && apt-get install -y curl
|
||||||
RUN curl --silent --show-error --location https://deb.nodesource.com/setup_6.x | bash -
|
RUN curl --silent --show-error --location https://deb.nodesource.com/setup_10.x | bash -
|
||||||
RUN curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
RUN curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
||||||
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
|
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
|
||||||
RUN echo "deb http://ftp.debian.org/debian jessie-backports main" | tee /etc/apt/sources.list.d/backports.list
|
RUN echo "deb http://ftp.debian.org/debian jessie-backports main" | tee /etc/apt/sources.list.d/backports.list
|
||||||
@ -99,9 +108,9 @@ RUN printenv | grep AIO_ >> /etc/environment
|
|||||||
# Set up dnsmasq
|
# Set up dnsmasq
|
||||||
COPY dnsmasq/dnsmasq.conf /etc/
|
COPY dnsmasq/dnsmasq.conf /etc/
|
||||||
RUN sed -i "s|{{\$AIO_NGINX_HOSTNAME}}|$AIO_NGINX_HOSTNAME|g" /etc/dnsmasq.conf
|
RUN sed -i "s|{{\$AIO_NGINX_HOSTNAME}}|$AIO_NGINX_HOSTNAME|g" /etc/dnsmasq.conf
|
||||||
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$AIO_UPLOAD_HOSTNAME|g" /etc/dnsmasq.conf
|
RUN sed -i "s|{{\$AIO_PREVIEW_SERVER_HOSTNAME}}|$AIO_PREVIEW_SERVER_HOSTNAME|g" /etc/dnsmasq.conf
|
||||||
RUN sed -i "s|{{\$TEST_AIO_NGINX_HOSTNAME}}|$TEST_AIO_NGINX_HOSTNAME|g" /etc/dnsmasq.conf
|
RUN sed -i "s|{{\$TEST_AIO_NGINX_HOSTNAME}}|$TEST_AIO_NGINX_HOSTNAME|g" /etc/dnsmasq.conf
|
||||||
RUN sed -i "s|{{\$TEST_AIO_UPLOAD_HOSTNAME}}|$TEST_AIO_UPLOAD_HOSTNAME|g" /etc/dnsmasq.conf
|
RUN sed -i "s|{{\$TEST_AIO_PREVIEW_SERVER_HOSTNAME}}|$TEST_AIO_PREVIEW_SERVER_HOSTNAME|g" /etc/dnsmasq.conf
|
||||||
|
|
||||||
|
|
||||||
# Set up SSL/TLS certificates
|
# Set up SSL/TLS certificates
|
||||||
@ -125,9 +134,9 @@ RUN sed -i "s|{{\$AIO_LOCALCERTS_DIR}}|$AIO_LOCALCERTS_DIR|g" /etc/nginx/conf.d/
|
|||||||
RUN sed -i "s|{{\$AIO_NGINX_LOGS_DIR}}|$AIO_NGINX_LOGS_DIR|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
RUN sed -i "s|{{\$AIO_NGINX_LOGS_DIR}}|$AIO_NGINX_LOGS_DIR|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTP}}|$AIO_NGINX_PORT_HTTP|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTP}}|$AIO_NGINX_PORT_HTTP|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTPS}}|$AIO_NGINX_PORT_HTTPS|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTPS}}|$AIO_NGINX_PORT_HTTPS|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||||
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$AIO_UPLOAD_HOSTNAME|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
RUN sed -i "s|{{\$AIO_PREVIEW_SERVER_HOSTNAME}}|$AIO_PREVIEW_SERVER_HOSTNAME|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||||
RUN sed -i "s|{{\$AIO_UPLOAD_MAX_SIZE}}|$AIO_UPLOAD_MAX_SIZE|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
RUN sed -i "s|{{\$AIO_ARTIFACT_MAX_SIZE}}|$AIO_ARTIFACT_MAX_SIZE|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||||
RUN sed -i "s|{{\$AIO_UPLOAD_PORT}}|$AIO_UPLOAD_PORT|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
RUN sed -i "s|{{\$AIO_PREVIEW_SERVER_PORT}}|$AIO_PREVIEW_SERVER_PORT|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||||
|
|
||||||
COPY nginx/aio-builds.conf /etc/nginx/conf.d/aio-builds-test.conf
|
COPY nginx/aio-builds.conf /etc/nginx/conf.d/aio-builds-test.conf
|
||||||
RUN sed -i "s|{{\$AIO_BUILDS_DIR}}|$TEST_AIO_BUILDS_DIR|g" /etc/nginx/conf.d/aio-builds-test.conf
|
RUN sed -i "s|{{\$AIO_BUILDS_DIR}}|$TEST_AIO_BUILDS_DIR|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||||
@ -136,9 +145,9 @@ RUN sed -i "s|{{\$AIO_LOCALCERTS_DIR}}|$TEST_AIO_LOCALCERTS_DIR|g" /etc/nginx/co
|
|||||||
RUN sed -i "s|{{\$AIO_NGINX_LOGS_DIR}}|$TEST_AIO_NGINX_LOGS_DIR|g" /etc/nginx/conf.d/aio-builds-test.conf
|
RUN sed -i "s|{{\$AIO_NGINX_LOGS_DIR}}|$TEST_AIO_NGINX_LOGS_DIR|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTP}}|$TEST_AIO_NGINX_PORT_HTTP|g" /etc/nginx/conf.d/aio-builds-test.conf
|
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTP}}|$TEST_AIO_NGINX_PORT_HTTP|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTPS}}|$TEST_AIO_NGINX_PORT_HTTPS|g" /etc/nginx/conf.d/aio-builds-test.conf
|
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTPS}}|$TEST_AIO_NGINX_PORT_HTTPS|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||||
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$TEST_AIO_UPLOAD_HOSTNAME|g" /etc/nginx/conf.d/aio-builds-test.conf
|
RUN sed -i "s|{{\$AIO_PREVIEW_SERVER_HOSTNAME}}|$TEST_AIO_PREVIEW_SERVER_HOSTNAME|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||||
RUN sed -i "s|{{\$AIO_UPLOAD_MAX_SIZE}}|$TEST_AIO_UPLOAD_MAX_SIZE|g" /etc/nginx/conf.d/aio-builds-test.conf
|
RUN sed -i "s|{{\$AIO_ARTIFACT_MAX_SIZE}}|$TEST_AIO_ARTIFACT_MAX_SIZE|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||||
RUN sed -i "s|{{\$AIO_UPLOAD_PORT}}|$TEST_AIO_UPLOAD_PORT|g" /etc/nginx/conf.d/aio-builds-test.conf
|
RUN sed -i "s|{{\$AIO_PREVIEW_SERVER_PORT}}|$TEST_AIO_PREVIEW_SERVER_PORT|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||||
|
|
||||||
|
|
||||||
# Set up pm2
|
# Set up pm2
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
# Periodically clean up builds that do not correspond to currently open PRs
|
# Periodically clean up builds that do not correspond to currently open PRs
|
||||||
0 12 * * * root /usr/local/bin/aio-clean-up >> /var/log/cron.log 2>&1
|
0 12 * * * /usr/local/bin/aio-clean-up >> /var/log/cron.log 2>&1
|
||||||
|
@ -8,9 +8,9 @@ listen-address=127.0.0.1
|
|||||||
|
|
||||||
# Force an IP address for these domains.
|
# Force an IP address for these domains.
|
||||||
address=/{{$AIO_NGINX_HOSTNAME}}/127.0.0.1
|
address=/{{$AIO_NGINX_HOSTNAME}}/127.0.0.1
|
||||||
address=/{{$AIO_UPLOAD_HOSTNAME}}/127.0.0.1
|
address=/{{$AIO_PREVIEW_SERVER_HOSTNAME}}/127.0.0.1
|
||||||
address=/{{$TEST_AIO_NGINX_HOSTNAME}}/127.0.0.1
|
address=/{{$TEST_AIO_NGINX_HOSTNAME}}/127.0.0.1
|
||||||
address=/{{$TEST_AIO_UPLOAD_HOSTNAME}}/127.0.0.1
|
address=/{{$TEST_AIO_PREVIEW_SERVER_HOSTNAME}}/127.0.0.1
|
||||||
|
|
||||||
# Run as root (required from inside docker container).
|
# Run as root (required from inside docker container).
|
||||||
user=root
|
user=root
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
/var/log/aio/upload-server-*.log {
|
/var/log/aio/preview-server-*.log {
|
||||||
compress
|
compress
|
||||||
copytruncate
|
copytruncate
|
||||||
delaycompress
|
delaycompress
|
@ -36,6 +36,11 @@ server {
|
|||||||
access_log {{$AIO_NGINX_LOGS_DIR}}/access.log;
|
access_log {{$AIO_NGINX_LOGS_DIR}}/access.log;
|
||||||
error_log {{$AIO_NGINX_LOGS_DIR}}/error.log;
|
error_log {{$AIO_NGINX_LOGS_DIR}}/error.log;
|
||||||
|
|
||||||
|
error_page 404 /404.html;
|
||||||
|
location "=/404.html" {
|
||||||
|
internal;
|
||||||
|
}
|
||||||
|
|
||||||
location "~/[^/]+\.[^/]+$" {
|
location "~/[^/]+\.[^/]+$" {
|
||||||
try_files $uri $uri/ =404;
|
try_files $uri $uri/ =404;
|
||||||
}
|
}
|
||||||
@ -66,24 +71,32 @@ server {
|
|||||||
return 200 '';
|
return 200 '';
|
||||||
}
|
}
|
||||||
|
|
||||||
# Upload builds
|
# Check PRs previewability
|
||||||
location "~^/create-build/(?<pr>[1-9][0-9]*)/(?<sha>[0-9a-f]{40})/?$" {
|
location "~^/can-have-public-preview/\d+/?$" {
|
||||||
|
if ($request_method != "GET") {
|
||||||
|
add_header Allow "GET";
|
||||||
|
return 405;
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy_pass_request_headers on;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_method GET;
|
||||||
|
proxy_pass http://{{$AIO_PREVIEW_SERVER_HOSTNAME}}:{{$AIO_PREVIEW_SERVER_PORT}}$request_uri;
|
||||||
|
|
||||||
|
resolver 127.0.0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Notify about CircleCI builds
|
||||||
|
location "~^/circle-build/?$" {
|
||||||
if ($request_method != "POST") {
|
if ($request_method != "POST") {
|
||||||
add_header Allow "POST";
|
add_header Allow "POST";
|
||||||
return 405;
|
return 405;
|
||||||
}
|
}
|
||||||
|
|
||||||
client_body_temp_path /tmp/aio-create-builds;
|
|
||||||
client_body_buffer_size 128K;
|
|
||||||
client_max_body_size {{$AIO_UPLOAD_MAX_SIZE}};
|
|
||||||
client_body_in_file_only on;
|
|
||||||
|
|
||||||
proxy_pass_request_headers on;
|
proxy_pass_request_headers on;
|
||||||
proxy_set_header X-FILE $request_body_file;
|
|
||||||
proxy_set_body off;
|
|
||||||
proxy_redirect off;
|
proxy_redirect off;
|
||||||
proxy_method GET;
|
proxy_method POST;
|
||||||
proxy_pass http://{{$AIO_UPLOAD_HOSTNAME}}:{{$AIO_UPLOAD_PORT}}$request_uri;
|
proxy_pass http://{{$AIO_PREVIEW_SERVER_HOSTNAME}}:{{$AIO_PREVIEW_SERVER_PORT}}$request_uri;
|
||||||
|
|
||||||
resolver 127.0.0.1;
|
resolver 127.0.0.1;
|
||||||
}
|
}
|
||||||
@ -98,7 +111,7 @@ server {
|
|||||||
proxy_pass_request_headers on;
|
proxy_pass_request_headers on;
|
||||||
proxy_redirect off;
|
proxy_redirect off;
|
||||||
proxy_method POST;
|
proxy_method POST;
|
||||||
proxy_pass http://{{$AIO_UPLOAD_HOSTNAME}}:{{$AIO_UPLOAD_PORT}}$request_uri;
|
proxy_pass http://{{$AIO_PREVIEW_SERVER_HOSTNAME}}:{{$AIO_PREVIEW_SERVER_PORT}}$request_uri;
|
||||||
|
|
||||||
resolver 127.0.0.1;
|
resolver 127.0.0.1;
|
||||||
}
|
}
|
||||||
|
@ -3,29 +3,53 @@ import * as fs from 'fs';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as shell from 'shelljs';
|
import * as shell from 'shelljs';
|
||||||
import {HIDDEN_DIR_PREFIX} from '../common/constants';
|
import {HIDDEN_DIR_PREFIX} from '../common/constants';
|
||||||
|
import {GithubApi} from '../common/github-api';
|
||||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||||
import {assertNotMissingOrEmpty} from '../common/utils';
|
import {assertNotMissingOrEmpty, getPrInfoFromDownloadPath, Logger} from '../common/utils';
|
||||||
|
|
||||||
// Classes
|
// Classes
|
||||||
export class BuildCleaner {
|
export class BuildCleaner {
|
||||||
|
|
||||||
|
private logger = new Logger('BuildCleaner');
|
||||||
|
|
||||||
// Constructor
|
// Constructor
|
||||||
constructor(protected buildsDir: string, protected repoSlug: string, protected githubToken: string) {
|
constructor(protected buildsDir: string, protected githubOrg: string, protected githubRepo: string,
|
||||||
|
protected githubToken: string, protected downloadsDir: string, protected artifactPath: string) {
|
||||||
assertNotMissingOrEmpty('buildsDir', buildsDir);
|
assertNotMissingOrEmpty('buildsDir', buildsDir);
|
||||||
assertNotMissingOrEmpty('repoSlug', repoSlug);
|
assertNotMissingOrEmpty('githubOrg', githubOrg);
|
||||||
|
assertNotMissingOrEmpty('githubRepo', githubRepo);
|
||||||
assertNotMissingOrEmpty('githubToken', githubToken);
|
assertNotMissingOrEmpty('githubToken', githubToken);
|
||||||
|
assertNotMissingOrEmpty('downloadsDir', downloadsDir);
|
||||||
|
assertNotMissingOrEmpty('artifactPath', artifactPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Methods - Public
|
// Methods - Public
|
||||||
public cleanUp(): Promise<void> {
|
public async cleanUp(): Promise<void> {
|
||||||
return Promise.all([
|
try {
|
||||||
this.getExistingBuildNumbers(),
|
this.logger.log('Cleaning up builds and downloads');
|
||||||
this.getOpenPrNumbers(),
|
const openPrs = await this.getOpenPrNumbers();
|
||||||
]).then(([existingBuilds, openPrs]) => this.removeUnnecessaryBuilds(existingBuilds, openPrs));
|
this.logger.log(`Open pull requests: ${openPrs.length}`);
|
||||||
|
await Promise.all([
|
||||||
|
this.cleanBuilds(openPrs),
|
||||||
|
this.cleanDownloads(openPrs),
|
||||||
|
]);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('ERROR:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Methods - Protected
|
public async cleanBuilds(openPrs: number[]): Promise<void> {
|
||||||
protected getExistingBuildNumbers(): Promise<number[]> {
|
const existingBuilds = await this.getExistingBuildNumbers();
|
||||||
return new Promise((resolve, reject) => {
|
await this.removeUnnecessaryBuilds(existingBuilds, openPrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async cleanDownloads(openPrs: number[]): Promise<void> {
|
||||||
|
const existingDownloads = await this.getExistingDownloads();
|
||||||
|
await this.removeUnnecessaryDownloads(existingDownloads, openPrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getExistingBuildNumbers(): Promise<number[]> {
|
||||||
|
return new Promise<number[]>((resolve, reject) => {
|
||||||
fs.readdir(this.buildsDir, (err, files) => {
|
fs.readdir(this.buildsDir, (err, files) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return reject(err);
|
return reject(err);
|
||||||
@ -41,31 +65,29 @@ export class BuildCleaner {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getOpenPrNumbers(): Promise<number[]> {
|
public async getOpenPrNumbers(): Promise<number[]> {
|
||||||
const githubPullRequests = new GithubPullRequests(this.githubToken, this.repoSlug);
|
const api = new GithubApi(this.githubToken);
|
||||||
|
const githubPullRequests = new GithubPullRequests(api, this.githubOrg, this.githubRepo);
|
||||||
return githubPullRequests.
|
const prs = await githubPullRequests.fetchAll('open');
|
||||||
fetchAll('open').
|
return prs.map(pr => pr.number);
|
||||||
then(prs => prs.map(pr => pr.number));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected removeDir(dir: string) {
|
public removeDir(dir: string): void {
|
||||||
try {
|
try {
|
||||||
if (shell.test('-d', dir)) {
|
if (shell.test('-d', dir)) {
|
||||||
shell.chmod('-R', 'a+w', dir);
|
shell.chmod('-R', 'a+w', dir);
|
||||||
shell.rm('-rf', dir);
|
shell.rm('-rf', dir);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`ERROR: Unable to remove '${dir}' due to:`, err);
|
this.logger.error(`ERROR: Unable to remove '${dir}' due to:`, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected removeUnnecessaryBuilds(existingBuildNumbers: number[], openPrNumbers: number[]) {
|
public removeUnnecessaryBuilds(existingBuildNumbers: number[], openPrNumbers: number[]): void {
|
||||||
const toRemove = existingBuildNumbers.filter(num => !openPrNumbers.includes(num));
|
const toRemove = existingBuildNumbers.filter(num => !openPrNumbers.includes(num));
|
||||||
|
|
||||||
console.log(`Existing builds: ${existingBuildNumbers.length}`);
|
this.logger.log(`Existing builds: ${existingBuildNumbers.length}`);
|
||||||
console.log(`Open pull requests: ${openPrNumbers.length}`);
|
this.logger.log(`Removing ${toRemove.length} build(s): ${toRemove.join(', ')}`);
|
||||||
console.log(`Removing ${toRemove.length} build(s): ${toRemove.join(', ')}`);
|
|
||||||
|
|
||||||
// Try removing public dirs.
|
// Try removing public dirs.
|
||||||
toRemove.
|
toRemove.
|
||||||
@ -77,4 +99,29 @@ export class BuildCleaner {
|
|||||||
map(num => path.join(this.buildsDir, HIDDEN_DIR_PREFIX + String(num))).
|
map(num => path.join(this.buildsDir, HIDDEN_DIR_PREFIX + String(num))).
|
||||||
forEach(dir => this.removeDir(dir));
|
forEach(dir => this.removeDir(dir));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getExistingDownloads(): Promise<string[]> {
|
||||||
|
const artifactFile = path.basename(this.artifactPath);
|
||||||
|
return new Promise<string[]>((resolve, reject) => {
|
||||||
|
fs.readdir(this.downloadsDir, (err, files) => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
files = files.filter(file => file.endsWith(artifactFile));
|
||||||
|
resolve(files);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeUnnecessaryDownloads(existingDownloads: string[], openPrNumbers: number[]): void {
|
||||||
|
const toRemove = existingDownloads.filter(filePath => {
|
||||||
|
const {pr} = getPrInfoFromDownloadPath(filePath);
|
||||||
|
return !openPrNumbers.includes(pr);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`Existing downloads: ${existingDownloads.length}`);
|
||||||
|
this.logger.log(`Removing ${toRemove.length} download(s): ${toRemove.join(', ')}`);
|
||||||
|
|
||||||
|
toRemove.forEach(filePath => shell.rm(path.join(this.downloadsDir, filePath)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,26 @@
|
|||||||
// Imports
|
// Imports
|
||||||
import {getEnvVar} from '../common/utils';
|
import {AIO_DOWNLOADS_DIR} from '../common/constants';
|
||||||
|
import {
|
||||||
|
AIO_ARTIFACT_PATH,
|
||||||
|
AIO_BUILDS_DIR,
|
||||||
|
AIO_GITHUB_ORGANIZATION,
|
||||||
|
AIO_GITHUB_REPO,
|
||||||
|
AIO_GITHUB_TOKEN,
|
||||||
|
} from '../common/env-variables';
|
||||||
import {BuildCleaner} from './build-cleaner';
|
import {BuildCleaner} from './build-cleaner';
|
||||||
|
|
||||||
// Constants
|
|
||||||
const AIO_BUILDS_DIR = getEnvVar('AIO_BUILDS_DIR');
|
|
||||||
const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN', true);
|
|
||||||
const AIO_REPO_SLUG = getEnvVar('AIO_REPO_SLUG');
|
|
||||||
|
|
||||||
// Run
|
// Run
|
||||||
_main();
|
_main();
|
||||||
|
|
||||||
// Functions
|
// Functions
|
||||||
function _main() {
|
function _main(): void {
|
||||||
console.log(`[${new Date()}] - Cleaning up builds...`);
|
const buildCleaner = new BuildCleaner(
|
||||||
|
AIO_BUILDS_DIR,
|
||||||
|
AIO_GITHUB_ORGANIZATION,
|
||||||
|
AIO_GITHUB_REPO,
|
||||||
|
AIO_GITHUB_TOKEN,
|
||||||
|
AIO_DOWNLOADS_DIR,
|
||||||
|
AIO_ARTIFACT_PATH);
|
||||||
|
|
||||||
const buildCleaner = new BuildCleaner(AIO_BUILDS_DIR, AIO_REPO_SLUG, AIO_GITHUB_TOKEN);
|
buildCleaner.cleanUp().catch(() => process.exit(1));
|
||||||
|
|
||||||
buildCleaner.cleanUp().catch(err => {
|
|
||||||
console.error('ERROR:', err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,90 @@
|
|||||||
|
// Imports
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
import {assertNotMissingOrEmpty} from './utils';
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const CIRCLE_CI_API_URL = 'https://circleci.com/api/v1.1/project/github';
|
||||||
|
|
||||||
|
// Interfaces - Types
|
||||||
|
export interface ArtifactInfo {
|
||||||
|
path: string;
|
||||||
|
pretty_path: string;
|
||||||
|
node_index: number;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ArtifactResponse = ArtifactInfo[];
|
||||||
|
|
||||||
|
export interface BuildInfo {
|
||||||
|
reponame: string;
|
||||||
|
failed: boolean;
|
||||||
|
branch: string;
|
||||||
|
username: string;
|
||||||
|
build_num: number;
|
||||||
|
has_artifacts: boolean;
|
||||||
|
outcome: string; // e.g. 'success'
|
||||||
|
vcs_revision: string; // HEAD SHA
|
||||||
|
// there are other fields but they are not used in this code
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Helper that can interact with the CircleCI API.
|
||||||
|
*/
|
||||||
|
export class CircleCiApi {
|
||||||
|
|
||||||
|
private tokenParam = `circle-token=${this.circleCiToken}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a helper that can interact with the CircleCI REST API.
|
||||||
|
* @param githubOrg The Github organisation whose repos we want to access in CircleCI (e.g. angular).
|
||||||
|
* @param githubRepo The Github repo whose builds we want to access in CircleCI (e.g. angular).
|
||||||
|
* @param circleCiToken The CircleCI API access token (secret).
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
private githubOrg: string,
|
||||||
|
private githubRepo: string,
|
||||||
|
private circleCiToken: string,
|
||||||
|
) {
|
||||||
|
assertNotMissingOrEmpty('githubOrg', githubOrg);
|
||||||
|
assertNotMissingOrEmpty('githubRepo', githubRepo);
|
||||||
|
assertNotMissingOrEmpty('circleCiToken', circleCiToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the info for a build from the CircleCI API
|
||||||
|
* @param buildNumber The CircleCI build number that generated the artifact.
|
||||||
|
* @returns A promise to the info about the build
|
||||||
|
*/
|
||||||
|
public async getBuildInfo(buildNumber: number): Promise<BuildInfo> {
|
||||||
|
try {
|
||||||
|
const baseUrl = `${CIRCLE_CI_API_URL}/${this.githubOrg}/${this.githubRepo}/${buildNumber}`;
|
||||||
|
const response = await fetch(`${baseUrl}?${this.tokenParam}`);
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(`${baseUrl}: ${response.status} - ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`CircleCI build info request failed (${error.message})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query the CircleCI API to get a URL for a specified artifact from a specified build.
|
||||||
|
* @param artifactPath The path, within the build to the artifact.
|
||||||
|
* @returns A promise to the URL that can be requested to download the actual build artifact file.
|
||||||
|
*/
|
||||||
|
public async getBuildArtifactUrl(buildNumber: number, artifactPath: string): Promise<string> {
|
||||||
|
const baseUrl = `${CIRCLE_CI_API_URL}/${this.githubOrg}/${this.githubRepo}/${buildNumber}`;
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${baseUrl}/artifacts?${this.tokenParam}`);
|
||||||
|
const artifacts = await response.json() as ArtifactResponse;
|
||||||
|
const artifact = artifacts.find(item => item.path === artifactPath);
|
||||||
|
if (!artifact) {
|
||||||
|
throw new Error(`Missing artifact (${artifactPath}) for CircleCI build: ${buildNumber}`);
|
||||||
|
}
|
||||||
|
return artifact.url;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`CircleCI artifact URL request failed (${error.message})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
// Constants
|
// Constants
|
||||||
|
export const AIO_DOWNLOADS_DIR = '/tmp/aio-downloads';
|
||||||
export const HIDDEN_DIR_PREFIX = 'hidden--';
|
export const HIDDEN_DIR_PREFIX = 'hidden--';
|
||||||
export const SHORT_SHA_LEN = 7;
|
export const SHORT_SHA_LEN = 7;
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
import {getEnvVar} from './utils';
|
||||||
|
|
||||||
|
export const AIO_ARTIFACT_PATH = getEnvVar('AIO_ARTIFACT_PATH');
|
||||||
|
export const AIO_BUILDS_DIR = getEnvVar('AIO_BUILDS_DIR');
|
||||||
|
export const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN');
|
||||||
|
export const AIO_CIRCLE_CI_TOKEN = getEnvVar('AIO_CIRCLE_CI_TOKEN');
|
||||||
|
export const AIO_DOMAIN_NAME = getEnvVar('AIO_DOMAIN_NAME');
|
||||||
|
export const AIO_GITHUB_ORGANIZATION = getEnvVar('AIO_GITHUB_ORGANIZATION');
|
||||||
|
export const AIO_GITHUB_REPO = getEnvVar('AIO_GITHUB_REPO');
|
||||||
|
export const AIO_GITHUB_TEAM_SLUGS = getEnvVar('AIO_GITHUB_TEAM_SLUGS');
|
||||||
|
export const AIO_NGINX_HOSTNAME = getEnvVar('AIO_NGINX_HOSTNAME');
|
||||||
|
export const AIO_NGINX_PORT_HTTP = +getEnvVar('AIO_NGINX_PORT_HTTP');
|
||||||
|
export const AIO_NGINX_PORT_HTTPS = +getEnvVar('AIO_NGINX_PORT_HTTPS');
|
||||||
|
export const AIO_SIGNIFICANT_FILES_PATTERN = getEnvVar('AIO_SIGNIFICANT_FILES_PATTERN');
|
||||||
|
export const AIO_TRUSTED_PR_LABEL = getEnvVar('AIO_TRUSTED_PR_LABEL');
|
||||||
|
export const AIO_PREVIEW_SERVER_HOSTNAME = getEnvVar('AIO_PREVIEW_SERVER_HOSTNAME');
|
||||||
|
export const AIO_PREVIEW_SERVER_PORT = +getEnvVar('AIO_PREVIEW_SERVER_PORT');
|
||||||
|
export const AIO_ARTIFACT_MAX_SIZE = +getEnvVar('AIO_ARTIFACT_MAX_SIZE');
|
||||||
|
export const AIO_WWW_USER = getEnvVar('AIO_WWW_USER');
|
@ -28,29 +28,18 @@ export class GithubApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Methods - Public
|
// Methods - Public
|
||||||
public get<T>(pathname: string, params?: RequestParamsOrNull): Promise<T> {
|
public get<T = any>(pathname: string, params?: RequestParamsOrNull): Promise<T> {
|
||||||
const path = this.buildPath(pathname, params);
|
const path = this.buildPath(pathname, params);
|
||||||
return this.request<T>('get', path);
|
return this.request<T>('get', path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public post<T>(pathname: string, params?: RequestParamsOrNull, data?: any): Promise<T> {
|
public post<T = any>(pathname: string, params?: RequestParamsOrNull, data?: any): Promise<T> {
|
||||||
const path = this.buildPath(pathname, params);
|
const path = this.buildPath(pathname, params);
|
||||||
return this.request<T>('post', path, data);
|
return this.request<T>('post', path, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Methods - Protected
|
// In GitHub API paginated requests, page numbering is 1-based. (https://developer.github.com/v3/#pagination)
|
||||||
protected buildPath(pathname: string, params?: RequestParamsOrNull): string {
|
public getPaginated<T>(pathname: string, baseParams: RequestParams = {}, currentPage: number = 1): Promise<T[]> {
|
||||||
if (params == null) {
|
|
||||||
return pathname;
|
|
||||||
}
|
|
||||||
|
|
||||||
const search = (params === null) ? '' : this.serializeSearchParams(params);
|
|
||||||
const joiner = search && '?';
|
|
||||||
|
|
||||||
return `${pathname}${joiner}${search}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getPaginated<T>(pathname: string, baseParams: RequestParams = {}, currentPage: number = 0): Promise<T[]> {
|
|
||||||
const perPage = 100;
|
const perPage = 100;
|
||||||
const params = {
|
const params = {
|
||||||
...baseParams,
|
...baseParams,
|
||||||
@ -67,6 +56,18 @@ export class GithubApi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Methods - Protected
|
||||||
|
protected buildPath(pathname: string, params?: RequestParamsOrNull): string {
|
||||||
|
if (params == null) {
|
||||||
|
return pathname;
|
||||||
|
}
|
||||||
|
|
||||||
|
const search = (params === null) ? '' : this.serializeSearchParams(params);
|
||||||
|
const joiner = search && '?';
|
||||||
|
|
||||||
|
return `${pathname}${joiner}${search}`;
|
||||||
|
}
|
||||||
|
|
||||||
protected request<T>(method: string, path: string, data: any = null): Promise<T> {
|
protected request<T>(method: string, path: string, data: any = null): Promise<T> {
|
||||||
return new Promise<T>((resolve, reject) => {
|
return new Promise<T>((resolve, reject) => {
|
||||||
const options = {
|
const options = {
|
||||||
@ -81,7 +82,7 @@ export class GithubApi {
|
|||||||
reject(`Request to '${url}' failed (status: ${statusCode}): ${responseText}`);
|
reject(`Request to '${url}' failed (status: ${statusCode}): ${responseText}`);
|
||||||
};
|
};
|
||||||
const onSuccess = (responseText: string) => {
|
const onSuccess = (responseText: string) => {
|
||||||
try { resolve(JSON.parse(responseText)); } catch (err) { reject(err); }
|
try { resolve(responseText && JSON.parse(responseText)); } catch (err) { reject(err); }
|
||||||
};
|
};
|
||||||
const onResponse = (res: IncomingMessage) => {
|
const onResponse = (res: IncomingMessage) => {
|
||||||
const statusCode = res.statusCode || -1;
|
const statusCode = res.statusCode || -1;
|
||||||
|
@ -1,46 +1,79 @@
|
|||||||
// Imports
|
|
||||||
import {assertNotMissingOrEmpty} from '../common/utils';
|
|
||||||
import {GithubApi} from './github-api';
|
import {GithubApi} from './github-api';
|
||||||
|
import {assert, assertNotMissingOrEmpty} from './utils';
|
||||||
|
|
||||||
// Interfaces - Types
|
|
||||||
export interface PullRequest {
|
export interface PullRequest {
|
||||||
number: number;
|
number: number;
|
||||||
user: {login: string};
|
user: {login: string};
|
||||||
labels: {name: string}[];
|
labels: {name: string}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FileInfo {
|
||||||
|
sha: string;
|
||||||
|
filename: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type PullRequestState = 'all' | 'closed' | 'open';
|
export type PullRequestState = 'all' | 'closed' | 'open';
|
||||||
|
|
||||||
// Classes
|
/**
|
||||||
export class GithubPullRequests extends GithubApi {
|
* Access pull requests on GitHub.
|
||||||
// Constructor
|
*/
|
||||||
constructor(githubToken: string, protected repoSlug: string) {
|
export class GithubPullRequests {
|
||||||
super(githubToken);
|
public repoSlug: string;
|
||||||
assertNotMissingOrEmpty('repoSlug', repoSlug);
|
|
||||||
|
/**
|
||||||
|
* Create an instance of this helper
|
||||||
|
* @param api An instance of the Github API helper.
|
||||||
|
* @param githubOrg The organisation on GitHub whose repo we will interrogate.
|
||||||
|
* @param githubRepo The repository on Github with whose PRs we will interact.
|
||||||
|
*/
|
||||||
|
constructor(private api: GithubApi, githubOrg: string, githubRepo: string) {
|
||||||
|
assertNotMissingOrEmpty('githubOrg', githubOrg);
|
||||||
|
assertNotMissingOrEmpty('githubRepo', githubRepo);
|
||||||
|
this.repoSlug = `${githubOrg}/${githubRepo}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Methods - Public
|
/**
|
||||||
public addComment(pr: number, body: string): Promise<void> {
|
* Post a comment on a PR.
|
||||||
if (!(pr > 0)) {
|
* @param pr The number of the PR on which to comment.
|
||||||
throw new Error(`Invalid PR number: ${pr}`);
|
* @param body The body of the comment to post.
|
||||||
} else if (!body) {
|
* @returns A promise that resolves when the comment has been posted.
|
||||||
throw new Error(`Invalid or empty comment body: ${body}`);
|
*/
|
||||||
}
|
public addComment(pr: number, body: string): Promise<any> {
|
||||||
|
assert(pr > 0, `Invalid PR number: ${pr}`);
|
||||||
return this.post<void>(`/repos/${this.repoSlug}/issues/${pr}/comments`, null, {body});
|
assert(!!body, `Invalid or empty comment body: ${body}`);
|
||||||
|
return this.api.post<any>(`/repos/${this.repoSlug}/issues/${pr}/comments`, null, {body});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request information about a PR.
|
||||||
|
* @param pr The number of the PR for which to request info.
|
||||||
|
* @returns A promise that is resolves with information about the specified PR.
|
||||||
|
*/
|
||||||
public fetch(pr: number): Promise<PullRequest> {
|
public fetch(pr: number): Promise<PullRequest> {
|
||||||
|
assert(pr > 0, `Invalid PR number: ${pr}`);
|
||||||
// Using the `/issues/` URL, because the `/pulls/` one does not provide labels.
|
// Using the `/issues/` URL, because the `/pulls/` one does not provide labels.
|
||||||
return this.get<PullRequest>(`/repos/${this.repoSlug}/issues/${pr}`);
|
return this.api.get<PullRequest>(`/repos/${this.repoSlug}/issues/${pr}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request information about all PRs that match the given state.
|
||||||
|
* @param state Only retrieve PRs that have this state.
|
||||||
|
* @returns A promise that is resolved with information about the requested PRs.
|
||||||
|
*/
|
||||||
public fetchAll(state: PullRequestState = 'all'): Promise<PullRequest[]> {
|
public fetchAll(state: PullRequestState = 'all'): Promise<PullRequest[]> {
|
||||||
console.log(`Fetching ${state} pull requests...`);
|
|
||||||
|
|
||||||
const pathname = `/repos/${this.repoSlug}/pulls`;
|
const pathname = `/repos/${this.repoSlug}/pulls`;
|
||||||
const params = {state};
|
const params = {state};
|
||||||
|
|
||||||
return this.getPaginated<PullRequest>(pathname, params);
|
return this.api.getPaginated<PullRequest>(pathname, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request a list of files for the given PR.
|
||||||
|
* @param pr The number of the PR for which to request files.
|
||||||
|
* @returns A promise that resolves to an array of file information
|
||||||
|
*/
|
||||||
|
public fetchFiles(pr: number): Promise<FileInfo[]> {
|
||||||
|
assert(pr > 0, `Invalid PR number: ${pr}`);
|
||||||
|
return this.api.getPaginated<FileInfo>(`/repos/${this.repoSlug}/pulls/${pr}/files`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,45 +1,72 @@
|
|||||||
// Imports
|
|
||||||
import {assertNotMissingOrEmpty} from '../common/utils';
|
|
||||||
import {GithubApi} from './github-api';
|
import {GithubApi} from './github-api';
|
||||||
|
import {assertNotMissingOrEmpty} from './utils';
|
||||||
|
|
||||||
// Interfaces - Types
|
export interface Team {
|
||||||
interface Team {
|
|
||||||
id: number;
|
id: number;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TeamMembership {
|
export interface TeamMembership {
|
||||||
state: string;
|
state: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Classes
|
export class GithubTeams {
|
||||||
export class GithubTeams extends GithubApi {
|
/**
|
||||||
// Constructor
|
* Create an instance of this helper
|
||||||
constructor(githubToken: string, protected organization: string) {
|
* @param api An instance of the Github API helper.
|
||||||
super(githubToken);
|
* @param githubOrg The organisation on GitHub whose repo we will interrogate.
|
||||||
assertNotMissingOrEmpty('organization', organization);
|
*/
|
||||||
|
constructor(private api: GithubApi, protected githubOrg: string) {
|
||||||
|
assertNotMissingOrEmpty('githubOrg', githubOrg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Methods - Public
|
/**
|
||||||
|
* Request information about all the organisation's teams in GitHub.
|
||||||
|
* @returns A promise that is resolved with information about the teams.
|
||||||
|
*/
|
||||||
public fetchAll(): Promise<Team[]> {
|
public fetchAll(): Promise<Team[]> {
|
||||||
return this.getPaginated<Team>(`/orgs/${this.organization}/teams`);
|
return this.api.getPaginated<Team>(`/orgs/${this.githubOrg}/teams`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public isMemberById(username: string, teamIds: number[]): Promise<boolean> {
|
/**
|
||||||
const getMembership = (teamId: number) =>
|
* Check whether the specified username is a member of the specified team.
|
||||||
this.get<TeamMembership>(`/teams/${teamId}/memberships/${username}`).
|
* @param username The usernane to check for in the team.
|
||||||
then(membership => membership.state === 'active').
|
* @param teamIds The team to check for the username.
|
||||||
catch(() => false);
|
* @returns a Promise that resolves to `true` if the username is a member of the team.
|
||||||
const reduceFn = (promise: Promise<boolean>, teamId: number) =>
|
*/
|
||||||
promise.then(isMember => isMember || getMembership(teamId));
|
public async isMemberById(username: string, teamIds: number[]): Promise<boolean> {
|
||||||
|
|
||||||
return teamIds.reduce(reduceFn, Promise.resolve(false));
|
const getMembership = async (teamId: number) => {
|
||||||
|
try {
|
||||||
|
const {state} = await this.api.get<TeamMembership>(`/teams/${teamId}/memberships/${username}`);
|
||||||
|
return state === 'active';
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const teamId of teamIds) {
|
||||||
|
if (await getMembership(teamId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public isMemberBySlug(username: string, teamSlugs: string[]): Promise<boolean> {
|
return false;
|
||||||
return this.fetchAll().
|
}
|
||||||
then(teams => teams.filter(team => teamSlugs.includes(team.slug)).map(team => team.id)).
|
|
||||||
then(teamIds => this.isMemberById(username, teamIds)).
|
/**
|
||||||
catch(() => false);
|
* Check whether the given username is a member of the teams specified by the team slugs.
|
||||||
|
* @param username The username to check for in the teams.
|
||||||
|
* @param teamSlugs A collection of slugs that represent the teams to check for the the username.
|
||||||
|
* @returns a Promise that resolves to `true` if the usernane is a member of at least one of the specified teams.
|
||||||
|
*/
|
||||||
|
public async isMemberBySlug(username: string, teamSlugs: string[]): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const teams = await this.fetchAll();
|
||||||
|
const teamIds = teams.filter(team => teamSlugs.includes(team.slug)).map(team => team.id);
|
||||||
|
return await this.isMemberById(username, teamIds);
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
export const runTests = (specFiles: string[], helpers?: string[]) => {
|
// We can't use `import...from` here, because of the following mess:
|
||||||
// We can't use `import` here, because of the following mess:
|
|
||||||
// - GitHub project `jasmine/jasmine` is `jasmine-core` on npm and its typings `@types/jasmine`.
|
// - GitHub project `jasmine/jasmine` is `jasmine-core` on npm and its typings `@types/jasmine`.
|
||||||
// - GitHub project `jasmine/jasmine-npm` is `jasmine` on npm and has no typings.
|
// - GitHub project `jasmine/jasmine-npm` is `jasmine` on npm and has no typings.
|
||||||
//
|
//
|
||||||
// Using `import...from 'jasmine'` here, would import from `@types/jasmine` (which refers to the
|
// Using `import...from 'jasmine'` here, would import from `@types/jasmine` (which refers to the
|
||||||
// `jasmine-core` module and the `jasmine` module).
|
// `jasmine-core` module and the `jasmine` module).
|
||||||
// tslint:disable-next-line: no-var-requires variable-name
|
import Jasmine = require('jasmine');
|
||||||
const Jasmine = require('jasmine');
|
import 'source-map-support/register';
|
||||||
|
|
||||||
|
export const runTests = (specFiles: string[]) => {
|
||||||
const config = {
|
const config = {
|
||||||
helpers,
|
|
||||||
random: true,
|
random: true,
|
||||||
spec_files: specFiles,
|
spec_files: specFiles,
|
||||||
stopSpecOnExpectationFailure: true,
|
stopSpecOnExpectationFailure: true,
|
||||||
@ -16,7 +16,7 @@ export const runTests = (specFiles: string[], helpers?: string[]) => {
|
|||||||
|
|
||||||
process.on('unhandledRejection', (reason: any) => console.log('Unhandled rejection:', reason));
|
process.on('unhandledRejection', (reason: any) => console.log('Unhandled rejection:', reason));
|
||||||
|
|
||||||
const runner = new Jasmine();
|
const runner = new Jasmine({});
|
||||||
runner.loadConfig(config);
|
runner.loadConfig(config);
|
||||||
runner.onComplete((passed: boolean) => process.exit(passed ? 0 : 1));
|
runner.onComplete((passed: boolean) => process.exit(passed ? 0 : 1));
|
||||||
runner.execute();
|
runner.execute();
|
||||||
|
@ -1,17 +1,98 @@
|
|||||||
// Functions
|
import {basename, resolve as resolvePath} from 'path';
|
||||||
export const assertNotMissingOrEmpty = (name: string, value: string | null | undefined) => {
|
import {SHORT_SHA_LEN} from './constants';
|
||||||
if (!value) {
|
|
||||||
throw new Error(`Missing or empty required parameter '${name}'!`);
|
/**
|
||||||
|
* Shorten a SHA to make it more readable
|
||||||
|
* @param sha The SHA to shorten.
|
||||||
|
*/
|
||||||
|
export function computeShortSha(sha: string) {
|
||||||
|
return sha.substr(0, SHORT_SHA_LEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the path for a downloaded artifact file.
|
||||||
|
* @param downloadsDir The directory where artifacts are downloaded
|
||||||
|
* @param pr The PR associated with this artifact.
|
||||||
|
* @param sha The SHA associated with the build for this artifact.
|
||||||
|
* @param artifactPath The path to the artifact on CircleCI.
|
||||||
|
* @returns The fully resolved location for the specified downloaded artifact.
|
||||||
|
*/
|
||||||
|
export function computeArtifactDownloadPath(downloadsDir: string, pr: number, sha: string, artifactPath: string) {
|
||||||
|
return resolvePath(downloadsDir, `${pr}-${computeShortSha(sha)}-${basename(artifactPath)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the PR number and latest commit SHA from a downloaded file path.
|
||||||
|
* @param downloadPath the path to the downloaded file.
|
||||||
|
* @returns An object whose keys are the PR and SHA extracted from the file path.
|
||||||
|
*/
|
||||||
|
export function getPrInfoFromDownloadPath(downloadPath: string) {
|
||||||
|
const file = basename(downloadPath);
|
||||||
|
const [pr, sha] = file.split('-');
|
||||||
|
return {pr: +pr, sha};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that a value is true.
|
||||||
|
* @param value The value to assert.
|
||||||
|
* @param message The message if the value is not true.
|
||||||
|
*/
|
||||||
|
export function assert(value: boolean, message: string) {
|
||||||
|
if (!value) {
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that a parameter is not equal to "".
|
||||||
|
* @param name The name of the parameter.
|
||||||
|
* @param value The value of the parameter.
|
||||||
|
*/
|
||||||
|
export const assertNotMissingOrEmpty = (name: string, value: string | null | undefined) => {
|
||||||
|
assert(!!value, `Missing or empty required parameter '${name}'!`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an environment variable.
|
||||||
|
* @param name The name of the environment variable.
|
||||||
|
* @param isOptional True if the variable is optional.
|
||||||
|
* @returns The value of the variable or "" if it is optional and falsy.
|
||||||
|
* @throws `Error` if the variable is falsy and not optional.
|
||||||
|
*/
|
||||||
export const getEnvVar = (name: string, isOptional = false): string => {
|
export const getEnvVar = (name: string, isOptional = false): string => {
|
||||||
const value = process.env[name];
|
const value = process.env[name];
|
||||||
|
|
||||||
if (!isOptional && !value) {
|
if (!isOptional && !value) {
|
||||||
console.error(`ERROR: Missing required environment variable '${name}'!`);
|
try {
|
||||||
|
throw new Error(`ERROR: Missing required environment variable '${name}'!`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error.stack);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return value || '';
|
return value || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A basic logger implementation.
|
||||||
|
* Delegates to `console`, but prepends each message with the current date and specified scope (i.e caller).
|
||||||
|
*/
|
||||||
|
export class Logger {
|
||||||
|
private padding = ' '.repeat(20 - this.scope.length);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new `Logger` instance for the specified `scope`.
|
||||||
|
* @param scope The logger's scope (added to all messages).
|
||||||
|
*/
|
||||||
|
constructor(private scope: string) {}
|
||||||
|
|
||||||
|
public error(...args: any[]) { this.callMethod('error', args); }
|
||||||
|
public info(...args: any[]) { this.callMethod('info', args); }
|
||||||
|
public log(...args: any[]) { this.callMethod('log', args); }
|
||||||
|
public warn(...args: any[]) { this.callMethod('warn', args); }
|
||||||
|
|
||||||
|
private callMethod(method: 'error' | 'info' | 'log' | 'warn', args: any[]) {
|
||||||
|
console[method](`[${new Date()}]`, `${this.scope}:${this.padding}`, ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -4,13 +4,16 @@ import {EventEmitter} from 'events';
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as shell from 'shelljs';
|
import * as shell from 'shelljs';
|
||||||
import {HIDDEN_DIR_PREFIX, SHORT_SHA_LEN} from '../common/constants';
|
import {HIDDEN_DIR_PREFIX} from '../common/constants';
|
||||||
import {assertNotMissingOrEmpty} from '../common/utils';
|
import {assertNotMissingOrEmpty, computeShortSha, Logger} from '../common/utils';
|
||||||
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events';
|
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events';
|
||||||
import {UploadError} from './upload-error';
|
import {PreviewServerError} from './preview-error';
|
||||||
|
|
||||||
// Classes
|
// Classes
|
||||||
export class BuildCreator extends EventEmitter {
|
export class BuildCreator extends EventEmitter {
|
||||||
|
|
||||||
|
private logger = new Logger('BuildCreator');
|
||||||
|
|
||||||
// Constructor
|
// Constructor
|
||||||
constructor(protected buildsDir: string) {
|
constructor(protected buildsDir: string) {
|
||||||
super();
|
super();
|
||||||
@ -18,9 +21,9 @@ export class BuildCreator extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Methods - Public
|
// Methods - Public
|
||||||
public create(pr: string, sha: string, archivePath: string, isPublic: boolean): Promise<void> {
|
public create(pr: number, sha: string, archivePath: string, isPublic: boolean): Promise<void> {
|
||||||
// Use only part of the SHA for more readable URLs.
|
// Use only part of the SHA for more readable URLs.
|
||||||
sha = sha.substr(0, SHORT_SHA_LEN);
|
sha = computeShortSha(sha);
|
||||||
|
|
||||||
const {newPrDir: prDir} = this.getCandidatePrDirs(pr, isPublic);
|
const {newPrDir: prDir} = this.getCandidatePrDirs(pr, isPublic);
|
||||||
const shaDir = path.join(prDir, sha);
|
const shaDir = path.join(prDir, sha);
|
||||||
@ -33,7 +36,7 @@ export class BuildCreator extends EventEmitter {
|
|||||||
then(([prDirExisted, shaDirExisted]) => {
|
then(([prDirExisted, shaDirExisted]) => {
|
||||||
if (shaDirExisted) {
|
if (shaDirExisted) {
|
||||||
const publicOrNot = isPublic ? 'public' : 'non-public';
|
const publicOrNot = isPublic ? 'public' : 'non-public';
|
||||||
throw new UploadError(409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`);
|
throw new PreviewServerError(409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
dirToRemoveOnError = prDirExisted ? shaDir : prDir;
|
dirToRemoveOnError = prDirExisted ? shaDir : prDir;
|
||||||
@ -49,15 +52,15 @@ export class BuildCreator extends EventEmitter {
|
|||||||
shell.rm('-rf', dirToRemoveOnError);
|
shell.rm('-rf', dirToRemoveOnError);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(err instanceof UploadError)) {
|
if (!(err instanceof PreviewServerError)) {
|
||||||
err = new UploadError(500, `Error while uploading to directory: ${shaDir}\n${err}`);
|
err = new PreviewServerError(500, `Error while creating preview at: ${shaDir}\n${err}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public updatePrVisibility(pr: string, makePublic: boolean): Promise<boolean> {
|
public updatePrVisibility(pr: number, makePublic: boolean): Promise<boolean> {
|
||||||
const {oldPrDir: otherVisPrDir, newPrDir: targetVisPrDir} = this.getCandidatePrDirs(pr, makePublic);
|
const {oldPrDir: otherVisPrDir, newPrDir: targetVisPrDir} = this.getCandidatePrDirs(pr, makePublic);
|
||||||
|
|
||||||
return Promise.
|
return Promise.
|
||||||
@ -68,7 +71,8 @@ export class BuildCreator extends EventEmitter {
|
|||||||
return false;
|
return false;
|
||||||
} else if (targetVisPrDirExisted) {
|
} else if (targetVisPrDirExisted) {
|
||||||
// Error: Directories for both visibilities exist.
|
// Error: Directories for both visibilities exist.
|
||||||
throw new UploadError(409, `Request to move '${otherVisPrDir}' to existing directory '${targetVisPrDir}'.`);
|
throw new PreviewServerError(409,
|
||||||
|
`Request to move '${otherVisPrDir}' to existing directory '${targetVisPrDir}'.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Visibility change: Moving `otherVisPrDir` to `targetVisPrDir`.
|
// Visibility change: Moving `otherVisPrDir` to `targetVisPrDir`.
|
||||||
@ -79,8 +83,8 @@ export class BuildCreator extends EventEmitter {
|
|||||||
then(() => true);
|
then(() => true);
|
||||||
}).
|
}).
|
||||||
catch(err => {
|
catch(err => {
|
||||||
if (!(err instanceof UploadError)) {
|
if (!(err instanceof PreviewServerError)) {
|
||||||
err = new UploadError(500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\n${err}`);
|
err = new PreviewServerError(500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\n${err}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw err;
|
throw err;
|
||||||
@ -102,7 +106,7 @@ export class BuildCreator extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (stderr) {
|
if (stderr) {
|
||||||
console.warn(stderr);
|
this.logger.warn(stderr);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -116,9 +120,9 @@ export class BuildCreator extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getCandidatePrDirs(pr: string, isPublic: boolean) {
|
protected getCandidatePrDirs(pr: number, isPublic: boolean): {oldPrDir: string, newPrDir: string} {
|
||||||
const hiddenPrDir = path.join(this.buildsDir, HIDDEN_DIR_PREFIX + pr);
|
const hiddenPrDir = path.join(this.buildsDir, HIDDEN_DIR_PREFIX + pr);
|
||||||
const publicPrDir = path.join(this.buildsDir, pr);
|
const publicPrDir = path.join(this.buildsDir, `${pr}`);
|
||||||
|
|
||||||
const oldPrDir = isPublic ? hiddenPrDir : publicPrDir;
|
const oldPrDir = isPublic ? hiddenPrDir : publicPrDir;
|
||||||
const newPrDir = isPublic ? publicPrDir : hiddenPrDir;
|
const newPrDir = isPublic ? publicPrDir : hiddenPrDir;
|
@ -0,0 +1,83 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
import {dirname} from 'path';
|
||||||
|
import {mkdir} from 'shelljs';
|
||||||
|
import {promisify} from 'util';
|
||||||
|
import {CircleCiApi} from '../common/circle-ci-api';
|
||||||
|
import {assert, assertNotMissingOrEmpty, computeArtifactDownloadPath, Logger} from '../common/utils';
|
||||||
|
import {PreviewServerError} from './preview-error';
|
||||||
|
|
||||||
|
export interface GithubInfo {
|
||||||
|
org: string;
|
||||||
|
pr: number;
|
||||||
|
repo: string;
|
||||||
|
sha: string;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper that can get information about builds and download build artifacts.
|
||||||
|
*/
|
||||||
|
export class BuildRetriever {
|
||||||
|
private logger = new Logger('BuildRetriever');
|
||||||
|
constructor(private api: CircleCiApi, private downloadSizeLimit: number, private downloadDir: string) {
|
||||||
|
assert(downloadSizeLimit > 0, 'Invalid parameter "downloadSizeLimit" should be a number greater than 0.');
|
||||||
|
assertNotMissingOrEmpty('downloadDir', downloadDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get GitHub information about a build
|
||||||
|
* @param buildNum The number of the build for which to retrieve the info.
|
||||||
|
* @returns The Github org, repo, PR and latest SHA for the specified build.
|
||||||
|
*/
|
||||||
|
public async getGithubInfo(buildNum: number): Promise<GithubInfo> {
|
||||||
|
const buildInfo = await this.api.getBuildInfo(buildNum);
|
||||||
|
const githubInfo: GithubInfo = {
|
||||||
|
org: buildInfo.username,
|
||||||
|
pr: getPrFromBranch(buildInfo.branch),
|
||||||
|
repo: buildInfo.reponame,
|
||||||
|
sha: buildInfo.vcs_revision,
|
||||||
|
success: !buildInfo.failed,
|
||||||
|
};
|
||||||
|
return githubInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a request to the given URL for a build artifact and store it locally.
|
||||||
|
* @param buildNum the number of the CircleCI build whose artifact we want to download.
|
||||||
|
* @param pr the number of the PR that triggered the CircleCI build.
|
||||||
|
* @param sha the commit in the PR that triggered the CircleCI build.
|
||||||
|
* @param artifactPath the path on CircleCI where the artifact was stored.
|
||||||
|
* @returns A promise to the file path where the downloaded file was stored.
|
||||||
|
*/
|
||||||
|
public async downloadBuildArtifact(buildNum: number, pr: number, sha: string, artifactPath: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const outPath = computeArtifactDownloadPath(this.downloadDir, pr, sha, artifactPath);
|
||||||
|
const downloadExists = await new Promise(resolve => fs.exists(outPath, exists => resolve(exists)));
|
||||||
|
if (!downloadExists) {
|
||||||
|
const url = await this.api.getBuildArtifactUrl(buildNum, artifactPath);
|
||||||
|
const response = await fetch(url, {size: this.downloadSizeLimit});
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new PreviewServerError(response.status, `Error ${response.status} - ${response.statusText}`);
|
||||||
|
}
|
||||||
|
const buffer = await response.buffer();
|
||||||
|
mkdir('-p', dirname(outPath));
|
||||||
|
await promisify(fs.writeFile)(outPath, buffer);
|
||||||
|
}
|
||||||
|
return outPath;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(error);
|
||||||
|
const status = (error.type === 'max-size') ? 413 : 500;
|
||||||
|
throw new PreviewServerError(status, `CircleCI artifact download failed (${error.message || error})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPrFromBranch(branch: string): number {
|
||||||
|
// CircleCI only exposes PR numbers via the `branch` field :-(
|
||||||
|
const match = /^pull\/(\d+)$/.exec(branch);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`No PR found in branch field: ${branch}`);
|
||||||
|
}
|
||||||
|
return +match[1];
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
import {GithubPullRequests, PullRequest} from '../common/github-pull-requests';
|
||||||
|
import {GithubTeams} from '../common/github-teams';
|
||||||
|
import {assertNotMissingOrEmpty} from '../common/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper to verify whether builds are trusted.
|
||||||
|
*/
|
||||||
|
export class BuildVerifier {
|
||||||
|
/**
|
||||||
|
* Construct a new BuildVerifier instance.
|
||||||
|
* @param prs A helper to access PR information.
|
||||||
|
* @param teams A helper to access Github team information.
|
||||||
|
* @param allowedTeamSlugs The teams that are trusted.
|
||||||
|
* @param trustedPrLabel The github label that indicates that a PR is trusted.
|
||||||
|
*/
|
||||||
|
constructor(protected prs: GithubPullRequests, protected teams: GithubTeams,
|
||||||
|
protected allowedTeamSlugs: string[], protected trustedPrLabel: string) {
|
||||||
|
assertNotMissingOrEmpty('allowedTeamSlugs', allowedTeamSlugs && allowedTeamSlugs.join(''));
|
||||||
|
assertNotMissingOrEmpty('trustedPrLabel', trustedPrLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a PR contains files that are significant to the build.
|
||||||
|
* @param pr The number of the PR to check
|
||||||
|
* @param significantFilePattern A regex that selects files that are significant.
|
||||||
|
*/
|
||||||
|
public async getSignificantFilesChanged(pr: number, significantFilePattern: RegExp): Promise<boolean> {
|
||||||
|
const files = await this.prs.fetchFiles(pr);
|
||||||
|
return files.some(file => significantFilePattern.test(file.filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a PR is trusted.
|
||||||
|
* @param pr The number of the PR to check.
|
||||||
|
* @returns true if the PR is trusted.
|
||||||
|
*/
|
||||||
|
public async getPrIsTrusted(pr: number): Promise<boolean> {
|
||||||
|
const prInfo = await this.prs.fetch(pr);
|
||||||
|
return this.hasLabel(prInfo, this.trustedPrLabel) ||
|
||||||
|
(await this.teams.isMemberBySlug(prInfo.user.login, this.allowedTeamSlugs));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected hasLabel(prInfo: PullRequest, label: string): boolean {
|
||||||
|
return prInfo.labels.some(labelObj => labelObj.name === label);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
// Imports
|
||||||
|
import {AIO_DOWNLOADS_DIR} from '../common/constants';
|
||||||
|
import {
|
||||||
|
AIO_ARTIFACT_MAX_SIZE,
|
||||||
|
AIO_ARTIFACT_PATH,
|
||||||
|
AIO_BUILDS_DIR,
|
||||||
|
AIO_CIRCLE_CI_TOKEN,
|
||||||
|
AIO_DOMAIN_NAME,
|
||||||
|
AIO_GITHUB_ORGANIZATION,
|
||||||
|
AIO_GITHUB_REPO,
|
||||||
|
AIO_GITHUB_TEAM_SLUGS,
|
||||||
|
AIO_GITHUB_TOKEN,
|
||||||
|
AIO_PREVIEW_SERVER_HOSTNAME,
|
||||||
|
AIO_PREVIEW_SERVER_PORT,
|
||||||
|
AIO_SIGNIFICANT_FILES_PATTERN,
|
||||||
|
AIO_TRUSTED_PR_LABEL,
|
||||||
|
} from '../common/env-variables';
|
||||||
|
import {PreviewServerFactory} from './preview-server-factory';
|
||||||
|
|
||||||
|
// Run
|
||||||
|
_main();
|
||||||
|
|
||||||
|
// Functions
|
||||||
|
function _main(): void {
|
||||||
|
PreviewServerFactory
|
||||||
|
.create({
|
||||||
|
buildArtifactPath: AIO_ARTIFACT_PATH,
|
||||||
|
buildsDir: AIO_BUILDS_DIR,
|
||||||
|
circleCiToken: AIO_CIRCLE_CI_TOKEN,
|
||||||
|
domainName: AIO_DOMAIN_NAME,
|
||||||
|
downloadSizeLimit: AIO_ARTIFACT_MAX_SIZE,
|
||||||
|
downloadsDir: AIO_DOWNLOADS_DIR,
|
||||||
|
githubOrg: AIO_GITHUB_ORGANIZATION,
|
||||||
|
githubRepo: AIO_GITHUB_REPO,
|
||||||
|
githubTeamSlugs: AIO_GITHUB_TEAM_SLUGS.split(','),
|
||||||
|
githubToken: AIO_GITHUB_TOKEN,
|
||||||
|
significantFilesPattern: AIO_SIGNIFICANT_FILES_PATTERN,
|
||||||
|
trustedPrLabel: AIO_TRUSTED_PR_LABEL,
|
||||||
|
})
|
||||||
|
.listen(AIO_PREVIEW_SERVER_PORT, AIO_PREVIEW_SERVER_HOSTNAME);
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
// Classes
|
// Classes
|
||||||
export class UploadError extends Error {
|
export class PreviewServerError extends Error {
|
||||||
// Constructor
|
// Constructor
|
||||||
constructor(public status: number = 500, message?: string) {
|
constructor(public status: number = 500, message?: string) {
|
||||||
super(message);
|
super(message);
|
||||||
Object.setPrototypeOf(this, UploadError.prototype);
|
Object.setPrototypeOf(this, PreviewServerError.prototype);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,210 @@
|
|||||||
|
// Imports
|
||||||
|
import * as bodyParser from 'body-parser';
|
||||||
|
import * as express from 'express';
|
||||||
|
import * as http from 'http';
|
||||||
|
import {AddressInfo} from 'net';
|
||||||
|
import {CircleCiApi} from '../common/circle-ci-api';
|
||||||
|
import {GithubApi} from '../common/github-api';
|
||||||
|
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||||
|
import {GithubTeams} from '../common/github-teams';
|
||||||
|
import {assert, assertNotMissingOrEmpty, Logger} from '../common/utils';
|
||||||
|
import {BuildCreator} from './build-creator';
|
||||||
|
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events';
|
||||||
|
import {BuildRetriever} from './build-retriever';
|
||||||
|
import {BuildVerifier} from './build-verifier';
|
||||||
|
import {respondWithError, throwRequestError} from './utils';
|
||||||
|
|
||||||
|
const AIO_PREVIEW_JOB = 'aio_preview';
|
||||||
|
|
||||||
|
// Interfaces - Types
|
||||||
|
export interface PreviewServerConfig {
|
||||||
|
downloadsDir: string;
|
||||||
|
downloadSizeLimit: number;
|
||||||
|
buildArtifactPath: string;
|
||||||
|
buildsDir: string;
|
||||||
|
domainName: string;
|
||||||
|
githubOrg: string;
|
||||||
|
githubRepo: string;
|
||||||
|
githubTeamSlugs: string[];
|
||||||
|
circleCiToken: string;
|
||||||
|
githubToken: string;
|
||||||
|
significantFilesPattern: string;
|
||||||
|
trustedPrLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const logger = new Logger('PreviewServer');
|
||||||
|
|
||||||
|
// Classes
|
||||||
|
export class PreviewServerFactory {
|
||||||
|
// Methods - Public
|
||||||
|
public static create(cfg: PreviewServerConfig): http.Server {
|
||||||
|
assertNotMissingOrEmpty('domainName', cfg.domainName);
|
||||||
|
|
||||||
|
const circleCiApi = new CircleCiApi(cfg.githubOrg, cfg.githubRepo, cfg.circleCiToken);
|
||||||
|
const githubApi = new GithubApi(cfg.githubToken);
|
||||||
|
const prs = new GithubPullRequests(githubApi, cfg.githubOrg, cfg.githubRepo);
|
||||||
|
const teams = new GithubTeams(githubApi, cfg.githubOrg);
|
||||||
|
|
||||||
|
const buildRetriever = new BuildRetriever(circleCiApi, cfg.downloadSizeLimit, cfg.downloadsDir);
|
||||||
|
const buildVerifier = new BuildVerifier(prs, teams, cfg.githubTeamSlugs, cfg.trustedPrLabel);
|
||||||
|
const buildCreator = PreviewServerFactory.createBuildCreator(prs, cfg.buildsDir, cfg.domainName);
|
||||||
|
|
||||||
|
const middleware = PreviewServerFactory.createMiddleware(buildRetriever, buildVerifier, buildCreator, cfg);
|
||||||
|
const httpServer = http.createServer(middleware as any);
|
||||||
|
|
||||||
|
httpServer.on('listening', () => {
|
||||||
|
const info = httpServer.address() as AddressInfo;
|
||||||
|
logger.info(`Up and running (and listening on ${info.address}:${info.port})...`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return httpServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static createMiddleware(buildRetriever: BuildRetriever, buildVerifier: BuildVerifier,
|
||||||
|
buildCreator: BuildCreator, cfg: PreviewServerConfig): express.Express {
|
||||||
|
const middleware = express();
|
||||||
|
const jsonParser = bodyParser.json();
|
||||||
|
const significantFilesRe = new RegExp(cfg.significantFilesPattern);
|
||||||
|
|
||||||
|
// RESPOND TO IS-ALIVE PING
|
||||||
|
middleware.get(/^\/health-check\/?$/, (_req, res) => res.sendStatus(200));
|
||||||
|
|
||||||
|
// RESPOND TO CAN-HAVE-PUBLIC-PREVIEW CHECK
|
||||||
|
const canHavePublicPreviewRe = /^\/can-have-public-preview\/(\d+)\/?$/;
|
||||||
|
middleware.get(canHavePublicPreviewRe, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pr = +canHavePublicPreviewRe.exec(req.url)![1];
|
||||||
|
|
||||||
|
if (!await buildVerifier.getSignificantFilesChanged(pr, significantFilesRe)) {
|
||||||
|
// Cannot have preview: PR did not touch relevant files: `aio/` or `packages/` (except for spec files).
|
||||||
|
res.send({canHavePublicPreview: false, reason: 'No significant files touched.'});
|
||||||
|
logger.log(`PR:${pr} - Cannot have a public preview, because it did not touch any significant files.`);
|
||||||
|
} else if (!await buildVerifier.getPrIsTrusted(pr)) {
|
||||||
|
// Cannot have preview: PR not automatically verifiable as "trusted".
|
||||||
|
res.send({canHavePublicPreview: false, reason: 'Not automatically verifiable as "trusted".'});
|
||||||
|
logger.log(`PR:${pr} - Cannot have a public preview, because not automatically verifiable as "trusted".`);
|
||||||
|
} else {
|
||||||
|
// Can have preview.
|
||||||
|
res.send({canHavePublicPreview: true, reason: null});
|
||||||
|
logger.log(`PR:${pr} - Can have a public preview.`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Previewability check error', err);
|
||||||
|
respondWithError(res, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// CIRCLE_CI BUILD COMPLETE WEBHOOK
|
||||||
|
middleware.post(/^\/circle-build\/?$/, jsonParser, async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!(
|
||||||
|
req.is('json') &&
|
||||||
|
req.body &&
|
||||||
|
req.body.payload &&
|
||||||
|
req.body.payload.build_num > 0 &&
|
||||||
|
req.body.payload.build_parameters &&
|
||||||
|
req.body.payload.build_parameters.CIRCLE_JOB
|
||||||
|
)) {
|
||||||
|
throwRequestError(400, `Incorrect body content. Expected JSON`, req);
|
||||||
|
}
|
||||||
|
|
||||||
|
const job = req.body.payload.build_parameters.CIRCLE_JOB;
|
||||||
|
const buildNum = req.body.payload.build_num;
|
||||||
|
|
||||||
|
logger.log(`Build:${buildNum}, Job:${job} - processing web-hook trigger`);
|
||||||
|
|
||||||
|
if (job !== AIO_PREVIEW_JOB) {
|
||||||
|
res.sendStatus(204);
|
||||||
|
logger.log(`Build:${buildNum}, Job:${job} -`,
|
||||||
|
`Skipping preview processing because this is not the "${AIO_PREVIEW_JOB}" job.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { pr, sha, org, repo, success } = await buildRetriever.getGithubInfo(buildNum);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
res.sendStatus(204);
|
||||||
|
logger.log(`PR:${pr}, Build:${buildNum} - Skipping preview processing because this build did not succeed.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(cfg.githubOrg === org,
|
||||||
|
`Invalid webhook: expected "githubOrg" property to equal "${cfg.githubOrg}" but got "${org}".`);
|
||||||
|
assert(cfg.githubRepo === repo,
|
||||||
|
`Invalid webhook: expected "githubRepo" property to equal "${cfg.githubRepo}" but got "${repo}".`);
|
||||||
|
|
||||||
|
// Do not deploy unless this PR has touched relevant files: `aio/` or `packages/` (except for spec files)
|
||||||
|
if (!await buildVerifier.getSignificantFilesChanged(pr, significantFilesRe)) {
|
||||||
|
res.sendStatus(204);
|
||||||
|
logger.log(`PR:${pr}, Build:${buildNum} - ` +
|
||||||
|
`Skipping preview processing because this PR did not touch any significant files.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const artifactPath = await buildRetriever.downloadBuildArtifact(buildNum, pr, sha, cfg.buildArtifactPath);
|
||||||
|
const isPublic = await buildVerifier.getPrIsTrusted(pr);
|
||||||
|
await buildCreator.create(pr, sha, artifactPath, isPublic);
|
||||||
|
res.sendStatus(isPublic ? 201 : 202);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('CircleCI webhook error', err);
|
||||||
|
respondWithError(res, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GITHUB PR UPDATED WEBHOOK
|
||||||
|
middleware.post(/^\/pr-updated\/?$/, jsonParser, async (req, res) => {
|
||||||
|
const { action, number: prNo }: { action?: string, number?: number } = req.body;
|
||||||
|
const visMayHaveChanged = !action || (action === 'labeled') || (action === 'unlabeled');
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!visMayHaveChanged) {
|
||||||
|
res.sendStatus(200);
|
||||||
|
} else if (!prNo) {
|
||||||
|
throwRequestError(400, `Missing or empty 'number' field`, req);
|
||||||
|
} else {
|
||||||
|
const isPublic = await buildVerifier.getPrIsTrusted(prNo);
|
||||||
|
await buildCreator.updatePrVisibility(prNo, isPublic);
|
||||||
|
res.sendStatus(200);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('PR update hook error', err);
|
||||||
|
respondWithError(res, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ALL OTHER REQUESTS
|
||||||
|
middleware.all('*', req => throwRequestError(404, 'Unknown resource', req));
|
||||||
|
middleware.use((err: any, _req: any, res: express.Response, _next: any) => {
|
||||||
|
const statusText = http.STATUS_CODES[err.status] || '???';
|
||||||
|
logger.error(`Preview server error: ${err.status} - ${statusText}:`, err.message);
|
||||||
|
respondWithError(res, err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return middleware;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static createBuildCreator(prs: GithubPullRequests, buildsDir: string, domainName: string): BuildCreator {
|
||||||
|
const buildCreator = new BuildCreator(buildsDir);
|
||||||
|
const postPreviewsComment = (pr: number, shas: string[]) => {
|
||||||
|
const body = shas.
|
||||||
|
map(sha => `You can preview ${sha} at https://pr${pr}-${sha}.${domainName}/.`).
|
||||||
|
join('\n');
|
||||||
|
|
||||||
|
return prs.addComment(pr, body);
|
||||||
|
};
|
||||||
|
|
||||||
|
buildCreator.on(CreatedBuildEvent.type, ({pr, sha, isPublic}: CreatedBuildEvent) => {
|
||||||
|
if (isPublic) {
|
||||||
|
postPreviewsComment(pr, [sha]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
buildCreator.on(ChangedPrVisibilityEvent.type, ({pr, shas, isPublic}: ChangedPrVisibilityEvent) => {
|
||||||
|
if (isPublic && shas.length) {
|
||||||
|
postPreviewsComment(pr, shas);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return buildCreator;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
import * as express from 'express';
|
||||||
|
import {promisify} from 'util';
|
||||||
|
import {PreviewServerError} from './preview-error';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the response to report that an error has occurred.
|
||||||
|
* @param res The response to configure as an error.
|
||||||
|
* @param err The error that needs to be reported.
|
||||||
|
*/
|
||||||
|
export async function respondWithError(res: express.Response, err: any): Promise<void> {
|
||||||
|
if (!(err instanceof PreviewServerError)) {
|
||||||
|
err = new PreviewServerError(500, String((err && err.message) || err));
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(err.status);
|
||||||
|
await promisify(res.end.bind(res))(err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throw an exception that describes the given error information.
|
||||||
|
* @param status The HTTP status code include in the error.
|
||||||
|
* @param error The error message to include in the error.
|
||||||
|
* @param req The request that triggered this error.
|
||||||
|
*/
|
||||||
|
export function throwRequestError(status: number, error: string, req: express.Request): never {
|
||||||
|
const message = `${error} in request: ${req.method} ${req.originalUrl}` +
|
||||||
|
(!req.body ? '' : ` ${JSON.stringify(req.body)}`);
|
||||||
|
throw new PreviewServerError(status, message);
|
||||||
|
}
|
@ -1,87 +0,0 @@
|
|||||||
// Imports
|
|
||||||
import * as jwt from 'jsonwebtoken';
|
|
||||||
import {GithubPullRequests, PullRequest} from '../common/github-pull-requests';
|
|
||||||
import {GithubTeams} from '../common/github-teams';
|
|
||||||
import {assertNotMissingOrEmpty} from '../common/utils';
|
|
||||||
import {UploadError} from './upload-error';
|
|
||||||
|
|
||||||
// Interfaces - Types
|
|
||||||
interface JwtPayload {
|
|
||||||
slug: string;
|
|
||||||
'pull-request': number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enums
|
|
||||||
export enum BUILD_VERIFICATION_STATUS {
|
|
||||||
verifiedAndTrusted,
|
|
||||||
verifiedNotTrusted,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Classes
|
|
||||||
export class BuildVerifier {
|
|
||||||
// Properties - Protected
|
|
||||||
protected githubPullRequests: GithubPullRequests;
|
|
||||||
protected githubTeams: GithubTeams;
|
|
||||||
|
|
||||||
// Constructor
|
|
||||||
constructor(protected secret: string, githubToken: string, protected repoSlug: string, organization: string,
|
|
||||||
protected allowedTeamSlugs: string[], protected trustedPrLabel: string) {
|
|
||||||
assertNotMissingOrEmpty('secret', secret);
|
|
||||||
assertNotMissingOrEmpty('githubToken', githubToken);
|
|
||||||
assertNotMissingOrEmpty('repoSlug', repoSlug);
|
|
||||||
assertNotMissingOrEmpty('organization', organization);
|
|
||||||
assertNotMissingOrEmpty('allowedTeamSlugs', allowedTeamSlugs && allowedTeamSlugs.join(''));
|
|
||||||
assertNotMissingOrEmpty('trustedPrLabel', trustedPrLabel);
|
|
||||||
|
|
||||||
this.githubPullRequests = new GithubPullRequests(githubToken, repoSlug);
|
|
||||||
this.githubTeams = new GithubTeams(githubToken, organization);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Methods - Public
|
|
||||||
public getPrIsTrusted(pr: number): Promise<boolean> {
|
|
||||||
return Promise.resolve().
|
|
||||||
then(() => this.githubPullRequests.fetch(pr)).
|
|
||||||
then(prInfo => this.hasLabel(prInfo, this.trustedPrLabel) ||
|
|
||||||
this.githubTeams.isMemberBySlug(prInfo.user.login, this.allowedTeamSlugs));
|
|
||||||
}
|
|
||||||
|
|
||||||
public verify(expectedPr: number, authHeader: string): Promise<BUILD_VERIFICATION_STATUS> {
|
|
||||||
return Promise.resolve().
|
|
||||||
then(() => this.extractJwtString(authHeader)).
|
|
||||||
then(jwtString => this.verifyJwt(expectedPr, jwtString)).
|
|
||||||
then(jwtPayload => this.verifyPr(jwtPayload['pull-request'])).
|
|
||||||
catch(err => { throw new UploadError(403, `Error while verifying upload for PR ${expectedPr}: ${err}`); });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Methods - Protected
|
|
||||||
protected extractJwtString(input: string): string {
|
|
||||||
return input.replace(/^token +/i, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected hasLabel(prInfo: PullRequest, label: string) {
|
|
||||||
return prInfo.labels.some(labelObj => labelObj.name === label);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected verifyJwt(expectedPr: number, token: string): Promise<JwtPayload> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
jwt.verify(token, this.secret, {issuer: 'Travis CI, GmbH'}, (err, payload: JwtPayload) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err.message || err);
|
|
||||||
} else if (payload.slug !== this.repoSlug) {
|
|
||||||
reject(`jwt slug invalid. expected: ${this.repoSlug}`);
|
|
||||||
} else if (payload['pull-request'] !== expectedPr) {
|
|
||||||
reject(`jwt pull-request invalid. expected: ${expectedPr}`);
|
|
||||||
} else {
|
|
||||||
resolve(payload);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected verifyPr(pr: number): Promise<BUILD_VERIFICATION_STATUS> {
|
|
||||||
return this.getPrIsTrusted(pr).
|
|
||||||
then(isTrusted => Promise.resolve(isTrusted ?
|
|
||||||
BUILD_VERIFICATION_STATUS.verifiedAndTrusted :
|
|
||||||
BUILD_VERIFICATION_STATUS.verifiedNotTrusted));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
// Imports
|
|
||||||
import {getEnvVar} from '../common/utils';
|
|
||||||
import {BuildVerifier} from './build-verifier';
|
|
||||||
|
|
||||||
// Run
|
|
||||||
_main();
|
|
||||||
|
|
||||||
// Functions
|
|
||||||
function _main() {
|
|
||||||
const secret = 'unused';
|
|
||||||
const githubToken = getEnvVar('AIO_GITHUB_TOKEN');
|
|
||||||
const repoSlug = getEnvVar('AIO_REPO_SLUG');
|
|
||||||
const organization = getEnvVar('AIO_GITHUB_ORGANIZATION');
|
|
||||||
const allowedTeamSlugs = getEnvVar('AIO_GITHUB_TEAM_SLUGS').split(',');
|
|
||||||
const trustedPrLabel = getEnvVar('AIO_TRUSTED_PR_LABEL');
|
|
||||||
const pr = +getEnvVar('AIO_PREVERIFY_PR');
|
|
||||||
|
|
||||||
const buildVerifier = new BuildVerifier(secret, githubToken, repoSlug, organization, allowedTeamSlugs,
|
|
||||||
trustedPrLabel);
|
|
||||||
|
|
||||||
// Exit codes:
|
|
||||||
// - 0: The PR can be automatically trusted (i.e. author belongs to trusted team or PR has the "trusted PR" label).
|
|
||||||
// - 1: An error occurred.
|
|
||||||
// - 2: The PR cannot be automatically trusted.
|
|
||||||
buildVerifier.getPrIsTrusted(pr).
|
|
||||||
then(isTrusted => {
|
|
||||||
if (!isTrusted) {
|
|
||||||
console.warn(
|
|
||||||
`The PR cannot be automatically verified, because it doesn't have the "${trustedPrLabel}" label and the ` +
|
|
||||||
`the author is not an active member of any of the following teams: ${allowedTeamSlugs.join(', ')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
process.exit(isTrusted ? 0 : 2);
|
|
||||||
}).
|
|
||||||
catch(err => {
|
|
||||||
console.error(err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
// Imports
|
|
||||||
import {getEnvVar} from '../common/utils';
|
|
||||||
import {uploadServerFactory} from './upload-server-factory';
|
|
||||||
|
|
||||||
// Constants
|
|
||||||
const AIO_BUILDS_DIR = getEnvVar('AIO_BUILDS_DIR');
|
|
||||||
const AIO_DOMAIN_NAME = getEnvVar('AIO_DOMAIN_NAME');
|
|
||||||
const AIO_GITHUB_ORGANIZATION = getEnvVar('AIO_GITHUB_ORGANIZATION');
|
|
||||||
const AIO_GITHUB_TEAM_SLUGS = getEnvVar('AIO_GITHUB_TEAM_SLUGS');
|
|
||||||
const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN');
|
|
||||||
const AIO_PREVIEW_DEPLOYMENT_TOKEN = getEnvVar('AIO_PREVIEW_DEPLOYMENT_TOKEN');
|
|
||||||
const AIO_REPO_SLUG = getEnvVar('AIO_REPO_SLUG');
|
|
||||||
const AIO_TRUSTED_PR_LABEL = getEnvVar('AIO_TRUSTED_PR_LABEL');
|
|
||||||
const AIO_UPLOAD_HOSTNAME = getEnvVar('AIO_UPLOAD_HOSTNAME');
|
|
||||||
const AIO_UPLOAD_PORT = +getEnvVar('AIO_UPLOAD_PORT');
|
|
||||||
|
|
||||||
// Run
|
|
||||||
_main();
|
|
||||||
|
|
||||||
// Functions
|
|
||||||
function _main() {
|
|
||||||
uploadServerFactory.
|
|
||||||
create({
|
|
||||||
buildsDir: AIO_BUILDS_DIR,
|
|
||||||
domainName: AIO_DOMAIN_NAME,
|
|
||||||
githubOrganization: AIO_GITHUB_ORGANIZATION,
|
|
||||||
githubTeamSlugs: AIO_GITHUB_TEAM_SLUGS.split(','),
|
|
||||||
githubToken: AIO_GITHUB_TOKEN,
|
|
||||||
repoSlug: AIO_REPO_SLUG,
|
|
||||||
secret: AIO_PREVIEW_DEPLOYMENT_TOKEN,
|
|
||||||
trustedPrLabel: AIO_TRUSTED_PR_LABEL,
|
|
||||||
}).
|
|
||||||
listen(AIO_UPLOAD_PORT, AIO_UPLOAD_HOSTNAME);
|
|
||||||
}
|
|
@ -1,153 +0,0 @@
|
|||||||
// Imports
|
|
||||||
import * as bodyParser from 'body-parser';
|
|
||||||
import * as express from 'express';
|
|
||||||
import * as http from 'http';
|
|
||||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
|
||||||
import {assertNotMissingOrEmpty} from '../common/utils';
|
|
||||||
import {BuildCreator} from './build-creator';
|
|
||||||
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events';
|
|
||||||
import {BUILD_VERIFICATION_STATUS, BuildVerifier} from './build-verifier';
|
|
||||||
import {UploadError} from './upload-error';
|
|
||||||
|
|
||||||
// Constants
|
|
||||||
const AUTHORIZATION_HEADER = 'AUTHORIZATION';
|
|
||||||
const X_FILE_HEADER = 'X-FILE';
|
|
||||||
|
|
||||||
// Interfaces - Types
|
|
||||||
interface UploadServerConfig {
|
|
||||||
buildsDir: string;
|
|
||||||
domainName: string;
|
|
||||||
githubOrganization: string;
|
|
||||||
githubTeamSlugs: string[];
|
|
||||||
githubToken: string;
|
|
||||||
repoSlug: string;
|
|
||||||
secret: string;
|
|
||||||
trustedPrLabel: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Classes
|
|
||||||
class UploadServerFactory {
|
|
||||||
// Methods - Public
|
|
||||||
public create({
|
|
||||||
buildsDir,
|
|
||||||
domainName,
|
|
||||||
githubOrganization,
|
|
||||||
githubTeamSlugs,
|
|
||||||
githubToken,
|
|
||||||
repoSlug,
|
|
||||||
secret,
|
|
||||||
trustedPrLabel,
|
|
||||||
}: UploadServerConfig): http.Server {
|
|
||||||
assertNotMissingOrEmpty('domainName', domainName);
|
|
||||||
|
|
||||||
const buildVerifier = new BuildVerifier(secret, githubToken, repoSlug, githubOrganization, githubTeamSlugs,
|
|
||||||
trustedPrLabel);
|
|
||||||
const buildCreator = this.createBuildCreator(buildsDir, githubToken, repoSlug, domainName);
|
|
||||||
|
|
||||||
const middleware = this.createMiddleware(buildVerifier, buildCreator);
|
|
||||||
const httpServer = http.createServer(middleware as any);
|
|
||||||
|
|
||||||
httpServer.on('listening', () => {
|
|
||||||
const info = httpServer.address();
|
|
||||||
console.info(`Up and running (and listening on ${info.address}:${info.port})...`);
|
|
||||||
});
|
|
||||||
|
|
||||||
return httpServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Methods - Protected
|
|
||||||
protected createBuildCreator(buildsDir: string, githubToken: string, repoSlug: string,
|
|
||||||
domainName: string): BuildCreator {
|
|
||||||
const buildCreator = new BuildCreator(buildsDir);
|
|
||||||
const githubPullRequests = new GithubPullRequests(githubToken, repoSlug);
|
|
||||||
const postPreviewsComment = (pr: number, shas: string[]) => {
|
|
||||||
const body = shas.
|
|
||||||
map(sha => `You can preview ${sha} at https://pr${pr}-${sha}.${domainName}/.`).
|
|
||||||
join('\n');
|
|
||||||
|
|
||||||
return githubPullRequests.addComment(pr, body);
|
|
||||||
};
|
|
||||||
|
|
||||||
buildCreator.on(CreatedBuildEvent.type, ({pr, sha, isPublic}: CreatedBuildEvent) => {
|
|
||||||
if (isPublic) {
|
|
||||||
postPreviewsComment(pr, [sha]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
buildCreator.on(ChangedPrVisibilityEvent.type, ({pr, shas, isPublic}: ChangedPrVisibilityEvent) => {
|
|
||||||
if (isPublic && shas.length) {
|
|
||||||
postPreviewsComment(pr, shas);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return buildCreator;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected createMiddleware(buildVerifier: BuildVerifier, buildCreator: BuildCreator): express.Express {
|
|
||||||
const middleware = express();
|
|
||||||
const jsonParser = bodyParser.json();
|
|
||||||
|
|
||||||
middleware.get(/^\/create-build\/([1-9][0-9]*)\/([0-9a-f]{40})\/?$/, (req, res) => {
|
|
||||||
const pr = req.params[0];
|
|
||||||
const sha = req.params[1];
|
|
||||||
const archive = req.header(X_FILE_HEADER);
|
|
||||||
const authHeader = req.header(AUTHORIZATION_HEADER);
|
|
||||||
|
|
||||||
if (!authHeader) {
|
|
||||||
this.throwRequestError(401, `Missing or empty '${AUTHORIZATION_HEADER}' header`, req);
|
|
||||||
} else if (!archive) {
|
|
||||||
this.throwRequestError(400, `Missing or empty '${X_FILE_HEADER}' header`, req);
|
|
||||||
} else {
|
|
||||||
Promise.resolve().
|
|
||||||
then(() => buildVerifier.verify(+pr, authHeader)).
|
|
||||||
then(verStatus => verStatus === BUILD_VERIFICATION_STATUS.verifiedAndTrusted).
|
|
||||||
then(isPublic => buildCreator.create(pr, sha, archive, isPublic).
|
|
||||||
then(() => res.sendStatus(isPublic ? 201 : 202))).
|
|
||||||
catch(err => this.respondWithError(res, err));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
middleware.get(/^\/health-check\/?$/, (_req, res) => res.sendStatus(200));
|
|
||||||
middleware.post(/^\/pr-updated\/?$/, jsonParser, (req, res) => {
|
|
||||||
const {action, number: prNo}: {action?: string, number?: number} = req.body;
|
|
||||||
const visMayHaveChanged = !action || (action === 'labeled') || (action === 'unlabeled');
|
|
||||||
|
|
||||||
if (!visMayHaveChanged) {
|
|
||||||
res.sendStatus(200);
|
|
||||||
} else if (!prNo) {
|
|
||||||
this.throwRequestError(400, `Missing or empty 'number' field`, req);
|
|
||||||
} else {
|
|
||||||
Promise.resolve().
|
|
||||||
then(() => buildVerifier.getPrIsTrusted(prNo)).
|
|
||||||
then(isPublic => buildCreator.updatePrVisibility(String(prNo), isPublic)).
|
|
||||||
then(() => res.sendStatus(200)).
|
|
||||||
catch(err => this.respondWithError(res, err));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
middleware.all('*', req => this.throwRequestError(404, 'Unknown resource', req));
|
|
||||||
middleware.use((err: any, _req: any, res: express.Response, _next: any) => this.respondWithError(res, err));
|
|
||||||
|
|
||||||
return middleware;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected respondWithError(res: express.Response, err: any) {
|
|
||||||
if (!(err instanceof UploadError)) {
|
|
||||||
err = new UploadError(500, String((err && err.message) || err));
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusText = http.STATUS_CODES[err.status] || '???';
|
|
||||||
console.error(`Upload error: ${err.status} - ${statusText}`);
|
|
||||||
console.error(err.message);
|
|
||||||
|
|
||||||
res.status(err.status).end(err.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected throwRequestError(status: number, error: string, req: express.Request) {
|
|
||||||
const message = `${error} in request: ${req.method} ${req.originalUrl}` +
|
|
||||||
(!req.body ? '' : ` ${JSON.stringify(req.body)}`);
|
|
||||||
|
|
||||||
throw new UploadError(status, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exports
|
|
||||||
export const uploadServerFactory = new UploadServerFactory();
|
|
@ -1,16 +1,37 @@
|
|||||||
// Using the values below, we can fake the response of the corresponding methods in tests. This is
|
export const enum BuildNums {
|
||||||
// necessary, because the test upload-server will be running as a separate node process, so we will
|
BUILD_INFO_ERROR = 1,
|
||||||
// not have direct access to the code (e.g. for mocking).
|
BUILD_INFO_404,
|
||||||
// (See also 'lib/verify-setup/start-test-upload-server.ts'.)
|
BUILD_INFO_BUILD_FAILED,
|
||||||
|
BUILD_INFO_INVALID_GH_ORG,
|
||||||
|
BUILD_INFO_INVALID_GH_REPO,
|
||||||
|
CHANGED_FILES_ERROR,
|
||||||
|
CHANGED_FILES_404,
|
||||||
|
CHANGED_FILES_NONE,
|
||||||
|
BUILD_ARTIFACTS_ERROR,
|
||||||
|
BUILD_ARTIFACTS_404,
|
||||||
|
BUILD_ARTIFACTS_EMPTY,
|
||||||
|
BUILD_ARTIFACTS_MISSING,
|
||||||
|
DOWNLOAD_ARTIFACT_ERROR,
|
||||||
|
DOWNLOAD_ARTIFACT_404,
|
||||||
|
DOWNLOAD_ARTIFACT_TOO_BIG,
|
||||||
|
TRUST_CHECK_ERROR,
|
||||||
|
TRUST_CHECK_UNTRUSTED,
|
||||||
|
TRUST_CHECK_TRUSTED_LABEL,
|
||||||
|
TRUST_CHECK_ACTIVE_TRUSTED_USER,
|
||||||
|
TRUST_CHECK_INACTIVE_TRUSTED_USER,
|
||||||
|
}
|
||||||
|
|
||||||
/* tslint:disable: variable-name */
|
export const enum PrNums {
|
||||||
|
CHANGED_FILES_ERROR = 1,
|
||||||
|
CHANGED_FILES_404,
|
||||||
|
CHANGED_FILES_NONE,
|
||||||
|
TRUST_CHECK_ERROR,
|
||||||
|
TRUST_CHECK_UNTRUSTED,
|
||||||
|
TRUST_CHECK_TRUSTED_LABEL,
|
||||||
|
TRUST_CHECK_ACTIVE_TRUSTED_USER,
|
||||||
|
TRUST_CHECK_INACTIVE_TRUSTED_USER,
|
||||||
|
}
|
||||||
|
|
||||||
// Special values to be used as `authHeader` in `BuildVerifier#verify()`.
|
export const SHA = '1234567890'.repeat(4);
|
||||||
export const BV_verify_error = 'FAKE_VERIFICATION_ERROR';
|
export const ALT_SHA = 'abcde'.repeat(8);
|
||||||
export const BV_verify_verifiedNotTrusted = 'FAKE_VERIFIED_NOT_TRUSTED';
|
export const SIMILAR_SHA = SHA.slice(0, -1) + 'A';
|
||||||
|
|
||||||
// Special values to be used as `pr` in `BuildVerifier#getPrIsTrusted()`.
|
|
||||||
export const BV_getPrIsTrusted_error = 32203;
|
|
||||||
export const BV_getPrIsTrusted_notTrusted = 72457;
|
|
||||||
|
|
||||||
/* tslint:enable: variable-name */
|
|
||||||
|
10
aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/delete-empty.d.ts
vendored
Normal file
10
aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/delete-empty.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
declare module 'delete-empty' {
|
||||||
|
interface Options {
|
||||||
|
dryRun: boolean;
|
||||||
|
verbose: boolean;
|
||||||
|
filter: (filePath: string) => boolean;
|
||||||
|
}
|
||||||
|
export default function deleteEmpty(cwd: string, options?: Options): Promise<string[]>;
|
||||||
|
export default function deleteEmpty(cwd: string, options?: Options, callback?: (err: any, deleted: string[]) => void): void;
|
||||||
|
export function sync(cwd: string, options?: Options): string[];
|
||||||
|
}
|
@ -4,18 +4,14 @@ import * as fs from 'fs';
|
|||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as shell from 'shelljs';
|
import * as shell from 'shelljs';
|
||||||
import {HIDDEN_DIR_PREFIX, SHORT_SHA_LEN} from '../common/constants';
|
import {AIO_DOWNLOADS_DIR, HIDDEN_DIR_PREFIX} from '../common/constants';
|
||||||
import {getEnvVar} from '../common/utils';
|
import {
|
||||||
|
AIO_BUILDS_DIR,
|
||||||
// Constans
|
AIO_NGINX_PORT_HTTP,
|
||||||
const TEST_AIO_BUILDS_DIR = getEnvVar('TEST_AIO_BUILDS_DIR');
|
AIO_NGINX_PORT_HTTPS,
|
||||||
const TEST_AIO_NGINX_HOSTNAME = getEnvVar('TEST_AIO_NGINX_HOSTNAME');
|
AIO_WWW_USER,
|
||||||
const TEST_AIO_NGINX_PORT_HTTP = +getEnvVar('TEST_AIO_NGINX_PORT_HTTP');
|
} from '../common/env-variables';
|
||||||
const TEST_AIO_NGINX_PORT_HTTPS = +getEnvVar('TEST_AIO_NGINX_PORT_HTTPS');
|
import {computeShortSha, Logger} from '../common/utils';
|
||||||
const TEST_AIO_UPLOAD_HOSTNAME = getEnvVar('TEST_AIO_UPLOAD_HOSTNAME');
|
|
||||||
const TEST_AIO_UPLOAD_MAX_SIZE = +getEnvVar('TEST_AIO_UPLOAD_MAX_SIZE');
|
|
||||||
const TEST_AIO_UPLOAD_PORT = +getEnvVar('TEST_AIO_UPLOAD_PORT');
|
|
||||||
const WWW_USER = getEnvVar('AIO_WWW_USER');
|
|
||||||
|
|
||||||
// Interfaces - Types
|
// Interfaces - Types
|
||||||
export interface CmdResult { success: boolean; err: Error | null; stdout: string; stderr: string; }
|
export interface CmdResult { success: boolean; err: Error | null; stdout: string; stderr: string; }
|
||||||
@ -27,61 +23,50 @@ export type VerifyCmdResultFn = (result: CmdResult) => void;
|
|||||||
|
|
||||||
// Classes
|
// Classes
|
||||||
class Helper {
|
class Helper {
|
||||||
// Properties - Public
|
|
||||||
public get buildsDir() { return TEST_AIO_BUILDS_DIR; }
|
|
||||||
public get nginxHostname() { return TEST_AIO_NGINX_HOSTNAME; }
|
|
||||||
public get nginxPortHttp() { return TEST_AIO_NGINX_PORT_HTTP; }
|
|
||||||
public get nginxPortHttps() { return TEST_AIO_NGINX_PORT_HTTPS; }
|
|
||||||
public get uploadHostname() { return TEST_AIO_UPLOAD_HOSTNAME; }
|
|
||||||
public get uploadPort() { return TEST_AIO_UPLOAD_PORT; }
|
|
||||||
public get uploadMaxSize() { return TEST_AIO_UPLOAD_MAX_SIZE; }
|
|
||||||
public get wwwUser() { return WWW_USER; }
|
|
||||||
|
|
||||||
// Properties - Protected
|
// Properties - Protected
|
||||||
protected cleanUpFns: CleanUpFn[] = [];
|
protected cleanUpFns: CleanUpFn[] = [];
|
||||||
protected portPerScheme: {[scheme: string]: number} = {
|
protected portPerScheme: {[scheme: string]: number} = {
|
||||||
http: this.nginxPortHttp,
|
http: AIO_NGINX_PORT_HTTP,
|
||||||
https: this.nginxPortHttps,
|
https: AIO_NGINX_PORT_HTTPS,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private logger = new Logger('TestHelper');
|
||||||
|
|
||||||
// Constructor
|
// Constructor
|
||||||
constructor() {
|
constructor() {
|
||||||
shell.mkdir('-p', this.buildsDir);
|
shell.mkdir('-p', AIO_BUILDS_DIR);
|
||||||
shell.exec(`chown -R ${this.wwwUser} ${this.buildsDir}`);
|
shell.exec(`chown -R ${AIO_WWW_USER} ${AIO_BUILDS_DIR}`);
|
||||||
|
shell.mkdir('-p', AIO_DOWNLOADS_DIR);
|
||||||
|
shell.exec(`chown -R ${AIO_WWW_USER} ${AIO_DOWNLOADS_DIR}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Methods - Public
|
// Methods - Public
|
||||||
public buildExists(pr: string, sha = '', isPublic = true, legacy = false): boolean {
|
public cleanUp(): void {
|
||||||
const prDir = this.getPrDir(pr, isPublic);
|
|
||||||
const dir = !sha ? prDir : this.getShaDir(prDir, sha, legacy);
|
|
||||||
return fs.existsSync(dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
public cleanUp() {
|
|
||||||
while (this.cleanUpFns.length) {
|
while (this.cleanUpFns.length) {
|
||||||
// Clean-up fns remove themselves from the list.
|
// Clean-up fns remove themselves from the list.
|
||||||
this.cleanUpFns[0]();
|
this.cleanUpFns[0]();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fs.readdirSync(this.buildsDir).length) {
|
const leftoverDownloads = fs.readdirSync(AIO_DOWNLOADS_DIR);
|
||||||
throw new Error(`Directory '${this.buildsDir}' is not empty after clean-up.`);
|
const leftoverBuilds = fs.readdirSync(AIO_BUILDS_DIR);
|
||||||
|
|
||||||
|
if (leftoverDownloads.length) {
|
||||||
|
this.logger.log(`Downloads directory '${AIO_DOWNLOADS_DIR}' is not empty after clean-up.`, leftoverDownloads);
|
||||||
|
shell.rm('-rf', `${AIO_DOWNLOADS_DIR}/*`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (leftoverBuilds.length) {
|
||||||
|
this.logger.log(`Builds directory '${AIO_BUILDS_DIR}' is not empty after clean-up.`, leftoverBuilds);
|
||||||
|
shell.rm('-rf', `${AIO_BUILDS_DIR}/*`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (leftoverBuilds.length || leftoverDownloads.length) {
|
||||||
|
throw new Error(`Unexpected test files not cleaned up.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public createDummyArchive(pr: string, sha: string, archivePath: string): CleanUpFn {
|
public createDummyBuild(pr: number, sha: string, isPublic = true, force = false, legacy = false): CleanUpFn {
|
||||||
const inputDir = this.getShaDir(this.getPrDir(`uploaded/${pr}`, true), sha);
|
|
||||||
const cmd1 = `tar --create --gzip --directory "${inputDir}" --file "${archivePath}" .`;
|
|
||||||
const cmd2 = `chown ${this.wwwUser} ${archivePath}`;
|
|
||||||
|
|
||||||
const cleanUpTemp = this.createDummyBuild(`uploaded/${pr}`, sha, true, true);
|
|
||||||
shell.exec(cmd1);
|
|
||||||
shell.exec(cmd2);
|
|
||||||
cleanUpTemp();
|
|
||||||
|
|
||||||
return this.createCleanUpFn(() => shell.rm('-rf', archivePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
public createDummyBuild(pr: string, sha: string, isPublic = true, force = false, legacy = false): CleanUpFn {
|
|
||||||
const prDir = this.getPrDir(pr, isPublic);
|
const prDir = this.getPrDir(pr, isPublic);
|
||||||
const shaDir = this.getShaDir(prDir, sha, legacy);
|
const shaDir = this.getShaDir(prDir, sha, legacy);
|
||||||
const idxPath = path.join(shaDir, 'index.html');
|
const idxPath = path.join(shaDir, 'index.html');
|
||||||
@ -89,34 +74,21 @@ class Helper {
|
|||||||
|
|
||||||
this.writeFile(idxPath, {content: `PR: ${pr} | SHA: ${sha} | File: /index.html`}, force);
|
this.writeFile(idxPath, {content: `PR: ${pr} | SHA: ${sha} | File: /index.html`}, force);
|
||||||
this.writeFile(barPath, {content: `PR: ${pr} | SHA: ${sha} | File: /foo/bar.js`}, force);
|
this.writeFile(barPath, {content: `PR: ${pr} | SHA: ${sha} | File: /foo/bar.js`}, force);
|
||||||
shell.exec(`chown -R ${this.wwwUser} ${prDir}`);
|
shell.exec(`chown -R ${AIO_WWW_USER} ${prDir}`);
|
||||||
|
|
||||||
return this.createCleanUpFn(() => shell.rm('-rf', prDir));
|
return this.createCleanUpFn(() => shell.rm('-rf', prDir));
|
||||||
}
|
}
|
||||||
|
|
||||||
public deletePrDir(pr: string, isPublic = true) {
|
public getPrDir(pr: number, isPublic: boolean): string {
|
||||||
const prDir = this.getPrDir(pr, isPublic);
|
const prDirName = isPublic ? '' + pr : HIDDEN_DIR_PREFIX + pr;
|
||||||
|
return path.join(AIO_BUILDS_DIR, prDirName);
|
||||||
if (fs.existsSync(prDir)) {
|
|
||||||
shell.chmod('-R', 'a+w', prDir);
|
|
||||||
shell.rm('-rf', prDir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public getPrDir(pr: string, isPublic: boolean): string {
|
|
||||||
const prDirName = isPublic ? pr : HIDDEN_DIR_PREFIX + pr;
|
|
||||||
return path.join(this.buildsDir, prDirName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getShaDir(prDir: string, sha: string, legacy = false): string {
|
public getShaDir(prDir: string, sha: string, legacy = false): string {
|
||||||
return path.join(prDir, legacy ? sha : this.getShordSha(sha));
|
return path.join(prDir, legacy ? sha : computeShortSha(sha));
|
||||||
}
|
}
|
||||||
|
|
||||||
public getShordSha(sha: string): string {
|
public readBuildFile(pr: number, sha: string, relFilePath: string, isPublic = true, legacy = false): string {
|
||||||
return sha.substr(0, SHORT_SHA_LEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
public readBuildFile(pr: string, sha: string, relFilePath: string, isPublic = true, legacy = false): string {
|
|
||||||
const shaDir = this.getShaDir(this.getPrDir(pr, isPublic), sha, legacy);
|
const shaDir = this.getShaDir(this.getPrDir(pr, isPublic), sha, legacy);
|
||||||
const absFilePath = path.join(shaDir, relFilePath);
|
const absFilePath = path.join(shaDir, relFilePath);
|
||||||
return fs.readFileSync(absFilePath, 'utf8');
|
return fs.readFileSync(absFilePath, 'utf8');
|
||||||
@ -129,11 +101,11 @@ class Helper {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public runForAllSupportedSchemes(suiteFactory: TestSuiteFactory) {
|
public runForAllSupportedSchemes(suiteFactory: TestSuiteFactory): void {
|
||||||
Object.keys(this.portPerScheme).forEach(scheme => suiteFactory(scheme, this.portPerScheme[scheme]));
|
Object.keys(this.portPerScheme).forEach(scheme => suiteFactory(scheme, this.portPerScheme[scheme]));
|
||||||
}
|
}
|
||||||
|
|
||||||
public verifyResponse(status: number | [number, string], regex = /^/): VerifyCmdResultFn {
|
public verifyResponse(status: number | [number, string], regex: string | RegExp = /^/): VerifyCmdResultFn {
|
||||||
let statusCode: number;
|
let statusCode: number;
|
||||||
let statusText: string;
|
let statusText: string;
|
||||||
|
|
||||||
@ -153,9 +125,9 @@ class Helper {
|
|||||||
// Only keep the last to sections (final headers and body).
|
// Only keep the last to sections (final headers and body).
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
console.log('Stdout:', result.stdout);
|
this.logger.log('Stdout:', result.stdout);
|
||||||
console.log('Stderr:', result.stderr);
|
this.logger.error('Stderr:', result.stderr);
|
||||||
console.log('Error:', result.err);
|
this.logger.error('Error:', result.err);
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
@ -164,14 +136,14 @@ class Helper {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public writeBuildFile(pr: string, sha: string, relFilePath: string, content: string, isPublic = true,
|
public writeBuildFile(pr: number, sha: string, relFilePath: string, content: string, isPublic = true,
|
||||||
legacy = false): CleanUpFn {
|
legacy = false): void {
|
||||||
const shaDir = this.getShaDir(this.getPrDir(pr, isPublic), sha, legacy);
|
const shaDir = this.getShaDir(this.getPrDir(pr, isPublic), sha, legacy);
|
||||||
const absFilePath = path.join(shaDir, relFilePath);
|
const absFilePath = path.join(shaDir, relFilePath);
|
||||||
return this.writeFile(absFilePath, {content}, true);
|
this.writeFile(absFilePath, {content}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public writeFile(filePath: string, {content, size}: FileSpecs, force = false): CleanUpFn {
|
public writeFile(filePath: string, {content, size}: FileSpecs, force = false): void {
|
||||||
if (!force && fs.existsSync(filePath)) {
|
if (!force && fs.existsSync(filePath)) {
|
||||||
throw new Error(`Refusing to overwrite existing file '${filePath}'.`);
|
throw new Error(`Refusing to overwrite existing file '${filePath}'.`);
|
||||||
}
|
}
|
||||||
@ -189,9 +161,7 @@ class Helper {
|
|||||||
// Create a file with the specified content.
|
// Create a file with the specified content.
|
||||||
fs.writeFileSync(filePath, content || '');
|
fs.writeFileSync(filePath, content || '');
|
||||||
}
|
}
|
||||||
shell.exec(`chown ${this.wwwUser} ${filePath}`);
|
shell.exec(`chown ${AIO_WWW_USER} ${filePath}`);
|
||||||
|
|
||||||
return this.createCleanUpFn(() => shell.rm('-rf', cleanUpTarget));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Methods - Protected
|
// Methods - Protected
|
||||||
@ -210,5 +180,70 @@ class Helper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DefaultCurlOptions {
|
||||||
|
defaultMethod?: CurlOptions['method'];
|
||||||
|
defaultOptions?: CurlOptions['options'];
|
||||||
|
defaultHeaders?: CurlOptions['headers'];
|
||||||
|
defaultData?: CurlOptions['data'];
|
||||||
|
defaultExtraPath?: CurlOptions['extraPath'];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CurlOptions {
|
||||||
|
method?: string;
|
||||||
|
options?: string;
|
||||||
|
headers?: string[];
|
||||||
|
data?: any;
|
||||||
|
url?: string;
|
||||||
|
extraPath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeCurl(baseUrl: string, {
|
||||||
|
defaultMethod = 'POST',
|
||||||
|
defaultOptions = '',
|
||||||
|
defaultHeaders = ['Content-Type: application/json'],
|
||||||
|
defaultData = {},
|
||||||
|
defaultExtraPath = '',
|
||||||
|
}: DefaultCurlOptions = {}) {
|
||||||
|
return function curl({
|
||||||
|
method = defaultMethod,
|
||||||
|
options = defaultOptions,
|
||||||
|
headers = defaultHeaders,
|
||||||
|
data = defaultData,
|
||||||
|
url = baseUrl,
|
||||||
|
extraPath = defaultExtraPath,
|
||||||
|
}: CurlOptions) {
|
||||||
|
const dataString = data ? JSON.stringify(data) : '';
|
||||||
|
const cmd = `curl -iLX ${method} ` +
|
||||||
|
`${options} ` +
|
||||||
|
headers.map(header => `--header "${header}" `).join('') +
|
||||||
|
`--data '${dataString}' ` +
|
||||||
|
`${url}${extraPath}`;
|
||||||
|
return helper.runCmd(cmd);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PayloadData {
|
||||||
|
data: {
|
||||||
|
payload: {
|
||||||
|
build_num: number,
|
||||||
|
build_parameters: {
|
||||||
|
CIRCLE_JOB: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function payload(buildNum: number): PayloadData {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
payload: {
|
||||||
|
build_num: buildNum,
|
||||||
|
build_parameters: { CIRCLE_JOB: 'aio_preview' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Exports
|
// Exports
|
||||||
export const helper = new Helper();
|
export const helper = new Helper();
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
declare module jasmine {
|
||||||
|
interface Matchers {
|
||||||
|
toExistAsAFile(remove = true): boolean;
|
||||||
|
toExistAsABuild(remove = true): boolean;
|
||||||
|
toExistAsAnArtifact(remove = true): boolean;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,88 @@
|
|||||||
|
import {sync as deleteEmpty} from 'delete-empty';
|
||||||
|
import {existsSync, unlinkSync} from 'fs';
|
||||||
|
import {join} from 'path';
|
||||||
|
import {AIO_DOWNLOADS_DIR} from '../common/constants';
|
||||||
|
import {computeShortSha} from '../common/utils';
|
||||||
|
import {SHA} from './constants';
|
||||||
|
import {helper} from './helper';
|
||||||
|
|
||||||
|
function checkFile(filePath: string, remove: boolean): boolean {
|
||||||
|
const exists = existsSync(filePath);
|
||||||
|
if (exists && remove) {
|
||||||
|
// if we expected the file to exist then we remove it to prevent leftover file errors
|
||||||
|
unlinkSync(filePath);
|
||||||
|
}
|
||||||
|
return exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getArtifactPath(prNum: number, sha: string = SHA): string {
|
||||||
|
return `${AIO_DOWNLOADS_DIR}/${prNum}-${computeShortSha(sha)}-aio-snapshot.tgz`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkFiles(prNum: number, isPublic: boolean, sha: string, isLegacy: boolean, remove: boolean) {
|
||||||
|
const files = ['/index.html', '/foo/bar.js'];
|
||||||
|
const prPath = helper.getPrDir(prNum, isPublic);
|
||||||
|
const shaPath = helper.getShaDir(prPath, sha, isLegacy);
|
||||||
|
|
||||||
|
const existingFiles: string[] = [];
|
||||||
|
const missingFiles: string[] = [];
|
||||||
|
files
|
||||||
|
.map(file => join(shaPath, file))
|
||||||
|
.forEach(file => (checkFile(file, remove) ? existingFiles : missingFiles).push(file));
|
||||||
|
|
||||||
|
deleteEmpty(prPath);
|
||||||
|
|
||||||
|
return { existingFiles, missingFiles };
|
||||||
|
}
|
||||||
|
|
||||||
|
class ToExistAsAFile implements jasmine.CustomMatcher {
|
||||||
|
public compare(actual: string, remove = true): jasmine.CustomMatcherResult {
|
||||||
|
const pass = checkFile(actual, remove);
|
||||||
|
return {
|
||||||
|
message: `Expected file at "${actual}" ${pass ? 'not' : ''} to exist`,
|
||||||
|
pass,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ToExistAsAnArtifact implements jasmine.CustomMatcher {
|
||||||
|
public compare(actual: {prNum: number, sha?: string}, remove = true): jasmine.CustomMatcherResult {
|
||||||
|
const { prNum, sha = SHA } = actual;
|
||||||
|
const filePath = getArtifactPath(prNum, sha);
|
||||||
|
const pass = checkFile(filePath, remove);
|
||||||
|
return {
|
||||||
|
message: `Expected artifact "PR:${prNum}, SHA:${sha}, FILE:${filePath}" ${pass ? 'not' : '\b'} to exist`,
|
||||||
|
pass,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ToExistAsABuild implements jasmine.CustomMatcher {
|
||||||
|
public compare(actual: {prNum: number, isPublic?: boolean, sha?: string, isLegacy?: boolean}, remove = true):
|
||||||
|
jasmine.CustomMatcherResult {
|
||||||
|
const {prNum, isPublic = true, sha = SHA, isLegacy = false} = actual;
|
||||||
|
const {missingFiles} = checkFiles(prNum, isPublic, sha, isLegacy, remove);
|
||||||
|
return {
|
||||||
|
message: `Expected files for build "PR:${prNum}, SHA:${sha}" to exist:\n` +
|
||||||
|
missingFiles.map(file => ` - ${file}`).join('\n'),
|
||||||
|
pass: missingFiles.length === 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
public negativeCompare(actual: {prNum: number, isPublic?: boolean, sha?: string, isLegacy?: boolean}):
|
||||||
|
jasmine.CustomMatcherResult {
|
||||||
|
const {prNum, isPublic = true, sha = SHA, isLegacy = false} = actual;
|
||||||
|
const { existingFiles } = checkFiles(prNum, isPublic, sha, isLegacy, false);
|
||||||
|
return {
|
||||||
|
message: `Expected files for build "PR:${prNum}, SHA:${sha}" not to exist:\n` +
|
||||||
|
existingFiles.map(file => ` - ${file}`).join('\n'),
|
||||||
|
pass: existingFiles.length === 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const customMatchers = {
|
||||||
|
toExistAsABuild: () => new ToExistAsABuild(),
|
||||||
|
toExistAsAFile: () => new ToExistAsAFile(),
|
||||||
|
toExistAsAnArtifact: () => new ToExistAsAnArtifact(),
|
||||||
|
};
|
@ -0,0 +1,171 @@
|
|||||||
|
/* tslint:disable:max-line-length */
|
||||||
|
import * as nock from 'nock';
|
||||||
|
import * as tar from 'tar-stream';
|
||||||
|
import {gzipSync} from 'zlib';
|
||||||
|
import {getEnvVar, Logger} from '../common/utils';
|
||||||
|
import {BuildNums, PrNums, SHA} from './constants';
|
||||||
|
|
||||||
|
// We are using the `nock` library to fake responses from REST requests, when testing.
|
||||||
|
// This is necessary, because the test preview-server runs as a separate node process to
|
||||||
|
// the test harness, so we do not have direct access to the code (e.g. for mocking).
|
||||||
|
// (See also 'lib/verify-setup/start-test-preview-server.ts'.)
|
||||||
|
|
||||||
|
// Each of the potential requests to an external API (e.g. Github or CircleCI) are mocked
|
||||||
|
// below and return a suitable response. This is quite complicated to setup since the
|
||||||
|
// response from, say, CircleCI will affect what request is made to, say, Github.
|
||||||
|
|
||||||
|
const logger = new Logger('mock-external-apis');
|
||||||
|
|
||||||
|
const log = (...args: any[]) => {
|
||||||
|
// Filter out non-matching URL checks
|
||||||
|
if (!/^matching.+: false$/.test(args[0])) {
|
||||||
|
logger.log(...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const AIO_CIRCLE_CI_TOKEN = getEnvVar('AIO_CIRCLE_CI_TOKEN');
|
||||||
|
const AIO_GITHUB_TOKEN = getEnvVar('AIO_GITHUB_TOKEN');
|
||||||
|
|
||||||
|
const AIO_ARTIFACT_PATH = getEnvVar('AIO_ARTIFACT_PATH');
|
||||||
|
const AIO_GITHUB_ORGANIZATION = getEnvVar('AIO_GITHUB_ORGANIZATION');
|
||||||
|
const AIO_GITHUB_REPO = getEnvVar('AIO_GITHUB_REPO');
|
||||||
|
const AIO_TRUSTED_PR_LABEL = getEnvVar('AIO_TRUSTED_PR_LABEL');
|
||||||
|
const AIO_GITHUB_TEAM_SLUGS = getEnvVar('AIO_GITHUB_TEAM_SLUGS').split(',');
|
||||||
|
|
||||||
|
const ACTIVE_TRUSTED_USER = 'active-trusted-user';
|
||||||
|
const INACTIVE_TRUSTED_USER = 'inactive-trusted-user';
|
||||||
|
const UNTRUSTED_USER = 'untrusted-user';
|
||||||
|
|
||||||
|
const BASIC_BUILD_INFO = {
|
||||||
|
branch: `pull/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`,
|
||||||
|
failed: false,
|
||||||
|
reponame: AIO_GITHUB_REPO,
|
||||||
|
username: AIO_GITHUB_ORGANIZATION,
|
||||||
|
vcs_revision: SHA,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ISSUE_INFO_TRUSTED_LABEL = { labels: [{ name: AIO_TRUSTED_PR_LABEL }], user: { login: UNTRUSTED_USER } };
|
||||||
|
const ISSUE_INFO_ACTIVE_TRUSTED_USER = { labels: [], user: { login: ACTIVE_TRUSTED_USER } };
|
||||||
|
const ISSUE_INFO_INACTIVE_TRUSTED_USER = { labels: [], user: { login: INACTIVE_TRUSTED_USER } };
|
||||||
|
const ISSUE_INFO_UNTRUSTED = { labels: [], user: { login: UNTRUSTED_USER } };
|
||||||
|
const ACTIVE_STATE = { state: 'active' };
|
||||||
|
const INACTIVE_STATE = { state: 'inactive' };
|
||||||
|
|
||||||
|
const TEST_TEAM_INFO = AIO_GITHUB_TEAM_SLUGS.map((slug, index) => ({ slug, id: index }));
|
||||||
|
|
||||||
|
const CIRCLE_CI_API_HOST = 'https://circleci.com';
|
||||||
|
const CIRCLE_CI_TOKEN_PARAM = `circle-token=${AIO_CIRCLE_CI_TOKEN}`;
|
||||||
|
const ARTIFACT_1 = { path: 'artifact-1', url: `${CIRCLE_CI_API_HOST}/artifacts/artifact-1`, _urlPath: '/artifacts/artifact-1' };
|
||||||
|
const ARTIFACT_2 = { path: 'artifact-2', url: `${CIRCLE_CI_API_HOST}/artifacts/artifact-2`, _urlPath: '/artifacts/artifact-2' };
|
||||||
|
const ARTIFACT_3 = { path: 'artifact-3', url: `${CIRCLE_CI_API_HOST}/artifacts/artifact-3`, _urlPath: '/artifacts/artifact-3' };
|
||||||
|
const ARTIFACT_ERROR = { path: AIO_ARTIFACT_PATH, url: `${CIRCLE_CI_API_HOST}/artifacts/error`, _urlPath: '/artifacts/error' };
|
||||||
|
const ARTIFACT_404 = { path: AIO_ARTIFACT_PATH, url: `${CIRCLE_CI_API_HOST}/artifacts/404`, _urlPath: '/artifacts/404' };
|
||||||
|
const ARTIFACT_VALID_TRUSTED_USER = { path: AIO_ARTIFACT_PATH, url: `${CIRCLE_CI_API_HOST}/artifacts/valid/user`, _urlPath: '/artifacts/valid/user' };
|
||||||
|
const ARTIFACT_VALID_TRUSTED_LABEL = { path: AIO_ARTIFACT_PATH, url: `${CIRCLE_CI_API_HOST}/artifacts/valid/label`, _urlPath: '/artifacts/valid/label' };
|
||||||
|
const ARTIFACT_VALID_UNTRUSTED = { path: AIO_ARTIFACT_PATH, url: `${CIRCLE_CI_API_HOST}/artifacts/valid/untrusted`, _urlPath: '/artifacts/valid/untrusted' };
|
||||||
|
|
||||||
|
const CIRCLE_CI_BUILD_INFO_URL = `/api/v1.1/project/github/${AIO_GITHUB_ORGANIZATION}/${AIO_GITHUB_REPO}`;
|
||||||
|
|
||||||
|
const buildInfoUrl = (buildNum: number) => `${CIRCLE_CI_BUILD_INFO_URL}/${buildNum}?${CIRCLE_CI_TOKEN_PARAM}`;
|
||||||
|
const buildArtifactsUrl = (buildNum: number) => `${CIRCLE_CI_BUILD_INFO_URL}/${buildNum}/artifacts?${CIRCLE_CI_TOKEN_PARAM}`;
|
||||||
|
const buildInfo = (prNum: number) => ({ ...BASIC_BUILD_INFO, branch: `pull/${prNum}` });
|
||||||
|
|
||||||
|
const GITHUB_API_HOST = 'https://api.github.com';
|
||||||
|
const GITHUB_ISSUES_URL = `/repos/${AIO_GITHUB_ORGANIZATION}/${AIO_GITHUB_REPO}/issues`;
|
||||||
|
const GITHUB_PULLS_URL = `/repos/${AIO_GITHUB_ORGANIZATION}/${AIO_GITHUB_REPO}/pulls`;
|
||||||
|
const GITHUB_TEAMS_URL = `/orgs/${AIO_GITHUB_ORGANIZATION}/teams`;
|
||||||
|
|
||||||
|
const getIssueUrl = (prNum: number) => `${GITHUB_ISSUES_URL}/${prNum}`;
|
||||||
|
const getFilesUrl = (prNum: number, pageNum = 1) => `${GITHUB_PULLS_URL}/${prNum}/files?page=${pageNum}&per_page=100`;
|
||||||
|
const getCommentUrl = (prNum: number) => `${getIssueUrl(prNum)}/comments`;
|
||||||
|
const getTeamMembershipUrl = (teamId: number, username: string) => `/teams/${teamId}/memberships/${username}`;
|
||||||
|
|
||||||
|
const createArchive = (buildNum: number, prNum: number, sha: string) => {
|
||||||
|
logger.log('createArchive', buildNum, prNum, sha);
|
||||||
|
const pack = tar.pack();
|
||||||
|
pack.entry({name: 'index.html'}, `BUILD: ${buildNum} | PR: ${prNum} | SHA: ${sha} | File: /index.html`);
|
||||||
|
pack.entry({name: 'foo/bar.js'}, `BUILD: ${buildNum} | PR: ${prNum} | SHA: ${sha} | File: /foo/bar.js`);
|
||||||
|
pack.finalize();
|
||||||
|
const zip = gzipSync(pack.read());
|
||||||
|
return zip;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create request scopes
|
||||||
|
const circleCiApi = nock(CIRCLE_CI_API_HOST).log(log).persist();
|
||||||
|
const githubApi = nock(GITHUB_API_HOST).log(log).persist().matchHeader('Authorization', `token ${AIO_GITHUB_TOKEN}`);
|
||||||
|
|
||||||
|
//////////////////////////////
|
||||||
|
|
||||||
|
// GENERAL responses
|
||||||
|
githubApi.get(GITHUB_TEAMS_URL + '?page=1&per_page=100').reply(200, TEST_TEAM_INFO);
|
||||||
|
githubApi.post(getCommentUrl(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)).reply(200);
|
||||||
|
|
||||||
|
// BUILD_INFO errors
|
||||||
|
circleCiApi.get(buildInfoUrl(BuildNums.BUILD_INFO_ERROR)).replyWithError('BUILD_INFO_ERROR');
|
||||||
|
circleCiApi.get(buildInfoUrl(BuildNums.BUILD_INFO_404)).reply(404, 'BUILD_INFO_404');
|
||||||
|
circleCiApi.get(buildInfoUrl(BuildNums.BUILD_INFO_BUILD_FAILED)).reply(200, { ...BASIC_BUILD_INFO, failed: true });
|
||||||
|
circleCiApi.get(buildInfoUrl(BuildNums.BUILD_INFO_INVALID_GH_ORG)).reply(200, { ...BASIC_BUILD_INFO, username: 'bad' });
|
||||||
|
circleCiApi.get(buildInfoUrl(BuildNums.BUILD_INFO_INVALID_GH_REPO)).reply(200, { ...BASIC_BUILD_INFO, reponame: 'bad' });
|
||||||
|
|
||||||
|
// CHANGED FILE errors
|
||||||
|
circleCiApi.get(buildInfoUrl(BuildNums.CHANGED_FILES_ERROR)).reply(200, buildInfo(PrNums.CHANGED_FILES_ERROR));
|
||||||
|
githubApi.get(getFilesUrl(PrNums.CHANGED_FILES_ERROR)).replyWithError('CHANGED_FILES_ERROR');
|
||||||
|
circleCiApi.get(buildInfoUrl(BuildNums.CHANGED_FILES_404)).reply(200, buildInfo(PrNums.CHANGED_FILES_404));
|
||||||
|
githubApi.get(getFilesUrl(PrNums.CHANGED_FILES_404)).reply(404, 'CHANGED_FILES_404');
|
||||||
|
circleCiApi.get(buildInfoUrl(BuildNums.CHANGED_FILES_NONE)).reply(200, buildInfo(PrNums.CHANGED_FILES_NONE));
|
||||||
|
githubApi.get(getFilesUrl(PrNums.CHANGED_FILES_NONE)).reply(200, []);
|
||||||
|
|
||||||
|
// ARTIFACT URL errors
|
||||||
|
circleCiApi.get(buildInfoUrl(BuildNums.BUILD_ARTIFACTS_ERROR)).reply(200, buildInfo(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER));
|
||||||
|
circleCiApi.get(buildArtifactsUrl(BuildNums.BUILD_ARTIFACTS_ERROR)).replyWithError('BUILD_ARTIFACTS_ERROR');
|
||||||
|
circleCiApi.get(buildInfoUrl(BuildNums.BUILD_ARTIFACTS_404)).reply(200, buildInfo(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER));
|
||||||
|
circleCiApi.get(buildArtifactsUrl(BuildNums.BUILD_ARTIFACTS_404)).reply(404, 'BUILD_ARTIFACTS_ERROR');
|
||||||
|
circleCiApi.get(buildInfoUrl(BuildNums.BUILD_ARTIFACTS_EMPTY)).reply(200, buildInfo(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER));
|
||||||
|
circleCiApi.get(buildArtifactsUrl(BuildNums.BUILD_ARTIFACTS_EMPTY)).reply(200, []);
|
||||||
|
circleCiApi.get(buildInfoUrl(BuildNums.BUILD_ARTIFACTS_MISSING)).reply(200, buildInfo(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER));
|
||||||
|
circleCiApi.get(buildArtifactsUrl(BuildNums.BUILD_ARTIFACTS_MISSING)).reply(200, [ARTIFACT_1, ARTIFACT_2, ARTIFACT_3]);
|
||||||
|
|
||||||
|
// ARTIFACT DOWNLOAD errors
|
||||||
|
circleCiApi.get(buildInfoUrl(BuildNums.DOWNLOAD_ARTIFACT_ERROR)).reply(200, buildInfo(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER));
|
||||||
|
circleCiApi.get(buildArtifactsUrl(BuildNums.DOWNLOAD_ARTIFACT_ERROR)).reply(200, [ARTIFACT_ERROR]);
|
||||||
|
circleCiApi.get(ARTIFACT_ERROR._urlPath).replyWithError(ARTIFACT_ERROR._urlPath);
|
||||||
|
circleCiApi.get(buildInfoUrl(BuildNums.DOWNLOAD_ARTIFACT_404)).reply(200, buildInfo(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER));
|
||||||
|
circleCiApi.get(buildArtifactsUrl(BuildNums.DOWNLOAD_ARTIFACT_404)).reply(200, [ARTIFACT_404]);
|
||||||
|
circleCiApi.get(ARTIFACT_ERROR._urlPath).reply(404, ARTIFACT_ERROR._urlPath);
|
||||||
|
|
||||||
|
// TRUST CHECK errors
|
||||||
|
circleCiApi.get(buildInfoUrl(BuildNums.TRUST_CHECK_ERROR)).reply(200, buildInfo(PrNums.TRUST_CHECK_ERROR));
|
||||||
|
githubApi.get(getFilesUrl(PrNums.TRUST_CHECK_ERROR)).reply(200, [{ filename: 'aio/a' }]);
|
||||||
|
circleCiApi.get(buildArtifactsUrl(BuildNums.TRUST_CHECK_ERROR)).reply(200, [ARTIFACT_VALID_TRUSTED_USER]);
|
||||||
|
githubApi.get(getIssueUrl(PrNums.TRUST_CHECK_ERROR)).replyWithError('TRUST_CHECK_ERROR');
|
||||||
|
|
||||||
|
// ACTIVE TRUSTED USER response
|
||||||
|
circleCiApi.get(buildInfoUrl(BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)).reply(200, BASIC_BUILD_INFO);
|
||||||
|
githubApi.get(getFilesUrl(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)).reply(200, [{ filename: 'aio/a' }]);
|
||||||
|
circleCiApi.get(buildArtifactsUrl(BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)).reply(200, [ARTIFACT_VALID_TRUSTED_USER]);
|
||||||
|
circleCiApi.get(ARTIFACT_VALID_TRUSTED_USER._urlPath).reply(200, createArchive(BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, SHA));
|
||||||
|
githubApi.get(getIssueUrl(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER)).reply(200, ISSUE_INFO_ACTIVE_TRUSTED_USER);
|
||||||
|
githubApi.get(getTeamMembershipUrl(0, ACTIVE_TRUSTED_USER)).reply(200, ACTIVE_STATE);
|
||||||
|
|
||||||
|
// TRUSTED LABEL response
|
||||||
|
circleCiApi.get(buildInfoUrl(BuildNums.TRUST_CHECK_TRUSTED_LABEL)).reply(200, BASIC_BUILD_INFO);
|
||||||
|
githubApi.get(getFilesUrl(PrNums.TRUST_CHECK_TRUSTED_LABEL)).reply(200, [{ filename: 'aio/a' }]);
|
||||||
|
circleCiApi.get(buildArtifactsUrl(BuildNums.TRUST_CHECK_TRUSTED_LABEL)).reply(200, [ARTIFACT_VALID_TRUSTED_LABEL]);
|
||||||
|
circleCiApi.get(ARTIFACT_VALID_TRUSTED_LABEL._urlPath).reply(200, createArchive(BuildNums.TRUST_CHECK_TRUSTED_LABEL, PrNums.TRUST_CHECK_TRUSTED_LABEL, SHA));
|
||||||
|
githubApi.get(getIssueUrl(PrNums.TRUST_CHECK_TRUSTED_LABEL)).reply(200, ISSUE_INFO_TRUSTED_LABEL);
|
||||||
|
githubApi.get(getTeamMembershipUrl(0, ACTIVE_TRUSTED_USER)).reply(200, ACTIVE_STATE);
|
||||||
|
|
||||||
|
// INACTIVE TRUSTED USER response
|
||||||
|
circleCiApi.get(buildInfoUrl(BuildNums.TRUST_CHECK_INACTIVE_TRUSTED_USER)).reply(200, BASIC_BUILD_INFO);
|
||||||
|
githubApi.get(getFilesUrl(PrNums.TRUST_CHECK_INACTIVE_TRUSTED_USER)).reply(200, [{ filename: 'aio/a' }]);
|
||||||
|
circleCiApi.get(buildArtifactsUrl(BuildNums.TRUST_CHECK_INACTIVE_TRUSTED_USER)).reply(200, [ARTIFACT_VALID_TRUSTED_USER]);
|
||||||
|
githubApi.get(getIssueUrl(PrNums.TRUST_CHECK_INACTIVE_TRUSTED_USER)).reply(200, ISSUE_INFO_INACTIVE_TRUSTED_USER);
|
||||||
|
githubApi.get(getTeamMembershipUrl(0, INACTIVE_TRUSTED_USER)).reply(200, INACTIVE_STATE);
|
||||||
|
|
||||||
|
// UNTRUSTED reponse
|
||||||
|
circleCiApi.get(buildInfoUrl(BuildNums.TRUST_CHECK_UNTRUSTED)).reply(200, buildInfo(PrNums.TRUST_CHECK_UNTRUSTED));
|
||||||
|
githubApi.get(getFilesUrl(PrNums.TRUST_CHECK_UNTRUSTED)).reply(200, [{ filename: 'aio/a' }]);
|
||||||
|
circleCiApi.get(buildArtifactsUrl(BuildNums.TRUST_CHECK_UNTRUSTED)).reply(200, [ARTIFACT_VALID_UNTRUSTED]);
|
||||||
|
circleCiApi.get(ARTIFACT_VALID_UNTRUSTED._urlPath).reply(200, createArchive(BuildNums.TRUST_CHECK_UNTRUSTED, PrNums.TRUST_CHECK_UNTRUSTED, SHA));
|
||||||
|
githubApi.get(getIssueUrl(PrNums.TRUST_CHECK_UNTRUSTED)).reply(200, ISSUE_INFO_UNTRUSTED);
|
||||||
|
githubApi.get(getTeamMembershipUrl(0, UNTRUSTED_USER)).reply(404);
|
@ -1,17 +1,23 @@
|
|||||||
// Imports
|
// Imports
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import {rm} from 'shelljs';
|
||||||
|
import {AIO_BUILDS_DIR, AIO_NGINX_HOSTNAME, AIO_NGINX_PORT_HTTP, AIO_NGINX_PORT_HTTPS} from '../common/env-variables';
|
||||||
|
import {computeShortSha} from '../common/utils';
|
||||||
|
import {PrNums} from './constants';
|
||||||
import {helper as h} from './helper';
|
import {helper as h} from './helper';
|
||||||
|
import {customMatchers} from './jasmine-custom-matchers';
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
describe(`nginx`, () => {
|
describe(`nginx`, () => {
|
||||||
|
|
||||||
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000);
|
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000);
|
||||||
|
beforeEach(() => jasmine.addMatchers(customMatchers));
|
||||||
afterEach(() => h.cleanUp());
|
afterEach(() => h.cleanUp());
|
||||||
|
|
||||||
|
|
||||||
it('should redirect HTTP to HTTPS', done => {
|
it('should redirect HTTP to HTTPS', done => {
|
||||||
const httpHost = `${h.nginxHostname}:${h.nginxPortHttp}`;
|
const httpHost = `${AIO_NGINX_HOSTNAME}:${AIO_NGINX_PORT_HTTP}`;
|
||||||
const httpsHost = `${h.nginxHostname}:${h.nginxPortHttps}`;
|
const httpsHost = `${AIO_NGINX_HOSTNAME}:${AIO_NGINX_PORT_HTTPS}`;
|
||||||
const urlMap = {
|
const urlMap = {
|
||||||
[`http://${httpHost}/`]: `https://${httpsHost}/`,
|
[`http://${httpHost}/`]: `https://${httpsHost}/`,
|
||||||
[`http://${httpHost}/foo`]: `https://${httpsHost}/foo`,
|
[`http://${httpHost}/foo`]: `https://${httpsHost}/foo`,
|
||||||
@ -32,13 +38,13 @@ describe(`nginx`, () => {
|
|||||||
|
|
||||||
|
|
||||||
h.runForAllSupportedSchemes((scheme, port) => describe(`(on ${scheme.toUpperCase()})`, () => {
|
h.runForAllSupportedSchemes((scheme, port) => describe(`(on ${scheme.toUpperCase()})`, () => {
|
||||||
const hostname = h.nginxHostname;
|
const hostname = AIO_NGINX_HOSTNAME;
|
||||||
const host = `${hostname}:${port}`;
|
const host = `${hostname}:${port}`;
|
||||||
const pr = '9';
|
const pr = 9;
|
||||||
const sha9 = '9'.repeat(40);
|
const sha9 = '9'.repeat(40);
|
||||||
const sha0 = '0'.repeat(40);
|
const sha0 = '0'.repeat(40);
|
||||||
const shortSha9 = h.getShordSha(sha9);
|
const shortSha9 = computeShortSha(sha9);
|
||||||
const shortSha0 = h.getShordSha(sha0);
|
const shortSha0 = computeShortSha(sha0);
|
||||||
|
|
||||||
|
|
||||||
describe(`pr<pr>-<sha>.${host}/*`, () => {
|
describe(`pr<pr>-<sha>.${host}/*`, () => {
|
||||||
@ -50,6 +56,11 @@ describe(`nginx`, () => {
|
|||||||
h.createDummyBuild(pr, sha0);
|
h.createDummyBuild(pr, sha0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
expect({ prNum: pr, sha: sha9 }).toExistAsABuild();
|
||||||
|
expect({ prNum: pr, sha: sha0 }).toExistAsABuild();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should return /index.html', done => {
|
it('should return /index.html', done => {
|
||||||
const origin = `${scheme}://pr${pr}-${shortSha9}.${host}`;
|
const origin = `${scheme}://pr${pr}-${shortSha9}.${host}`;
|
||||||
@ -63,17 +74,19 @@ describe(`nginx`, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should return /index.html (for legacy builds)', done => {
|
it('should return /index.html (for legacy builds)', async () => {
|
||||||
const origin = `${scheme}://pr${pr}-${sha9}.${host}`;
|
const origin = `${scheme}://pr${pr}-${sha9}.${host}`;
|
||||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||||
|
|
||||||
h.createDummyBuild(pr, sha9, true, false, true);
|
h.createDummyBuild(pr, sha9, true, false, true);
|
||||||
|
|
||||||
Promise.all([
|
await Promise.all([
|
||||||
h.runCmd(`curl -iL ${origin}/index.html`).then(h.verifyResponse(200, bodyRegex)),
|
h.runCmd(`curl -iL ${origin}/index.html`).then(h.verifyResponse(200, bodyRegex)),
|
||||||
h.runCmd(`curl -iL ${origin}/`).then(h.verifyResponse(200, bodyRegex)),
|
h.runCmd(`curl -iL ${origin}/`).then(h.verifyResponse(200, bodyRegex)),
|
||||||
h.runCmd(`curl -iL ${origin}`).then(h.verifyResponse(200, bodyRegex)),
|
h.runCmd(`curl -iL ${origin}`).then(h.verifyResponse(200, bodyRegex)),
|
||||||
]).then(done);
|
]);
|
||||||
|
|
||||||
|
expect({ prNum: pr, sha: sha9, isLegacy: true }).toExistAsABuild();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -86,15 +99,15 @@ describe(`nginx`, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should return /foo/bar.js (for legacy builds)', done => {
|
it('should return /foo/bar.js (for legacy builds)', async () => {
|
||||||
const origin = `${scheme}://pr${pr}-${sha9}.${host}`;
|
const origin = `${scheme}://pr${pr}-${sha9}.${host}`;
|
||||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /foo/bar\\.js$`);
|
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /foo/bar\\.js$`);
|
||||||
|
|
||||||
h.createDummyBuild(pr, sha9, true, false, true);
|
h.createDummyBuild(pr, sha9, true, false, true);
|
||||||
|
|
||||||
h.runCmd(`curl -iL ${origin}/foo/bar.js`).
|
await h.runCmd(`curl -iL ${origin}/foo/bar.js`).then(h.verifyResponse(200, bodyRegex));
|
||||||
then(h.verifyResponse(200, bodyRegex)).
|
|
||||||
then(done);
|
expect({ prNum: pr, sha: sha9, isLegacy: true }).toExistAsABuild();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -126,7 +139,7 @@ describe(`nginx`, () => {
|
|||||||
|
|
||||||
it('should respond with 404 for unknown PRs/SHAs', done => {
|
it('should respond with 404 for unknown PRs/SHAs', done => {
|
||||||
const otherPr = 54321;
|
const otherPr = 54321;
|
||||||
const otherShortSha = h.getShordSha('8'.repeat(40));
|
const otherShortSha = computeShortSha('8'.repeat(40));
|
||||||
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
h.runCmd(`curl -iL ${scheme}://pr${pr}9-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
h.runCmd(`curl -iL ${scheme}://pr${pr}9-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||||
@ -174,39 +187,41 @@ describe(`nginx`, () => {
|
|||||||
|
|
||||||
describe('(for hidden builds)', () => {
|
describe('(for hidden builds)', () => {
|
||||||
|
|
||||||
it('should respond with 404 for any file or directory', done => {
|
it('should respond with 404 for any file or directory', async () => {
|
||||||
const origin = `${scheme}://pr${pr}-${shortSha9}.${host}`;
|
const origin = `${scheme}://pr${pr}-${shortSha9}.${host}`;
|
||||||
const assert404 = h.verifyResponse(404);
|
const assert404 = h.verifyResponse(404);
|
||||||
|
|
||||||
h.createDummyBuild(pr, sha9, false);
|
h.createDummyBuild(pr, sha9, false);
|
||||||
expect(h.buildExists(pr, sha9, false)).toBe(true);
|
|
||||||
|
|
||||||
Promise.all([
|
await Promise.all([
|
||||||
h.runCmd(`curl -iL ${origin}/index.html`).then(assert404),
|
h.runCmd(`curl -iL ${origin}/index.html`).then(assert404),
|
||||||
h.runCmd(`curl -iL ${origin}/`).then(assert404),
|
h.runCmd(`curl -iL ${origin}/`).then(assert404),
|
||||||
h.runCmd(`curl -iL ${origin}`).then(assert404),
|
h.runCmd(`curl -iL ${origin}`).then(assert404),
|
||||||
h.runCmd(`curl -iL ${origin}/foo/bar.js`).then(assert404),
|
h.runCmd(`curl -iL ${origin}/foo/bar.js`).then(assert404),
|
||||||
h.runCmd(`curl -iL ${origin}/foo/`).then(assert404),
|
h.runCmd(`curl -iL ${origin}/foo/`).then(assert404),
|
||||||
h.runCmd(`curl -iL ${origin}/foo`).then(assert404),
|
h.runCmd(`curl -iL ${origin}/foo`).then(assert404),
|
||||||
]).then(done);
|
]);
|
||||||
|
|
||||||
|
expect({ prNum: pr, sha: sha9, isPublic: false }).toExistAsABuild();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should respond with 404 for any file or directory (for legacy builds)', done => {
|
it('should respond with 404 for any file or directory (for legacy builds)', async () => {
|
||||||
const origin = `${scheme}://pr${pr}-${sha9}.${host}`;
|
const origin = `${scheme}://pr${pr}-${sha9}.${host}`;
|
||||||
const assert404 = h.verifyResponse(404);
|
const assert404 = h.verifyResponse(404);
|
||||||
|
|
||||||
h.createDummyBuild(pr, sha9, false, false, true);
|
h.createDummyBuild(pr, sha9, false, false, true);
|
||||||
expect(h.buildExists(pr, sha9, false, true)).toBe(true);
|
|
||||||
|
|
||||||
Promise.all([
|
await Promise.all([
|
||||||
h.runCmd(`curl -iL ${origin}/index.html`).then(assert404),
|
h.runCmd(`curl -iL ${origin}/index.html`).then(assert404),
|
||||||
h.runCmd(`curl -iL ${origin}/`).then(assert404),
|
h.runCmd(`curl -iL ${origin}/`).then(assert404),
|
||||||
h.runCmd(`curl -iL ${origin}`).then(assert404),
|
h.runCmd(`curl -iL ${origin}`).then(assert404),
|
||||||
h.runCmd(`curl -iL ${origin}/foo/bar.js`).then(assert404),
|
h.runCmd(`curl -iL ${origin}/foo/bar.js`).then(assert404),
|
||||||
h.runCmd(`curl -iL ${origin}/foo/`).then(assert404),
|
h.runCmd(`curl -iL ${origin}/foo/`).then(assert404),
|
||||||
h.runCmd(`curl -iL ${origin}/foo`).then(assert404),
|
h.runCmd(`curl -iL ${origin}/foo`).then(assert404),
|
||||||
]).then(done);
|
]);
|
||||||
|
|
||||||
|
expect({ prNum: pr, sha: sha9, isPublic: false, isLegacy: true }).toExistAsABuild();
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
@ -238,10 +253,46 @@ describe(`nginx`, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe(`${host}/create-build/<pr>/<sha>`, () => {
|
describe(`${host}/can-have-public-preview`, () => {
|
||||||
|
const baseUrl = `${scheme}://${host}/can-have-public-preview`;
|
||||||
|
|
||||||
|
|
||||||
|
it('should disallow non-GET requests', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
h.runCmd(`curl -iLX POST ${baseUrl}/42`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||||
|
h.runCmd(`curl -iLX PUT ${baseUrl}/42`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||||
|
h.runCmd(`curl -iLX PATCH ${baseUrl}/42`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||||
|
h.runCmd(`curl -iLX DELETE ${baseUrl}/42`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should pass requests through to the preview server', async () => {
|
||||||
|
await h.runCmd(`curl -iLX GET ${baseUrl}/${PrNums.CHANGED_FILES_ERROR}`).
|
||||||
|
then(h.verifyResponse(500, /CHANGED_FILES_ERROR/));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 404 for unknown paths', async () => {
|
||||||
|
const cmdPrefix = `curl -iLX GET ${baseUrl}`;
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
h.runCmd(`${cmdPrefix}/foo/42`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`${cmdPrefix}-foo/42`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`${cmdPrefix}nfoo/42`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`${cmdPrefix}/42/foo`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`${cmdPrefix}/f00`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`${cmdPrefix}/`).then(h.verifyResponse(404)),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe(`${host}/circle-build`, () => {
|
||||||
|
|
||||||
it('should disallow non-POST requests', done => {
|
it('should disallow non-POST requests', done => {
|
||||||
const url = `${scheme}://${host}/create-build/${pr}/${sha9}`;
|
const url = `${scheme}://${host}/circle-build`;
|
||||||
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
h.runCmd(`curl -iLX GET ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
|
h.runCmd(`curl -iLX GET ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||||
@ -252,31 +303,9 @@ describe(`nginx`, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it(`should reject files larger than ${h.uploadMaxSize}B (according to header)`, done => {
|
it('should pass requests through to the preview server', done => {
|
||||||
const headers = `--header "Content-Length: ${1.5 * h.uploadMaxSize}"`;
|
h.runCmd(`curl -iLX POST ${scheme}://${host}/circle-build`).
|
||||||
const url = `${scheme}://${host}/create-build/${pr}/${sha9}`;
|
then(h.verifyResponse(400, /Incorrect body content. Expected JSON/)).
|
||||||
|
|
||||||
h.runCmd(`curl -iLX POST ${headers} ${url}`).
|
|
||||||
then(h.verifyResponse([413, 'Request Entity Too Large'])).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it(`should reject files larger than ${h.uploadMaxSize}B (without header)`, done => {
|
|
||||||
const filePath = path.join(h.buildsDir, 'snapshot.tar.gz');
|
|
||||||
const url = `${scheme}://${host}/create-build/${pr}/${sha9}`;
|
|
||||||
|
|
||||||
h.writeFile(filePath, {size: 1.5 * h.uploadMaxSize});
|
|
||||||
|
|
||||||
h.runCmd(`curl -iLX POST --data-binary "@${filePath}" ${url}`).
|
|
||||||
then(h.verifyResponse([413, 'Request Entity Too Large'])).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should pass requests through to the upload server', done => {
|
|
||||||
h.runCmd(`curl -iLX POST ${scheme}://${host}/create-build/${pr}/${sha9}`).
|
|
||||||
then(h.verifyResponse(401, /Missing or empty 'AUTHORIZATION' header/)).
|
|
||||||
then(done);
|
then(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -285,32 +314,14 @@ describe(`nginx`, () => {
|
|||||||
const cmdPrefix = `curl -iLX POST ${scheme}://${host}`;
|
const cmdPrefix = `curl -iLX POST ${scheme}://${host}`;
|
||||||
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
h.runCmd(`${cmdPrefix}/foo/create-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
h.runCmd(`${cmdPrefix}/foo/circle-build/`).then(h.verifyResponse(404)),
|
||||||
h.runCmd(`${cmdPrefix}/foo-create-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
h.runCmd(`${cmdPrefix}/foo-circle-build/`).then(h.verifyResponse(404)),
|
||||||
h.runCmd(`${cmdPrefix}/fooncreate-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
h.runCmd(`${cmdPrefix}/fooncircle-build/`).then(h.verifyResponse(404)),
|
||||||
h.runCmd(`${cmdPrefix}/create-build/foo/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
h.runCmd(`${cmdPrefix}/circle-build/foo/`).then(h.verifyResponse(404)),
|
||||||
h.runCmd(`${cmdPrefix}/create-build-foo/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
h.runCmd(`${cmdPrefix}/circle-build-foo/`).then(h.verifyResponse(404)),
|
||||||
h.runCmd(`${cmdPrefix}/create-buildnfoo/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
h.runCmd(`${cmdPrefix}/circle-buildnfoo/`).then(h.verifyResponse(404)),
|
||||||
h.runCmd(`${cmdPrefix}/create-build/pr${pr}/${sha9}`).then(h.verifyResponse(404)),
|
h.runCmd(`${cmdPrefix}/circle-build/pr`).then(h.verifyResponse(404)),
|
||||||
h.runCmd(`${cmdPrefix}/create-build/${pr}/${sha9}42`).then(h.verifyResponse(404)),
|
h.runCmd(`${cmdPrefix}/circle-build/42`).then(h.verifyResponse(404)),
|
||||||
]).then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should reject PRs with leading zeros', done => {
|
|
||||||
h.runCmd(`curl -iLX POST ${scheme}://${host}/create-build/0${pr}/${sha9}`).
|
|
||||||
then(h.verifyResponse(404)).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should accept SHAs with leading zeros (but not trim the zeros)', done => {
|
|
||||||
const cmdPrefix = `curl -iLX POST ${scheme}://${host}/create-build/${pr}`;
|
|
||||||
const bodyRegex = /Missing or empty 'AUTHORIZATION' header/;
|
|
||||||
|
|
||||||
Promise.all([
|
|
||||||
h.runCmd(`${cmdPrefix}/0${sha9}`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`${cmdPrefix}/${sha0}`).then(h.verifyResponse(401, bodyRegex)),
|
|
||||||
]).then(done);
|
]).then(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -331,17 +342,13 @@ describe(`nginx`, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should pass requests through to the upload server', done => {
|
it('should pass requests through to the preview server', done => {
|
||||||
const cmdPrefix = `curl -iLX POST --header "Content-Type: application/json"`;
|
const cmdPrefix = `curl -iLX POST --header "Content-Type: application/json"`;
|
||||||
|
|
||||||
const cmd1 = `${cmdPrefix} ${url}`;
|
const cmd1 = `${cmdPrefix} ${url}`;
|
||||||
const cmd2 = `${cmdPrefix} --data '{"number":${pr}}' ${url}`;
|
|
||||||
const cmd3 = `${cmdPrefix} --data '{"number":${pr},"action":"foo"}' ${url}`;
|
|
||||||
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
h.runCmd(cmd1).then(h.verifyResponse(400, /Missing or empty 'number' field/)),
|
h.runCmd(cmd1).then(h.verifyResponse(400, /Missing or empty 'number' field/)),
|
||||||
h.runCmd(cmd2).then(h.verifyResponse(200)),
|
|
||||||
h.runCmd(cmd3).then(h.verifyResponse(200)),
|
|
||||||
]).then(done);
|
]).then(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -364,13 +371,15 @@ describe(`nginx`, () => {
|
|||||||
|
|
||||||
describe(`${host}/*`, () => {
|
describe(`${host}/*`, () => {
|
||||||
|
|
||||||
it('should respond with 404 for unknown URLs (even if the resource exists)', done => {
|
beforeEach(() => {
|
||||||
['index.html', 'foo.js', 'foo/index.html'].forEach(relFilePath => {
|
['index.html', 'foo.js', 'foo/index.html'].forEach(relFilePath => {
|
||||||
const absFilePath = path.join(h.buildsDir, relFilePath);
|
const absFilePath = path.join(AIO_BUILDS_DIR, relFilePath);
|
||||||
h.writeFile(absFilePath, {content: `File: /${relFilePath}`});
|
return h.writeFile(absFilePath, {content: `File: /${relFilePath}`});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Promise.all([
|
it('should respond with 404 for unknown URLs (even if the resource exists)', async () => {
|
||||||
|
await Promise.all([
|
||||||
h.runCmd(`curl -iL ${scheme}://${host}/index.html`).then(h.verifyResponse(404)),
|
h.runCmd(`curl -iL ${scheme}://${host}/index.html`).then(h.verifyResponse(404)),
|
||||||
h.runCmd(`curl -iL ${scheme}://${host}/`).then(h.verifyResponse(404)),
|
h.runCmd(`curl -iL ${scheme}://${host}/`).then(h.verifyResponse(404)),
|
||||||
h.runCmd(`curl -iL ${scheme}://${host}`).then(h.verifyResponse(404)),
|
h.runCmd(`curl -iL ${scheme}://${host}`).then(h.verifyResponse(404)),
|
||||||
@ -379,7 +388,14 @@ describe(`nginx`, () => {
|
|||||||
h.runCmd(`curl -iL ${scheme}://foo.${host}`).then(h.verifyResponse(404)),
|
h.runCmd(`curl -iL ${scheme}://foo.${host}`).then(h.verifyResponse(404)),
|
||||||
h.runCmd(`curl -iL ${scheme}://${host}/foo.js`).then(h.verifyResponse(404)),
|
h.runCmd(`curl -iL ${scheme}://${host}/foo.js`).then(h.verifyResponse(404)),
|
||||||
h.runCmd(`curl -iL ${scheme}://${host}/foo/index.html`).then(h.verifyResponse(404)),
|
h.runCmd(`curl -iL ${scheme}://${host}/foo/index.html`).then(h.verifyResponse(404)),
|
||||||
]).then(done);
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
['index.html', 'foo.js', 'foo/index.html', 'foo'].forEach(relFilePath => {
|
||||||
|
const absFilePath = path.join(AIO_BUILDS_DIR, relFilePath);
|
||||||
|
rm('-r', absFilePath);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,569 @@
|
|||||||
|
// Imports
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import {join} from 'path';
|
||||||
|
import {AIO_PREVIEW_SERVER_HOSTNAME, AIO_PREVIEW_SERVER_PORT, AIO_WWW_USER} from '../common/env-variables';
|
||||||
|
import {computeShortSha} from '../common/utils';
|
||||||
|
import {ALT_SHA, BuildNums, PrNums, SHA, SIMILAR_SHA} from './constants';
|
||||||
|
import {helper as h, makeCurl, payload} from './helper';
|
||||||
|
import {customMatchers} from './jasmine-custom-matchers';
|
||||||
|
|
||||||
|
// Tests
|
||||||
|
describe('preview-server', () => {
|
||||||
|
const hostname = AIO_PREVIEW_SERVER_HOSTNAME;
|
||||||
|
const port = AIO_PREVIEW_SERVER_PORT;
|
||||||
|
const host = `http://${hostname}:${port}`;
|
||||||
|
|
||||||
|
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000);
|
||||||
|
beforeEach(() => jasmine.addMatchers(customMatchers));
|
||||||
|
afterEach(() => h.cleanUp());
|
||||||
|
|
||||||
|
|
||||||
|
describe(`${host}/can-have-public-preview`, () => {
|
||||||
|
const curl = makeCurl(`${host}/can-have-public-preview`, {
|
||||||
|
defaultData: null,
|
||||||
|
defaultExtraPath: `/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`,
|
||||||
|
defaultHeaders: [],
|
||||||
|
defaultMethod: 'GET',
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should disallow non-GET requests', async () => {
|
||||||
|
const bodyRegex = /^Unknown resource in request/;
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
curl({method: 'POST'}).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
curl({method: 'PUT'}).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
curl({method: 'PATCH'}).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
curl({method: 'DELETE'}).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 404 for unknown paths', async () => {
|
||||||
|
const bodyRegex = /^Unknown resource in request/;
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
curl({extraPath: `/foo/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
curl({extraPath: `-foo/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
curl({extraPath: `nfoo/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
curl({extraPath: `/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}/foo`}).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
curl({extraPath: '/f00'}).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
curl({extraPath: '/'}).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 500 if checking for significant file changes fails', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
curl({extraPath: `/${PrNums.CHANGED_FILES_404}`}).then(h.verifyResponse(500, /CHANGED_FILES_404/)),
|
||||||
|
curl({extraPath: `/${PrNums.CHANGED_FILES_ERROR}`}).then(h.verifyResponse(500, /CHANGED_FILES_ERROR/)),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 200 (false) if no significant files were touched', async () => {
|
||||||
|
const expectedResponse = JSON.stringify({
|
||||||
|
canHavePublicPreview: false,
|
||||||
|
reason: 'No significant files touched.',
|
||||||
|
});
|
||||||
|
|
||||||
|
await curl({extraPath: `/${PrNums.CHANGED_FILES_NONE}`}).then(h.verifyResponse(200, expectedResponse));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 500 if checking "trusted" status fails', async () => {
|
||||||
|
await curl({extraPath: `/${PrNums.TRUST_CHECK_ERROR}`}).then(h.verifyResponse(500, 'TRUST_CHECK_ERROR'));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 200 (false) if the PR is not automatically verifiable as "trusted"', async () => {
|
||||||
|
const expectedResponse = JSON.stringify({
|
||||||
|
canHavePublicPreview: false,
|
||||||
|
reason: 'Not automatically verifiable as \\"trusted\\".',
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
curl({extraPath: `/${PrNums.TRUST_CHECK_INACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(200, expectedResponse)),
|
||||||
|
curl({extraPath: `/${PrNums.TRUST_CHECK_UNTRUSTED}`}).then(h.verifyResponse(200, expectedResponse)),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 200 (true) if the PR can have a public preview', async () => {
|
||||||
|
const expectedResponse = JSON.stringify({
|
||||||
|
canHavePublicPreview: true,
|
||||||
|
reason: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
curl({extraPath: `/${PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER}`}).then(h.verifyResponse(200, expectedResponse)),
|
||||||
|
curl({extraPath: `/${PrNums.TRUST_CHECK_TRUSTED_LABEL}`}).then(h.verifyResponse(200, expectedResponse)),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe(`${host}/circle-build`, () => {
|
||||||
|
|
||||||
|
const curl = makeCurl(`${host}/circle-build`);
|
||||||
|
|
||||||
|
it('should disallow non-POST requests', async () => {
|
||||||
|
const bodyRegex = /^Unknown resource/;
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
curl({method: 'GET'}).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
curl({method: 'PUT'}).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
curl({method: 'PATCH'}).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
curl({method: 'DELETE'}).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 404 for unknown paths', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
curl({url: `${host}/foo/circle-build`}).then(h.verifyResponse(404)),
|
||||||
|
curl({url: `${host}/foo-circle-build`}).then(h.verifyResponse(404)),
|
||||||
|
curl({url: `${host}/fooncircle-build`}).then(h.verifyResponse(404)),
|
||||||
|
curl({url: `${host}/circle-build/foo`}).then(h.verifyResponse(404)),
|
||||||
|
curl({url: `${host}/circle-build-foo`}).then(h.verifyResponse(404)),
|
||||||
|
curl({url: `${host}/circle-buildnfoo`}).then(h.verifyResponse(404)),
|
||||||
|
curl({url: `${host}/circle-build/pr`}).then(h.verifyResponse(404)),
|
||||||
|
curl({url: `${host}/circle-build42`}).then(h.verifyResponse(404)),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respond with 400 if the body is not valid', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
curl({ data: '' }).then(h.verifyResponse(400)),
|
||||||
|
curl({ data: {} }).then(h.verifyResponse(400)),
|
||||||
|
curl({ data: { payload: {} } }).then(h.verifyResponse(400)),
|
||||||
|
curl({ data: { payload: { build_num: 1 } } }).then(h.verifyResponse(400)),
|
||||||
|
curl({ data: { payload: { build_num: 1, build_parameters: {} } } }).then(h.verifyResponse(400)),
|
||||||
|
curl(payload(0)).then(h.verifyResponse(400)),
|
||||||
|
curl(payload(-1)).then(h.verifyResponse(400)),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respond with 500 if the CircleCI API request errors', async () => {
|
||||||
|
await curl(payload(BuildNums.BUILD_INFO_ERROR)).then(h.verifyResponse(500));
|
||||||
|
await curl(payload(BuildNums.BUILD_INFO_404)).then(h.verifyResponse(500));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respond with 204 if the build on CircleCI failed', async () => {
|
||||||
|
await curl(payload(BuildNums.BUILD_INFO_BUILD_FAILED)).then(h.verifyResponse(204));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respond with 500 if the github org from CircleCI does not match what is configured', async () => {
|
||||||
|
await curl(payload(BuildNums.BUILD_INFO_INVALID_GH_ORG)).then(h.verifyResponse(500));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respond with 500 if the github repo from CircleCI does not match what is configured', async () => {
|
||||||
|
await curl(payload(BuildNums.BUILD_INFO_INVALID_GH_REPO)).then(h.verifyResponse(500));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respond with 500 if the github files API errors', async () => {
|
||||||
|
await curl(payload(BuildNums.CHANGED_FILES_ERROR)).then(h.verifyResponse(500));
|
||||||
|
await curl(payload(BuildNums.CHANGED_FILES_404)).then(h.verifyResponse(500));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respond with 204 if no significant files are changed by the PR', async () => {
|
||||||
|
await curl(payload(BuildNums.CHANGED_FILES_NONE)).then(h.verifyResponse(204));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respond with 500 if the CircleCI artifact API fails', async () => {
|
||||||
|
await curl(payload(BuildNums.BUILD_ARTIFACTS_ERROR)).then(h.verifyResponse(500));
|
||||||
|
await curl(payload(BuildNums.BUILD_ARTIFACTS_404)).then(h.verifyResponse(500));
|
||||||
|
await curl(payload(BuildNums.BUILD_ARTIFACTS_EMPTY)).then(h.verifyResponse(500));
|
||||||
|
await curl(payload(BuildNums.BUILD_ARTIFACTS_MISSING)).then(h.verifyResponse(500));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respond with 500 if fetching the artifact errors', async () => {
|
||||||
|
await curl(payload(BuildNums.DOWNLOAD_ARTIFACT_ERROR)).then(h.verifyResponse(500));
|
||||||
|
await curl(payload(BuildNums.DOWNLOAD_ARTIFACT_404)).then(h.verifyResponse(500));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respond with 500 if the GH trusted API fails', async () => {
|
||||||
|
await curl(payload(BuildNums.TRUST_CHECK_ERROR)).then(h.verifyResponse(500));
|
||||||
|
expect({ prNum: PrNums.TRUST_CHECK_ERROR }).toExistAsAnArtifact();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respond with 201 if a new public build is created', async () => {
|
||||||
|
await curl(payload(BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER))
|
||||||
|
.then(h.verifyResponse(201));
|
||||||
|
expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER }).toExistAsABuild();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respond with 202 if a new private build is created', async () => {
|
||||||
|
await curl(payload(BuildNums.TRUST_CHECK_UNTRUSTED)).then(h.verifyResponse(202));
|
||||||
|
expect({ prNum: PrNums.TRUST_CHECK_UNTRUSTED, isPublic: false }).toExistAsABuild();
|
||||||
|
});
|
||||||
|
|
||||||
|
[true].forEach(isPublic => {
|
||||||
|
const build = isPublic ? BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER : BuildNums.TRUST_CHECK_UNTRUSTED;
|
||||||
|
const prNum = isPublic ? PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER : PrNums.TRUST_CHECK_UNTRUSTED;
|
||||||
|
const label = isPublic ? 'public' : 'non-public';
|
||||||
|
const overwriteRe = RegExp(`^Request to overwrite existing ${label} directory`);
|
||||||
|
const statusCode = isPublic ? 201 : 202;
|
||||||
|
|
||||||
|
describe(`for ${label} builds`, () => {
|
||||||
|
|
||||||
|
it('should extract the contents of the build artifact', async () => {
|
||||||
|
await curl(payload(build))
|
||||||
|
.then(h.verifyResponse(statusCode));
|
||||||
|
expect(h.readBuildFile(prNum, SHA, 'index.html', isPublic))
|
||||||
|
.toContain(`PR: ${prNum} | SHA: ${SHA} | File: /index.html`);
|
||||||
|
expect(h.readBuildFile(prNum, SHA, 'foo/bar.js', isPublic))
|
||||||
|
.toContain(`PR: ${prNum} | SHA: ${SHA} | File: /foo/bar.js`);
|
||||||
|
expect({ prNum, isPublic }).toExistAsABuild();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should create files/directories owned by '${AIO_WWW_USER}'`, async () => {
|
||||||
|
await curl(payload(build))
|
||||||
|
.then(h.verifyResponse(statusCode));
|
||||||
|
|
||||||
|
const shaDir = h.getShaDir(h.getPrDir(prNum, isPublic), SHA);
|
||||||
|
const { stdout: allFiles } = await h.runCmd(`find ${shaDir}`);
|
||||||
|
const { stdout: userFiles } = await h.runCmd(`find ${shaDir} -user ${AIO_WWW_USER}`);
|
||||||
|
|
||||||
|
expect(userFiles).toBe(allFiles);
|
||||||
|
expect(userFiles).toContain(shaDir);
|
||||||
|
expect(userFiles).toContain(join(shaDir, 'index.html'));
|
||||||
|
expect(userFiles).toContain(join(shaDir, 'foo', 'bar.js'));
|
||||||
|
|
||||||
|
expect({ prNum, isPublic }).toExistAsABuild();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete the build artifact file', async () => {
|
||||||
|
await curl(payload(build))
|
||||||
|
.then(h.verifyResponse(statusCode));
|
||||||
|
expect({ prNum, SHA }).not.toExistAsAnArtifact();
|
||||||
|
expect({ prNum, isPublic }).toExistAsABuild();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should make the build directory non-writable', async () => {
|
||||||
|
await curl(payload(build))
|
||||||
|
.then(h.verifyResponse(statusCode));
|
||||||
|
|
||||||
|
// See https://github.com/nodejs/node-v0.x-archive/issues/3045#issuecomment-4862588.
|
||||||
|
const isNotWritable = (fileOrDir: string) => {
|
||||||
|
const mode = fs.statSync(fileOrDir).mode;
|
||||||
|
// tslint:disable-next-line: no-bitwise
|
||||||
|
return !(mode & parseInt('222', 8));
|
||||||
|
};
|
||||||
|
|
||||||
|
const shaDir = h.getShaDir(h.getPrDir(prNum, isPublic), SHA);
|
||||||
|
expect(isNotWritable(shaDir)).toBe(true);
|
||||||
|
expect(isNotWritable(join(shaDir, 'index.html'))).toBe(true);
|
||||||
|
expect(isNotWritable(join(shaDir, 'foo', 'bar.js'))).toBe(true);
|
||||||
|
|
||||||
|
expect({ prNum, isPublic }).toExistAsABuild();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore a legacy 40-chars long build directory (even if it starts with the same chars)',
|
||||||
|
async () => {
|
||||||
|
// It is possible that 40-chars long build directories exist, if they had been deployed
|
||||||
|
// before implementing the shorter build directory names. In that case, we don't want the
|
||||||
|
// second (shorter) name to be considered the same as the old one (even if they originate
|
||||||
|
// from the same SHA).
|
||||||
|
|
||||||
|
h.createDummyBuild(prNum, SHA, isPublic, false, true);
|
||||||
|
h.writeBuildFile(prNum, SHA, 'index.html', 'My content', isPublic, true);
|
||||||
|
expect(h.readBuildFile(prNum, SHA, 'index.html', isPublic, true)).toBe('My content');
|
||||||
|
|
||||||
|
await curl(payload(build))
|
||||||
|
.then(h.verifyResponse(statusCode));
|
||||||
|
|
||||||
|
expect(h.readBuildFile(prNum, SHA, 'index.html', isPublic, false)).toContain('index.html');
|
||||||
|
expect(h.readBuildFile(prNum, SHA, 'index.html', isPublic, true)).toBe('My content');
|
||||||
|
|
||||||
|
expect({ prNum, isPublic, sha: SHA, isLegacy: false }).toExistAsABuild();
|
||||||
|
expect({ prNum, isPublic, sha: SHA, isLegacy: true }).toExistAsABuild();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should not overwrite existing builds`, async () => {
|
||||||
|
// setup a build already in place
|
||||||
|
h.createDummyBuild(prNum, SHA, isPublic);
|
||||||
|
// distinguish this build from the downloaded one
|
||||||
|
h.writeBuildFile(prNum, SHA, 'index.html', 'My content', isPublic);
|
||||||
|
await curl(payload(build)).then(h.verifyResponse(409, overwriteRe));
|
||||||
|
expect(h.readBuildFile(prNum, SHA, 'index.html', isPublic)).toBe('My content');
|
||||||
|
expect({ prNum, isPublic }).toExistAsABuild();
|
||||||
|
expect({ prNum }).toExistAsAnArtifact();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should not overwrite existing builds (even if the SHA is different)`, async () => {
|
||||||
|
// Since only the first few characters of the SHA are used, it is possible for two different
|
||||||
|
// SHAs to correspond to the same directory. In that case, we don't want the second SHA to
|
||||||
|
// overwrite the first.
|
||||||
|
expect(SIMILAR_SHA).not.toEqual(SHA);
|
||||||
|
expect(computeShortSha(SIMILAR_SHA)).toEqual(computeShortSha(SHA));
|
||||||
|
h.createDummyBuild(prNum, SIMILAR_SHA, isPublic);
|
||||||
|
expect(h.readBuildFile(prNum, SIMILAR_SHA, 'index.html', isPublic)).toContain('index.html');
|
||||||
|
h.writeBuildFile(prNum, SIMILAR_SHA, 'index.html', 'My content', isPublic);
|
||||||
|
expect(h.readBuildFile(prNum, SIMILAR_SHA, 'index.html', isPublic)).toBe('My content');
|
||||||
|
|
||||||
|
await curl(payload(build)).then(h.verifyResponse(409, overwriteRe));
|
||||||
|
expect(h.readBuildFile(prNum, SIMILAR_SHA, 'index.html', isPublic)).toBe('My content');
|
||||||
|
expect({ prNum, isPublic, sha: SIMILAR_SHA }).toExistAsABuild();
|
||||||
|
expect({ prNum, sha: SIMILAR_SHA }).toExistAsAnArtifact();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only delete the SHA directory on error (for existing PR)', async () => {
|
||||||
|
h.createDummyBuild(prNum, ALT_SHA, isPublic);
|
||||||
|
await curl(payload(BuildNums.TRUST_CHECK_ERROR)).then(h.verifyResponse(500));
|
||||||
|
expect({ prNum: PrNums.TRUST_CHECK_ERROR }).toExistAsAnArtifact();
|
||||||
|
expect({ prNum, isPublic, sha: SHA }).not.toExistAsABuild();
|
||||||
|
expect({ prNum, isPublic, sha: ALT_SHA }).toExistAsABuild();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the PR\'s visibility has changed', () => {
|
||||||
|
|
||||||
|
it('should update the PR\'s visibility', async () => {
|
||||||
|
h.createDummyBuild(prNum, ALT_SHA, !isPublic);
|
||||||
|
await curl(payload(build)).then(h.verifyResponse(statusCode));
|
||||||
|
expect({ prNum, isPublic }).toExistAsABuild();
|
||||||
|
expect({ prNum, isPublic, sha: ALT_SHA }).toExistAsABuild();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should not overwrite existing builds (but keep the updated visibility)', async () => {
|
||||||
|
h.createDummyBuild(prNum, SHA, !isPublic);
|
||||||
|
await curl(payload(build)).then(h.verifyResponse(409));
|
||||||
|
expect({ prNum, isPublic }).toExistAsABuild();
|
||||||
|
expect({ prNum, isPublic: !isPublic }).not.toExistAsABuild();
|
||||||
|
// since it errored we didn't clear up the downloaded artifact - perhaps we should?
|
||||||
|
expect({ prNum }).toExistAsAnArtifact();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should reject the request if it fails to update the PR\'s visibility', async () => {
|
||||||
|
// One way to cause an error is to have both a public and a hidden directory for the same PR.
|
||||||
|
h.createDummyBuild(prNum, ALT_SHA, isPublic);
|
||||||
|
h.createDummyBuild(prNum, ALT_SHA, !isPublic);
|
||||||
|
|
||||||
|
const errorRegex = new RegExp(`^Request to move '${h.getPrDir(prNum, !isPublic)}' ` +
|
||||||
|
`to existing directory '${h.getPrDir(prNum, isPublic)}'.`);
|
||||||
|
|
||||||
|
await curl(payload(build)).then(h.verifyResponse(409, errorRegex));
|
||||||
|
|
||||||
|
expect({ prNum, isPublic }).not.toExistAsABuild();
|
||||||
|
|
||||||
|
// The bad folders should have been deleted
|
||||||
|
expect({ prNum, sha: ALT_SHA, isPublic }).toExistAsABuild();
|
||||||
|
expect({ prNum, sha: ALT_SHA, isPublic: !isPublic }).toExistAsABuild();
|
||||||
|
|
||||||
|
// since it errored we didn't clear up the downloaded artifact - perhaps we should?
|
||||||
|
expect({ prNum }).toExistAsAnArtifact();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe(`${host}/health-check`, () => {
|
||||||
|
|
||||||
|
it('should respond with 200', done => {
|
||||||
|
Promise.all([
|
||||||
|
h.runCmd(`curl -iL ${host}/health-check`).then(h.verifyResponse(200)),
|
||||||
|
h.runCmd(`curl -iL ${host}/health-check/`).then(h.verifyResponse(200)),
|
||||||
|
]).then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 404 if the path does not match exactly', done => {
|
||||||
|
Promise.all([
|
||||||
|
h.runCmd(`curl -iL ${host}/health-check/foo`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`curl -iL ${host}/health-check-foo`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`curl -iL ${host}/health-checknfoo`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`curl -iL ${host}/foo/health-check`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`curl -iL ${host}/foo-health-check`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`curl -iL ${host}/foonhealth-check`).then(h.verifyResponse(404)),
|
||||||
|
]).then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe(`${host}/pr-updated`, () => {
|
||||||
|
const curl = makeCurl(`${host}/pr-updated`);
|
||||||
|
|
||||||
|
it('should disallow non-POST requests', async () => {
|
||||||
|
const bodyRegex = /^Unknown resource in request/;
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
curl({method: 'GET'}).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
curl({method: 'PUT'}).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
curl({method: 'PATCH'}).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
curl({method: 'DELETE'}).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 400 for requests without a payload', async () => {
|
||||||
|
const bodyRegex = /^Missing or empty 'number' field in request/;
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
curl({ data: '' }).then(h.verifyResponse(400, bodyRegex)),
|
||||||
|
curl({ data: {} }).then(h.verifyResponse(400, bodyRegex)),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 400 for requests without a \'number\' field', async () => {
|
||||||
|
const bodyRegex = /^Missing or empty 'number' field in request/;
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
curl({ data: {} }).then(h.verifyResponse(400, bodyRegex)),
|
||||||
|
curl({ data: { number: null} }).then(h.verifyResponse(400, bodyRegex)),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should reject requests for which checking the PR visibility fails', async () => {
|
||||||
|
await curl({ data: { number: PrNums.TRUST_CHECK_ERROR } }).then(h.verifyResponse(500, /TRUST_CHECK_ERROR/));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 404 for unknown paths', done => {
|
||||||
|
const mockPayload = JSON.stringify({number: 1}); // MockExternalApiFlags.TRUST_CHECK_ACTIVE_TRUSTED_USER });
|
||||||
|
const cmdPrefix = `curl -iLX POST --data "${mockPayload}" ${host}`;
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
h.runCmd(`${cmdPrefix}/foo/pr-updated`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`${cmdPrefix}/foo-pr-updated`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`${cmdPrefix}/foonpr-updated`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`${cmdPrefix}/pr-updated/foo`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`${cmdPrefix}/pr-updated-foo`).then(h.verifyResponse(404)),
|
||||||
|
h.runCmd(`${cmdPrefix}/pr-updatednfoo`).then(h.verifyResponse(404)),
|
||||||
|
]).then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should do nothing if PR\'s visibility is already up-to-date', async () => {
|
||||||
|
const publicPr = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
|
||||||
|
const hiddenPr = PrNums.TRUST_CHECK_UNTRUSTED;
|
||||||
|
|
||||||
|
const checkVisibilities = (remove: boolean) => {
|
||||||
|
// Public build is already public.
|
||||||
|
expect({ prNum: publicPr, isPublic: false }).not.toExistAsABuild(remove);
|
||||||
|
expect({ prNum: publicPr, isPublic: true }).toExistAsABuild(remove);
|
||||||
|
// Hidden build is already hidden.
|
||||||
|
expect({ prNum: hiddenPr, isPublic: false }).toExistAsABuild(remove);
|
||||||
|
expect({ prNum: hiddenPr, isPublic: true }).not.toExistAsABuild(remove);
|
||||||
|
};
|
||||||
|
|
||||||
|
h.createDummyBuild(publicPr, SHA, true);
|
||||||
|
h.createDummyBuild(hiddenPr, SHA, false);
|
||||||
|
checkVisibilities(false);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
curl({ data: {number: +publicPr, action: 'foo' } }).then(h.verifyResponse(200)),
|
||||||
|
curl({ data: {number: +hiddenPr, action: 'foo' } }).then(h.verifyResponse(200)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Visibilities should not have changed, because the specified action could not have triggered a change.
|
||||||
|
checkVisibilities(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should do nothing if \'action\' implies no visibility change', async () => {
|
||||||
|
const publicPr = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
|
||||||
|
const hiddenPr = PrNums.TRUST_CHECK_UNTRUSTED;
|
||||||
|
|
||||||
|
const checkVisibilities = (remove: boolean) => {
|
||||||
|
// Public build is hidden atm.
|
||||||
|
expect({ prNum: publicPr, isPublic: false }).toExistAsABuild(remove);
|
||||||
|
expect({ prNum: publicPr, isPublic: true }).not.toExistAsABuild(remove);
|
||||||
|
// Hidden build is public atm.
|
||||||
|
expect({ prNum: hiddenPr, isPublic: false }).not.toExistAsABuild(remove);
|
||||||
|
expect({ prNum: hiddenPr, isPublic: true }).toExistAsABuild(remove);
|
||||||
|
};
|
||||||
|
|
||||||
|
h.createDummyBuild(publicPr, SHA, false);
|
||||||
|
h.createDummyBuild(hiddenPr, SHA, true);
|
||||||
|
checkVisibilities(false);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
curl({ data: {number: +publicPr, action: 'foo' } }).then(h.verifyResponse(200)),
|
||||||
|
curl({ data: {number: +hiddenPr, action: 'foo' } }).then(h.verifyResponse(200)),
|
||||||
|
]);
|
||||||
|
// Visibilities should not have changed, because the specified action could not have triggered a change.
|
||||||
|
checkVisibilities(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('when the visiblity has changed', () => {
|
||||||
|
const publicPr = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
|
||||||
|
const hiddenPr = PrNums.TRUST_CHECK_UNTRUSTED;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Create initial PR builds with opposite visibilities as the ones that will be reported:
|
||||||
|
// - The now public PR was previously hidden.
|
||||||
|
// - The now hidden PR was previously public.
|
||||||
|
h.createDummyBuild(publicPr, SHA, false);
|
||||||
|
h.createDummyBuild(hiddenPr, SHA, true);
|
||||||
|
|
||||||
|
expect({ prNum: publicPr, isPublic: false }).toExistAsABuild(false);
|
||||||
|
expect({ prNum: publicPr, isPublic: true }).not.toExistAsABuild(false);
|
||||||
|
expect({ prNum: hiddenPr, isPublic: false }).not.toExistAsABuild(false);
|
||||||
|
expect({ prNum: hiddenPr, isPublic: true }).toExistAsABuild(false);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
// Expect PRs' visibility to have been updated:
|
||||||
|
// - The public PR should be actually public (previously it was hidden).
|
||||||
|
// - The hidden PR should be actually hidden (previously it was public).
|
||||||
|
expect({ prNum: publicPr, isPublic: false }).not.toExistAsABuild();
|
||||||
|
expect({ prNum: publicPr, isPublic: true }).toExistAsABuild();
|
||||||
|
expect({ prNum: hiddenPr, isPublic: false }).toExistAsABuild();
|
||||||
|
expect({ prNum: hiddenPr, isPublic: true }).not.toExistAsABuild();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should update the PR\'s visibility (action: undefined)', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
curl({ data: {number: +publicPr } }).then(h.verifyResponse(200)),
|
||||||
|
curl({ data: {number: +hiddenPr } }).then(h.verifyResponse(200)),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should update the PR\'s visibility (action: labeled)', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
curl({ data: {number: +publicPr, action: 'labeled' } }).then(h.verifyResponse(200)),
|
||||||
|
curl({ data: {number: +hiddenPr, action: 'labeled' } }).then(h.verifyResponse(200)),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should update the PR\'s visibility (action: unlabeled)', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
curl({ data: {number: +publicPr, action: 'unlabeled' } }).then(h.verifyResponse(200)),
|
||||||
|
curl({ data: {number: +hiddenPr, action: 'unlabeled' } }).then(h.verifyResponse(200)),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe(`${host}/*`, () => {
|
||||||
|
|
||||||
|
it('should respond with 404 for requests to unknown URLs', done => {
|
||||||
|
const bodyRegex = /^Unknown resource/;
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
h.runCmd(`curl -iL ${host}/index.html`).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
h.runCmd(`curl -iL ${host}/`).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
h.runCmd(`curl -iL ${host}`).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
h.runCmd(`curl -iLX PUT ${host}`).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
h.runCmd(`curl -iLX POST ${host}`).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
h.runCmd(`curl -iLX PATCH ${host}`).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
h.runCmd(`curl -iLX DELETE ${host}`).then(h.verifyResponse(404, bodyRegex)),
|
||||||
|
]).then(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
@ -1,101 +1,80 @@
|
|||||||
// Imports
|
// Imports
|
||||||
import * as path from 'path';
|
import {AIO_NGINX_HOSTNAME} from '../common/env-variables';
|
||||||
import * as c from './constants';
|
import {computeShortSha} from '../common/utils';
|
||||||
import {helper as h} from './helper';
|
import {ALT_SHA, BuildNums, PrNums, SHA} from './constants';
|
||||||
|
import {helper as h, makeCurl, payload} from './helper';
|
||||||
|
import {customMatchers} from './jasmine-custom-matchers';
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme.toUpperCase()})`, () => {
|
h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme.toUpperCase()})`, () => {
|
||||||
const hostname = h.nginxHostname;
|
const hostname = AIO_NGINX_HOSTNAME;
|
||||||
const host = `${hostname}:${port}`;
|
const host = `${hostname}:${port}`;
|
||||||
const pr9 = '9';
|
const curlPrUpdated = makeCurl(`${scheme}://${host}/pr-updated`);
|
||||||
const sha9 = '9'.repeat(40);
|
|
||||||
const sha0 = '0'.repeat(40);
|
|
||||||
const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz');
|
|
||||||
|
|
||||||
const getFile = (pr: string, sha: string, file: string) =>
|
const getFile = (pr: number, sha: string, file: string) =>
|
||||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${h.getShordSha(sha)}.${host}/${file}`);
|
h.runCmd(`curl -iL ${scheme}://pr${pr}-${computeShortSha(sha)}.${host}/${file}`);
|
||||||
const uploadBuild = (pr: string, sha: string, archive: string, authHeader = 'Token FOO') => {
|
const prUpdated = (prNum: number, action?: string) => curlPrUpdated({ data: { number: prNum, action } });
|
||||||
const curlPost = `curl -iLX POST --header "Authorization: ${authHeader}"`;
|
const circleBuild = makeCurl(`${scheme}://${host}/circle-build`);
|
||||||
return h.runCmd(`${curlPost} --data-binary "@${archive}" ${scheme}://${host}/create-build/${pr}/${sha}`);
|
|
||||||
};
|
|
||||||
const prUpdated = (pr: number, action?: string) => {
|
|
||||||
const url = `${scheme}://${host}/pr-updated`;
|
|
||||||
const payloadStr = JSON.stringify({number: pr, action});
|
|
||||||
return h.runCmd(`curl -iLX POST --header "Content-Type: application/json" --data '${payloadStr}' ${url}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000);
|
beforeEach(() => {
|
||||||
afterEach(() => {
|
jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000;
|
||||||
h.deletePrDir(pr9);
|
jasmine.addMatchers(customMatchers);
|
||||||
h.deletePrDir(pr9, false);
|
|
||||||
h.cleanUp();
|
|
||||||
});
|
});
|
||||||
|
afterEach(() => h.cleanUp());
|
||||||
|
|
||||||
|
|
||||||
describe('for a new/non-existing PR', () => {
|
describe('for a new/non-existing PR', () => {
|
||||||
|
|
||||||
it('should be able to upload and serve a public build', done => {
|
it('should be able to create and serve a public preview', async () => {
|
||||||
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`;
|
const BUILD = BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
|
||||||
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
|
const PR = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
|
||||||
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
|
|
||||||
|
|
||||||
h.createDummyArchive(pr9, sha9, archivePath);
|
const regexPrefix = `^BUILD: ${BUILD} \\| PR: ${PR} \\| SHA: ${SHA} \\| File:`;
|
||||||
|
const idxContentRegex = new RegExp(`${regexPrefix} \\/index\\.html$`);
|
||||||
|
const barContentRegex = new RegExp(`${regexPrefix} \\/foo\\/bar\\.js$`);
|
||||||
|
|
||||||
uploadBuild(pr9, sha9, archivePath).
|
await circleBuild(payload(BUILD)).then(h.verifyResponse(201));
|
||||||
then(() => Promise.all([
|
await Promise.all([
|
||||||
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)),
|
getFile(PR, SHA, 'index.html').then(h.verifyResponse(200, idxContentRegex)),
|
||||||
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)),
|
getFile(PR, SHA, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex)),
|
||||||
])).
|
]);
|
||||||
then(done);
|
|
||||||
|
expect({ prNum: PR }).toExistAsABuild();
|
||||||
|
expect({ prNum: PR, isPublic: false }).not.toExistAsABuild();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should be able to upload but not serve a hidden build', done => {
|
it('should be able to create but not serve a hidden preview', async () => {
|
||||||
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`;
|
const BUILD = BuildNums.TRUST_CHECK_UNTRUSTED;
|
||||||
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
|
const PR = PrNums.TRUST_CHECK_UNTRUSTED;
|
||||||
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
|
|
||||||
|
|
||||||
h.createDummyArchive(pr9, sha9, archivePath);
|
await circleBuild(payload(BUILD)).then(h.verifyResponse(202));
|
||||||
|
await Promise.all([
|
||||||
|
getFile(PR, SHA, 'index.html').then(h.verifyResponse(404)),
|
||||||
|
getFile(PR, SHA, 'foo/bar.js').then(h.verifyResponse(404)),
|
||||||
|
]);
|
||||||
|
|
||||||
uploadBuild(pr9, sha9, archivePath, c.BV_verify_verifiedNotTrusted).
|
expect({ prNum: PR }).not.toExistAsABuild();
|
||||||
then(() => Promise.all([
|
expect({ prNum: PR, isPublic: false }).toExistAsABuild();
|
||||||
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(404)),
|
|
||||||
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(404)),
|
|
||||||
])).
|
|
||||||
then(() => {
|
|
||||||
expect(h.buildExists(pr9, sha9)).toBe(false);
|
|
||||||
expect(h.buildExists(pr9, sha9, false)).toBe(true);
|
|
||||||
expect(h.readBuildFile(pr9, sha9, 'index.html', false)).toMatch(idxContentRegex9);
|
|
||||||
expect(h.readBuildFile(pr9, sha9, 'foo/bar.js', false)).toMatch(barContentRegex9);
|
|
||||||
}).
|
|
||||||
then(done);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should reject an upload if verification fails', done => {
|
it('should reject if verification fails', async () => {
|
||||||
const errorRegex9 = new RegExp(`Error while verifying upload for PR ${pr9}: Test`);
|
const BUILD = BuildNums.TRUST_CHECK_ERROR;
|
||||||
|
const PR = PrNums.TRUST_CHECK_ERROR;
|
||||||
|
|
||||||
h.createDummyArchive(pr9, sha9, archivePath);
|
await circleBuild(payload(BUILD)).then(h.verifyResponse(500));
|
||||||
|
expect({ prNum: PR }).toExistAsAnArtifact();
|
||||||
uploadBuild(pr9, sha9, archivePath, c.BV_verify_error).
|
expect({ prNum: PR }).not.toExistAsABuild();
|
||||||
then(h.verifyResponse(403, errorRegex9)).
|
expect({ prNum: PR, isPublic: false }).not.toExistAsABuild();
|
||||||
then(() => {
|
|
||||||
expect(h.buildExists(pr9)).toBe(false);
|
|
||||||
expect(h.buildExists(pr9, '', false)).toBe(false);
|
|
||||||
}).
|
|
||||||
then(done);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should be able to notify that a PR has been updated (and do nothing)', done => {
|
it('should be able to notify that a PR has been updated (and do nothing)', async () => {
|
||||||
prUpdated(+pr9).
|
await prUpdated(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER).then(h.verifyResponse(200));
|
||||||
then(h.verifyResponse(200)).
|
|
||||||
then(() => {
|
|
||||||
// The PR should still not exist.
|
// The PR should still not exist.
|
||||||
expect(h.buildExists(pr9, '', false)).toBe(false);
|
expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, isPublic: false }).not.toExistAsABuild();
|
||||||
expect(h.buildExists(pr9, '', true)).toBe(false);
|
expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, isPublic: true }).not.toExistAsABuild();
|
||||||
}).
|
|
||||||
then(done);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
@ -103,215 +82,186 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
|
|||||||
|
|
||||||
describe('for an existing PR', () => {
|
describe('for an existing PR', () => {
|
||||||
|
|
||||||
it('should be able to upload and serve a public build', done => {
|
it('should be able to create and serve a public preview', async () => {
|
||||||
const regexPrefix0 = `^PR: ${pr9} \\| SHA: ${sha0} \\| File:`;
|
const BUILD = BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
|
||||||
const idxContentRegex0 = new RegExp(`${regexPrefix0} \\/index\\.html$`);
|
const PR = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
|
||||||
const barContentRegex0 = new RegExp(`${regexPrefix0} \\/foo\\/bar\\.js$`);
|
|
||||||
|
|
||||||
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`;
|
const regexPrefix1 = `^PR: ${PR} \\| SHA: ${ALT_SHA} \\| File:`;
|
||||||
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
|
const idxContentRegex1 = new RegExp(`${regexPrefix1} \\/index\\.html$`);
|
||||||
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
|
const barContentRegex1 = new RegExp(`${regexPrefix1} \\/foo\\/bar\\.js$`);
|
||||||
|
|
||||||
h.createDummyBuild(pr9, sha0);
|
const regexPrefix2 = `^BUILD: ${BUILD} \\| PR: ${PR} \\| SHA: ${SHA} \\| File:`;
|
||||||
h.createDummyArchive(pr9, sha9, archivePath);
|
const idxContentRegex2 = new RegExp(`${regexPrefix2} \\/index\\.html$`);
|
||||||
|
const barContentRegex2 = new RegExp(`${regexPrefix2} \\/foo\\/bar\\.js$`);
|
||||||
|
|
||||||
uploadBuild(pr9, sha9, archivePath).
|
h.createDummyBuild(PR, ALT_SHA);
|
||||||
then(() => Promise.all([
|
await circleBuild(payload(BUILD)).then(h.verifyResponse(201));
|
||||||
getFile(pr9, sha0, 'index.html').then(h.verifyResponse(200, idxContentRegex0)),
|
await Promise.all([
|
||||||
getFile(pr9, sha0, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex0)),
|
getFile(PR, ALT_SHA, 'index.html').then(h.verifyResponse(200, idxContentRegex1)),
|
||||||
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)),
|
getFile(PR, ALT_SHA, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex1)),
|
||||||
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)),
|
getFile(PR, SHA, 'index.html').then(h.verifyResponse(200, idxContentRegex2)),
|
||||||
])).
|
getFile(PR, SHA, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex2)),
|
||||||
then(done);
|
]);
|
||||||
|
|
||||||
|
expect({ prNum: PR, sha: SHA }).toExistAsABuild();
|
||||||
|
expect({ prNum: PR, sha: ALT_SHA }).toExistAsABuild();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should be able to upload but not serve a hidden build', done => {
|
it('should be able to create but not serve a hidden preview', async () => {
|
||||||
const regexPrefix0 = `^PR: ${pr9} \\| SHA: ${sha0} \\| File:`;
|
const BUILD = BuildNums.TRUST_CHECK_UNTRUSTED;
|
||||||
const idxContentRegex0 = new RegExp(`${regexPrefix0} \\/index\\.html$`);
|
const PR = PrNums.TRUST_CHECK_UNTRUSTED;
|
||||||
const barContentRegex0 = new RegExp(`${regexPrefix0} \\/foo\\/bar\\.js$`);
|
|
||||||
|
|
||||||
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`;
|
h.createDummyBuild(PR, ALT_SHA, false);
|
||||||
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
|
await circleBuild(payload(BUILD)).then(h.verifyResponse(202));
|
||||||
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
|
|
||||||
|
|
||||||
h.createDummyBuild(pr9, sha0, false);
|
await Promise.all([
|
||||||
h.createDummyArchive(pr9, sha9, archivePath);
|
getFile(PR, ALT_SHA, 'index.html').then(h.verifyResponse(404)),
|
||||||
|
getFile(PR, ALT_SHA, 'foo/bar.js').then(h.verifyResponse(404)),
|
||||||
|
getFile(PR, SHA, 'index.html').then(h.verifyResponse(404)),
|
||||||
|
getFile(PR, SHA, 'foo/bar.js').then(h.verifyResponse(404)),
|
||||||
|
]);
|
||||||
|
|
||||||
uploadBuild(pr9, sha9, archivePath, c.BV_verify_verifiedNotTrusted).
|
expect({ prNum: PR, sha: SHA }).not.toExistAsABuild();
|
||||||
then(() => Promise.all([
|
expect({ prNum: PR, sha: SHA, isPublic: false }).toExistAsABuild();
|
||||||
getFile(pr9, sha0, 'index.html').then(h.verifyResponse(404)),
|
expect({ prNum: PR, sha: ALT_SHA }).not.toExistAsABuild();
|
||||||
getFile(pr9, sha0, 'foo/bar.js').then(h.verifyResponse(404)),
|
expect({ prNum: PR, sha: ALT_SHA, isPublic: false }).toExistAsABuild();
|
||||||
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(404)),
|
|
||||||
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(404)),
|
|
||||||
])).
|
|
||||||
then(() => {
|
|
||||||
expect(h.buildExists(pr9, sha9)).toBe(false);
|
|
||||||
expect(h.buildExists(pr9, sha9, false)).toBe(true);
|
|
||||||
expect(h.readBuildFile(pr9, sha0, 'index.html', false)).toMatch(idxContentRegex0);
|
|
||||||
expect(h.readBuildFile(pr9, sha0, 'foo/bar.js', false)).toMatch(barContentRegex0);
|
|
||||||
expect(h.readBuildFile(pr9, sha9, 'index.html', false)).toMatch(idxContentRegex9);
|
|
||||||
expect(h.readBuildFile(pr9, sha9, 'foo/bar.js', false)).toMatch(barContentRegex9);
|
|
||||||
}).
|
|
||||||
then(done);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should reject an upload if verification fails', done => {
|
it('should reject if verification fails', async () => {
|
||||||
const errorRegex9 = new RegExp(`Error while verifying upload for PR ${pr9}: Test`);
|
const BUILD = BuildNums.TRUST_CHECK_ERROR;
|
||||||
|
const PR = PrNums.TRUST_CHECK_ERROR;
|
||||||
|
|
||||||
h.createDummyBuild(pr9, sha0);
|
h.createDummyBuild(PR, ALT_SHA, false);
|
||||||
h.createDummyArchive(pr9, sha9, archivePath);
|
|
||||||
|
|
||||||
uploadBuild(pr9, sha9, archivePath, c.BV_verify_error).
|
await circleBuild(payload(BUILD)).then(h.verifyResponse(500));
|
||||||
then(h.verifyResponse(403, errorRegex9)).
|
|
||||||
then(() => {
|
|
||||||
expect(h.buildExists(pr9)).toBe(true);
|
|
||||||
expect(h.buildExists(pr9, sha0)).toBe(true);
|
|
||||||
expect(h.buildExists(pr9, sha9)).toBe(false);
|
|
||||||
}).
|
|
||||||
then(done);
|
|
||||||
|
|
||||||
|
expect({ prNum: PR }).toExistAsAnArtifact();
|
||||||
|
expect({ prNum: PR }).not.toExistAsABuild();
|
||||||
|
expect({ prNum: PR, isPublic: false }).not.toExistAsABuild();
|
||||||
|
expect({ prNum: PR, sha: ALT_SHA, isPublic: false }).toExistAsABuild();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should not be able to overwrite an existing public build', done => {
|
it('should not be able to overwrite an existing public preview', async () => {
|
||||||
const regexPrefix9 = `^PR: ${pr9} \\| SHA: ${sha9} \\| File:`;
|
const BUILD = BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
|
||||||
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
|
const PR = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
|
||||||
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
|
|
||||||
|
|
||||||
h.createDummyBuild(pr9, sha9);
|
const regexPrefix = `^PR: ${PR} \\| SHA: ${SHA} \\| File:`;
|
||||||
h.createDummyArchive(pr9, sha9, archivePath);
|
const idxContentRegex = new RegExp(`${regexPrefix} \\/index\\.html$`);
|
||||||
|
const barContentRegex = new RegExp(`${regexPrefix} \\/foo\\/bar\\.js$`);
|
||||||
|
|
||||||
uploadBuild(pr9, sha9, archivePath).
|
h.createDummyBuild(PR, SHA);
|
||||||
then(h.verifyResponse(409)).
|
|
||||||
then(() => Promise.all([
|
await circleBuild(payload(BUILD)).then(h.verifyResponse(409));
|
||||||
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)),
|
await Promise.all([
|
||||||
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)),
|
getFile(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, SHA, 'index.html').then(h.verifyResponse(200, idxContentRegex)),
|
||||||
])).
|
getFile(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, SHA, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex)),
|
||||||
then(done);
|
]);
|
||||||
|
|
||||||
|
expect({ prNum: PR }).toExistAsAnArtifact();
|
||||||
|
expect({ prNum: PR }).toExistAsABuild();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should not be able to overwrite an existing hidden build', done => {
|
it('should not be able to overwrite an existing hidden preview', async () => {
|
||||||
const regexPrefix9 = `^PR: ${pr9} \\| SHA: ${sha9} \\| File:`;
|
const BUILD = BuildNums.TRUST_CHECK_UNTRUSTED;
|
||||||
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
|
const PR = PrNums.TRUST_CHECK_UNTRUSTED;
|
||||||
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
|
h.createDummyBuild(PR, SHA, false);
|
||||||
|
|
||||||
h.createDummyBuild(pr9, sha9, false);
|
await circleBuild(payload(BUILD)).then(h.verifyResponse(409));
|
||||||
h.createDummyArchive(pr9, sha9, archivePath);
|
|
||||||
|
|
||||||
uploadBuild(pr9, sha9, archivePath, c.BV_verify_verifiedNotTrusted).
|
expect({ prNum: PR }).toExistAsAnArtifact();
|
||||||
then(h.verifyResponse(409)).
|
expect({ prNum: PR, isPublic: false }).toExistAsABuild();
|
||||||
then(() => {
|
|
||||||
expect(h.readBuildFile(pr9, sha9, 'index.html', false)).toMatch(idxContentRegex9);
|
|
||||||
expect(h.readBuildFile(pr9, sha9, 'foo/bar.js', false)).toMatch(barContentRegex9);
|
|
||||||
}).
|
|
||||||
then(done);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should be able to request re-checking visibility (if outdated)', done => {
|
it('should be able to request re-checking visibility (if outdated)', async () => {
|
||||||
const publicPr = pr9;
|
const publicPr = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
|
||||||
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
|
const hiddenPr = PrNums.TRUST_CHECK_UNTRUSTED;
|
||||||
|
|
||||||
h.createDummyBuild(publicPr, sha9, false);
|
h.createDummyBuild(publicPr, SHA, false);
|
||||||
h.createDummyBuild(hiddenPr, sha9, true);
|
h.createDummyBuild(hiddenPr, SHA, true);
|
||||||
|
|
||||||
// PR visibilities are outdated (i.e. the opposte of what the should).
|
// PR visibilities are outdated (i.e. the opposte of what the should).
|
||||||
expect(h.buildExists(publicPr, '', false)).toBe(true);
|
expect({ prNum: publicPr, sha: SHA, isPublic: false }).toExistAsABuild(false);
|
||||||
expect(h.buildExists(publicPr, '', true)).toBe(false);
|
expect({ prNum: publicPr, sha: SHA, isPublic: true }).not.toExistAsABuild(false);
|
||||||
expect(h.buildExists(hiddenPr, '', false)).toBe(false);
|
expect({ prNum: hiddenPr, sha: SHA, isPublic: false }).not.toExistAsABuild(false);
|
||||||
expect(h.buildExists(hiddenPr, '', true)).toBe(true);
|
expect({ prNum: hiddenPr, sha: SHA, isPublic: true }).toExistAsABuild(false);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
prUpdated(publicPr).then(h.verifyResponse(200)),
|
||||||
|
prUpdated(hiddenPr).then(h.verifyResponse(200)),
|
||||||
|
]);
|
||||||
|
|
||||||
Promise.
|
|
||||||
all([
|
|
||||||
prUpdated(+publicPr).then(h.verifyResponse(200)),
|
|
||||||
prUpdated(+hiddenPr).then(h.verifyResponse(200)),
|
|
||||||
]).
|
|
||||||
then(() => {
|
|
||||||
// PR visibilities should have been updated.
|
// PR visibilities should have been updated.
|
||||||
expect(h.buildExists(publicPr, '', false)).toBe(false);
|
expect({ prNum: publicPr, isPublic: false }).not.toExistAsABuild();
|
||||||
expect(h.buildExists(publicPr, '', true)).toBe(true);
|
expect({ prNum: publicPr, isPublic: true }).toExistAsABuild();
|
||||||
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
|
expect({ prNum: hiddenPr, isPublic: false }).toExistAsABuild();
|
||||||
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
|
expect({ prNum: hiddenPr, isPublic: true }).not.toExistAsABuild();
|
||||||
}).
|
|
||||||
then(() => {
|
|
||||||
h.deletePrDir(publicPr, true);
|
|
||||||
h.deletePrDir(hiddenPr, false);
|
|
||||||
}).
|
|
||||||
then(done);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should be able to request re-checking visibility (if up-to-date)', done => {
|
it('should be able to request re-checking visibility (if up-to-date)', async () => {
|
||||||
const publicPr = pr9;
|
const publicPr = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
|
||||||
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
|
const hiddenPr = PrNums.TRUST_CHECK_UNTRUSTED;
|
||||||
|
|
||||||
h.createDummyBuild(publicPr, sha9, true);
|
h.createDummyBuild(publicPr, SHA, true);
|
||||||
h.createDummyBuild(hiddenPr, sha9, false);
|
h.createDummyBuild(hiddenPr, SHA, false);
|
||||||
|
|
||||||
// PR visibilities are already up-to-date.
|
// PR visibilities are already up-to-date.
|
||||||
expect(h.buildExists(publicPr, '', false)).toBe(false);
|
expect({ prNum: publicPr, sha: SHA, isPublic: false }).not.toExistAsABuild(false);
|
||||||
expect(h.buildExists(publicPr, '', true)).toBe(true);
|
expect({ prNum: publicPr, sha: SHA, isPublic: true }).toExistAsABuild(false);
|
||||||
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
|
expect({ prNum: hiddenPr, sha: SHA, isPublic: false }).toExistAsABuild(false);
|
||||||
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
|
expect({ prNum: hiddenPr, sha: SHA, isPublic: true }).not.toExistAsABuild(false);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
prUpdated(publicPr).then(h.verifyResponse(200)),
|
||||||
|
prUpdated(hiddenPr).then(h.verifyResponse(200)),
|
||||||
|
]);
|
||||||
|
|
||||||
Promise.
|
|
||||||
all([
|
|
||||||
prUpdated(+publicPr).then(h.verifyResponse(200)),
|
|
||||||
prUpdated(+hiddenPr).then(h.verifyResponse(200)),
|
|
||||||
]).
|
|
||||||
then(() => {
|
|
||||||
// PR visibilities are still up-to-date.
|
// PR visibilities are still up-to-date.
|
||||||
expect(h.buildExists(publicPr, '', false)).toBe(false);
|
expect({ prNum: publicPr, isPublic: true }).toExistAsABuild();
|
||||||
expect(h.buildExists(publicPr, '', true)).toBe(true);
|
expect({ prNum: publicPr, isPublic: false }).not.toExistAsABuild();
|
||||||
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
|
expect({ prNum: hiddenPr, isPublic: true }).not.toExistAsABuild();
|
||||||
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
|
expect({ prNum: hiddenPr, isPublic: false }).toExistAsABuild();
|
||||||
}).
|
|
||||||
then(done);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should reject a request if re-checking visibility fails', done => {
|
it('should reject a request if re-checking visibility fails', async () => {
|
||||||
const errorPr = String(c.BV_getPrIsTrusted_error);
|
const errorPr = PrNums.TRUST_CHECK_ERROR;
|
||||||
|
|
||||||
h.createDummyBuild(errorPr, sha9, true);
|
h.createDummyBuild(errorPr, SHA, true);
|
||||||
|
|
||||||
expect(h.buildExists(errorPr, '', false)).toBe(false);
|
expect({ prNum: errorPr, isPublic: false }).not.toExistAsABuild(false);
|
||||||
expect(h.buildExists(errorPr, '', true)).toBe(true);
|
expect({ prNum: errorPr, isPublic: true }).toExistAsABuild(false);
|
||||||
|
|
||||||
|
await prUpdated(errorPr).then(h.verifyResponse(500, /TRUST_CHECK_ERROR/));
|
||||||
|
|
||||||
prUpdated(+errorPr).
|
|
||||||
then(h.verifyResponse(500, /Test/)).
|
|
||||||
then(() => {
|
|
||||||
// PR visibility should not have been updated.
|
// PR visibility should not have been updated.
|
||||||
expect(h.buildExists(errorPr, '', false)).toBe(false);
|
expect({ prNum: errorPr, isPublic: false }).not.toExistAsABuild();
|
||||||
expect(h.buildExists(errorPr, '', true)).toBe(true);
|
expect({ prNum: errorPr, isPublic: true }).toExistAsABuild();
|
||||||
}).
|
|
||||||
then(done);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should reject a request if updating visibility fails', done => {
|
it('should reject a request if updating visibility fails', async () => {
|
||||||
// One way to cause an error is to have both a public and a hidden directory for the same PR.
|
// One way to cause an error is to have both a public and a hidden directory for the same PR.
|
||||||
h.createDummyBuild(pr9, sha9, false);
|
h.createDummyBuild(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, SHA, false);
|
||||||
h.createDummyBuild(pr9, sha9, true);
|
h.createDummyBuild(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, SHA, true);
|
||||||
|
|
||||||
const hiddenPrDir = h.getPrDir(pr9, false);
|
const hiddenPrDir = h.getPrDir(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, false);
|
||||||
const publicPrDir = h.getPrDir(pr9, true);
|
const publicPrDir = h.getPrDir(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, true);
|
||||||
const bodyRegex = new RegExp(`Request to move '${hiddenPrDir}' to existing directory '${publicPrDir}'`);
|
const bodyRegex = new RegExp(`Request to move '${hiddenPrDir}' to existing directory '${publicPrDir}'`);
|
||||||
|
|
||||||
expect(h.buildExists(pr9, '', false)).toBe(true);
|
expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, isPublic: false }).toExistAsABuild(false);
|
||||||
expect(h.buildExists(pr9, '', true)).toBe(true);
|
expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, isPublic: true }).toExistAsABuild(false);
|
||||||
|
|
||||||
|
await prUpdated(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER).then(h.verifyResponse(409, bodyRegex));
|
||||||
|
|
||||||
prUpdated(+pr9).
|
|
||||||
then(h.verifyResponse(409, bodyRegex)).
|
|
||||||
then(() => {
|
|
||||||
// PR visibility should not have been updated.
|
// PR visibility should not have been updated.
|
||||||
expect(h.buildExists(pr9, '', false)).toBe(true);
|
expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, isPublic: false }).toExistAsABuild();
|
||||||
expect(h.buildExists(pr9, '', true)).toBe(true);
|
expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, isPublic: true }).toExistAsABuild();
|
||||||
}).
|
|
||||||
then(done);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
import '../preview-server';
|
||||||
|
import './mock-external-apis';
|
@ -1,38 +0,0 @@
|
|||||||
// Imports
|
|
||||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
|
||||||
import {BUILD_VERIFICATION_STATUS, BuildVerifier} from '../upload-server/build-verifier';
|
|
||||||
import {UploadError} from '../upload-server/upload-error';
|
|
||||||
import * as c from './constants';
|
|
||||||
|
|
||||||
// Run
|
|
||||||
// TODO(gkalpak): Add e2e tests to cover these interactions as well.
|
|
||||||
GithubPullRequests.prototype.addComment = () => Promise.resolve();
|
|
||||||
BuildVerifier.prototype.getPrIsTrusted = (pr: number) => {
|
|
||||||
switch (pr) {
|
|
||||||
case c.BV_getPrIsTrusted_error:
|
|
||||||
// For e2e tests, fake an error.
|
|
||||||
return Promise.reject('Test');
|
|
||||||
case c.BV_getPrIsTrusted_notTrusted:
|
|
||||||
// For e2e tests, fake an untrusted PR (`false`).
|
|
||||||
return Promise.resolve(false);
|
|
||||||
default:
|
|
||||||
// For e2e tests, default to trusted PRs (`true`).
|
|
||||||
return Promise.resolve(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
BuildVerifier.prototype.verify = (expectedPr: number, authHeader: string) => {
|
|
||||||
switch (authHeader) {
|
|
||||||
case c.BV_verify_error:
|
|
||||||
// For e2e tests, fake a verification error.
|
|
||||||
return Promise.reject(new UploadError(403, `Error while verifying upload for PR ${expectedPr}: Test`));
|
|
||||||
case c.BV_verify_verifiedNotTrusted:
|
|
||||||
// For e2e tests, fake a `verifiedNotTrusted` verification status.
|
|
||||||
return Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedNotTrusted);
|
|
||||||
default:
|
|
||||||
// For e2e tests, default to `verifiedAndTrusted` verification status.
|
|
||||||
return Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedAndTrusted);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// tslint:disable-next-line: no-var-requires
|
|
||||||
require('../upload-server/index');
|
|
30
aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/tar-stream.d.ts
vendored
Normal file
30
aio/aio-builds-setup/dockerbuild/scripts-js/lib/verify-setup/tar-stream.d.ts
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
declare module 'tar-stream' {
|
||||||
|
|
||||||
|
import {Readable, Writable} from 'stream';
|
||||||
|
|
||||||
|
export interface Pack extends Readable {
|
||||||
|
entry(header: Header, callback?: (err?: any) => {}): Writable;
|
||||||
|
entry(header: Header, contents: string, callback?: (err?: any) => {}): Writable;
|
||||||
|
entry(header: Header, buffer: Buffer, callback?: (err?: any) => {}): Writable;
|
||||||
|
entry(header: Header, buffer: string|Buffer, callback?: (err?: any) => {}): Writable;
|
||||||
|
finalize();
|
||||||
|
destroy(err: any);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Header {
|
||||||
|
name: string;
|
||||||
|
mode?: number;
|
||||||
|
uid?: number;
|
||||||
|
gid?: number;
|
||||||
|
size?: number;
|
||||||
|
mtime?: Date;
|
||||||
|
type?: type;
|
||||||
|
linkname?: string;
|
||||||
|
uname?: string;
|
||||||
|
gname?: string;
|
||||||
|
devmajor?: number;
|
||||||
|
devminor?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pack(): Pack;
|
||||||
|
}
|
@ -1,571 +0,0 @@
|
|||||||
// Imports
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import * as c from './constants';
|
|
||||||
import {CmdResult, helper as h} from './helper';
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
describe('upload-server (on HTTP)', () => {
|
|
||||||
const hostname = h.uploadHostname;
|
|
||||||
const port = h.uploadPort;
|
|
||||||
const host = `${hostname}:${port}`;
|
|
||||||
const pr = '9';
|
|
||||||
const sha9 = '9'.repeat(40);
|
|
||||||
const sha0 = '0'.repeat(40);
|
|
||||||
|
|
||||||
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000);
|
|
||||||
afterEach(() => h.cleanUp());
|
|
||||||
|
|
||||||
|
|
||||||
describe(`${host}/create-build/<pr>/<sha>`, () => {
|
|
||||||
const authorizationHeader = `--header "Authorization: Token FOO"`;
|
|
||||||
const xFileHeader = `--header "X-File: ${h.buildsDir}/snapshot.tar.gz"`;
|
|
||||||
const defaultHeaders = `${authorizationHeader} ${xFileHeader}`;
|
|
||||||
const curl = (url: string, headers = defaultHeaders) => `curl -iL ${headers} ${url}`;
|
|
||||||
|
|
||||||
|
|
||||||
it('should disallow non-GET requests', done => {
|
|
||||||
const url = `http://${host}/create-build/${pr}/${sha9}`;
|
|
||||||
const bodyRegex = /^Unknown resource/;
|
|
||||||
|
|
||||||
Promise.all([
|
|
||||||
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
|
||||||
h.runCmd(`curl -iLX POST ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
|
||||||
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
|
||||||
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
|
||||||
]).then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should reject requests without an \'AUTHORIZATION\' header', done => {
|
|
||||||
const headers1 = '';
|
|
||||||
const headers2 = '--header "AUTHORIXATION: "';
|
|
||||||
const url = `http://${host}/create-build/${pr}/${sha9}`;
|
|
||||||
const bodyRegex = /^Missing or empty 'AUTHORIZATION' header/;
|
|
||||||
|
|
||||||
Promise.all([
|
|
||||||
h.runCmd(curl(url, headers1)).then(h.verifyResponse(401, bodyRegex)),
|
|
||||||
h.runCmd(curl(url, headers2)).then(h.verifyResponse(401, bodyRegex)),
|
|
||||||
]).then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should reject requests without an \'X-FILE\' header', done => {
|
|
||||||
const headers1 = authorizationHeader;
|
|
||||||
const headers2 = `${authorizationHeader} --header "X-FILE: "`;
|
|
||||||
const url = `http://${host}/create-build/${pr}/${sha9}`;
|
|
||||||
const bodyRegex = /^Missing or empty 'X-FILE' header/;
|
|
||||||
|
|
||||||
Promise.all([
|
|
||||||
h.runCmd(curl(url, headers1)).then(h.verifyResponse(400, bodyRegex)),
|
|
||||||
h.runCmd(curl(url, headers2)).then(h.verifyResponse(400, bodyRegex)),
|
|
||||||
]).then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should reject requests for which the PR verification fails', done => {
|
|
||||||
const headers = `--header "Authorization: ${c.BV_verify_error}" ${xFileHeader}`;
|
|
||||||
const url = `http://${host}/create-build/${pr}/${sha9}`;
|
|
||||||
const bodyRegex = new RegExp(`Error while verifying upload for PR ${pr}: Test`);
|
|
||||||
|
|
||||||
h.runCmd(curl(url, headers)).
|
|
||||||
then(h.verifyResponse(403, bodyRegex)).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should respond with 404 for unknown paths', done => {
|
|
||||||
const cmdPrefix = curl(`http://${host}`);
|
|
||||||
|
|
||||||
Promise.all([
|
|
||||||
h.runCmd(`${cmdPrefix}/foo/create-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`${cmdPrefix}/foo-create-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`${cmdPrefix}/fooncreate-build/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`${cmdPrefix}/create-build/foo/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`${cmdPrefix}/create-build-foo/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`${cmdPrefix}/create-buildnfoo/${pr}/${sha9}`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`${cmdPrefix}/create-build/pr${pr}/${sha9}`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`${cmdPrefix}/create-build/${pr}/${sha9}42`).then(h.verifyResponse(404)),
|
|
||||||
]).then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should reject PRs with leading zeros', done => {
|
|
||||||
h.runCmd(curl(`http://${host}/create-build/0${pr}/${sha9}`)).
|
|
||||||
then(h.verifyResponse(404)).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should accept SHAs with leading zeros (but not trim the zeros)', done => {
|
|
||||||
Promise.all([
|
|
||||||
h.runCmd(curl(`http://${host}/create-build/${pr}/0${sha9}`)).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(curl(`http://${host}/create-build/${pr}/${sha9}`)).then(h.verifyResponse(500)),
|
|
||||||
h.runCmd(curl(`http://${host}/create-build/${pr}/${sha0}`)).then(h.verifyResponse(500)),
|
|
||||||
]).then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
[true, false].forEach(isPublic => describe(`(for ${isPublic ? 'public' : 'hidden'} builds)`, () => {
|
|
||||||
const authorizationHeader2 = isPublic ?
|
|
||||||
authorizationHeader : `--header "Authorization: ${c.BV_verify_verifiedNotTrusted}"`;
|
|
||||||
const cmdPrefix = curl('', `${authorizationHeader2} ${xFileHeader}`);
|
|
||||||
const overwriteRe = RegExp(`^Request to overwrite existing ${isPublic ? 'public' : 'non-public'} directory`);
|
|
||||||
|
|
||||||
|
|
||||||
it('should not overwrite existing builds', done => {
|
|
||||||
h.createDummyBuild(pr, sha9, isPublic);
|
|
||||||
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain('index.html');
|
|
||||||
|
|
||||||
h.writeBuildFile(pr, sha9, 'index.html', 'My content', isPublic);
|
|
||||||
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toBe('My content');
|
|
||||||
|
|
||||||
h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9}`).
|
|
||||||
then(h.verifyResponse(409, overwriteRe)).
|
|
||||||
then(() => expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toBe('My content')).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should not overwrite existing builds (even if the SHA is different)', done => {
|
|
||||||
// Since only the first few characters of the SHA are used, it is possible for two different
|
|
||||||
// SHAs to correspond to the same directory. In that case, we don't want the second SHA to
|
|
||||||
// overwrite the first.
|
|
||||||
|
|
||||||
const sha9Almost = sha9.replace(/.$/, '8');
|
|
||||||
expect(sha9Almost).not.toBe(sha9);
|
|
||||||
|
|
||||||
h.createDummyBuild(pr, sha9, isPublic);
|
|
||||||
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain('index.html');
|
|
||||||
|
|
||||||
h.writeBuildFile(pr, sha9, 'index.html', 'My content', isPublic);
|
|
||||||
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toBe('My content');
|
|
||||||
|
|
||||||
h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9Almost}`).
|
|
||||||
then(h.verifyResponse(409, overwriteRe)).
|
|
||||||
then(() => expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toBe('My content')).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should delete the PR directory on error (for new PR)', done => {
|
|
||||||
h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9}`).
|
|
||||||
then(h.verifyResponse(500)).
|
|
||||||
then(() => expect(h.buildExists(pr, '', isPublic)).toBe(false)).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should only delete the SHA directory on error (for existing PR)', done => {
|
|
||||||
h.createDummyBuild(pr, sha0, isPublic);
|
|
||||||
|
|
||||||
h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9}`).
|
|
||||||
then(h.verifyResponse(500)).
|
|
||||||
then(() => {
|
|
||||||
expect(h.buildExists(pr, sha9, isPublic)).toBe(false);
|
|
||||||
expect(h.buildExists(pr, '', isPublic)).toBe(true);
|
|
||||||
}).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe('on successful upload', () => {
|
|
||||||
const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz');
|
|
||||||
const statusCode = isPublic ? 201 : 202;
|
|
||||||
let uploadPromise: Promise<CmdResult>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
h.createDummyArchive(pr, sha9, archivePath);
|
|
||||||
uploadPromise = h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9}`);
|
|
||||||
});
|
|
||||||
afterEach(() => h.deletePrDir(pr, isPublic));
|
|
||||||
|
|
||||||
|
|
||||||
it(`should respond with ${statusCode}`, done => {
|
|
||||||
uploadPromise.then(h.verifyResponse(statusCode)).then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should extract the contents of the uploaded file', done => {
|
|
||||||
uploadPromise.
|
|
||||||
then(() => {
|
|
||||||
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain(`uploaded/${pr}`);
|
|
||||||
expect(h.readBuildFile(pr, sha9, 'foo/bar.js', isPublic)).toContain(`uploaded/${pr}`);
|
|
||||||
}).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it(`should create files/directories owned by '${h.wwwUser}'`, done => {
|
|
||||||
const prDir = h.getPrDir(pr, isPublic);
|
|
||||||
const shaDir = h.getShaDir(prDir, sha9);
|
|
||||||
const idxPath = path.join(shaDir, 'index.html');
|
|
||||||
const barPath = path.join(shaDir, 'foo', 'bar.js');
|
|
||||||
|
|
||||||
uploadPromise.
|
|
||||||
then(() => Promise.all([
|
|
||||||
h.runCmd(`find ${shaDir}`),
|
|
||||||
h.runCmd(`find ${shaDir} -user ${h.wwwUser}`),
|
|
||||||
])).
|
|
||||||
then(([{stdout: allFiles}, {stdout: userFiles}]) => {
|
|
||||||
expect(userFiles).toBe(allFiles);
|
|
||||||
expect(userFiles).toContain(shaDir);
|
|
||||||
expect(userFiles).toContain(idxPath);
|
|
||||||
expect(userFiles).toContain(barPath);
|
|
||||||
}).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should delete the uploaded file', done => {
|
|
||||||
expect(fs.existsSync(archivePath)).toBe(true);
|
|
||||||
uploadPromise.
|
|
||||||
then(() => expect(fs.existsSync(archivePath)).toBe(false)).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should make the build directory non-writable', done => {
|
|
||||||
const prDir = h.getPrDir(pr, isPublic);
|
|
||||||
const shaDir = h.getShaDir(prDir, sha9);
|
|
||||||
const idxPath = path.join(shaDir, 'index.html');
|
|
||||||
const barPath = path.join(shaDir, 'foo', 'bar.js');
|
|
||||||
|
|
||||||
// See https://github.com/nodejs/node-v0.x-archive/issues/3045#issuecomment-4862588.
|
|
||||||
const isNotWritable = (fileOrDir: string) => {
|
|
||||||
const mode = fs.statSync(fileOrDir).mode;
|
|
||||||
// tslint:disable-next-line: no-bitwise
|
|
||||||
return !(mode & parseInt('222', 8));
|
|
||||||
};
|
|
||||||
|
|
||||||
uploadPromise.
|
|
||||||
then(() => {
|
|
||||||
expect(isNotWritable(shaDir)).toBe(true);
|
|
||||||
expect(isNotWritable(idxPath)).toBe(true);
|
|
||||||
expect(isNotWritable(barPath)).toBe(true);
|
|
||||||
}).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should ignore a legacy 40-chars long build directory (even if it starts with the same chars)', done => {
|
|
||||||
// It is possible that 40-chars long build directories exist, if they had been deployed
|
|
||||||
// before implementing the shorter build directory names. In that case, we don't want the
|
|
||||||
// second (shorter) name to be considered the same as the old one (even if they originate
|
|
||||||
// from the same SHA).
|
|
||||||
|
|
||||||
h.createDummyBuild(pr, sha9, isPublic, false, true);
|
|
||||||
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic, true)).toContain('index.html');
|
|
||||||
|
|
||||||
h.writeBuildFile(pr, sha9, 'index.html', 'My content', isPublic, true);
|
|
||||||
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic, true)).toBe('My content');
|
|
||||||
|
|
||||||
h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9}`).
|
|
||||||
then(h.verifyResponse(statusCode)).
|
|
||||||
then(() => {
|
|
||||||
expect(h.buildExists(pr, sha9, isPublic)).toBe(true);
|
|
||||||
expect(h.buildExists(pr, sha9, isPublic, true)).toBe(true);
|
|
||||||
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain('index.html');
|
|
||||||
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic, true)).toBe('My content');
|
|
||||||
}).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe('when the PR\'s visibility has changed', () => {
|
|
||||||
const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz');
|
|
||||||
const statusCode = isPublic ? 201 : 202;
|
|
||||||
|
|
||||||
const checkPrVisibility = (isPublic2: boolean) => {
|
|
||||||
expect(h.buildExists(pr, '', isPublic2)).toBe(true);
|
|
||||||
expect(h.buildExists(pr, '', !isPublic2)).toBe(false);
|
|
||||||
expect(h.buildExists(pr, sha0, isPublic2)).toBe(true);
|
|
||||||
expect(h.buildExists(pr, sha0, !isPublic2)).toBe(false);
|
|
||||||
};
|
|
||||||
const uploadBuild = (sha: string) => h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha}`);
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
h.createDummyBuild(pr, sha0, !isPublic);
|
|
||||||
h.createDummyArchive(pr, sha9, archivePath);
|
|
||||||
checkPrVisibility(!isPublic);
|
|
||||||
});
|
|
||||||
afterEach(() => h.deletePrDir(pr, isPublic));
|
|
||||||
|
|
||||||
|
|
||||||
it('should update the PR\'s visibility', done => {
|
|
||||||
uploadBuild(sha9).
|
|
||||||
then(h.verifyResponse(statusCode)).
|
|
||||||
then(() => {
|
|
||||||
checkPrVisibility(isPublic);
|
|
||||||
expect(h.buildExists(pr, sha9, isPublic)).toBe(true);
|
|
||||||
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain(`uploaded/${pr}`);
|
|
||||||
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toContain(sha9);
|
|
||||||
}).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should not overwrite existing builds (but keep the updated visibility)', done => {
|
|
||||||
expect(h.buildExists(pr, sha0, isPublic)).toBe(false);
|
|
||||||
|
|
||||||
uploadBuild(sha0).
|
|
||||||
then(h.verifyResponse(409, overwriteRe)).
|
|
||||||
then(() => {
|
|
||||||
checkPrVisibility(isPublic);
|
|
||||||
expect(h.readBuildFile(pr, sha0, 'index.html', isPublic)).toContain(pr);
|
|
||||||
expect(h.readBuildFile(pr, sha0, 'index.html', isPublic)).not.toContain(`uploaded/${pr}`);
|
|
||||||
expect(h.readBuildFile(pr, sha0, 'index.html', isPublic)).toContain(sha0);
|
|
||||||
expect(h.readBuildFile(pr, sha0, 'index.html', isPublic)).not.toContain(sha9);
|
|
||||||
}).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should reject the request if it fails to update the PR\'s visibility', done => {
|
|
||||||
// One way to cause an error is to have both a public and a hidden directory for the same PR.
|
|
||||||
h.createDummyBuild(pr, sha0, isPublic);
|
|
||||||
|
|
||||||
expect(h.buildExists(pr, sha0, isPublic)).toBe(true);
|
|
||||||
expect(h.buildExists(pr, sha0, !isPublic)).toBe(true);
|
|
||||||
|
|
||||||
const errorRegex = new RegExp(`^Request to move '${h.getPrDir(pr, !isPublic)}' ` +
|
|
||||||
`to existing directory '${h.getPrDir(pr, isPublic)}'.`);
|
|
||||||
|
|
||||||
uploadBuild(sha9).
|
|
||||||
then(h.verifyResponse(409, errorRegex)).
|
|
||||||
then(() => {
|
|
||||||
expect(h.buildExists(pr, sha0, isPublic)).toBe(true);
|
|
||||||
expect(h.buildExists(pr, sha0, !isPublic)).toBe(true);
|
|
||||||
expect(h.buildExists(pr, sha9, isPublic)).toBe(false);
|
|
||||||
expect(h.buildExists(pr, sha9, !isPublic)).toBe(false);
|
|
||||||
}).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
}));
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe(`${host}/health-check`, () => {
|
|
||||||
|
|
||||||
it('should respond with 200', done => {
|
|
||||||
Promise.all([
|
|
||||||
h.runCmd(`curl -iL http://${host}/health-check`).then(h.verifyResponse(200)),
|
|
||||||
h.runCmd(`curl -iL http://${host}/health-check/`).then(h.verifyResponse(200)),
|
|
||||||
]).then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should respond with 404 if the path does not match exactly', done => {
|
|
||||||
Promise.all([
|
|
||||||
h.runCmd(`curl -iL http://${host}/health-check/foo`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`curl -iL http://${host}/health-check-foo`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`curl -iL http://${host}/health-checknfoo`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`curl -iL http://${host}/foo/health-check`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`curl -iL http://${host}/foo-health-check`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`curl -iL http://${host}/foonhealth-check`).then(h.verifyResponse(404)),
|
|
||||||
]).then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe(`${host}/pr-updated`, () => {
|
|
||||||
const url = `http://${host}/pr-updated`;
|
|
||||||
|
|
||||||
// Helpers
|
|
||||||
const curl = (payload?: {number: number, action?: string}) => {
|
|
||||||
const payloadStr = payload && JSON.stringify(payload) || '';
|
|
||||||
return `curl -iLX POST --header "Content-Type: application/json" --data '${payloadStr}' ${url}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
it('should disallow non-POST requests', done => {
|
|
||||||
const bodyRegex = /^Unknown resource in request/;
|
|
||||||
|
|
||||||
Promise.all([
|
|
||||||
h.runCmd(`curl -iLX GET ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
|
||||||
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
|
||||||
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
|
||||||
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse(404, bodyRegex)),
|
|
||||||
]).then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should respond with 400 for requests without a payload', done => {
|
|
||||||
const bodyRegex = /^Missing or empty 'number' field in request/;
|
|
||||||
|
|
||||||
h.runCmd(curl()).
|
|
||||||
then(h.verifyResponse(400, bodyRegex)).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should respond with 400 for requests without a \'number\' field', done => {
|
|
||||||
const bodyRegex = /^Missing or empty 'number' field in request/;
|
|
||||||
|
|
||||||
Promise.all([
|
|
||||||
h.runCmd(curl({} as any)).then(h.verifyResponse(400, bodyRegex)),
|
|
||||||
h.runCmd(curl({number: null} as any)).then(h.verifyResponse(400, bodyRegex)),
|
|
||||||
]).then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should reject requests for which checking the PR visibility fails', done => {
|
|
||||||
h.runCmd(curl({number: c.BV_getPrIsTrusted_error})).
|
|
||||||
then(h.verifyResponse(500, /Test/)).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should respond with 404 for unknown paths', done => {
|
|
||||||
const mockPayload = JSON.stringify({number: +pr});
|
|
||||||
const cmdPrefix = `curl -iLX POST --data "${mockPayload}" http://${host}`;
|
|
||||||
|
|
||||||
Promise.all([
|
|
||||||
h.runCmd(`${cmdPrefix}/foo/pr-updated`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`${cmdPrefix}/foo-pr-updated`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`${cmdPrefix}/foonpr-updated`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`${cmdPrefix}/pr-updated/foo`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`${cmdPrefix}/pr-updated-foo`).then(h.verifyResponse(404)),
|
|
||||||
h.runCmd(`${cmdPrefix}/pr-updatednfoo`).then(h.verifyResponse(404)),
|
|
||||||
]).then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should do nothing if PR\'s visibility is already up-to-date', done => {
|
|
||||||
const publicPr = pr;
|
|
||||||
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
|
|
||||||
const checkVisibilities = () => {
|
|
||||||
// Public build is already public.
|
|
||||||
expect(h.buildExists(publicPr, '', false)).toBe(false);
|
|
||||||
expect(h.buildExists(publicPr, '', true)).toBe(true);
|
|
||||||
// Hidden build is already hidden.
|
|
||||||
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
|
|
||||||
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
h.createDummyBuild(publicPr, sha9, true);
|
|
||||||
h.createDummyBuild(hiddenPr, sha9, false);
|
|
||||||
checkVisibilities();
|
|
||||||
|
|
||||||
Promise.
|
|
||||||
all([
|
|
||||||
h.runCmd(curl({number: +publicPr, action: 'foo'})).then(h.verifyResponse(200)),
|
|
||||||
h.runCmd(curl({number: +hiddenPr, action: 'foo'})).then(h.verifyResponse(200)),
|
|
||||||
]).
|
|
||||||
// Visibilities should not have changed, because the specified action could not have triggered a change.
|
|
||||||
then(checkVisibilities).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should do nothing if \'action\' implies no visibility change', done => {
|
|
||||||
const publicPr = pr;
|
|
||||||
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
|
|
||||||
const checkVisibilities = () => {
|
|
||||||
// Public build is hidden atm.
|
|
||||||
expect(h.buildExists(publicPr, '', false)).toBe(true);
|
|
||||||
expect(h.buildExists(publicPr, '', true)).toBe(false);
|
|
||||||
// Hidden build is public atm.
|
|
||||||
expect(h.buildExists(hiddenPr, '', false)).toBe(false);
|
|
||||||
expect(h.buildExists(hiddenPr, '', true)).toBe(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
h.createDummyBuild(publicPr, sha9, false);
|
|
||||||
h.createDummyBuild(hiddenPr, sha9, true);
|
|
||||||
checkVisibilities();
|
|
||||||
|
|
||||||
Promise.
|
|
||||||
all([
|
|
||||||
h.runCmd(curl({number: +publicPr, action: 'foo'})).then(h.verifyResponse(200)),
|
|
||||||
h.runCmd(curl({number: +hiddenPr, action: 'foo'})).then(h.verifyResponse(200)),
|
|
||||||
]).
|
|
||||||
// Visibilities should not have changed, because the specified action could not have triggered a change.
|
|
||||||
then(checkVisibilities).
|
|
||||||
then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe('when the visiblity has changed', () => {
|
|
||||||
const publicPr = pr;
|
|
||||||
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// Create initial PR builds with opposite visibilities as the ones that will be reported:
|
|
||||||
// - The now public PR was previously hidden.
|
|
||||||
// - The now hidden PR was previously public.
|
|
||||||
h.createDummyBuild(publicPr, sha9, false);
|
|
||||||
h.createDummyBuild(hiddenPr, sha9, true);
|
|
||||||
|
|
||||||
expect(h.buildExists(publicPr, '', false)).toBe(true);
|
|
||||||
expect(h.buildExists(publicPr, '', true)).toBe(false);
|
|
||||||
expect(h.buildExists(hiddenPr, '', false)).toBe(false);
|
|
||||||
expect(h.buildExists(hiddenPr, '', true)).toBe(true);
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
|
||||||
// Expect PRs' visibility to have been updated:
|
|
||||||
// - The public PR should be actually public (previously it was hidden).
|
|
||||||
// - The hidden PR should be actually hidden (previously it was public).
|
|
||||||
expect(h.buildExists(publicPr, '', false)).toBe(false);
|
|
||||||
expect(h.buildExists(publicPr, '', true)).toBe(true);
|
|
||||||
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
|
|
||||||
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
|
|
||||||
|
|
||||||
h.deletePrDir(publicPr, true);
|
|
||||||
h.deletePrDir(hiddenPr, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should update the PR\'s visibility (action: undefined)', done => {
|
|
||||||
Promise.all([
|
|
||||||
h.runCmd(curl({number: +publicPr})).then(h.verifyResponse(200)),
|
|
||||||
h.runCmd(curl({number: +hiddenPr})).then(h.verifyResponse(200)),
|
|
||||||
]).then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should update the PR\'s visibility (action: labeled)', done => {
|
|
||||||
Promise.all([
|
|
||||||
h.runCmd(curl({number: +publicPr, action: 'labeled'})).then(h.verifyResponse(200)),
|
|
||||||
h.runCmd(curl({number: +hiddenPr, action: 'labeled'})).then(h.verifyResponse(200)),
|
|
||||||
]).then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should update the PR\'s visibility (action: unlabeled)', done => {
|
|
||||||
Promise.all([
|
|
||||||
h.runCmd(curl({number: +publicPr, action: 'unlabeled'})).then(h.verifyResponse(200)),
|
|
||||||
h.runCmd(curl({number: +hiddenPr, action: 'unlabeled'})).then(h.verifyResponse(200)),
|
|
||||||
]).then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe(`${host}/*`, () => {
|
|
||||||
|
|
||||||
it('should respond with 404 for requests to unknown URLs', done => {
|
|
||||||
const bodyRegex = /^Unknown resource/;
|
|
||||||
|
|
||||||
Promise.all([
|
|
||||||
h.runCmd(`curl -iL http://${host}/index.html`).then(h.verifyResponse(404, bodyRegex)),
|
|
||||||
h.runCmd(`curl -iL http://${host}/`).then(h.verifyResponse(404, bodyRegex)),
|
|
||||||
h.runCmd(`curl -iL http://${host}`).then(h.verifyResponse(404, bodyRegex)),
|
|
||||||
h.runCmd(`curl -iLX PUT http://${host}`).then(h.verifyResponse(404, bodyRegex)),
|
|
||||||
h.runCmd(`curl -iLX POST http://${host}`).then(h.verifyResponse(404, bodyRegex)),
|
|
||||||
h.runCmd(`curl -iLX PATCH http://${host}`).then(h.verifyResponse(404, bodyRegex)),
|
|
||||||
h.runCmd(`curl -iLX DELETE http://${host}`).then(h.verifyResponse(404, bodyRegex)),
|
|
||||||
]).then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
@ -7,39 +7,49 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prebuild": "yarn clean-dist",
|
"prebuild": "yarn clean-dist",
|
||||||
"build": "tsc",
|
"build": "yarn ~~build",
|
||||||
"build-watch": "yarn build --watch",
|
"prebuild-watch": "yarn prebuild",
|
||||||
|
"build-watch": "yarn ~~build-watch",
|
||||||
"clean-dist": "node --eval \"require('shelljs').rm('-rf', 'dist')\"",
|
"clean-dist": "node --eval \"require('shelljs').rm('-rf', 'dist')\"",
|
||||||
"dev": "concurrently --kill-others --raw --success first \"yarn build-watch\" \"yarn test-watch\"",
|
"predev": "yarn build || true",
|
||||||
|
"dev": "run-p ~~build-watch ~~test-watch",
|
||||||
"lint": "tslint --project tsconfig.json",
|
"lint": "tslint --project tsconfig.json",
|
||||||
"pre~~test-only": "yarn lint",
|
|
||||||
"~~test-only": "node dist/test",
|
|
||||||
"pretest": "yarn build",
|
"pretest": "yarn build",
|
||||||
"test": "yarn ~~test-only",
|
"test": "yarn ~~test-only",
|
||||||
"pretest-watch": "yarn build",
|
"pretest-watch": "yarn pretest",
|
||||||
"test-watch": "nodemon --exec \"yarn ~~test-only\" --watch dist"
|
"test-watch": "yarn ~~test-watch",
|
||||||
|
"~~build": "tsc",
|
||||||
|
"~~build-watch": "yarn ~~build --watch",
|
||||||
|
"pre~~test-only": "yarn lint",
|
||||||
|
"~~test-only": "node dist/test",
|
||||||
|
"~~test-watch": "nodemon --delay 1 --exec \"yarn ~~test-only\" --watch dist"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"body-parser": "^1.18.2",
|
"body-parser": "^1.18.3",
|
||||||
"express": "^4.15.4",
|
"delete-empty": "^2.0.0",
|
||||||
"jasmine": "^2.8.0",
|
"express": "^4.16.3",
|
||||||
"jsonwebtoken": "^8.0.1",
|
"jasmine": "^3.2.0",
|
||||||
"shelljs": "^0.7.8",
|
"nock": "^9.6.1",
|
||||||
"tslib": "^1.7.1"
|
"node-fetch": "^2.2.0",
|
||||||
|
"shelljs": "^0.8.2",
|
||||||
|
"source-map-support": "^0.5.9",
|
||||||
|
"tar-stream": "^1.6.1",
|
||||||
|
"tslib": "^1.9.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/body-parser": "^1.16.5",
|
"@types/body-parser": "^1.17.0",
|
||||||
"@types/express": "^4.0.37",
|
"@types/express": "^4.16.0",
|
||||||
"@types/jasmine": "^2.6.0",
|
"@types/jasmine": "^2.8.8",
|
||||||
"@types/jsonwebtoken": "^7.2.3",
|
"@types/nock": "^9.3.0",
|
||||||
"@types/node": "^8.0.30",
|
"@types/node": "^10.9.2",
|
||||||
|
"@types/node-fetch": "^2.1.2",
|
||||||
"@types/shelljs": "^0.8.0",
|
"@types/shelljs": "^0.8.0",
|
||||||
"@types/supertest": "^2.0.3",
|
"@types/supertest": "^2.0.5",
|
||||||
"concurrently": "^3.5.0",
|
"nodemon": "^1.18.3",
|
||||||
"nodemon": "^1.12.1",
|
"npm-run-all": "^4.1.3",
|
||||||
"supertest": "^3.0.0",
|
"supertest": "^3.1.0",
|
||||||
"tslint": "^5.7.0",
|
"tslint": "^5.11.0",
|
||||||
"tslint-jasmine-noSkipOrFocus": "^1.0.8",
|
"tslint-jasmine-noSkipOrFocus": "^1.0.9",
|
||||||
"typescript": "^2.5.2"
|
"typescript": "^3.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,135 +1,186 @@
|
|||||||
// Imports
|
// Imports
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import {normalize} from 'path';
|
||||||
import * as shell from 'shelljs';
|
import * as shell from 'shelljs';
|
||||||
import {BuildCleaner} from '../../lib/clean-up/build-cleaner';
|
import {BuildCleaner} from '../../lib/clean-up/build-cleaner';
|
||||||
import {HIDDEN_DIR_PREFIX} from '../../lib/common/constants';
|
import {HIDDEN_DIR_PREFIX} from '../../lib/common/constants';
|
||||||
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
||||||
|
import {Logger} from '../../lib/common/utils';
|
||||||
|
|
||||||
|
const EXISTING_BUILDS = [10, 20, 30, 40];
|
||||||
|
const EXISTING_DOWNLOADS = [
|
||||||
|
'10-ABCDEF0-build.zip',
|
||||||
|
'10-1234567-build.zip',
|
||||||
|
'20-ABCDEF0-build.zip',
|
||||||
|
'20-1234567-build.zip',
|
||||||
|
];
|
||||||
|
const OPEN_PRS = [10, 40];
|
||||||
|
const ANY_DATE = jasmine.any(String);
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
describe('BuildCleaner', () => {
|
describe('BuildCleaner', () => {
|
||||||
|
let loggerErrorSpy: jasmine.Spy;
|
||||||
|
let loggerLogSpy: jasmine.Spy;
|
||||||
let cleaner: BuildCleaner;
|
let cleaner: BuildCleaner;
|
||||||
|
|
||||||
beforeEach(() => cleaner = new BuildCleaner('/foo/bar', 'baz/qux', '12345'));
|
beforeEach(() => {
|
||||||
|
loggerErrorSpy = spyOn(Logger.prototype, 'error');
|
||||||
|
loggerLogSpy = spyOn(Logger.prototype, 'log');
|
||||||
|
cleaner = new BuildCleaner('/foo/bar', 'baz', 'qux', '12345', '/downloads', 'build.zip');
|
||||||
|
});
|
||||||
|
|
||||||
describe('constructor()', () => {
|
describe('constructor()', () => {
|
||||||
|
|
||||||
it('should throw if \'buildsDir\' is empty', () => {
|
it('should throw if \'buildsDir\' is empty', () => {
|
||||||
expect(() => new BuildCleaner('', '/baz/qux', '12345')).
|
expect(() => new BuildCleaner('', 'baz', 'qux', '12345', 'downloads', 'build.zip')).
|
||||||
toThrowError('Missing or empty required parameter \'buildsDir\'!');
|
toThrowError('Missing or empty required parameter \'buildsDir\'!');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should throw if \'repoSlug\' is empty', () => {
|
it('should throw if \'githubOrg\' is empty', () => {
|
||||||
expect(() => new BuildCleaner('/foo/bar', '', '12345')).
|
expect(() => new BuildCleaner('/foo/bar', '', 'qux', '12345', 'downloads', 'build.zip')).
|
||||||
toThrowError('Missing or empty required parameter \'repoSlug\'!');
|
toThrowError('Missing or empty required parameter \'githubOrg\'!');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should throw if \'githubRepo\' is empty', () => {
|
||||||
|
expect(() => new BuildCleaner('/foo/bar', 'baz', '', '12345', 'downloads', 'build.zip')).
|
||||||
|
toThrowError('Missing or empty required parameter \'githubRepo\'!');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should throw if \'githubToken\' is empty', () => {
|
it('should throw if \'githubToken\' is empty', () => {
|
||||||
expect(() => new BuildCleaner('/foo/bar', 'baz/qux', '')).
|
expect(() => new BuildCleaner('/foo/bar', 'baz', 'qux', '', 'downloads', 'build.zip')).
|
||||||
toThrowError('Missing or empty required parameter \'githubToken\'!');
|
toThrowError('Missing or empty required parameter \'githubToken\'!');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should throw if \'downloadsDir\' is empty', () => {
|
||||||
|
expect(() => new BuildCleaner('/foo/bar', 'baz', 'qux', '12345', '', 'build.zip')).
|
||||||
|
toThrowError('Missing or empty required parameter \'downloadsDir\'!');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should throw if \'artifactPath\' is empty', () => {
|
||||||
|
expect(() => new BuildCleaner('/foo/bar', 'baz', 'qux', '12345', 'downloads', '')).
|
||||||
|
toThrowError('Missing or empty required parameter \'artifactPath\'!');
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('cleanUp()', () => {
|
describe('cleanUp()', () => {
|
||||||
let cleanerGetExistingBuildNumbersSpy: jasmine.Spy;
|
let cleanerGetExistingBuildNumbersSpy: jasmine.Spy;
|
||||||
let cleanerGetOpenPrNumbersSpy: jasmine.Spy;
|
let cleanerGetOpenPrNumbersSpy: jasmine.Spy;
|
||||||
|
let cleanerGetExistingDownloadsSpy: jasmine.Spy;
|
||||||
let cleanerRemoveUnnecessaryBuildsSpy: jasmine.Spy;
|
let cleanerRemoveUnnecessaryBuildsSpy: jasmine.Spy;
|
||||||
let existingBuildsDeferred: {resolve: (v?: any) => void, reject: (e?: any) => void};
|
let cleanerRemoveUnnecessaryDownloadsSpy: jasmine.Spy;
|
||||||
let openPrsDeferred: {resolve: (v?: any) => void, reject: (e?: any) => void};
|
|
||||||
let promise: Promise<void>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cleanerGetExistingBuildNumbersSpy = spyOn(cleaner as any, 'getExistingBuildNumbers').and.callFake(() => {
|
cleanerGetExistingBuildNumbersSpy = spyOn(cleaner, 'getExistingBuildNumbers')
|
||||||
return new Promise((resolve, reject) => existingBuildsDeferred = {resolve, reject});
|
.and.callFake(() => Promise.resolve(EXISTING_BUILDS));
|
||||||
});
|
cleanerGetOpenPrNumbersSpy = spyOn(cleaner, 'getOpenPrNumbers')
|
||||||
cleanerGetOpenPrNumbersSpy = spyOn(cleaner as any, 'getOpenPrNumbers').and.callFake(() => {
|
.and.callFake(() => Promise.resolve(OPEN_PRS));
|
||||||
return new Promise((resolve, reject) => openPrsDeferred = {resolve, reject});
|
cleanerGetExistingDownloadsSpy = spyOn(cleaner, 'getExistingDownloads')
|
||||||
});
|
.and.callFake(() => Promise.resolve(EXISTING_DOWNLOADS));
|
||||||
cleanerRemoveUnnecessaryBuildsSpy = spyOn(cleaner as any, 'removeUnnecessaryBuilds');
|
|
||||||
|
cleanerRemoveUnnecessaryBuildsSpy = spyOn(cleaner, 'removeUnnecessaryBuilds');
|
||||||
|
cleanerRemoveUnnecessaryDownloadsSpy = spyOn(cleaner, 'removeUnnecessaryDownloads');
|
||||||
|
|
||||||
promise = cleaner.cleanUp();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should return a promise', () => {
|
it('should return a promise', async () => {
|
||||||
|
const promise = cleaner.cleanUp();
|
||||||
expect(promise).toEqual(jasmine.any(Promise));
|
expect(promise).toEqual(jasmine.any(Promise));
|
||||||
|
|
||||||
|
// Do not complete the test and release the spies synchronously, to avoid running the actual implementations.
|
||||||
|
await promise;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should get the existing builds', () => {
|
it('should get the open PRs', async () => {
|
||||||
expect(cleanerGetExistingBuildNumbersSpy).toHaveBeenCalled();
|
await cleaner.cleanUp();
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should get the open PRs', () => {
|
|
||||||
expect(cleanerGetOpenPrNumbersSpy).toHaveBeenCalled();
|
expect(cleanerGetOpenPrNumbersSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should reject if \'getExistingBuildNumbers()\' rejects', done => {
|
it('should get the existing builds', async () => {
|
||||||
promise.catch(err => {
|
await cleaner.cleanUp();
|
||||||
|
expect(cleanerGetExistingBuildNumbersSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should get the existing downloads', async () => {
|
||||||
|
await cleaner.cleanUp();
|
||||||
|
expect(cleanerGetExistingDownloadsSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should pass existing builds and open PRs to \'removeUnnecessaryBuilds()\'', async () => {
|
||||||
|
await cleaner.cleanUp();
|
||||||
|
expect(cleanerRemoveUnnecessaryBuildsSpy).toHaveBeenCalledWith(EXISTING_BUILDS, OPEN_PRS);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should pass existing downloads and open PRs to \'removeUnnecessaryDownloads()\'', async () => {
|
||||||
|
await cleaner.cleanUp();
|
||||||
|
expect(cleanerRemoveUnnecessaryDownloadsSpy).toHaveBeenCalledWith(EXISTING_DOWNLOADS, OPEN_PRS);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should reject if \'getOpenPrNumbers()\' rejects', async () => {
|
||||||
|
try {
|
||||||
|
cleanerGetOpenPrNumbersSpy.and.callFake(() => Promise.reject('Test'));
|
||||||
|
await cleaner.cleanUp();
|
||||||
|
} catch (err) {
|
||||||
expect(err).toBe('Test');
|
expect(err).toBe('Test');
|
||||||
done();
|
}
|
||||||
});
|
|
||||||
|
|
||||||
existingBuildsDeferred.reject('Test');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should reject if \'getOpenPrNumbers()\' rejects', done => {
|
it('should reject if \'getExistingBuildNumbers()\' rejects', async () => {
|
||||||
promise.catch(err => {
|
try {
|
||||||
|
cleanerGetExistingBuildNumbersSpy.and.callFake(() => Promise.reject('Test'));
|
||||||
|
await cleaner.cleanUp();
|
||||||
|
} catch (err) {
|
||||||
expect(err).toBe('Test');
|
expect(err).toBe('Test');
|
||||||
done();
|
}
|
||||||
});
|
|
||||||
|
|
||||||
openPrsDeferred.reject('Test');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should reject if \'removeUnnecessaryBuilds()\' rejects', done => {
|
it('should reject if \'getExistingDownloads()\' rejects', async () => {
|
||||||
promise.catch(err => {
|
try {
|
||||||
|
cleanerGetExistingDownloadsSpy.and.callFake(() => Promise.reject('Test'));
|
||||||
|
await cleaner.cleanUp();
|
||||||
|
} catch (err) {
|
||||||
expect(err).toBe('Test');
|
expect(err).toBe('Test');
|
||||||
done();
|
}
|
||||||
});
|
|
||||||
|
|
||||||
cleanerRemoveUnnecessaryBuildsSpy.and.returnValue(Promise.reject('Test'));
|
|
||||||
existingBuildsDeferred.resolve();
|
|
||||||
openPrsDeferred.resolve();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should pass existing builds and open PRs to \'removeUnnecessaryBuilds()\'', done => {
|
it('should reject if \'removeUnnecessaryBuilds()\' rejects', async () => {
|
||||||
promise.then(() => {
|
try {
|
||||||
expect(cleanerRemoveUnnecessaryBuildsSpy).toHaveBeenCalledWith('foo', 'bar');
|
cleanerRemoveUnnecessaryBuildsSpy.and.callFake(() => Promise.reject('Test'));
|
||||||
done();
|
await cleaner.cleanUp();
|
||||||
});
|
} catch (err) {
|
||||||
|
expect(err).toBe('Test');
|
||||||
existingBuildsDeferred.resolve('foo');
|
}
|
||||||
openPrsDeferred.resolve('bar');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should resolve with the value returned by \'removeUnnecessaryBuilds()\'', done => {
|
it('should reject if \'removeUnnecessaryDownloads()\' rejects', async () => {
|
||||||
promise.then(result => {
|
try {
|
||||||
expect(result as any).toBe('Test');
|
cleanerRemoveUnnecessaryDownloadsSpy.and.callFake(() => Promise.reject('Test'));
|
||||||
done();
|
await cleaner.cleanUp();
|
||||||
});
|
} catch (err) {
|
||||||
|
expect(err).toBe('Test');
|
||||||
cleanerRemoveUnnecessaryBuildsSpy.and.returnValue(Promise.resolve('Test'));
|
}
|
||||||
existingBuildsDeferred.resolve();
|
|
||||||
openPrsDeferred.resolve();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Protected methods
|
|
||||||
|
|
||||||
describe('getExistingBuildNumbers()', () => {
|
describe('getExistingBuildNumbers()', () => {
|
||||||
let fsReaddirSpy: jasmine.Spy;
|
let fsReaddirSpy: jasmine.Spy;
|
||||||
let readdirCb: (err: any, files?: string[]) => void;
|
let readdirCb: (err: any, files?: string[]) => void;
|
||||||
@ -137,7 +188,7 @@ describe('BuildCleaner', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fsReaddirSpy = spyOn(fs, 'readdir').and.callFake((_: string, cb: typeof readdirCb) => readdirCb = cb);
|
fsReaddirSpy = spyOn(fs, 'readdir').and.callFake((_: string, cb: typeof readdirCb) => readdirCb = cb);
|
||||||
promise = (cleaner as any).getExistingBuildNumbers();
|
promise = cleaner.getExistingBuildNumbers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -203,7 +254,7 @@ describe('BuildCleaner', () => {
|
|||||||
return new Promise((resolve, reject) => prDeferred = {resolve, reject});
|
return new Promise((resolve, reject) => prDeferred = {resolve, reject});
|
||||||
});
|
});
|
||||||
|
|
||||||
promise = (cleaner as any).getOpenPrNumbers();
|
promise = cleaner.getOpenPrNumbers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -236,6 +287,68 @@ describe('BuildCleaner', () => {
|
|||||||
prDeferred.resolve([{id: 0, number: 1}, {id: 1, number: 2}, {id: 2, number: 3}]);
|
prDeferred.resolve([{id: 0, number: 1}, {id: 1, number: 2}, {id: 2, number: 3}]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should log the number of open PRs', () => {
|
||||||
|
promise.then(prNumbers => {
|
||||||
|
expect(loggerLogSpy).toHaveBeenCalledWith(
|
||||||
|
ANY_DATE, 'BuildCleaner: ', `Open pull requests: ${prNumbers}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('getExistingDownloads()', () => {
|
||||||
|
let fsReaddirSpy: jasmine.Spy;
|
||||||
|
let readdirCb: (err: any, files?: string[]) => void;
|
||||||
|
let promise: Promise<string[]>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fsReaddirSpy = spyOn(fs, 'readdir').and.callFake((_: string, cb: typeof readdirCb) => readdirCb = cb);
|
||||||
|
promise = cleaner.getExistingDownloads();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should return a promise', () => {
|
||||||
|
expect(promise).toEqual(jasmine.any(Promise));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should get the contents of the downloads directory', () => {
|
||||||
|
expect(fsReaddirSpy).toHaveBeenCalled();
|
||||||
|
expect(fsReaddirSpy.calls.argsFor(0)[0]).toBe('/downloads');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should reject if an error occurs while getting the files', done => {
|
||||||
|
promise.catch(err => {
|
||||||
|
expect(err).toBe('Test');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
readdirCb('Test');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should resolve with the returned file names', done => {
|
||||||
|
promise.then(result => {
|
||||||
|
expect(result).toEqual(EXISTING_DOWNLOADS);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
readdirCb(null, EXISTING_DOWNLOADS);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should ignore files that do not match the artifactPath', done => {
|
||||||
|
promise.then(result => {
|
||||||
|
expect(result).toEqual(['10-ABCDEF-build.zip', '30-FFFFFFF-build.zip']);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
readdirCb(null, ['10-ABCDEF-build.zip', '20-AAAAAAA-otherfile.zip', '30-FFFFFFF-build.zip']);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -253,7 +366,7 @@ describe('BuildCleaner', () => {
|
|||||||
|
|
||||||
it('should test if the directory exists (and return if is does not)', () => {
|
it('should test if the directory exists (and return if is does not)', () => {
|
||||||
shellTestSpy.and.returnValue(false);
|
shellTestSpy.and.returnValue(false);
|
||||||
(cleaner as any).removeDir('/foo/bar');
|
cleaner.removeDir('/foo/bar');
|
||||||
|
|
||||||
expect(shellTestSpy).toHaveBeenCalledWith('-d', '/foo/bar');
|
expect(shellTestSpy).toHaveBeenCalledWith('-d', '/foo/bar');
|
||||||
expect(shellChmodSpy).not.toHaveBeenCalled();
|
expect(shellChmodSpy).not.toHaveBeenCalled();
|
||||||
@ -262,99 +375,127 @@ describe('BuildCleaner', () => {
|
|||||||
|
|
||||||
|
|
||||||
it('should remove the specified directory and its content', () => {
|
it('should remove the specified directory and its content', () => {
|
||||||
(cleaner as any).removeDir('/foo/bar');
|
cleaner.removeDir('/foo/bar');
|
||||||
expect(shellRmSpy).toHaveBeenCalledWith('-rf', '/foo/bar');
|
expect(shellRmSpy).toHaveBeenCalledWith('-rf', '/foo/bar');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should make the directory and its content writable before removing', () => {
|
it('should make the directory and its content writable before removing', () => {
|
||||||
shellRmSpy.and.callFake(() => expect(shellChmodSpy).toHaveBeenCalledWith('-R', 'a+w', '/foo/bar'));
|
shellRmSpy.and.callFake(() => expect(shellChmodSpy).toHaveBeenCalledWith('-R', 'a+w', '/foo/bar'));
|
||||||
(cleaner as any).removeDir('/foo/bar');
|
cleaner.removeDir('/foo/bar');
|
||||||
|
|
||||||
expect(shellRmSpy).toHaveBeenCalled();
|
expect(shellRmSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should catch errors and log them', () => {
|
it('should catch errors and log them', () => {
|
||||||
const consoleErrorSpy = spyOn(console, 'error');
|
|
||||||
shellRmSpy.and.callFake(() => {
|
shellRmSpy.and.callFake(() => {
|
||||||
// tslint:disable-next-line: no-string-throw
|
// tslint:disable-next-line: no-string-throw
|
||||||
throw 'Test';
|
throw 'Test';
|
||||||
});
|
});
|
||||||
|
|
||||||
(cleaner as any).removeDir('/foo/bar');
|
cleaner.removeDir('/foo/bar');
|
||||||
|
|
||||||
expect(consoleErrorSpy).toHaveBeenCalled();
|
expect(loggerErrorSpy).toHaveBeenCalledWith('ERROR: Unable to remove \'/foo/bar\' due to:', 'Test');
|
||||||
expect(consoleErrorSpy.calls.argsFor(0)[0]).toContain('Unable to remove \'/foo/bar\'');
|
|
||||||
expect(consoleErrorSpy.calls.argsFor(0)[1]).toBe('Test');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('removeUnnecessaryBuilds()', () => {
|
describe('removeUnnecessaryBuilds()', () => {
|
||||||
let consoleLogSpy: jasmine.Spy;
|
|
||||||
let cleanerRemoveDirSpy: jasmine.Spy;
|
let cleanerRemoveDirSpy: jasmine.Spy;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
consoleLogSpy = spyOn(console, 'log');
|
cleanerRemoveDirSpy = spyOn(cleaner, 'removeDir');
|
||||||
cleanerRemoveDirSpy = spyOn(cleaner as any, 'removeDir');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should log the number of existing builds, open PRs and builds to be removed', () => {
|
it('should log the number of existing builds and builds to be removed', () => {
|
||||||
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3], [3, 4, 5, 6]);
|
cleaner.removeUnnecessaryBuilds([1, 2, 3], [3, 4, 5, 6]);
|
||||||
|
|
||||||
expect(console.log).toHaveBeenCalledWith('Existing builds: 3');
|
expect(loggerLogSpy).toHaveBeenCalledWith('Existing builds: 3');
|
||||||
expect(console.log).toHaveBeenCalledWith('Open pull requests: 4');
|
expect(loggerLogSpy).toHaveBeenCalledWith('Removing 2 build(s): 1, 2');
|
||||||
expect(console.log).toHaveBeenCalledWith('Removing 2 build(s): 1, 2');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should construct full paths to directories (by prepending \'buildsDir\')', () => {
|
it('should construct full paths to directories (by prepending \'buildsDir\')', () => {
|
||||||
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3], []);
|
cleaner.removeUnnecessaryBuilds([1, 2, 3], []);
|
||||||
|
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/1'));
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/1'));
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/2'));
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/2'));
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/3'));
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/3'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should try removing hidden directories as well', () => {
|
it('should try removing hidden directories as well', () => {
|
||||||
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3], []);
|
cleaner.removeUnnecessaryBuilds([1, 2, 3], []);
|
||||||
|
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`));
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`));
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}2`));
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}2`));
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}3`));
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}3`));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should remove the builds that do not correspond to open PRs', () => {
|
it('should remove the builds that do not correspond to open PRs', () => {
|
||||||
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3, 4], [2, 4]);
|
cleaner.removeUnnecessaryBuilds([1, 2, 3, 4], [2, 4]);
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(4);
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(4);
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/1'));
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/1'));
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/3'));
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/3'));
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`));
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`));
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}3`));
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}3`));
|
||||||
cleanerRemoveDirSpy.calls.reset();
|
cleanerRemoveDirSpy.calls.reset();
|
||||||
|
|
||||||
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3, 4], [1, 2, 3, 4]);
|
cleaner.removeUnnecessaryBuilds([1, 2, 3, 4], [1, 2, 3, 4]);
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(0);
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(0);
|
||||||
cleanerRemoveDirSpy.calls.reset();
|
cleanerRemoveDirSpy.calls.reset();
|
||||||
|
|
||||||
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3, 4], []);
|
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3, 4], []);
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(8);
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(8);
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/1'));
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/1'));
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/2'));
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/2'));
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/3'));
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/3'));
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize('/foo/bar/4'));
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/4'));
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`));
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`));
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}2`));
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}2`));
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}3`));
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}3`));
|
||||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(path.normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}4`));
|
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}4`));
|
||||||
cleanerRemoveDirSpy.calls.reset();
|
cleanerRemoveDirSpy.calls.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('removeUnnecessaryDownloads()', () => {
|
||||||
|
let shellRmSpy: jasmine.Spy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
shellRmSpy = spyOn(shell, 'rm');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should log the number of existing downloads and downloads to be removed', () => {
|
||||||
|
cleaner.removeUnnecessaryDownloads(EXISTING_DOWNLOADS, OPEN_PRS);
|
||||||
|
|
||||||
|
expect(loggerLogSpy).toHaveBeenCalledWith('Existing downloads: 4');
|
||||||
|
expect(loggerLogSpy).toHaveBeenCalledWith('Removing 2 download(s): 20-ABCDEF0-build.zip, 20-1234567-build.zip');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should construct full paths to directories (by prepending \'downloadsDir\')', () => {
|
||||||
|
cleaner.removeUnnecessaryDownloads(['dl-1', 'dl-2', 'dl-3'], []);
|
||||||
|
|
||||||
|
expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/dl-1'));
|
||||||
|
expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/dl-2'));
|
||||||
|
expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/dl-3'));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should remove the downloads that do not correspond to open PRs', () => {
|
||||||
|
cleaner.removeUnnecessaryDownloads(EXISTING_DOWNLOADS, OPEN_PRS);
|
||||||
|
expect(shellRmSpy).toHaveBeenCalledTimes(2);
|
||||||
|
expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/20-ABCDEF0-build.zip'));
|
||||||
|
expect(shellRmSpy).toHaveBeenCalledWith(normalize('/downloads/20-1234567-build.zip'));
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,134 @@
|
|||||||
|
import * as nock from 'nock';
|
||||||
|
import {CircleCiApi} from '../../lib/common/circle-ci-api';
|
||||||
|
|
||||||
|
const ORG = 'testorg';
|
||||||
|
const REPO = 'testrepo';
|
||||||
|
const TOKEN = 'xxxx';
|
||||||
|
const BASE_URL = `https://circleci.com/api/v1.1/project/github/${ORG}/${REPO}`;
|
||||||
|
|
||||||
|
describe('CircleCIApi', () => {
|
||||||
|
describe('constructor()', () => {
|
||||||
|
it('should throw if \'githubOrg\' is missing or empty', () => {
|
||||||
|
expect(() => new CircleCiApi('', REPO, TOKEN)).
|
||||||
|
toThrowError('Missing or empty required parameter \'githubOrg\'!');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if \'githubRepo\' is missing or empty', () => {
|
||||||
|
expect(() => new CircleCiApi(ORG, '', TOKEN)).
|
||||||
|
toThrowError('Missing or empty required parameter \'githubRepo\'!');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if \'circleCiToken\' is missing or empty', () => {
|
||||||
|
expect(() => new CircleCiApi(ORG, REPO, '')).
|
||||||
|
toThrowError('Missing or empty required parameter \'circleCiToken\'!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getBuildInfo', () => {
|
||||||
|
it('should make a request to the CircleCI API for the given build number', async () => {
|
||||||
|
const api = new CircleCiApi(ORG, REPO, TOKEN);
|
||||||
|
const buildNum = 12345;
|
||||||
|
const expectedBuildInfo: any = { org: ORG, repo: REPO, build_num: buildNum };
|
||||||
|
|
||||||
|
const request = nock(BASE_URL)
|
||||||
|
.get(`/${buildNum}?circle-token=${TOKEN}`)
|
||||||
|
.reply(200, expectedBuildInfo);
|
||||||
|
|
||||||
|
const buildInfo = await api.getBuildInfo(buildNum);
|
||||||
|
expect(buildInfo).toEqual(expectedBuildInfo);
|
||||||
|
request.done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if the request fails', async () => {
|
||||||
|
const api = new CircleCiApi(ORG, REPO, TOKEN);
|
||||||
|
const buildNum = 12345;
|
||||||
|
const errorMessage = 'Invalid request';
|
||||||
|
const request = nock(BASE_URL).get(`/${buildNum}?circle-token=${TOKEN}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
request.replyWithError(errorMessage);
|
||||||
|
await api.getBuildInfo(buildNum);
|
||||||
|
throw new Error('Exception Expected');
|
||||||
|
} catch (err) {
|
||||||
|
expect(err.message).toEqual(
|
||||||
|
`CircleCI build info request failed ` +
|
||||||
|
`(request to ${BASE_URL}/${buildNum}?circle-token=${TOKEN} failed, reason: ${errorMessage})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
request.reply(404, errorMessage);
|
||||||
|
await api.getBuildInfo(buildNum);
|
||||||
|
throw new Error('Exception Expected');
|
||||||
|
} catch (err) {
|
||||||
|
expect(err.message).toEqual(
|
||||||
|
`CircleCI build info request failed ` +
|
||||||
|
`(request to ${BASE_URL}/${buildNum}?circle-token=${TOKEN} failed, reason: ${errorMessage})`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getBuildArtifactUrl', () => {
|
||||||
|
it('should make a request to the CircleCI API for the given build number', async () => {
|
||||||
|
const api = new CircleCiApi(ORG, REPO, TOKEN);
|
||||||
|
const buildNum = 12345;
|
||||||
|
const artifact0: any = { path: 'some/path/0', url: 'https://url/0' };
|
||||||
|
const artifact1: any = { path: 'some/path/1', url: 'https://url/1' };
|
||||||
|
const artifact2: any = { path: 'some/path/2', url: 'https://url/2' };
|
||||||
|
const request = nock(BASE_URL)
|
||||||
|
.get(`/${buildNum}/artifacts?circle-token=${TOKEN}`)
|
||||||
|
.reply(200, [artifact0, artifact1, artifact2]);
|
||||||
|
|
||||||
|
const artifactUrl = await api.getBuildArtifactUrl(buildNum, 'some/path/1');
|
||||||
|
expect(artifactUrl).toEqual('https://url/1');
|
||||||
|
request.done();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should throw an error if the request fails', async () => {
|
||||||
|
const api = new CircleCiApi(ORG, REPO, TOKEN);
|
||||||
|
const buildNum = 12345;
|
||||||
|
const errorMessage = 'Invalid request';
|
||||||
|
const request = nock(BASE_URL).get(`/${buildNum}/artifacts?circle-token=${TOKEN}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
request.replyWithError(errorMessage);
|
||||||
|
await api.getBuildArtifactUrl(buildNum, 'some/path/1');
|
||||||
|
throw new Error('Exception Expected');
|
||||||
|
} catch (err) {
|
||||||
|
expect(err.message).toEqual(
|
||||||
|
`CircleCI artifact URL request failed ` +
|
||||||
|
`(request to ${BASE_URL}/${buildNum}/artifacts?circle-token=${TOKEN} failed, reason: ${errorMessage})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
request.reply(404, errorMessage);
|
||||||
|
await api.getBuildArtifactUrl(buildNum, 'some/path/1');
|
||||||
|
throw new Error('Exception Expected');
|
||||||
|
} catch (err) {
|
||||||
|
expect(err.message).toEqual(
|
||||||
|
`CircleCI artifact URL request failed ` +
|
||||||
|
`(request to ${BASE_URL}/${buildNum}/artifacts?circle-token=${TOKEN} failed, reason: ${errorMessage})`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if the response does not contain the specified artifact', async () => {
|
||||||
|
const api = new CircleCiApi(ORG, REPO, TOKEN);
|
||||||
|
const buildNum = 12345;
|
||||||
|
const artifact0: any = { path: 'some/path/0', url: 'https://url/0' };
|
||||||
|
const artifact1: any = { path: 'some/path/1', url: 'https://url/1' };
|
||||||
|
const artifact2: any = { path: 'some/path/2', url: 'https://url/2' };
|
||||||
|
nock(BASE_URL)
|
||||||
|
.get(`/${buildNum}/artifacts?circle-token=${TOKEN}`)
|
||||||
|
.reply(200, [artifact0, artifact1, artifact2]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.getBuildArtifactUrl(buildNum, 'some/path/3');
|
||||||
|
throw new Error('Exception Expected');
|
||||||
|
} catch (err) {
|
||||||
|
expect(err.message).toEqual(
|
||||||
|
`CircleCI artifact URL request failed ` +
|
||||||
|
`(Missing artifact (some/path/3) for CircleCI build: ${buildNum})`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,7 +1,5 @@
|
|||||||
// Imports
|
// Imports
|
||||||
import {EventEmitter} from 'events';
|
import * as nock from 'nock';
|
||||||
import {ClientRequest, IncomingMessage} from 'http';
|
|
||||||
import * as https from 'https';
|
|
||||||
import {GithubApi} from '../../lib/common/github-api';
|
import {GithubApi} from '../../lib/common/github-api';
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
@ -110,39 +108,6 @@ describe('GithubApi', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Protected methods
|
|
||||||
|
|
||||||
describe('buildPath()', () => {
|
|
||||||
|
|
||||||
it('should return the pathname if no params', () => {
|
|
||||||
expect((api as any).buildPath('/foo')).toBe('/foo');
|
|
||||||
expect((api as any).buildPath('/foo', undefined)).toBe('/foo');
|
|
||||||
expect((api as any).buildPath('/foo', null)).toBe('/foo');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should append the params to the pathname', () => {
|
|
||||||
expect((api as any).buildPath('/foo', {bar: 'baz'})).toBe('/foo?bar=baz');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should join the params with \'&\'', () => {
|
|
||||||
expect((api as any).buildPath('/foo', {bar: 1, baz: 2})).toBe('/foo?bar=1&baz=2');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should ignore undefined/null params', () => {
|
|
||||||
expect((api as any).buildPath('/foo', {bar: undefined, baz: null})).toBe('/foo');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should encode param values as URI components', () => {
|
|
||||||
expect((api as any).buildPath('/foo', {bar: 'b a&z'})).toBe('/foo?bar=b%20a%26z');
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe('getPaginated()', () => {
|
describe('getPaginated()', () => {
|
||||||
let deferreds: {resolve: (v: any) => void, reject: (v: any) => void}[];
|
let deferreds: {resolve: (v: any) => void, reject: (v: any) => void}[];
|
||||||
|
|
||||||
@ -161,8 +126,8 @@ describe('GithubApi', () => {
|
|||||||
(api as any).getPaginated('/foo/bar');
|
(api as any).getPaginated('/foo/bar');
|
||||||
(api as any).getPaginated('/foo/bar', {baz: 'qux'});
|
(api as any).getPaginated('/foo/bar', {baz: 'qux'});
|
||||||
|
|
||||||
expect(api.get).toHaveBeenCalledWith('/foo/bar', {page: 0, per_page: 100});
|
expect(api.get).toHaveBeenCalledWith('/foo/bar', {page: 1, per_page: 100});
|
||||||
expect(api.get).toHaveBeenCalledWith('/foo/bar', {baz: 'qux', page: 0, per_page: 100});
|
expect(api.get).toHaveBeenCalledWith('/foo/bar', {baz: 'qux', page: 1, per_page: 100});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -197,9 +162,9 @@ describe('GithubApi', () => {
|
|||||||
const paramsForPage = (page: number) => ({baz: 'qux', page, per_page: 100});
|
const paramsForPage = (page: number) => ({baz: 'qux', page, per_page: 100});
|
||||||
|
|
||||||
expect(apiGetSpy).toHaveBeenCalledTimes(3);
|
expect(apiGetSpy).toHaveBeenCalledTimes(3);
|
||||||
expect(apiGetSpy.calls.argsFor(0)).toEqual(['/foo/bar', paramsForPage(0)]);
|
expect(apiGetSpy.calls.argsFor(0)).toEqual(['/foo/bar', paramsForPage(1)]);
|
||||||
expect(apiGetSpy.calls.argsFor(1)).toEqual(['/foo/bar', paramsForPage(1)]);
|
expect(apiGetSpy.calls.argsFor(1)).toEqual(['/foo/bar', paramsForPage(2)]);
|
||||||
expect(apiGetSpy.calls.argsFor(2)).toEqual(['/foo/bar', paramsForPage(2)]);
|
expect(apiGetSpy.calls.argsFor(2)).toEqual(['/foo/bar', paramsForPage(3)]);
|
||||||
|
|
||||||
expect(data).toEqual(allItems);
|
expect(data).toEqual(allItems);
|
||||||
|
|
||||||
@ -218,191 +183,162 @@ describe('GithubApi', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Protected methods
|
||||||
|
|
||||||
|
describe('buildPath()', () => {
|
||||||
|
|
||||||
|
it('should return the pathname if no params', () => {
|
||||||
|
expect((api as any).buildPath('/foo')).toBe('/foo');
|
||||||
|
expect((api as any).buildPath('/foo', undefined)).toBe('/foo');
|
||||||
|
expect((api as any).buildPath('/foo', null)).toBe('/foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should append the params to the pathname', () => {
|
||||||
|
expect((api as any).buildPath('/foo', {bar: 'baz'})).toBe('/foo?bar=baz');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should join the params with \'&\'', () => {
|
||||||
|
expect((api as any).buildPath('/foo', {bar: 1, baz: 2})).toBe('/foo?bar=1&baz=2');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should ignore undefined/null params', () => {
|
||||||
|
expect((api as any).buildPath('/foo', {bar: undefined, baz: null})).toBe('/foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should encode param values as URI components', () => {
|
||||||
|
expect((api as any).buildPath('/foo', {bar: 'b a&z'})).toBe('/foo?bar=b%20a%26z');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
describe('request()', () => {
|
describe('request()', () => {
|
||||||
let httpsRequestSpy: jasmine.Spy;
|
|
||||||
let latestRequest: ClientRequest;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
const originalRequest = https.request;
|
|
||||||
|
|
||||||
httpsRequestSpy = spyOn(https, 'request').and.callFake((...args: any[]) => {
|
|
||||||
latestRequest = originalRequest.apply(https, args);
|
|
||||||
|
|
||||||
spyOn(latestRequest, 'on').and.callThrough();
|
|
||||||
spyOn(latestRequest, 'end');
|
|
||||||
|
|
||||||
return latestRequest;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should return a promise', () => {
|
it('should return a promise', () => {
|
||||||
|
nock('https://api.github.com').get('').reply(200);
|
||||||
expect((api as any).request()).toEqual(jasmine.any(Promise));
|
expect((api as any).request()).toEqual(jasmine.any(Promise));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should call \'https.request()\' with the correct options', () => {
|
it('should call \'https.request()\' with the correct options', () => {
|
||||||
(api as any).request('method', 'path');
|
const requestHandler = nock('https://api.github.com')
|
||||||
|
.intercept('/path', 'method')
|
||||||
|
.reply(200);
|
||||||
|
|
||||||
expect(httpsRequestSpy).toHaveBeenCalled();
|
(api as any).request('method', '/path');
|
||||||
expect(httpsRequestSpy.calls.argsFor(0)[0]).toEqual(jasmine.objectContaining({
|
requestHandler.done();
|
||||||
headers: jasmine.objectContaining({
|
|
||||||
'User-Agent': `Node/${process.versions.node}`,
|
|
||||||
}),
|
|
||||||
host: 'api.github.com',
|
|
||||||
method: 'method',
|
|
||||||
path: 'path',
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should call specify an \'Authorization\' header if \'githubToken\' is present', () => {
|
it('should add the \'Authorization\' header containing the \'githubToken\'', () => {
|
||||||
(api as any).request('method', 'path');
|
const requestHandler = nock('https://api.github.com')
|
||||||
|
.intercept('/path', 'method', undefined, {
|
||||||
expect(httpsRequestSpy).toHaveBeenCalled();
|
reqheaders: {Authorization: 'token 12345'},
|
||||||
expect(httpsRequestSpy.calls.argsFor(0)[0].headers).toEqual(jasmine.objectContaining({
|
})
|
||||||
Authorization: 'token 12345',
|
.reply(200);
|
||||||
}));
|
(api as any).request('method', '/path');
|
||||||
|
requestHandler.done();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should reject on request error', done => {
|
it('should reject on request error', async () => {
|
||||||
(api as any).request('method', 'path').catch((err: any) => {
|
nock('https://api.github.com')
|
||||||
expect(err).toBe('Test');
|
.intercept('/path', 'method')
|
||||||
done();
|
.replyWithError('Test');
|
||||||
});
|
let message = 'Failed to reject error';
|
||||||
|
await (api as any).request('method', '/path').catch((err: any) => message = err.message);
|
||||||
latestRequest.emit('error', 'Test');
|
expect(message).toEqual('Test');
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should send the request (i.e. call \'end()\')', () => {
|
|
||||||
(api as any).request('method', 'path');
|
|
||||||
expect(latestRequest.end).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should \'JSON.stringify\' and send the data along with the request', () => {
|
it('should \'JSON.stringify\' and send the data along with the request', () => {
|
||||||
(api as any).request('method', 'path');
|
const data = {key: 'value'};
|
||||||
expect(latestRequest.end).toHaveBeenCalledWith(null);
|
const requestHandler = nock('https://api.github.com')
|
||||||
|
.intercept('/path', 'method', JSON.stringify(data))
|
||||||
(api as any).request('method', 'path', {key: 'value'});
|
.reply(200);
|
||||||
expect(latestRequest.end).toHaveBeenCalledWith('{"key":"value"}');
|
(api as any).request('method', '/path', data);
|
||||||
|
requestHandler.done();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('onResponse', () => {
|
it('should reject if response statusCode is <200', done => {
|
||||||
let promise: Promise<object>;
|
const requestHandler = nock('https://api.github.com')
|
||||||
let respond: (statusCode: number) => IncomingMessage;
|
.intercept('/path', 'method')
|
||||||
|
.reply(199);
|
||||||
|
|
||||||
beforeEach(() => {
|
(api as any).request('method', '/path')
|
||||||
promise = (api as any).request('method', 'path');
|
.catch((err: string) => {
|
||||||
|
|
||||||
respond = (statusCode: number) => {
|
|
||||||
const mockResponse = new EventEmitter() as IncomingMessage;
|
|
||||||
mockResponse.statusCode = statusCode;
|
|
||||||
|
|
||||||
const onResponse = httpsRequestSpy.calls.argsFor(0)[1];
|
|
||||||
onResponse(mockResponse);
|
|
||||||
|
|
||||||
return mockResponse;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should reject on response error', done => {
|
|
||||||
promise.catch(err => {
|
|
||||||
expect(err).toBe('Test');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
const res = respond(200);
|
|
||||||
res.emit('error', 'Test');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should reject if returned statusCode is <200', done => {
|
|
||||||
promise.catch(err => {
|
|
||||||
expect(err).toContain('failed');
|
expect(err).toContain('failed');
|
||||||
expect(err).toContain('status: 199');
|
expect(err).toContain('status: 199');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
requestHandler.done();
|
||||||
const res = respond(199);
|
|
||||||
res.emit('end');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should reject if returned statusCode is >=400', done => {
|
it('should reject if response statusCode is >=400', done => {
|
||||||
promise.catch(err => {
|
const requestHandler = nock('https://api.github.com')
|
||||||
|
.intercept('/path', 'method')
|
||||||
|
.reply(400);
|
||||||
|
|
||||||
|
(api as any).request('method', '/path')
|
||||||
|
.catch((err: string) => {
|
||||||
expect(err).toContain('failed');
|
expect(err).toContain('failed');
|
||||||
expect(err).toContain('status: 400');
|
expect(err).toContain('status: 400');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
requestHandler.done();
|
||||||
const res = respond(400);
|
|
||||||
res.emit('end');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should include the response text in the rejection message', done => {
|
it('should include the response text in the rejection message', done => {
|
||||||
promise.catch(err => {
|
const requestHandler = nock('https://api.github.com')
|
||||||
|
.intercept('/path', 'method')
|
||||||
|
.reply(500, 'Test');
|
||||||
|
|
||||||
|
(api as any).request('method', '/path')
|
||||||
|
.catch((err: string) => {
|
||||||
expect(err).toContain('Test');
|
expect(err).toContain('Test');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
requestHandler.done();
|
||||||
const res = respond(500);
|
|
||||||
res.emit('data', 'Test');
|
|
||||||
res.emit('end');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should resolve if returned statusCode is <=200 <400', done => {
|
it('should resolve if returned statusCode is >=200 and <400', done => {
|
||||||
promise.then(done);
|
const requestHandler = nock('https://api.github.com')
|
||||||
|
.intercept('/path', 'method')
|
||||||
|
.reply(200);
|
||||||
|
|
||||||
const res = respond(200);
|
(api as any).request('method', '/path').then(done);
|
||||||
res.emit('data', '{}');
|
requestHandler.done();
|
||||||
res.emit('end');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should resolve with the response text \'JSON.parsed\'', done => {
|
it('should parse the response body into an object using \'JSON.parse\'', done => {
|
||||||
promise.then(data => {
|
const requestHandler = nock('https://api.github.com')
|
||||||
|
.intercept('/path', 'method')
|
||||||
|
.reply(300, '{"foo": "bar"}');
|
||||||
|
|
||||||
|
(api as any).request('method', '/path').then((data: any) => {
|
||||||
expect(data).toEqual({foo: 'bar'});
|
expect(data).toEqual({foo: 'bar'});
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
requestHandler.done();
|
||||||
const res = respond(300);
|
|
||||||
res.emit('data', '{"foo":"bar"}');
|
|
||||||
res.emit('end');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should collect and concatenate the whole response text', done => {
|
|
||||||
promise.then(data => {
|
|
||||||
expect(data).toEqual({foo: 'bar', baz: 'qux'});
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
const res = respond(300);
|
|
||||||
res.emit('data', '{"foo":');
|
|
||||||
res.emit('data', '"bar","baz"');
|
|
||||||
res.emit('data', ':"qux"}');
|
|
||||||
res.emit('end');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should reject if the response text is malformed JSON', done => {
|
it('should reject if the response text is malformed JSON', done => {
|
||||||
promise.catch(err => {
|
const requestHandler = nock('https://api.github.com')
|
||||||
|
.intercept('/path', 'method')
|
||||||
|
.reply(300, '}');
|
||||||
|
|
||||||
|
(api as any).request('method', '/path').catch((err: any) => {
|
||||||
expect(err).toEqual(jasmine.any(SyntaxError));
|
expect(err).toEqual(jasmine.any(SyntaxError));
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
requestHandler.done();
|
||||||
const res = respond(300);
|
|
||||||
res.emit('data', '}');
|
|
||||||
res.emit('end');
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -1,20 +1,27 @@
|
|||||||
// Imports
|
// Imports
|
||||||
|
import {GithubApi} from '../../lib/common/github-api';
|
||||||
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
describe('GithubPullRequests', () => {
|
describe('GithubPullRequests', () => {
|
||||||
|
let githubApi: jasmine.SpyObj<GithubApi>;
|
||||||
|
|
||||||
describe('constructor()', () => {
|
beforeEach(() => {
|
||||||
|
githubApi = jasmine.createSpyObj('githubApi', ['post', 'get', 'getPaginated']);
|
||||||
it('should throw if \'githubToken\' is missing or empty', () => {
|
|
||||||
expect(() => new GithubPullRequests('', 'foo/bar')).
|
|
||||||
toThrowError('Missing or empty required parameter \'githubToken\'!');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should throw if \'repoSlug\' is missing or empty', () => {
|
describe('constructor()', () => {
|
||||||
expect(() => new GithubPullRequests('12345', '')).
|
|
||||||
toThrowError('Missing or empty required parameter \'repoSlug\'!');
|
it('should throw if \'githubOrg\' is missing or empty', () => {
|
||||||
|
expect(() => new GithubPullRequests(githubApi, '', 'bar')).
|
||||||
|
toThrowError('Missing or empty required parameter \'githubOrg\'!');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should throw if \'githubRepo\' is missing or empty', () => {
|
||||||
|
expect(() => new GithubPullRequests(githubApi, 'foo', '')).
|
||||||
|
toThrowError('Missing or empty required parameter \'githubRepo\'!');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
@ -22,17 +29,9 @@ describe('GithubPullRequests', () => {
|
|||||||
|
|
||||||
describe('addComment()', () => {
|
describe('addComment()', () => {
|
||||||
let prs: GithubPullRequests;
|
let prs: GithubPullRequests;
|
||||||
let deferred: {resolve: (v: any) => void, reject: (v: any) => void};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
prs = new GithubPullRequests('12345', 'foo/bar');
|
prs = new GithubPullRequests(githubApi, 'foo', 'bar');
|
||||||
|
|
||||||
spyOn(prs, 'post').and.callFake(() => new Promise((resolve, reject) => deferred = {resolve, reject}));
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should return a promise', () => {
|
|
||||||
expect(prs.addComment(42, 'body')).toEqual(jasmine.any(Promise));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -47,30 +46,28 @@ describe('GithubPullRequests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should call \'post()\' with the correct pathname, params and data', () => {
|
it('should make a POST request to Github with the correct pathname, params and data', () => {
|
||||||
|
githubApi.post.and.callFake(() => Promise.resolve());
|
||||||
prs.addComment(42, 'body');
|
prs.addComment(42, 'body');
|
||||||
|
expect(githubApi.post).toHaveBeenCalledWith('/repos/foo/bar/issues/42/comments', null, {body: 'body'});
|
||||||
expect(prs.post).toHaveBeenCalledWith('/repos/foo/bar/issues/42/comments', null, {body: 'body'});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should reject if the request fails', done => {
|
it('should reject if the request fails', done => {
|
||||||
|
githubApi.post.and.callFake(() => Promise.reject('Test'));
|
||||||
prs.addComment(42, 'body').catch(err => {
|
prs.addComment(42, 'body').catch(err => {
|
||||||
expect(err).toBe('Test');
|
expect(err).toBe('Test');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
deferred.reject('Test');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should resolve with the returned response', done => {
|
it('should resolve with the data from the Github POST', done => {
|
||||||
|
githubApi.post.and.callFake(() => Promise.resolve('Test'));
|
||||||
prs.addComment(42, 'body').then(data => {
|
prs.addComment(42, 'body').then(data => {
|
||||||
expect(data as any).toBe('Test');
|
expect(data).toBe('Test');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
deferred.resolve('Test');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
@ -78,23 +75,25 @@ describe('GithubPullRequests', () => {
|
|||||||
|
|
||||||
describe('fetch()', () => {
|
describe('fetch()', () => {
|
||||||
let prs: GithubPullRequests;
|
let prs: GithubPullRequests;
|
||||||
let prsGetSpy: jasmine.Spy;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
prs = new GithubPullRequests('12345', 'foo/bar');
|
prs = new GithubPullRequests(githubApi, 'foo', 'bar');
|
||||||
prsGetSpy = spyOn(prs as any, 'get');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should call \'get()\' with the correct pathname', () => {
|
it('should make a GET request to GitHub with the correct pathname', () => {
|
||||||
prs.fetch(42);
|
prs.fetch(42);
|
||||||
expect(prsGetSpy).toHaveBeenCalledWith('/repos/foo/bar/issues/42');
|
expect(githubApi.get).toHaveBeenCalledWith('/repos/foo/bar/issues/42');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should forward the value returned by \'get()\'', () => {
|
it('should resolve with the data returned from GitHub', done => {
|
||||||
prsGetSpy.and.returnValue('Test');
|
const expected: any = {number: 42};
|
||||||
expect(prs.fetch(42) as any).toBe('Test');
|
githubApi.get.and.callFake(() => Promise.resolve(expected));
|
||||||
|
prs.fetch(42).then(data => {
|
||||||
|
expect(data).toEqual(expected);
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
@ -102,13 +101,8 @@ describe('GithubPullRequests', () => {
|
|||||||
|
|
||||||
describe('fetchAll()', () => {
|
describe('fetchAll()', () => {
|
||||||
let prs: GithubPullRequests;
|
let prs: GithubPullRequests;
|
||||||
let prsGetPaginatedSpy: jasmine.Spy;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => prs = new GithubPullRequests(githubApi, 'foo', 'bar'));
|
||||||
prs = new GithubPullRequests('12345', 'foo/bar');
|
|
||||||
prsGetPaginatedSpy = spyOn(prs as any, 'getPaginated');
|
|
||||||
spyOn(console, 'log');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should call \'getPaginated()\' with the correct pathname and params', () => {
|
it('should call \'getPaginated()\' with the correct pathname and params', () => {
|
||||||
@ -118,24 +112,50 @@ describe('GithubPullRequests', () => {
|
|||||||
prs.fetchAll('closed');
|
prs.fetchAll('closed');
|
||||||
prs.fetchAll('open');
|
prs.fetchAll('open');
|
||||||
|
|
||||||
expect(prsGetPaginatedSpy).toHaveBeenCalledTimes(3);
|
expect(githubApi.getPaginated).toHaveBeenCalledTimes(3);
|
||||||
expect(prsGetPaginatedSpy.calls.argsFor(0)).toEqual([expectedPathname, {state: 'all'}]);
|
expect(githubApi.getPaginated.calls.argsFor(0)).toEqual([expectedPathname, {state: 'all'}]);
|
||||||
expect(prsGetPaginatedSpy.calls.argsFor(1)).toEqual([expectedPathname, {state: 'closed'}]);
|
expect(githubApi.getPaginated.calls.argsFor(1)).toEqual([expectedPathname, {state: 'closed'}]);
|
||||||
expect(prsGetPaginatedSpy.calls.argsFor(2)).toEqual([expectedPathname, {state: 'open'}]);
|
expect(githubApi.getPaginated.calls.argsFor(2)).toEqual([expectedPathname, {state: 'open'}]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should default to \'all\' if no state is specified', () => {
|
it('should default to \'all\' if no state is specified', () => {
|
||||||
prs.fetchAll();
|
prs.fetchAll();
|
||||||
expect(prsGetPaginatedSpy).toHaveBeenCalledWith('/repos/foo/bar/pulls', {state: 'all'});
|
expect(githubApi.getPaginated).toHaveBeenCalledWith('/repos/foo/bar/pulls', {state: 'all'});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should forward the value returned by \'getPaginated()\'', () => {
|
it('should forward the value returned by \'getPaginated()\'', () => {
|
||||||
prsGetPaginatedSpy.and.returnValue('Test');
|
githubApi.getPaginated.and.returnValue('Test');
|
||||||
expect(prs.fetchAll() as any).toBe('Test');
|
expect(prs.fetchAll() as any).toBe('Test');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('fetchFiles()', () => {
|
||||||
|
let prs: GithubPullRequests;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
prs = new GithubPullRequests(githubApi, 'foo', 'bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should make a paginated GET request to GitHub with the correct pathname', () => {
|
||||||
|
prs.fetchFiles(42);
|
||||||
|
expect(githubApi.getPaginated).toHaveBeenCalledWith('/repos/foo/bar/pulls/42/files');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should resolve with the data returned from GitHub', done => {
|
||||||
|
const expected: any = [{sha: 'ABCDE', filename: 'a/b/c'}, {sha: '12345', filename: 'x/y/z'}];
|
||||||
|
githubApi.getPaginated.and.callFake(() => Promise.resolve(expected));
|
||||||
|
prs.fetchFiles(42).then(data => {
|
||||||
|
expect(data).toEqual(expected);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -1,43 +1,40 @@
|
|||||||
// Imports
|
import {GithubApi} from '../../lib/common/github-api';
|
||||||
import {GithubTeams} from '../../lib/common/github-teams';
|
import {GithubTeams} from '../../lib/common/github-teams';
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
describe('GithubTeams', () => {
|
describe('GithubTeams', () => {
|
||||||
|
|
||||||
|
let githubApi: jasmine.SpyObj<GithubApi>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
githubApi = jasmine.createSpyObj('githubApi', ['post', 'get', 'getPaginated']);
|
||||||
|
});
|
||||||
|
|
||||||
describe('constructor()', () => {
|
describe('constructor()', () => {
|
||||||
|
|
||||||
it('should throw if \'githubToken\' is missing or empty', () => {
|
it('should throw if \'githubOrg\' is missing or empty', () => {
|
||||||
expect(() => new GithubTeams('', 'org')).
|
expect(() => new GithubTeams(githubApi, '')).
|
||||||
toThrowError('Missing or empty required parameter \'githubToken\'!');
|
toThrowError('Missing or empty required parameter \'githubOrg\'!');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should throw if \'organization\' is missing or empty', () => {
|
|
||||||
expect(() => new GithubTeams('12345', '')).
|
|
||||||
toThrowError('Missing or empty required parameter \'organization\'!');
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('fetchAll()', () => {
|
describe('fetchAll()', () => {
|
||||||
let teams: GithubTeams;
|
let teams: GithubTeams;
|
||||||
let teamsGetPaginatedSpy: jasmine.Spy;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
teams = new GithubTeams('12345', 'foo');
|
teams = new GithubTeams(githubApi, 'foo');
|
||||||
teamsGetPaginatedSpy = spyOn(teams as any, 'getPaginated');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should call \'getPaginated()\' with the correct pathname and params', () => {
|
it('should call \'getPaginated()\' with the correct pathname and params', () => {
|
||||||
teams.fetchAll();
|
teams.fetchAll();
|
||||||
expect(teamsGetPaginatedSpy).toHaveBeenCalledWith('/orgs/foo/teams');
|
expect(githubApi.getPaginated).toHaveBeenCalledWith('/orgs/foo/teams');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should forward the value returned by \'getPaginated()\'', () => {
|
it('should forward the value returned by \'getPaginated()\'', () => {
|
||||||
teamsGetPaginatedSpy.and.returnValue('Test');
|
githubApi.getPaginated.and.returnValue('Test');
|
||||||
expect(teams.fetchAll() as any).toBe('Test');
|
expect(teams.fetchAll() as any).toBe('Test');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -46,19 +43,15 @@ describe('GithubTeams', () => {
|
|||||||
|
|
||||||
describe('isMemberById()', () => {
|
describe('isMemberById()', () => {
|
||||||
let teams: GithubTeams;
|
let teams: GithubTeams;
|
||||||
let teamsGetSpy: jasmine.Spy;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
teams = new GithubTeams('12345', 'foo');
|
teams = new GithubTeams(githubApi, 'foo');
|
||||||
teamsGetSpy = spyOn(teams, 'get').and.returnValue(Promise.resolve(null));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should return a promise', done => {
|
it('should return a promise', () => {
|
||||||
|
githubApi.get.and.callFake(() => Promise.resolve());
|
||||||
const promise = teams.isMemberById('user', [1]);
|
const promise = teams.isMemberById('user', [1]);
|
||||||
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
|
||||||
// to avoid running the actual `get()`.
|
|
||||||
|
|
||||||
expect(promise).toEqual(jasmine.any(Promise));
|
expect(promise).toEqual(jasmine.any(Promise));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -66,42 +59,43 @@ describe('GithubTeams', () => {
|
|||||||
it('should resolve with false if called with an empty array', done => {
|
it('should resolve with false if called with an empty array', done => {
|
||||||
teams.isMemberById('user', []).then(isMember => {
|
teams.isMemberById('user', []).then(isMember => {
|
||||||
expect(isMember).toBe(false);
|
expect(isMember).toBe(false);
|
||||||
expect(teamsGetSpy).not.toHaveBeenCalled();
|
expect(githubApi.get).not.toHaveBeenCalled();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should call \'get()\' with the correct pathname', done => {
|
it('should call \'get()\' with the correct pathname', done => {
|
||||||
|
githubApi.get.and.callFake(() => Promise.resolve());
|
||||||
teams.isMemberById('user', [1]).then(() => {
|
teams.isMemberById('user', [1]).then(() => {
|
||||||
expect(teamsGetSpy).toHaveBeenCalledWith('/teams/1/memberships/user');
|
expect(githubApi.get).toHaveBeenCalledWith('/teams/1/memberships/user');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should resolve with false if \'get()\' rejects', done => {
|
it('should resolve with false if \'get()\' rejects', done => {
|
||||||
teamsGetSpy.and.returnValue(Promise.reject(null));
|
githubApi.get.and.callFake(() => Promise.reject(null));
|
||||||
teams.isMemberById('user', [1]).then(isMember => {
|
teams.isMemberById('user', [1]).then(isMember => {
|
||||||
expect(isMember).toBe(false);
|
expect(isMember).toBe(false);
|
||||||
expect(teamsGetSpy).toHaveBeenCalled();
|
expect(githubApi.get).toHaveBeenCalled();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should resolve with false if the membership is not active', done => {
|
it('should resolve with false if the membership is not active', done => {
|
||||||
teamsGetSpy.and.returnValue(Promise.resolve({state: 'pending'}));
|
githubApi.get.and.callFake(() => Promise.resolve({state: 'pending'}));
|
||||||
teams.isMemberById('user', [1]).then(isMember => {
|
teams.isMemberById('user', [1]).then(isMember => {
|
||||||
expect(isMember).toBe(false);
|
expect(isMember).toBe(false);
|
||||||
expect(teamsGetSpy).toHaveBeenCalled();
|
expect(githubApi.get).toHaveBeenCalled();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should resolve with true if the membership is active', done => {
|
it('should resolve with true if the membership is active', done => {
|
||||||
teamsGetSpy.and.returnValue(Promise.resolve({state: 'active'}));
|
githubApi.get.and.callFake(() => Promise.resolve({state: 'active'}));
|
||||||
teams.isMemberById('user', [1]).then(isMember => {
|
teams.isMemberById('user', [1]).then(isMember => {
|
||||||
expect(isMember).toBe(true);
|
expect(isMember).toBe(true);
|
||||||
done();
|
done();
|
||||||
@ -115,15 +109,15 @@ describe('GithubTeams', () => {
|
|||||||
'/teams/2/memberships/user': Promise.reject(null),
|
'/teams/2/memberships/user': Promise.reject(null),
|
||||||
'/teams/3/memberships/user': Promise.resolve({state: 'active'}),
|
'/teams/3/memberships/user': Promise.resolve({state: 'active'}),
|
||||||
};
|
};
|
||||||
teamsGetSpy.and.callFake((pathname: string) => trainedResponses[pathname]);
|
githubApi.get.and.callFake((pathname: string) => trainedResponses[pathname]);
|
||||||
|
|
||||||
teams.isMemberById('user', [1, 2, 3, 4]).then(isMember => {
|
teams.isMemberById('user', [1, 2, 3, 4]).then(isMember => {
|
||||||
expect(isMember).toBe(true);
|
expect(isMember).toBe(true);
|
||||||
|
|
||||||
expect(teamsGetSpy).toHaveBeenCalledTimes(3);
|
expect(githubApi.get).toHaveBeenCalledTimes(3);
|
||||||
expect(teamsGetSpy.calls.argsFor(0)[0]).toBe('/teams/1/memberships/user');
|
expect(githubApi.get.calls.argsFor(0)[0]).toBe('/teams/1/memberships/user');
|
||||||
expect(teamsGetSpy.calls.argsFor(1)[0]).toBe('/teams/2/memberships/user');
|
expect(githubApi.get.calls.argsFor(1)[0]).toBe('/teams/2/memberships/user');
|
||||||
expect(teamsGetSpy.calls.argsFor(2)[0]).toBe('/teams/3/memberships/user');
|
expect(githubApi.get.calls.argsFor(2)[0]).toBe('/teams/3/memberships/user');
|
||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@ -137,16 +131,16 @@ describe('GithubTeams', () => {
|
|||||||
'/teams/3/memberships/user': Promise.resolve({state: 'not active'}),
|
'/teams/3/memberships/user': Promise.resolve({state: 'not active'}),
|
||||||
'/teams/4/memberships/user': Promise.reject(null),
|
'/teams/4/memberships/user': Promise.reject(null),
|
||||||
};
|
};
|
||||||
teamsGetSpy.and.callFake((pathname: string) => trainedResponses[pathname]);
|
githubApi.get.and.callFake((pathname: string) => trainedResponses[pathname]);
|
||||||
|
|
||||||
teams.isMemberById('user', [1, 2, 3, 4]).then(isMember => {
|
teams.isMemberById('user', [1, 2, 3, 4]).then(isMember => {
|
||||||
expect(isMember).toBe(false);
|
expect(isMember).toBe(false);
|
||||||
|
|
||||||
expect(teamsGetSpy).toHaveBeenCalledTimes(4);
|
expect(githubApi.get).toHaveBeenCalledTimes(4);
|
||||||
expect(teamsGetSpy.calls.argsFor(0)[0]).toBe('/teams/1/memberships/user');
|
expect(githubApi.get.calls.argsFor(0)[0]).toBe('/teams/1/memberships/user');
|
||||||
expect(teamsGetSpy.calls.argsFor(1)[0]).toBe('/teams/2/memberships/user');
|
expect(githubApi.get.calls.argsFor(1)[0]).toBe('/teams/2/memberships/user');
|
||||||
expect(teamsGetSpy.calls.argsFor(2)[0]).toBe('/teams/3/memberships/user');
|
expect(githubApi.get.calls.argsFor(2)[0]).toBe('/teams/3/memberships/user');
|
||||||
expect(teamsGetSpy.calls.argsFor(3)[0]).toBe('/teams/4/memberships/user');
|
expect(githubApi.get.calls.argsFor(3)[0]).toBe('/teams/4/memberships/user');
|
||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@ -161,7 +155,7 @@ describe('GithubTeams', () => {
|
|||||||
let teamsIsMemberByIdSpy: jasmine.Spy;
|
let teamsIsMemberByIdSpy: jasmine.Spy;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
teams = new GithubTeams('12345', 'foo');
|
teams = new GithubTeams(githubApi, 'foo');
|
||||||
|
|
||||||
const mockResponse = Promise.resolve([{id: 1, slug: 'team1'}, {id: 2, slug: 'team2'}]);
|
const mockResponse = Promise.resolve([{id: 1, slug: 'team1'}, {id: 2, slug: 'team2'}]);
|
||||||
teamsFetchAllSpy = spyOn(teams, 'fetchAll').and.returnValue(mockResponse);
|
teamsFetchAllSpy = spyOn(teams, 'fetchAll').and.returnValue(mockResponse);
|
||||||
@ -181,7 +175,7 @@ describe('GithubTeams', () => {
|
|||||||
|
|
||||||
|
|
||||||
it('should resolve with false if \'fetchAll()\' rejects', done => {
|
it('should resolve with false if \'fetchAll()\' rejects', done => {
|
||||||
teamsFetchAllSpy.and.returnValue(Promise.reject(null));
|
teamsFetchAllSpy.and.callFake(() => Promise.reject(null));
|
||||||
teams.isMemberBySlug('user', ['team-slug']).then(isMember => {
|
teams.isMemberBySlug('user', ['team-slug']).then(isMember => {
|
||||||
expect(isMember).toBe(false);
|
expect(isMember).toBe(false);
|
||||||
done();
|
done();
|
||||||
@ -209,7 +203,7 @@ describe('GithubTeams', () => {
|
|||||||
|
|
||||||
|
|
||||||
it('should resolve with false if \'isMemberById()\' rejects', done => {
|
it('should resolve with false if \'isMemberById()\' rejects', done => {
|
||||||
teamsIsMemberByIdSpy.and.returnValue(Promise.reject(null));
|
teamsIsMemberByIdSpy.and.callFake(() => Promise.reject(null));
|
||||||
teams.isMemberBySlug('user', ['team1']).then(isMember => {
|
teams.isMemberBySlug('user', ['team1']).then(isMember => {
|
||||||
expect(isMember).toBe(false);
|
expect(isMember).toBe(false);
|
||||||
expect(teamsIsMemberByIdSpy).toHaveBeenCalled();
|
expect(teamsIsMemberByIdSpy).toHaveBeenCalled();
|
||||||
@ -218,16 +212,17 @@ describe('GithubTeams', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should resolve with the value \'isMemberById()\' resolves with', done => {
|
it('should resolve with the value \'isMemberById()\' resolves with', async () => {
|
||||||
teamsIsMemberByIdSpy.and.returnValues(Promise.resolve(false), Promise.resolve(true));
|
|
||||||
|
|
||||||
Promise.all([
|
teamsIsMemberByIdSpy.and.callFake(() => Promise.resolve(true));
|
||||||
teams.isMemberBySlug('user', ['team1']).then(isMember => expect(isMember).toBe(false)),
|
const isMember1 = await teams.isMemberBySlug('user', ['team1']);
|
||||||
teams.isMemberBySlug('user', ['team1']).then(isMember => expect(isMember).toBe(true)),
|
expect(isMember1).toBe(true);
|
||||||
]).then(() => {
|
expect(teamsIsMemberByIdSpy).toHaveBeenCalledWith('user', [1]);
|
||||||
expect(teamsIsMemberByIdSpy).toHaveBeenCalledTimes(2);
|
|
||||||
done();
|
teamsIsMemberByIdSpy.and.callFake(() => Promise.resolve(false));
|
||||||
});
|
const isMember2 = await teams.isMemberBySlug('user', ['team1']);
|
||||||
|
expect(isMember2).toBe(false);
|
||||||
|
expect(teamsIsMemberByIdSpy).toHaveBeenCalledWith('user', [1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -1,9 +1,59 @@
|
|||||||
// Imports
|
// Imports
|
||||||
import {assertNotMissingOrEmpty, getEnvVar} from '../../lib/common/utils';
|
import {resolve as resolvePath} from 'path';
|
||||||
|
import {
|
||||||
|
assert,
|
||||||
|
assertNotMissingOrEmpty,
|
||||||
|
computeArtifactDownloadPath,
|
||||||
|
computeShortSha,
|
||||||
|
getEnvVar,
|
||||||
|
getPrInfoFromDownloadPath,
|
||||||
|
Logger,
|
||||||
|
} from '../../lib/common/utils';
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
describe('utils', () => {
|
describe('utils', () => {
|
||||||
|
|
||||||
|
describe('computeShortSha', () => {
|
||||||
|
it('should return only the first SHORT_SHA_LEN characters of the SHA', () => {
|
||||||
|
expect(computeShortSha('0123456789')).toEqual('0123456');
|
||||||
|
expect(computeShortSha('ABC')).toEqual('ABC');
|
||||||
|
expect(computeShortSha('')).toEqual('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('assert', () => {
|
||||||
|
it('should throw if passed a false value', () => {
|
||||||
|
expect(() => assert(false, 'error message')).toThrowError('error message');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw if passed a true value', () => {
|
||||||
|
expect(() => assert(true, 'error message')).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('computeArtifactDownloadPath', () => {
|
||||||
|
it('should compute an absolute path based on the artifact info provided', () => {
|
||||||
|
const downloadDir = '/a/b/c';
|
||||||
|
const pr = 123;
|
||||||
|
const sha = 'ABCDEF1234567';
|
||||||
|
const artifactPath = 'a/path/to/file.zip';
|
||||||
|
const path = computeArtifactDownloadPath(downloadDir, pr, sha, artifactPath);
|
||||||
|
expect(path).toBe(resolvePath('/a/b/c/123-ABCDEF1-file.zip'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('getPrInfoFromDownloadPath', () => {
|
||||||
|
it('should extract the PR and SHA from the file path', () => {
|
||||||
|
const {pr, sha} = getPrInfoFromDownloadPath('a/b/c/12345-ABCDE-artifact.zip');
|
||||||
|
expect(pr).toEqual(12345);
|
||||||
|
expect(sha).toEqual('ABCDE');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('assertNotMissingOrEmpty()', () => {
|
describe('assertNotMissingOrEmpty()', () => {
|
||||||
|
|
||||||
it('should throw if passed an empty value', () => {
|
it('should throw if passed an empty value', () => {
|
||||||
@ -78,4 +128,79 @@ describe('utils', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('Logger', () => {
|
||||||
|
let consoleErrorSpy: jasmine.Spy;
|
||||||
|
let consoleInfoSpy: jasmine.Spy;
|
||||||
|
let consoleLogSpy: jasmine.Spy;
|
||||||
|
let consoleWarnSpy: jasmine.Spy;
|
||||||
|
let logger: Logger;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
consoleErrorSpy = spyOn(console, 'error');
|
||||||
|
consoleInfoSpy = spyOn(console, 'info');
|
||||||
|
consoleLogSpy = spyOn(console, 'log');
|
||||||
|
consoleWarnSpy = spyOn(console, 'warn');
|
||||||
|
|
||||||
|
logger = new Logger('TestScope');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should delegate to `console`', () => {
|
||||||
|
logger.error('foo');
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleErrorSpy.calls.argsFor(0)).toContain('foo');
|
||||||
|
|
||||||
|
logger.info('bar');
|
||||||
|
expect(consoleInfoSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleInfoSpy.calls.argsFor(0)).toContain('bar');
|
||||||
|
|
||||||
|
logger.log('baz');
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleLogSpy.calls.argsFor(0)).toContain('baz');
|
||||||
|
|
||||||
|
logger.warn('qux');
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(consoleWarnSpy.calls.argsFor(0)).toContain('qux');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should prepend messages with the current date and logger\'s scope', () => {
|
||||||
|
const mockDate = new Date(1337);
|
||||||
|
const expectedDateStr = `[${mockDate}]`;
|
||||||
|
const expectedScopeStr = 'TestScope: ';
|
||||||
|
|
||||||
|
jasmine.clock().mockDate(mockDate);
|
||||||
|
jasmine.clock().withMock(() => {
|
||||||
|
logger.error();
|
||||||
|
logger.info();
|
||||||
|
logger.log();
|
||||||
|
logger.warn();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledWith(expectedDateStr, expectedScopeStr);
|
||||||
|
expect(consoleInfoSpy).toHaveBeenCalledWith(expectedDateStr, expectedScopeStr);
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith(expectedDateStr, expectedScopeStr);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledWith(expectedDateStr, expectedScopeStr);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should pass all arguments to `console`', () => {
|
||||||
|
const someString = jasmine.any(String);
|
||||||
|
|
||||||
|
logger.error('foo1', 'foo2');
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledWith(someString, someString, 'foo1', 'foo2');
|
||||||
|
|
||||||
|
logger.info('bar1', 'bar2');
|
||||||
|
expect(consoleInfoSpy).toHaveBeenCalledWith(someString, someString, 'bar1', 'bar2');
|
||||||
|
|
||||||
|
logger.log('baz1', 'baz2');
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith(someString, someString, 'baz1', 'baz2');
|
||||||
|
|
||||||
|
logger.warn('qux1', 'qux2');
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledWith(someString, someString, 'qux1', 'qux2');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
declare namespace jasmine {
|
|
||||||
export interface DoneFn extends Function {
|
|
||||||
(): void;
|
|
||||||
fail: (message: Error | string) => void;
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,5 +3,4 @@ import {runTests} from '../lib/common/run-tests';
|
|||||||
|
|
||||||
// Run
|
// Run
|
||||||
const specFiles = [`${__dirname}/**/*.spec.js`];
|
const specFiles = [`${__dirname}/**/*.spec.js`];
|
||||||
const helpers = [`${__dirname}/helpers.js`];
|
runTests(specFiles);
|
||||||
runTests(specFiles, helpers);
|
|
||||||
|
@ -5,20 +5,21 @@ import * as fs from 'fs';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as shell from 'shelljs';
|
import * as shell from 'shelljs';
|
||||||
import {SHORT_SHA_LEN} from '../../lib/common/constants';
|
import {SHORT_SHA_LEN} from '../../lib/common/constants';
|
||||||
import {BuildCreator} from '../../lib/upload-server/build-creator';
|
import {Logger} from '../../lib/common/utils';
|
||||||
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/upload-server/build-events';
|
import {BuildCreator} from '../../lib/preview-server/build-creator';
|
||||||
import {UploadError} from '../../lib/upload-server/upload-error';
|
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/preview-server/build-events';
|
||||||
import {expectToBeUploadError} from './helpers';
|
import {PreviewServerError} from '../../lib/preview-server/preview-error';
|
||||||
|
import {expectToBePreviewServerError} from './helpers';
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
describe('BuildCreator', () => {
|
describe('BuildCreator', () => {
|
||||||
const pr = '9';
|
const pr = 9;
|
||||||
const sha = '9'.repeat(40);
|
const sha = '9'.repeat(40);
|
||||||
const shortSha = sha.substr(0, SHORT_SHA_LEN);
|
const shortSha = sha.substr(0, SHORT_SHA_LEN);
|
||||||
const archive = 'snapshot.tar.gz';
|
const archive = 'snapshot.tar.gz';
|
||||||
const buildsDir = 'builds/dir';
|
const buildsDir = 'builds/dir';
|
||||||
const hiddenPrDir = path.join(buildsDir, `hidden--${pr}`);
|
const hiddenPrDir = path.join(buildsDir, `hidden--${pr}`);
|
||||||
const publicPrDir = path.join(buildsDir, pr);
|
const publicPrDir = path.join(buildsDir, `${pr}`);
|
||||||
const hiddenShaDir = path.join(hiddenPrDir, shortSha);
|
const hiddenShaDir = path.join(hiddenPrDir, shortSha);
|
||||||
const publicShaDir = path.join(publicPrDir, shortSha);
|
const publicShaDir = path.join(publicPrDir, shortSha);
|
||||||
let bc: BuildCreator;
|
let bc: BuildCreator;
|
||||||
@ -134,8 +135,8 @@ describe('BuildCreator', () => {
|
|||||||
|
|
||||||
|
|
||||||
it('should abort and skip further operations if changing the PR\'s visibility fails', done => {
|
it('should abort and skip further operations if changing the PR\'s visibility fails', done => {
|
||||||
const mockError = new UploadError(543, 'Test');
|
const mockError = new PreviewServerError(543, 'Test');
|
||||||
bcUpdatePrVisibilitySpy.and.returnValue(Promise.reject(mockError));
|
bcUpdatePrVisibilitySpy.and.callFake(() => Promise.reject(mockError));
|
||||||
|
|
||||||
bc.create(pr, sha, archive, isPublic).catch(err => {
|
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||||
expect(err).toBe(mockError);
|
expect(err).toBe(mockError);
|
||||||
@ -154,7 +155,7 @@ describe('BuildCreator', () => {
|
|||||||
existsValues[shaDir] = true;
|
existsValues[shaDir] = true;
|
||||||
bc.create(pr, sha, archive, isPublic).catch(err => {
|
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||||
const publicOrNot = isPublic ? 'public' : 'non-public';
|
const publicOrNot = isPublic ? 'public' : 'non-public';
|
||||||
expectToBeUploadError(err, 409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`);
|
expectToBePreviewServerError(err, 409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`);
|
||||||
expect(shellMkdirSpy).not.toHaveBeenCalled();
|
expect(shellMkdirSpy).not.toHaveBeenCalled();
|
||||||
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
|
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
|
||||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||||
@ -171,7 +172,7 @@ describe('BuildCreator', () => {
|
|||||||
|
|
||||||
bc.create(pr, sha, archive, isPublic).catch(err => {
|
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||||
const publicOrNot = isPublic ? 'public' : 'non-public';
|
const publicOrNot = isPublic ? 'public' : 'non-public';
|
||||||
expectToBeUploadError(err, 409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`);
|
expectToBePreviewServerError(err, 409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`);
|
||||||
expect(shellMkdirSpy).not.toHaveBeenCalled();
|
expect(shellMkdirSpy).not.toHaveBeenCalled();
|
||||||
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
|
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
|
||||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||||
@ -222,20 +223,20 @@ describe('BuildCreator', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should reject with an UploadError', done => {
|
it('should reject with an PreviewServerError', done => {
|
||||||
// tslint:disable-next-line: no-string-throw
|
// tslint:disable-next-line: no-string-throw
|
||||||
shellMkdirSpy.and.callFake(() => { throw 'Test'; });
|
shellMkdirSpy.and.callFake(() => { throw 'Test'; });
|
||||||
bc.create(pr, sha, archive, isPublic).catch(err => {
|
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||||
expectToBeUploadError(err, 500, `Error while uploading to directory: ${shaDir}\nTest`);
|
expectToBePreviewServerError(err, 500, `Error while creating preview at: ${shaDir}\nTest`);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should pass UploadError instances unmodified', done => {
|
it('should pass PreviewServerError instances unmodified', done => {
|
||||||
shellMkdirSpy.and.callFake(() => { throw new UploadError(543, 'Test'); });
|
shellMkdirSpy.and.callFake(() => { throw new PreviewServerError(543, 'Test'); });
|
||||||
bc.create(pr, sha, archive, isPublic).catch(err => {
|
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||||
expectToBeUploadError(err, 543, 'Test');
|
expectToBePreviewServerError(err, 543, 'Test');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -324,7 +325,7 @@ describe('BuildCreator', () => {
|
|||||||
const shas = ['foo', 'bar', 'baz'];
|
const shas = ['foo', 'bar', 'baz'];
|
||||||
let emitted = false;
|
let emitted = false;
|
||||||
|
|
||||||
bcListShasByDate.and.returnValue(Promise.resolve(shas));
|
bcListShasByDate.and.callFake(() => Promise.resolve(shas));
|
||||||
bcEmitSpy.and.callFake((type: string, evt: ChangedPrVisibilityEvent) => {
|
bcEmitSpy.and.callFake((type: string, evt: ChangedPrVisibilityEvent) => {
|
||||||
expect(bcListShasByDate).toHaveBeenCalledWith(newPrDir);
|
expect(bcListShasByDate).toHaveBeenCalledWith(newPrDir);
|
||||||
|
|
||||||
@ -376,7 +377,8 @@ describe('BuildCreator', () => {
|
|||||||
it('should abort and skip further operations if both directories exist', done => {
|
it('should abort and skip further operations if both directories exist', done => {
|
||||||
bcExistsSpy.and.returnValue(true);
|
bcExistsSpy.and.returnValue(true);
|
||||||
bc.updatePrVisibility(pr, makePublic).catch(err => {
|
bc.updatePrVisibility(pr, makePublic).catch(err => {
|
||||||
expectToBeUploadError(err, 409, `Request to move '${oldPrDir}' to existing directory '${newPrDir}'.`);
|
expectToBePreviewServerError(err, 409,
|
||||||
|
`Request to move '${oldPrDir}' to existing directory '${newPrDir}'.`);
|
||||||
expect(shellMvSpy).not.toHaveBeenCalled();
|
expect(shellMvSpy).not.toHaveBeenCalled();
|
||||||
expect(bcListShasByDate).not.toHaveBeenCalled();
|
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||||
@ -407,20 +409,21 @@ describe('BuildCreator', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should reject with an UploadError', done => {
|
it('should reject with an PreviewServerError', done => {
|
||||||
// tslint:disable-next-line: no-string-throw
|
// tslint:disable-next-line: no-string-throw
|
||||||
shellMvSpy.and.callFake(() => { throw 'Test'; });
|
shellMvSpy.and.callFake(() => { throw 'Test'; });
|
||||||
bc.updatePrVisibility(pr, makePublic).catch(err => {
|
bc.updatePrVisibility(pr, makePublic).catch(err => {
|
||||||
expectToBeUploadError(err, 500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\nTest`);
|
expectToBePreviewServerError(err, 500,
|
||||||
|
`Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\nTest`);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should pass UploadError instances unmodified', done => {
|
it('should pass PreviewServerError instances unmodified', done => {
|
||||||
shellMvSpy.and.callFake(() => { throw new UploadError(543, 'Test'); });
|
shellMvSpy.and.callFake(() => { throw new PreviewServerError(543, 'Test'); });
|
||||||
bc.updatePrVisibility(pr, makePublic).catch(err => {
|
bc.updatePrVisibility(pr, makePublic).catch(err => {
|
||||||
expectToBeUploadError(err, 543, 'Test');
|
expectToBePreviewServerError(err, 543, 'Test');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -451,7 +454,7 @@ describe('BuildCreator', () => {
|
|||||||
|
|
||||||
it('should call \'fs.access()\' with the specified argument', () => {
|
it('should call \'fs.access()\' with the specified argument', () => {
|
||||||
(bc as any).exists('foo');
|
(bc as any).exists('foo');
|
||||||
expect(fs.access).toHaveBeenCalledWith('foo', jasmine.any(Function));
|
expect(fsAccessSpy).toHaveBeenCalledWith('foo', jasmine.any(Function));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -489,7 +492,7 @@ describe('BuildCreator', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cpExecCbs = [];
|
cpExecCbs = [];
|
||||||
|
|
||||||
consoleWarnSpy = spyOn(console, 'warn');
|
consoleWarnSpy = spyOn(Logger.prototype, 'warn');
|
||||||
shellChmodSpy = spyOn(shell, 'chmod');
|
shellChmodSpy = spyOn(shell, 'chmod');
|
||||||
shellRmSpy = spyOn(shell, 'rm');
|
shellRmSpy = spyOn(shell, 'rm');
|
||||||
cpExecSpy = spyOn(cp, 'exec').and.callFake((_: string, cb: (...args: any[]) => void) => cpExecCbs.push(cb));
|
cpExecSpy = spyOn(cp, 'exec').and.callFake((_: string, cb: (...args: any[]) => void) => cpExecCbs.push(cb));
|
||||||
@ -527,7 +530,7 @@ describe('BuildCreator', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should delete the uploaded file on success', done => {
|
it('should delete the build artifact file on success', done => {
|
||||||
(bc as any).extractArchive('input/file', 'output/dir').
|
(bc as any).extractArchive('input/file', 'output/dir').
|
||||||
then(() => expect(shellRmSpy).toHaveBeenCalledWith('-f', 'input/file')).
|
then(() => expect(shellRmSpy).toHaveBeenCalledWith('-f', 'input/file')).
|
||||||
then(done);
|
then(done);
|
||||||
@ -567,7 +570,7 @@ describe('BuildCreator', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should abort and reject if it fails to remove the uploaded file', done => {
|
it('should abort and reject if it fails to remove the build artifact file', done => {
|
||||||
(bc as any).extractArchive('foo', 'bar').catch((err: any) => {
|
(bc as any).extractArchive('foo', 'bar').catch((err: any) => {
|
||||||
expect(shellChmodSpy).toHaveBeenCalled();
|
expect(shellChmodSpy).toHaveBeenCalled();
|
||||||
expect(shellRmSpy).toHaveBeenCalled();
|
expect(shellRmSpy).toHaveBeenCalled();
|
||||||
@ -618,7 +621,7 @@ describe('BuildCreator', () => {
|
|||||||
|
|
||||||
|
|
||||||
it('should reject if listing files fails', done => {
|
it('should reject if listing files fails', done => {
|
||||||
shellLsSpy.and.returnValue(Promise.reject('Test'));
|
shellLsSpy.and.callFake(() => Promise.reject('Test'));
|
||||||
(bc as any).listShasByDate('input/dir').catch((err: string) => {
|
(bc as any).listShasByDate('input/dir').catch((err: string) => {
|
||||||
expect(err).toBe('Test');
|
expect(err).toBe('Test');
|
||||||
done();
|
done();
|
||||||
@ -627,7 +630,7 @@ describe('BuildCreator', () => {
|
|||||||
|
|
||||||
|
|
||||||
it('should return the filenames', done => {
|
it('should return the filenames', done => {
|
||||||
shellLsSpy.and.returnValue(Promise.resolve([
|
shellLsSpy.and.callFake(() => Promise.resolve([
|
||||||
lsResult('foo', 100),
|
lsResult('foo', 100),
|
||||||
lsResult('bar', 200),
|
lsResult('bar', 200),
|
||||||
lsResult('baz', 300),
|
lsResult('baz', 300),
|
||||||
@ -640,7 +643,7 @@ describe('BuildCreator', () => {
|
|||||||
|
|
||||||
|
|
||||||
it('should sort by date', done => {
|
it('should sort by date', done => {
|
||||||
shellLsSpy.and.returnValue(Promise.resolve([
|
shellLsSpy.and.callFake(() => Promise.resolve([
|
||||||
lsResult('foo', 300),
|
lsResult('foo', 300),
|
||||||
lsResult('bar', 100),
|
lsResult('bar', 100),
|
||||||
lsResult('baz', 200),
|
lsResult('baz', 200),
|
||||||
@ -660,7 +663,7 @@ describe('BuildCreator', () => {
|
|||||||
];
|
];
|
||||||
mockArray.sort = jasmine.createSpy('sort');
|
mockArray.sort = jasmine.createSpy('sort');
|
||||||
|
|
||||||
shellLsSpy.and.returnValue(Promise.resolve(mockArray));
|
shellLsSpy.and.callFake(() => Promise.resolve(mockArray));
|
||||||
(bc as any).listShasByDate('input/dir').
|
(bc as any).listShasByDate('input/dir').
|
||||||
then((shas: string[]) => {
|
then((shas: string[]) => {
|
||||||
expect(shas).toEqual(['bar', 'baz', 'foo']);
|
expect(shas).toEqual(['bar', 'baz', 'foo']);
|
||||||
@ -671,7 +674,7 @@ describe('BuildCreator', () => {
|
|||||||
|
|
||||||
|
|
||||||
it('should only include directories', done => {
|
it('should only include directories', done => {
|
||||||
shellLsSpy.and.returnValue(Promise.resolve([
|
shellLsSpy.and.callFake(() => Promise.resolve([
|
||||||
lsResult('foo', 100),
|
lsResult('foo', 100),
|
||||||
lsResult('bar', 200, false),
|
lsResult('bar', 200, false),
|
||||||
lsResult('baz', 300),
|
lsResult('baz', 300),
|
@ -1,5 +1,5 @@
|
|||||||
// Imports
|
// Imports
|
||||||
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/upload-server/build-events';
|
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/preview-server/build-events';
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
describe('ChangedPrVisibilityEvent', () => {
|
describe('ChangedPrVisibilityEvent', () => {
|
@ -0,0 +1,193 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as nock from 'nock';
|
||||||
|
import {resolve as resolvePath} from 'path';
|
||||||
|
import {BuildInfo, CircleCiApi} from '../../lib/common/circle-ci-api';
|
||||||
|
import {Logger} from '../../lib/common/utils';
|
||||||
|
import {BuildRetriever} from '../../lib/preview-server/build-retriever';
|
||||||
|
|
||||||
|
describe('BuildRetriever', () => {
|
||||||
|
const MAX_DOWNLOAD_SIZE = 10000;
|
||||||
|
const DOWNLOAD_DIR = resolvePath('/DOWNLOAD/DIR');
|
||||||
|
const BASE_URL = 'http://test.com';
|
||||||
|
const ARTIFACT_PATH = '/some/path/build.zip';
|
||||||
|
|
||||||
|
let api: CircleCiApi;
|
||||||
|
let BUILD_INFO: BuildInfo;
|
||||||
|
let WRITEFILE_RESULT: any;
|
||||||
|
let writeFileSpy: jasmine.Spy;
|
||||||
|
let EXISTS_RESULT: boolean;
|
||||||
|
let existsSpy: jasmine.Spy;
|
||||||
|
let getBuildArtifactUrlSpy: jasmine.Spy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
BUILD_INFO = {
|
||||||
|
branch: 'pull/777',
|
||||||
|
build_num: 12345,
|
||||||
|
failed: false,
|
||||||
|
has_artifacts: true,
|
||||||
|
outcome: 'success',
|
||||||
|
reponame: 'REPO',
|
||||||
|
username: 'ORG',
|
||||||
|
vcs_revision: 'COMMIT',
|
||||||
|
};
|
||||||
|
|
||||||
|
api = new CircleCiApi('ORG', 'REPO', 'TOKEN');
|
||||||
|
spyOn(api, 'getBuildInfo').and.callFake(() => Promise.resolve(BUILD_INFO));
|
||||||
|
getBuildArtifactUrlSpy = spyOn(api, 'getBuildArtifactUrl')
|
||||||
|
.and.callFake(() => Promise.resolve(BASE_URL + ARTIFACT_PATH));
|
||||||
|
|
||||||
|
WRITEFILE_RESULT = undefined;
|
||||||
|
writeFileSpy = spyOn(fs, 'writeFile').and.callFake(
|
||||||
|
(_path: string, _buffer: Buffer, callback: (err?: any) => {}) => callback(WRITEFILE_RESULT),
|
||||||
|
);
|
||||||
|
|
||||||
|
EXISTS_RESULT = false;
|
||||||
|
existsSpy = spyOn(fs, 'exists').and.callFake(
|
||||||
|
(_path: string, callback: (exists: boolean) => {}) => callback(EXISTS_RESULT),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('should fail if the "downloadSizeLimit" is invalid', () => {
|
||||||
|
expect(() => new BuildRetriever(api, NaN, DOWNLOAD_DIR))
|
||||||
|
.toThrowError(`Invalid parameter "downloadSizeLimit" should be a number greater than 0.`);
|
||||||
|
expect(() => new BuildRetriever(api, 0, DOWNLOAD_DIR))
|
||||||
|
.toThrowError(`Invalid parameter "downloadSizeLimit" should be a number greater than 0.`);
|
||||||
|
expect(() => new BuildRetriever(api, -1, DOWNLOAD_DIR))
|
||||||
|
.toThrowError(`Invalid parameter "downloadSizeLimit" should be a number greater than 0.`);
|
||||||
|
});
|
||||||
|
it('should fail if the "downloadDir" is missing', () => {
|
||||||
|
expect(() => new BuildRetriever(api, MAX_DOWNLOAD_SIZE, ''))
|
||||||
|
.toThrowError(`Missing or empty required parameter 'downloadDir'!`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('getGithubInfo', () => {
|
||||||
|
it('should request the info from CircleCI', async () => {
|
||||||
|
const retriever = new BuildRetriever(api, MAX_DOWNLOAD_SIZE, DOWNLOAD_DIR);
|
||||||
|
const info = await retriever.getGithubInfo(12345);
|
||||||
|
expect(api.getBuildInfo).toHaveBeenCalledWith(12345);
|
||||||
|
expect(info).toEqual({org: 'ORG', pr: 777, repo: 'REPO', sha: 'COMMIT', success: true});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should error if it is not possible to extract the PR number from the branch', async () => {
|
||||||
|
const retriever = new BuildRetriever(api, MAX_DOWNLOAD_SIZE, DOWNLOAD_DIR);
|
||||||
|
try {
|
||||||
|
BUILD_INFO.branch = 'master';
|
||||||
|
await retriever.getGithubInfo(12345);
|
||||||
|
throw new Error('Exception Expected');
|
||||||
|
} catch (error) {
|
||||||
|
expect(error.message).toEqual('No PR found in branch field: master');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('downloadBuildArtifact', () => {
|
||||||
|
const ARTIFACT_CONTENTS = 'ARTIFACT CONTENTS';
|
||||||
|
let retriever: BuildRetriever;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(Logger.prototype, 'warn');
|
||||||
|
retriever = new BuildRetriever(api, MAX_DOWNLOAD_SIZE, DOWNLOAD_DIR);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get the artifact URL from the CircleCI API', async () => {
|
||||||
|
const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(200, ARTIFACT_CONTENTS);
|
||||||
|
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
|
||||||
|
expect(api.getBuildArtifactUrl).toHaveBeenCalledWith(12345, ARTIFACT_PATH);
|
||||||
|
artifactRequest.done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should download the artifact from its URL', async () => {
|
||||||
|
const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(200, ARTIFACT_CONTENTS);
|
||||||
|
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
|
||||||
|
// The following line proves that the artifact URL fetch occurred.
|
||||||
|
artifactRequest.done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail if the artifact is too large', async () => {
|
||||||
|
const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(200, ARTIFACT_CONTENTS);
|
||||||
|
retriever = new BuildRetriever(api, 10, DOWNLOAD_DIR);
|
||||||
|
try {
|
||||||
|
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
|
||||||
|
throw new Error('Exception Expected');
|
||||||
|
} catch (error) {
|
||||||
|
expect(error.status).toEqual(413);
|
||||||
|
}
|
||||||
|
artifactRequest.done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not download the artifact if it already exists', async () => {
|
||||||
|
const artifactRequestInterceptor = nock(BASE_URL).get(ARTIFACT_PATH);
|
||||||
|
const artifactRequest = artifactRequestInterceptor.reply(200, ARTIFACT_CONTENTS);
|
||||||
|
EXISTS_RESULT = true;
|
||||||
|
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
|
||||||
|
expect(existsSpy).toHaveBeenCalled();
|
||||||
|
expect(getBuildArtifactUrlSpy).not.toHaveBeenCalled();
|
||||||
|
expect(artifactRequest.isDone()).toEqual(false);
|
||||||
|
nock.removeInterceptor(artifactRequestInterceptor);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should write the artifact file to disk', async () => {
|
||||||
|
const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(200, ARTIFACT_CONTENTS);
|
||||||
|
const downloadPath = resolvePath(`${DOWNLOAD_DIR}/777-COMMIT-build.zip`);
|
||||||
|
|
||||||
|
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
|
||||||
|
expect(writeFileSpy).toHaveBeenCalledWith(downloadPath, jasmine.any(Buffer), jasmine.any(Function));
|
||||||
|
|
||||||
|
const buffer: Buffer = writeFileSpy.calls.mostRecent().args[1];
|
||||||
|
expect(buffer.toString()).toEqual(ARTIFACT_CONTENTS);
|
||||||
|
|
||||||
|
artifactRequest.done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail if the CircleCI API fails', async () => {
|
||||||
|
try {
|
||||||
|
getBuildArtifactUrlSpy.and.callFake(() => Promise.reject('getBuildArtifactUrl failed'));
|
||||||
|
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
|
||||||
|
throw new Error('Exception Expected');
|
||||||
|
} catch (error) {
|
||||||
|
expect(error.message).toEqual('CircleCI artifact download failed (getBuildArtifactUrl failed)');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail if the URL fetch errors', async () => {
|
||||||
|
// create a new handler that errors
|
||||||
|
const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).replyWithError('Artifact Request Failed');
|
||||||
|
try {
|
||||||
|
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
|
||||||
|
throw new Error('Exception Expected');
|
||||||
|
} catch (error) {
|
||||||
|
expect(error.message).toEqual('CircleCI artifact download failed ' +
|
||||||
|
'(request to http://test.com/some/path/build.zip failed, reason: Artifact Request Failed)');
|
||||||
|
}
|
||||||
|
artifactRequest.done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail if the URL fetch 404s', async () => {
|
||||||
|
// create a new handler that errors
|
||||||
|
const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(404, 'No such artifact');
|
||||||
|
try {
|
||||||
|
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
|
||||||
|
throw new Error('Exception Expected');
|
||||||
|
} catch (error) {
|
||||||
|
expect(error.message).toEqual('CircleCI artifact download failed (Error 404 - Not Found)');
|
||||||
|
}
|
||||||
|
artifactRequest.done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail if file write fails', async () => {
|
||||||
|
const artifactRequest = nock(BASE_URL).get(ARTIFACT_PATH).reply(200, ARTIFACT_CONTENTS);
|
||||||
|
try {
|
||||||
|
WRITEFILE_RESULT = 'Test Error';
|
||||||
|
await retriever.downloadBuildArtifact(12345, 777, 'COMMIT', ARTIFACT_PATH);
|
||||||
|
throw new Error('Exception Expected');
|
||||||
|
} catch (error) {
|
||||||
|
expect(error.message).toEqual('CircleCI artifact download failed (Test Error)');
|
||||||
|
}
|
||||||
|
artifactRequest.done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,180 @@
|
|||||||
|
// Imports
|
||||||
|
import {GithubApi} from '../../lib/common/github-api';
|
||||||
|
import {GithubPullRequests, PullRequest} from '../../lib/common/github-pull-requests';
|
||||||
|
import {GithubTeams} from '../../lib/common/github-teams';
|
||||||
|
import {BuildVerifier} from '../../lib/preview-server/build-verifier';
|
||||||
|
|
||||||
|
// Tests
|
||||||
|
describe('BuildVerifier', () => {
|
||||||
|
const defaultConfig = {
|
||||||
|
allowedTeamSlugs: ['team1', 'team2'],
|
||||||
|
githubOrg: 'organization',
|
||||||
|
githubRepo: 'repo',
|
||||||
|
githubToken: 'githubToken',
|
||||||
|
secret: 'secret',
|
||||||
|
trustedPrLabel: 'trusted: pr-label',
|
||||||
|
};
|
||||||
|
let prs: GithubPullRequests;
|
||||||
|
let bv: BuildVerifier;
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
const createBuildVerifier = (partialConfig: Partial<typeof defaultConfig> = {}) => {
|
||||||
|
const cfg = {...defaultConfig, ...partialConfig} as typeof defaultConfig;
|
||||||
|
const api = new GithubApi(cfg.githubToken);
|
||||||
|
prs = new GithubPullRequests(api, cfg.githubOrg, cfg.githubRepo);
|
||||||
|
const teams = new GithubTeams(api, cfg.githubOrg);
|
||||||
|
return new BuildVerifier(prs, teams, cfg.allowedTeamSlugs, cfg.trustedPrLabel);
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => bv = createBuildVerifier());
|
||||||
|
|
||||||
|
|
||||||
|
describe('constructor()', () => {
|
||||||
|
|
||||||
|
['githubToken', 'githubRepo', 'githubOrg', 'allowedTeamSlugs', 'trustedPrLabel'].
|
||||||
|
forEach(param => {
|
||||||
|
it(`should throw if '${param}' is missing or empty`, () => {
|
||||||
|
expect(() => createBuildVerifier({[param]: ''})).
|
||||||
|
toThrowError(`Missing or empty required parameter '${param}'!`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should throw if \'allowedTeamSlugs\' is an empty array', () => {
|
||||||
|
expect(() => createBuildVerifier({allowedTeamSlugs: []})).
|
||||||
|
toThrowError('Missing or empty required parameter \'allowedTeamSlugs\'!');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('getSignificantFilesChanged', () => {
|
||||||
|
it('should return false if none of the fetched files match the given pattern', async () => {
|
||||||
|
const fetchFilesSpy = spyOn(prs, 'fetchFiles');
|
||||||
|
fetchFilesSpy.and.callFake(() => Promise.resolve([{filename: 'a/b/c'}, {filename: 'd/e/f'}]));
|
||||||
|
expect(await bv.getSignificantFilesChanged(777, /^x/)).toEqual(false);
|
||||||
|
expect(fetchFilesSpy).toHaveBeenCalledWith(777);
|
||||||
|
|
||||||
|
fetchFilesSpy.calls.reset();
|
||||||
|
expect(await bv.getSignificantFilesChanged(777, /^a/)).toEqual(true);
|
||||||
|
expect(fetchFilesSpy).toHaveBeenCalledWith(777);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('getPrIsTrusted()', () => {
|
||||||
|
const pr = 9;
|
||||||
|
let mockPrInfo: PullRequest;
|
||||||
|
let prsFetchSpy: jasmine.Spy;
|
||||||
|
let teamsIsMemberBySlugSpy: jasmine.Spy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockPrInfo = {
|
||||||
|
labels: [
|
||||||
|
{name: 'foo'},
|
||||||
|
{name: 'bar'},
|
||||||
|
],
|
||||||
|
number: 9,
|
||||||
|
user: {login: 'username'},
|
||||||
|
};
|
||||||
|
|
||||||
|
prsFetchSpy = spyOn(GithubPullRequests.prototype, 'fetch').
|
||||||
|
and.callFake(() => Promise.resolve(mockPrInfo));
|
||||||
|
|
||||||
|
teamsIsMemberBySlugSpy = spyOn(GithubTeams.prototype, 'isMemberBySlug').
|
||||||
|
and.callFake(() => Promise.resolve(true));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should return a promise', done => {
|
||||||
|
const promise = bv.getPrIsTrusted(pr);
|
||||||
|
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
||||||
|
// to avoid running the actual `GithubTeams#isMemberBySlug()`.
|
||||||
|
|
||||||
|
expect(promise).toEqual(jasmine.any(Promise));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should fetch the corresponding PR', done => {
|
||||||
|
bv.getPrIsTrusted(pr).then(() => {
|
||||||
|
expect(prsFetchSpy).toHaveBeenCalledWith(pr);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should fail if fetching the PR errors', done => {
|
||||||
|
prsFetchSpy.and.callFake(() => Promise.reject('Test'));
|
||||||
|
bv.getPrIsTrusted(pr).catch(err => {
|
||||||
|
expect(err).toBe('Test');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('when the PR has the "trusted PR" label', () => {
|
||||||
|
|
||||||
|
beforeEach(() => mockPrInfo.labels.push({name: 'trusted: pr-label'}));
|
||||||
|
|
||||||
|
|
||||||
|
it('should resolve to true', done => {
|
||||||
|
bv.getPrIsTrusted(pr).then(isTrusted => {
|
||||||
|
expect(isTrusted).toBe(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should not try to verify the author\'s membership status', done => {
|
||||||
|
bv.getPrIsTrusted(pr).then(() => {
|
||||||
|
expect(teamsIsMemberBySlugSpy).not.toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('when the PR does not have the "trusted PR" label', () => {
|
||||||
|
|
||||||
|
it('should verify the PR author\'s membership in the specified teams', done => {
|
||||||
|
bv.getPrIsTrusted(pr).then(() => {
|
||||||
|
expect(teamsIsMemberBySlugSpy).toHaveBeenCalledWith('username', ['team1', 'team2']);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should fail if verifying membership errors', done => {
|
||||||
|
teamsIsMemberBySlugSpy.and.callFake(() => Promise.reject('Test'));
|
||||||
|
bv.getPrIsTrusted(pr).catch(err => {
|
||||||
|
expect(err).toBe('Test');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should resolve to true if the PR\'s author is a member', done => {
|
||||||
|
teamsIsMemberBySlugSpy.and.callFake(() => Promise.resolve(true));
|
||||||
|
|
||||||
|
bv.getPrIsTrusted(pr).then(isTrusted => {
|
||||||
|
expect(isTrusted).toBe(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should resolve to false if the PR\'s author is not a member', done => {
|
||||||
|
teamsIsMemberBySlugSpy.and.callFake(() => Promise.resolve(false));
|
||||||
|
|
||||||
|
bv.getPrIsTrusted(pr).then(isTrusted => {
|
||||||
|
expect(isTrusted).toBe(false);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -0,0 +1,11 @@
|
|||||||
|
import {PreviewServerError} from '../../lib/preview-server/preview-error';
|
||||||
|
|
||||||
|
export const expectToBePreviewServerError = (actual: PreviewServerError, status?: number, message?: string) => {
|
||||||
|
expect(actual).toEqual(jasmine.any(PreviewServerError));
|
||||||
|
if (status != null) {
|
||||||
|
expect(actual.status).toBe(status);
|
||||||
|
}
|
||||||
|
if (message != null) {
|
||||||
|
expect(actual.message).toBe(message);
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,39 @@
|
|||||||
|
// Imports
|
||||||
|
import {PreviewServerError} from '../../lib/preview-server/preview-error';
|
||||||
|
|
||||||
|
// Tests
|
||||||
|
describe('PreviewServerError', () => {
|
||||||
|
let err: PreviewServerError;
|
||||||
|
|
||||||
|
beforeEach(() => err = new PreviewServerError(999, 'message'));
|
||||||
|
|
||||||
|
|
||||||
|
it('should extend Error', () => {
|
||||||
|
expect(err).toEqual(jasmine.any(PreviewServerError));
|
||||||
|
expect(err).toEqual(jasmine.any(Error));
|
||||||
|
|
||||||
|
expect(Object.getPrototypeOf(err)).toBe(PreviewServerError.prototype);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should have a \'status\' property', () => {
|
||||||
|
expect(err.status).toBe(999);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should have a \'message\' property', () => {
|
||||||
|
expect(err.message).toBe('message');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should have a 500 \'status\' by default', () => {
|
||||||
|
expect(new PreviewServerError().status).toBe(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should have an empty \'message\' by default', () => {
|
||||||
|
expect(new PreviewServerError().message).toBe('');
|
||||||
|
expect(new PreviewServerError(999).message).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -0,0 +1,692 @@
|
|||||||
|
// Imports
|
||||||
|
import * as express from 'express';
|
||||||
|
import * as http from 'http';
|
||||||
|
import * as supertest from 'supertest';
|
||||||
|
import {CircleCiApi} from '../../lib/common/circle-ci-api';
|
||||||
|
import {GithubApi} from '../../lib/common/github-api';
|
||||||
|
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
||||||
|
import {GithubTeams} from '../../lib/common/github-teams';
|
||||||
|
import {Logger} from '../../lib/common/utils';
|
||||||
|
import {BuildCreator} from '../../lib/preview-server/build-creator';
|
||||||
|
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/preview-server/build-events';
|
||||||
|
import {BuildRetriever, GithubInfo} from '../../lib/preview-server/build-retriever';
|
||||||
|
import {BuildVerifier} from '../../lib/preview-server/build-verifier';
|
||||||
|
import {PreviewServerConfig, PreviewServerFactory} from '../../lib/preview-server/preview-server-factory';
|
||||||
|
|
||||||
|
interface CircleCiWebHookPayload {
|
||||||
|
payload: {
|
||||||
|
build_num: number;
|
||||||
|
build_parameters: {
|
||||||
|
CIRCLE_JOB: string;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests
|
||||||
|
describe('PreviewServerFactory', () => {
|
||||||
|
const defaultConfig: PreviewServerConfig = {
|
||||||
|
buildArtifactPath: 'artifact/path.zip',
|
||||||
|
buildsDir: 'builds/dir',
|
||||||
|
circleCiToken: 'CIRCLE_CI_TOKEN',
|
||||||
|
domainName: 'domain.name',
|
||||||
|
downloadSizeLimit: 999,
|
||||||
|
downloadsDir: '/tmp/aio-create-builds',
|
||||||
|
githubOrg: 'organisation',
|
||||||
|
githubRepo: 'repo',
|
||||||
|
githubTeamSlugs: ['team1', 'team2'],
|
||||||
|
githubToken: '12345',
|
||||||
|
significantFilesPattern: '^(?:aio|packages)\\/(?!.*[._]spec\\.[jt]s$)',
|
||||||
|
trustedPrLabel: 'trusted: pr-label',
|
||||||
|
};
|
||||||
|
let loggerErrorSpy: jasmine.Spy;
|
||||||
|
let loggerInfoSpy: jasmine.Spy;
|
||||||
|
let loggerLogSpy: jasmine.Spy;
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
const createPreviewServer = (partialConfig: Partial<PreviewServerConfig> = {}) =>
|
||||||
|
PreviewServerFactory.create({...defaultConfig, ...partialConfig});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
loggerErrorSpy = spyOn(Logger.prototype, 'error');
|
||||||
|
loggerInfoSpy = spyOn(Logger.prototype, 'info');
|
||||||
|
loggerLogSpy = spyOn(Logger.prototype, 'log');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('create()', () => {
|
||||||
|
let usfCreateMiddlewareSpy: jasmine.Spy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
usfCreateMiddlewareSpy = spyOn(PreviewServerFactory, 'createMiddleware').and.callThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should throw if \'buildsDir\' is missing or empty', () => {
|
||||||
|
expect(() => createPreviewServer({buildsDir: ''})).
|
||||||
|
toThrowError('Missing or empty required parameter \'buildsDir\'!');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should throw if \'domainName\' is missing or empty', () => {
|
||||||
|
expect(() => createPreviewServer({domainName: ''})).
|
||||||
|
toThrowError('Missing or empty required parameter \'domainName\'!');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should throw if \'githubToken\' is missing or empty', () => {
|
||||||
|
expect(() => createPreviewServer({githubToken: ''})).
|
||||||
|
toThrowError('Missing or empty required parameter \'githubToken\'!');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should throw if \'githubOrg\' is missing or empty', () => {
|
||||||
|
expect(() => createPreviewServer({githubOrg: ''})).
|
||||||
|
toThrowError('Missing or empty required parameter \'githubOrg\'!');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should throw if \'githubTeamSlugs\' is missing or empty', () => {
|
||||||
|
expect(() => createPreviewServer({githubTeamSlugs: []})).
|
||||||
|
toThrowError('Missing or empty required parameter \'allowedTeamSlugs\'!');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should throw if \'githubRepo\' is missing or empty', () => {
|
||||||
|
expect(() => createPreviewServer({githubRepo: ''})).
|
||||||
|
toThrowError('Missing or empty required parameter \'githubRepo\'!');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should throw if \'trustedPrLabel\' is missing or empty', () => {
|
||||||
|
expect(() => createPreviewServer({trustedPrLabel: ''})).
|
||||||
|
toThrowError('Missing or empty required parameter \'trustedPrLabel\'!');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should return an http.Server', () => {
|
||||||
|
const httpCreateServerSpy = spyOn(http, 'createServer').and.callThrough();
|
||||||
|
const server = createPreviewServer();
|
||||||
|
|
||||||
|
expect(server).toBe(httpCreateServerSpy.calls.mostRecent().returnValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should create and use an appropriate BuildCreator', () => {
|
||||||
|
const usfCreateBuildCreatorSpy = spyOn(PreviewServerFactory, 'createBuildCreator').and.callThrough();
|
||||||
|
|
||||||
|
createPreviewServer();
|
||||||
|
const buildRetriever = jasmine.any(BuildRetriever);
|
||||||
|
const buildVerifier = jasmine.any(BuildVerifier);
|
||||||
|
const prs = jasmine.any(GithubPullRequests);
|
||||||
|
const buildCreator: BuildCreator = usfCreateBuildCreatorSpy.calls.mostRecent().returnValue;
|
||||||
|
|
||||||
|
expect(usfCreateMiddlewareSpy).toHaveBeenCalledWith(buildRetriever, buildVerifier, buildCreator, defaultConfig);
|
||||||
|
expect(usfCreateBuildCreatorSpy).toHaveBeenCalledWith(prs, 'builds/dir', 'domain.name');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should create and use an appropriate middleware', () => {
|
||||||
|
const httpCreateServerSpy = spyOn(http, 'createServer').and.callThrough();
|
||||||
|
|
||||||
|
createPreviewServer();
|
||||||
|
|
||||||
|
const buildRetriever = jasmine.any(BuildRetriever);
|
||||||
|
const buildVerifier = jasmine.any(BuildVerifier);
|
||||||
|
const buildCreator = jasmine.any(BuildCreator);
|
||||||
|
expect(usfCreateMiddlewareSpy).toHaveBeenCalledWith(buildRetriever, buildVerifier, buildCreator, defaultConfig);
|
||||||
|
|
||||||
|
const middleware: express.Express = usfCreateMiddlewareSpy.calls.mostRecent().returnValue;
|
||||||
|
expect(httpCreateServerSpy).toHaveBeenCalledWith(middleware);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should log the server address info on \'listening\'', () => {
|
||||||
|
const server = createPreviewServer();
|
||||||
|
server.address = () => ({address: 'foo', family: '', port: 1337});
|
||||||
|
|
||||||
|
expect(loggerInfoSpy).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
server.emit('listening');
|
||||||
|
expect(loggerInfoSpy).toHaveBeenCalledWith('Up and running (and listening on foo:1337)...');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Protected methods
|
||||||
|
|
||||||
|
describe('createBuildCreator()', () => {
|
||||||
|
let buildCreator: BuildCreator;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const api = new GithubApi(defaultConfig.githubToken);
|
||||||
|
const prs = new GithubPullRequests(api, defaultConfig.githubOrg, defaultConfig.githubRepo);
|
||||||
|
buildCreator = PreviewServerFactory.createBuildCreator(prs, defaultConfig.buildsDir, defaultConfig.domainName);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass the \'buildsDir\' to the BuildCreator', () => {
|
||||||
|
expect((buildCreator as any).buildsDir).toBe('builds/dir');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('on \'build.created\'', () => {
|
||||||
|
let prsAddCommentSpy: jasmine.Spy;
|
||||||
|
|
||||||
|
beforeEach(() => prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment'));
|
||||||
|
|
||||||
|
|
||||||
|
it('should post a comment on GitHub for public previews', () => {
|
||||||
|
const commentBody = 'You can preview 1234567890 at https://pr42-1234567890.domain.name/.';
|
||||||
|
|
||||||
|
buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890', isPublic: true});
|
||||||
|
expect(prsAddCommentSpy).toHaveBeenCalledWith(42, commentBody);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should not post a comment on GitHub for non-public previews', () => {
|
||||||
|
buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890', isPublic: false});
|
||||||
|
expect(prsAddCommentSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('on \'pr.changedVisibility\'', () => {
|
||||||
|
let prsAddCommentSpy: jasmine.Spy;
|
||||||
|
|
||||||
|
beforeEach(() => prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment'));
|
||||||
|
|
||||||
|
|
||||||
|
it('should post a comment on GitHub (for all SHAs) for PRs made public', () => {
|
||||||
|
const commentBody = 'You can preview 12345 at https://pr42-12345.domain.name/.\n' +
|
||||||
|
'You can preview 67890 at https://pr42-67890.domain.name/.';
|
||||||
|
|
||||||
|
buildCreator.emit(ChangedPrVisibilityEvent.type, {pr: 42, shas: ['12345', '67890'], isPublic: true});
|
||||||
|
expect(prsAddCommentSpy).toHaveBeenCalledWith(42, commentBody);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should not post a comment on GitHub if no SHAs were affected', () => {
|
||||||
|
buildCreator.emit(ChangedPrVisibilityEvent.type, {pr: 42, shas: [], isPublic: true});
|
||||||
|
expect(prsAddCommentSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should not post a comment on GitHub for PRs made non-public', () => {
|
||||||
|
buildCreator.emit(ChangedPrVisibilityEvent.type, {pr: 42, shas: ['12345', '67890'], isPublic: false});
|
||||||
|
expect(prsAddCommentSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should pass the correct parameters to GithubPullRequests', () => {
|
||||||
|
const prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment');
|
||||||
|
|
||||||
|
buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890', isPublic: true});
|
||||||
|
buildCreator.emit(ChangedPrVisibilityEvent.type, {pr: 42, shas: ['12345', '67890'], isPublic: true});
|
||||||
|
|
||||||
|
const allCalls = prsAddCommentSpy.calls.all();
|
||||||
|
const prs: GithubPullRequests = allCalls[0].object;
|
||||||
|
|
||||||
|
expect(prsAddCommentSpy).toHaveBeenCalledTimes(2);
|
||||||
|
expect(prs).toBe(allCalls[1].object);
|
||||||
|
expect(prs).toEqual(jasmine.any(GithubPullRequests));
|
||||||
|
expect(prs.repoSlug).toBe('organisation/repo');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('createMiddleware()', () => {
|
||||||
|
let buildRetriever: BuildRetriever;
|
||||||
|
let buildVerifier: BuildVerifier;
|
||||||
|
let buildCreator: BuildCreator;
|
||||||
|
let agent: supertest.SuperTest<supertest.Test>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const circleCiApi = new CircleCiApi(defaultConfig.githubOrg, defaultConfig.githubRepo,
|
||||||
|
defaultConfig.circleCiToken);
|
||||||
|
const githubApi = new GithubApi(defaultConfig.githubToken);
|
||||||
|
const prs = new GithubPullRequests(githubApi, defaultConfig.githubOrg, defaultConfig.githubRepo);
|
||||||
|
const teams = new GithubTeams(githubApi, defaultConfig.githubOrg);
|
||||||
|
|
||||||
|
buildRetriever = new BuildRetriever(circleCiApi, defaultConfig.downloadSizeLimit, defaultConfig.downloadsDir);
|
||||||
|
buildVerifier = new BuildVerifier(prs, teams, defaultConfig.githubTeamSlugs, defaultConfig.trustedPrLabel);
|
||||||
|
buildCreator = new BuildCreator(defaultConfig.buildsDir);
|
||||||
|
|
||||||
|
const middleware = PreviewServerFactory.createMiddleware(buildRetriever, buildVerifier, buildCreator,
|
||||||
|
defaultConfig);
|
||||||
|
agent = supertest.agent(middleware);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('GET /health-check', () => {
|
||||||
|
|
||||||
|
it('should respond with 200', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
agent.get('/health-check').expect(200),
|
||||||
|
agent.get('/health-check/').expect(200),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 404 for non-GET requests', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
agent.put('/health-check').expect(404),
|
||||||
|
agent.post('/health-check').expect(404),
|
||||||
|
agent.patch('/health-check').expect(404),
|
||||||
|
agent.delete('/health-check').expect(404),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 404 if the path does not match exactly', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
agent.get('/health-check/foo').expect(404),
|
||||||
|
agent.get('/health-check-foo').expect(404),
|
||||||
|
agent.get('/health-checknfoo').expect(404),
|
||||||
|
agent.get('/foo/health-check').expect(404),
|
||||||
|
agent.get('/foo-health-check').expect(404),
|
||||||
|
agent.get('/foonhealth-check').expect(404),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('GET /can-have-public-preview/<pr>', () => {
|
||||||
|
const baseUrl = '/can-have-public-preview';
|
||||||
|
const pr = 777;
|
||||||
|
const url = `${baseUrl}/${pr}`;
|
||||||
|
let bvGetPrIsTrustedSpy: jasmine.Spy;
|
||||||
|
let bvGetSignificantFilesChangedSpy: jasmine.Spy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
bvGetPrIsTrustedSpy = spyOn(buildVerifier, 'getPrIsTrusted').and.returnValue(Promise.resolve(true));
|
||||||
|
bvGetSignificantFilesChangedSpy = spyOn(buildVerifier, 'getSignificantFilesChanged').
|
||||||
|
and.returnValue(Promise.resolve(true));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 404 for non-GET requests', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
agent.put(url).expect(404),
|
||||||
|
agent.post(url).expect(404),
|
||||||
|
agent.patch(url).expect(404),
|
||||||
|
agent.delete(url).expect(404),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 404 if the path does not match exactly', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
agent.get('/can-have-public-preview/42/foo').expect(404),
|
||||||
|
agent.get('/can-have-public-preview-foo/42').expect(404),
|
||||||
|
agent.get('/can-have-public-previewnfoo/42').expect(404),
|
||||||
|
agent.get('/foo/can-have-public-preview/42').expect(404),
|
||||||
|
agent.get('/foo-can-have-public-preview/42').expect(404),
|
||||||
|
agent.get('/fooncan-have-public-preview/42').expect(404),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond appropriately if the PR did not touch any significant files', async () => {
|
||||||
|
bvGetSignificantFilesChangedSpy.and.returnValue(Promise.resolve(false));
|
||||||
|
|
||||||
|
const expectedResponse = {canHavePublicPreview: false, reason: 'No significant files touched.'};
|
||||||
|
const expectedLog = `PR:${pr} - Cannot have a public preview, because it did not touch any significant files.`;
|
||||||
|
|
||||||
|
await agent.get(url).expect(200, expectedResponse);
|
||||||
|
|
||||||
|
expect(bvGetSignificantFilesChangedSpy).toHaveBeenCalledWith(pr, jasmine.any(RegExp));
|
||||||
|
expect(bvGetPrIsTrustedSpy).not.toHaveBeenCalled();
|
||||||
|
expect(loggerLogSpy).toHaveBeenCalledWith(expectedLog);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond appropriately if the PR is not automatically verifiable as "trusted"', async () => {
|
||||||
|
bvGetPrIsTrustedSpy.and.returnValue(Promise.resolve(false));
|
||||||
|
|
||||||
|
const expectedResponse = {canHavePublicPreview: false, reason: 'Not automatically verifiable as "trusted".'};
|
||||||
|
const expectedLog =
|
||||||
|
`PR:${pr} - Cannot have a public preview, because not automatically verifiable as "trusted".`;
|
||||||
|
|
||||||
|
await agent.get(url).expect(200, expectedResponse);
|
||||||
|
|
||||||
|
expect(bvGetSignificantFilesChangedSpy).toHaveBeenCalledWith(pr, jasmine.any(RegExp));
|
||||||
|
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(pr);
|
||||||
|
expect(loggerLogSpy).toHaveBeenCalledWith(expectedLog);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond appropriately if the PR can have a preview', async () => {
|
||||||
|
const expectedResponse = {canHavePublicPreview: true, reason: null};
|
||||||
|
const expectedLog = `PR:${pr} - Can have a public preview.`;
|
||||||
|
|
||||||
|
await agent.get(url).expect(200, expectedResponse);
|
||||||
|
|
||||||
|
expect(bvGetSignificantFilesChangedSpy).toHaveBeenCalledWith(pr, jasmine.any(RegExp));
|
||||||
|
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(pr);
|
||||||
|
expect(loggerLogSpy).toHaveBeenCalledWith(expectedLog);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with error if `getSignificantFilesChanged()` fails', async () => {
|
||||||
|
bvGetSignificantFilesChangedSpy.and.callFake(() => Promise.reject('getSignificantFilesChanged error'));
|
||||||
|
|
||||||
|
await agent.get(url).expect(500, 'getSignificantFilesChanged error');
|
||||||
|
expect(loggerErrorSpy).toHaveBeenCalledWith('Previewability check error', 'getSignificantFilesChanged error');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with error if `getPrIsTrusted()` fails', async () => {
|
||||||
|
const error = new Error('getPrIsTrusted error');
|
||||||
|
bvGetPrIsTrustedSpy.and.callFake(() => { throw error; });
|
||||||
|
|
||||||
|
await agent.get(url).expect(500, 'getPrIsTrusted error');
|
||||||
|
expect(loggerErrorSpy).toHaveBeenCalledWith('Previewability check error', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('POST /circle-build', () => {
|
||||||
|
let getGithubInfoSpy: jasmine.Spy;
|
||||||
|
let getSignificantFilesChangedSpy: jasmine.Spy;
|
||||||
|
let downloadBuildArtifactSpy: jasmine.Spy;
|
||||||
|
let getPrIsTrustedSpy: jasmine.Spy;
|
||||||
|
let createBuildSpy: jasmine.Spy;
|
||||||
|
let IS_PUBLIC: boolean;
|
||||||
|
let BUILD_INFO: GithubInfo;
|
||||||
|
let AFFECTS_SIGNIFICANT_FILES: boolean;
|
||||||
|
let BASIC_PAYLOAD: CircleCiWebHookPayload;
|
||||||
|
const URL = '/circle-build';
|
||||||
|
const BUILD_NUM = 12345;
|
||||||
|
const PR = 777;
|
||||||
|
const SHA = 'COMMIT';
|
||||||
|
const DOWNLOADED_ARTIFACT_PATH = 'downloads/777-COMMIT-build.zip';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
IS_PUBLIC = true;
|
||||||
|
BUILD_INFO = {
|
||||||
|
org: defaultConfig.githubOrg,
|
||||||
|
pr: PR,
|
||||||
|
repo: defaultConfig.githubRepo,
|
||||||
|
sha: SHA,
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
BASIC_PAYLOAD = { payload: { build_num: BUILD_NUM, build_parameters: { CIRCLE_JOB: 'aio_preview' } } };
|
||||||
|
AFFECTS_SIGNIFICANT_FILES = true;
|
||||||
|
getGithubInfoSpy = spyOn(buildRetriever, 'getGithubInfo')
|
||||||
|
.and.callFake(() => Promise.resolve(BUILD_INFO));
|
||||||
|
getSignificantFilesChangedSpy = spyOn(buildVerifier, 'getSignificantFilesChanged')
|
||||||
|
.and.callFake(() => Promise.resolve(AFFECTS_SIGNIFICANT_FILES));
|
||||||
|
downloadBuildArtifactSpy = spyOn(buildRetriever, 'downloadBuildArtifact')
|
||||||
|
.and.callFake(() => Promise.resolve(DOWNLOADED_ARTIFACT_PATH));
|
||||||
|
getPrIsTrustedSpy = spyOn(buildVerifier, 'getPrIsTrusted')
|
||||||
|
.and.callFake(() => Promise.resolve(IS_PUBLIC));
|
||||||
|
createBuildSpy = spyOn(buildCreator, 'create');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respond with 400 if the request body is not in the correct format', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
agent.post(URL).expect(400),
|
||||||
|
agent.post(URL).send().expect(400),
|
||||||
|
agent.post(URL).send({}).expect(400),
|
||||||
|
agent.post(URL).send({ payload: {} }).expect(400),
|
||||||
|
agent.post(URL).send({ payload: { build_num: -1 } }).expect(400),
|
||||||
|
agent.post(URL).send({ payload: { build_num: 4000 } }).expect(400),
|
||||||
|
agent.post(URL).send({ payload: { build_num: 4000, build_parameters: { } } }).expect(400),
|
||||||
|
agent.post(URL).send({ payload: { build_num: 4000, build_parameters: { CIRCLE_JOB: '' } } }).expect(400),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a preview if everything is good and the build succeeded', async () => {
|
||||||
|
await agent.post(URL).send(BASIC_PAYLOAD).expect(201);
|
||||||
|
expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM);
|
||||||
|
expect(getSignificantFilesChangedSpy).toHaveBeenCalledWith(PR, jasmine.any(RegExp));
|
||||||
|
expect(downloadBuildArtifactSpy).toHaveBeenCalledWith(BUILD_NUM, PR, SHA, defaultConfig.buildArtifactPath);
|
||||||
|
expect(getPrIsTrustedSpy).toHaveBeenCalledWith(PR);
|
||||||
|
expect(createBuildSpy).toHaveBeenCalledWith(PR, SHA, DOWNLOADED_ARTIFACT_PATH, IS_PUBLIC);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respond with 204 if the reported build is not the "AIO preview" job', async () => {
|
||||||
|
BASIC_PAYLOAD.payload.build_parameters.CIRCLE_JOB = 'lint';
|
||||||
|
await agent.post(URL).send(BASIC_PAYLOAD).expect(204);
|
||||||
|
expect(getGithubInfoSpy).not.toHaveBeenCalled();
|
||||||
|
expect(getSignificantFilesChangedSpy).not.toHaveBeenCalled();
|
||||||
|
expect(loggerLogSpy).toHaveBeenCalledWith(
|
||||||
|
'Build:12345, Job:lint -', 'Skipping preview processing because this is not the "aio_preview" job.');
|
||||||
|
expect(downloadBuildArtifactSpy).not.toHaveBeenCalled();
|
||||||
|
expect(getPrIsTrustedSpy).not.toHaveBeenCalled();
|
||||||
|
expect(createBuildSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respond with 204 if the build did not affect any significant files', async () => {
|
||||||
|
AFFECTS_SIGNIFICANT_FILES = false;
|
||||||
|
await agent.post(URL).send(BASIC_PAYLOAD).expect(204);
|
||||||
|
expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM);
|
||||||
|
expect(getSignificantFilesChangedSpy).toHaveBeenCalledWith(PR, jasmine.any(RegExp));
|
||||||
|
expect(loggerLogSpy).toHaveBeenCalledWith(
|
||||||
|
'PR:777, Build:12345 - Skipping preview processing because this PR did not touch any significant files.');
|
||||||
|
expect(downloadBuildArtifactSpy).not.toHaveBeenCalled();
|
||||||
|
expect(getPrIsTrustedSpy).not.toHaveBeenCalled();
|
||||||
|
expect(createBuildSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respond with 201 if the build is trusted', async () => {
|
||||||
|
IS_PUBLIC = true;
|
||||||
|
await agent.post(URL).send(BASIC_PAYLOAD).expect(201);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respond with 202 if the build is not trusted', async () => {
|
||||||
|
IS_PUBLIC = false;
|
||||||
|
await agent.post(URL).send(BASIC_PAYLOAD).expect(202);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not create a preview if the build was not successful', async () => {
|
||||||
|
BUILD_INFO.success = false;
|
||||||
|
await agent.post(URL).send(BASIC_PAYLOAD).expect(204);
|
||||||
|
expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM);
|
||||||
|
expect(downloadBuildArtifactSpy).not.toHaveBeenCalled();
|
||||||
|
expect(getPrIsTrustedSpy).not.toHaveBeenCalled();
|
||||||
|
expect(createBuildSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail if the CircleCI request fails', async () => {
|
||||||
|
// Note it is important to put the `reject` into `and.callFake`;
|
||||||
|
// If you just `and.returnValue` the rejected promise
|
||||||
|
// then you get an "unhandled rejection" message in the console.
|
||||||
|
getGithubInfoSpy.and.callFake(() => Promise.reject('Test Error'));
|
||||||
|
await agent.post(URL).send(BASIC_PAYLOAD).expect(500, 'Test Error');
|
||||||
|
expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM);
|
||||||
|
expect(downloadBuildArtifactSpy).not.toHaveBeenCalled();
|
||||||
|
expect(getPrIsTrustedSpy).not.toHaveBeenCalled();
|
||||||
|
expect(createBuildSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail if the Github organisation of the build does not match the configured organisation', async () => {
|
||||||
|
BUILD_INFO.org = 'bad';
|
||||||
|
await agent.post(URL).send(BASIC_PAYLOAD)
|
||||||
|
.expect(500, `Invalid webhook: expected "githubOrg" property to equal "organisation" but got "bad".`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail if the Github repo of the build does not match the configured repo', async () => {
|
||||||
|
BUILD_INFO.repo = 'bad';
|
||||||
|
await agent.post(URL).send(BASIC_PAYLOAD)
|
||||||
|
.expect(500, `Invalid webhook: expected "githubRepo" property to equal "repo" but got "bad".`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail if the artifact fetch request fails', async () => {
|
||||||
|
downloadBuildArtifactSpy.and.callFake(() => Promise.reject('Test Error'));
|
||||||
|
await agent.post(URL).send(BASIC_PAYLOAD).expect(500, 'Test Error');
|
||||||
|
expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM);
|
||||||
|
expect(downloadBuildArtifactSpy).toHaveBeenCalled();
|
||||||
|
expect(getPrIsTrustedSpy).not.toHaveBeenCalled();
|
||||||
|
expect(createBuildSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail if verifying the PR fails', async () => {
|
||||||
|
getPrIsTrustedSpy.and.callFake(() => Promise.reject('Test Error'));
|
||||||
|
await agent.post(URL).send(BASIC_PAYLOAD).expect(500, 'Test Error');
|
||||||
|
expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM);
|
||||||
|
expect(downloadBuildArtifactSpy).toHaveBeenCalled();
|
||||||
|
expect(getPrIsTrustedSpy).toHaveBeenCalled();
|
||||||
|
expect(createBuildSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail if creating the preview build fails', async () => {
|
||||||
|
createBuildSpy.and.callFake(() => Promise.reject('Test Error'));
|
||||||
|
await agent.post(URL).send(BASIC_PAYLOAD).expect(500, 'Test Error');
|
||||||
|
expect(getGithubInfoSpy).toHaveBeenCalledWith(BUILD_NUM);
|
||||||
|
expect(downloadBuildArtifactSpy).toHaveBeenCalled();
|
||||||
|
expect(getPrIsTrustedSpy).toHaveBeenCalled();
|
||||||
|
expect(createBuildSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('POST /pr-updated', () => {
|
||||||
|
const pr = '9';
|
||||||
|
const url = '/pr-updated';
|
||||||
|
let bvGetPrIsTrustedSpy: jasmine.Spy;
|
||||||
|
let bcUpdatePrVisibilitySpy: jasmine.Spy;
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
const createRequest = (num: number, action?: string) =>
|
||||||
|
agent.post(url).send({number: num, action});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
bvGetPrIsTrustedSpy = spyOn(buildVerifier, 'getPrIsTrusted');
|
||||||
|
bcUpdatePrVisibilitySpy = spyOn(buildCreator, 'updatePrVisibility');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 404 for non-POST requests', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
agent.get(url).expect(404),
|
||||||
|
agent.put(url).expect(404),
|
||||||
|
agent.patch(url).expect(404),
|
||||||
|
agent.delete(url).expect(404),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 400 for requests without a payload', async () => {
|
||||||
|
const responseBody = `Missing or empty 'number' field in request: POST ${url} {}`;
|
||||||
|
|
||||||
|
const request1 = agent.post(url);
|
||||||
|
const request2 = agent.post(url).send();
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
request1.expect(400, responseBody),
|
||||||
|
request2.expect(400, responseBody),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 400 for requests without a \'number\' field', async () => {
|
||||||
|
const responseBodyPrefix = `Missing or empty 'number' field in request: POST ${url}`;
|
||||||
|
|
||||||
|
const request1 = agent.post(url).send({});
|
||||||
|
const request2 = agent.post(url).send({number: null});
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
request1.expect(400, `${responseBodyPrefix} {}`),
|
||||||
|
request2.expect(400, `${responseBodyPrefix} {"number":null}`),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should call \'BuildVerifier#gtPrIsTrusted()\' with the correct arguments', async () => {
|
||||||
|
await createRequest(+pr);
|
||||||
|
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should propagate errors from BuildVerifier', async () => {
|
||||||
|
bvGetPrIsTrustedSpy.and.callFake(() => Promise.reject('Test'));
|
||||||
|
|
||||||
|
await createRequest(+pr).expect(500, 'Test');
|
||||||
|
|
||||||
|
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9);
|
||||||
|
expect(bcUpdatePrVisibilitySpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should call \'BuildCreator#updatePrVisibility()\' with the correct arguments', async () => {
|
||||||
|
bvGetPrIsTrustedSpy.and.callFake((pr2: number) => Promise.resolve(pr2 === 42));
|
||||||
|
|
||||||
|
await createRequest(24);
|
||||||
|
expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(24, false);
|
||||||
|
|
||||||
|
await createRequest(42);
|
||||||
|
expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(42, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should propagate errors from BuildCreator', async () => {
|
||||||
|
bcUpdatePrVisibilitySpy.and.callFake(() => Promise.reject('Test'));
|
||||||
|
await createRequest(+pr).expect(500, 'Test');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('on success', () => {
|
||||||
|
|
||||||
|
it('should respond with 200 (action: undefined)', async () => {
|
||||||
|
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||||
|
|
||||||
|
const reqs = [4, 2].map(num => createRequest(num).expect(200, http.STATUS_CODES[200]));
|
||||||
|
await Promise.all(reqs);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 200 (action: labeled)', async () => {
|
||||||
|
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||||
|
|
||||||
|
const reqs = [4, 2].map(num => createRequest(num, 'labeled').expect(200, http.STATUS_CODES[200]));
|
||||||
|
await Promise.all(reqs);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 200 (action: unlabeled)', async () => {
|
||||||
|
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||||
|
|
||||||
|
const reqs = [4, 2].map(num => createRequest(num, 'unlabeled').expect(200, http.STATUS_CODES[200]));
|
||||||
|
await Promise.all(reqs);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should respond with 200 (and do nothing) if \'action\' implies no visibility change', async () => {
|
||||||
|
const promises = ['foo', 'notlabeled'].
|
||||||
|
map(action => createRequest(+pr, action).expect(200, http.STATUS_CODES[200]));
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
expect(bvGetPrIsTrustedSpy).not.toHaveBeenCalled();
|
||||||
|
expect(bcUpdatePrVisibilitySpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('ALL *', () => {
|
||||||
|
|
||||||
|
it('should respond with 404', async () => {
|
||||||
|
const responseFor = (method: string) => `Unknown resource in request: ${method.toUpperCase()} /some/url`;
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
agent.get('/some/url').expect(404, responseFor('get')),
|
||||||
|
agent.put('/some/url').expect(404, responseFor('put')),
|
||||||
|
agent.post('/some/url').expect(404, responseFor('post')),
|
||||||
|
agent.patch('/some/url').expect(404, responseFor('patch')),
|
||||||
|
agent.delete('/some/url').expect(404, responseFor('delete')),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -0,0 +1,49 @@
|
|||||||
|
import * as express from 'express';
|
||||||
|
import {PreviewServerError} from '../../lib/preview-server/preview-error';
|
||||||
|
import {respondWithError, throwRequestError} from '../../lib/preview-server/utils';
|
||||||
|
|
||||||
|
describe('preview-server/utils', () => {
|
||||||
|
describe('respondWithError', () => {
|
||||||
|
let endSpy: jasmine.Spy;
|
||||||
|
let statusSpy: jasmine.Spy;
|
||||||
|
let response: express.Response;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
endSpy = jasmine.createSpy('end');
|
||||||
|
statusSpy = jasmine.createSpy('status').and.callFake(() => response);
|
||||||
|
response = {status: statusSpy, end: endSpy} as any;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the status on the response', () => {
|
||||||
|
respondWithError(response, new PreviewServerError(505, 'TEST MESSAGE'));
|
||||||
|
expect(statusSpy).toHaveBeenCalledWith(505);
|
||||||
|
expect(endSpy).toHaveBeenCalledWith('TEST MESSAGE', jasmine.any(Function));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert non-PreviewServerError errors to 500 PreviewServerErrors', () => {
|
||||||
|
respondWithError(response, new Error('OTHER MESSAGE'));
|
||||||
|
expect(statusSpy).toHaveBeenCalledWith(500);
|
||||||
|
expect(endSpy).toHaveBeenCalledWith('OTHER MESSAGE', jasmine.any(Function));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('throwRequestError', () => {
|
||||||
|
it('should throw a suitable error', () => {
|
||||||
|
let caught = false;
|
||||||
|
try {
|
||||||
|
const request = {
|
||||||
|
body: 'The request body',
|
||||||
|
method: 'POST',
|
||||||
|
originalUrl: 'some.domain.com/path',
|
||||||
|
} as express.Request;
|
||||||
|
throwRequestError(505, 'ERROR MESSAGE', request);
|
||||||
|
} catch (error) {
|
||||||
|
caught = true;
|
||||||
|
expect(error).toEqual(jasmine.any(PreviewServerError));
|
||||||
|
expect(error.status).toEqual(505);
|
||||||
|
expect(error.message).toEqual(`ERROR MESSAGE in request: POST some.domain.com/path "The request body"`);
|
||||||
|
}
|
||||||
|
expect(caught).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,303 +0,0 @@
|
|||||||
// Imports
|
|
||||||
import * as jwt from 'jsonwebtoken';
|
|
||||||
import {GithubPullRequests, PullRequest} from '../../lib/common/github-pull-requests';
|
|
||||||
import {GithubTeams} from '../../lib/common/github-teams';
|
|
||||||
import {BUILD_VERIFICATION_STATUS, BuildVerifier} from '../../lib/upload-server/build-verifier';
|
|
||||||
import {expectToBeUploadError} from './helpers';
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
describe('BuildVerifier', () => {
|
|
||||||
const defaultConfig = {
|
|
||||||
allowedTeamSlugs: ['team1', 'team2'],
|
|
||||||
githubToken: 'githubToken',
|
|
||||||
organization: 'organization',
|
|
||||||
repoSlug: 'repo/slug',
|
|
||||||
secret: 'secret',
|
|
||||||
trustedPrLabel: 'trusted: pr-label',
|
|
||||||
};
|
|
||||||
let bv: BuildVerifier;
|
|
||||||
|
|
||||||
// Helpers
|
|
||||||
const createBuildVerifier = (partialConfig: Partial<typeof defaultConfig> = {}) => {
|
|
||||||
const cfg = {...defaultConfig, ...partialConfig} as typeof defaultConfig;
|
|
||||||
return new BuildVerifier(cfg.secret, cfg.githubToken, cfg.repoSlug, cfg.organization,
|
|
||||||
cfg.allowedTeamSlugs, cfg.trustedPrLabel);
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => bv = createBuildVerifier());
|
|
||||||
|
|
||||||
|
|
||||||
describe('constructor()', () => {
|
|
||||||
|
|
||||||
['secret', 'githubToken', 'repoSlug', 'organization', 'allowedTeamSlugs', 'trustedPrLabel'].
|
|
||||||
forEach(param => {
|
|
||||||
it(`should throw if '${param}' is missing or empty`, () => {
|
|
||||||
expect(() => createBuildVerifier({[param]: ''})).
|
|
||||||
toThrowError(`Missing or empty required parameter '${param}'!`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should throw if \'allowedTeamSlugs\' is an empty array', () => {
|
|
||||||
expect(() => createBuildVerifier({allowedTeamSlugs: []})).
|
|
||||||
toThrowError('Missing or empty required parameter \'allowedTeamSlugs\'!');
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe('getPrIsTrusted()', () => {
|
|
||||||
const pr = 9;
|
|
||||||
let mockPrInfo: PullRequest;
|
|
||||||
let prsFetchSpy: jasmine.Spy;
|
|
||||||
let teamsIsMemberBySlugSpy: jasmine.Spy;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockPrInfo = {
|
|
||||||
labels: [
|
|
||||||
{name: 'foo'},
|
|
||||||
{name: 'bar'},
|
|
||||||
],
|
|
||||||
number: 9,
|
|
||||||
user: {login: 'username'},
|
|
||||||
};
|
|
||||||
|
|
||||||
prsFetchSpy = spyOn(GithubPullRequests.prototype, 'fetch').
|
|
||||||
and.returnValue(Promise.resolve(mockPrInfo));
|
|
||||||
|
|
||||||
teamsIsMemberBySlugSpy = spyOn(GithubTeams.prototype, 'isMemberBySlug').
|
|
||||||
and.returnValue(Promise.resolve(true));
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should return a promise', done => {
|
|
||||||
const promise = bv.getPrIsTrusted(pr);
|
|
||||||
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
|
||||||
// to avoid running the actual `GithubTeams#isMemberBySlug()`.
|
|
||||||
|
|
||||||
expect(promise).toEqual(jasmine.any(Promise));
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should fetch the corresponding PR', done => {
|
|
||||||
bv.getPrIsTrusted(pr).then(() => {
|
|
||||||
expect(prsFetchSpy).toHaveBeenCalledWith(pr);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should fail if fetching the PR errors', done => {
|
|
||||||
prsFetchSpy.and.callFake(() => Promise.reject('Test'));
|
|
||||||
bv.getPrIsTrusted(pr).catch(err => {
|
|
||||||
expect(err).toBe('Test');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe('when the PR has the "trusted PR" label', () => {
|
|
||||||
|
|
||||||
beforeEach(() => mockPrInfo.labels.push({name: 'trusted: pr-label'}));
|
|
||||||
|
|
||||||
|
|
||||||
it('should resolve to true', done => {
|
|
||||||
bv.getPrIsTrusted(pr).then(isTrusted => {
|
|
||||||
expect(isTrusted).toBe(true);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should not try to verify the author\'s membership status', done => {
|
|
||||||
bv.getPrIsTrusted(pr).then(() => {
|
|
||||||
expect(teamsIsMemberBySlugSpy).not.toHaveBeenCalled();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe('when the PR does not have the "trusted PR" label', () => {
|
|
||||||
|
|
||||||
it('should verify the PR author\'s membership in the specified teams', done => {
|
|
||||||
bv.getPrIsTrusted(pr).then(() => {
|
|
||||||
expect(teamsIsMemberBySlugSpy).toHaveBeenCalledWith('username', ['team1', 'team2']);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should fail if verifying membership errors', done => {
|
|
||||||
teamsIsMemberBySlugSpy.and.callFake(() => Promise.reject('Test'));
|
|
||||||
bv.getPrIsTrusted(pr).catch(err => {
|
|
||||||
expect(err).toBe('Test');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should resolve to true if the PR\'s author is a member', done => {
|
|
||||||
teamsIsMemberBySlugSpy.and.returnValue(Promise.resolve(true));
|
|
||||||
|
|
||||||
bv.getPrIsTrusted(pr).then(isTrusted => {
|
|
||||||
expect(isTrusted).toBe(true);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should resolve to false if the PR\'s author is not a member', done => {
|
|
||||||
teamsIsMemberBySlugSpy.and.returnValue(Promise.resolve(false));
|
|
||||||
|
|
||||||
bv.getPrIsTrusted(pr).then(isTrusted => {
|
|
||||||
expect(isTrusted).toBe(false);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe('verify()', () => {
|
|
||||||
const pr = 9;
|
|
||||||
const defaultJwt = {
|
|
||||||
'exp': Math.floor(Date.now() / 1000) + 30,
|
|
||||||
'iat': Math.floor(Date.now() / 1000) - 30,
|
|
||||||
'iss': 'Travis CI, GmbH',
|
|
||||||
'pull-request': pr,
|
|
||||||
'slug': defaultConfig.repoSlug,
|
|
||||||
};
|
|
||||||
let bvGetPrIsTrusted: jasmine.Spy;
|
|
||||||
|
|
||||||
// Heleprs
|
|
||||||
const createAuthHeader = (partialJwt: Partial<typeof defaultJwt> = {}, secret: string = defaultConfig.secret) =>
|
|
||||||
`Token ${jwt.sign({...defaultJwt, ...partialJwt}, secret)}`;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
bvGetPrIsTrusted = spyOn(bv, 'getPrIsTrusted').and.returnValue(Promise.resolve(true));
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should return a promise', done => {
|
|
||||||
const promise = bv.verify(pr, createAuthHeader());
|
|
||||||
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
|
||||||
// to avoid running the actual `bvGetPrIsTrusted()`.
|
|
||||||
|
|
||||||
expect(promise).toEqual(jasmine.any(Promise));
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should fail if the authorization header is invalid', done => {
|
|
||||||
bv.verify(pr, 'foo').catch(err => {
|
|
||||||
const errorMessage = 'Error while verifying upload for PR 9: jwt malformed';
|
|
||||||
|
|
||||||
expectToBeUploadError(err, 403, errorMessage);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should fail if the secret is invalid', done => {
|
|
||||||
bv.verify(pr, createAuthHeader({}, 'foo')).catch(err => {
|
|
||||||
const errorMessage = 'Error while verifying upload for PR 9: invalid signature';
|
|
||||||
|
|
||||||
expectToBeUploadError(err, 403, errorMessage);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should fail if the issuer is invalid', done => {
|
|
||||||
bv.verify(pr, createAuthHeader({iss: 'not valid'})).catch(err => {
|
|
||||||
const errorMessage = 'Error while verifying upload for PR 9: ' +
|
|
||||||
`jwt issuer invalid. expected: ${defaultJwt.iss}`;
|
|
||||||
|
|
||||||
expectToBeUploadError(err, 403, errorMessage);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should fail if the token has expired', done => {
|
|
||||||
bv.verify(pr, createAuthHeader({exp: 0})).catch(err => {
|
|
||||||
const errorMessage = 'Error while verifying upload for PR 9: jwt expired';
|
|
||||||
|
|
||||||
expectToBeUploadError(err, 403, errorMessage);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should fail if the repo slug does not match', done => {
|
|
||||||
bv.verify(pr, createAuthHeader({slug: 'foo/bar'})).catch(err => {
|
|
||||||
const errorMessage = 'Error while verifying upload for PR 9: ' +
|
|
||||||
`jwt slug invalid. expected: ${defaultConfig.repoSlug}`;
|
|
||||||
|
|
||||||
expectToBeUploadError(err, 403, errorMessage);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should fail if the PR does not match', done => {
|
|
||||||
bv.verify(pr, createAuthHeader({'pull-request': 1337})).catch(err => {
|
|
||||||
const errorMessage = 'Error while verifying upload for PR 9: ' +
|
|
||||||
`jwt pull-request invalid. expected: ${pr}`;
|
|
||||||
|
|
||||||
expectToBeUploadError(err, 403, errorMessage);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should not fail if the token is valid', done => {
|
|
||||||
bv.verify(pr, createAuthHeader()).then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should not fail even if the token has been issued in the future', done => {
|
|
||||||
const in30s = Math.floor(Date.now() / 1000) + 30;
|
|
||||||
bv.verify(pr, createAuthHeader({iat: in30s})).then(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should call \'getPrIsTrusted()\' if the token is valid', done => {
|
|
||||||
bv.verify(pr, createAuthHeader()).then(() => {
|
|
||||||
expect(bvGetPrIsTrusted).toHaveBeenCalledWith(pr);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should fail if \'getPrIsTrusted()\' rejects', done => {
|
|
||||||
bvGetPrIsTrusted.and.callFake(() => Promise.reject('Test'));
|
|
||||||
bv.verify(pr, createAuthHeader()).catch(err => {
|
|
||||||
expectToBeUploadError(err, 403, `Error while verifying upload for PR ${pr}: Test`);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should resolve to `verifiedNotTrusted` if \'getPrIsTrusted()\' returns false', done => {
|
|
||||||
bvGetPrIsTrusted.and.returnValue(Promise.resolve(false));
|
|
||||||
bv.verify(pr, createAuthHeader()).then(value => {
|
|
||||||
expect(value).toBe(BUILD_VERIFICATION_STATUS.verifiedNotTrusted);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should resolve to `verifiedAndTrusted` if \'getPrIsTrusted()\' returns true', done => {
|
|
||||||
bv.verify(pr, createAuthHeader()).then(value => {
|
|
||||||
expect(value).toBe(BUILD_VERIFICATION_STATUS.verifiedAndTrusted);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
@ -1,11 +0,0 @@
|
|||||||
import {UploadError} from '../../lib/upload-server/upload-error';
|
|
||||||
|
|
||||||
export const expectToBeUploadError = (actual: UploadError, status?: number, message?: string) => {
|
|
||||||
expect(actual).toEqual(jasmine.any(UploadError));
|
|
||||||
if (status != null) {
|
|
||||||
expect(actual.status).toBe(status);
|
|
||||||
}
|
|
||||||
if (message != null) {
|
|
||||||
expect(actual.message).toBe(message);
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,39 +0,0 @@
|
|||||||
// Imports
|
|
||||||
import {UploadError} from '../../lib/upload-server/upload-error';
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
describe('UploadError', () => {
|
|
||||||
let err: UploadError;
|
|
||||||
|
|
||||||
beforeEach(() => err = new UploadError(999, 'message'));
|
|
||||||
|
|
||||||
|
|
||||||
it('should extend Error', () => {
|
|
||||||
expect(err).toEqual(jasmine.any(UploadError));
|
|
||||||
expect(err).toEqual(jasmine.any(Error));
|
|
||||||
|
|
||||||
expect(Object.getPrototypeOf(err)).toBe(UploadError.prototype);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should have a \'status\' property', () => {
|
|
||||||
expect(err.status).toBe(999);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should have a \'message\' property', () => {
|
|
||||||
expect(err.message).toBe('message');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should have a 500 \'status\' by default', () => {
|
|
||||||
expect(new UploadError().status).toBe(500);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should have an empty \'message\' by default', () => {
|
|
||||||
expect(new UploadError().message).toBe('');
|
|
||||||
expect(new UploadError(999).message).toBe('');
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
@ -1,603 +0,0 @@
|
|||||||
// Imports
|
|
||||||
import * as express from 'express';
|
|
||||||
import * as http from 'http';
|
|
||||||
import * as supertest from 'supertest';
|
|
||||||
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
|
||||||
import {BuildCreator} from '../../lib/upload-server/build-creator';
|
|
||||||
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/upload-server/build-events';
|
|
||||||
import {BUILD_VERIFICATION_STATUS, BuildVerifier} from '../../lib/upload-server/build-verifier';
|
|
||||||
import {uploadServerFactory as usf} from '../../lib/upload-server/upload-server-factory';
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
describe('uploadServerFactory', () => {
|
|
||||||
const defaultConfig = {
|
|
||||||
buildsDir: 'builds/dir',
|
|
||||||
domainName: 'domain.name',
|
|
||||||
githubOrganization: 'organization',
|
|
||||||
githubTeamSlugs: ['team1', 'team2'],
|
|
||||||
githubToken: '12345',
|
|
||||||
repoSlug: 'repo/slug',
|
|
||||||
secret: 'secret',
|
|
||||||
trustedPrLabel: 'trusted: pr-label',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helpers
|
|
||||||
const createUploadServer = (partialConfig: Partial<typeof defaultConfig> = {}) =>
|
|
||||||
usf.create({...defaultConfig, ...partialConfig} as typeof defaultConfig);
|
|
||||||
|
|
||||||
|
|
||||||
describe('create()', () => {
|
|
||||||
let usfCreateMiddlewareSpy: jasmine.Spy;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
usfCreateMiddlewareSpy = spyOn(usf as any, 'createMiddleware').and.callThrough();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should throw if \'buildsDir\' is missing or empty', () => {
|
|
||||||
expect(() => createUploadServer({buildsDir: ''})).
|
|
||||||
toThrowError('Missing or empty required parameter \'buildsDir\'!');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should throw if \'domainName\' is missing or empty', () => {
|
|
||||||
expect(() => createUploadServer({domainName: ''})).
|
|
||||||
toThrowError('Missing or empty required parameter \'domainName\'!');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should throw if \'githubToken\' is missing or empty', () => {
|
|
||||||
expect(() => createUploadServer({githubToken: ''})).
|
|
||||||
toThrowError('Missing or empty required parameter \'githubToken\'!');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should throw if \'githubOrganization\' is missing or empty', () => {
|
|
||||||
expect(() => createUploadServer({githubOrganization: ''})).
|
|
||||||
toThrowError('Missing or empty required parameter \'organization\'!');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should throw if \'githubTeamSlugs\' is missing or empty', () => {
|
|
||||||
expect(() => createUploadServer({githubTeamSlugs: []})).
|
|
||||||
toThrowError('Missing or empty required parameter \'allowedTeamSlugs\'!');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should throw if \'repoSlug\' is missing or empty', () => {
|
|
||||||
expect(() => createUploadServer({repoSlug: ''})).
|
|
||||||
toThrowError('Missing or empty required parameter \'repoSlug\'!');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should throw if \'secret\' is missing or empty', () => {
|
|
||||||
expect(() => createUploadServer({secret: ''})).
|
|
||||||
toThrowError('Missing or empty required parameter \'secret\'!');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should throw if \'trustedPrLabel\' is missing or empty', () => {
|
|
||||||
expect(() => createUploadServer({trustedPrLabel: ''})).
|
|
||||||
toThrowError('Missing or empty required parameter \'trustedPrLabel\'!');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should return an http.Server', () => {
|
|
||||||
const httpCreateServerSpy = spyOn(http, 'createServer').and.callThrough();
|
|
||||||
const server = createUploadServer();
|
|
||||||
|
|
||||||
expect(server).toBe(httpCreateServerSpy.calls.mostRecent().returnValue);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should create and use an appropriate BuildCreator', () => {
|
|
||||||
const usfCreateBuildCreatorSpy = spyOn(usf as any, 'createBuildCreator').and.callThrough();
|
|
||||||
|
|
||||||
createUploadServer();
|
|
||||||
const buildCreator: BuildCreator = usfCreateBuildCreatorSpy.calls.mostRecent().returnValue;
|
|
||||||
|
|
||||||
expect(usfCreateMiddlewareSpy).toHaveBeenCalledWith(jasmine.any(BuildVerifier), buildCreator);
|
|
||||||
expect(usfCreateBuildCreatorSpy).toHaveBeenCalledWith('builds/dir', '12345', 'repo/slug', 'domain.name');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should create and use an appropriate middleware', () => {
|
|
||||||
const httpCreateServerSpy = spyOn(http, 'createServer').and.callThrough();
|
|
||||||
|
|
||||||
createUploadServer();
|
|
||||||
const middleware: express.Express = usfCreateMiddlewareSpy.calls.mostRecent().returnValue;
|
|
||||||
const buildVerifier = jasmine.any(BuildVerifier);
|
|
||||||
const buildCreator = jasmine.any(BuildCreator);
|
|
||||||
|
|
||||||
expect(httpCreateServerSpy).toHaveBeenCalledWith(middleware);
|
|
||||||
expect(usfCreateMiddlewareSpy).toHaveBeenCalledWith(buildVerifier, buildCreator);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should log the server address info on \'listening\'', () => {
|
|
||||||
const consoleInfoSpy = spyOn(console, 'info');
|
|
||||||
const server = createUploadServer();
|
|
||||||
server.address = () => ({address: 'foo', family: '', port: 1337});
|
|
||||||
|
|
||||||
expect(consoleInfoSpy).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
server.emit('listening');
|
|
||||||
expect(consoleInfoSpy).toHaveBeenCalledWith('Up and running (and listening on foo:1337)...');
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Protected methods
|
|
||||||
|
|
||||||
describe('createBuildCreator()', () => {
|
|
||||||
let buildCreator: BuildCreator;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
buildCreator = (usf as any).createBuildCreator(
|
|
||||||
defaultConfig.buildsDir,
|
|
||||||
defaultConfig.githubToken,
|
|
||||||
defaultConfig.repoSlug,
|
|
||||||
defaultConfig.domainName,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should pass the \'buildsDir\' to the BuildCreator', () => {
|
|
||||||
expect((buildCreator as any).buildsDir).toBe('builds/dir');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe('on \'build.created\'', () => {
|
|
||||||
let prsAddCommentSpy: jasmine.Spy;
|
|
||||||
|
|
||||||
beforeEach(() => prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment'));
|
|
||||||
|
|
||||||
|
|
||||||
it('should post a comment on GitHub for public previews', () => {
|
|
||||||
const commentBody = 'You can preview 1234567890 at https://pr42-1234567890.domain.name/.';
|
|
||||||
|
|
||||||
buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890', isPublic: true});
|
|
||||||
expect(prsAddCommentSpy).toHaveBeenCalledWith(42, commentBody);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should not post a comment on GitHub for non-public previews', () => {
|
|
||||||
buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890', isPublic: false});
|
|
||||||
expect(prsAddCommentSpy).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe('on \'pr.changedVisibility\'', () => {
|
|
||||||
let prsAddCommentSpy: jasmine.Spy;
|
|
||||||
|
|
||||||
beforeEach(() => prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment'));
|
|
||||||
|
|
||||||
|
|
||||||
it('should post a comment on GitHub (for all SHAs) for PRs made public', () => {
|
|
||||||
const commentBody = 'You can preview 12345 at https://pr42-12345.domain.name/.\n' +
|
|
||||||
'You can preview 67890 at https://pr42-67890.domain.name/.';
|
|
||||||
|
|
||||||
buildCreator.emit(ChangedPrVisibilityEvent.type, {pr: 42, shas: ['12345', '67890'], isPublic: true});
|
|
||||||
expect(prsAddCommentSpy).toHaveBeenCalledWith(42, commentBody);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should not post a comment on GitHub if no SHAs were affected', () => {
|
|
||||||
buildCreator.emit(ChangedPrVisibilityEvent.type, {pr: 42, shas: [], isPublic: true});
|
|
||||||
expect(prsAddCommentSpy).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should not post a comment on GitHub for PRs made non-public', () => {
|
|
||||||
buildCreator.emit(ChangedPrVisibilityEvent.type, {pr: 42, shas: ['12345', '67890'], isPublic: false});
|
|
||||||
expect(prsAddCommentSpy).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should pass the correct \'githubToken\' and \'repoSlug\' to GithubPullRequests', () => {
|
|
||||||
const prsAddCommentSpy = spyOn(GithubPullRequests.prototype, 'addComment');
|
|
||||||
|
|
||||||
buildCreator.emit(CreatedBuildEvent.type, {pr: 42, sha: '1234567890', isPublic: true});
|
|
||||||
buildCreator.emit(ChangedPrVisibilityEvent.type, {pr: 42, shas: ['12345', '67890'], isPublic: true});
|
|
||||||
|
|
||||||
const allCalls = prsAddCommentSpy.calls.all();
|
|
||||||
const prs = allCalls[0].object;
|
|
||||||
|
|
||||||
expect(prsAddCommentSpy).toHaveBeenCalledTimes(2);
|
|
||||||
expect(prs).toBe(allCalls[1].object);
|
|
||||||
expect(prs).toEqual(jasmine.any(GithubPullRequests));
|
|
||||||
expect(prs.repoSlug).toBe('repo/slug');
|
|
||||||
expect(prs.requestHeaders.Authorization).toContain('12345');
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe('createMiddleware()', () => {
|
|
||||||
let buildVerifier: BuildVerifier;
|
|
||||||
let buildCreator: BuildCreator;
|
|
||||||
let agent: supertest.SuperTest<supertest.Test>;
|
|
||||||
|
|
||||||
// Helpers
|
|
||||||
const promisifyRequest = (req: supertest.Request) =>
|
|
||||||
new Promise((resolve, reject) => req.end(err => err ? reject(err) : resolve()));
|
|
||||||
const verifyRequests = (reqs: supertest.Request[], done: jasmine.DoneFn) =>
|
|
||||||
Promise.all(reqs.map(promisifyRequest)).then(done, done.fail);
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
buildVerifier = new BuildVerifier(
|
|
||||||
defaultConfig.secret,
|
|
||||||
defaultConfig.githubToken,
|
|
||||||
defaultConfig.repoSlug,
|
|
||||||
defaultConfig.githubOrganization,
|
|
||||||
defaultConfig.githubTeamSlugs,
|
|
||||||
defaultConfig.trustedPrLabel,
|
|
||||||
);
|
|
||||||
buildCreator = new BuildCreator(defaultConfig.buildsDir);
|
|
||||||
agent = supertest.agent((usf as any).createMiddleware(buildVerifier, buildCreator));
|
|
||||||
|
|
||||||
spyOn(console, 'error');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe('GET /create-build/<pr>/<sha>', () => {
|
|
||||||
const pr = '9';
|
|
||||||
const sha = '9'.repeat(40);
|
|
||||||
let buildVerifierVerifySpy: jasmine.Spy;
|
|
||||||
let buildCreatorCreateSpy: jasmine.Spy;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
const verStatus = BUILD_VERIFICATION_STATUS.verifiedAndTrusted;
|
|
||||||
buildVerifierVerifySpy = spyOn(buildVerifier, 'verify').and.returnValue(Promise.resolve(verStatus));
|
|
||||||
buildCreatorCreateSpy = spyOn(buildCreator, 'create').and.returnValue(Promise.resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should respond with 404 for non-GET requests', done => {
|
|
||||||
verifyRequests([
|
|
||||||
agent.put(`/create-build/${pr}/${sha}`).expect(404),
|
|
||||||
agent.post(`/create-build/${pr}/${sha}`).expect(404),
|
|
||||||
agent.patch(`/create-build/${pr}/${sha}`).expect(404),
|
|
||||||
agent.delete(`/create-build/${pr}/${sha}`).expect(404),
|
|
||||||
], done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should respond with 401 for requests without an \'AUTHORIZATION\' header', done => {
|
|
||||||
const url = `/create-build/${pr}/${sha}`;
|
|
||||||
const responseBody = `Missing or empty 'AUTHORIZATION' header in request: GET ${url}`;
|
|
||||||
|
|
||||||
verifyRequests([
|
|
||||||
agent.get(url).expect(401, responseBody),
|
|
||||||
agent.get(url).set('AUTHORIZATION', '').expect(401, responseBody),
|
|
||||||
], done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should respond with 400 for requests without an \'X-FILE\' header', done => {
|
|
||||||
const url = `/create-build/${pr}/${sha}`;
|
|
||||||
const responseBody = `Missing or empty 'X-FILE' header in request: GET ${url}`;
|
|
||||||
|
|
||||||
const request1 = agent.get(url).set('AUTHORIZATION', 'foo');
|
|
||||||
const request2 = agent.get(url).set('AUTHORIZATION', 'foo').set('X-FILE', '');
|
|
||||||
|
|
||||||
verifyRequests([
|
|
||||||
request1.expect(400, responseBody),
|
|
||||||
request2.expect(400, responseBody),
|
|
||||||
], done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should respond with 404 for unknown paths', done => {
|
|
||||||
verifyRequests([
|
|
||||||
agent.get(`/foo/create-build/${pr}/${sha}`).expect(404),
|
|
||||||
agent.get(`/foo-create-build/${pr}/${sha}`).expect(404),
|
|
||||||
agent.get(`/fooncreate-build/${pr}/${sha}`).expect(404),
|
|
||||||
agent.get(`/create-build/foo/${pr}/${sha}`).expect(404),
|
|
||||||
agent.get(`/create-build-foo/${pr}/${sha}`).expect(404),
|
|
||||||
agent.get(`/create-buildnfoo/${pr}/${sha}`).expect(404),
|
|
||||||
agent.get(`/create-build/pr${pr}/${sha}`).expect(404),
|
|
||||||
agent.get(`/create-build/${pr}/${sha}42`).expect(404),
|
|
||||||
], done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should call \'BuildVerifier#verify()\' with the correct arguments', done => {
|
|
||||||
const req = agent.
|
|
||||||
get(`/create-build/${pr}/${sha}`).
|
|
||||||
set('AUTHORIZATION', 'foo').
|
|
||||||
set('X-FILE', 'bar');
|
|
||||||
|
|
||||||
promisifyRequest(req).
|
|
||||||
then(() => expect(buildVerifierVerifySpy).toHaveBeenCalledWith(9, 'foo')).
|
|
||||||
then(done, done.fail);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should propagate errors from BuildVerifier', done => {
|
|
||||||
buildVerifierVerifySpy.and.callFake(() => Promise.reject('Test'));
|
|
||||||
|
|
||||||
const req = agent.
|
|
||||||
get(`/create-build/${pr}/${sha}`).
|
|
||||||
set('AUTHORIZATION', 'foo').
|
|
||||||
set('X-FILE', 'bar').
|
|
||||||
expect(500, 'Test');
|
|
||||||
|
|
||||||
promisifyRequest(req).
|
|
||||||
then(() => {
|
|
||||||
expect(buildVerifierVerifySpy).toHaveBeenCalledWith(9, 'foo');
|
|
||||||
expect(buildCreatorCreateSpy).not.toHaveBeenCalled();
|
|
||||||
}).
|
|
||||||
then(done, done.fail);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should call \'BuildCreator#create()\' with the correct arguments', done => {
|
|
||||||
buildVerifierVerifySpy.and.returnValues(
|
|
||||||
Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedAndTrusted),
|
|
||||||
Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedNotTrusted));
|
|
||||||
|
|
||||||
const req1 = agent.get(`/create-build/${pr}/${sha}`).set('AUTHORIZATION', 'foo').set('X-FILE', 'bar');
|
|
||||||
const req2 = agent.get(`/create-build/${pr}/${sha}`).set('AUTHORIZATION', 'foo').set('X-FILE', 'bar');
|
|
||||||
|
|
||||||
Promise.all([
|
|
||||||
promisifyRequest(req1).then(() => expect(buildCreatorCreateSpy).toHaveBeenCalledWith(pr, sha, 'bar', true)),
|
|
||||||
promisifyRequest(req2).then(() => expect(buildCreatorCreateSpy).toHaveBeenCalledWith(pr, sha, 'bar', false)),
|
|
||||||
]).then(done, done.fail);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should propagate errors from BuildCreator', done => {
|
|
||||||
buildCreatorCreateSpy.and.callFake(() => Promise.reject('Test'));
|
|
||||||
const req = agent.
|
|
||||||
get(`/create-build/${pr}/${sha}`).
|
|
||||||
set('AUTHORIZATION', 'foo').
|
|
||||||
set('X-FILE', 'bar').
|
|
||||||
expect(500, 'Test');
|
|
||||||
|
|
||||||
verifyRequests([req], done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should respond with 201 on successful upload (for public builds)', done => {
|
|
||||||
const req = agent.
|
|
||||||
get(`/create-build/${pr}/${sha}`).
|
|
||||||
set('AUTHORIZATION', 'foo').
|
|
||||||
set('X-FILE', 'bar').
|
|
||||||
expect(201, http.STATUS_CODES[201]);
|
|
||||||
|
|
||||||
verifyRequests([req], done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should respond with 202 on successful upload (for hidden builds)', done => {
|
|
||||||
buildVerifierVerifySpy.and.returnValue(Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedNotTrusted));
|
|
||||||
const req = agent.
|
|
||||||
get(`/create-build/${pr}/${sha}`).
|
|
||||||
set('AUTHORIZATION', 'foo').
|
|
||||||
set('X-FILE', 'bar').
|
|
||||||
expect(202, http.STATUS_CODES[202]);
|
|
||||||
|
|
||||||
verifyRequests([req], done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should reject PRs with leading zeros', done => {
|
|
||||||
verifyRequests([agent.get(`/create-build/0${pr}/${sha}`).expect(404)], done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should accept SHAs with leading zeros (but not trim the zeros)', done => {
|
|
||||||
const sha40 = '0'.repeat(40);
|
|
||||||
const sha41 = `0${sha40}`;
|
|
||||||
|
|
||||||
const request40 = agent.get(`/create-build/${pr}/${sha40}`).set('AUTHORIZATION', 'foo').set('X-FILE', 'bar');
|
|
||||||
const request41 = agent.get(`/create-build/${pr}/${sha41}`).set('AUTHORIZATION', 'baz').set('X-FILE', 'qux');
|
|
||||||
|
|
||||||
Promise.all([
|
|
||||||
promisifyRequest(request40.expect(201)),
|
|
||||||
promisifyRequest(request41.expect(404)),
|
|
||||||
]).then(done, done.fail);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe('GET /health-check', () => {
|
|
||||||
|
|
||||||
it('should respond with 200', done => {
|
|
||||||
verifyRequests([
|
|
||||||
agent.get('/health-check').expect(200),
|
|
||||||
agent.get('/health-check/').expect(200),
|
|
||||||
], done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should respond with 404 for non-GET requests', done => {
|
|
||||||
verifyRequests([
|
|
||||||
agent.put('/health-check').expect(404),
|
|
||||||
agent.post('/health-check').expect(404),
|
|
||||||
agent.patch('/health-check').expect(404),
|
|
||||||
agent.delete('/health-check').expect(404),
|
|
||||||
], done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should respond with 404 if the path does not match exactly', done => {
|
|
||||||
verifyRequests([
|
|
||||||
agent.get('/health-check/foo').expect(404),
|
|
||||||
agent.get('/health-check-foo').expect(404),
|
|
||||||
agent.get('/health-checknfoo').expect(404),
|
|
||||||
agent.get('/foo/health-check').expect(404),
|
|
||||||
agent.get('/foo-health-check').expect(404),
|
|
||||||
agent.get('/foonhealth-check').expect(404),
|
|
||||||
], done);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe('POST /pr-updated', () => {
|
|
||||||
const pr = '9';
|
|
||||||
const url = '/pr-updated';
|
|
||||||
let bvGetPrIsTrustedSpy: jasmine.Spy;
|
|
||||||
let bcUpdatePrVisibilitySpy: jasmine.Spy;
|
|
||||||
|
|
||||||
// Helpers
|
|
||||||
const createRequest = (num: number, action?: string) =>
|
|
||||||
agent.post(url).send({number: num, action});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
bvGetPrIsTrustedSpy = spyOn(buildVerifier, 'getPrIsTrusted');
|
|
||||||
bcUpdatePrVisibilitySpy = spyOn(buildCreator, 'updatePrVisibility');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should respond with 404 for non-POST requests', done => {
|
|
||||||
verifyRequests([
|
|
||||||
agent.get(url).expect(404),
|
|
||||||
agent.put(url).expect(404),
|
|
||||||
agent.patch(url).expect(404),
|
|
||||||
agent.delete(url).expect(404),
|
|
||||||
], done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should respond with 400 for requests without a payload', done => {
|
|
||||||
const responseBody = `Missing or empty 'number' field in request: POST ${url} {}`;
|
|
||||||
|
|
||||||
const request1 = agent.post(url);
|
|
||||||
const request2 = agent.post(url).send();
|
|
||||||
|
|
||||||
verifyRequests([
|
|
||||||
request1.expect(400, responseBody),
|
|
||||||
request2.expect(400, responseBody),
|
|
||||||
], done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should respond with 400 for requests without a \'number\' field', done => {
|
|
||||||
const responseBodyPrefix = `Missing or empty 'number' field in request: POST ${url}`;
|
|
||||||
|
|
||||||
const request1 = agent.post(url).send({});
|
|
||||||
const request2 = agent.post(url).send({number: null});
|
|
||||||
|
|
||||||
verifyRequests([
|
|
||||||
request1.expect(400, `${responseBodyPrefix} {}`),
|
|
||||||
request2.expect(400, `${responseBodyPrefix} {"number":null}`),
|
|
||||||
], done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should call \'BuildVerifier#gtPrIsTrusted()\' with the correct arguments', done => {
|
|
||||||
const req = createRequest(+pr);
|
|
||||||
|
|
||||||
promisifyRequest(req).
|
|
||||||
then(() => expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9)).
|
|
||||||
then(done, done.fail);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should propagate errors from BuildVerifier', done => {
|
|
||||||
bvGetPrIsTrustedSpy.and.callFake(() => Promise.reject('Test'));
|
|
||||||
|
|
||||||
const req = createRequest(+pr).expect(500, 'Test');
|
|
||||||
|
|
||||||
promisifyRequest(req).
|
|
||||||
then(() => {
|
|
||||||
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9);
|
|
||||||
expect(bcUpdatePrVisibilitySpy).not.toHaveBeenCalled();
|
|
||||||
}).
|
|
||||||
then(done, done.fail);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should call \'BuildCreator#updatePrVisibility()\' with the correct arguments', done => {
|
|
||||||
bvGetPrIsTrustedSpy.and.callFake((pr2: number) => Promise.resolve(pr2 === 42));
|
|
||||||
|
|
||||||
const req1 = createRequest(24);
|
|
||||||
const req2 = createRequest(42);
|
|
||||||
|
|
||||||
Promise.all([
|
|
||||||
promisifyRequest(req1).then(() => expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith('24', false)),
|
|
||||||
promisifyRequest(req2).then(() => expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith('42', true)),
|
|
||||||
]).then(done, done.fail);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should propagate errors from BuildCreator', done => {
|
|
||||||
bcUpdatePrVisibilitySpy.and.callFake(() => Promise.reject('Test'));
|
|
||||||
|
|
||||||
const req = createRequest(+pr).expect(500, 'Test');
|
|
||||||
verifyRequests([req], done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe('on success', () => {
|
|
||||||
|
|
||||||
it('should respond with 200 (action: undefined)', done => {
|
|
||||||
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
|
||||||
|
|
||||||
const reqs = [4, 2].map(num => createRequest(num).expect(200, http.STATUS_CODES[200]));
|
|
||||||
verifyRequests(reqs, done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should respond with 200 (action: labeled)', done => {
|
|
||||||
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
|
||||||
|
|
||||||
const reqs = [4, 2].map(num => createRequest(num, 'labeled').expect(200, http.STATUS_CODES[200]));
|
|
||||||
verifyRequests(reqs, done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should respond with 200 (action: unlabeled)', done => {
|
|
||||||
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
|
||||||
|
|
||||||
const reqs = [4, 2].map(num => createRequest(num, 'unlabeled').expect(200, http.STATUS_CODES[200]));
|
|
||||||
verifyRequests(reqs, done);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should respond with 200 (and do nothing) if \'action\' implies no visibility change', done => {
|
|
||||||
const promises = ['foo', 'notlabeled'].
|
|
||||||
map(action => createRequest(+pr, action).expect(200, http.STATUS_CODES[200])).
|
|
||||||
map(promisifyRequest);
|
|
||||||
|
|
||||||
Promise.all(promises).
|
|
||||||
then(() => {
|
|
||||||
expect(bvGetPrIsTrustedSpy).not.toHaveBeenCalled();
|
|
||||||
expect(bcUpdatePrVisibilitySpy).not.toHaveBeenCalled();
|
|
||||||
}).
|
|
||||||
then(done, done.fail);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe('ALL *', () => {
|
|
||||||
|
|
||||||
it('should respond with 404', done => {
|
|
||||||
const responseFor = (method: string) => `Unknown resource in request: ${method.toUpperCase()} /some/url`;
|
|
||||||
|
|
||||||
verifyRequests([
|
|
||||||
agent.get('/some/url').expect(404, responseFor('get')),
|
|
||||||
agent.put('/some/url').expect(404, responseFor('put')),
|
|
||||||
agent.post('/some/url').expect(404, responseFor('post')),
|
|
||||||
agent.patch('/some/url').expect(404, responseFor('patch')),
|
|
||||||
agent.delete('/some/url').expect(404, responseFor('delete')),
|
|
||||||
], done);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
File diff suppressed because it is too large
Load Diff
1
aio/aio-builds-setup/dockerbuild/scripts-sh/clean-up.sh
Executable file → Normal file
1
aio/aio-builds-setup/dockerbuild/scripts-sh/clean-up.sh
Executable file → Normal file
@ -2,6 +2,7 @@
|
|||||||
set -eu -o pipefail
|
set -eu -o pipefail
|
||||||
|
|
||||||
# Set up env variables
|
# Set up env variables
|
||||||
|
export AIO_CIRCLE_CI_TOKEN=UNUSED_CIRCLE_CI_TOKEN
|
||||||
export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/GITHUB_TOKEN 2>/dev/null)
|
export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/GITHUB_TOKEN 2>/dev/null)
|
||||||
|
|
||||||
# Run the clean-up
|
# Run the clean-up
|
||||||
|
12
aio/aio-builds-setup/dockerbuild/scripts-sh/dev-mode.sh
Normal file
12
aio/aio-builds-setup/dockerbuild/scripts-sh/dev-mode.sh
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Link the scripts on the host to the scripts in the container
|
||||||
|
# - the host scripts are mounted as a volume at `/dockerbuild`)
|
||||||
|
# - the original scripts are moved to `..._prod` in case they are needed later
|
||||||
|
# See `aio/aio-builds-setup/docs/misc--debug-docker-container.md` for more info
|
||||||
|
|
||||||
|
mv $AIO_SCRIPTS_SH_DIR ${AIO_SCRIPTS_SH_DIR}_prod
|
||||||
|
ln -s /dockerbuild/scripts-sh $AIO_SCRIPTS_SH_DIR
|
||||||
|
chmod a+x $AIO_SCRIPTS_SH_DIR/*
|
||||||
|
|
||||||
|
mv $AIO_SCRIPTS_JS_DIR ${AIO_SCRIPTS_JS_DIR}_prod
|
||||||
|
ln -s /dockerbuild/scripts-js $AIO_SCRIPTS_JS_DIR
|
||||||
|
|
@ -30,7 +30,7 @@ done
|
|||||||
|
|
||||||
# Check servers
|
# Check servers
|
||||||
origins=(
|
origins=(
|
||||||
http://$AIO_UPLOAD_HOSTNAME:$AIO_UPLOAD_PORT
|
http://$AIO_PREVIEW_SERVER_HOSTNAME:$AIO_PREVIEW_SERVER_PORT
|
||||||
http://$AIO_NGINX_HOSTNAME:$AIO_NGINX_PORT_HTTP
|
http://$AIO_NGINX_HOSTNAME:$AIO_NGINX_PORT_HTTP
|
||||||
https://$AIO_NGINX_HOSTNAME:$AIO_NGINX_PORT_HTTPS
|
https://$AIO_NGINX_HOSTNAME:$AIO_NGINX_PORT_HTTPS
|
||||||
)
|
)
|
||||||
|
2
aio/aio-builds-setup/dockerbuild/scripts-sh/init.sh
Executable file → Normal file
2
aio/aio-builds-setup/dockerbuild/scripts-sh/init.sh
Executable file → Normal file
@ -14,5 +14,5 @@ service cron start
|
|||||||
service dnsmasq start
|
service dnsmasq start
|
||||||
service nginx start
|
service nginx start
|
||||||
service pm2-root start
|
service pm2-root start
|
||||||
aio-upload-server-prod start
|
aio-preview-server-prod start
|
||||||
echo [`date`] - Services started successfully.
|
echo [`date`] - Services started successfully.
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -eu -o pipefail
|
||||||
|
|
||||||
|
# Set up env variables for production
|
||||||
|
export AIO_CIRCLE_CI_TOKEN=$(head -c -1 /aio-secrets/CIRCLE_CI_TOKEN 2>/dev/null || echo "MISSING_CIRCLE_CI_TOKEN")
|
||||||
|
export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/GITHUB_TOKEN 2>/dev/null || echo "MISSING_GITHUB_TOKEN")
|
||||||
|
|
||||||
|
# Start the preview-server instance
|
||||||
|
action=$([ "$1" == "stop" ] && echo "stop" || echo "start")
|
||||||
|
pm2 $action $AIO_SCRIPTS_JS_DIR/dist/lib/preview-server \
|
||||||
|
--uid $AIO_WWW_USER \
|
||||||
|
--log /var/log/aio/preview-server-prod.log \
|
||||||
|
--name aio-preview-server-prod \
|
||||||
|
${@:2}
|
@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -eu -o pipefail
|
||||||
|
|
||||||
|
# Start the preview-server instance
|
||||||
|
appName=aio-preview-server-test
|
||||||
|
if [[ "$1" == "stop" ]]; then
|
||||||
|
pm2 delete $appName
|
||||||
|
else
|
||||||
|
source aio-test-env
|
||||||
|
pm2 start $AIO_SCRIPTS_JS_DIR/dist/lib/verify-setup/start-test-preview-server.js \
|
||||||
|
--uid $AIO_WWW_USER \
|
||||||
|
--log /var/log/aio/preview-server-test.log \
|
||||||
|
--name $appName \
|
||||||
|
--no-autorestart \
|
||||||
|
${@:2}
|
||||||
|
fi
|
19
aio/aio-builds-setup/dockerbuild/scripts-sh/test-env.sh
Normal file
19
aio/aio-builds-setup/dockerbuild/scripts-sh/test-env.sh
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Set up env variables for testing
|
||||||
|
export AIO_NGINX_HOSTNAME=$TEST_AIO_NGINX_HOSTNAME
|
||||||
|
export AIO_NGINX_PORT_HTTP=$TEST_AIO_NGINX_PORT_HTTP
|
||||||
|
export AIO_NGINX_PORT_HTTPS=$TEST_AIO_NGINX_PORT_HTTPS
|
||||||
|
|
||||||
|
export AIO_ARTIFACT_PATH=$TEST_AIO_ARTIFACT_PATH
|
||||||
|
export AIO_BUILDS_DIR=$TEST_AIO_BUILDS_DIR
|
||||||
|
export AIO_DOMAIN_NAME=$TEST_AIO_DOMAIN_NAME
|
||||||
|
export AIO_GITHUB_ORGANIZATION=$TEST_AIO_GITHUB_ORGANIZATION
|
||||||
|
export AIO_GITHUB_REPO=$TEST_AIO_GITHUB_REPO
|
||||||
|
export AIO_GITHUB_TEAM_SLUGS=$TEST_AIO_GITHUB_TEAM_SLUGS
|
||||||
|
export AIO_SIGNIFICANT_FILES_PATTERN=$TEST_AIO_SIGNIFICANT_FILES_PATTERN
|
||||||
|
export AIO_TRUSTED_PR_LABEL=$TEST_AIO_TRUSTED_PR_LABEL
|
||||||
|
export AIO_PREVIEW_SERVER_HOSTNAME=$TEST_AIO_PREVIEW_SERVER_HOSTNAME
|
||||||
|
export AIO_PREVIEW_SERVER_PORT=$TEST_AIO_PREVIEW_SERVER_PORT
|
||||||
|
export AIO_ARTIFACT_MAX_SIZE=$TEST_AIO_ARTIFACT_MAX_SIZE
|
||||||
|
|
||||||
|
export AIO_CIRCLE_CI_TOKEN=TEST_CIRCLE_CI_TOKEN
|
||||||
|
export AIO_GITHUB_TOKEN=TEST_GITHUB_TOKEN
|
@ -1,14 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -eu -o pipefail
|
|
||||||
|
|
||||||
# Set up env variables for production
|
|
||||||
export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/GITHUB_TOKEN 2>/dev/null || echo "MISSING_GITHUB_TOKEN")
|
|
||||||
export AIO_PREVIEW_DEPLOYMENT_TOKEN=$(head -c -1 /aio-secrets/PREVIEW_DEPLOYMENT_TOKEN 2>/dev/null || echo "MISSING_PREVIEW_DEPLOYMENT_TOKEN")
|
|
||||||
|
|
||||||
# Start the upload-server instance
|
|
||||||
action=$([ "$1" == "stop" ] && echo "stop" || echo "start")
|
|
||||||
pm2 $action $AIO_SCRIPTS_JS_DIR/dist/lib/upload-server \
|
|
||||||
--uid $AIO_WWW_USER \
|
|
||||||
--log /var/log/aio/upload-server-prod.log \
|
|
||||||
--name aio-upload-server-prod \
|
|
||||||
${@:2}
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user