Compare commits
5121 Commits
Author | SHA1 | Date | |
---|---|---|---|
912911220a | |||
395f9cd8d2 | |||
94e4589d30 | |||
f43e29b3d5 | |||
9ab35a1165 | |||
d622558458 | |||
26501495fc | |||
ad160257fa | |||
b108e9a036 | |||
be08611d11 | |||
6b5c151b88 | |||
183f27894c | |||
d1a14f6989 | |||
ce4cff60f0 | |||
78bd3c70de | |||
b22405767f | |||
cd1e206cb3 | |||
453589f9fc | |||
fa3af8b1e0 | |||
c220328274 | |||
0d8528b0df | |||
9ac4a4d8a0 | |||
fe8c6b04ef | |||
63d395cb7e | |||
f5e7208bea | |||
b3759fdc90 | |||
ef056c5fb1 | |||
a808c7d150 | |||
89ace1a2d9 | |||
52fd4a2d35 | |||
8c32a2e8bc | |||
3ed1e842d6 | |||
d53c421768 | |||
01102cb235 | |||
97eb85826f | |||
1059789f9f | |||
0dba3ee359 | |||
80818549fe | |||
348c94911c | |||
26c79efa4a | |||
0b8db92a2b | |||
d0e6eeb51e | |||
93078e3709 | |||
29ab72980c | |||
5452889aa6 | |||
590b84fc1d | |||
b6deb00b97 | |||
01114dc10a | |||
7b1f3527be | |||
575ef9f5a4 | |||
11996cd1c4 | |||
8fc763d58f | |||
7a5136bcc1 | |||
a9f39a4fdf | |||
35d0284f45 | |||
74346e987a | |||
f221e9010e | |||
90c273a788 | |||
11d2a8cc81 | |||
ad106abece | |||
84f2928c8c | |||
610472bf3b | |||
7bed896f11 | |||
d7c72fced7 | |||
3ca5de7efd | |||
485e2a147a | |||
b24f1984f3 | |||
f8846ef14e | |||
9a93259b64 | |||
194f1f084f | |||
fba9fa2302 | |||
820e2027e0 | |||
0508143128 | |||
a97fbfd823 | |||
e552cadc90 | |||
35ab809326 | |||
919ef66a7c | |||
a37c65827a | |||
1371cf6d33 | |||
e9655720f8 | |||
8512e34521 | |||
21c9cfff5a | |||
e902f5ee6a | |||
013a241b94 | |||
0d0bc1f725 | |||
a655d288ca | |||
e55088a364 | |||
2e41631026 | |||
2bc85fb77e | |||
8167bfc87c | |||
711ef2ded4 | |||
5c57920dbb | |||
9b4d95974a | |||
8087b6b611 | |||
1ca292392b | |||
079c4b3482 | |||
e476c38dee | |||
8799ea7ea0 | |||
03787924da | |||
1022db1c8c | |||
560cb99f55 | |||
5acfdee3ca | |||
fc2c23ef4b | |||
1ddc34cbc5 | |||
184d63d91d | |||
236ac060b0 | |||
2cce8d4ebc | |||
a149c3a8db | |||
013c0ae2e1 | |||
c8aebaeb76 | |||
56254b53bd | |||
5d0e1147df | |||
a0f239fc71 | |||
88032eb302 | |||
827e223edb | |||
14d75d6971 | |||
8282e15c2b | |||
bdf5f3e499 | |||
a71038b5fa | |||
d735160a11 | |||
498c4c32b1 | |||
b9c7da6503 | |||
bf71b107b3 | |||
0d9b27ff26 | |||
c8c8648abf | |||
8ce59a583b | |||
573fb783e1 | |||
d666370e16 | |||
a72250bace | |||
026b7e34b3 | |||
4390e10dfd | |||
e56c8bf8d1 | |||
ca40565f9a | |||
975c269752 | |||
34306c356e | |||
2a7210e382 | |||
391767fb8f | |||
bf3beb5959 | |||
01917733a1 | |||
859da3af50 | |||
a43998c089 | |||
20729b3378 | |||
cf1ebdc079 | |||
893c1735dd | |||
81e975ad93 | |||
20ea5b5634 | |||
4222b63639 | |||
b5dbf5154e | |||
92e80af875 | |||
a4934a74b6 | |||
91bffa9c53 | |||
60800da6c1 | |||
b07bd30b70 | |||
4f965ad2a1 | |||
848f4148c0 | |||
65943b458f | |||
6574e61062 | |||
ee12e725c0 | |||
3ec7c5081d | |||
e22a302cad | |||
92b05652b2 | |||
4756324b5c | |||
ce5242462b | |||
1c9e526a83 | |||
8a626288a6 | |||
bf6ac6cef8 | |||
3da82338d1 | |||
f8f1168fa6 | |||
a752971bca | |||
f0c4f31771 | |||
a63fd2d0f5 | |||
d97994b27f | |||
c31e78f670 | |||
bc652a2943 | |||
e6e590479e | |||
123da1a8c2 | |||
7695dbd0bd | |||
9741f5b8cf | |||
1b97971eb7 | |||
0ada23a5fb | |||
6b1780d77e | |||
7865abf667 | |||
efa443bba3 | |||
f5a0ec0d7c | |||
5247594e86 | |||
095b6e8113 | |||
8b9249a670 | |||
f80c6008af | |||
b278ea1f09 | |||
1810cdf2c3 | |||
97ef8ae9e7 | |||
99c5db1fb1 | |||
e5c9f7a507 | |||
9729e8f4b9 | |||
78b6f88cb3 | |||
552836ebf0 | |||
2f36a9591d | |||
83c9bff242 | |||
4efb460127 | |||
ac1988d8e6 | |||
edb50a2f51 | |||
c89045fb77 | |||
1258ec041d | |||
499e303ea3 | |||
e618032d53 | |||
b9eeb1c383 | |||
1529ecd8fa | |||
51f26cb4e0 | |||
0d972d9bbf | |||
19546c234e | |||
791d192e63 | |||
15e671ec5a | |||
3567e81175 | |||
ff2ee644b4 | |||
b57ce9299a | |||
9e26216c40 | |||
a4398aa17f | |||
53e4e0c1a3 | |||
5e769d9a25 | |||
931a363612 | |||
3ca1a57f81 | |||
297c54ebb3 | |||
dc2464eaaa | |||
7dbc103cbe | |||
93837e9545 | |||
6c5c97b2f9 | |||
099d1a67a0 | |||
6744b19297 | |||
f9a4abb6dc | |||
4e4bca6bbc | |||
d0037b22ef | |||
d568d7a352 | |||
86d41a1db9 | |||
e6a0c45674 | |||
7d2a746090 | |||
c13f46c7c5 | |||
516af6c531 | |||
9c2d0d0b24 | |||
64647af1a6 | |||
c016066d9b | |||
beabfb7960 | |||
804fb99d66 | |||
387db75003 | |||
c40677a4f5 | |||
d4b46e271a | |||
53bae68617 | |||
952ca59336 | |||
affcbbdd7e | |||
496372dd30 | |||
e9e804fb02 | |||
aed95fd8c7 | |||
aca8ea9c0b | |||
4a01ada291 | |||
10618752e6 | |||
c60418b1f4 | |||
c78c221028 | |||
1aca54da06 | |||
f5d5a3df59 | |||
f017b26e5d | |||
c4ad83e7cd | |||
cf6ea283bb | |||
ea0a99610d | |||
df6a8b28de | |||
3b9bc73db4 | |||
18b6d580c5 | |||
03417df54e | |||
61cd5f7c0f | |||
68dfa04f8a | |||
6902977665 | |||
bc68b592b1 | |||
a64859b4bc | |||
2f30bbb495 | |||
603e7935aa | |||
030d43b9f3 | |||
dff10085e8 | |||
e804143183 | |||
bec4ca0c73 | |||
81acbad058 | |||
360be02c0e | |||
adce5064b0 | |||
1918f8d5b5 | |||
eb5d3088a4 | |||
683d53db4b | |||
a2929dfd57 | |||
911bfef04c | |||
aadbc8a9d3 | |||
2e7b5c5d64 | |||
86580a8460 | |||
8634d0bcd8 | |||
492576114d | |||
ca1e538752 | |||
afbee736ea | |||
84e311038d | |||
4dfa71f018 | |||
4e9f2e5895 | |||
081f95c812 | |||
152ca66eba | |||
17586f1e94 | |||
6712492dc2 | |||
9f4299d47b | |||
ad6771dcd4 | |||
2c25d29b25 | |||
ec29fd3e7b | |||
d042c4afe0 | |||
5d740785a9 | |||
68b2211e64 | |||
332394d87c | |||
62b4ff5250 | |||
634e9e970a | |||
678c28175e | |||
87a7f2dcb9 | |||
8fc4ae51fb | |||
66be3c9f51 | |||
b95089db20 | |||
8a4ec8e565 | |||
95743e3a64 | |||
9ad54d74d2 | |||
0e1cceed50 | |||
23648b052a | |||
91d2368e37 | |||
ede65dbede | |||
578e4c7642 | |||
96770e5c6b | |||
1130e48541 | |||
2a869271f6 | |||
c0bf222a05 | |||
d72d50d83e | |||
ef6f1605db | |||
ea7ec4a6b3 | |||
72f3d99920 | |||
f6991dec3b | |||
0367d14044 | |||
8bf51db3e7 | |||
4f250726aa | |||
86d3099141 | |||
d4c3742e25 | |||
95993e1dd5 | |||
d52d82d744 | |||
d2e6d6978e | |||
da4a28e692 | |||
b876356527 | |||
07509da78d | |||
9ff8155cd9 | |||
56f44be0aa | |||
d50d400231 | |||
19fcfc3d00 | |||
c048358cf9 | |||
8171a2ab94 | |||
2fd4c372d5 | |||
f76ce84ae1 | |||
f3859130f2 | |||
0f274d65c4 | |||
cb71b93351 | |||
4b51d31aea | |||
40bbfbf961 | |||
30f319a11f | |||
5d7ba57dd5 | |||
9cd8327051 | |||
1d8f939c96 | |||
2e28d9f012 | |||
7c730fe5b3 | |||
f233131974 | |||
aefa06f7df | |||
d878f3df93 | |||
f8741c0985 | |||
5e2ce9b2a6 | |||
d725ab5142 | |||
f1a860fbf7 | |||
141f9b2386 | |||
95f852e856 | |||
2c7386c961 | |||
d5cbcef0ea | |||
b0476f308b | |||
9dc52d9d04 | |||
297dc2bc02 | |||
0cc9842bf6 | |||
54ea10288e | |||
1880c9531f | |||
13a803d4f7 | |||
f6c2db818e | |||
67789f10ef | |||
b83f1300bd | |||
6e16a17015 | |||
d744dd70e0 | |||
4e91ead40b | |||
7f39f37003 | |||
bf9ab53f12 | |||
d99f661af7 | |||
5d58a31d86 | |||
07b89902d5 | |||
ce6948fc1b | |||
38d626a3fa | |||
9b8a244a15 | |||
1bbf28ad19 | |||
3b24e0edb6 | |||
b647608c96 | |||
31c462ae3f | |||
da39fd70d2 | |||
ee0b857172 | |||
e0d6782d26 | |||
2f9e957523 | |||
38dbae9ca0 | |||
5a3077f46c | |||
d0cc019c1a | |||
26e8032bd0 | |||
84af7b065d | |||
83dc3c0ee0 | |||
5952775a03 | |||
57531737e5 | |||
3fd50f07fd | |||
4237c34c78 | |||
361eaf1888 | |||
cca89ec36e | |||
e4c7f369f2 | |||
d573a14119 | |||
ff767dd153 | |||
34c6ce6b08 | |||
6737e91974 | |||
1006eab482 | |||
a9f2952882 | |||
100c7eff25 | |||
dc7349915d | |||
213c25fb08 | |||
615a515bba | |||
838a3f204f | |||
15c2467dbd | |||
74ea4e9b5d | |||
4481571999 | |||
2132c8f461 | |||
2b3cac5b31 | |||
8d5e3e6981 | |||
30f1dc002a | |||
5da3b00222 | |||
631998b2df | |||
30d6233e83 | |||
6468711e16 | |||
d698b0eadf | |||
1f3331f5e6 | |||
b6c9678f21 | |||
4c2ce4e8ba | |||
bb0f95b6fb | |||
853faf4c71 | |||
00c7db02d1 | |||
13143b850e | |||
c1724062f1 | |||
7570f7222f | |||
f1e3d5125d | |||
3511f08a81 | |||
982bc7f2aa | |||
3903e5ebe7 | |||
0918adf39d | |||
42c331bbf2 | |||
0bae97a726 | |||
c5949f85ef | |||
24521f549c | |||
4bd9f53e8f | |||
2a78dcbd5a | |||
2ea57cdcc3 | |||
31022cbecf | |||
ce8053103e | |||
0b885ecaf7 | |||
1700bd6f08 | |||
b89bc37170 | |||
39df4dbde7 | |||
716d887e51 | |||
5b4cf38166 | |||
4c0ad5238e | |||
9e8903ada1 | |||
aa55d88408 | |||
07fc4c2464 | |||
b9bd95b3b2 | |||
3f89aeb80a | |||
6c530d3a85 | |||
03bf0d6b41 | |||
d4cee514f6 | |||
331989cea3 | |||
bbf96db2f2 | |||
72c2578d14 | |||
1a666dea61 | |||
7634c1cb31 | |||
95914a0fbf | |||
9c50891d6e | |||
624433c51d | |||
09cc458bfc | |||
efe0c5f371 | |||
d9d226087c | |||
7bad1d356d | |||
0add00a743 | |||
d557f1d9de | |||
665627e254 | |||
a609bf50ed | |||
f8195e5e3d | |||
65b209359a | |||
3a65c9ad4e | |||
77e58c6179 | |||
23d625172a | |||
c3643615fc | |||
638aaecc7d | |||
5a79decba4 | |||
225162aa6c | |||
a43b80a4c9 | |||
beebf7fe14 | |||
0ef1f7ef0d | |||
fc6dad40ac | |||
ecd473bbce | |||
8a3fd58cad | |||
8024857d4c | |||
3fa876c5e7 | |||
516515d9e3 | |||
948f507ba0 | |||
81a8ee1ddb | |||
5e58da14f0 | |||
fa8e633be5 | |||
be337a2e52 | |||
4d164b6ca4 | |||
371ffef4ce | |||
fdfedceeec | |||
9e5d440a0b | |||
0f7d2ca7a8 | |||
81c9720acb | |||
ea2cfbbd2e | |||
2326b9c294 | |||
41de0e0d98 | |||
f6a2dbf4f5 | |||
b115374601 | |||
7d82053117 | |||
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 | |||
48d7205873 | |||
e1c6fd5453 | |||
968f153491 | |||
1e28495c89 | |||
0bcf20c9fa | |||
cf81823b07 | |||
d4ac9698ba | |||
c205516f0d | |||
777bd412b2 | |||
1e79014fc4 | |||
d0c066a223 | |||
65e18dc1bf | |||
1ceddb6290 | |||
22731a7588 | |||
72dd10f78f | |||
c0e3852384 | |||
2cb0f68a7b | |||
8450e0ab2f | |||
e38b2b502c | |||
445b9a5627 | |||
d523630ea2 | |||
d6016f1d1d | |||
be3cca4fd5 | |||
169e9dd2c8 | |||
13f3157823 | |||
edef58f466 | |||
7c89af34a9 | |||
bd576bb83f | |||
168c2a645b | |||
7729bb2bdc | |||
426324513d | |||
4d6f467fea | |||
6b859daea4 | |||
7960d1879d | |||
f1ab394218 | |||
86203736e9 | |||
41ef75869c | |||
2b8b647006 | |||
ed1db40322 | |||
d3594fc1c5 | |||
9fd70c9715 | |||
b7bbc82e3e | |||
139f5b3672 | |||
6f8ec256ef | |||
5d7005eef5 | |||
2e724ec68b | |||
76f8f78920 | |||
6eb6ac7c12 | |||
9644873023 | |||
ae4563202c | |||
42d4287153 | |||
f9a6a175bf | |||
53a16006d6 | |||
8a986d4642 | |||
e346c3c2f2 | |||
60aeee7abf | |||
1008bb6287 | |||
8a5cd2200a | |||
f58f3dc07a | |||
bb58138579 | |||
b8f740b253 | |||
23766b85e9 | |||
3cd9645daa | |||
2d38fa104b | |||
56b3f1703e | |||
c438b5eeda | |||
70b51a6255 | |||
7ebd8e59a8 | |||
1c533c913d | |||
ead3f926cb | |||
9be222f448 | |||
b137f09345 | |||
453693fd33 | |||
270176bbe4 | |||
5840a86f98 | |||
2aab1c9dd6 | |||
f9669e50ff | |||
99a393e84f | |||
d76531d16e | |||
23dc9a90b0 | |||
0b28732d77 | |||
06a33984af | |||
ba3eb8b654 | |||
c8ad9657c9 | |||
9be8abd012 | |||
74b250b146 | |||
d8c828c9b1 | |||
97277bc9fb | |||
1821b75530 | |||
82004c76ac | |||
a663565403 | |||
85d9c20b1d | |||
80a74b450a | |||
9a6f27c34c | |||
d723a69b31 | |||
d98b1c3bc4 | |||
02b5087685 | |||
48394c64ae | |||
cde0b4b361 | |||
9f20dd937a | |||
a1b630ee8f | |||
d05d28629d | |||
ee50ee493d | |||
161ff5c79d | |||
71e0df039c | |||
0399c6972a | |||
328971ffcc | |||
4d8b8ad372 | |||
0d6b74dd87 | |||
52d11f63cf | |||
a14f25c338 | |||
0b4d85e9f1 | |||
b9e095aa31 | |||
05e3e4d71e | |||
81a9db2b0a | |||
b7823e7087 | |||
3f8ab80583 | |||
ffb9dc6cf9 | |||
86d254d386 | |||
505b54b86b | |||
a527c695aa | |||
80576641a8 | |||
50fbed8e5f | |||
7d27ecc319 | |||
03616bcb43 | |||
3a19f70d1c | |||
dc1f1295ee | |||
49df4ef454 | |||
e1146f3d06 | |||
0d5f2d3c7e | |||
a167bca927 | |||
e3709f5d48 | |||
197387d05e | |||
1089261717 | |||
ddb792da28 | |||
89203c96ad | |||
3d20c50156 | |||
dcabb05102 | |||
68814040e3 | |||
3980640d53 | |||
52d43a99ef | |||
45feb10c46 | |||
250527ca68 | |||
94076c934c | |||
f936b8cbd2 | |||
d571a51739 | |||
86b1cc7313 | |||
787c54736c | |||
19544060d3 | |||
c0e2dba07b | |||
e01b539ee5 | |||
809e8f742e | |||
00c110b055 | |||
1e74ea9e60 | |||
f62876bbcb | |||
fddd2af4fc | |||
d5a9396017 | |||
3e6a722ddb | |||
5fe1e74dd3 | |||
f974c48885 | |||
568612349f | |||
b719905f9b | |||
56a8533cf3 | |||
b72dbc843f | |||
8fe8b8fcff | |||
b6af8700ce | |||
3d52174bf1 | |||
dbdcfed2bd | |||
ffbacdf4ac | |||
7f3242affb | |||
e3064d5432 | |||
0c3738a780 | |||
0922228024 | |||
c94a2c9e3f | |||
948e2236c0 | |||
a294e0dd79 | |||
3553977bd7 | |||
1ae3f87383 | |||
4e7a44c816 | |||
d1805d04d5 | |||
d243baf48a | |||
ff84c5c4da | |||
87ddbdf919 | |||
9803cb011e | |||
13d60eac61 | |||
d876700c26 | |||
99bdd257a6 | |||
3db9d57de3 | |||
66e50f28d2 | |||
0ede987ced | |||
71100e6d72 | |||
676ec411b9 | |||
01e7ff682c | |||
34c42836cf | |||
50d4a4fe5c | |||
69510acb20 | |||
ef1c6d8c26 | |||
2ecaa40e64 | |||
fc4dc35426 | |||
104d30507a | |||
c57b491778 | |||
1dc7d0d29e | |||
39c8baea31 | |||
abed2cd52c | |||
22758912a0 | |||
bb6b59128f | |||
4258c3d1df | |||
70156bc4ed | |||
2ac2ab7ff6 | |||
ca0a55f4ee | |||
0b3d25d67e | |||
24e0c3d43d | |||
922908818f | |||
8dec381145 | |||
32da3e1602 | |||
6d68f3e39a | |||
50fb13fb09 | |||
fe8fcc834c | |||
5c0e681bf3 | |||
7d6e833a6f | |||
49e900d6fc | |||
5feb9e1935 | |||
002a5afa98 | |||
cf0968f98e | |||
855e8ad9f6 | |||
89c442270a | |||
ae9418c7de | |||
166d90d2a9 | |||
7d318743c1 | |||
2a68ba4cbb | |||
d244523ae6 | |||
941d2cdaaf | |||
7d1f9c8a7c | |||
f841e36543 | |||
f229449c67 | |||
6e20e0aac8 | |||
1e139d4339 | |||
fba3f10938 | |||
c95437f15d | |||
39c7769c9e | |||
8c51ce6f3b | |||
71b0c3d469 | |||
b8760a0ca5 | |||
50fb58fd01 | |||
fe8fe9ba9e | |||
637805a0c9 | |||
7b2b1afe71 | |||
7d3fd4d655 | |||
10da6a45c6 | |||
84272e2227 | |||
cb31381734 | |||
3e1a3b2e32 | |||
1e6a226703 | |||
b91254fc43 | |||
8b8168262d | |||
a26965812b | |||
def354de16 | |||
9782736e00 | |||
e8354edcd2 | |||
5b76f04b7f | |||
a57825acf3 | |||
efc7639352 | |||
3e26cabe02 | |||
9d114c052a | |||
43e61c25e1 | |||
4e1493a1d6 | |||
794584e353 | |||
45862d0812 | |||
f3625e424b | |||
ccbda9de65 | |||
27bc7dcb43 | |||
0f7e4fae20 | |||
a45fad3dd9 | |||
f00ae516eb | |||
6d246d6c72 | |||
7c8159b3e2 | |||
5aa12c73ae | |||
d8f7b293d7 | |||
39af314e29 | |||
8daadf360c | |||
859a3d5784 | |||
66f6a48210 | |||
8a4c577917 | |||
2b15108f7e | |||
bc4f10ca20 | |||
e6516b0229 | |||
77309e2ea4 | |||
e371b226fa | |||
ccb19fea68 | |||
38a0d1fac5 | |||
e7b392bf3a | |||
8977b9690e | |||
d4d8125b2d | |||
62443b04a0 | |||
3c1eb9413f | |||
4168c946c6 | |||
293ec78069 | |||
131d0d8e8a | |||
5fb0b567ce | |||
03f93b3772 | |||
3ccb4490a4 | |||
2ea197b99f | |||
503a524d27 | |||
a577c9e1f4 | |||
52ce9d5dcb | |||
2b49bf77af | |||
92b278c097 | |||
513f645894 | |||
0bd2d7bac6 | |||
82c5313740 | |||
c8e865ac8e | |||
d9bf6e37ae | |||
e3c54e4465 | |||
153ba4dff3 | |||
5731d0741a | |||
70ef061fa6 | |||
c2b5ebfa24 | |||
0d07d273dc | |||
131602474d | |||
282d3510cf | |||
3ed2d75336 | |||
4d55dfd9d9 | |||
86bf5f3912 | |||
dbfb6b9d45 | |||
8dd99ac550 | |||
014949f74c | |||
5e8bf2f88d | |||
ea143e7498 | |||
29eb24b142 | |||
dc4a3d00d0 | |||
8aa70c2477 | |||
49c5234c68 | |||
1b253e14ff | |||
8c1ac28275 | |||
5ef7a07c4b | |||
113556357a | |||
7983f0a69b | |||
8be6892777 | |||
9f877f4416 | |||
4664226b97 | |||
d4c66d5edb | |||
ce1543fcde | |||
a6e797b8f5 | |||
ca79e11bfa | |||
f781f741ea | |||
bd02b27ee1 | |||
e3759f7a73 | |||
7de2ba0e22 | |||
07b4c8be42 | |||
3128b26e5c | |||
4f5b01a98a | |||
c151f9cdc8 | |||
24ab0a7db0 | |||
31988a6ff9 | |||
8ac74da016 | |||
355e0b0587 | |||
d96ae123b2 | |||
7e73287676 | |||
9dd647b087 | |||
47814b4cdf | |||
700e55ce14 | |||
68d37ef0c1 | |||
acf270d724 | |||
1007d1ad27 | |||
51e9e64c5a | |||
1915e47d11 | |||
e994b11105 | |||
856ee73464 | |||
0d06c866c6 | |||
1208a35373 | |||
b415010222 | |||
d6989c80d3 | |||
81e4b2a4bf | |||
c494d3cf60 | |||
22b58a717a | |||
86b13ccf80 | |||
8db928df9d | |||
9367e91402 | |||
87b16710e7 | |||
20c463e97c | |||
57eacf4b5a | |||
d814eaad95 | |||
678fd32406 | |||
3e938279d0 | |||
d700a409da | |||
b750919ce0 | |||
9c403753e2 | |||
83a06863f9 | |||
08a18b82de | |||
255463ed48 | |||
b4bbdb4ce2 | |||
7623d74607 | |||
ccaa199366 | |||
069062236c | |||
5794506c64 | |||
cb65724761 | |||
44856bfc2f | |||
5db4f1a5ba | |||
0561b66a2b | |||
5cbcb5680b | |||
6948ef125c | |||
08f943a1f3 | |||
f69ac670ee | |||
60aa943e2d | |||
68a799e950 | |||
5f178f3a5a | |||
81c13e2f86 | |||
2d9111bfb6 | |||
a5c47d0045 | |||
7e3f8f77a9 | |||
6a663a4073 | |||
ec57133b61 | |||
3647cb7f3b | |||
49d5de68f6 | |||
4ab70fb93d | |||
5d6074eaff | |||
b86d4dee4d | |||
9add50129d | |||
9d364203a6 | |||
4247176b6e | |||
3b9c5c849c | |||
e79b845a45 | |||
b492b9e12b | |||
b99ef2b80a | |||
27d811a7ce | |||
accda00190 | |||
c25e6142d2 | |||
b96a3c8def | |||
c917e5b5bb | |||
2a78d5e6fe | |||
95074ca303 | |||
1cd9e6c2eb | |||
855d9c00e0 | |||
49d97f1ba0 | |||
62f751cd87 | |||
646b42a113 | |||
24e5c5b425 | |||
42a7295203 | |||
7c39216083 | |||
223882aeb6 | |||
aafb46a8fe | |||
c73196eb59 | |||
d6595ebd39 | |||
5b25c07795 | |||
3ed7fc6686 | |||
7c1bd7170e | |||
2e21690c66 | |||
f6f44edcc0 | |||
90bf5d8961 | |||
b87d650da2 | |||
e53179ef8c | |||
31795b620f | |||
41cd8f3efb | |||
3fd3c2ac4c | |||
1eafd04eb3 | |||
00c4751f37 | |||
c2e131119b | |||
7866684f2b | |||
6e05ae02a2 | |||
431a42a238 | |||
7a9c987e56 | |||
ae86cb3be0 | |||
a6f34be9f5 | |||
d74078fb88 | |||
ddd6124802 | |||
96a0e131bf | |||
e43d3fa4b7 | |||
7657535718 | |||
3de80fc7fb | |||
729c797890 | |||
188ff848d2 | |||
280a784fe3 | |||
4f36340de7 | |||
83bb5d1922 | |||
3e39fef274 | |||
36cc72ee5b | |||
01b5acd7cf | |||
186118e684 | |||
609e6b9787 | |||
68bf8c36c6 | |||
8216657681 | |||
13cb75da8b | |||
23a98b9e51 | |||
bd149e5d67 | |||
fb906a87e8 | |||
0bdd30e34f | |||
373fa78d7f | |||
26fbf1d13c | |||
608c3748e8 | |||
6d8c847e7b | |||
919f42fea1 | |||
1b6b936ef4 | |||
db2329ef6a | |||
de267e97c9 | |||
f8c6947205 | |||
41fea84957 | |||
a7b07defe1 | |||
6e7d071c6b | |||
99d330a1b7 | |||
3cdf5afc6e | |||
ea4321d912 | |||
88ab1d0e55 | |||
20d76374ed | |||
8ee25e6b58 | |||
43597279d6 | |||
55103419e9 | |||
547efb5f4d | |||
091b11a4ab | |||
4042a84ad6 | |||
6a24c02d73 | |||
b7c417f618 | |||
313bdce590 | |||
5cf82f8f3f | |||
e5e5c24d48 | |||
1d378e2987 | |||
017d67cdf8 | |||
83631b28cb | |||
d4b6c41a5f | |||
66b2d78305 | |||
67b8d57a8d | |||
02acb5e3e5 | |||
a2e8b3a6a8 | |||
d4b8b24406 | |||
cfde36da84 | |||
d889f57ae2 | |||
816bc8af17 | |||
d2a86872a9 | |||
474dbf09ec | |||
e129b18d17 | |||
8a27a034c4 | |||
4ecae6449e | |||
5e307d5ba7 | |||
089fe83865 | |||
b1cda3639f | |||
c4221dad11 | |||
fe3679a356 | |||
72eab4d254 | |||
db2d67cc00 | |||
117c7eebc3 | |||
89f64e58c3 | |||
553a680817 | |||
858e48a794 | |||
e942d8b681 | |||
f1e4a153f0 | |||
e0ed59e55f | |||
d6b1466c81 | |||
d1abf4e897 | |||
08e7efc69e | |||
46674d5fac | |||
c5ca5c0d9f | |||
61170856ee | |||
a800ccd922 | |||
971e78dc35 | |||
b0eca85e51 | |||
d01ec03f54 | |||
9e2d87f5b8 | |||
fc034270ce | |||
77ff72f93b | |||
44095d95c9 | |||
e3518967ad | |||
005dc8f68b | |||
7e9649bdf1 | |||
e3e15773ee | |||
b25e15c317 | |||
3b067c8579 | |||
57cf5509e6 | |||
3f20a5c7c8 | |||
14d8a98001 | |||
5cb36ed706 | |||
490e39a23f | |||
33c1c1df36 | |||
d8d4f654a6 | |||
2c4850dc58 | |||
2ef4760ff7 | |||
52f0e3cc3b | |||
61265b42ef | |||
6601d0f7ba | |||
cccc328a52 | |||
65211f46cf | |||
da9ff255dd | |||
2cf6244b1d | |||
b45fa5e263 | |||
d7ed9c9e9e | |||
266d97de95 | |||
d71329d55c | |||
7ba26b140b | |||
297723d0bc | |||
bb07fbde76 | |||
d7e8d15578 | |||
bfad6b4fa1 | |||
fd9d1888ce | |||
94fbe3b5ac | |||
56828e43b6 | |||
c5cfc3a1b6 | |||
b76f5a6a7d | |||
fb41b7dc30 | |||
ca1019a950 | |||
9ebf0c8e5e | |||
8062f7de9e | |||
cc6c4346c2 | |||
4cb46ce10c | |||
7ef9d4a582 | |||
a522bb9f03 | |||
31b96e99ff | |||
b7a6e1fef7 | |||
84b4593d01 | |||
0c6dc45c85 | |||
5b96078624 | |||
1a44a0b4a8 | |||
b1f040f5a2 | |||
eb031c6ff1 | |||
b4c252bcc5 | |||
db77d8dc92 | |||
ab5bc42da0 | |||
f567e1898f | |||
8d0ee34939 | |||
43a49d3f64 | |||
811a7f2863 | |||
9ed5fb6d2c | |||
e1c4930a1a | |||
dab5df9734 | |||
b1d03fe70b | |||
06c0d9666f | |||
1c9200eca8 | |||
ace6440460 | |||
b26ac1c22f | |||
60e5507076 | |||
4cfa571258 | |||
999ab0a690 | |||
ba47997715 | |||
a35bf114eb | |||
6761a64522 | |||
0b47902ad7 | |||
4662878a1f | |||
ca776c59dd | |||
f2563ca800 | |||
9757347e71 | |||
a19e018439 | |||
6ff164be0e | |||
84f024309a | |||
c6b206ee4b | |||
1d1e75ee2b | |||
acf6781ccc | |||
fd48e53986 | |||
fe312ccb4c | |||
764f471dc0 | |||
8b02c0e769 | |||
5a2ee7a6f5 | |||
529d4fc9ee | |||
fac7dde5b1 | |||
1f005908a4 | |||
2278fe8f0e | |||
aad3444a58 | |||
44377adbcc | |||
b28b3acb83 | |||
7493435911 | |||
937f7cea37 | |||
7d1990e4d1 | |||
76f8ae31ad | |||
103846a51d | |||
0a536af093 | |||
4f29287399 | |||
62e6c1f43a | |||
c3c513ed9e | |||
ed495bc9f1 | |||
a3de5f8f20 | |||
2491b7249a | |||
a851ba3781 | |||
0468a649af | |||
47d3acdc49 | |||
acbfb9eb4d | |||
d35f84a167 | |||
87e9f333d4 | |||
08fc4f3ad8 | |||
c6c79ab5dc | |||
6837491f08 | |||
fc5af69fb2 | |||
81ccb718b1 | |||
0c56dfadef | |||
7be7abdebd | |||
5a1ddee88c | |||
99f8e10809 | |||
d665d9a18c | |||
8b2101be9f | |||
0d56cee9e1 | |||
7f612fc828 | |||
010a4efa8c | |||
c3280b2c2f | |||
d11b249d36 | |||
9c29127723 | |||
43615604d1 | |||
d9792309ec | |||
fd1c39ce42 | |||
896811df64 | |||
fb59b2dd97 | |||
b64a276d4b | |||
815ae29b83 | |||
8f690c9062 | |||
7b63f861c6 | |||
674c3def31 | |||
80e483ceac | |||
ac683d7abb | |||
33630dd3ed | |||
7e581dab5f | |||
102ed3b03c | |||
29b838c35d | |||
328b48b697 | |||
d1177c75f8 | |||
12f90ef428 | |||
2c09b707ce | |||
f4017ce5e3 | |||
4384a92271 | |||
da31db757b | |||
6f213a74f2 | |||
490772e680 | |||
e7ef02722d | |||
d5e7f60f04 | |||
6e73300ff1 | |||
6c2c95851a | |||
08325aaffc | |||
1e1c7fd408 | |||
d1e716b2cb | |||
639d52fe71 | |||
aa27155618 | |||
eac36d7e1a | |||
0224f1aaf3 | |||
74b203a55e | |||
0e10e731dc | |||
9fabe2f5fa | |||
c2a53bbf61 | |||
3bd682f432 | |||
2bb783824e | |||
6199ea5d4a | |||
2e270bb96a | |||
b76dd3b979 | |||
a025f7e2a6 | |||
221ae6998a | |||
b551f844e4 | |||
f958293205 | |||
993eeababb | |||
75febe7511 | |||
b8f4e433d5 | |||
a4e06b685b | |||
01975ff021 | |||
77c4b82938 | |||
1fc72e53f5 | |||
bbfa1d31a4 | |||
58faa0c7e7 | |||
aae437cb1e | |||
fa11d7822c | |||
1619160c8a | |||
7a8c58162c | |||
e0ae74d40e | |||
af46d097ff | |||
7e8cee6b61 | |||
37d2cb4553 | |||
ce40e85cbb | |||
5706810af2 | |||
a4ac8728cb | |||
d1e33d2df7 | |||
0d516f1658 | |||
de3ca56769 | |||
764760ba63 | |||
b3a10e0a42 | |||
0cb4f12a7a | |||
4b96a58c5a | |||
ee145790d7 | |||
c973830d9a | |||
8555a3a3cd | |||
f1db789450 | |||
50030f650b | |||
3fb4e190a8 | |||
4f7fac0e03 | |||
50ab8d8ad4 | |||
f544635014 | |||
c059670792 | |||
9a2479d423 | |||
720031b5f6 | |||
10ecdb13bf | |||
bb3f0e5ed2 | |||
37c1634276 | |||
58143555bc | |||
60e8392722 | |||
f4c56f4931 | |||
30a6861fd0 | |||
5a298b1c5e | |||
3e8eef6015 | |||
1fac5f4eb1 | |||
33f8120164 | |||
2cc2389fc6 | |||
da58a55ece | |||
11ea3a3f33 | |||
195193b9be | |||
0fba9a6e57 | |||
3a9d916632 | |||
eb9968ab2a | |||
b5f41f2c35 | |||
629629d1bd | |||
ae8a0092bd | |||
628303d2cb | |||
d80e9304c6 | |||
5cd36c7764 | |||
f3d2623f0f | |||
083474e429 | |||
fc813f67f4 | |||
3b607e60e9 | |||
b874501025 | |||
ce43e96d49 | |||
524e5d8ae7 | |||
79cecf9a5e | |||
ac316be79b | |||
b8053f1d4f | |||
9d9fb607cd | |||
5dc50e4688 | |||
9141424ac6 | |||
31b90436b4 | |||
f66f408b04 | |||
92821e338b | |||
1aef4df127 | |||
0a065bbdcf | |||
8ea15b4f12 | |||
7dc150c1e8 | |||
5bb14a68d2 | |||
381da1af45 | |||
cdd05bd2ca | |||
079d8e57d5 | |||
46ba7f69dd | |||
5db9ab12c0 | |||
7c039613dd | |||
dae4689b1c | |||
bf88c5c9f8 | |||
1aebee42eb | |||
6699fb5d77 | |||
6dd8f6efe4 | |||
5b6e59cfb3 | |||
f48e215305 | |||
32a41bc738 | |||
f99cb5c995 | |||
aaefd51a88 | |||
4a426696c9 | |||
5c8340aae0 | |||
c560423b52 | |||
46eadb5cfb | |||
1678423619 | |||
412b85ba89 | |||
d37064ce28 | |||
cae48df25b | |||
23cc3ef2eb | |||
550433a128 | |||
4506230fbb | |||
5671ae6a58 | |||
fab6b39c3d | |||
18ac228a27 | |||
7ca772060b | |||
580f05bd9c | |||
d284404060 | |||
92724b396b | |||
62e6e21895 | |||
fa2c9a81dd | |||
a4bf5621ed | |||
55c9fb298f | |||
60065935be | |||
5266ffe04a | |||
4290ea4bb9 | |||
e7f1af3c54 | |||
a2330ff2db | |||
ff34d5ea7a | |||
ad9ce5cb41 | |||
7a1c43733b | |||
85d3b591b6 | |||
43d62029f0 | |||
3df811767e | |||
9c7805be2f | |||
d6bf71ae95 | |||
9cd446565c | |||
6e5fb99304 | |||
6cb1adf105 | |||
a9e05ac82f | |||
120673a3ac | |||
8c10df30d7 | |||
0c4e3718f5 | |||
6880766fb7 | |||
a30728ca5a | |||
aebf04e32c | |||
15278784fc | |||
22cb2c9441 | |||
88a93e730e | |||
dc95e7bc33 | |||
7fdc24db72 | |||
3603a10ea2 | |||
6f7d14064c | |||
a5f0939eae | |||
193737a1ea | |||
be10bf538b | |||
439030fb57 | |||
481b22ecb0 | |||
45e090b614 | |||
0d9140cdce | |||
9fb08e2377 | |||
de0b13d41d | |||
07d33d4e5a | |||
9f7bd8f618 | |||
28058b784b | |||
7de13b60d6 | |||
00497437a6 | |||
4e004f3783 | |||
d9dc46e651 | |||
9e9b8dd494 | |||
12665a749c | |||
e2e80ec61c | |||
5a86f7144f | |||
bd024c02e2 | |||
884bf0ef09 | |||
0b348c8ffe | |||
e1ea7ed019 | |||
ed53c5ccdd | |||
d28ce50067 | |||
ab348ee2be | |||
0ebdb3d12f | |||
de90314304 | |||
f739f756ce | |||
910a16a1ff | |||
2aabbc51fa | |||
2388f24256 | |||
27e14b2fb3 | |||
7a406a32fa | |||
bf6a416bce | |||
7b7757dd3d | |||
4bd3a65764 | |||
6f0191744c | |||
4f0cae0676 | |||
65f296a676 | |||
b9cbe83104 | |||
60d99839de | |||
7966744a44 | |||
d9a0a8ff3e | |||
bcaa07b0ac | |||
d98e9e7c7f | |||
16f021c319 | |||
3a30f5d937 | |||
0f88fc73db | |||
01d2dd2a3a | |||
da76db9601 | |||
91503e538e | |||
53227abe7b | |||
768100516f | |||
328511be8e | |||
56e545735c | |||
07e9969cb7 | |||
5ab9d4d437 | |||
129bb1800b | |||
49396ca2ae | |||
34981063ec | |||
8ca26a9ebb | |||
e44f69c387 | |||
99711b12f9 | |||
204ba9d413 | |||
e5e1b0da33 | |||
d77bb460b0 | |||
bb58664b13 | |||
bfe35dac85 | |||
c84817970e | |||
b524e4b142 | |||
f88fba020b | |||
32105c8012 | |||
4a075e885f | |||
838a610197 | |||
f461f43d72 | |||
64efcf103c | |||
b12ea30a66 | |||
8c2a57878b | |||
4f2c51fe56 | |||
061564394f | |||
7cd8e8dbd1 | |||
73261a8b70 | |||
f285cff10b | |||
8768665587 | |||
623d769858 | |||
17fb9832f4 | |||
3cc5c2e4d0 | |||
fad86a67ca | |||
5f1be9b89b | |||
e6c731f791 | |||
7d095b96cd | |||
67f570caeb | |||
689f351092 | |||
4648597d14 | |||
b43f8bc7d3 | |||
c445314239 | |||
4a7be487da | |||
5caad5fe93 | |||
e5fcf650f8 | |||
2b3de6390f | |||
5c387a7f3c | |||
fc50c77bd3 | |||
bfe077ad64 | |||
1a0cb21538 | |||
0bede54b2d | |||
243c86cd04 | |||
9054e357d6 | |||
0b68a35ff2 | |||
e27cfd6236 | |||
1612985e48 | |||
4f21d373b7 | |||
ce63dc6f95 | |||
d54615d555 | |||
912fe08756 | |||
99408d0445 | |||
f258ec67bf | |||
5d82d8da6d | |||
6ef9f2278f | |||
688096b7a3 | |||
2e450f6fda | |||
fe21437232 | |||
cf3ff7d219 | |||
d72f44556d | |||
0b45dfac29 | |||
167fdf745c | |||
39a12d2c3d | |||
87f60bccfd | |||
46efd4b938 | |||
19368085aa | |||
d2be675acc | |||
44f637a88b | |||
863aff1a77 | |||
400460cc93 | |||
4d506acba0 | |||
7c9b411777 | |||
22b96b9690 | |||
cedc04c320 | |||
bafdad9083 | |||
9220521149 | |||
b149424b11 | |||
269c3a1908 | |||
f9247e4b2e | |||
44de10e2db | |||
0ebd577db4 | |||
4e6ac185e5 | |||
e55bf8fa79 | |||
3b167be069 | |||
02e6ac2117 | |||
04ca77e38e | |||
a011654c71 | |||
88b3198c80 | |||
6e5e819e80 | |||
19e6b8dad5 | |||
f256c02b5e | |||
dd20898bd5 | |||
d509bd6849 | |||
cd2ebd22fd | |||
112431db69 | |||
a0a01f1e1e | |||
ae19d071bb | |||
f5a98f41fe | |||
c09bd67aee | |||
9df13add5d | |||
049757b237 | |||
1f9734315d | |||
6f0dad1710 | |||
37fedd001c | |||
b1365d1fa8 | |||
4ac606b419 | |||
51027d73cc | |||
48636f3e85 | |||
bd9d4df735 | |||
34e355a3b0 | |||
58b94e6f5e | |||
db56836425 | |||
21e44c6ba9 | |||
f5d75d8efd | |||
6e00410e1c | |||
f95730b8e2 | |||
cd58c0a6d9 | |||
38fef1588d | |||
3f70aba272 | |||
eb6fb2d8f9 | |||
c602563589 | |||
8449eb8d62 | |||
a225b48482 | |||
129d1e0fb1 | |||
aa7dba244b | |||
0bf6fa5b32 | |||
40315bef3d | |||
123efba388 | |||
fa451bcd19 | |||
0d8deb0795 | |||
e8326e600d | |||
b5be18f405 | |||
1e6cc42a01 | |||
b26a90567c | |||
b0b9ca3386 | |||
5412e10bcd | |||
489fec1299 | |||
2fee5cc095 | |||
f13f4db9dc | |||
2ca77d80ec | |||
73c203fda9 | |||
c499c8f4db | |||
4c089c1d93 | |||
d485346d3c | |||
8407fcc979 | |||
f64ee15487 | |||
5d4fa7f0c8 | |||
8fb34bcd43 | |||
6d1367d297 | |||
538f1d980f | |||
065bcc5aad | |||
8ad4ae0a07 | |||
ffc6ee0086 | |||
78167915ee | |||
ad8fb8484f | |||
ce649f725f | |||
fcb8c492d6 | |||
4f744cc66f | |||
505ae752b6 | |||
363dfa5437 | |||
b3d1761825 | |||
d0db9ded90 | |||
4c40812b71 | |||
fa974c7d4e | |||
2c75acc5b3 | |||
f86d8ae0fd | |||
94bc277b1b | |||
8ea4c57174 | |||
53b0fe8144 | |||
2a1e3d191f | |||
2c2b62f45f | |||
58932c7f38 | |||
5bb9f64218 | |||
405d97431f | |||
41064fcb36 | |||
b80fd6be58 | |||
3aea8fd5f3 | |||
b64139650c | |||
1d2bdcb4d0 | |||
12be311618 | |||
51ca643c27 | |||
69d359bb51 | |||
4f60968704 | |||
25faf808a5 | |||
d7e5d45f43 | |||
ba8df8a3f1 | |||
0451fd93df | |||
49f074f61d | |||
ec445b5c73 | |||
ab790f3c84 | |||
dd534471ec | |||
8bb2f5c71d | |||
0e311e3918 | |||
997b30a093 | |||
94707fe795 | |||
2f0ab7ee98 | |||
b8fe121a16 | |||
e751a0a2bb | |||
288851c41e | |||
9eaf1bbe67 | |||
aabe16c08c | |||
b6c941053e | |||
c82cef8bc6 | |||
f8749bfb70 | |||
7d65356ae3 | |||
11f30fc351 | |||
b107131f8a | |||
11264e2174 | |||
b924ce3a62 | |||
e75f0cee18 | |||
8c358844dd | |||
e454c5a98e | |||
930ecacd86 | |||
5170ffe844 | |||
e95b61d42a | |||
8a85888773 | |||
83b32a0a0a | |||
8d34364ff5 | |||
142117f6bb | |||
79656e7f96 | |||
d100f1b187 | |||
4bd3e5f92f | |||
3f3be429c9 | |||
7e4c13f2de | |||
82a791223c | |||
ef99126aea | |||
c10c831b8e | |||
40ba009e25 | |||
d3827a0017 | |||
a7e1f236ff | |||
1f599818bd | |||
5a32d7e36f | |||
1ea41d48d3 | |||
25a43041d2 | |||
c593d69ce7 | |||
13ab91e05d | |||
f089bf5333 | |||
8e1e040f72 | |||
28240625e6 | |||
0d248079ba | |||
a4032296cc | |||
4180912538 | |||
094666da17 | |||
3a809cb431 | |||
b43b164a61 | |||
1dcbc12fd3 | |||
ae2e85e8ef | |||
aad431642a | |||
a81d599bfc | |||
7effb0016c | |||
f791862e52 | |||
b2f366b3b7 | |||
9eecb0b27f | |||
45eff4cc65 | |||
b3ffeaa22b | |||
f194d00366 | |||
b7544cccc6 | |||
894b098eb3 | |||
022ad4a420 | |||
a4f9e8180b | |||
e86b64b620 | |||
90e9c59e23 | |||
ca06af40f4 | |||
6091a954cc | |||
d27fca9301 | |||
5c89d6bffa | |||
3e6a86fb0a | |||
a7ebf5aadd | |||
b42921bbd2 | |||
722dec11b0 | |||
9e6268ba59 | |||
435f6eecd2 | |||
1c1cbba04b | |||
3b692a55a7 | |||
69a0578e00 | |||
b5ca275590 | |||
519f022b02 | |||
236a9320df | |||
28ac24444f | |||
99909bbf2c | |||
ee60bb5b36 | |||
f6120c09e7 | |||
e2bdef4cf6 | |||
8115edc82f | |||
a8b5465e24 | |||
9ce495b3d8 | |||
d40263447d | |||
31c5c1060a | |||
c9ebd60435 | |||
5a14e2238f | |||
3ceee99e22 | |||
28b23f954c | |||
ad17e5e791 | |||
c30d329faa | |||
991300b86c | |||
7c45db3a19 | |||
67cf11d071 | |||
49082d7ab2 | |||
6b627f67db | |||
5c320b4c2a | |||
ac2b04a5ab | |||
a63b764b54 | |||
2654357c72 | |||
4ec40c6ab2 | |||
80d424798e | |||
7fa2d4b503 | |||
f4845fae12 | |||
f693be3996 | |||
a73d5308e0 | |||
e1bf067090 | |||
884de18cba | |||
dfa2fb95d5 | |||
2639b4bffb | |||
978f97cc59 | |||
f1a063298e | |||
d241532488 | |||
f755db78dc | |||
5dd2b5135d | |||
7ac34e42a0 | |||
029dbf0e18 | |||
bba65e0f41 | |||
a069e08354 | |||
03d93c96a3 | |||
020338230f | |||
a1d86daa71 | |||
7078fbffb4 | |||
0aa9b46b79 | |||
831592c381 | |||
f628797d91 | |||
47f51c2ead | |||
ba9cd5bbc4 | |||
b54ad053f9 | |||
5b8eb9c5c7 | |||
0b683123d2 | |||
363498b6b4 | |||
a1bb56f739 | |||
5bb9fcad3e | |||
f4697f351e | |||
1d571b299d | |||
3a0b5a928c | |||
265ac8a106 | |||
fa7d8907d0 | |||
0220ce7002 | |||
3bd0b2ab28 | |||
a589ca0adb | |||
72f8abd7b3 | |||
20a900b648 | |||
6435ecd3c6 | |||
16d1700a8e | |||
b75cf3f70b | |||
4f19491fec | |||
8f36fd1374 | |||
2de0d4c1db | |||
5e4af7c550 | |||
8ec21fc325 | |||
eb48750705 | |||
be59c3a98c | |||
b333919722 | |||
235a235fab | |||
2d5e7d1b52 | |||
647b8595d0 | |||
0a1a397cd7 | |||
7f9b1b78f6 | |||
1e9484673d | |||
88bec238ac | |||
62e7b9da1e | |||
61341b2791 | |||
92a5876f51 | |||
a57df4ee20 | |||
92d7060cb0 | |||
7e9b120452 | |||
b081dfe705 | |||
4a4d749710 | |||
c878d55397 | |||
263a2eca88 | |||
44154e71fd | |||
0b2f7d13d0 | |||
420cc7afc6 | |||
5fc77c90cb | |||
c3484450b8 | |||
fbef94a8ee | |||
aa456edafc | |||
7007f51c35 | |||
bc1e22922a | |||
cf8d512e43 | |||
0b1f5d2127 | |||
dcf64a0d01 | |||
a9545aba4d | |||
d9ae70c699 | |||
a751649c8d | |||
3f5a3d6ea1 | |||
10a014d89e | |||
8feb8e5408 | |||
16dada28f5 | |||
67cf7128ae | |||
16e5b866d2 | |||
83d43ac850 | |||
cd25939be9 | |||
b58c3527e9 | |||
efc67ee5ef | |||
7a406a3896 | |||
98001a065d | |||
e442881ead | |||
b37cee36f9 | |||
e56de1025a | |||
64ae6d206e | |||
54a14312d1 | |||
7e95802cc1 | |||
e3e7044d06 | |||
eb3bfc25be | |||
94d769de71 | |||
66191e8a37 | |||
bec188506c | |||
4f869ff755 | |||
8f6047340e | |||
9744a1c966 | |||
0bcfae7cac | |||
140e7c00d1 | |||
941e88ff79 | |||
71ea931df5 | |||
545fdf10e2 | |||
7e928db204 | |||
cd4c0eab94 | |||
5b06069fd9 | |||
d0f3162e84 | |||
81537cb161 | |||
370ab66c4f | |||
2707012181 | |||
4d62be69c5 | |||
7e51e52f55 | |||
e81606c97a | |||
f791e9f081 | |||
3aa7e0228a | |||
9d3326caa7 | |||
1940b18124 | |||
0846784b98 | |||
0d10b9002e | |||
0c9ec37e26 | |||
9a0700f5bd | |||
ae7bc2238d | |||
5df626bbe1 | |||
5a624fa1be | |||
3a86940ca5 | |||
7b120b5f73 | |||
de25d1886e | |||
d77444b88a | |||
240aed29e0 | |||
bf29936af9 | |||
339ca83f9d | |||
447783e575 | |||
743d8bc845 | |||
f816666ede | |||
d3c2aa5f95 | |||
3cc1d76ee7 | |||
124283982b | |||
65cf1add97 | |||
8b14488827 | |||
f9fa157a09 | |||
eb8ddd2983 | |||
1aa2947f70 | |||
61abba4bed | |||
5da72cc465 | |||
3db75b3f64 | |||
db3e65fb17 | |||
7a20691f13 | |||
ccd0298ec9 | |||
eeab433c8d | |||
c83c4168ca | |||
06d6c76192 | |||
9dca5f2743 | |||
81306c1f61 | |||
0365592119 | |||
407b5cf408 | |||
4c1743cce3 | |||
7305e8b45e | |||
285dd6be34 | |||
b10540a0b5 | |||
b62739a989 | |||
bb577c624b | |||
02483a01ad | |||
120bdeecdc | |||
8dff9d84ed | |||
b4cd27979b | |||
11b12670b2 | |||
dd48df105b | |||
18174e5564 | |||
72265f796f | |||
6e8bc310f0 | |||
c40ae7f7cf | |||
5bd93b1f0f | |||
a1cc02f0bd | |||
5778bb820a | |||
87754ad5ec | |||
d364117aa8 | |||
6245637e81 | |||
ab69f12e2c | |||
1278cca883 | |||
ede9cb7c2f | |||
c3fb820473 | |||
2af19c96f2 | |||
dcca799dbb | |||
d3d3f7191a | |||
e0b31dbfef | |||
8df56fe93a | |||
676d9c2c4b | |||
2b68e8d98a | |||
d964491f2a | |||
e6080527c6 | |||
3b7bab7d22 | |||
108fa15792 | |||
1b2271a3b1 | |||
f9381e42de | |||
bbb8f386f1 | |||
08aa54e1d9 | |||
fac4d8d42a | |||
170885c51b | |||
5713faa667 | |||
23596b3f30 | |||
f8fa20d71a | |||
d6d8fe829a | |||
5269ce287e | |||
3e03dbe576 | |||
33b338120c | |||
811679a583 | |||
2c33d17609 | |||
9c99e6a838 | |||
98174758ad | |||
97b928053d | |||
1fe55e252c | |||
53ed4b4648 | |||
d08785d2e1 | |||
c24533eb51 | |||
4b1aadee94 | |||
e4b11b2de6 | |||
92ddd46bcc | |||
bf5f50d9ee | |||
753622414a | |||
c6ecb638a0 | |||
5f681f9745 | |||
c7c5214029 | |||
12f801e3dc | |||
925e654a29 | |||
d0e086a5fe | |||
15c2a93f14 | |||
8522546a8a | |||
1a75934cc0 | |||
1e9cd95f5c | |||
acf381bcb7 | |||
cb5090cdc8 | |||
4b70bcd200 | |||
8baff1858b | |||
f11b2fb00b | |||
01cef016ee | |||
43d1b9864f | |||
f9a6a94be4 | |||
dd8679037e | |||
e82812b9a9 | |||
c65634215b | |||
0ad02de47e | |||
6472661ae8 | |||
c5586b7dfa | |||
95fbb7d675 | |||
a64af40c0b | |||
2015ccd1f5 | |||
7d49443060 | |||
86d9612230 | |||
8c51c276c7 | |||
6c51665c82 | |||
d39d1ce412 | |||
88045a5050 | |||
0b38a039d0 | |||
a0dc0a2f46 | |||
6397062e22 | |||
e18f1de003 | |||
21e37e47c6 | |||
8e8924ac7c | |||
b2e902deb4 | |||
97b18b2a5c | |||
135a2822ea | |||
1104d17252 | |||
a1492a73ce | |||
c8a1a14b87 | |||
64d16dee02 | |||
ce8b5877e2 | |||
44a4b1680f | |||
4596b9d0df | |||
81e87095b4 | |||
bf248792eb | |||
1ce46b56f8 | |||
d07760f5e3 | |||
2b4f3004ac | |||
7f93aad836 | |||
879756d44c | |||
2cf9d6d75b | |||
af2a843446 | |||
02377335ec | |||
e79e98ab44 | |||
52cfe3952a | |||
f74130c9f7 | |||
d3d9aac4e9 | |||
1ccc3242f1 | |||
47b7898697 | |||
0eabd07f3a | |||
8503cc1db7 | |||
8b46b3893f | |||
bb3b93c6e6 | |||
a8fe0cb733 | |||
030c68df32 | |||
6e8cf63b8c | |||
d3bf54bdeb | |||
7e3f9a482a | |||
fb4d84d5b8 | |||
efe545a878 | |||
3bcc0e6f76 | |||
54bf179888 | |||
d6ba9f9fb4 | |||
7c414fc746 | |||
43e1520260 | |||
2d44a2ab0d | |||
c5c6d84fe6 | |||
246de65140 | |||
3e3c22f507 | |||
f6db9521ab | |||
05208b8513 | |||
bb62458566 | |||
cffa0fe734 | |||
9b5a485243 | |||
63478d7a3c | |||
50ca01e255 | |||
4f3149242f | |||
6af3672185 | |||
9b84a325ff | |||
00300f66e8 | |||
5eaaac35a8 | |||
16232f000f | |||
9f43f5f09e | |||
a6d41c47a9 | |||
c0080d76c4 | |||
c6eb9eebcc | |||
456f57dde8 | |||
f82013f956 | |||
53f91189a1 | |||
e64b1e99c2 | |||
6be9c0466c | |||
a33ff2c68f | |||
92984ba794 | |||
137b9c784c | |||
85013ad160 | |||
8be11e49e7 | |||
a0a73fe030 | |||
7c9ba2d6f2 | |||
2ad92ed254 | |||
e3e2fc0c3b | |||
7493b8ae10 | |||
c4f02e21dd | |||
f15ca6ce50 | |||
0de219adcd | |||
ff6047cb58 | |||
c6b2f07fd7 | |||
a76ab1f42a | |||
395109817b | |||
72246d48db | |||
d2aa8acbe0 | |||
ef956a20c3 | |||
fc3e7e0381 | |||
908129e9a2 | |||
04dd9713f0 | |||
48d579b2ae | |||
3db91ffd96 | |||
229b76cfde | |||
80a5b91575 | |||
9728dce048 | |||
463e2872a6 | |||
fa03ae14b0 | |||
27196b676b | |||
6040ee39eb | |||
74dbf7bad5 | |||
7269fe5203 | |||
8e80db4982 | |||
e99475260f | |||
b15d50af9b | |||
db55e86e91 | |||
a2f3f4550d | |||
d2cfc6a719 | |||
fecf768f43 | |||
2fc4cf67be | |||
f9436f7a5b | |||
e726d81822 | |||
6cd4b1b41c | |||
24df08efc7 | |||
4f1a571ba6 | |||
c8d2e0a3bc | |||
b5934fc582 | |||
2402224b4e | |||
03d16fcd24 | |||
dcc3eba962 | |||
6588ca8a6c | |||
81823e89cd | |||
ef78538a06 | |||
ccea37256e | |||
cdc66f6164 | |||
5bceb89a7f | |||
2582eca265 | |||
3e47ea27f5 | |||
d365077dfa | |||
bc7a6d7b00 | |||
9f538a6cac | |||
3750ea9dff | |||
afd89ed8d9 | |||
a62371c0eb | |||
c516bc3b35 | |||
8bf1305490 | |||
d8abf70f1f | |||
efd9c09456 | |||
a66cd526c3 | |||
a0ffdf1ef2 | |||
83d207d0a7 | |||
83c1383701 | |||
c66283ad66 | |||
ae97920fe2 | |||
533a010b28 | |||
6b81d1c9b9 | |||
fefc081e1b | |||
bdee1f4a25 | |||
40dfe39e64 | |||
3d50fd7cac | |||
cc1058f6e1 | |||
47e251a80a | |||
47bcb5bc35 | |||
f3fc74ab67 | |||
86a36eaadd | |||
5ba1cf1063 | |||
07b81ae741 | |||
5a7bf36723 | |||
66528a21f6 | |||
a77757277b | |||
4f05d022c1 | |||
5df343169e | |||
764fea1344 | |||
bbdea96a66 | |||
d1de587ce0 | |||
147aec43bd | |||
1f5049f30c | |||
09e3839994 | |||
19eeba2281 | |||
6cc8f2298e | |||
83b27bac17 | |||
b462f49ce7 | |||
1a9064ba2b | |||
5bc869cb24 | |||
8fdb1e09c1 | |||
f3d38ce053 | |||
05953b3b83 | |||
f75296e04e | |||
0867e85163 | |||
93b00cceb6 | |||
0fa818b318 | |||
bc66d27938 | |||
6a5818454f | |||
27fc458ef6 | |||
0487a9f140 | |||
871ece6123 | |||
abca7c0243 | |||
1f5256e745 | |||
6990354047 | |||
135ead6c97 | |||
33c0ee3441 | |||
5efea2f6a0 | |||
5f23a1223f | |||
30208759cd | |||
e4c53f8529 | |||
b61e3e9d20 | |||
f593552cfe | |||
8458647232 | |||
a693c5614c | |||
8ceffd8b48 | |||
2986e25abb | |||
f8fe53aeb0 | |||
74e3115686 | |||
3846f19f22 | |||
057513536b | |||
82bcd83566 | |||
e48f477477 | |||
20e1cc049f | |||
0b2d636b75 | |||
f5bb999319 | |||
a4742763b9 | |||
05ff6c09ca | |||
d91ff17adc | |||
d213a20dfc | |||
2e7e935b02 | |||
b89e7c2cb7 | |||
b4db2e25d6 | |||
a33eaf6e07 | |||
0d47c39609 | |||
cbe7e39bbe | |||
6d57cb04f6 | |||
6e2a8a2ba4 | |||
7874697b6c | |||
767141761a | |||
b3eb1db6dd | |||
ee0dab025b | |||
b7738e1fe5 | |||
634d33f5dd | |||
3401283399 | |||
981947d104 | |||
8c52088346 | |||
add3589451 | |||
81d497ce1f | |||
70cd124ede | |||
7363b3d4b5 | |||
f05937db4d | |||
d684f55423 | |||
db06cb170f | |||
77a1f9f2e8 | |||
13e663c232 | |||
d098cf5a8b | |||
3ce3b4d2af | |||
e7d9cb3e4c | |||
e544742156 | |||
c9ad529afc | |||
75e468494c | |||
ddada6e2be | |||
22ae17bb0b | |||
d546be48e1 | |||
753a130aaa | |||
94e2ea7361 | |||
1539cd8819 | |||
131c8ab6be | |||
7d81309e11 | |||
225baf4686 | |||
70b061be2e | |||
46aa0a1cf6 | |||
661fdcd3e2 | |||
590d93b30d | |||
c26e1bba1d | |||
10771d0bd8 | |||
d8cc09b76c | |||
d41d2c460a | |||
4efc32dabf | |||
ef534c0cc1 | |||
073f485c72 | |||
c2dbc55f11 | |||
9ee2703824 | |||
b78ada198a | |||
6790e02a13 | |||
7cabaa0ae7 | |||
da3563ce19 | |||
9bbec42a6c | |||
be994496cd | |||
77ef527993 | |||
f092a7c824 | |||
5e25d3986d | |||
35977e3830 | |||
f7328c69b3 | |||
25f2211726 | |||
18793c896b | |||
e7cdb9f660 | |||
6911a250ef | |||
7e7ff2e0aa | |||
d34f0bf573 | |||
1f5fa25583 | |||
d507057476 | |||
f582620d5b | |||
3fbcde9048 | |||
f841fbe60f | |||
b9a91a5e74 | |||
65f4fad801 | |||
60a30818ef | |||
b967cbfc66 | |||
8826a8235b | |||
47addd169d | |||
ba6af2a6dd | |||
b9e4d62d5a | |||
71e5de646b | |||
3def2cc552 | |||
4ec4a99f16 | |||
3203069d6c | |||
54bfe14313 | |||
ba850b36de | |||
f3c5481181 | |||
48300067fb | |||
b841e0d530 | |||
65a2cb8307 | |||
0bef021321 | |||
aafa75da84 | |||
503be69af6 | |||
eb01ad583f | |||
15a54df7d3 | |||
eaaae2edf4 | |||
c2b3792a3b | |||
b2a586cee1 | |||
8bb42df47e | |||
add5953aa1 | |||
6b4c24020d | |||
24bf3e2a25 | |||
8ecda94899 | |||
1366762d12 | |||
dd6237ecd9 | |||
6e83204238 | |||
a53a040071 | |||
5bef070e16 | |||
89de98b25e | |||
de78307928 | |||
6293ca23c3 | |||
330bb2a360 | |||
59455ea8d1 | |||
e9e4ffd6e1 | |||
0444e13efb | |||
ffb6dbeefe | |||
0e012c9669 | |||
a0819d3af1 | |||
ffd8c361eb | |||
1594f8c09e | |||
d7a727cc07 | |||
437a0446e2 | |||
ac93f1235e | |||
e2b76bb386 | |||
ae0275e2dd | |||
ecce90718b | |||
c2cb475a2c | |||
7cf5e95ac9 | |||
a740e4f00a | |||
2a9d2bacd5 | |||
4ed04392d3 | |||
23ab83b504 | |||
9332161e01 | |||
816d5ba3fd | |||
69c53c3e03 | |||
365712e2f0 | |||
997336b790 | |||
1861e416a1 | |||
7c44637fbf | |||
68b53c07fd | |||
368cb5ad4e | |||
949836d003 | |||
697fb76960 | |||
0fedb57cb0 | |||
717ac5ac4d | |||
4064cbe945 | |||
a88306d671 | |||
c702ffc471 | |||
dcfffbf828 | |||
336041aac9 | |||
7e38f4fd1f | |||
c28b52187a | |||
5ec1717c58 | |||
c2a24b4241 | |||
a9f3e2bd95 | |||
f8658cdc38 | |||
043e408805 | |||
c004d483ab | |||
2586846ee2 | |||
d47b2a6f70 | |||
1adbcda12e | |||
12af6d356e | |||
20aafff092 | |||
a622e19df6 | |||
1db7c0d139 | |||
b9bd3204f2 | |||
bf651a504f | |||
e5c4371d72 | |||
3caae94261 | |||
e7a2b31472 | |||
c03186013c | |||
5a2531ee45 | |||
6ca780178c | |||
9608b0636d | |||
89187d9b6b | |||
335b72f301 | |||
74071210eb | |||
fde966832b | |||
75d474e1d3 | |||
24cf8b3269 | |||
743651f5e8 | |||
161f88fe6f | |||
c33a57666b | |||
cf618c564c | |||
401ead07b8 | |||
b55c2ba342 | |||
d8db0f12a2 | |||
eb8013e853 | |||
fb4b90a564 | |||
7830d74615 | |||
8e24c0fff4 | |||
cf0444b731 | |||
a7bbe9a1ff | |||
ffe323036e | |||
b4a39f9c30 | |||
3257fcdcee | |||
9bcd7097d0 | |||
c32f5fd393 | |||
78ba39bfe2 | |||
119034c642 | |||
6e8e3bd248 | |||
a460066972 | |||
05d96dc507 | |||
b489259a34 | |||
6b748835be | |||
d30ea61f0d | |||
0c47ea704e | |||
049c89645b | |||
bf22f2df88 | |||
880201681f | |||
63d26a1777 | |||
8b50ed083c | |||
3997d97806 | |||
200d92d030 | |||
dbec3ca716 | |||
f7c9b941cb | |||
f0764016f9 | |||
a99eb16320 | |||
e36bac9e90 | |||
196ce6d475 | |||
faa621218e | |||
169cedd43b | |||
567cc26b8e | |||
1d19d61970 | |||
03f080b7da | |||
26f82995f6 | |||
f1da1419fa | |||
5079d2d37c | |||
c7fd172ba7 | |||
dcf8840831 | |||
60c0b178af | |||
0899f4f8fc | |||
aed4a11d01 | |||
75cf70ae04 | |||
6b30fbf94e | |||
24f17f913a | |||
ebfa204af0 | |||
a28d616e10 | |||
613a9e3672 | |||
65d57a07e0 | |||
22946cfd40 | |||
9975486954 | |||
068348e9b1 | |||
1beab0da6a | |||
3a03ff6b2d | |||
feae55b264 | |||
0355142737 | |||
5b16ce9302 | |||
17ed14faea | |||
d156e72ad7 | |||
7186c9c839 | |||
a41558eb30 | |||
a91252a90c | |||
132c0719dc | |||
3db7112b89 | |||
2ea76cce31 | |||
b8bb2dd0f5 | |||
27ae0f9475 | |||
171dceb010 | |||
1ebc0d1e33 | |||
1d8e0758fa | |||
cd55643f85 | |||
486b8e6f69 | |||
215d373ebd | |||
4038b42396 | |||
9ab1f4a9c9 | |||
e9afc59a81 | |||
83c826c3f9 | |||
abfc41d661 | |||
11fd7eaf63 | |||
54eba606cb | |||
3337865913 | |||
f24397c5d0 | |||
9d52bf27de | |||
19fbfbc371 | |||
6578b30b77 | |||
10c1c2ba5a | |||
e18bfd1f3d | |||
3c2ddbe9ab | |||
a149605c9f | |||
b396029d39 | |||
040b376052 | |||
175c872514 | |||
71291aa2c0 | |||
415e75716a | |||
3216abee2e | |||
327ad02a28 | |||
1df0c9e1b0 | |||
280dadaaad | |||
c30eff898a | |||
ca129ba549 | |||
171ae154c2 | |||
4426c0f14e | |||
901436e46f | |||
6fbc2b3be0 | |||
93d23ddcc8 | |||
548a809a05 | |||
a161b4ab6d | |||
60b91656cd | |||
1858d99559 | |||
3a8665409d | |||
005a78bd83 | |||
7553ce9dfe | |||
42bfe4477f | |||
3bdbb18c8b | |||
13f8648a00 | |||
b6abcb2500 | |||
f1a9e1e361 | |||
54480f7dfc | |||
951bd33b09 | |||
bf57df3e04 | |||
420852e2f5 | |||
04eb80cc2b | |||
308fc8e328 | |||
9bb2939d68 | |||
2f63899be2 | |||
22c66f0e02 | |||
00bc80bb37 | |||
394d951883 | |||
98b26381f6 | |||
31797d3b50 | |||
957be960d2 | |||
18e9d86a3b | |||
a869aeecd2 | |||
eca822b756 | |||
17142a778a | |||
5adb7c9669 | |||
703fcda590 | |||
f16a7cd7e3 | |||
fde5f2fa14 | |||
d56724659f | |||
56b18ff063 | |||
7bfeac746e | |||
c92efc15fb | |||
b51d57deb8 | |||
c357b40dca | |||
a0ae120093 | |||
d3211a2468 | |||
adab4f3e49 | |||
82fed62af2 | |||
bde57016c6 | |||
b16f4bce98 | |||
6bed189e37 | |||
4abacb58f1 | |||
04200150d5 | |||
fc0b1d5b61 | |||
8d45fefc31 | |||
5da96c75a2 | |||
90d1423fb4 | |||
910735d732 | |||
fc86352adf | |||
441e01c568 | |||
14380ff086 | |||
820bb7bd8c | |||
230b98d4dd | |||
065ea926c0 | |||
9b9820858e | |||
b922743f6e | |||
04ab9f1917 | |||
25cbc98979 | |||
3861ba2929 | |||
3bcf0cf472 | |||
396c2417d9 | |||
fcfb1544e8 | |||
56774dfb79 | |||
c0cc6eeca1 | |||
6f2939da62 | |||
7d1abd9adb | |||
81173b0d29 | |||
b0c7ea8181 | |||
30ecb6e88a | |||
8d735da5d8 | |||
5a9ed2de27 | |||
41f57affb6 | |||
c569b75249 | |||
01e4aa5427 | |||
ad130d62d8 | |||
621f87b2bd | |||
64b36190de | |||
507290d30d | |||
15a8429b96 | |||
9723a362b6 | |||
d75a9fabdc | |||
9b264c5c78 | |||
60bdcd6f5f | |||
d035175cdb | |||
f42d317d2f | |||
405ccc7195 | |||
836c889baa | |||
43f9d917d9 | |||
62c7b7842b | |||
97969a85cd | |||
91fcfcb042 | |||
88c46feb20 | |||
c3f07b329f | |||
6121083ba5 | |||
717c68089d | |||
09b4244baf | |||
2e45267705 | |||
e81d1fc361 | |||
02394d2d80 | |||
653a211743 | |||
522ec9a25b | |||
0fdb7f7181 | |||
443ff33f63 | |||
6b7cead0c5 | |||
94a925a1b0 | |||
055b802713 | |||
41fec319c8 | |||
a22121d65d | |||
3acf9c7063 | |||
566606ef46 | |||
afa2bb246b | |||
dc038113a5 | |||
ad7e781a18 | |||
437e803f27 | |||
dbe6cdad7e | |||
46992b4bda | |||
d0af45c31a | |||
8b571309ed | |||
b0befd7376 | |||
dfa0973563 | |||
ac50bd678e | |||
9130505b57 | |||
931cf78057 | |||
6ade68cff1 | |||
d30ce19231 | |||
d7eac7ee56 | |||
d53b96f2a2 | |||
79deeacbd7 | |||
32ca6851af | |||
cff8c05442 | |||
0ea5f8b5ed | |||
555b1cdf29 | |||
0b0d25fa33 | |||
963a4d0dc8 | |||
f393d86c96 | |||
6651541230 | |||
43c5b638b9 | |||
c3a52697f5 | |||
c4704c8abc | |||
b1ca5d4ddf | |||
9fecd72f44 | |||
f4480d46b4 | |||
8e5b582b61 | |||
0cc87c5ba1 | |||
6aa7cc1d96 | |||
a8920eb774 | |||
fdfa31798b | |||
b67d574a95 | |||
1c77cdadaf | |||
f12e15e682 | |||
d5c9c5f183 | |||
f83989bb0d | |||
81167d9c4a | |||
0b06ea177a | |||
03227e65cf | |||
ca7f2f8c8f | |||
340d94afb9 | |||
9ef8d8b85a | |||
ab8ee4d0d9 | |||
d1a00459a8 | |||
9fe6363575 | |||
142a2b7341 | |||
6586265a0c | |||
d3fd088162 | |||
4b3f82a47e | |||
1279d75f7f | |||
81e9bdc010 | |||
6fa0929f3f | |||
f10f8db5fb | |||
b804d4bde5 | |||
01f711281c | |||
5b5108363d | |||
c80e02b881 | |||
fbc9537952 | |||
f580ffab4f | |||
696af79dc7 | |||
0833b59aab | |||
caa51950e8 | |||
f3f4c3d835 | |||
9ae3742565 | |||
0f432994b8 | |||
d4d9009696 | |||
7c5ecb5b5c | |||
1058b2a778 | |||
c4129137e7 | |||
e3a4ecef35 | |||
9783c92935 | |||
7cecaf2a4f | |||
eef7d8aa11 | |||
617b3d2ba5 | |||
546eb7d851 | |||
29dbc3b67c | |||
4e6aa9c2db | |||
745b59f49c | |||
b0868915ae | |||
0038712474 | |||
db74f44a97 | |||
30d5a2ca83 | |||
6d3487f1c1 | |||
d442b6855f | |||
7c1d3e0f5a | |||
2f6ae527d1 | |||
ec2be5dccb | |||
14e8e88022 | |||
6a9ce67714 | |||
bed8ac70f7 | |||
a9d32a3f89 | |||
ff5b050a92 | |||
b3db3f80ba | |||
2b84b86fc0 | |||
3aa3d5c548 | |||
2d2300e118 | |||
f24ea59f74 | |||
c1b029a413 | |||
82e49230ff | |||
f7199aa8c8 | |||
0e00265647 | |||
5406a49f06 | |||
8dbe2af0bc | |||
334caf8e7c | |||
8d8995f1ab | |||
48c6ece2be | |||
f7c7038171 | |||
627f04883a | |||
a7798f2a93 | |||
b21a1d1961 | |||
8a0e45826a | |||
dfb8d21ef4 | |||
a75040d0a1 | |||
86ffacf7ce | |||
b54368bf35 | |||
e889c68aff | |||
2e1fec17f5 | |||
f9b0863c8a | |||
f57b7df4d7 | |||
2cf52c1b55 | |||
27c6638913 | |||
3f100eb23a | |||
ffceae0a01 | |||
7a965dc58f | |||
bb1665cbd8 | |||
1dacae2c3c | |||
e31a76ce24 | |||
3799f43c71 | |||
a8a9660112 | |||
13613d4acb | |||
9ad4b3bd31 | |||
e224e3d62d | |||
476790290e | |||
68078fd620 | |||
620407720c | |||
adb0b761f1 | |||
97e02c2fa0 | |||
4ae546be1f | |||
0f5c70d563 | |||
f48b343af1 | |||
213baa37b0 | |||
8959d7d3a7 | |||
516759b1ff | |||
aaf826e043 | |||
c6adddf02e | |||
11b5d1b286 | |||
12f77ca523 | |||
9bbf009dff | |||
344a5ca545 | |||
6e1896b333 | |||
cfd9ca0d6f | |||
f96142cd7c | |||
4c73b52d5c | |||
48cd503d4a | |||
ba20e3bed0 | |||
6f7aa466ae | |||
e6c612f5cd | |||
4530654501 | |||
28a80e6e05 | |||
abceaa2f33 | |||
f2bad195bc | |||
1a647c399b | |||
c76da27240 | |||
04997c8418 | |||
45747ed531 | |||
62602b9bd8 | |||
b826e0c636 | |||
4586fccae4 | |||
9e847198f5 | |||
2c4107c531 | |||
34fdb91899 | |||
473a577e34 | |||
b14c2d1568 | |||
549f2254b4 | |||
17eaef0311 | |||
bc0750eb93 | |||
381e680758 | |||
988b9f8378 | |||
a5ee653da0 | |||
580372f23d | |||
5751865228 | |||
23f2bea353 | |||
79c7fe05c6 | |||
15e8d50313 | |||
6665d76fbb | |||
ad7251c8bb | |||
8f95b751e0 | |||
edd5f5a333 | |||
9d2236a4b5 | |||
e17128e7cb | |||
b6b18c1d7f | |||
2373186239 | |||
b6431c60e6 | |||
0c44e733ad | |||
996c7c2dde | |||
554fe65690 | |||
4695c69cf1 | |||
42c69d3ba6 | |||
0fb2ba577e | |||
9b571a1deb | |||
1aa8401ddf | |||
bf94f878bc | |||
c8f742e288 | |||
97cc6caa33 | |||
831613aab5 | |||
9ab9437319 | |||
867ab11def | |||
5cd0d6ab25 | |||
a2b50ec8c9 | |||
77d24afea3 | |||
39386eadea | |||
ca5aebaa6b | |||
112e777b90 | |||
a176cd646c | |||
07153f7e7f | |||
bd928f3dcc | |||
e78402dd4c | |||
626555c013 | |||
a9ef858359 | |||
8f7915022c | |||
4a0466e574 | |||
917d0870e8 | |||
290b0b48a9 | |||
a69172f6ce | |||
b1055a5edb | |||
5ef6e6366f | |||
b6833d1bbd | |||
865b48d78b | |||
818f6dbe9e | |||
c056b8c7f1 | |||
3c480e4e63 | |||
29762f1b6a | |||
d2707f1457 | |||
8dfc3c386a | |||
15945c8791 | |||
66f0ab0371 | |||
dce36751f5 | |||
1a9d382da9 | |||
b56dd32454 | |||
6ac75a0674 | |||
ab838920a1 | |||
71b58769af | |||
a95e03ae85 | |||
7d8036a2e3 | |||
66a5dab85a | |||
d52f42688a | |||
ed1175f27e | |||
35bc1eb218 | |||
dc7de0896c | |||
5d70c6e34f | |||
53be85a7fb | |||
fa6b802be4 | |||
54c1784da7 | |||
7f4f20b249 | |||
6ab4966d44 | |||
068feccc37 | |||
77a8492405 | |||
3c4eef8a86 | |||
36d37cc6ae | |||
185a941acc | |||
6af2396cf7 | |||
d1afadbd22 | |||
450a13dea0 | |||
22c409029c | |||
590b8052fe | |||
fef3d2ad53 | |||
56238fe94e | |||
fcadeb2079 | |||
fd701b07f0 | |||
ee04217d53 | |||
a1293b26ef | |||
667473c32d | |||
fce7ae16f5 | |||
4059a72710 | |||
f1e526f046 | |||
83e5deb988 | |||
56b751ead5 | |||
1706382f4c | |||
93d73fd6c2 | |||
c679a6fb7b | |||
39cd56038f | |||
f544128138 | |||
57abe8d078 | |||
cf7d47dda0 | |||
0e64261f26 | |||
c7e1bda32f | |||
3a6d270bb8 | |||
c2136d18bd | |||
9f5996f1ab | |||
ba04de6508 | |||
1953e2af7a | |||
06d01b2287 | |||
c685cc2f0a | |||
2f2d5f35bd | |||
30d53a8942 | |||
043f104738 | |||
4ec5e282d9 | |||
4fa6ae82ee | |||
e03559867b | |||
60935b29fc | |||
452a7ae88b | |||
1b1d5f10a1 | |||
65e26d713c | |||
fdd5010832 | |||
946e5bda22 | |||
24faaf44cb | |||
90bd984ff7 | |||
48789360b1 | |||
4c5aac8cd5 | |||
535c7bb60c | |||
6970396c01 | |||
832876d0a1 | |||
4c45347635 | |||
72c7b6edea | |||
1ef558b57c | |||
506d2e98a4 | |||
8c858d76dd | |||
f83b819bea | |||
0262e37301 | |||
2fbc92fd2e | |||
c1a4621c8f | |||
d1764fc3dd | |||
2e714f9f2a | |||
6815e4fceb | |||
655b495e8e | |||
92dbaf397b | |||
f0c681a6de | |||
1b9b34ed46 | |||
3d2ee6c3fe | |||
1c2f3d47e9 | |||
f692655ec5 | |||
3db84c8220 | |||
f2f5286020 | |||
47220997e1 | |||
9ffa490d3f | |||
a80ecf6a77 | |||
7236095f6f | |||
d1c4a94bbf | |||
d76761bf01 | |||
c055dc7441 | |||
3dc4115c8b | |||
1f1caacbfd | |||
713d7c2360 | |||
079d884b6c | |||
a73389bc71 | |||
33d250ffaa | |||
409688fe17 | |||
ec56760c9b | |||
7ce9e06dab | |||
8ea6c56fe1 | |||
c0ba2b9ca7 | |||
fc7c858e62 | |||
55d151a82d | |||
5a1b9a34df | |||
70628112e8 | |||
29aa8b33df | |||
e25f05ae7c | |||
791c7efe29 | |||
2159342038 | |||
e228f2caa6 | |||
7522987a51 | |||
ff6a20d138 | |||
4eb1f91bee | |||
f53f7241a0 | |||
f2a2a6b478 | |||
d61b9021e0 | |||
499d05ddee | |||
b8a3736275 | |||
7d72d0eb9b | |||
7f4c964eef | |||
be9713c6e2 | |||
596e9f4e04 | |||
da14391cff | |||
60c803649b | |||
17b71ae382 | |||
a56468cf2f | |||
d7f42bfbe6 | |||
8f413268cf | |||
2a62d9f056 | |||
5f5a8e1da6 | |||
6e3498ca8e | |||
7bfd850493 | |||
ffb1553282 | |||
56a5b02d04 | |||
0cc77b4a69 | |||
a0ca01d580 | |||
2da45e629d | |||
845c68fdb3 | |||
21c44672c4 | |||
83713ddea4 | |||
3a500981ef | |||
0d45828460 | |||
43226cb93d | |||
2572bf508f | |||
6a1ab61cce | |||
27d5058e01 | |||
9aa05211ff | |||
40d69c317c | |||
bc22ff1517 | |||
75d484e29d | |||
32ff21c16b | |||
c65f18a2fa | |||
697c6ed0fe | |||
0a73e8d062 | |||
64b4be9670 | |||
77747e10c0 | |||
1cfa79ca4e | |||
cce2ab2625 | |||
38addacda0 | |||
3efd4a15d6 | |||
96b57bfcc1 | |||
a93dece069 | |||
14d8186bf6 | |||
856278cfea | |||
5e840e1e79 | |||
233ef93e88 | |||
9320f34f19 | |||
d2c0d986d4 | |||
088532bf2e | |||
27d901a51d | |||
cac130eff9 | |||
679608db65 | |||
06faac8b5c | |||
4d523fda98 | |||
cea02414b0 | |||
f0ec31e47f | |||
ff5c58be6b | |||
dca50deae4 | |||
7f2037f0b6 | |||
fd6ae571b8 | |||
b14250bef9 | |||
6fb5250185 | |||
2f9d8ff46d | |||
e54bd59f22 | |||
1e1833198d | |||
6f2038cc85 | |||
f0a55016af | |||
fcadbf4bf6 | |||
d9d00bd9b5 | |||
f69561b2de | |||
685cc26ab2 | |||
5b7432b6ea | |||
04b18a9f46 | |||
05472cb21b | |||
c0c03dc4ba | |||
f9f8924c49 | |||
10897d6473 | |||
340837aa46 | |||
42ef1be75c | |||
a5801b6020 | |||
70b62949de | |||
36161d99f6 | |||
0714139e37 | |||
bcb36d9b6d | |||
ca695e0632 | |||
9b015a95eb | |||
939dc44391 | |||
5651e4ac72 | |||
1dca575701 | |||
333a708bb6 | |||
3a227a1f6f | |||
81cb5bc3a7 | |||
1640d2aa0b | |||
5f501c722b | |||
ea07856cc5 | |||
7c47b62a96 | |||
e25b3dd163 | |||
6a88659c9a | |||
44ae6e94e3 | |||
1635a06bda | |||
3923c30df0 | |||
99017bf3ff | |||
4d117faf1a | |||
5cc9913ded | |||
1d09838622 | |||
9adf40aa77 | |||
89c616199f | |||
1e1af7ffcb | |||
a84b2bc945 | |||
7abcb99d57 | |||
49cd8513e4 | |||
82b067fc40 | |||
9479a106bb | |||
e64b54b67b | |||
cc2a4c41f9 | |||
a11542a375 | |||
b6c4af6495 | |||
67dff7bd5d | |||
381471d338 | |||
ebef5e697a | |||
d71ae278ef | |||
46207538ef | |||
71eb7437b6 | |||
b5ffbe342b | |||
0f79223008 | |||
a085223331 | |||
c383048259 | |||
b18eb04b46 | |||
c8c2ab012a | |||
ecff8e6c93 | |||
51f1da1b85 | |||
a5e18c4cdf | |||
cf6284656f | |||
3182ddaf3e | |||
416ed691e5 | |||
0fb7484d51 | |||
6a3454e81e | |||
c3fbe87012 | |||
24117d7a49 | |||
5808153359 | |||
9030c8a03e | |||
b14fc06fa2 | |||
a7f2468184 | |||
fae47d86b3 | |||
d20ac14fe2 | |||
cae3e6dca0 | |||
086f4aa72c | |||
82923a381d | |||
5152abb037 | |||
67f7032321 | |||
205abe8140 | |||
b582e2b311 | |||
91ab39cc55 | |||
38ec05f533 | |||
b3085e96c2 | |||
4cea2bd612 | |||
ce47546188 | |||
6279e50d78 | |||
8bcb268140 | |||
6fc5940959 | |||
0317c4c478 | |||
b7a6f52d59 | |||
7ae7573bc8 | |||
abee785821 | |||
619e625ee2 | |||
a6c635e69e | |||
e0a9625e46 | |||
fd0cc01eed | |||
1bfc77bf8c | |||
a094769bca | |||
b4c98305da | |||
a3a54299af | |||
15a3e2d307 | |||
54e0244954 | |||
43c33d5663 | |||
6d7799fce9 | |||
d31dc7b2b3 | |||
4cd4f7a208 | |||
72fe45db2b | |||
8d2819121b | |||
073e8ba2f2 | |||
5d1864fe68 | |||
eaa843b55f | |||
c6cf678a07 | |||
31cb418370 | |||
8de44cf5e3 | |||
c67bad4f43 | |||
410f21c75c | |||
54ea5b6ffd | |||
0af03beaed | |||
d71fa734f5 | |||
6f45519d6f | |||
65c9e13105 | |||
9208f0beea | |||
5344be5182 | |||
5db6f38b73 | |||
d22f8f54db | |||
23146c9201 | |||
a5205c686e | |||
807648251f | |||
5c62e300e1 | |||
256bc8acdd | |||
59c23c7bd7 | |||
e03adb9edd | |||
b399cb26d9 | |||
3b588fe2b0 | |||
95635c18c7 | |||
e20cfe1bbc | |||
eb6fb5f87e | |||
ad3029e786 | |||
2a2fe11e8d | |||
7d0f2cd51e | |||
36faba1aab | |||
92179bcc64 | |||
cdb069ab0e | |||
c453b7bcfa | |||
9d97163c64 | |||
f054c8360b | |||
758848961e | |||
99b666614d | |||
3f331b53b2 | |||
375d598a9f | |||
cd67fced1c | |||
a77cf7ee37 | |||
2150b45954 | |||
9f99f4fae2 | |||
c6ad212a98 | |||
47b3ecd9a3 | |||
8c81c62d46 | |||
7e72317059 | |||
0bb8423df9 | |||
95698d93ad | |||
c649da9f0a | |||
0bf0c35bca | |||
97e6901ded | |||
30e76fcd80 | |||
44b50427d9 | |||
a0b06befb6 | |||
4fbb5b29ea | |||
e0fa727594 | |||
3ecc5e5398 | |||
f7686d4124 | |||
01a2688848 | |||
8e56c3cb30 | |||
7955cacec4 | |||
dd04f09483 | |||
3d85f72652 | |||
9f28e838d3 | |||
ddb766e456 | |||
72143e80da | |||
bc1ea8c54b | |||
45ffe54ae4 | |||
1bf7ba87a0 | |||
db96c963a8 | |||
18559897a0 | |||
ce0f4f0d7c | |||
4f1e4ffa4e | |||
f0beb4d750 | |||
bc3b2ac251 | |||
ffda3e41e0 | |||
a301dba68f | |||
04f3a4a7a5 | |||
f06ce9adc8 | |||
660eec4a23 | |||
be3352a084 | |||
998049ec9b | |||
a7ea0086ee | |||
edb8375a5f | |||
26b9492315 | |||
e110a80caf | |||
20127c1456 | |||
a50d935a48 | |||
7c479f073e | |||
bbf2133fa9 | |||
4300439ab2 | |||
ec14679668 | |||
df06e8b7a4 | |||
1b1a6ba0bb | |||
876ca9ee3a | |||
d9b03be08f | |||
b6aad07634 | |||
3d0406c247 | |||
db3bcc939e | |||
4d45fe6fb5 | |||
076ea2281f | |||
aec39c28d8 | |||
b9525ece77 | |||
719101338a | |||
e131f6bbe8 | |||
a9757ec674 | |||
9003770f02 | |||
e8bbf86e66 | |||
3a0886dc12 | |||
062a7aa2cf | |||
e28f097fc2 | |||
b30c5fc874 | |||
d52ab8e2c9 | |||
df7b875f6c | |||
0e71836cd5 | |||
470a7c6bcd | |||
3abf208235 | |||
92c18d167e | |||
99b38f52cb | |||
633ec30291 | |||
57cfcb0830 | |||
433d479a1e | |||
7c4ac68e66 | |||
20556346a3 | |||
5a417b8514 | |||
8cfc2e2ec0 | |||
11647e4c78 | |||
9e1b61326c | |||
cb7609109d | |||
b3d90365b6 | |||
cc611c93b6 | |||
8928a58796 | |||
05a33d5035 | |||
09f1609f81 | |||
c723d42d0a | |||
9dd550fa1e | |||
abbac4bc69 | |||
671a175dfb | |||
c1474f33be | |||
0ede642cb9 | |||
9c1f6fd06f | |||
798947efa4 | |||
7ae8ad6aab | |||
9c3386b1b7 | |||
2ba3ada27f | |||
8e28382e4a | |||
3203639d7d | |||
f85b543cc1 | |||
c81ad9d19d | |||
37797e2b4e | |||
2a7ebbe982 | |||
72747e5213 | |||
504500de50 | |||
e1174f3774 | |||
6bae73c076 | |||
11db3bd85e | |||
0193be7c9b | |||
2ea73513ea | |||
67e9c62013 | |||
227dbbcfba | |||
cb16e9c747 | |||
3b2d2c467a | |||
ae27af7399 | |||
c69fff15c9 | |||
dd7c1134e3 | |||
b116901400 | |||
70981c601e | |||
f2f61c9cf0 | |||
1bb2476804 | |||
ec58246a1b | |||
6fc5174a13 | |||
105e920b69 | |||
858dea98e5 | |||
71ee0c5b03 | |||
578bdeb522 | |||
6282a86135 | |||
e9b67243ed | |||
fa1c187abc | |||
b51697c197 | |||
c6b75b0823 | |||
0d7e1a9b4e | |||
9d15d85391 | |||
92d7ecf627 | |||
9263da570f | |||
dc88e0a881 | |||
fa34ed8bf3 | |||
f54a901b8d | |||
8a1a989a1c | |||
b479ed9407 | |||
d5dc53ead8 | |||
8e00161601 | |||
01d4eae984 | |||
154154dde2 | |||
4459e0c1c8 | |||
b052ef5f1e | |||
40921bb927 | |||
dfcca66fdc | |||
1ac9085b0a | |||
1cfe67dac4 | |||
8d01db4638 | |||
4268c82898 | |||
3c4eef99be | |||
96b17034e1 | |||
e47a77f941 | |||
af14b1e384 | |||
40f77cb563 | |||
6c1a8daafc | |||
d699c354db | |||
34f3832af9 | |||
f1626574dd | |||
68fc65dbcb | |||
693f79e88a | |||
448d9f9f46 | |||
8786ba95fb | |||
97bb374218 | |||
233044e337 | |||
f365a0f45c | |||
263c1a1d7e | |||
3097083277 | |||
43c187b624 | |||
3165fd3dc9 | |||
e80851d98b | |||
b754e600e3 | |||
81734cf7b6 | |||
30f4fe26e0 | |||
d6265dfcbe | |||
d51f86291f | |||
97ace57d39 | |||
86949e0c20 | |||
6924780ae9 | |||
1b0b69eeec | |||
fa85389f62 | |||
2e55857c82 | |||
ca970f5ee5 | |||
204a2cf942 | |||
0440251919 | |||
08ecfd891d | |||
7395a64668 | |||
979bfd07e1 | |||
b6ce814279 | |||
66088fef1a | |||
808bd4af41 | |||
f90b35a85e | |||
8ae0eec230 | |||
0fe685102f | |||
a98440bb85 | |||
3112311134 | |||
1b13bdea4b | |||
3ce9d51a9c | |||
14d2de13bb | |||
5713e7c9b6 | |||
87206e1986 | |||
414c7e956b | |||
6191d53a78 | |||
7d30ccc4a9 | |||
494a0d064a | |||
849200b576 | |||
60273a941f | |||
7ba720a62a | |||
eacc36bbd5 | |||
8b4acbbcbf | |||
b1fe63d081 | |||
f2ee1dcdb7 | |||
21018af2bf | |||
67ffbae6f9 | |||
5dd5bfde72 | |||
400486ced7 | |||
1a947e4b75 | |||
92bcfefc35 | |||
133b5e6e36 | |||
5c576d3b9d | |||
68b64a261a | |||
68f939ea8c | |||
8c129d73b8 | |||
1fc0d05565 | |||
20a04f9076 | |||
02a38d3ea5 | |||
37cdc4f759 | |||
b1dab181e0 | |||
fd6c4e371b | |||
4352dd27c4 | |||
34cc3f2982 | |||
97fd2480e7 | |||
1d93cf2e85 | |||
3fb98fe4ea | |||
bb804dd3e9 | |||
0034bb28e5 | |||
ca51e020cb | |||
d16852898f | |||
46ddf501a9 | |||
8c89cc4fc5 | |||
00874c27f4 | |||
c59c390cdc | |||
009651e14f | |||
f194f18dbd | |||
4e6be15069 | |||
3e685f98c6 | |||
6c8e7dd63e | |||
2447bd1bac | |||
1c6a252596 | |||
d3c92a307a | |||
8f5836cb14 | |||
319ce182db | |||
aa92f3a721 | |||
afbb6bb797 | |||
4f37f86433 | |||
3093c55e9e | |||
5ac3919259 | |||
f58211d9d8 | |||
fe126cb737 | |||
adc1b129e4 | |||
9315ab88d7 | |||
1ddbddb0db | |||
232bd9395d | |||
956a7e95d7 | |||
ce00fa3627 | |||
3515860b15 | |||
77e717e872 | |||
b46cc744b3 | |||
2cc931ed2a | |||
e096a85874 | |||
2c3e948e61 | |||
53f57d74b8 | |||
2b1de07f02 | |||
fb877696bf | |||
01173b9441 | |||
0564dd25e2 | |||
ba0f6decc3 | |||
06a0cf2e31 | |||
a8afa65a54 | |||
f73a4c229c | |||
d378a29565 | |||
2bdf2feea7 | |||
607fb1fff8 | |||
38fc2a0055 | |||
0c07f8c099 | |||
d8d21c77d5 | |||
b4cd20cbbc | |||
f840afb983 | |||
93d1b4ed9d | |||
fa81c8eeb3 | |||
98308cd79c | |||
052331fabc | |||
541c9a94bf | |||
12452d4ab4 | |||
709a3f6de7 | |||
7caa0a8aa4 | |||
0f56296c24 | |||
0a846a2fce | |||
bffccf4622 | |||
fc774a1871 | |||
d647db222c | |||
0c7eb93889 | |||
0658e1da7f | |||
d2d8e5d40f | |||
5e794492c1 | |||
7a2d2efff8 | |||
e95062d1df | |||
0268818fbb | |||
234268eec2 | |||
ed73d4f3ac | |||
6ca46929fa | |||
185075d870 | |||
451257a2fd | |||
3d5f520ff0 | |||
a1065bfaf0 | |||
d6f345cc3c | |||
d961911a98 | |||
e543272eb3 | |||
c8c0ceeee9 | |||
afbc2b0082 | |||
69ec1e2e31 | |||
ff15509bc7 | |||
7e22a2fc0b | |||
0a48c92d2c | |||
991ca92a9d | |||
c550f885a4 | |||
851038c3a8 | |||
df7774c018 | |||
15090a8ad4 | |||
8575e3f71c | |||
90b0713e32 | |||
d56b7ed96d | |||
db5e5067a0 | |||
0020dad595 | |||
c2d31fb01e | |||
65d49d5c94 | |||
3a99af2696 | |||
503fd1fbaf | |||
4c74dba2f3 | |||
33df13ad65 | |||
ce18c68eca | |||
2d5623911a | |||
a0b30e5dfb | |||
76a920651c | |||
ef6609a723 | |||
c0dcb342f3 | |||
8a9a5ecdd7 | |||
12c5ead39c | |||
3db6b6ca7a | |||
e894f5c399 | |||
8524187869 | |||
f7422a9607 | |||
cf0a9e0730 | |||
8e5beb024f | |||
576d6d8f86 | |||
41a765d715 | |||
f370fd36e0 | |||
a222c3e609 | |||
5a71df0cb3 | |||
6a46cabb10 | |||
74673545c0 | |||
75a311f250 | |||
391ed6334d | |||
0940e6d6ed | |||
4d2ee51bb0 | |||
680128bc09 | |||
5364b51979 | |||
67ef0f0c8f | |||
e9886d701d | |||
022835bab2 | |||
8be2e4c325 | |||
33ba3e31ed | |||
cba2b3c72d | |||
a4a2901294 | |||
bb46f54ad7 | |||
c9b930dd82 | |||
a39f7d63bb | |||
e317f7d51c | |||
3ddd28d37d | |||
fe6b39d585 | |||
d837bfc2d7 | |||
4759975be6 | |||
078a4b00a7 | |||
b00b80a45b | |||
269bbe0e7d | |||
bb2fc6b8da | |||
d4e196035c | |||
b8979c8701 | |||
bfdd3398f6 | |||
3a121a621f | |||
c06f4fc702 | |||
95f1ea2f12 | |||
784347f61f | |||
42176a7ac4 | |||
76b4b80a23 | |||
7c78282ce8 | |||
47c2a2e411 | |||
5faf520067 | |||
02d74cafba | |||
734f30d14c | |||
f2d810febc | |||
819514aeba | |||
b55adee982 | |||
4c32cb952f | |||
35f714e438 | |||
d5ce086089 | |||
82ec02daf8 | |||
8e7a3f031b | |||
7822187b17 | |||
068133ec85 | |||
598fdad089 | |||
ad6a57e0a3 | |||
230255f887 | |||
1338995ee9 | |||
47e4fca7fd | |||
665e7071fa | |||
c20f60b144 | |||
1408357198 | |||
1f9a3dd1e6 | |||
11505fa0d8 | |||
9326e062d8 | |||
cb2cb7c3bd | |||
92d99ba279 | |||
dfbbbb5e3e | |||
89f317915d | |||
4d5fa5c855 | |||
2f35392cd8 | |||
b056adc032 | |||
0d894a18fc | |||
e5138081ec | |||
8ffa483bb6 | |||
ea8a43def0 | |||
535d9da6b6 | |||
18bf77204e | |||
160221c815 | |||
6220b49463 | |||
aa683a765d | |||
b8b91d3418 | |||
0018acfede | |||
51d7a65a2b | |||
a1724f7816 | |||
d108479d84 | |||
cd5bc64658 | |||
5f723cb92c | |||
7ffb75f476 | |||
c0981b8e13 | |||
80e506563c | |||
694951096c | |||
9e7e178585 | |||
db3113ba16 | |||
3117f565c1 | |||
8443d199b2 | |||
8e2f72c644 | |||
0c1d1e2396 | |||
740450287d | |||
0c691af1d2 | |||
5aa53d70aa | |||
3ab86bd661 | |||
2538094e13 | |||
b37a0484d4 | |||
4c5e28e53a | |||
e10d763446 | |||
573b8611bc | |||
966eb2fbd0 | |||
b0c5018c70 | |||
19a509a92c | |||
9e17a147ec | |||
eba59aaf87 | |||
fa809ec8cf | |||
1651a8f189 | |||
27761b4500 | |||
a80ac0a8d3 | |||
5af143e8e4 | |||
255d7226d1 | |||
50abca4583 | |||
de8d7c65f2 | |||
6123b9c0c6 | |||
3b28c75d1f | |||
368169dc15 | |||
f5b2ce0206 | |||
78e3be12a4 | |||
e7d9fd8056 | |||
08dfe91b95 | |||
85d4c4b82e | |||
6e41add867 | |||
11c10b2ab8 | |||
98849de127 | |||
afd703d08c | |||
ce18fdb399 | |||
670771da33 | |||
c8b08f3a59 | |||
3d382dc750 | |||
9081f84543 | |||
f1a9e3c1bb | |||
b10029c18b | |||
5d4f5434fd | |||
81ca51a8f0 | |||
6cb93c1fac | |||
eed67ddafb | |||
be9e8b99ff | |||
8a0e5659c0 | |||
2842b0cc3d | |||
a42322da0c | |||
4ccb2269a5 | |||
b836aca999 | |||
7d9f96abf0 | |||
86b7bd9c8e | |||
a0a6029915 | |||
6531806996 | |||
3a604ba0bf | |||
39f2977fa8 | |||
faacbe4dac | |||
47c72e4627 | |||
59136fdbe4 | |||
35d1922006 | |||
b40aae54b7 | |||
baa3fbab46 | |||
3361a7b834 | |||
0e13a5956c | |||
9466908c22 | |||
93d27d283a | |||
6f59a4a5b2 | |||
71c3103312 | |||
198edb3109 | |||
44d48d9d7a | |||
712630ca65 | |||
6416e79933 | |||
238c5238a5 | |||
593fe5ed25 | |||
54a6e4ff9e | |||
8a6eb1ac78 | |||
16c8167886 | |||
d761059e4d | |||
c805082648 | |||
9a7f5d580f | |||
af99cf2a41 | |||
221f85af3f | |||
a68ad6d58d | |||
7ae0440cca | |||
2ee7662737 | |||
44195858e6 | |||
39b92f7e54 | |||
1eba623d12 | |||
ea2b24fb4e | |||
a9d9aa18a0 | |||
ecc356ecea | |||
98e31290a7 | |||
7a759df49e | |||
38c524d655 | |||
06264645fd | |||
21d213dfc7 | |||
3065fc6cca | |||
bcefc61da4 | |||
569b1e0eb7 | |||
b83fe6f2a4 | |||
b016984c04 | |||
90bc5b221b | |||
37f1fcbfc8 | |||
4a599eec45 | |||
fdfeaaf1f3 | |||
decb4cc4a3 | |||
44c7ac0fe9 | |||
abb36e3cba | |||
84c30be164 | |||
5afaa39e68 | |||
ce1d7c4a6e | |||
b9521b568f | |||
2eca6e67e1 | |||
c757e5794f | |||
67dc970ce4 | |||
56ccd5e6a1 | |||
bcbee13e26 | |||
d28a3f7878 | |||
162dffb7e8 | |||
0f0b9896b7 | |||
e242e20ca3 | |||
f148ebe99e | |||
f795f649cf | |||
063db641f8 | |||
b44f5c69e1 | |||
56833a6171 | |||
efa2928547 | |||
f5335d17ec | |||
dc7d24267d | |||
412ab3f20c | |||
954c08e97c | |||
a2dcb7b476 | |||
bb0902c592 | |||
9e661e58d1 | |||
d0e72a8f8f | |||
b44eb328e0 | |||
7b4a8d53a7 | |||
73505e2ff0 | |||
e11f5294ab | |||
0190df9cf3 | |||
1af42aedfa | |||
215611aa9e | |||
98d83b2e2d | |||
cf7689ea7b | |||
2848f0499f | |||
5d4b36f80f | |||
415a0f8047 | |||
e0a8376237 | |||
f0f65443c6 | |||
fcc91d862f | |||
3887d8a429 | |||
041f57cb7d | |||
c5ce0408a1 | |||
98dd609f89 | |||
6dc67772b0 | |||
5cf64266f8 | |||
03513e9d70 | |||
b9ed97c0d9 | |||
799be9c98a | |||
566dab1140 | |||
42dc2c19ee | |||
518eb540aa | |||
309ada5df5 | |||
9da63408b0 | |||
7ae7a8440c | |||
aef524506b | |||
547c363473 | |||
2714644528 | |||
d27588b5fb | |||
a8379a46cf | |||
b7caa3e024 | |||
3e33482386 | |||
5057e16874 | |||
9449eff6fd | |||
03610526e5 | |||
cd28df627c | |||
59d62bc5aa | |||
8f46db3701 | |||
109229246c | |||
5856298798 | |||
7f9c589ba3 | |||
8931e71c5c | |||
5bc435eba3 | |||
4dabec6a48 | |||
978376a46e | |||
895f47a9c2 | |||
aec65dee71 | |||
2f66932bd1 | |||
919ff12377 | |||
6748aeabb6 | |||
b3e63c09ab | |||
77b8a76f2e | |||
55b8de9fdd | |||
eb56ab38dc | |||
f29c6bbc6f | |||
464701a899 | |||
5b96fb9320 | |||
1cfb263ee3 | |||
9ca2b4c967 | |||
673d8ae583 | |||
8760bf7be4 | |||
ea02073c84 | |||
b051d7ff48 | |||
cade722e48 | |||
58817f55e1 | |||
3f1d7f7a76 | |||
c8dc116951 | |||
9684d78cae | |||
71f5b73296 | |||
5ba8c14893 | |||
ea9d8a6bc7 | |||
9650ff0824 | |||
00dce1698a | |||
c946a929b7 | |||
21c96a5af1 | |||
a0b9c23100 | |||
cb5bc76766 | |||
1c8772a879 | |||
79ed0e7121 | |||
0c69903123 | |||
04dc24820d | |||
e263e19a2a | |||
ac220fc2bb | |||
3b80472bad | |||
ca17d4f639 | |||
64335d3521 | |||
9945ce2259 | |||
6d9da73090 | |||
c889fb1ef5 | |||
d32bc5df3e | |||
efaf502e95 | |||
ff32c53099 | |||
235eb17cca | |||
aaa562898f | |||
c8fd904c32 | |||
65a8f7f6c9 | |||
f32bfcdbe5 | |||
24b9067047 | |||
de8de84cea | |||
8d300ffbfc | |||
f74dafcb07 | |||
065b76d14c | |||
253345c0c0 | |||
270d694ed2 | |||
f4b771a0c6 | |||
a4de214e2b | |||
aa8bba4865 | |||
392d584572 | |||
900a88b15d | |||
cb384e8ed3 | |||
a2b2afb21c | |||
b70b9606eb | |||
938406122e | |||
951a575db3 | |||
8c50457385 | |||
8c09d10ba9 | |||
c04e51cb15 | |||
7b94f493b9 | |||
f1f04fa782 | |||
4be1966a21 | |||
de25cfc0cb | |||
de36a9b718 | |||
883a3250e4 | |||
1ceb2f9c79 | |||
fbde2a8010 | |||
81925fa66d | |||
6e2abcd5fc | |||
3f46645f5f | |||
2a7f63650c | |||
cd5b1f306a | |||
749bcf3d9c | |||
a9027a2570 | |||
72e32b1f17 | |||
96648ba98a | |||
6561d46349 | |||
2e5e37ac58 | |||
6bac4e65ce | |||
a28f5eeed3 | |||
c0c50133e3 | |||
b9723f9765 | |||
cb35c26d70 | |||
bb52e22ecf | |||
35a2dfc177 | |||
d8e2829e5b | |||
ac5e6baced | |||
710b4a3503 | |||
5331fc1aab | |||
9b7f2ce6c3 | |||
11b2f62ed2 | |||
e7c37d77a8 | |||
1762047bc0 | |||
9a4c8d543d | |||
f5aaa55f21 | |||
23e6502ef3 | |||
ff0f53c915 | |||
64ef69fb34 | |||
249cd8c2ec | |||
d1fb066d07 | |||
2f977312be | |||
feda017c50 | |||
2991221551 | |||
d7719aa0f5 | |||
54e587a46d | |||
4ac5096232 | |||
3d06b18fee | |||
446153675f | |||
bb1850a912 | |||
93516ea8a1 | |||
c3fa8803d3 | |||
3cad5da5a4 | |||
7a8bd99ab1 | |||
7520ddcf40 | |||
9c1318d731 | |||
062fc4afee | |||
87ec8d13ce | |||
cf034f759a | |||
b0c5d21f31 | |||
cbe5f78c1a | |||
4598af23c0 | |||
d855e90524 | |||
897fc87620 | |||
b37f5fc59b | |||
9730351048 | |||
ce687550ae | |||
563b909279 | |||
c390b06da2 | |||
db4e9ea04a | |||
5e6a3ff6a7 | |||
d0dcabd700 | |||
6ed812ff75 | |||
5461182e25 | |||
cbe95f3d25 | |||
ce57b6fb45 | |||
134f542b36 | |||
c3727f330b | |||
d313aad671 | |||
a54fe634ee | |||
ccb6c45466 | |||
800b1b060e | |||
ab03852234 | |||
f90258162a | |||
745731e1a3 | |||
11b0213d20 | |||
cd29d68f3c | |||
da668848c9 | |||
bfd5f27525 | |||
9f66c9c1d1 | |||
1259fe75c9 | |||
e9db74f937 | |||
f99cb96533 | |||
1e848d696b | |||
1bfa7c6f14 | |||
05d1008a08 | |||
9a98fc0f05 | |||
2ca6258a0f | |||
24c34385ee | |||
e07b7ea114 | |||
73c7503ae0 | |||
79db6c3d19 | |||
b6ddacdccf | |||
c9983e6440 | |||
51098c4f86 | |||
4624406ce8 | |||
76269f4a1f | |||
50174d9fb5 | |||
648de7dd86 | |||
9d40ab9e20 | |||
691e86c9bf | |||
8fa4453aec | |||
93d8f8cd63 | |||
c2fca3d9e0 | |||
8b31178647 | |||
e951612af2 | |||
b668c2c781 | |||
ecd0348d96 | |||
69b86925b3 | |||
5293794316 | |||
728c9d0632 | |||
01d93f3af8 | |||
a0d124bd91 | |||
57bc245cb4 | |||
bc431888f3 | |||
ec028b8109 | |||
014594fe8f | |||
3c8a61e40c | |||
bccfaa46ec | |||
2f442062d2 | |||
840cb3d69e | |||
7dfa0b9da9 | |||
f5d0fac800 | |||
baecb553a6 | |||
6e89f0bf8d | |||
f21ff904c2 | |||
38a7e0d1c7 | |||
90814e4449 | |||
e927aeae86 | |||
a77b126d72 | |||
82417b3ca5 | |||
5b141fbf27 | |||
268884296a | |||
ea8ffc9841 | |||
6d930d2fc5 | |||
2ddf3bcdd1 | |||
0a3a9afe58 | |||
5a88d2f68b | |||
7165eb15bd | |||
e20a72280b | |||
abecf3eb6d | |||
7c418a7e06 | |||
58f080a325 | |||
be719e4817 | |||
11b54f69e5 | |||
4a3c66fe22 | |||
9a5084412d | |||
24670667f1 | |||
3f307ff061 | |||
7433da546f | |||
eda2a7b2dc | |||
196203f6d7 | |||
46b0c7a18c | |||
b6a0098aa3 | |||
7d986ae5dc | |||
a2bdcc9ba8 | |||
624b5a5f83 | |||
b57c9605ce | |||
49dfc9fe2d | |||
8f9ba62dc3 | |||
16673fa38b | |||
49d97e1216 | |||
a73050de48 | |||
2535769a65 | |||
b1a3c47766 | |||
8479e9e6d7 | |||
c0b1bbea3e | |||
092f0df7a6 | |||
dfc81c3dab | |||
6649743a2d | |||
56c46d70f7 | |||
c36ec9bf60 | |||
86396a43e9 | |||
590e68c251 | |||
bd704c90dd | |||
ea4afebeb9 | |||
886cca028f | |||
4054055d0d | |||
09c4cb2540 | |||
8ad464d90e | |||
65af9641c2 | |||
cb5a7efa91 | |||
ce47d33cd9 | |||
2e47a0d19f | |||
6a2e08d0a8 | |||
fdb3f26448 | |||
14b7dfa007 | |||
9394835db4 | |||
70b1d6dd9d | |||
d263595c63 | |||
9cb5964b4d | |||
3ad0cc5736 | |||
f09fd6ec16 | |||
8cfa58715c | |||
909264feb5 | |||
cc1ed77dd8 | |||
7d69a91bfe | |||
a6545ddd4d | |||
498bd64d9c | |||
ad9a3a2d3b | |||
2da0f1639f | |||
3ec58ecea0 | |||
061475402c | |||
8f1359d25f | |||
8659bd2c4b | |||
4a052cd343 | |||
28c2a71cfc | |||
69b37fff26 | |||
df619adc76 | |||
374bf1ed98 | |||
540581da3e | |||
6f5fccfeb7 | |||
14669f20bf | |||
075f3f8c9f | |||
2798c1bbec | |||
38d75d410e | |||
04fb29b589 | |||
9037593ab8 | |||
3ced940b5a | |||
f5a98d98c8 | |||
0799f184dc | |||
c530c5d317 | |||
91b2e394d5 | |||
a487563768 | |||
0ab04bd62c | |||
46ce3317c3 | |||
83527fd4fb | |||
6d12aa978d | |||
96aa2365ae | |||
258d5392d5 | |||
09d9f5fe54 | |||
d8b73e4223 | |||
4c566dbfbb | |||
bde9771991 | |||
fe0d02fc47 | |||
7b005aadc1 | |||
7764c5c697 | |||
c6917d9d4f | |||
25132bff86 | |||
0e38bf9de0 | |||
309bae0a0b | |||
ae70293df3 | |||
1e623a3710 | |||
928e2543cd | |||
92e14a36f9 | |||
34a1990c57 | |||
e4c3882a81 | |||
62f9738a9a | |||
a562c64ed6 | |||
8c726ea87e | |||
c8be3960a0 | |||
759af8b56b | |||
97deb01b1f | |||
ad639d783f | |||
e1c2e50d92 | |||
dd4e3f8704 | |||
08941aa0c7 | |||
03fb766428 | |||
2454a3b641 | |||
3e793f079d | |||
5fbb0d050c | |||
71a8627c5d | |||
5c38012980 | |||
7c2ce296d3 | |||
9d37d86223 | |||
21ef5a1961 | |||
46f8a6dd85 | |||
0aa90c6be4 | |||
c05a8cf7bb | |||
4c21114087 | |||
65bb2373ae | |||
8c925bca71 | |||
330a8c90e4 | |||
d8c35c893b | |||
0fe308c0ef | |||
2d14c3b17a | |||
8ef621ad2a | |||
28bf222a6a | |||
9e883f5873 | |||
43a9619e57 | |||
f518e3a75f | |||
146b30796e | |||
5942b2b27c | |||
330b1090f4 | |||
3ccbe28d9f | |||
67719f2185 | |||
6388768d73 | |||
1dda05a965 | |||
14fd78fd85 | |||
a9321b1387 | |||
19cb503531 | |||
9f2acf54bc | |||
9c77a7cdaf | |||
d58a242fe7 | |||
910c0d9ee7 | |||
331b9f6425 | |||
49162784a8 | |||
0c36f2353d | |||
a580f8c61f | |||
f368381d12 | |||
7c2f795ea6 | |||
93d48f1d89 | |||
861953c95c | |||
aa16ccda79 | |||
6269d28bb0 | |||
d438b88f19 | |||
8e03f65645 | |||
606b8fafb0 | |||
b7fa5dec21 | |||
a5c972aa8b | |||
d05aa70c6b | |||
eac99c1b16 | |||
b7a89cec59 | |||
9e0d7be014 | |||
99951911d5 | |||
fd72fad8fd | |||
ff82756415 | |||
d28243d5fc | |||
447e534350 | |||
cda887896a | |||
09b548db75 | |||
920bf373fe | |||
a9ae4daab2 | |||
15662efec4 | |||
5846c46b76 | |||
816b389759 | |||
6605dd1c7c | |||
837ed788f4 | |||
2e4fe7fd2e | |||
52ea193638 | |||
5e3ef775d5 | |||
228238e602 | |||
a6fd22c399 | |||
1616cae5cf | |||
d5cf684d99 | |||
c9e51d9911 | |||
b2830384f5 | |||
487a0e1b5d | |||
8fe42e58b3 | |||
afb17a5b6e | |||
b8f3533d53 | |||
4918cd241f | |||
45983301d5 | |||
acf57def13 | |||
800591db00 | |||
159c98c202 | |||
c837b3dfb9 | |||
53b89ec312 | |||
c17b912eb9 | |||
8785b2bf6d | |||
fb1be83a1b | |||
ea848f74af | |||
b8d5f87f96 |
3
.bazelignore
Normal file
3
.bazelignore
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
dist
|
||||
aio/node_modules
|
101
.bazelrc
Normal file
101
.bazelrc
Normal file
@ -0,0 +1,101 @@
|
||||
# Load any settings specific to the current user
|
||||
try-import .bazelrc.user
|
||||
################################
|
||||
# Settings for Angular team members only
|
||||
################################
|
||||
# To enable this feature check the "Remote caching" section in docs/BAZEL.md.
|
||||
build:angular-team --remote_http_cache=https://storage.googleapis.com/angular-team-cache
|
||||
|
||||
###############################
|
||||
# Typescript / Angular / Sass #
|
||||
###############################
|
||||
|
||||
# Make compilation fast, by keeping a few copies of the compilers
|
||||
# running as daemons, and cache SourceFile AST's to reduce parse time.
|
||||
build --strategy=TypeScriptCompile=worker
|
||||
build --strategy=AngularTemplateCompile=worker
|
||||
|
||||
# Enable debugging tests with --config=debug
|
||||
test:debug --test_arg=--node_options=--inspect-brk --test_output=streamed --test_strategy=exclusive --test_timeout=9999 --nocache_test_results
|
||||
|
||||
###############################
|
||||
# Filesystem interactions #
|
||||
###############################
|
||||
|
||||
# Don't create symlinks like bazel-out in the project.
|
||||
# These cause VSCode to traverse a massive tree, opening file handles and
|
||||
# eventually a surprising failure with auto-discovery of the C++ toolchain in
|
||||
# MacOS High Sierra.
|
||||
# See https://github.com/bazelbuild/bazel/issues/4603
|
||||
build --symlink_prefix=/
|
||||
|
||||
# Performance: avoid stat'ing input files
|
||||
build --watchfs
|
||||
|
||||
# Turn off legacy external runfiles
|
||||
run --nolegacy_external_runfiles
|
||||
test --nolegacy_external_runfiles
|
||||
|
||||
###############################
|
||||
# Release support #
|
||||
# Turn on these settings with #
|
||||
# --config=release #
|
||||
###############################
|
||||
|
||||
# Releases should always be stamped with version control info
|
||||
build:release --workspace_status_command=./tools/bazel_stamp_vars.sh
|
||||
|
||||
###############################
|
||||
# Output #
|
||||
###############################
|
||||
|
||||
# A more useful default output mode for bazel query
|
||||
# Prints eg. "ng_module rule //foo:bar" rather than just "//foo:bar"
|
||||
query --output=label_kind
|
||||
|
||||
# By default, failing tests don't print any output, it goes to the log file
|
||||
test --test_output=errors
|
||||
|
||||
# Show which actions are run under workers,
|
||||
# and print all the actions running in parallel.
|
||||
# Helps to demonstrate that bazel uses all the cores on the machine.
|
||||
build --experimental_ui
|
||||
test --experimental_ui
|
||||
|
||||
################################
|
||||
# Settings for CircleCI #
|
||||
################################
|
||||
|
||||
# Bazel flags for CircleCI are in /.circleci/bazel.rc
|
||||
|
||||
################################
|
||||
# Temporary Settings for Ivy #
|
||||
################################
|
||||
# to determine if the compiler used should be Ivy or ViewEngine one can use `--define=compile=local` on
|
||||
# any bazel target. This is a temporary flag until codebase is permanently switched to Ivy.
|
||||
build --define=compile=legacy
|
||||
|
||||
###############################
|
||||
# Remote Build Execution support
|
||||
# Turn on these settings with
|
||||
# --config=remote
|
||||
###############################
|
||||
|
||||
# Load default settings for Remote Build Execution
|
||||
import %workspace%/third_party/github.com/bazelbuild/bazel-toolchains/bazelrc/bazel-0.20.0.bazelrc
|
||||
|
||||
# Increase the default number of jobs by 50% because our build has lots of
|
||||
# parallelism
|
||||
build:remote --jobs=150
|
||||
|
||||
# Point to our custom execution platform; see tools/BUILD.bazel
|
||||
build:remote --extra_execution_platforms=//tools:rbe_ubuntu1604-angular
|
||||
build:remote --host_platform=//tools:rbe_ubuntu1604-angular
|
||||
build:remote --platforms=//tools:rbe_ubuntu1604-angular
|
||||
|
||||
# Remote instance.
|
||||
build:remote --remote_instance_name=projects/internal-200822/instances/default_instance
|
||||
|
||||
# Do not accept remote cache.
|
||||
# We need to understand the security risks of using prior build artifacts.
|
||||
build:remote --remote_accept_cached=false
|
42
.buildkite/Dockerfile
Normal file
42
.buildkite/Dockerfile
Normal file
@ -0,0 +1,42 @@
|
||||
# Heavily based on https://github.com/StefanScherer/dockerfiles-windows/ images.
|
||||
# Combines the node windowsservercore image with the Bazel Prerequisites (https://docs.bazel.build/versions/master/install-windows.html).
|
||||
# msys install taken from https://github.com/StefanScherer/dockerfiles-windows/issues/30
|
||||
# VS redist install taken from https://github.com/StefanScherer/dockerfiles-windows/blob/master/apache/Dockerfile
|
||||
# The nanoserver image won't work because MSYS2 does not run in it https://github.com/Alexpux/MSYS2-packages/issues/1493
|
||||
|
||||
# Before building this image, you must locally build node-windows:10.13.0-windowsservercore-1803.
|
||||
# Clone https://github.com/StefanScherer/dockerfiles-windows/commit/4ce7101a766b9b880ac262479dd9126b64d656cf and build using
|
||||
# docker build -t node-windows:10.13.0-windowsservercore-1803 --build-arg core=microsoft/windowsservercore:1803 --build-arg target=microsoft/windowsservercore:1803 .
|
||||
FROM node-windows:10.13.0-windowsservercore-1803
|
||||
|
||||
SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]
|
||||
|
||||
# Install 7zip to extract msys2
|
||||
RUN Invoke-WebRequest -UseBasicParsing 'https://www.7-zip.org/a/7z1805-x64.exe' -OutFile 7z.exe
|
||||
# For some reason the last letter in the destination directory is lost. So '/D=C:\\7zip0' will extract to '/D=C:\\7zip'.
|
||||
RUN Start-Process -FilePath 'C:\\7z.exe' -ArgumentList '/S', '/D=C:\\7zip0' -NoNewWindow -Wait
|
||||
|
||||
# Extract msys2
|
||||
RUN Invoke-WebRequest -UseBasicParsing 'http://repo.msys2.org/distrib/x86_64/msys2-base-x86_64-20180531.tar.xz' -OutFile msys2.tar.xz
|
||||
RUN Start-Process -FilePath 'C:\\7zip\\7z' -ArgumentList 'e', 'msys2.tar.xz' -Wait
|
||||
RUN Start-Process -FilePath 'C:\\7zip\\7z' -ArgumentList 'x', 'msys2.tar', '-oC:\\' -Wait
|
||||
RUN Remove-Item msys2.tar.xz
|
||||
RUN Remove-Item msys2.tar
|
||||
RUN Remove-Item 7z.exe
|
||||
RUN Remove-Item -Recurse 7zip
|
||||
|
||||
# Add MSYS2 to PATH, and set BAZEL_SH
|
||||
RUN [Environment]::SetEnvironmentVariable('Path', $env:Path + ';C:\msys64\usr\bin', [System.EnvironmentVariableTarget]::Machine)
|
||||
RUN [Environment]::SetEnvironmentVariable('BAZEL_SH', 'C:\msys64\usr\bin\bash.exe', [System.EnvironmentVariableTarget]::Machine)
|
||||
|
||||
# Install Microsoft Visual C++ Redistributable for Visual Studio 2015
|
||||
RUN Invoke-WebRequest -UseBasicParsing 'https://download.microsoft.com/download/9/3/F/93FCF1E7-E6A4-478B-96E7-D4B285925B00/vc_redist.x64.exe' -OutFile vc_redist.x64.exe
|
||||
RUN Start-Process 'c:\\vc_redist.x64.exe' -ArgumentList '/Install', '/Passive', '/NoRestart' -NoNewWindow -Wait
|
||||
RUN Remove-Item vc_redist.x64.exe
|
||||
|
||||
# Add a fix for https://github.com/docker/for-win/issues/2920 as entry point to the container.
|
||||
SHELL ["cmd", "/c"]
|
||||
COPY "fix-msys64.cmd" "C:\\fix-msys64.cmd"
|
||||
ENTRYPOINT cmd /C C:\\fix-msys64.cmd && cmd /c
|
||||
|
||||
CMD ["cmd.exe"]
|
96
.buildkite/README.md
Normal file
96
.buildkite/README.md
Normal file
@ -0,0 +1,96 @@
|
||||
# BuildKite configuration
|
||||
|
||||
This folder contains configuration for the [BuildKite](https://buildkite.com) based CI checks for
|
||||
this repository.
|
||||
|
||||
BuildKite is a CI provider that provides build coordination and reports while we provide the
|
||||
infrastructure.
|
||||
|
||||
CI runs are triggered by new PRs and will show up on the GitHub checks interface, along with the
|
||||
other current CI solutions.
|
||||
|
||||
Currently it is only used for tests on Windows platforms.
|
||||
|
||||
|
||||
## The build pipeline
|
||||
|
||||
BuildKite uses a pipeline for each repository. The `pipeline.yml` file defines pipeline
|
||||
[build steps](https://buildkite.com/docs/pipelines/defining-steps) for this repository.
|
||||
|
||||
Run results can be seen in the GitHub checks interface and in the
|
||||
[pipeline dashboard](https://buildkite.com/angular/angular).
|
||||
|
||||
Although most configuration is done via `pipeline.yml`, some options are only available
|
||||
in the online [pipeline settings](https://buildkite.com/angular/angular/settings).
|
||||
|
||||
|
||||
## Infrastructure
|
||||
|
||||
BuildKite does not provide the host machines where the builds runs, providing instead the
|
||||
[BuildKite Agent](https://buildkite.com/docs/agent/v3) that should be run our own infrastructure.
|
||||
|
||||
|
||||
### Agents
|
||||
|
||||
This agent polls the BuildKite API for builds, runs them, and reports back the results.
|
||||
Agents are the unit of concurrency: each agent can run one build at any given time.
|
||||
Adding agents allows more builds to be ran at the same time.
|
||||
|
||||
Individual agents can have tags, and pipeline steps can target only agents with certain tags via the
|
||||
`agents` field in `pipeline.yml`.
|
||||
For example: agents on Windows machines are tagged as `windows`, and the Windows specific build
|
||||
steps list `windows: true` in their `agents` field.
|
||||
|
||||
You can see the current agent pool, along with their tags, in the
|
||||
[agents list](https://buildkite.com/organizations/angular/agents).
|
||||
|
||||
|
||||
### Our host machines
|
||||
|
||||
We use [Google Cloud](https://cloud.google.com/) as our cloud provider, under the
|
||||
[Angular project](https://console.cloud.google.com/home/dashboard?project=internal-200822).
|
||||
To access this project you need need to be logged in with a Google account that's a member of
|
||||
team@angular.io.
|
||||
For googlers this may be your google.com account, for others it is an angular.io account.
|
||||
|
||||
In this project we have a number of Windows VMs running, each of them with several agents.
|
||||
The `provision-windows-buildkite.ps1` file contains instructions on how to create new host VMs that
|
||||
are fully configured to run the BuildKite agents as services.
|
||||
|
||||
Our pipeline uses [docker-buildkite-plugin](https://github.com/buildkite-plugins/docker-buildkite-plugin)
|
||||
to run build steps inside docker containers.
|
||||
This way we achieve isolation and hermeticity.
|
||||
|
||||
The `Dockerfile` file describes a custom Docker image that includes NodeJs, Yarn, and the Bazel
|
||||
pre-requisites on Windows.
|
||||
|
||||
To upload a new version of the docker image, follow any build instructions in `Dockerfile` and then
|
||||
run `docker build -t angular/node-bazel-windows:NEW_VERSION`, followed by
|
||||
`docker push angular/node-bazel-windows:NEW_VERSION`.
|
||||
After being pushed it should be available online, and you can use the new version in `pipeline.yml`.
|
||||
|
||||
|
||||
## Caretaker
|
||||
|
||||
BuildKite status can be found at https://www.buildkitestatus.com/.
|
||||
|
||||
Issues related to the BuildKite setup should be escalated to the Tools Team via the current
|
||||
caretaker, followed by Alex Eagle and Filipe Silva.
|
||||
|
||||
Support requests should be submitted via email to support@buildkite.com and cc Igor, Misko, Alex,
|
||||
Jeremy and Manu
|
||||
|
||||
|
||||
## Rollout strategy
|
||||
|
||||
At the moment our BuildKite CI uses 1 host VM running 4 agents, thus being capable of 4 concurrent
|
||||
builds.
|
||||
The only test running is `bazel test //tools/ts-api-guardian:all`, and the PR check is not
|
||||
mandatory.
|
||||
|
||||
In the future we should add cache support to speed up the initial `yarn` install, and also Bazel
|
||||
remote caching to speed up Bazel builds.
|
||||
|
||||
After the current setup is verified as stable and reliable the GitHub PR check can become mandatory.
|
||||
|
||||
The tests ran should also be expanded to cover most, if not all, of the Bazel tests.
|
6
.buildkite/fix-msys64.cmd
Normal file
6
.buildkite/fix-msys64.cmd
Normal file
@ -0,0 +1,6 @@
|
||||
@echo off
|
||||
REM Fix for https://github.com/docker/for-win/issues/2920
|
||||
REM echo "Fixing msys64 folder..."
|
||||
REM Touch all .dll files inside C:\msys64\
|
||||
forfiles /p C:\msys64\ /s /m *.dll /c "cmd /c Copy /B @path+,, >NUL"
|
||||
REM echo "Fixed msys64 folder."
|
10
.buildkite/pipeline.yml
Normal file
10
.buildkite/pipeline.yml
Normal file
@ -0,0 +1,10 @@
|
||||
steps:
|
||||
- label: windows-test
|
||||
commands:
|
||||
- "yarn install --frozen-lockfile --non-interactive --network-timeout 100000"
|
||||
- "yarn bazel test //tools/ts-api-guardian:all --noshow_progress"
|
||||
plugins:
|
||||
- docker#v2.1.0:
|
||||
image: "filipesilva/node-bazel-windows:0.0.2"
|
||||
agents:
|
||||
windows: true
|
92
.buildkite/provision-windows-buildkite.ps1
Normal file
92
.buildkite/provision-windows-buildkite.ps1
Normal file
@ -0,0 +1,92 @@
|
||||
# PowerShell script to provision a Windows Server with BuildKite
|
||||
# This script follows https://buildkite.com/docs/agent/v3/windows.
|
||||
|
||||
# Instructions
|
||||
|
||||
# VM creation:
|
||||
# In Google Cloud Platform, create a Compute Engine instance.
|
||||
# We recommend machine type n1-highcpu-16 (16 vCPUs, 14.4 GB memory).
|
||||
# Use a windows boot disk with container support such as
|
||||
# "Windows Server version 1803 Datacenter Core for Containers".
|
||||
# Give it a name, then click "Create".
|
||||
|
||||
# VM setup:
|
||||
# In the Compute Engine menu, select "VM Instances". Click on the VM name you chose before.
|
||||
# Click "Set Windows Password" to choose a username and password.
|
||||
# Click RDP to open a remote desktop via browser, using the username and password.
|
||||
# In the Windows command prompt start an elevated powershell by inputing
|
||||
# "powershell -Command "Start-Process PowerShell -Verb RunAs" followed by Enter.
|
||||
# Download and execute this script from GitHub, passing the token (mandatory), tags (optional)
|
||||
# and number of agents (optional) as args:
|
||||
# ```
|
||||
# Invoke-WebRequest -Uri https://raw.githubusercontent.com/angular/angular/master/.buildkite/provision-windows-buildkite.ps1 -OutFile provision.ps1
|
||||
# .\provision.ps1 -token "MY_TOKEN" -tags "windows=true,another_tag=true" -agents 4
|
||||
# ```
|
||||
# The VM should restart and be fully configured.
|
||||
|
||||
# Creating extra VMs
|
||||
# You can create an image of the current VM by following the instructions below.
|
||||
# https://cloud.google.com/compute/docs/instances/windows/creating-windows-os-image
|
||||
# Then create a new VM and choose "Custom images".
|
||||
|
||||
|
||||
# Script proper.
|
||||
|
||||
# Get the token and tags from arguments.
|
||||
param (
|
||||
[Parameter(Mandatory=$true)][string]$token,
|
||||
[string]$tags = ""
|
||||
[Int]$agents = 1
|
||||
)
|
||||
|
||||
# Allow HTTPS
|
||||
[Net.ServicePointManager]::SecurityProtocol = "tls12, tls11, tls"
|
||||
|
||||
# Helper to add to PATH.
|
||||
# Will take current PATH so avoid running it after anything to modifies only the powershell session path.
|
||||
function Add-Path ([string]$newPathItem) {
|
||||
$Env:Path+= ";" + $newPathItem + ";"
|
||||
[Environment]::SetEnvironmentVariable("Path",$env:Path, [System.EnvironmentVariableTarget]::Machine)
|
||||
}
|
||||
|
||||
# Install Git for Windows
|
||||
Write-Host "Installing Git for Windows."
|
||||
Invoke-WebRequest -Uri https://github.com/git-for-windows/git/releases/download/v2.19.1.windows.1/Git-2.19.1-64-bit.exe -OutFile git.exe
|
||||
.\git.exe /VERYSILENT /NORESTART /NOCANCEL /SP- /CLOSEAPPLICATIONS /RESTARTAPPLICATIONS /COMPONENTS="icons,ext\reg\shellhere,assoc,assoc_sh" /DIR="C:\git"
|
||||
Add-Path "C:\git\bin"
|
||||
Remove-Item git.exe
|
||||
|
||||
# Download NSSM (https://nssm.cc/) to run the BuildKite agent as a service.
|
||||
Write-Host "Downloading NSSM."
|
||||
Invoke-WebRequest -Uri https://nssm.cc/ci/nssm-2.24-101-g897c7ad.zip -OutFile nssm.zip
|
||||
Expand-Archive -Path nssm.zip -DestinationPath C:\nssm
|
||||
Add-Path "C:\nssm\nssm-2.24-101-g897c7ad\win64"
|
||||
Remove-Item nssm.zip
|
||||
|
||||
# Run the BuildKite agent install script
|
||||
Write-Host "Installing BuildKite agent."
|
||||
$env:buildkiteAgentToken = $token
|
||||
$env:buildkiteAgentTags = $tags
|
||||
Set-ExecutionPolicy Bypass -Scope Process -Force
|
||||
iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/buildkite/agent/master/install.ps1'))
|
||||
|
||||
# Configure the BuildKite agent clone and timestamp behavior
|
||||
Add-Content C:\buildkite-agent\buildkite-agent.cfg "`ngit-clone-flags=--config core.autocrlf=input --config core.eol=lf --config core.longpaths=true --config core.symlinks=true`n"
|
||||
Add-Content C:\buildkite-agent\buildkite-agent.cfg "`ntimestamp-lines=true`n"
|
||||
|
||||
# Register the BuildKite agent service using NSSM, so that it persists through restarts and is
|
||||
# restarted if the process dies.
|
||||
for ($i=1; $i -le $agents; $i++)
|
||||
{
|
||||
$agentName = "buildkite-agent-$i"
|
||||
Write-Host "Registering $agentName as a service."
|
||||
nssm.exe install $agentName "C:\buildkite-agent\bin\buildkite-agent.exe" "start"
|
||||
nssm.exe set $agentName AppStdout "C:\buildkite-agent\$agentName.log"
|
||||
nssm.exe set $agentName AppStderr "C:\buildkite-agent\$agentName.log"
|
||||
nssm.exe status $agentName
|
||||
nssm.exe start $agentName
|
||||
nssm.exe status $agentName
|
||||
}
|
||||
|
||||
# Restart the machine.
|
||||
Restart-Computer
|
19
.circleci/README.md
Normal file
19
.circleci/README.md
Normal file
@ -0,0 +1,19 @@
|
||||
# Encryption
|
||||
|
||||
Based on https://github.com/circleci/encrypted-files
|
||||
|
||||
In the CircleCI web UI, we have a secret variable called `KEY`
|
||||
https://circleci.com/gh/angular/angular/edit#env-vars
|
||||
which is only exposed to non-fork builds
|
||||
(see "Pass secrets to builds from forked pull requests" under
|
||||
https://circleci.com/gh/angular/angular/edit#advanced-settings)
|
||||
|
||||
We use this as a symmetric AES encryption key to encrypt tokens like
|
||||
a GitHub token that enables publishing snapshots.
|
||||
|
||||
To create the github_token file, we take this approach:
|
||||
- Find the angular-builds:token in http://valentine
|
||||
- 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
|
||||
- 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`
|
26
.circleci/bazel.rc
Normal file
26
.circleci/bazel.rc
Normal file
@ -0,0 +1,26 @@
|
||||
# These options are enabled when running on CI
|
||||
# We do this by copying this file to /etc/bazel.bazelrc at the start of the build.
|
||||
# See documentation in /docs/BAZEL.md
|
||||
|
||||
# Don't be spammy in the logs
|
||||
# TODO(gmagolan): Hide progress again once build performance improves
|
||||
# Presently, CircleCI can timeout during bazel test ... with the following
|
||||
# error: Too long with no output (exceeded 10m0s)
|
||||
# build --noshow_progress
|
||||
|
||||
# Print all the options that apply to the build.
|
||||
# This helps us diagnose which options override others
|
||||
# (e.g. /etc/bazel.bazelrc vs. tools/bazel.rc)
|
||||
build --announce_rc
|
||||
|
||||
# Workaround https://github.com/bazelbuild/bazel/issues/3645
|
||||
# Bazel doesn't calculate the memory ceiling correctly when running under Docker.
|
||||
# Limit Bazel to consuming resources that fit in CircleCI "xlarge" class
|
||||
# https://circleci.com/docs/2.0/configuration-reference/#resource_class
|
||||
build --local_resources=14336,8.0,1.0
|
||||
|
||||
# Retry in the event of flakes, eg. https://circleci.com/gh/angular/angular/31309
|
||||
test --flaky_test_attempts=2
|
||||
|
||||
# More details on failures
|
||||
build --verbose_failures=true
|
465
.circleci/config.yml
Normal file
465
.circleci/config.yml
Normal file
@ -0,0 +1,465 @@
|
||||
# Configuration file for https://circleci.com/gh/angular/angular
|
||||
|
||||
# Note: YAML anchors allow an object to be re-used, reducing duplication.
|
||||
# The ampersand declares an alias for an object, then later the `<<: *name`
|
||||
# syntax dereferences it.
|
||||
# See http://blog.daemonl.com/2016/02/yaml.html
|
||||
# To validate changes, use an online parser, eg.
|
||||
# http://yaml-online-parser.appspot.com/
|
||||
|
||||
# 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
|
||||
# fetched by the Webtesting rules. Therefore for jobs that run tests with Bazel, we don't need a
|
||||
# docker image with browsers pre-installed.
|
||||
# **NOTE**: If you change the version of the docker images, also change the `cache_key` suffix.
|
||||
var_1: &default_docker_image circleci/node:10.12
|
||||
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
|
||||
var_4: &define_env_vars
|
||||
run:
|
||||
name: Define environment variables
|
||||
command: ./.circleci/env.sh
|
||||
|
||||
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 "echo 'build --config=remote' >> /etc/bazel.bazelrc"
|
||||
|
||||
# Settings common to each job
|
||||
var_6: &job_defaults
|
||||
working_directory: ~/ng
|
||||
docker:
|
||||
- image: *default_docker_image
|
||||
|
||||
# After checkout, rebase on top of master.
|
||||
# Similar to travis behavior, but not quite the same.
|
||||
# See https://discuss.circleci.com/t/1662
|
||||
var_7: &post_checkout
|
||||
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
|
||||
jobs:
|
||||
lint:
|
||||
<<: *job_defaults
|
||||
steps:
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
- *define_env_vars
|
||||
- *yarn_install
|
||||
|
||||
- run: 'yarn bazel:format -mode=check ||
|
||||
(echo "BUILD files not formatted. Please run ''yarn bazel:format''" ; exit 1)'
|
||||
# Run the skylark linter to check our Bazel rules
|
||||
- run: 'yarn bazel:lint ||
|
||||
(echo -e "\n.bzl files have lint errors. Please run ''yarn bazel:lint-fix''"; exit 1)'
|
||||
|
||||
- run: ./node_modules/.bin/gulp lint
|
||||
|
||||
test:
|
||||
<<: *job_defaults
|
||||
resource_class: xlarge
|
||||
steps:
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
- *define_env_vars
|
||||
- *setup_circleci_bazel_config
|
||||
- *yarn_install
|
||||
|
||||
# Setup remote execution and run RBE-compatible tests.
|
||||
- *setup_bazel_remote_execution
|
||||
- run: yarn bazel test //... --build_tag_filters=-ivy-only --test_tag_filters=-ivy-only,-local
|
||||
# Now run RBE incompatible tests locally.
|
||||
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
|
||||
- run: yarn bazel test //... --build_tag_filters=-ivy-only,local --test_tag_filters=-ivy-only,local
|
||||
|
||||
- save_cache:
|
||||
key: *cache_key
|
||||
paths:
|
||||
- "node_modules"
|
||||
- "~/bazel_repository_cache"
|
||||
|
||||
# Temporary job to test what will happen when we flip the Ivy flag to true
|
||||
test_ivy_aot:
|
||||
<<: *job_defaults
|
||||
resource_class: xlarge
|
||||
steps:
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
- *define_env_vars
|
||||
- *setup_circleci_bazel_config
|
||||
- *yarn_install
|
||||
- *setup_bazel_remote_execution
|
||||
|
||||
# We need to explicitly specify the --symlink_prefix option because otherwise we would
|
||||
# not be able to easily find the output bin directory when uploading artifacts for size
|
||||
# measurements.
|
||||
- run: yarn test-ivy-aot //... --symlink_prefix=dist/
|
||||
|
||||
# Publish bundle artifacts which will be used to calculate the size change. **Note**: Make
|
||||
# sure that the size plugin from the Angular robot fetches the artifacts from this CircleCI
|
||||
# job (see .github/angular-robot.yml). Additionally any artifacts need to be stored with the
|
||||
# following path format: "{projectName}/{context}/{fileName}". This format is necessary
|
||||
# because otherwise the bot is not able to pick up the artifacts from CircleCI. See:
|
||||
# https://github.com/angular/github-robot/blob/master/functions/src/plugins/size.ts#L392-L394
|
||||
- store_artifacts:
|
||||
path: dist/bin/packages/core/test/bundling/hello_world/bundle.min.js
|
||||
destination: core/hello_world/bundle
|
||||
- store_artifacts:
|
||||
path: dist/bin/packages/core/test/bundling/todo/bundle.min.js
|
||||
destination: core/todo/bundle
|
||||
- store_artifacts:
|
||||
path: dist/bin/packages/core/test/bundling/hello_world/bundle.min.js.br
|
||||
destination: core/hello_world/bundle.br
|
||||
- store_artifacts:
|
||||
path: dist/bin/packages/core/test/bundling/todo/bundle.min.js.br
|
||||
destination: core/todo/bundle.br
|
||||
|
||||
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
|
||||
- run: node ./aio/scripts/create-preview $CIRCLE_BUILD_NUM
|
||||
|
||||
# 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
|
||||
# that rely on the pre-Bazel dist/packages-dist layout.
|
||||
# It duplicates some work with the job above: we build the bazel packages
|
||||
# twice. Even though we have a remote cache, these jobs will typically run in
|
||||
# parallel so up-to-date outputs will not be available at the time the build
|
||||
# starts.
|
||||
# No new jobs should depend on this one.
|
||||
build-packages-dist:
|
||||
<<: *job_defaults
|
||||
resource_class: xlarge
|
||||
steps:
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
- *define_env_vars
|
||||
- *setup_circleci_bazel_config
|
||||
- *yarn_install
|
||||
- *setup_bazel_remote_execution
|
||||
|
||||
- run: scripts/build-packages-dist.sh
|
||||
|
||||
# Save the npm packages from //packages/... for other workflow jobs to read
|
||||
# https://circleci.com/docs/2.0/workflows/#using-workspaces-to-share-data-among-jobs
|
||||
- persist_to_workspace:
|
||||
root: dist
|
||||
paths:
|
||||
- packages-dist
|
||||
- packages-dist-ivy-jit
|
||||
- packages-dist-ivy-aot
|
||||
|
||||
# We run the integration tests outside of Bazel for now.
|
||||
# They are a separate workflow job so that they can be easily re-run.
|
||||
# When the tests are ported to bazel test targets, they should move to the "test"
|
||||
# job above, as part of the bazel test command. That has flaky_test_attempts so the
|
||||
# need to re-run manually should be alleviated.
|
||||
# See comments inside the integration/run_tests.sh script.
|
||||
integration_test:
|
||||
<<: *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
|
||||
# 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.
|
||||
resource_class: xlarge
|
||||
steps:
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
- attach_workspace:
|
||||
at: dist
|
||||
- *define_env_vars
|
||||
- run: ./integration/run_tests.sh
|
||||
|
||||
# This job updates the content of repos like github.com/angular/core-builds
|
||||
# for every green build on angular/angular.
|
||||
publish_snapshot:
|
||||
<<: *job_defaults
|
||||
steps:
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
- *define_env_vars
|
||||
# See below - ideally this job should not trigger for non-upstream builds.
|
||||
# But since it does, we have to check this condition.
|
||||
- run:
|
||||
name: Skip this job for Pull Requests and Fork builds
|
||||
# Note, `|| true` on the end makes this step always exit 0
|
||||
command: '[[
|
||||
"$CI_PULL_REQUEST" != "false"
|
||||
|| "$CI_REPO_OWNER" != "angular"
|
||||
|| "$CI_REPO_NAME" != "angular"
|
||||
]] && circleci step halt || true'
|
||||
- attach_workspace:
|
||||
at: dist
|
||||
# CircleCI has a config setting to force SSH for all github connections
|
||||
# This is not compatible with our mechanism of using a Personal Access Token
|
||||
# Clear the global setting
|
||||
- run: git config --global --unset "url.ssh://git@github.com.insteadof"
|
||||
- run:
|
||||
name: Decrypt github credentials
|
||||
command: 'openssl aes-256-cbc -d -in .circleci/github_token -k "${KEY}" -out ~/.git_credentials'
|
||||
- run: ./scripts/ci/publish-build-artifacts.sh
|
||||
|
||||
aio_monitoring:
|
||||
<<: *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:
|
||||
- checkout:
|
||||
<<: *post_checkout
|
||||
- restore_cache:
|
||||
key: *cache_key
|
||||
- *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:
|
||||
version: 2
|
||||
default_workflow:
|
||||
jobs:
|
||||
- lint
|
||||
- test
|
||||
- test_ivy_aot
|
||||
- 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:
|
||||
requires:
|
||||
- build-packages-dist
|
||||
- publish_snapshot:
|
||||
# Note: no filters on this job because we want it to run for all upstream branches
|
||||
# We'd really like to filter out pull requests here, but not yet available:
|
||||
# https://discuss.circleci.com/t/workflows-pull-request-filter/14396/4
|
||||
# Instead, the job just exits immediately at the first step.
|
||||
requires:
|
||||
# Only publish if tests and integration tests pass
|
||||
- test
|
||||
- test_ivy_aot
|
||||
- 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
|
||||
# since the publishing script expects the legacy outputs layout.
|
||||
- build-packages-dist
|
||||
|
||||
aio_monitoring:
|
||||
jobs:
|
||||
- aio_monitoring
|
||||
triggers:
|
||||
- schedule:
|
||||
cron: "0 0 * * *"
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- master
|
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.
BIN
.circleci/github_token
Normal file
BIN
.circleci/github_token
Normal file
Binary file not shown.
11
.circleci/setup_cache.sh
Executable file
11
.circleci/setup_cache.sh
Executable file
@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
# Install bazel remote cache proxy
|
||||
# This is temporary until the feature is no longer experimental on CircleCI.
|
||||
# See remote cache documentation in /docs/BAZEL.md
|
||||
|
||||
set -u -e
|
||||
|
||||
readonly DOWNLOAD_URL="https://5-116431813-gh.circle-artifacts.com/0/pkg/bazel-remote-proxy-$(uname -s)_$(uname -m)"
|
||||
|
||||
curl --fail -o ~/bazel-remote-proxy "$DOWNLOAD_URL"
|
||||
chmod +x ~/bazel-remote-proxy
|
107
.circleci/trigger-webhook.js
Normal file
107
.circleci/trigger-webhook.js
Normal file
@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Usage (cli):
|
||||
* ```
|
||||
* node create-preview <build-number> <job-name> <webhook-url>
|
||||
* ```
|
||||
*
|
||||
* Usage (JS):
|
||||
* ```js
|
||||
* require('./trigger-webhook').
|
||||
* triggerWebhook(buildNumber, jobName, webhookUrl).
|
||||
* then(...);
|
||||
* ```
|
||||
*
|
||||
* Triggers a notification webhook with CircleCI specific info.
|
||||
*
|
||||
* It can be used for notifying external servers and trigger operations based on CircleCI job status
|
||||
* (e.g. triggering the creation of a preview based on previously stored build atrifacts).
|
||||
*
|
||||
* The body of the sent payload is of the form:
|
||||
* ```json
|
||||
* {
|
||||
* "payload": {
|
||||
* "build_num": ${buildNumber}
|
||||
* "build_parameters": {
|
||||
* "CIRCLE_JOB": "${jobName}"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* When used from JS, it returns a promise which resolves to an object of the form:
|
||||
* ```json
|
||||
* {
|
||||
* "statucCode": ${statusCode},
|
||||
* "responseText": "${responseText}"
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* NOTE:
|
||||
* - When used from the cli, the command will exit with an error code if the response's status code
|
||||
* is outside the [200, 400) range.
|
||||
* - When used from JS, the returned promise will be resolved, even if the response's status code is
|
||||
* outside the [200, 400) range. It is up to the caller to decide how this should be handled.
|
||||
*/
|
||||
|
||||
// Imports
|
||||
const {request} = require('https');
|
||||
|
||||
// Exports
|
||||
module.exports = {
|
||||
triggerWebhook,
|
||||
};
|
||||
|
||||
// Run
|
||||
if (require.resolve === module) {
|
||||
_main(process.argv.slice(2));
|
||||
}
|
||||
|
||||
// Helpers
|
||||
function _main(args) {
|
||||
triggerWebhook(...args).
|
||||
then(({statusCode, responseText}) => (200 <= statusCode && statusCode < 400) ?
|
||||
console.log(`Status: ${statusCode}\n${responseText}`) :
|
||||
Promise.reject(new Error(`Request failed (status: ${statusCode}): ${responseText}`))).
|
||||
catch(err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
function postJson(url, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const opts = {method: 'post', headers: {'Content-Type': 'application/json'}};
|
||||
const onResponse = res => {
|
||||
const statusCode = res.statusCode || -1;
|
||||
let responseText = '';
|
||||
|
||||
res.
|
||||
on('error', reject).
|
||||
on('data', d => responseText += d).
|
||||
on('end', () => resolve({statusCode, responseText}));
|
||||
};
|
||||
|
||||
request(url, opts, onResponse).
|
||||
on('error', reject).
|
||||
end(JSON.stringify(data));
|
||||
});
|
||||
}
|
||||
|
||||
async function triggerWebhook(buildNumber, jobName, webhookUrl) {
|
||||
if (!buildNumber || !jobName || !webhookUrl || isNaN(buildNumber)) {
|
||||
throw new Error(
|
||||
'Missing or invalid arguments.\n' +
|
||||
'Expected: buildNumber (number), jobName (string), webhookUrl (string)');
|
||||
}
|
||||
|
||||
const data = {
|
||||
payload: {
|
||||
build_num: +buildNumber,
|
||||
build_parameters: {CIRCLE_JOB: jobName},
|
||||
},
|
||||
};
|
||||
|
||||
return postJson(webhookUrl, data);
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
# http://editorconfig.org
|
||||
# https://editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
|
41
.github/ISSUE_TEMPLATE.md
vendored
41
.github/ISSUE_TEMPLATE.md
vendored
@ -1,39 +1,10 @@
|
||||
<!--
|
||||
IF YOU DON'T FILL OUT THE FOLLOWING INFORMATION WE MIGHT CLOSE YOUR ISSUE WITHOUT INVESTIGATING
|
||||
-->
|
||||
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
||||
|
||||
**I'm submitting a ...** (check one with "x")
|
||||
```
|
||||
[ ] bug report => search github for a similar issue or PR before submitting
|
||||
[ ] feature request
|
||||
[ ] support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question
|
||||
```
|
||||
Please help us process issues more efficiently by filing an
|
||||
issue using one of the following templates:
|
||||
|
||||
**Current behavior**
|
||||
<!-- Describe how the bug manifests. -->
|
||||
https://github.com/angular/angular/issues/new/choose
|
||||
|
||||
**Expected behavior**
|
||||
<!-- Describe what the behavior would be without the bug. -->
|
||||
Thank you!
|
||||
|
||||
**Minimal reproduction of the problem with instructions**
|
||||
<!--
|
||||
If the current behavior is a bug or you can illustrate your feature request better with an example,
|
||||
please provide the *STEPS TO REPRODUCE* and if possible a *MINIMAL DEMO* of the problem via
|
||||
https://plnkr.co or similar (you can use this template as a starting point: http://plnkr.co/edit/tpl:AvJOMERrnz94ekVua0u5).
|
||||
-->
|
||||
|
||||
**What is the motivation / use case for changing the behavior?**
|
||||
<!-- Describe the motivation or the concrete use case -->
|
||||
|
||||
**Please tell us about your environment:**
|
||||
<!-- Operating system, IDE, package manager, HTTP server, ... -->
|
||||
|
||||
* **Angular version:** 2.0.X
|
||||
<!-- Check whether this is still an issue in the most recent Angular version -->
|
||||
|
||||
* **Browser:** [all | Chrome XX | Firefox XX | IE XX | Safari XX | Mobile Chrome XX | Android X.X Web Browser | iOS XX Safari | iOS XX UIWebView | iOS XX WKWebView ]
|
||||
<!-- All browsers where this could be reproduced -->
|
||||
|
||||
* **Language:** [all | TypeScript X.X | ES6/7 | ES5]
|
||||
|
||||
* **Node (for AoT issues):** `node --version` =
|
||||
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
||||
|
67
.github/ISSUE_TEMPLATE/1-bug-report.md
vendored
Normal file
67
.github/ISSUE_TEMPLATE/1-bug-report.md
vendored
Normal file
@ -0,0 +1,67 @@
|
||||
---
|
||||
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.
|
||||
|
||||
Issues that don't have enough info and can't be reproduced will be closed.
|
||||
|
||||
You can read more about issue submission guidelines here: https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-submitting-an-issue
|
||||
-->
|
||||
|
||||
## 🔥 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
|
||||
|
||||
### Relevant 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.
|
||||
|
||||
🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑
|
51
.github/PULL_REQUEST_TEMPLATE.md
vendored
51
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,36 +1,43 @@
|
||||
**Please check if the PR fulfills these requirements**
|
||||
## PR Checklist
|
||||
Please check if your PR fulfills the following requirements:
|
||||
|
||||
- [ ] The commit message follows our guidelines: https://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit
|
||||
- [ ] Tests for the changes have been added (for bug fixes / features)
|
||||
- [ ] Docs have been added / updated (for bug fixes / features)
|
||||
|
||||
|
||||
**What kind of change does this PR introduce?** (check one with "x")
|
||||
```
|
||||
[ ] Bugfix
|
||||
[ ] Feature
|
||||
[ ] Code style update (formatting, local variables)
|
||||
[ ] Refactoring (no functional changes, no api changes)
|
||||
[ ] Build related changes
|
||||
[ ] CI related changes
|
||||
[ ] Other... Please describe:
|
||||
```
|
||||
## PR Type
|
||||
What kind of change does this PR introduce?
|
||||
|
||||
**What is the current behavior?** (You can also link to an open issue here)
|
||||
<!-- Please check the one that applies to this PR using "x". -->
|
||||
|
||||
- [ ] Bugfix
|
||||
- [ ] Feature
|
||||
- [ ] Code style update (formatting, local variables)
|
||||
- [ ] Refactoring (no functional changes, no api changes)
|
||||
- [ ] Build related changes
|
||||
- [ ] CI related changes
|
||||
- [ ] Documentation content changes
|
||||
- [ ] angular.io application / infrastructure changes
|
||||
- [ ] Other... Please describe:
|
||||
|
||||
|
||||
## What is the current behavior?
|
||||
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->
|
||||
|
||||
**What is the new behavior?**
|
||||
Issue Number: N/A
|
||||
|
||||
|
||||
|
||||
**Does this PR introduce a breaking change?** (check one with "x")
|
||||
```
|
||||
[ ] Yes
|
||||
[ ] No
|
||||
```
|
||||
|
||||
If this PR contains a breaking change, please describe the impact and migration path for existing applications: ...
|
||||
## What is the new behavior?
|
||||
|
||||
|
||||
**Other information**:
|
||||
## Does this PR introduce a breaking change?
|
||||
|
||||
- [ ] Yes
|
||||
- [ ] No
|
||||
|
||||
|
||||
<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->
|
||||
|
||||
|
||||
## Other information
|
||||
|
164
.github/angular-robot.yml
vendored
Normal file
164
.github/angular-robot.yml
vendored
Normal file
@ -0,0 +1,164 @@
|
||||
# Configuration for angular-robot
|
||||
|
||||
#options for the size plugin
|
||||
size:
|
||||
disabled: false
|
||||
maxSizeIncrease: 2000
|
||||
circleCiStatusName: "ci/circleci: test_ivy_aot"
|
||||
|
||||
# options for the merge plugin
|
||||
merge:
|
||||
# the status will be added to your pull requests
|
||||
status:
|
||||
# set to true to disable
|
||||
disabled: false
|
||||
# the name of the status
|
||||
context: "ci/angular: merge status"
|
||||
# text to show when all checks pass
|
||||
successText: "All checks passed!"
|
||||
# text to show when some checks are failing
|
||||
failureText: "The following checks are failing:"
|
||||
|
||||
# the g3 status will be added to your pull requests if they include files that match the patterns
|
||||
g3Status:
|
||||
# set to true to disable
|
||||
disabled: false
|
||||
# the name of the status
|
||||
context: "google3"
|
||||
# text to show when the status is pending, {{PRNumber}} will be replaced by the PR number
|
||||
pendingDesc: "Googler: run g3sync presubmit {{PRNumber}}"
|
||||
# text to show when the status is success
|
||||
successDesc: "Does not affect google3"
|
||||
# link to use for the details
|
||||
url: "http://go/angular-g3sync"
|
||||
# list of patterns to check for the files changed by the PR
|
||||
# this list must be manually kept in sync with google3/third_party/javascript/angular2/copy.bara.sky
|
||||
include:
|
||||
- "LICENSE"
|
||||
- "modules/**"
|
||||
- "packages/**"
|
||||
# list of patterns to ignore for the files changed by the PR
|
||||
exclude:
|
||||
- "packages/*"
|
||||
- "packages/bazel/*"
|
||||
- "packages/bazel/src/builders/**"
|
||||
- "packages/bazel/src/ng_package/**"
|
||||
- "packages/bazel/src/protractor/**"
|
||||
- "packages/bazel/src/schematics/**"
|
||||
- "packages/compiler-cli/src/ngcc/**"
|
||||
- "packages/docs/**"
|
||||
- "packages/elements/schematics/**"
|
||||
- "packages/examples/**"
|
||||
- "packages/language-service/**"
|
||||
- "packages/private/**"
|
||||
- "packages/service-worker/**"
|
||||
- "**/.gitignore"
|
||||
- "**/.gitkeep"
|
||||
- "**/yarn.lock"
|
||||
- "**/package.json"
|
||||
- "**/tsconfig-build.json"
|
||||
- "**/tsconfig.json"
|
||||
- "**/rollup.config.js"
|
||||
- "**/BUILD.bazel"
|
||||
- "**/*.md"
|
||||
- "packages/**/integrationtest/**"
|
||||
- "packages/**/test/**"
|
||||
|
||||
# comment that will be added to a PR when there is a conflict, leave empty or set to false to disable
|
||||
mergeConflictComment: "Hi @{{PRAuthor}}! This PR has merge conflicts due to recent upstream merges.
|
||||
\nPlease help to unblock it by resolving these conflicts. Thanks!"
|
||||
|
||||
# label to monitor
|
||||
mergeLabel: "PR action: merge"
|
||||
|
||||
# adding any of these labels will also add the merge label
|
||||
mergeLinkedLabels:
|
||||
- "PR action: merge-assistance"
|
||||
|
||||
# list of checks that will determine if the merge label can be added
|
||||
checks:
|
||||
|
||||
# require that the PR has reviews from all requested reviewers
|
||||
#
|
||||
# This enables us to request reviews from both eng and tech writers, or multiple eng folks, and prevents accidental merges.
|
||||
# Rather than merging PRs with pending reviews, if all PullApprove requirements are satisfied and additional reviews are not needed pending reviewers should be removed via GitHub UI (this also leaves an audit trail behind these decisions).
|
||||
requireReviews: true,
|
||||
|
||||
# whether the PR shouldn't have a conflict with the base branch
|
||||
noConflict: true
|
||||
# list of labels that a PR needs to have, checked with a regexp (e.g. "PR target:" will work for the label "PR target: master")
|
||||
requiredLabels:
|
||||
- "PR target: *"
|
||||
- "cla: yes"
|
||||
|
||||
# list of labels that a PR shouldn't have, checked after the required labels with a regexp
|
||||
forbiddenLabels:
|
||||
- "PR target: TBD"
|
||||
- "PR action: cleanup"
|
||||
- "PR action: review"
|
||||
- "PR state: blocked"
|
||||
- "cla: no"
|
||||
|
||||
# list of PR statuses that need to be successful
|
||||
requiredStatuses:
|
||||
- "continuous-integration/travis-ci/pr"
|
||||
- "code-review/pullapprove"
|
||||
- "ci/circleci: build"
|
||||
- "ci/circleci: lint"
|
||||
|
||||
# the comment that will be added when the merge label is added despite failing checks, leave empty or set to false to disable
|
||||
# {{MERGE_LABEL}} will be replaced by the value of the mergeLabel option
|
||||
# {{PLACEHOLDER}} will be replaced by the list of failing checks
|
||||
mergeRemovedComment: "I see that you just added the `{{MERGE_LABEL}}` label, but the following checks are still failing:
|
||||
\n{{PLACEHOLDER}}
|
||||
\n
|
||||
\n**If you want your PR to be merged, it has to pass all the CI checks.**
|
||||
\n
|
||||
\nIf you can't get the PR to a green state due to flakes or broken master, please try rebasing to master and/or restarting the CI job. If that fails and you believe that the issue is not due to your change, please contact the caretaker and ask for help."
|
||||
|
||||
# options for the triage plugin
|
||||
triage:
|
||||
# number of the milestone to apply when the issue has not been triaged yet
|
||||
needsTriageMilestone: 83,
|
||||
# number of the milestone to apply when the issue is triaged
|
||||
defaultMilestone: 82,
|
||||
# arrays of labels that determine if an issue has been triaged by the caretaker
|
||||
l1TriageLabels:
|
||||
-
|
||||
- "comp: *"
|
||||
# arrays of labels that determine if an issue has been fully triaged
|
||||
l2TriageLabels:
|
||||
-
|
||||
- "type: bug/fix"
|
||||
- "severity*"
|
||||
- "freq*"
|
||||
- "comp: *"
|
||||
-
|
||||
- "type: feature"
|
||||
- "comp: *"
|
||||
-
|
||||
- "type: refactor"
|
||||
- "comp: *"
|
||||
-
|
||||
- "type: RFC / Discussion / question"
|
||||
- "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: *"
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -1,8 +1,12 @@
|
||||
.DS_STORE
|
||||
|
||||
/dist/
|
||||
/bazel-*
|
||||
/integration/bazel/bazel-*
|
||||
e2e_test.*
|
||||
node_modules
|
||||
bower_components
|
||||
tools/gulp-tasks/cldr/cldr-data/
|
||||
|
||||
# Include when developing application packages.
|
||||
pubspec.lock
|
||||
@ -26,3 +30,7 @@ yarn-error.log
|
||||
|
||||
# rollup-test output
|
||||
/modules/rollup-test/dist/
|
||||
|
||||
# User specific bazel settings
|
||||
.bazelrc.user
|
||||
|
||||
|
389
.pullapprove.yml
389
.pullapprove.yml
@ -7,21 +7,25 @@
|
||||
#
|
||||
# alexeagle - Alex Eagle
|
||||
# alxhub - Alex Rickabaugh
|
||||
# chuckjaz - Chuck Jazdzewski
|
||||
# andrewseguin - Andrew Seguin
|
||||
# benlesh - Ben Lesh
|
||||
# brandonroberts - Brandon Roberts
|
||||
# brocco - Mike Brocchi
|
||||
# filipesilva - Filipe Silva
|
||||
# gkalpak - George Kalpakas
|
||||
# hansl - Hans Larsen
|
||||
# IgorMinar - Igor Minar
|
||||
# jasonaden - Jason Aden
|
||||
# jenniferfell - Jennifer Fell
|
||||
# kara - Kara Erickson
|
||||
# kyliau - Keen Yee Liau
|
||||
# matsko - Matias Niemelä
|
||||
# mhevery - Misko Hevery
|
||||
# petebacondarwin - Pete Bacon Darwin
|
||||
# pkozlowski-opensource - Pawel Kozlowski
|
||||
# robwormald - Rob Wormald
|
||||
# tbosch - Tobias Bosch
|
||||
# vicb - Victor Berchet
|
||||
# vikerman - Vikram Subramanian
|
||||
# wardbell - Ward Bell
|
||||
# tinayuangao - Tina Gao
|
||||
|
||||
|
||||
version: 2
|
||||
|
||||
@ -31,20 +35,41 @@ group_defaults:
|
||||
enabled: true
|
||||
approve_by_comment:
|
||||
enabled: false
|
||||
# see http://docs.pullapprove.com/groups/author_approval/
|
||||
author_approval:
|
||||
# If the author is a reviewer on the PR, they will automatically have an "approved" status.
|
||||
auto: true
|
||||
|
||||
groups:
|
||||
# Require all PRs to have at least one approval from *someone*
|
||||
all:
|
||||
users: all
|
||||
required: 1
|
||||
rejection_value: -999
|
||||
# In this group, your self-approval does not count
|
||||
author_approval:
|
||||
auto: false
|
||||
ignored: true
|
||||
files:
|
||||
include:
|
||||
- "*"
|
||||
|
||||
root:
|
||||
conditions:
|
||||
files:
|
||||
include:
|
||||
- "*"
|
||||
exclude:
|
||||
- "WORKSPACE"
|
||||
- "BUILD.bazel"
|
||||
- ".circleci/*"
|
||||
- "aio/*"
|
||||
- "integration/*"
|
||||
- "modules/*"
|
||||
- "packages/*"
|
||||
- "tools/*"
|
||||
users:
|
||||
- alexeagle
|
||||
- IgorMinar
|
||||
- mhevery
|
||||
|
||||
@ -57,6 +82,24 @@ groups:
|
||||
- IgorMinar
|
||||
- mhevery
|
||||
|
||||
bazel:
|
||||
conditions:
|
||||
files:
|
||||
include:
|
||||
- "WORKSPACE"
|
||||
- ".bazel*"
|
||||
- "*.bazel"
|
||||
- "*.bzl"
|
||||
- "packages/bazel/*"
|
||||
- "/docs/BAZEL.md"
|
||||
users:
|
||||
- alexeagle #primary
|
||||
- kyliau
|
||||
- IgorMinar #fallback
|
||||
- mhevery
|
||||
- vikerman #fallback
|
||||
- kara
|
||||
|
||||
build-and-ci:
|
||||
conditions:
|
||||
files:
|
||||
@ -66,9 +109,9 @@ groups:
|
||||
- "*.lock"
|
||||
- "tools/*"
|
||||
exclude:
|
||||
- "tools/@angular/tsc-wrapped/*"
|
||||
- "tools/public_api_guard/*"
|
||||
- "aio/*"
|
||||
- "packages/core/test/bundling/*"
|
||||
- "tools/public_api_guard/*"
|
||||
users:
|
||||
- IgorMinar #primary
|
||||
- alexeagle
|
||||
@ -82,175 +125,417 @@ groups:
|
||||
users:
|
||||
- alexeagle
|
||||
- mhevery
|
||||
- tbosch
|
||||
- vicb
|
||||
- IgorMinar #fallback
|
||||
|
||||
|
||||
core:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/core/*"
|
||||
- "aio/content/guide/bootstrapping.md"
|
||||
- "aio/content/examples/bootstrapping/*"
|
||||
- "aio/content/guide/attribute-directives.md"
|
||||
- "aio/content/examples/attribute-directives/*"
|
||||
- "aio/content/images/guide/attribute-directives/*"
|
||||
- "aio/content/guide/structural-directives.md"
|
||||
- "aio/content/examples/structural-directives/*"
|
||||
- "aio/content/images/guide/structural-directives/*"
|
||||
- "aio/content/guide/dynamic-component-loader.md"
|
||||
- "aio/content/examples/dynamic-component-loader/*"
|
||||
- "aio/content/images/guide/dynamic-component-loader/*"
|
||||
- "aio/content/guide/template-syntax.md"
|
||||
- "aio/content/examples/template-syntax/*"
|
||||
- "aio/content/images/guide/template-syntax/*"
|
||||
- "aio/content/guide/dependency-injection.md"
|
||||
- "aio/content/examples/dependency-injection/*"
|
||||
- "aio/content/images/guide/dependency-injection/*"
|
||||
- "aio/content/guide/dependency-injection-in-action.md"
|
||||
- "aio/content/examples/dependency-injection-in-action/*"
|
||||
- "aio/content/images/guide/dependency-injection-in-action/*"
|
||||
- "aio/content/guide/hierarchical-dependency-injection.md"
|
||||
- "aio/content/examples/hierarchical-dependency-injection/*"
|
||||
- "aio/content/guide/singleton-services.md"
|
||||
- "aio/content/guide/dependency-injection-pattern.md"
|
||||
- "aio/content/guide/providers.md"
|
||||
- "aio/content/examples/providers/*"
|
||||
- "aio/content/guide/component-interaction.md"
|
||||
- "aio/content/examples/component-interaction/*"
|
||||
- "aio/content/images/guide/component-interaction/*"
|
||||
- "aio/content/guide/component-styles.md"
|
||||
- "aio/content/examples/component-styles/*"
|
||||
- "aio/content/guide/lifecycle-hooks.md"
|
||||
- "aio/content/examples/lifecycle-hooks/*"
|
||||
- "aio/content/images/guide/lifecycle-hooks/*"
|
||||
- "aio/content/examples/ngcontainer/*"
|
||||
- "aio/content/images/guide/ngcontainer/*"
|
||||
- "aio/content/guide/pipes.md"
|
||||
- "aio/content/examples/pipes/*"
|
||||
- "aio/content/images/guide/pipes/*"
|
||||
- "aio/content/guide/entry-components.md"
|
||||
- "aio/content/guide/set-document-title.md"
|
||||
- "aio/content/examples/set-document-title/*"
|
||||
- "aio/content/images/guide/set-document-title/*"
|
||||
- "aio/content/guide/ngmodules.md"
|
||||
- "aio/content/examples/ngmodules/*"
|
||||
- "aio/content/examples/ngmodule/*"
|
||||
- "aio/content/images/guide/ngmodule/*"
|
||||
- "aio/content/guide/ngmodule-faq.md"
|
||||
- "aio/content/examples/ngmodule-faq/*"
|
||||
- "aio/content/guide/module-types.md"
|
||||
- "aio/content/guide/sharing-ngmodules.md"
|
||||
- "aio/content/guide/frequent-ngmodules.md"
|
||||
- "aio/content/images/guide/frequent-ngmodules/*"
|
||||
- "aio/content/guide/ngmodule-api.md"
|
||||
- "aio/content/guide/ngmodule-vs-jsmodule.md"
|
||||
- "aio/content/guide/feature-modules.md"
|
||||
- "aio/content/examples/feature-modules/*"
|
||||
- "aio/content/images/guide/feature-modules/*"
|
||||
- "aio/content/guide/lazy-loading-ngmodules.md"
|
||||
- "aio/content/examples/lazy-loading-ngmodules/*"
|
||||
- "aio/content/images/guide/lazy-loading-ngmodules"
|
||||
users:
|
||||
- tbosch #primary
|
||||
- mhevery
|
||||
- vicb
|
||||
- IgorMinar #fallback
|
||||
- mhevery #primary
|
||||
- jasonaden
|
||||
- kara
|
||||
- IgorMinar
|
||||
- jenniferfell #docs only
|
||||
|
||||
compiler/animations:
|
||||
animations:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/compiler/src/animation/*"
|
||||
- "packages/animations/*"
|
||||
- "packages/platform-browser/animations/*"
|
||||
- "aio/content/guide/animations.md"
|
||||
- "aio/content/examples/animations/*"
|
||||
- "aio/content/images/guide/animations/*"
|
||||
users:
|
||||
- matsko #primary
|
||||
- tbosch
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
- IgorMinar #fallback
|
||||
- jenniferfell #docs only
|
||||
- kara
|
||||
|
||||
compiler/i18n:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/compiler/src/i18n/*"
|
||||
- "aio/content/guide/i18n.md"
|
||||
- "aio/content/examples/i18n/*"
|
||||
users:
|
||||
- vicb #primary
|
||||
- tbosch
|
||||
- alxhub #primary
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
- jenniferfell #docs only
|
||||
- kara
|
||||
|
||||
compiler:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/compiler/*"
|
||||
- "aio/content/guide/aot-compiler.md"
|
||||
users:
|
||||
- tbosch #primary
|
||||
- vicb
|
||||
- chuckjaz
|
||||
- alxhub #primary
|
||||
- mhevery
|
||||
- IgorMinar #fallback
|
||||
- jenniferfell #docs only
|
||||
- kara
|
||||
|
||||
compiler-cli/ngtools:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/compiler-cli/src/ngtools*"
|
||||
users:
|
||||
- hansl
|
||||
- filipesilva #fallback
|
||||
- IgorMinar #fallback
|
||||
- kara
|
||||
|
||||
compiler-cli:
|
||||
conditions:
|
||||
files:
|
||||
- "tools/@angular/tsc-wrapped/*"
|
||||
- "packages/compiler-cli/*"
|
||||
include:
|
||||
- "packages/compiler-cli/*"
|
||||
exclude:
|
||||
- "packages/compiler-cli/src/ngtools*"
|
||||
users:
|
||||
- alexeagle
|
||||
- chuckjaz
|
||||
- tbosch
|
||||
- alxhub
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
- kara
|
||||
|
||||
common:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/common/*"
|
||||
include:
|
||||
- "packages/common/*"
|
||||
exclude:
|
||||
- "packages/common/http/*"
|
||||
users:
|
||||
- pkozlowski-opensource #primary
|
||||
- vicb
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
- kara
|
||||
|
||||
forms:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/forms/*"
|
||||
- "aio/content/guide/forms.md"
|
||||
- "aio/content/examples/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/examples/form-validation/*"
|
||||
- "aio/content/images/guide/form-validation/*"
|
||||
- "aio/content/guide/dynamic-form.md"
|
||||
- "aio/content/examples/dynamic-form/*"
|
||||
- "aio/content/images/guide/dynamic-form/*"
|
||||
- "aio/content/guide/reactive-forms.md"
|
||||
- "aio/content/examples/reactive-forms/*"
|
||||
- "aio/content/images/guide/reactive-forms/*"
|
||||
users:
|
||||
- kara #primary
|
||||
- tinayuangao #secondary
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
- jenniferfell #docs only
|
||||
|
||||
http:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/common/http/*"
|
||||
- "packages/http/*"
|
||||
- "aio/content/guide/http.md"
|
||||
- "aio/content/examples/http/*"
|
||||
- "aio/content/images/guide/http/*"
|
||||
users:
|
||||
- vikerman #primary
|
||||
- alxhub
|
||||
- IgorMinar #fallback
|
||||
- alxhub #primary
|
||||
- IgorMinar
|
||||
- mhevery #fallback
|
||||
- jenniferfell #docs only
|
||||
|
||||
language-service:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/language-service/*"
|
||||
- "aio/content/guide/language-service.md"
|
||||
- "aio/content/images/guide/language-service/*"
|
||||
users:
|
||||
- chuckjaz #primary
|
||||
- kyliau #primary
|
||||
# needs secondary
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
- jenniferfell #docs only
|
||||
|
||||
router:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/router/*"
|
||||
- "aio/content/guide/router.md"
|
||||
- "aio/content/examples/router/*"
|
||||
- "aio/content/images/guide/router/*"
|
||||
users:
|
||||
- vicb #primary
|
||||
# needs secondary
|
||||
- jasonaden #primary
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
- jenniferfell #docs only
|
||||
- kara
|
||||
|
||||
testing:
|
||||
conditions:
|
||||
files:
|
||||
- "*/testing/*"
|
||||
- "aio/content/guide/testing.md"
|
||||
- "aio/content/examples/testing/*"
|
||||
- "aio/content/images/guide/testing/*"
|
||||
users:
|
||||
- vikerman
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
- jenniferfell #docs only
|
||||
|
||||
upgrade:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/upgrade/*"
|
||||
- "aio/content/guide/upgrade.md"
|
||||
- "aio/content/examples/upgrade-module/*"
|
||||
- "aio/content/images/guide/upgrade/*"
|
||||
- "aio/content/examples/upgrade-phonecat-1-typescript/*"
|
||||
- "aio/content/examples/upgrade-phonecat-2-hybrid/*"
|
||||
- "aio/content/examples/upgrade-phonecat-3-final/*"
|
||||
- "aio/content/guide/upgrade-performance.md"
|
||||
- "aio/content/guide/ajs-quick-reference.md"
|
||||
- "aio/content/examples/ajs-quick-reference/*"
|
||||
users:
|
||||
- petebacondarwin #primary
|
||||
- gkalpak
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
- jenniferfell #docs only
|
||||
- kara
|
||||
|
||||
platform-browser:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/platform-browser/*"
|
||||
users:
|
||||
- tbosch #primary
|
||||
- vicb #secondary
|
||||
- mhevery #primary
|
||||
# needs secondary
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
- kara
|
||||
|
||||
platform-server:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/platform-server/*"
|
||||
- "aio/content/guide/universal.md"
|
||||
- "aio/content/examples/universal/*"
|
||||
users:
|
||||
- vikerman #primary
|
||||
- alxhub
|
||||
- vicb
|
||||
- tbosch
|
||||
- alxhub #secondary
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
- jenniferfell #docs only
|
||||
|
||||
platform-webworker:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/platform-webworker/*"
|
||||
users:
|
||||
- vicb #primary
|
||||
- tbosch #secondary
|
||||
- mhevery #primary
|
||||
# needs secondary
|
||||
- IgorMinar #fallback
|
||||
- kara
|
||||
|
||||
service-worker:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/service-worker/*"
|
||||
- "aio/content/guide/service-worker-getting-started.md"
|
||||
- "aio/content/examples/service-worker-getting-started/*"
|
||||
- "aio/content/guide/service-worker-communications.md"
|
||||
- "aio/content/guide/service-worker-config.md"
|
||||
- "aio/content/guide/service-worker-devops.md"
|
||||
- "aio/content/guide/service-worker-intro.md"
|
||||
- "aio/content/images/guide/service-worker/*"
|
||||
users:
|
||||
- gkalpak #primary
|
||||
- alxhub
|
||||
- IgorMinar
|
||||
- mhevery #fallback
|
||||
- jenniferfell #docs only
|
||||
|
||||
elements:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/elements/*"
|
||||
- "aio/content/examples/elements/*"
|
||||
- "aio/content/images/guide/elements/*"
|
||||
- "aio/content/guide/elements.md"
|
||||
users:
|
||||
- andrewseguin #primary
|
||||
- gkalpak
|
||||
- robwormald
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
|
||||
- jenniferfell #docs only
|
||||
- kara
|
||||
|
||||
benchpress:
|
||||
conditions:
|
||||
files:
|
||||
- "packages/benchpress/*"
|
||||
users:
|
||||
- tbosch #primary
|
||||
- alxhub # primary
|
||||
# needs secondary
|
||||
- IgorMinar #fallback
|
||||
- mhevery #fallback
|
||||
|
||||
angular.io:
|
||||
docs-infra:
|
||||
conditions:
|
||||
files:
|
||||
- "aio/*"
|
||||
include:
|
||||
- "aio/*"
|
||||
exclude:
|
||||
- "aio/content/*"
|
||||
users:
|
||||
- petebacondarwin #primary
|
||||
- IgorMinar
|
||||
- gkalpak
|
||||
- mhevery #fallback
|
||||
|
||||
docs/guide-and-tutorial:
|
||||
conditions:
|
||||
files:
|
||||
include:
|
||||
- "aio/content/*"
|
||||
exclude:
|
||||
- "aio/content/marketing/*"
|
||||
- "aio/content/navigation.json"
|
||||
- "aio/content/license.md"
|
||||
users:
|
||||
- stephenfluin
|
||||
- jenniferfell
|
||||
- brandonroberts
|
||||
- petebacondarwin
|
||||
- gkalpak
|
||||
- IgorMinar
|
||||
- mhevery #fallback
|
||||
|
||||
docs/marketing:
|
||||
conditions:
|
||||
files:
|
||||
include:
|
||||
- "aio/content/marketing/*"
|
||||
- "aio/content/images/marketing/*"
|
||||
- "aio/content/navigation.json"
|
||||
- "aio/content/license.md"
|
||||
users:
|
||||
- stephenfluin
|
||||
- petebacondarwin
|
||||
- gkalpak
|
||||
- IgorMinar
|
||||
- robwormald
|
||||
- mhevery #fallback
|
||||
|
||||
docs/observables:
|
||||
conditions:
|
||||
files:
|
||||
- "aio/content/examples/observables/*"
|
||||
- "aio/content/images/guide/observables/*"
|
||||
- "aio/content/guide/observables.md"
|
||||
- "aio/content/guide/comparing-observables.md"
|
||||
- "aio/content/examples/observables-in-angular/*"
|
||||
- "aio/content/images/guide/observables-in-angular/*"
|
||||
- "aio/content/guide/observables-in-angular.md"
|
||||
- "aio/content/examples/practical-observable-usage/*"
|
||||
- "aio/content/guide/practical-observable-usage.md"
|
||||
- "aio/content/examples/rx-library/*"
|
||||
- "aio/content/guide/rx-library.md"
|
||||
users:
|
||||
- jasonaden
|
||||
- benlesh
|
||||
- IgorMinar
|
||||
- mhevery
|
||||
- jenniferfell #docs only
|
||||
|
||||
docs/packaging:
|
||||
conditions:
|
||||
files:
|
||||
- "aio/content/guide/npm-packages.md"
|
||||
- "aio/content/guide/browser-support.md"
|
||||
- "aio/content/guide/typescript-configuration.md"
|
||||
- "aio/content/guide/setup-systemjs-anatomy.md"
|
||||
- "aio/content/examples/setup/*"
|
||||
- "aio/content/guide/setup.md"
|
||||
- "aio/content/guide/deployment.md"
|
||||
- "aio/content/guide/releases.md"
|
||||
- "aio/content/guide/updating.md"
|
||||
users:
|
||||
- IgorMinar #primary
|
||||
- petebacondarwin #secondary
|
||||
- gkalpak
|
||||
- wardbell
|
||||
- alexeagle
|
||||
- hansl
|
||||
- mhevery #fallback
|
||||
- jenniferfell #docs only
|
||||
|
31
.travis.yml
31
.travis.yml
@ -1,7 +1,8 @@
|
||||
language: node_js
|
||||
sudo: false
|
||||
dist: trusty
|
||||
node_js:
|
||||
- '6.9.5'
|
||||
- '10.9.0'
|
||||
|
||||
addons:
|
||||
# firefox: "38.0"
|
||||
@ -12,11 +13,7 @@ addons:
|
||||
packages:
|
||||
# needed to install g++ that is used by npms's native modules
|
||||
- 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:
|
||||
except:
|
||||
- g3
|
||||
@ -32,22 +29,20 @@ env:
|
||||
global:
|
||||
# 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.
|
||||
- secure: "rNqXoy2gqjbF5tBXlRBy+oiYntO3BtzcxZuEtlLMzNaTNzC4dyMOFub0GkzIPWwOzkARoEU9Kv+bC97fDVbCBUKeyzzEqxqddUKhzRxeaYjsefJ6XeTvBvDxwo7wDwyxZSuWdBeGAe4eARVHm7ypsd+AlvqxtzjyS27TK2BzdL4="
|
||||
# FIREBASE_TOKEN
|
||||
# This is needed for publishing builds to the "aio-staging" firebase site.
|
||||
# TODO(i): the token was generated using the iminar@google account, we should switch to a shared/role-base account.
|
||||
- secure: "MPx3UM77o5IlhT75PKHL0FXoB5tSXDc3vnCXCd1sRy4XUTZ9vjcV6nNuyqEf+SOw659bGbC1FI4mACGx1Q+z7MQDR85b1mcA9uSgHDkh+IR82CnCVdaX9d1RXafdJIArahxfmorbiiPPLyPIKggo7ituRm+2c+iraoCkE/pXxYg="
|
||||
- secure: "aCdHveZuY8AT4Jr1JoJB4LxZsnGWRe/KseZh1YXYe5UtufFCtTVHvUcLn0j2aLBF0KpdyS+hWf0i4np9jthKu2xPKriefoPgCMpisYeC0MFkwbmv+XlgkUbgkgVZMGiVyX7DCYXVahxIoOUjVMEDCbNiHTIrfEuyq24U3ok2tHc="
|
||||
matrix:
|
||||
# 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_2
|
||||
- CI_MODE=js
|
||||
- CI_MODE=saucelabs_required
|
||||
- CI_MODE=browserstack_required
|
||||
- CI_MODE=saucelabs_optional
|
||||
- CI_MODE=browserstack_optional
|
||||
- CI_MODE=docs_test
|
||||
- CI_MODE=aio
|
||||
# deactivated, see #19768
|
||||
# - CI_MODE=browserstack_required
|
||||
|
||||
# We disable these optional jobs because those acquire tunnel and browser instances which
|
||||
# could lead to rate limit excess while those are failing most of the time and nobody pays
|
||||
# attention anyway.
|
||||
# - CI_MODE=saucelabs_optional
|
||||
# - CI_MODE=browserstack_optional
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
@ -65,8 +60,6 @@ install:
|
||||
script:
|
||||
- ./scripts/ci/build.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
|
||||
# all the scripts under this line will not quickly abort in case ${TRAVIS_TEST_RESULT} is 1 (job failure)
|
||||
- ./scripts/ci/cleanup.sh
|
||||
|
56
BUILD.bazel
Normal file
56
BUILD.bazel
Normal file
@ -0,0 +1,56 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load("@build_bazel_rules_nodejs//:defs.bzl", "node_modules_filegroup")
|
||||
|
||||
exports_files([
|
||||
"tsconfig.json",
|
||||
"LICENSE",
|
||||
"protractor-perf.conf.js",
|
||||
])
|
||||
|
||||
filegroup(
|
||||
name = "web_test_bootstrap_scripts",
|
||||
# do not sort
|
||||
srcs = [
|
||||
"@ngdeps//node_modules/reflect-metadata:Reflect.js",
|
||||
"@ngdeps//node_modules/zone.js:dist/zone.js",
|
||||
"@ngdeps//node_modules/zone.js:dist/zone-testing.js",
|
||||
"@ngdeps//node_modules/zone.js:dist/task-tracking.js",
|
||||
"//:test-events.js",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "angularjs_scripts",
|
||||
srcs = [
|
||||
# We also declare the unminfied AngularJS files since these can be used for
|
||||
# local debugging (e.g. see: packages/upgrade/test/common/test_helpers.ts)
|
||||
"@ngdeps//node_modules/angular:angular.js",
|
||||
"@ngdeps//node_modules/angular:angular.min.js",
|
||||
"@ngdeps//node_modules/angular-1.5:angular.js",
|
||||
"@ngdeps//node_modules/angular-1.5:angular.min.js",
|
||||
"@ngdeps//node_modules/angular-1.6:angular.js",
|
||||
"@ngdeps//node_modules/angular-1.6:angular.min.js",
|
||||
"@ngdeps//node_modules/angular-mocks:angular-mocks.js",
|
||||
"@ngdeps//node_modules/angular-mocks-1.5:angular-mocks.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"],
|
||||
)
|
6277
CHANGELOG.md
6277
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
12
CODE_OF_CONDUCT.md
Normal file
12
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,12 @@
|
||||
# Contributor Code of Conduct
|
||||
## Version 0.3b-angular
|
||||
|
||||
As contributors and maintainers of the Angular project, we pledge to respect everyone who contributes by posting issues, updating documentation, submitting pull requests, providing feedback in comments, and any other activities.
|
||||
|
||||
Communication through any of Angular's channels (GitHub, Gitter, IRC, mailing lists, Google+, Twitter, etc.) must be constructive and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
|
||||
|
||||
We promise to extend courtesy and respect to everyone involved in this project regardless of gender, gender identity, sexual orientation, disability, age, race, ethnicity, religion, or level of experience. We expect anyone contributing to the Angular project to do the same.
|
||||
|
||||
If any member of the community violates this code of conduct, the maintainers of the Angular project may take action, removing issues, comments, and PRs or blocking accounts as deemed appropriate.
|
||||
|
||||
If you are subject to or witness unacceptable behavior, or have any other concerns, please email us at [conduct@angular.io](mailto:conduct@angular.io).
|
@ -17,15 +17,15 @@ Help us keep Angular open and inclusive. Please read and follow our [Code of Con
|
||||
|
||||
## <a name="question"></a> Got a Question or Problem?
|
||||
|
||||
Please, do not open issues for the general support questions as we want to keep GitHub issues for bug reports and feature requests. You've got much better chances of getting your question answered on [StackOverflow](https://stackoverflow.com/questions/tagged/angular) where the questions should be tagged with tag `angular`.
|
||||
Do not open issues for general support questions as we want to keep GitHub issues for bug reports and feature requests. You've got much better chances of getting your question answered on [Stack Overflow](https://stackoverflow.com/questions/tagged/angular) where the questions should be tagged with tag `angular`.
|
||||
|
||||
StackOverflow is a much better place to ask questions since:
|
||||
Stack Overflow is a much better place to ask questions since:
|
||||
|
||||
- there are thousands of people willing to help on StackOverflow
|
||||
- there are thousands of people willing to help on Stack Overflow
|
||||
- questions and answers stay available for public viewing so your question / answer might help someone else
|
||||
- StackOverflow's voting system assures that the best answers are prominently visible.
|
||||
- Stack Overflow's voting system assures that the best answers are prominently visible.
|
||||
|
||||
To save your and our time we will be systematically closing all the issues that are requests for general support and redirecting people to StackOverflow.
|
||||
To save your and our time, we will systematically close all issues that are requests for general support and redirect people to Stack Overflow.
|
||||
|
||||
If you would like to chat about the question in real-time, you can reach out via [our gitter channel][gitter].
|
||||
|
||||
@ -51,54 +51,53 @@ and help you to craft the change so that it is successfully accepted into the pr
|
||||
|
||||
Before you submit an issue, please search the issue tracker, maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available.
|
||||
|
||||
We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it. In order to reproduce bugs we will systematically ask you to provide a minimal reproduction scenario using http://plnkr.co. Having a live, reproducible scenario gives us wealth of important information without going back & forth to you with additional questions like:
|
||||
We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it. In order to reproduce bugs, we will systematically ask you to provide a minimal reproduction. Having a minimal reproducible scenario gives us a wealth of important information without going back & forth to you with additional questions.
|
||||
|
||||
- version of Angular used
|
||||
- 3rd-party libraries and their versions
|
||||
- and most importantly - a use-case that fails
|
||||
A minimal reproduction allows us to quickly confirm a bug (or point out a coding problem) as well as confirm that we are fixing the right problem.
|
||||
|
||||
A minimal reproduce scenario using http://plnkr.co/ allows us to quickly confirm a bug (or point out coding problem) as well as confirm that we are fixing the right problem. If plunker is not a suitable way to demonstrate the problem (for example for issues related to our npm packaging), please create a standalone git repository demonstrating the problem.
|
||||
We will be insisting on a minimal reproduction scenario in order to save maintainers time and ultimately be able to fix more bugs. Interestingly, from our experience users often find coding problems themselves while preparing a minimal reproduction. We understand that sometimes it might be hard to extract essential bits of code from a larger code-base but we really need to isolate the problem before we can fix it.
|
||||
|
||||
We will be insisting on a minimal reproduce scenario in order to save maintainers time and ultimately be able to fix more bugs. Interestingly, from our experience users often find coding problems themselves while preparing a minimal plunk. We understand that sometimes it might be hard to extract essentials bits of code from a larger code-base but we really need to isolate the problem before we can fix it.
|
||||
Unfortunately, we are not able to investigate / fix bugs without a minimal reproduction, so if we don't hear back from you we are going to close an issue that doesn't have enough info to be reproduced.
|
||||
|
||||
Unfortunately we are not able to investigate / fix bugs without a minimal reproduction, so if we don't hear back from you we are going to close an issue that don't have enough info to be reproduced.
|
||||
|
||||
You can file new issues by filling out our [new issue form](https://github.com/angular/angular/issues/new).
|
||||
You can file new issues by selecting from our [new issue templates](https://github.com/angular/angular/issues/new/choose) and filling out the issue template.
|
||||
|
||||
|
||||
### <a name="submit-pr"></a> Submitting a Pull Request (PR)
|
||||
Before you submit your Pull Request (PR) consider the following guidelines:
|
||||
|
||||
* 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.
|
||||
* Please sign our [Contributor License Agreement (CLA)](#cla) before sending PRs.
|
||||
We cannot accept code without this.
|
||||
* Make your changes in a new git branch:
|
||||
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.
|
||||
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. Make your changes in a new git branch:
|
||||
|
||||
```shell
|
||||
git checkout -b my-fix-branch master
|
||||
```
|
||||
|
||||
* Create your patch, **including appropriate test cases**.
|
||||
* Follow our [Coding Rules](#rules).
|
||||
* Run the full Angular test suite, as described in the [developer documentation][dev-doc],
|
||||
1. Create your patch, **including appropriate test cases**.
|
||||
1. Follow our [Coding Rules](#rules).
|
||||
1. Run the full Angular test suite, as described in the [developer documentation][dev-doc],
|
||||
and ensure that all tests pass.
|
||||
* Commit your changes using a descriptive commit message that follows our
|
||||
1. Commit your changes using a descriptive commit message that follows our
|
||||
[commit message conventions](#commit). Adherence to these conventions
|
||||
is necessary because release notes are automatically generated from these messages.
|
||||
|
||||
```shell
|
||||
git commit -a
|
||||
```
|
||||
Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files.
|
||||
Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files.
|
||||
|
||||
* Push your branch to GitHub:
|
||||
1. Push your branch to GitHub:
|
||||
|
||||
```shell
|
||||
git push origin my-fix-branch
|
||||
```
|
||||
|
||||
* In GitHub, send a pull request to `angular:master`.
|
||||
1. In GitHub, send a pull request to `angular:master`.
|
||||
* If we suggest changes then:
|
||||
* Make the required updates.
|
||||
* Re-run the Angular test suites to ensure tests are still passing.
|
||||
@ -172,12 +171,12 @@ The **header** is mandatory and the **scope** of the header is optional.
|
||||
Any line of the commit message cannot be longer 100 characters! This allows the message to be easier
|
||||
to read on GitHub as well as in various git tools.
|
||||
|
||||
Footer should contain a [closing reference to an issue](https://help.github.com/articles/closing-issues-via-commit-messages/) if any.
|
||||
The footer should contain a [closing reference to an issue](https://help.github.com/articles/closing-issues-via-commit-messages/) if any.
|
||||
|
||||
Samples: (even more [samples](https://github.com/angular/angular/commits/master))
|
||||
|
||||
```
|
||||
docs(changelog): update change log to beta.5
|
||||
docs(changelog): update changelog to beta.5
|
||||
```
|
||||
```
|
||||
fix(release): need to depend on latest rxjs and zone.js
|
||||
@ -198,19 +197,20 @@ Must be one of the following:
|
||||
* **fix**: A bug fix
|
||||
* **perf**: A code change that improves performance
|
||||
* **refactor**: A code change that neither fixes a bug nor adds a feature
|
||||
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing
|
||||
semi-colons, etc)
|
||||
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
||||
* **test**: Adding missing tests or correcting existing tests
|
||||
|
||||
### Scope
|
||||
The scope should be the name of the npm package affected (as perceived by person reading changelog generated from commit messages.
|
||||
The scope should be the name of the npm package affected (as perceived by the person reading the changelog generated from commit messages.
|
||||
|
||||
The following is the list of supported scopes:
|
||||
|
||||
* **animations**
|
||||
* **common**
|
||||
* **compiler**
|
||||
* **compiler-cli**
|
||||
* **core**
|
||||
* **elements**
|
||||
* **forms**
|
||||
* **http**
|
||||
* **language-service**
|
||||
@ -220,21 +220,26 @@ The following is the list of supported scopes:
|
||||
* **platform-webworker**
|
||||
* **platform-webworker-dynamic**
|
||||
* **router**
|
||||
* **service-worker**
|
||||
* **upgrade**
|
||||
* **tsc-wrapped**
|
||||
|
||||
There is currently few exception to the "use package name" rule:
|
||||
There are currently a few exceptions to the "use package name" rule:
|
||||
|
||||
* **packaging**: used for changes that change the npm package layout in all of our packages, e.g. public path changes, package.json changes done to all packages, d.ts file/format changes, changes to bundles, etc.
|
||||
* **packaging**: used for changes that change the npm package layout in all of our packages, e.g.
|
||||
public path changes, package.json changes done to all packages, d.ts file/format changes, changes
|
||||
to bundles, etc.
|
||||
* **changelog**: used for updating the release notes in CHANGELOG.md
|
||||
* **aio**: used for docs-app (angular.io) related changes within the /aio directory of the repo
|
||||
* none/empty string: useful for `style`, `test` and `refactor` changes that are done across all packages (e.g. `style: add missing semicolons`)
|
||||
* **docs-infra**: used for docs-app (angular.io) related changes within the /aio directory of the
|
||||
repo
|
||||
* none/empty string: useful for `style`, `test` and `refactor` changes that are done across all
|
||||
packages (e.g. `style: add missing semicolons`) and for docs changes that are not related to a
|
||||
specific package (e.g. `docs: fix typo in tutorial`).
|
||||
|
||||
### Subject
|
||||
The subject contains succinct description of the change:
|
||||
The subject contains a succinct description of the change:
|
||||
|
||||
* use the imperative, present tense: "change" not "changed" nor "changes"
|
||||
* don't capitalize first letter
|
||||
* don't capitalize the first letter
|
||||
* no dot (.) at the end
|
||||
|
||||
### Body
|
||||
@ -258,6 +263,19 @@ changes to be accepted, the CLA must be signed. It's a quick process, we promise
|
||||
* For corporations we'll need you to
|
||||
[print, sign and one of scan+email, fax or mail the form][corporate-cla].
|
||||
|
||||
<hr>
|
||||
|
||||
If you have more than one Git identity, you must make sure that you sign the CLA using the primary email address associated with the ID that has been granted access to the Angular repository. Git identities can be associated with more than one email address, and only one is primary. Here are some links to help you sort out multiple Git identities and email addresses:
|
||||
|
||||
* https://help.github.com/articles/setting-your-commit-email-address-in-git/
|
||||
* https://stackoverflow.com/questions/37245303/what-does-usera-committed-with-userb-13-days-ago-on-github-mean
|
||||
* https://help.github.com/articles/about-commit-email-addresses/
|
||||
* https://help.github.com/articles/blocking-command-line-pushes-that-expose-your-personal-email-address/
|
||||
|
||||
Note that if you have more than one Git identity, it is important to verify that you are logged in with the same ID with which you signed the CLA, before you commit changes. If not, your PR will fail the CLA check.
|
||||
|
||||
<hr>
|
||||
|
||||
|
||||
[angular-group]: https://groups.google.com/forum/#!forum/angular
|
||||
[coc]: https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md
|
||||
|
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2014-2017 Google, Inc. http://angular.io
|
||||
Copyright (c) 2014-2018 Google, Inc. http://angular.io
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
25
README.md
25
README.md
@ -1,30 +1,29 @@
|
||||
[](https://travis-ci.org/angular/angular)
|
||||
[](https://circleci.com/gh/angular/angular/tree/master)
|
||||
[](https://www.browserstack.com/automate/public-build/LzF3RzBVVGt6VWE2S0hHaC9uYllOZz09LS1BVjNTclBKV0x4eVRlcjA4QVY1M0N3PT0=--eb4ce8c8dc2c1c5b2b5352d473ee12a73ac20e06)
|
||||
[](https://gitter.im/angular/angular?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](http://issuestats.com/github/angular/angular)
|
||||
[](http://issuestats.com/github/angular/angular)
|
||||
[](https://badge.fury.io/js/%40angular%2Fcore)
|
||||
[](https://www.npmjs.com/@angular/core)
|
||||
|
||||
[](https://saucelabs.com/u/angular2-ci)
|
||||
*Safari (7+), iOS (7+), Edge (14) and IE mobile (11) are tested on [BrowserStack][browserstack].*
|
||||
|
||||
Angular
|
||||
=========
|
||||
|
||||
Angular is a development platform for building mobile and desktop web applications using Typescript/JavaScript (JS) and other languages.
|
||||
# Angular
|
||||
|
||||
Angular is a development platform for building mobile and desktop web applications using Typescript/JavaScript and other languages.
|
||||
|
||||
## Quickstart
|
||||
|
||||
[Get started in 5 minutes][quickstart].
|
||||
|
||||
## Changelog
|
||||
|
||||
[Learn about the latest improvements][changelog].
|
||||
|
||||
## Want to help?
|
||||
|
||||
Want to file a bug, contribute some code, or improve documentation? Excellent! Read up on our
|
||||
guidelines for [contributing][contributing] and then check out one of our issues in the [hotlist: community-help](https://github.com/angular/angular/labels/hotlist%3A%20community-help).
|
||||
|
||||
[browserstack]: https://www.browserstack.com/
|
||||
[contributing]: http://github.com/angular/angular/blob/master/CONTRIBUTING.md
|
||||
[quickstart]: https://angular.io/docs/ts/latest/quickstart.html
|
||||
[ng]: http://angular.io
|
||||
[browserstack]: https://www.browserstack.com/automate/public-build/LzF3RzBVVGt6VWE2S0hHaC9uYllOZz09LS1BVjNTclBKV0x4eVRlcjA4QVY1M0N3PT0=--eb4ce8c8dc2c1c5b2b5352d473ee12a73ac20e06
|
||||
[contributing]: https://github.com/angular/angular/blob/master/CONTRIBUTING.md
|
||||
[quickstart]: https://angular.io/guide/quickstart
|
||||
[changelog]: https://github.com/angular/angular/blob/master/CHANGELOG.md
|
||||
[ng]: https://angular.io
|
||||
|
116
WORKSPACE
Normal file
116
WORKSPACE
Normal file
@ -0,0 +1,116 @@
|
||||
workspace(name = "angular")
|
||||
|
||||
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
||||
load(
|
||||
"//packages/bazel:package.bzl",
|
||||
"rules_angular_dependencies",
|
||||
"rules_angular_dev_dependencies",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "io_bazel_rules_go",
|
||||
sha256 = "b7a62250a3a73277ade0ce306d22f122365b513f5402222403e507f2f997d421",
|
||||
url = "https://github.com/bazelbuild/rules_go/releases/download/0.16.3/rules_go-0.16.3.tar.gz",
|
||||
)
|
||||
|
||||
# Uncomment for local bazel rules development
|
||||
#local_repository(
|
||||
# name = "build_bazel_rules_nodejs",
|
||||
# path = "../rules_nodejs",
|
||||
#)
|
||||
#local_repository(
|
||||
# name = "build_bazel_rules_typescript",
|
||||
# path = "../rules_typescript",
|
||||
#)
|
||||
|
||||
# Angular Bazel users will call this function
|
||||
rules_angular_dependencies()
|
||||
|
||||
# Install transitive deps of rules_nodejs
|
||||
load("@build_bazel_rules_nodejs//:package.bzl", "rules_nodejs_dependencies")
|
||||
|
||||
rules_nodejs_dependencies()
|
||||
|
||||
# These are the dependencies only for us
|
||||
rules_angular_dev_dependencies()
|
||||
|
||||
# Install transitive deps of rules_typescript
|
||||
load("@build_bazel_rules_typescript//:package.bzl", "rules_typescript_dependencies")
|
||||
|
||||
rules_typescript_dependencies()
|
||||
|
||||
#
|
||||
# Point Bazel to WORKSPACEs that live in subdirectories
|
||||
#
|
||||
http_archive(
|
||||
name = "rxjs",
|
||||
sha256 = "72b0b4e517f43358f554c125e40e39f67688cd2738a8998b4a266981ed32f403",
|
||||
strip_prefix = "package/src",
|
||||
url = "https://registry.yarnpkg.com/rxjs/-/rxjs-6.3.3.tgz",
|
||||
)
|
||||
|
||||
# Point to the integration test workspace just so that Bazel doesn't descend into it
|
||||
# when expanding the //... pattern
|
||||
local_repository(
|
||||
name = "bazel_integration_test",
|
||||
path = "integration/bazel",
|
||||
)
|
||||
|
||||
#
|
||||
# Load and install our dependencies downloaded above.
|
||||
#
|
||||
load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories", "yarn_install")
|
||||
|
||||
check_bazel_version("0.20.0", """
|
||||
You no longer need to install Bazel on your machine.
|
||||
Angular has a dependency on the @bazel/bazel package which supplies it.
|
||||
Try running `yarn bazel` instead.
|
||||
(If you did run that, check that you've got a fresh `yarn install`)
|
||||
|
||||
""")
|
||||
|
||||
node_repositories(
|
||||
node_version = "10.9.0",
|
||||
package_json = ["//:package.json"],
|
||||
preserve_symlinks = True,
|
||||
yarn_version = "1.12.1",
|
||||
)
|
||||
|
||||
local_repository(
|
||||
name = "npm",
|
||||
path = "tools/npm_workspace",
|
||||
)
|
||||
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_register_toolchains", "go_rules_dependencies")
|
||||
|
||||
go_rules_dependencies()
|
||||
|
||||
go_register_toolchains()
|
||||
|
||||
load("@io_bazel_rules_webtesting//web:repositories.bzl", "browser_repositories", "web_test_repositories")
|
||||
|
||||
web_test_repositories()
|
||||
|
||||
browser_repositories(
|
||||
chromium = True,
|
||||
firefox = True,
|
||||
)
|
||||
|
||||
load("@build_bazel_rules_typescript//:defs.bzl", "ts_setup_workspace")
|
||||
|
||||
ts_setup_workspace()
|
||||
|
||||
load("@angular//:index.bzl", "ng_setup_workspace")
|
||||
|
||||
ng_setup_workspace()
|
||||
|
||||
##################################
|
||||
# Skylark documentation generation
|
||||
|
||||
load("@io_bazel_rules_sass//sass:sass_repositories.bzl", "sass_repositories")
|
||||
|
||||
sass_repositories()
|
||||
|
||||
load("@io_bazel_skydoc//skylark:skylark.bzl", "skydoc_repositories")
|
||||
|
||||
skydoc_repositories()
|
@ -1,73 +0,0 @@
|
||||
{
|
||||
"project": {
|
||||
"version": "1.0.0-beta.32.3",
|
||||
"name": "site"
|
||||
},
|
||||
"apps": [
|
||||
{
|
||||
"root": "src",
|
||||
"outDir": "dist",
|
||||
"assets": [
|
||||
"assets",
|
||||
"content",
|
||||
"app/search/search-worker.js",
|
||||
"favicon.ico"
|
||||
],
|
||||
"index": "index.html",
|
||||
"main": "main.ts",
|
||||
"polyfills": "polyfills.ts",
|
||||
"test": "test.ts",
|
||||
"tsconfig": "tsconfig.json",
|
||||
"prefix": "aio",
|
||||
"serviceWorker": true,
|
||||
"styles": [
|
||||
"styles.scss"
|
||||
],
|
||||
"scripts": [
|
||||
|
||||
],
|
||||
"environmentSource": "environments/environment.ts",
|
||||
"environments": {
|
||||
"dev": "environments/environment.ts",
|
||||
"prod": "environments/environment.prod.ts"
|
||||
}
|
||||
}
|
||||
],
|
||||
"e2e": {
|
||||
"protractor": {
|
||||
"config": "./protractor.conf.js"
|
||||
}
|
||||
},
|
||||
"lint": [
|
||||
{
|
||||
"files": "src/**/*.ts",
|
||||
"project": "src/tsconfig.json"
|
||||
},
|
||||
{
|
||||
"files": "e2e/**/*.ts",
|
||||
"project": "e2e/tsconfig.json"
|
||||
}
|
||||
],
|
||||
"test": {
|
||||
"karma": {
|
||||
"config": "./karma.conf.js"
|
||||
}
|
||||
},
|
||||
"defaults": {
|
||||
"styleExt": "scss",
|
||||
"component": {},
|
||||
"prefixInterfaces": false,
|
||||
"inline": {
|
||||
"style": false,
|
||||
"template": false
|
||||
},
|
||||
"spec": {
|
||||
"class": false,
|
||||
"component": true,
|
||||
"directive": true,
|
||||
"module": false,
|
||||
"pipe": true,
|
||||
"service": true
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"projects": {
|
||||
"staging": "aio-staging"
|
||||
}
|
||||
}
|
49
aio/.gitignore
vendored
49
aio/.gitignore
vendored
@ -1,11 +1,46 @@
|
||||
# Ignore node_modules
|
||||
node_modules
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# Ignore npm/yarn debug log
|
||||
# compiled output
|
||||
/dist
|
||||
/out-tsc
|
||||
/src/generated
|
||||
/tmp
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
debug.log
|
||||
npm-debug.log
|
||||
testem.log
|
||||
/typings
|
||||
yarn-error.log
|
||||
|
||||
# Ignore generated content
|
||||
/dist
|
||||
/src/content
|
||||
/.sass-cache
|
||||
# e2e
|
||||
/e2e/*.js
|
||||
/e2e/*.map
|
||||
protractor-results*.txt
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
138
aio/README.md
138
aio/README.md
@ -1,31 +1,135 @@
|
||||
# Site
|
||||
# Angular documentation project (https://angular.io)
|
||||
|
||||
This project was generated with [angular-cli](https://github.com/angular/angular-cli) version 1.0.0-beta.26.
|
||||
Everything in this folder is part of the documentation project. This includes
|
||||
|
||||
## Development server
|
||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
||||
* the web site for displaying the documentation
|
||||
* the dgeni configuration for converting source files to rendered files that can be viewed in the web site.
|
||||
* the tooling for setting up examples for development; and generating live-example and zip files from the examples.
|
||||
|
||||
## Code scaffolding
|
||||
## Developer tasks
|
||||
|
||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive/pipe/service/class/module`.
|
||||
We use [Yarn](https://yarnpkg.com) to manage the dependencies and to run build tasks.
|
||||
You should run all these tasks from the `angular/aio` folder.
|
||||
Here are the most important tasks you might need to use:
|
||||
|
||||
## Build
|
||||
* `yarn` - install all the dependencies.
|
||||
* `yarn setup` - install all the dependencies, boilerplate, stackblitz, zips and run dgeni on the docs.
|
||||
* `yarn setup-local` - same as `setup`, but use the locally built Angular packages for aio and docs examples boilerplate.
|
||||
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
|
||||
* `yarn build` - create a production build of the application (after installing dependencies, boilerplate, etc).
|
||||
* `yarn build-local` - same as `build`, but use `setup-local` instead of `setup`.
|
||||
|
||||
## Running unit tests
|
||||
* `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 lint` - check that the doc-viewer code follows our style rules.
|
||||
* `yarn test` - 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.
|
||||
|
||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
* `yarn docs` - generate all the docs from the source files.
|
||||
* `yarn docs-watch` - watch the Angular source and the docs files and run a short-circuited doc-gen for the docs that changed.
|
||||
* `yarn docs-lint` - check that the doc gen code follows our style rules.
|
||||
* `yarn docs-test` - run the unit tests for the doc generation code.
|
||||
|
||||
## Running end-to-end tests
|
||||
* `yarn boilerplate:add` - generate all the boilerplate code for the examples, so that they can be run locally. Add the option `--local` to use your local version of Angular contained in the "dist" folder.
|
||||
* `yarn boilerplate:remove` - remove all the boilerplate code that was added via `yarn boilerplate:add`.
|
||||
* `yarn generate-stackblitz` - generate the stackblitz files that are used by the `live-example` tags in the docs.
|
||||
* `yarn generate-zips` - generate the zip files from the examples. Zip available via the `live-example` tags in the docs.
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||
Before running the tests make sure you are serving the app via `ng serve`.
|
||||
* `yarn example-e2e` - run all e2e tests for examples
|
||||
- `yarn example-e2e --setup` - force webdriver update & other setup, then run tests
|
||||
- `yarn example-e2e --filter=foo` - limit e2e tests to those containing the word "foo"
|
||||
- `yarn example-e2e --setup --local` - run e2e tests with the local version of Angular contained in the "dist" folder
|
||||
|
||||
## Deploying to GitHub Pages
|
||||
## Developing on Windows
|
||||
The `packages/` directory may contain Linux-specific symlinks, which are not recognized by Windows.
|
||||
These unresolved links cause the docs generation process to fail because it cannot locate certain files.
|
||||
|
||||
Run `ng github-pages:deploy` to deploy to GitHub Pages.
|
||||
> Hint: The following steps require administration rights or [Windows Developer Mode](https://docs.microsoft.com/en-us/windows/uwp/get-started/enable-your-device-for-development) enabled!
|
||||
|
||||
## Further help
|
||||
To fix this problem, run `scripts/windows/create-symlinks.sh`. This command creates temporary files where the symlinks used to be. Make sure not to commit those files with your documentation changes.
|
||||
When you are done making and testing your documentation changes, you can restore the original symlinks and delete the temporary files by running `scripts/windows/remove-symlinks.sh`.
|
||||
|
||||
To get more help on the `angular-cli` use `ng help` or go check out the [Angular-CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
|
||||
It's necessary to remove the temporary files, because otherwise they're displayed as local changes in your git working copy and certain operations are blocked.
|
||||
|
||||
## Using ServiceWorker locally
|
||||
|
||||
Running `yarn start` (even when explicitly targeting production mode) does not set up the
|
||||
ServiceWorker. If you want to test the ServiceWorker locally, you can use `yarn build` and then
|
||||
serve the files in `dist/` with `yarn http-server dist -p 4200`.
|
||||
|
||||
|
||||
## Guide to authoring
|
||||
|
||||
There are two types of content in the documentation:
|
||||
|
||||
* **API docs**: descriptions of the modules, classes, interfaces, decorators, etc that make up the Angular platform.
|
||||
API docs are generated directly from the source code.
|
||||
The source code is contained in TypeScript files, located in the `angular/packages` folder.
|
||||
Each API item may have a preceding comment, which contains JSDoc style tags and content.
|
||||
The content is written in markdown.
|
||||
|
||||
* **Other content**: guides, tutorials, and other marketing material.
|
||||
All other content is written using markdown in text files, located in the `angular/aio/content` folder.
|
||||
More specifically, there are sub-folders that contain particular types of content: guides, tutorial and marketing.
|
||||
|
||||
* **Code examples**: code examples need to be testable to ensure their accuracy.
|
||||
Also, our examples have a specific look and feel and allow the user to copy the source code. For larger
|
||||
examples they are rendered in a tabbed interface (e.g. template, HTML, and TypeScript on separate
|
||||
tabs). Additionally, some are live examples, which provide links where the code can be edited, executed, and/or downloaded. For details on working with code examples, please read the [Code snippets](https://angular.io/guide/docs-style-guide#code-snippets), [Source code markup](https://angular.io/guide/docs-style-guide#source-code-markup), and [Live examples](https://angular.io/guide/docs-style-guide#live-examples) pages of the [Authors Style Guide](https://angular.io/guide/docs-style-guide).
|
||||
|
||||
We use the [dgeni](https://github.com/angular/dgeni) tool to convert these files into docs that can be viewed in the doc-viewer.
|
||||
|
||||
The [Authors Style Guide](https://angular.io/guide/docs-style-guide) prescribes guidelines for
|
||||
writing guide pages, explains how to use the documentation classes and components, and how to markup sample source code to produce code snippets.
|
||||
|
||||
### Generating the complete docs
|
||||
|
||||
The main task for generating the docs is `yarn docs`. This will process all the source files (API and other),
|
||||
extracting the documentation and generating JSON files that can be consumed by the doc-viewer.
|
||||
|
||||
### Partial doc generation for editors
|
||||
|
||||
Full doc generation can take up to one minute. That's too slow for efficient document creation and editing.
|
||||
|
||||
You can make small changes in a smart editor that displays formatted markdown:
|
||||
>In VS Code, _Cmd-K, V_ opens markdown preview in side pane; _Cmd-B_ toggles left sidebar
|
||||
|
||||
You also want to see those changes displayed properly in the doc viewer
|
||||
with a quick, edit/view cycle time.
|
||||
|
||||
For this purpose, use the `yarn docs-watch` task, which watches for changes to source files and only
|
||||
re-processes the the files necessary to generate the docs that are related to the file that has changed.
|
||||
Since this task takes shortcuts, it is much faster (often less than 1 second) but it won't produce full
|
||||
fidelity content. For example, links to other docs and code examples may not render correctly. This is
|
||||
most particularly noticed in links to other docs and in the embedded examples, which may not always render
|
||||
correctly.
|
||||
|
||||
The general setup is as follows:
|
||||
|
||||
* Open a terminal, ensure the dependencies are installed; run an initial doc generation; then start the doc-viewer:
|
||||
|
||||
```bash
|
||||
yarn setup
|
||||
yarn start
|
||||
```
|
||||
|
||||
* Open a second terminal and start watching the docs
|
||||
|
||||
```bash
|
||||
yarn docs-watch
|
||||
```
|
||||
|
||||
>Alternatively, try the consolidated `serve-and-sync` command that builds, watches and serves in the same terminal window
|
||||
```bash
|
||||
yarn serve-and-sync
|
||||
```
|
||||
|
||||
* Open a browser at https://localhost:4200/ and navigate to the document on which you want to work.
|
||||
You can automatically open the browser by using `yarn start -o` in the first terminal.
|
||||
|
||||
* Make changes to the page's associated doc or example files. Every time a file is saved, the doc will
|
||||
be regenerated, the app will rebuild and the page will reload.
|
||||
|
||||
* If you get a build error complaining about examples or any other odd behavior, be sure to consult
|
||||
the [Authors Style Guide](https://angular.io/guide/docs-style-guide).
|
||||
|
@ -8,49 +8,62 @@ LABEL name="angular.io PR preview" \
|
||||
|
||||
VOLUME /aio-secrets
|
||||
VOLUME /var/www/aio-builds
|
||||
VOLUME /dockerbuild
|
||||
|
||||
EXPOSE 80 443
|
||||
|
||||
|
||||
# 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 TEST_AIO_BUILDS_DIR=/tmp/aio-builds
|
||||
ARG AIO_DOMAIN_NAME=ngbuilds.io
|
||||
ARG TEST_AIO_DOMAIN_NAME=$AIO_DOMAIN_NAME.localhost
|
||||
ARG AIO_GITHUB_ORGANIZATION=angular
|
||||
ARG TEST_AIO_GITHUB_ORGANIZATION=angular
|
||||
ARG AIO_GITHUB_TEAM_SLUGS=angular-core,aio-contributors
|
||||
ARG TEST_AIO_GITHUB_TEAM_SLUGS=angular-core,aio-contributors
|
||||
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 TEST_AIO_GITHUB_TEAM_SLUGS=team,aio-contributors
|
||||
ARG AIO_NGINX_HOSTNAME=$AIO_DOMAIN_NAME
|
||||
ARG TEST_AIO_NGINX_HOSTNAME=$TEST_AIO_DOMAIN_NAME
|
||||
ARG AIO_NGINX_PORT_HTTP=80
|
||||
ARG TEST_AIO_NGINX_PORT_HTTP=8080
|
||||
ARG AIO_NGINX_PORT_HTTPS=443
|
||||
ARG TEST_AIO_NGINX_PORT_HTTPS=4433
|
||||
ARG AIO_REPO_SLUG=angular/angular
|
||||
ARG TEST_AIO_REPO_SLUG=test-repo/test-slug
|
||||
ARG AIO_UPLOAD_HOSTNAME=upload.localhost
|
||||
ARG TEST_AIO_UPLOAD_HOSTNAME=upload.localhost
|
||||
ARG AIO_UPLOAD_MAX_SIZE=20971520
|
||||
ARG TEST_AIO_UPLOAD_MAX_SIZE=20971520
|
||||
ARG AIO_UPLOAD_PORT=3000
|
||||
ARG TEST_AIO_UPLOAD_PORT=3001
|
||||
ARG AIO_SIGNIFICANT_FILES_PATTERN='^(?:aio|packages)/(?!.*[._]spec\\.[jt]s$)'
|
||||
ARG TEST_AIO_SIGNIFICANT_FILES_PATTERN=$AIO_SIGNIFICANT_FILES_PATTERN
|
||||
ARG AIO_TRUSTED_PR_LABEL="aio: preview"
|
||||
ARG TEST_AIO_TRUSTED_PR_LABEL="aio: preview"
|
||||
ARG AIO_PREVIEW_SERVER_HOSTNAME=preview.localhost
|
||||
ARG TEST_AIO_PREVIEW_SERVER_HOSTNAME=preview.localhost
|
||||
ARG AIO_ARTIFACT_MAX_SIZE=20971520
|
||||
ARG TEST_AIO_ARTIFACT_MAX_SIZE=200
|
||||
ARG AIO_PREVIEW_SERVER_PORT=3000
|
||||
ARG TEST_AIO_PREVIEW_SERVER_PORT=3001
|
||||
|
||||
ENV 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_GITHUB_ORGANIZATION=$AIO_GITHUB_ORGANIZATION TEST_AIO_GITHUB_ORGANIZATION=$TEST_AIO_GITHUB_ORGANIZATION \
|
||||
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_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_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_REPO_SLUG=$AIO_REPO_SLUG TEST_AIO_REPO_SLUG=$TEST_AIO_REPO_SLUG \
|
||||
AIO_SCRIPTS_JS_DIR=/usr/share/aio-scripts-js \
|
||||
AIO_SCRIPTS_SH_DIR=/usr/share/aio-scripts-sh \
|
||||
AIO_UPLOAD_HOSTNAME=$AIO_UPLOAD_HOSTNAME TEST_AIO_UPLOAD_HOSTNAME=$TEST_AIO_UPLOAD_HOSTNAME \
|
||||
AIO_UPLOAD_MAX_SIZE=$AIO_UPLOAD_MAX_SIZE TEST_AIO_UPLOAD_MAX_SIZE=$TEST_AIO_UPLOAD_MAX_SIZE \
|
||||
AIO_UPLOAD_PORT=$AIO_UPLOAD_PORT TEST_AIO_UPLOAD_PORT=$TEST_AIO_UPLOAD_PORT \
|
||||
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_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_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_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_HTTPS=$AIO_NGINX_PORT_HTTPS TEST_AIO_NGINX_PORT_HTTPS=$TEST_AIO_NGINX_PORT_HTTPS \
|
||||
AIO_SCRIPTS_JS_DIR=/usr/share/aio-scripts-js \
|
||||
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_PREVIEW_SERVER_HOSTNAME=$AIO_PREVIEW_SERVER_HOSTNAME TEST_AIO_PREVIEW_SERVER_HOSTNAME=$TEST_AIO_PREVIEW_SERVER_HOSTNAME \
|
||||
AIO_ARTIFACT_MAX_SIZE=$AIO_ARTIFACT_MAX_SIZE TEST_AIO_ARTIFACT_MAX_SIZE=$TEST_AIO_ARTIFACT_MAX_SIZE \
|
||||
AIO_PREVIEW_SERVER_PORT=$AIO_PREVIEW_SERVER_PORT TEST_AIO_PREVIEW_SERVER_PORT=$TEST_AIO_PREVIEW_SERVER_PORT \
|
||||
AIO_WWW_USER=www-data \
|
||||
NODE_ENV=production
|
||||
|
||||
|
||||
@ -60,9 +73,10 @@ RUN mkdir /var/log/aio
|
||||
|
||||
# Add extra package sources
|
||||
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 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
|
||||
|
||||
|
||||
# Install packages
|
||||
@ -71,14 +85,19 @@ RUN apt-get update -y && apt-get install -y \
|
||||
cron \
|
||||
dnsmasq \
|
||||
nano \
|
||||
nginx \
|
||||
nodejs \
|
||||
openssl \
|
||||
rsyslog \
|
||||
yarn
|
||||
RUN apt-get install -t jessie-backports -y nginx
|
||||
RUN yarn global add pm2@2
|
||||
|
||||
|
||||
# Set up log rotation
|
||||
COPY logrotate/* /etc/logrotate.d/
|
||||
RUN chmod 0644 /etc/logrotate.d/*
|
||||
|
||||
|
||||
# Set up cronjobs
|
||||
COPY cronjobs/aio-builds-cleanup /etc/cron.d/
|
||||
RUN chmod 0744 /etc/cron.d/aio-builds-cleanup
|
||||
@ -89,9 +108,9 @@ RUN printenv | grep AIO_ >> /etc/environment
|
||||
# Set up dnsmasq
|
||||
COPY dnsmasq/dnsmasq.conf /etc/
|
||||
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_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
|
||||
@ -104,31 +123,31 @@ RUN update-ca-certificates
|
||||
|
||||
|
||||
# Set up nginx (for production and testing)
|
||||
RUN rm /etc/nginx/sites-enabled/*
|
||||
RUN sed -i -E "s|^user\s+\S+;|user $AIO_WWW_USER;|" /etc/nginx/nginx.conf
|
||||
RUN rm -f /etc/nginx/conf.d/*
|
||||
RUN rm -f /etc/nginx/sites-enabled/*
|
||||
|
||||
COPY nginx/aio-builds.conf /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_BUILDS_DIR}}|$AIO_BUILDS_DIR|g" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_DOMAIN_NAME}}|$AIO_DOMAIN_NAME|g" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_LOCALCERTS_DIR}}|$AIO_LOCALCERTS_DIR|g" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_LOGS_DIR}}|$AIO_NGINX_LOGS_DIR|g" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTP}}|$AIO_NGINX_PORT_HTTP|g" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTPS}}|$AIO_NGINX_PORT_HTTPS|g" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$AIO_UPLOAD_HOSTNAME|g" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_MAX_SIZE}}|$AIO_UPLOAD_MAX_SIZE|g" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_PORT}}|$AIO_UPLOAD_PORT|g" /etc/nginx/sites-available/aio-builds-prod.conf
|
||||
RUN ln -s /etc/nginx/sites-available/aio-builds-prod.conf /etc/nginx/sites-enabled/aio-builds-prod.conf
|
||||
COPY nginx/aio-builds.conf /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_BUILDS_DIR}}|$AIO_BUILDS_DIR|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_DOMAIN_NAME}}|$AIO_DOMAIN_NAME|g" /etc/nginx/conf.d/aio-builds-prod.conf
|
||||
RUN sed -i "s|{{\$AIO_LOCALCERTS_DIR}}|$AIO_LOCALCERTS_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_HTTPS}}|$AIO_NGINX_PORT_HTTPS|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_ARTIFACT_MAX_SIZE}}|$AIO_ARTIFACT_MAX_SIZE|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/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_BUILDS_DIR}}|$TEST_AIO_BUILDS_DIR|g" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_DOMAIN_NAME}}|$TEST_AIO_DOMAIN_NAME|g" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_LOCALCERTS_DIR}}|$TEST_AIO_LOCALCERTS_DIR|g" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_LOGS_DIR}}|$TEST_AIO_NGINX_LOGS_DIR|g" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTP}}|$TEST_AIO_NGINX_PORT_HTTP|g" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_NGINX_PORT_HTTPS}}|$TEST_AIO_NGINX_PORT_HTTPS|g" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_HOSTNAME}}|$TEST_AIO_UPLOAD_HOSTNAME|g" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_MAX_SIZE}}|$TEST_AIO_UPLOAD_MAX_SIZE|g" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_UPLOAD_PORT}}|$TEST_AIO_UPLOAD_PORT|g" /etc/nginx/sites-available/aio-builds-test.conf
|
||||
RUN ln -s /etc/nginx/sites-available/aio-builds-test.conf /etc/nginx/sites-enabled/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_DOMAIN_NAME}}|$TEST_AIO_DOMAIN_NAME|g" /etc/nginx/conf.d/aio-builds-test.conf
|
||||
RUN sed -i "s|{{\$AIO_LOCALCERTS_DIR}}|$TEST_AIO_LOCALCERTS_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_HTTPS}}|$TEST_AIO_NGINX_PORT_HTTPS|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_ARTIFACT_MAX_SIZE}}|$TEST_AIO_ARTIFACT_MAX_SIZE|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
|
||||
@ -146,7 +165,7 @@ RUN find $AIO_SCRIPTS_SH_DIR -maxdepth 1 -type f -printf "%P\n" \
|
||||
# Set up the Node.js scripts
|
||||
COPY scripts-js/ $AIO_SCRIPTS_JS_DIR/
|
||||
WORKDIR $AIO_SCRIPTS_JS_DIR/
|
||||
RUN yarn install --production
|
||||
RUN yarn install --production --frozen-lockfile
|
||||
|
||||
|
||||
# Set up health check
|
||||
|
@ -1,2 +1,2 @@
|
||||
# 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
|
||||
|
@ -6,11 +6,11 @@ server=8.8.4.4
|
||||
# Listen for DHCP and DNS requests only on this address.
|
||||
listen-address=127.0.0.1
|
||||
|
||||
# Force an IP addres for these domains.
|
||||
# Force an IP address for these domains.
|
||||
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_UPLOAD_HOSTNAME}}/127.0.0.1
|
||||
address=/{{$TEST_AIO_PREVIEW_SERVER_HOSTNAME}}/127.0.0.1
|
||||
|
||||
# Run as root (required from inside docker container).
|
||||
user=root
|
||||
|
9
aio/aio-builds-setup/dockerbuild/logrotate/aio-misc
Normal file
9
aio/aio-builds-setup/dockerbuild/logrotate/aio-misc
Normal file
@ -0,0 +1,9 @@
|
||||
/var/log/aio/clean-up.log /var/log/aio/init.log /var/log/aio/verify-setup.log {
|
||||
compress
|
||||
create
|
||||
delaycompress
|
||||
missingok
|
||||
monthly
|
||||
notifempty
|
||||
rotate 6
|
||||
}
|
13
aio/aio-builds-setup/dockerbuild/logrotate/aio-nginx
Normal file
13
aio/aio-builds-setup/dockerbuild/logrotate/aio-nginx
Normal file
@ -0,0 +1,13 @@
|
||||
/var/log/aio/nginx/*.log /var/log/aio/nginx-test/*.log {
|
||||
compress
|
||||
create
|
||||
delaycompress
|
||||
missingok
|
||||
monthly
|
||||
notifempty
|
||||
rotate 6
|
||||
sharedscripts
|
||||
postrotate
|
||||
service nginx rotate >/dev/null 2>&1
|
||||
endscript
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
/var/log/aio/preview-server-*.log {
|
||||
compress
|
||||
copytruncate
|
||||
delaycompress
|
||||
missingok
|
||||
monthly
|
||||
notifempty
|
||||
rotate 6
|
||||
}
|
@ -15,21 +15,32 @@ server {
|
||||
|
||||
# Serve PR-preview requests
|
||||
server {
|
||||
server_name "~^pr(?<pr>[1-9][0-9]*)-(?<sha>[0-9a-f]{40})\.";
|
||||
server_name "~^pr(?<pr>[1-9][0-9]*)-(?<sha>[0-9a-f]{7,40})\.";
|
||||
|
||||
listen {{$AIO_NGINX_PORT_HTTPS}} ssl;
|
||||
listen [::]:{{$AIO_NGINX_PORT_HTTPS}} ssl;
|
||||
listen {{$AIO_NGINX_PORT_HTTPS}} ssl http2;
|
||||
listen [::]:{{$AIO_NGINX_PORT_HTTPS}} ssl http2;
|
||||
|
||||
ssl_certificate {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.crt;
|
||||
ssl_certificate_key {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.key;
|
||||
ssl_certificate {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.crt;
|
||||
ssl_certificate_key {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.key;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
|
||||
|
||||
root {{$AIO_BUILDS_DIR}}/$pr/$sha;
|
||||
disable_symlinks on from=$document_root;
|
||||
index index.html;
|
||||
|
||||
gzip on;
|
||||
gzip_comp_level 7;
|
||||
gzip_types *;
|
||||
|
||||
access_log {{$AIO_NGINX_LOGS_DIR}}/access.log;
|
||||
error_log {{$AIO_NGINX_LOGS_DIR}}/error.log;
|
||||
|
||||
error_page 404 /404.html;
|
||||
location "=/404.html" {
|
||||
internal;
|
||||
}
|
||||
|
||||
location "~/[^/]+\.[^/]+$" {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
@ -43,11 +54,13 @@ server {
|
||||
server {
|
||||
server_name _;
|
||||
|
||||
listen {{$AIO_NGINX_PORT_HTTPS}} ssl default_server;
|
||||
listen [::]:{{$AIO_NGINX_PORT_HTTPS}} ssl;
|
||||
listen {{$AIO_NGINX_PORT_HTTPS}} ssl http2 default_server;
|
||||
listen [::]:{{$AIO_NGINX_PORT_HTTPS}} ssl http2;
|
||||
|
||||
ssl_certificate {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.crt;
|
||||
ssl_certificate_key {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.key;
|
||||
ssl_certificate {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.crt;
|
||||
ssl_certificate_key {{$AIO_LOCALCERTS_DIR}}/{{$AIO_DOMAIN_NAME}}.key;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
|
||||
|
||||
access_log {{$AIO_NGINX_LOGS_DIR}}/access.log;
|
||||
error_log {{$AIO_NGINX_LOGS_DIR}}/error.log;
|
||||
@ -58,24 +71,47 @@ server {
|
||||
return 200 '';
|
||||
}
|
||||
|
||||
# Upload builds
|
||||
location "~^/create-build/(?<pr>[1-9][0-9]*)/(?<sha>[0-9a-f]{40})/?$" {
|
||||
# Check PRs previewability
|
||||
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") {
|
||||
add_header Allow "POST";
|
||||
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_redirect off;
|
||||
proxy_method POST;
|
||||
proxy_pass http://{{$AIO_PREVIEW_SERVER_HOSTNAME}}:{{$AIO_PREVIEW_SERVER_PORT}}$request_uri;
|
||||
|
||||
resolver 127.0.0.1;
|
||||
}
|
||||
|
||||
# Notify about PR changes
|
||||
location "~^/pr-updated/?$" {
|
||||
if ($request_method != "POST") {
|
||||
add_header Allow "POST";
|
||||
return 405;
|
||||
}
|
||||
|
||||
proxy_pass_request_headers on;
|
||||
proxy_set_header X-FILE $request_body_file;
|
||||
proxy_set_body off;
|
||||
proxy_redirect off;
|
||||
proxy_method GET;
|
||||
proxy_pass http://{{$AIO_UPLOAD_HOSTNAME}}:{{$AIO_UPLOAD_PORT}}$request_uri;
|
||||
proxy_method POST;
|
||||
proxy_pass http://{{$AIO_PREVIEW_SERVER_HOSTNAME}}:{{$AIO_PREVIEW_SERVER_PORT}}$request_uri;
|
||||
|
||||
resolver 127.0.0.1;
|
||||
}
|
||||
|
@ -1 +1,2 @@
|
||||
/dist/
|
||||
/dist
|
||||
/node_modules
|
||||
|
@ -2,70 +2,126 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as shell from 'shelljs';
|
||||
import {HIDDEN_DIR_PREFIX} from '../common/constants';
|
||||
import {GithubApi} from '../common/github-api';
|
||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||
import {assertNotMissingOrEmpty} from '../common/utils';
|
||||
import {assertNotMissingOrEmpty, getPrInfoFromDownloadPath, Logger} from '../common/utils';
|
||||
|
||||
// Classes
|
||||
export class BuildCleaner {
|
||||
|
||||
private logger = new Logger('BuildCleaner');
|
||||
|
||||
// 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('repoSlug', repoSlug);
|
||||
assertNotMissingOrEmpty('githubOrg', githubOrg);
|
||||
assertNotMissingOrEmpty('githubRepo', githubRepo);
|
||||
assertNotMissingOrEmpty('githubToken', githubToken);
|
||||
assertNotMissingOrEmpty('downloadsDir', downloadsDir);
|
||||
assertNotMissingOrEmpty('artifactPath', artifactPath);
|
||||
}
|
||||
|
||||
// Methods - Public
|
||||
public cleanUp(): Promise<void> {
|
||||
return Promise.all([
|
||||
this.getExistingBuildNumbers(),
|
||||
this.getOpenPrNumbers(),
|
||||
]).then(([existingBuilds, openPrs]) => this.removeUnnecessaryBuilds(existingBuilds, openPrs));
|
||||
public async cleanUp(): Promise<void> {
|
||||
try {
|
||||
this.logger.log('Cleaning up builds and downloads');
|
||||
const openPrs = await this.getOpenPrNumbers();
|
||||
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
|
||||
protected getExistingBuildNumbers(): Promise<number[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
public async cleanBuilds(openPrs: number[]): Promise<void> {
|
||||
const existingBuilds = await this.getExistingBuildNumbers();
|
||||
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) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
const buildNumbers = files.
|
||||
map(Number). // Convert string to number
|
||||
filter(Boolean); // Ignore NaN (or 0), because they are not builds
|
||||
map(name => name.replace(HIDDEN_DIR_PREFIX, '')). // Remove the "hidden dir" prefix
|
||||
map(Number). // Convert string to number
|
||||
filter(Boolean); // Ignore NaN (or 0), because they are not builds
|
||||
|
||||
resolve(buildNumbers);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected getOpenPrNumbers(): Promise<number[]> {
|
||||
const githubPullRequests = new GithubPullRequests(this.githubToken, this.repoSlug);
|
||||
|
||||
return githubPullRequests.
|
||||
fetchAll('open').
|
||||
then(prs => prs.map(pr => pr.number));
|
||||
public async getOpenPrNumbers(): Promise<number[]> {
|
||||
const api = new GithubApi(this.githubToken);
|
||||
const githubPullRequests = new GithubPullRequests(api, this.githubOrg, this.githubRepo);
|
||||
const prs = await githubPullRequests.fetchAll('open');
|
||||
return prs.map(pr => pr.number);
|
||||
}
|
||||
|
||||
protected removeDir(dir: string) {
|
||||
public removeDir(dir: string): void {
|
||||
try {
|
||||
// Undocumented signature (see https://github.com/shelljs/shelljs/pull/663).
|
||||
(shell as any).chmod('-R', 'a+w', dir);
|
||||
shell.rm('-rf', dir);
|
||||
if (shell.test('-d', dir)) {
|
||||
shell.chmod('-R', 'a+w', dir);
|
||||
shell.rm('-rf', dir);
|
||||
}
|
||||
} 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));
|
||||
|
||||
console.log(`Existing builds: ${existingBuildNumbers.length}`);
|
||||
console.log(`Open pull requests: ${openPrNumbers.length}`);
|
||||
console.log(`Removing ${toRemove.length} build(s): ${toRemove.join(', ')}`);
|
||||
this.logger.log(`Existing builds: ${existingBuildNumbers.length}`);
|
||||
this.logger.log(`Removing ${toRemove.length} build(s): ${toRemove.join(', ')}`);
|
||||
|
||||
// Try removing public dirs.
|
||||
toRemove.
|
||||
map(num => path.join(this.buildsDir, String(num))).
|
||||
forEach(dir => this.removeDir(dir));
|
||||
|
||||
// Try removing hidden dirs.
|
||||
toRemove.
|
||||
map(num => path.join(this.buildsDir, HIDDEN_DIR_PREFIX + String(num))).
|
||||
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
|
||||
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';
|
||||
|
||||
// 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
|
||||
_main();
|
||||
|
||||
// Functions
|
||||
function _main() {
|
||||
console.log(`[${new Date()}] - Cleaning up builds...`);
|
||||
function _main(): void {
|
||||
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(err => {
|
||||
console.error('ERROR:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
buildCleaner.cleanUp().catch(() => 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})`);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
// Constants
|
||||
export const AIO_DOWNLOADS_DIR = '/tmp/aio-downloads';
|
||||
export const HIDDEN_DIR_PREFIX = 'hidden--';
|
||||
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
|
||||
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);
|
||||
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);
|
||||
return this.request<T>('post', path, data);
|
||||
}
|
||||
|
||||
// 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 getPaginated<T>(pathname: string, baseParams: RequestParams = {}, currentPage: number = 0): Promise<T[]> {
|
||||
// In GitHub API paginated requests, page numbering is 1-based. (https://developer.github.com/v3/#pagination)
|
||||
public getPaginated<T>(pathname: string, baseParams: RequestParams = {}, currentPage: number = 1): Promise<T[]> {
|
||||
const perPage = 100;
|
||||
const params = {
|
||||
...baseParams,
|
||||
@ -63,10 +52,22 @@ export class GithubApi {
|
||||
return items;
|
||||
}
|
||||
|
||||
return this.getPaginated(pathname, baseParams, currentPage + 1).then(moreItems => [...items, ...moreItems]);
|
||||
return this.getPaginated<T>(pathname, baseParams, currentPage + 1).then(moreItems => [...items, ...moreItems]);
|
||||
});
|
||||
}
|
||||
|
||||
// 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> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const options = {
|
||||
@ -81,7 +82,7 @@ export class GithubApi {
|
||||
reject(`Request to '${url}' failed (status: ${statusCode}): ${responseText}`);
|
||||
};
|
||||
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 statusCode = res.statusCode || -1;
|
||||
|
@ -1,44 +1,79 @@
|
||||
// Imports
|
||||
import {assertNotMissingOrEmpty} from '../common/utils';
|
||||
import {GithubApi} from './github-api';
|
||||
import {assert, assertNotMissingOrEmpty} from './utils';
|
||||
|
||||
// Interfaces - Types
|
||||
export interface PullRequest {
|
||||
export interface PullRequest {
|
||||
number: number;
|
||||
user: {login: string};
|
||||
labels: {name: string}[];
|
||||
}
|
||||
|
||||
export interface FileInfo {
|
||||
sha: string;
|
||||
filename: string;
|
||||
}
|
||||
|
||||
export type PullRequestState = 'all' | 'closed' | 'open';
|
||||
|
||||
// Classes
|
||||
export class GithubPullRequests extends GithubApi {
|
||||
// Constructor
|
||||
constructor(githubToken: string, protected repoSlug: string) {
|
||||
super(githubToken);
|
||||
assertNotMissingOrEmpty('repoSlug', repoSlug);
|
||||
/**
|
||||
* Access pull requests on GitHub.
|
||||
*/
|
||||
export class GithubPullRequests {
|
||||
public repoSlug: string;
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
if (!(pr > 0)) {
|
||||
throw new Error(`Invalid PR number: ${pr}`);
|
||||
} else if (!body) {
|
||||
throw new Error(`Invalid or empty comment body: ${body}`);
|
||||
}
|
||||
|
||||
return this.post<void>(`/repos/${this.repoSlug}/issues/${pr}/comments`, null, {body});
|
||||
/**
|
||||
* Post a comment on a PR.
|
||||
* @param pr The number of the PR on which to comment.
|
||||
* @param body The body of the comment to post.
|
||||
* @returns A promise that resolves when the comment has been posted.
|
||||
*/
|
||||
public addComment(pr: number, body: string): Promise<any> {
|
||||
assert(pr > 0, `Invalid PR number: ${pr}`);
|
||||
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> {
|
||||
return this.get<PullRequest>(`/repos/${this.repoSlug}/pulls/${pr}`);
|
||||
assert(pr > 0, `Invalid PR number: ${pr}`);
|
||||
// Using the `/issues/` URL, because the `/pulls/` one does not provide labels.
|
||||
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[]> {
|
||||
console.log(`Fetching ${state} pull requests...`);
|
||||
|
||||
const pathname = `/repos/${this.repoSlug}/pulls`;
|
||||
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 {assertNotMissingOrEmpty} from './utils';
|
||||
|
||||
// Interfaces - Types
|
||||
interface Team {
|
||||
export interface Team {
|
||||
id: number;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
interface TeamMembership {
|
||||
export interface TeamMembership {
|
||||
state: string;
|
||||
}
|
||||
|
||||
// Classes
|
||||
export class GithubTeams extends GithubApi {
|
||||
// Constructor
|
||||
constructor(githubToken: string, protected organization: string) {
|
||||
super(githubToken);
|
||||
assertNotMissingOrEmpty('organization', organization);
|
||||
export class GithubTeams {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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[]> {
|
||||
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) =>
|
||||
this.get<TeamMembership>(`/teams/${teamId}/memberships/${username}`).
|
||||
then(membership => membership.state === 'active').
|
||||
catch(() => false);
|
||||
const reduceFn = (promise: Promise<boolean>, teamId: number) =>
|
||||
promise.then(isMember => isMember || getMembership(teamId));
|
||||
/**
|
||||
* Check whether the specified username is a member of the specified team.
|
||||
* @param username The usernane to check for in the team.
|
||||
* @param teamIds The team to check for the username.
|
||||
* @returns a Promise that resolves to `true` if the username is a member of the team.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public isMemberBySlug(username: string, teamSlugs: string[]): Promise<boolean> {
|
||||
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` here, because of the following mess:
|
||||
// - 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.
|
||||
//
|
||||
// Using `import...from 'jasmine'` here, would import from `@types/jasmine` (which refers to the
|
||||
// `jasmine-core` module and the `jasmine` module).
|
||||
// tslint:disable-next-line: no-var-requires variable-name
|
||||
const Jasmine = require('jasmine');
|
||||
// We can't use `import...from` here, because of the following mess:
|
||||
// - 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.
|
||||
//
|
||||
// Using `import...from 'jasmine'` here, would import from `@types/jasmine` (which refers to the
|
||||
// `jasmine-core` module and the `jasmine` module).
|
||||
import Jasmine = require('jasmine');
|
||||
import 'source-map-support/register';
|
||||
|
||||
export const runTests = (specFiles: string[]) => {
|
||||
const config = {
|
||||
helpers,
|
||||
random: true,
|
||||
spec_files: specFiles,
|
||||
stopSpecOnExpectationFailure: true,
|
||||
@ -16,7 +16,7 @@ export const runTests = (specFiles: string[], helpers?: string[]) => {
|
||||
|
||||
process.on('unhandledRejection', (reason: any) => console.log('Unhandled rejection:', reason));
|
||||
|
||||
const runner = new Jasmine();
|
||||
const runner = new Jasmine({});
|
||||
runner.loadConfig(config);
|
||||
runner.onComplete((passed: boolean) => process.exit(passed ? 0 : 1));
|
||||
runner.execute();
|
||||
|
@ -1,17 +1,98 @@
|
||||
// Functions
|
||||
export const assertNotMissingOrEmpty = (name: string, value: string | null | undefined) => {
|
||||
import {basename, resolve as resolvePath} from 'path';
|
||||
import {SHORT_SHA_LEN} from './constants';
|
||||
|
||||
/**
|
||||
* 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(`Missing or empty required parameter '${name}'!`);
|
||||
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 => {
|
||||
const value = process.env[name];
|
||||
|
||||
if (!isOptional && !value) {
|
||||
console.error(`ERROR: Missing required environment variable '${name}'!`);
|
||||
process.exit(1);
|
||||
try {
|
||||
throw new Error(`ERROR: Missing required environment variable '${name}'!`);
|
||||
} catch (error) {
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,144 @@
|
||||
// Imports
|
||||
import * as cp from 'child_process';
|
||||
import {EventEmitter} from 'events';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as shell from 'shelljs';
|
||||
import {HIDDEN_DIR_PREFIX} from '../common/constants';
|
||||
import {assertNotMissingOrEmpty, computeShortSha, Logger} from '../common/utils';
|
||||
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events';
|
||||
import {PreviewServerError} from './preview-error';
|
||||
|
||||
// Classes
|
||||
export class BuildCreator extends EventEmitter {
|
||||
|
||||
private logger = new Logger('BuildCreator');
|
||||
|
||||
// Constructor
|
||||
constructor(protected buildsDir: string) {
|
||||
super();
|
||||
assertNotMissingOrEmpty('buildsDir', buildsDir);
|
||||
}
|
||||
|
||||
// Methods - Public
|
||||
public create(pr: number, sha: string, archivePath: string, isPublic: boolean): Promise<void> {
|
||||
// Use only part of the SHA for more readable URLs.
|
||||
sha = computeShortSha(sha);
|
||||
|
||||
const {newPrDir: prDir} = this.getCandidatePrDirs(pr, isPublic);
|
||||
const shaDir = path.join(prDir, sha);
|
||||
let dirToRemoveOnError: string;
|
||||
|
||||
return Promise.resolve().
|
||||
// If the same PR exists with different visibility, update the visibility first.
|
||||
then(() => this.updatePrVisibility(pr, isPublic)).
|
||||
then(() => Promise.all([this.exists(prDir), this.exists(shaDir)])).
|
||||
then(([prDirExisted, shaDirExisted]) => {
|
||||
if (shaDirExisted) {
|
||||
const publicOrNot = isPublic ? 'public' : 'non-public';
|
||||
throw new PreviewServerError(409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`);
|
||||
}
|
||||
|
||||
dirToRemoveOnError = prDirExisted ? shaDir : prDir;
|
||||
|
||||
return Promise.resolve().
|
||||
then(() => shell.mkdir('-p', shaDir)).
|
||||
then(() => this.extractArchive(archivePath, shaDir)).
|
||||
then(() => this.emit(CreatedBuildEvent.type, new CreatedBuildEvent(+pr, sha, isPublic))).
|
||||
then(() => undefined);
|
||||
}).
|
||||
catch(err => {
|
||||
if (dirToRemoveOnError) {
|
||||
shell.rm('-rf', dirToRemoveOnError);
|
||||
}
|
||||
|
||||
if (!(err instanceof PreviewServerError)) {
|
||||
err = new PreviewServerError(500, `Error while creating preview at: ${shaDir}\n${err}`);
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
public updatePrVisibility(pr: number, makePublic: boolean): Promise<boolean> {
|
||||
const {oldPrDir: otherVisPrDir, newPrDir: targetVisPrDir} = this.getCandidatePrDirs(pr, makePublic);
|
||||
|
||||
return Promise.
|
||||
all([this.exists(otherVisPrDir), this.exists(targetVisPrDir)]).
|
||||
then(([otherVisPrDirExisted, targetVisPrDirExisted]) => {
|
||||
if (!otherVisPrDirExisted) {
|
||||
// No visibility change: Either the visibility is up-to-date or the PR does not exist.
|
||||
return false;
|
||||
} else if (targetVisPrDirExisted) {
|
||||
// Error: Directories for both visibilities exist.
|
||||
throw new PreviewServerError(409,
|
||||
`Request to move '${otherVisPrDir}' to existing directory '${targetVisPrDir}'.`);
|
||||
}
|
||||
|
||||
// Visibility change: Moving `otherVisPrDir` to `targetVisPrDir`.
|
||||
return Promise.resolve().
|
||||
then(() => shell.mv(otherVisPrDir, targetVisPrDir)).
|
||||
then(() => this.listShasByDate(targetVisPrDir)).
|
||||
then(shas => this.emit(ChangedPrVisibilityEvent.type, new ChangedPrVisibilityEvent(+pr, shas, makePublic))).
|
||||
then(() => true);
|
||||
}).
|
||||
catch(err => {
|
||||
if (!(err instanceof PreviewServerError)) {
|
||||
err = new PreviewServerError(500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\n${err}`);
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
// Methods - Protected
|
||||
protected exists(fileOrDir: string): Promise<boolean> {
|
||||
return new Promise(resolve => fs.access(fileOrDir, err => resolve(!err)));
|
||||
}
|
||||
|
||||
protected extractArchive(inputFile: string, outputDir: string): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const cmd = `tar --extract --gzip --directory "${outputDir}" --file "${inputFile}"`;
|
||||
|
||||
cp.exec(cmd, (err, _stdout, stderr) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
this.logger.warn(stderr);
|
||||
}
|
||||
|
||||
try {
|
||||
shell.chmod('-R', 'a-w', outputDir);
|
||||
shell.rm('-f', inputFile);
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected getCandidatePrDirs(pr: number, isPublic: boolean): {oldPrDir: string, newPrDir: string} {
|
||||
const hiddenPrDir = path.join(this.buildsDir, HIDDEN_DIR_PREFIX + pr);
|
||||
const publicPrDir = path.join(this.buildsDir, `${pr}`);
|
||||
|
||||
const oldPrDir = isPublic ? hiddenPrDir : publicPrDir;
|
||||
const newPrDir = isPublic ? publicPrDir : hiddenPrDir;
|
||||
|
||||
return {oldPrDir, newPrDir};
|
||||
}
|
||||
|
||||
protected listShasByDate(inputDir: string): Promise<string[]> {
|
||||
return Promise.resolve().
|
||||
then(() => shell.ls('-l', inputDir) as any as Promise<(fs.Stats & {name: string})[]>).
|
||||
// Keep directories only.
|
||||
// (Also, convert to standard Array - ShellJS provides custom `sort()` method for sorting file contents.)
|
||||
then(items => items.filter(item => item.isDirectory())).
|
||||
// Sort by modification date.
|
||||
then(items => items.sort((a, b) => a.mtime.getTime() - b.mtime.getTime())).
|
||||
// Return directory names.
|
||||
then(items => items.map(item => item.name));
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
// Classes
|
||||
export class ChangedPrVisibilityEvent {
|
||||
// Properties - Public, Static
|
||||
public static type = 'pr.changedVisibility';
|
||||
|
||||
// Constructor
|
||||
constructor(public pr: number, public shas: string[], public isPublic: boolean) {}
|
||||
}
|
||||
|
||||
export class CreatedBuildEvent {
|
||||
// Properties - Public, Static
|
||||
public static type = 'build.created';
|
||||
|
||||
// Constructor
|
||||
constructor(public pr: number, public sha: string, public isPublic: boolean) {}
|
||||
}
|
@ -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);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
// Classes
|
||||
export class PreviewServerError extends Error {
|
||||
// Constructor
|
||||
constructor(public status: number = 500, message?: string) {
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, PreviewServerError.prototype);
|
||||
}
|
||||
}
|
@ -0,0 +1,213 @@
|
||||
// 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, computeShortSha, 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);
|
||||
logger.log(`PR:${pr}, SHA:${computeShortSha(sha)}, Build:${buildNum} - ` +
|
||||
`Successfully created ${isPublic ? 'public' : 'non-public'} preview.`);
|
||||
} 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,81 +0,0 @@
|
||||
// Imports
|
||||
import * as cp from 'child_process';
|
||||
import {EventEmitter} from 'events';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as shell from 'shelljs';
|
||||
import {assertNotMissingOrEmpty} from '../common/utils';
|
||||
import {CreatedBuildEvent} from './build-events';
|
||||
import {UploadError} from './upload-error';
|
||||
|
||||
// Classes
|
||||
export class BuildCreator extends EventEmitter {
|
||||
// Constructor
|
||||
constructor(protected buildsDir: string) {
|
||||
super();
|
||||
assertNotMissingOrEmpty('buildsDir', buildsDir);
|
||||
}
|
||||
|
||||
// Methods - Public
|
||||
public create(pr: string, sha: string, archivePath: string): Promise<any> {
|
||||
const prDir = path.join(this.buildsDir, pr);
|
||||
const shaDir = path.join(prDir, sha);
|
||||
let dirToRemoveOnError: string;
|
||||
|
||||
return Promise.
|
||||
all([this.exists(prDir), this.exists(shaDir)]).
|
||||
then(([prDirExisted, shaDirExisted]) => {
|
||||
if (shaDirExisted) {
|
||||
throw new UploadError(403, `Request to overwrite existing directory: ${shaDir}`);
|
||||
}
|
||||
|
||||
dirToRemoveOnError = prDirExisted ? shaDir : prDir;
|
||||
|
||||
return Promise.resolve().
|
||||
then(() => shell.mkdir('-p', shaDir)).
|
||||
then(() => this.extractArchive(archivePath, shaDir)).
|
||||
then(() => this.emit(CreatedBuildEvent.type, new CreatedBuildEvent(+pr, sha)));
|
||||
}).
|
||||
catch(err => {
|
||||
if (dirToRemoveOnError) {
|
||||
shell.rm('-rf', dirToRemoveOnError);
|
||||
}
|
||||
|
||||
if (!(err instanceof UploadError)) {
|
||||
err = new UploadError(500, `Error while uploading to directory: ${shaDir}\n${err}`);
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
// Methods - Protected
|
||||
protected exists(fileOrDir: string): Promise<boolean> {
|
||||
return new Promise(resolve => fs.access(fileOrDir, err => resolve(!err)));
|
||||
}
|
||||
|
||||
protected extractArchive(inputFile: string, outputDir: string): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const cmd = `tar --extract --gzip --directory "${outputDir}" --file "${inputFile}"`;
|
||||
|
||||
cp.exec(cmd, (err, _stdout, stderr) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.warn(stderr);
|
||||
}
|
||||
|
||||
try {
|
||||
// Undocumented signature (see https://github.com/shelljs/shelljs/pull/663).
|
||||
(shell as any).chmod('-R', 'a-w', outputDir);
|
||||
shell.rm('-f', inputFile);
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
// Classes
|
||||
export class BuildEvent {
|
||||
// Constructor
|
||||
constructor(public type: string, public pr: number, public sha: string) {}
|
||||
}
|
||||
|
||||
export class CreatedBuildEvent extends BuildEvent {
|
||||
// Properties - Public, Static
|
||||
public static type = 'build.created';
|
||||
|
||||
// Constructor
|
||||
constructor(pr: number, sha: string) {
|
||||
super(CreatedBuildEvent.type, pr, sha);
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
// Imports
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
import {GithubPullRequests} 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;
|
||||
}
|
||||
|
||||
// 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[]) {
|
||||
assertNotMissingOrEmpty('secret', secret);
|
||||
assertNotMissingOrEmpty('githubToken', githubToken);
|
||||
assertNotMissingOrEmpty('repoSlug', repoSlug);
|
||||
assertNotMissingOrEmpty('organization', organization);
|
||||
assertNotMissingOrEmpty('allowedTeamSlugs', allowedTeamSlugs && allowedTeamSlugs.join(''));
|
||||
|
||||
this.githubPullRequests = new GithubPullRequests(githubToken, repoSlug);
|
||||
this.githubTeams = new GithubTeams(githubToken, organization);
|
||||
}
|
||||
|
||||
// Methods - Public
|
||||
public getPrAuthorTeamMembership(pr: number): Promise<{author: string, isMember: boolean}> {
|
||||
return Promise.resolve().
|
||||
then(() => this.githubPullRequests.fetch(pr)).
|
||||
then(prInfo => prInfo.user.login).
|
||||
then(author => this.githubTeams.isMemberBySlug(author, this.allowedTeamSlugs).
|
||||
then(isMember => ({author, isMember})));
|
||||
}
|
||||
|
||||
public verify(expectedPr: number, authHeader: string): Promise<void> {
|
||||
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 verifyJwt(expectedPr: number, token: string): Promise<JwtPayload> {
|
||||
return new Promise((resolve, reject) => {
|
||||
jwt.verify(token, this.secret, {issuer: 'Travis CI, GmbH'}, (err, payload) => {
|
||||
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<void> {
|
||||
return this.getPrAuthorTeamMembership(pr).
|
||||
then(({author, isMember}) => isMember ? Promise.resolve() : Promise.reject(
|
||||
`User '${author}' is not an active member of any of the following teams: ` +
|
||||
`${this.allowedTeamSlugs.join(', ')}`,
|
||||
));
|
||||
}
|
||||
}
|
@ -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 pr = +getEnvVar('AIO_PREVERIFY_PR');
|
||||
|
||||
const buildVerifier = new BuildVerifier(secret, githubToken, repoSlug, organization, allowedTeamSlugs);
|
||||
|
||||
// Exit codes:
|
||||
// - 0: The PR author is a member.
|
||||
// - 1: The PR author is not a member.
|
||||
// - 2: An error occurred.
|
||||
buildVerifier.getPrAuthorTeamMembership(pr).
|
||||
then(({author, isMember}) => {
|
||||
if (isMember) {
|
||||
process.exit(0);
|
||||
} else {
|
||||
const errorMessage = `User '${author}' is not an active member of any of the following teams: ` +
|
||||
`${allowedTeamSlugs.join(', ')}`;
|
||||
onError(errorMessage, 1);
|
||||
}
|
||||
}).
|
||||
catch(err => onError(err, 2));
|
||||
}
|
||||
|
||||
function onError(err: string, exitCode: number) {
|
||||
console.error(err);
|
||||
process.exit(exitCode || 1);
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
// Imports
|
||||
import {GithubPullRequests} from '../common/github-pull-requests';
|
||||
import {BuildVerifier} from './build-verifier';
|
||||
|
||||
// Run
|
||||
// TODO(gkalpak): Add e2e tests to cover these interactions as well.
|
||||
GithubPullRequests.prototype.addComment = () => Promise.resolve();
|
||||
BuildVerifier.prototype.verify = () => Promise.resolve();
|
||||
// tslint:disable-next-line: no-var-requires
|
||||
require('./index');
|
@ -1,35 +0,0 @@
|
||||
// TODO(gkalpak): Find more suitable way to run as `www-data`.
|
||||
process.setuid('www-data');
|
||||
|
||||
// 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_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,
|
||||
}).
|
||||
listen(AIO_UPLOAD_PORT, AIO_UPLOAD_HOSTNAME);
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
// Classes
|
||||
export class UploadError extends Error {
|
||||
// Constructor
|
||||
constructor(public status: number = 500, message?: string) {
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, UploadError.prototype);
|
||||
}
|
||||
}
|
@ -1,117 +0,0 @@
|
||||
// Imports
|
||||
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 {CreatedBuildEvent} from './build-events';
|
||||
import {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;
|
||||
}
|
||||
|
||||
// Classes
|
||||
class UploadServerFactory {
|
||||
// Methods - Public
|
||||
public create({
|
||||
buildsDir,
|
||||
domainName,
|
||||
githubOrganization,
|
||||
githubTeamSlugs,
|
||||
githubToken,
|
||||
repoSlug,
|
||||
secret,
|
||||
}: UploadServerConfig): http.Server {
|
||||
assertNotMissingOrEmpty('domainName', domainName);
|
||||
|
||||
const buildVerifier = new BuildVerifier(secret, githubToken, repoSlug, githubOrganization, githubTeamSlugs);
|
||||
const buildCreator = this.createBuildCreator(buildsDir, githubToken, repoSlug, domainName);
|
||||
|
||||
const middleware = this.createMiddleware(buildVerifier, buildCreator);
|
||||
const httpServer = http.createServer(middleware);
|
||||
|
||||
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);
|
||||
|
||||
buildCreator.on(CreatedBuildEvent.type, ({pr, sha}: CreatedBuildEvent) => {
|
||||
const body = `The angular.io preview for ${sha.slice(0, 7)} is available [here][1].\n\n` +
|
||||
`[1]: https://pr${pr}-${sha}.${domainName}/`;
|
||||
|
||||
githubPullRequests.addComment(pr, body);
|
||||
});
|
||||
|
||||
return buildCreator;
|
||||
}
|
||||
|
||||
protected createMiddleware(buildVerifier: BuildVerifier, buildCreator: BuildCreator): express.Express {
|
||||
const middleware = express();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
buildVerifier.
|
||||
verify(+pr, authHeader).
|
||||
then(() => buildCreator.create(pr, sha, archive)).
|
||||
then(() => res.sendStatus(201)).
|
||||
catch(err => this.respondWithError(res, err));
|
||||
});
|
||||
middleware.get(/^\/health-check\/?$/, (_req, res) => res.sendStatus(200));
|
||||
middleware.get('*', req => this.throwRequestError(404, 'Unknown resource', req));
|
||||
middleware.all('*', req => this.throwRequestError(405, 'Unsupported method', 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) {
|
||||
throw new UploadError(status, `${error} in request: ${req.method} ${req.originalUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Exports
|
||||
export const uploadServerFactory = new UploadServerFactory();
|
@ -0,0 +1,37 @@
|
||||
export const enum BuildNums {
|
||||
BUILD_INFO_ERROR = 1,
|
||||
BUILD_INFO_404,
|
||||
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,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
export const SHA = '1234567890'.repeat(4);
|
||||
export const ALT_SHA = 'abcde'.repeat(8);
|
||||
export const SIMILAR_SHA = SHA.slice(0, -1) + 'A';
|
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,20 +4,17 @@ import * as fs from 'fs';
|
||||
import * as http from 'http';
|
||||
import * as path from 'path';
|
||||
import * as shell from 'shelljs';
|
||||
import {getEnvVar} from '../common/utils';
|
||||
|
||||
// Constans
|
||||
const SERVER_USER = 'www-data';
|
||||
const TEST_AIO_BUILDS_DIR = getEnvVar('TEST_AIO_BUILDS_DIR');
|
||||
const TEST_AIO_NGINX_HOSTNAME = getEnvVar('TEST_AIO_NGINX_HOSTNAME');
|
||||
const TEST_AIO_NGINX_PORT_HTTP = +getEnvVar('TEST_AIO_NGINX_PORT_HTTP');
|
||||
const TEST_AIO_NGINX_PORT_HTTPS = +getEnvVar('TEST_AIO_NGINX_PORT_HTTPS');
|
||||
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');
|
||||
import {AIO_DOWNLOADS_DIR, HIDDEN_DIR_PREFIX} from '../common/constants';
|
||||
import {
|
||||
AIO_BUILDS_DIR,
|
||||
AIO_NGINX_PORT_HTTP,
|
||||
AIO_NGINX_PORT_HTTPS,
|
||||
AIO_WWW_USER,
|
||||
} from '../common/env-variables';
|
||||
import {computeShortSha, Logger} from '../common/utils';
|
||||
|
||||
// Interfaces - Types
|
||||
export interface CmdResult { success: boolean; err: Error; stdout: string; stderr: string; }
|
||||
export interface CmdResult { success: boolean; err: Error | null; stdout: string; stderr: string; }
|
||||
export interface FileSpecs { content?: string; size?: number; }
|
||||
|
||||
export type CleanUpFn = () => void;
|
||||
@ -26,79 +23,74 @@ export type VerifyCmdResultFn = (result: CmdResult) => void;
|
||||
|
||||
// Classes
|
||||
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 serverUser() { return SERVER_USER; }
|
||||
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; }
|
||||
|
||||
// Properties - Protected
|
||||
protected cleanUpFns: CleanUpFn[] = [];
|
||||
protected portPerScheme: {[scheme: string]: number} = {
|
||||
http: this.nginxPortHttp,
|
||||
https: this.nginxPortHttps,
|
||||
http: AIO_NGINX_PORT_HTTP,
|
||||
https: AIO_NGINX_PORT_HTTPS,
|
||||
};
|
||||
|
||||
private logger = new Logger('TestHelper');
|
||||
|
||||
// Constructor
|
||||
constructor() {
|
||||
shell.mkdir('-p', this.buildsDir);
|
||||
shell.exec(`chown -R ${this.serverUser} ${this.buildsDir}`);
|
||||
shell.mkdir('-p', AIO_BUILDS_DIR);
|
||||
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
|
||||
public cleanUp() {
|
||||
public cleanUp(): void {
|
||||
while (this.cleanUpFns.length) {
|
||||
// Clean-up fns remove themselves from the list.
|
||||
this.cleanUpFns[0]();
|
||||
}
|
||||
|
||||
if (fs.readdirSync(this.buildsDir).length) {
|
||||
throw new Error(`Directory '${this.buildsDir}' is not empty after clean-up.`);
|
||||
const leftoverDownloads = fs.readdirSync(AIO_DOWNLOADS_DIR);
|
||||
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 {
|
||||
const inputDir = path.join(this.buildsDir, 'uploaded', pr, sha);
|
||||
const cmd1 = `tar --create --gzip --directory "${inputDir}" --file "${archivePath}" .`;
|
||||
const cmd2 = `chown ${this.serverUser} ${archivePath}`;
|
||||
|
||||
const cleanUpTemp = this.createDummyBuild(`uploaded/${pr}`, sha, true);
|
||||
shell.exec(cmd1);
|
||||
shell.exec(cmd2);
|
||||
cleanUpTemp();
|
||||
|
||||
return this.createCleanUpFn(() => shell.rm('-rf', archivePath));
|
||||
}
|
||||
|
||||
public createDummyBuild(pr: string, sha: string, force = false): CleanUpFn {
|
||||
const prDir = path.join(this.buildsDir, pr);
|
||||
const shaDir = path.join(prDir, sha);
|
||||
public createDummyBuild(pr: number, sha: string, isPublic = true, force = false, legacy = false): CleanUpFn {
|
||||
const prDir = this.getPrDir(pr, isPublic);
|
||||
const shaDir = this.getShaDir(prDir, sha, legacy);
|
||||
const idxPath = path.join(shaDir, 'index.html');
|
||||
const barPath = path.join(shaDir, 'foo', 'bar.js');
|
||||
|
||||
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);
|
||||
shell.exec(`chown -R ${this.serverUser} ${prDir}`);
|
||||
shell.exec(`chown -R ${AIO_WWW_USER} ${prDir}`);
|
||||
|
||||
return this.createCleanUpFn(() => shell.rm('-rf', prDir));
|
||||
}
|
||||
|
||||
public deletePrDir(pr: string) {
|
||||
const prDir = path.join(this.buildsDir, pr);
|
||||
|
||||
if (fs.existsSync(prDir)) {
|
||||
// Undocumented signature (see https://github.com/shelljs/shelljs/pull/663).
|
||||
(shell as any).chmod('-R', 'a+w', prDir);
|
||||
shell.rm('-rf', prDir);
|
||||
}
|
||||
public getPrDir(pr: number, isPublic: boolean): string {
|
||||
const prDirName = isPublic ? '' + pr : HIDDEN_DIR_PREFIX + pr;
|
||||
return path.join(AIO_BUILDS_DIR, prDirName);
|
||||
}
|
||||
|
||||
public readBuildFile(pr: string, sha: string, relFilePath: string): string {
|
||||
const absFilePath = path.join(this.buildsDir, pr, sha, relFilePath);
|
||||
public getShaDir(prDir: string, sha: string, legacy = false): string {
|
||||
return path.join(prDir, legacy ? sha : computeShortSha(sha));
|
||||
}
|
||||
|
||||
public readBuildFile(pr: number, sha: string, relFilePath: string, isPublic = true, legacy = false): string {
|
||||
const shaDir = this.getShaDir(this.getPrDir(pr, isPublic), sha, legacy);
|
||||
const absFilePath = path.join(shaDir, relFilePath);
|
||||
return fs.readFileSync(absFilePath, 'utf8');
|
||||
}
|
||||
|
||||
@ -109,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]));
|
||||
}
|
||||
|
||||
public verifyResponse(status: number | [number, string], regex = /^/): VerifyCmdResultFn {
|
||||
public verifyResponse(status: number | [number, string], regex: string | RegExp = /^/): VerifyCmdResultFn {
|
||||
let statusCode: number;
|
||||
let statusText: string;
|
||||
|
||||
@ -122,19 +114,20 @@ class Helper {
|
||||
statusText = status[1];
|
||||
} else {
|
||||
statusCode = status;
|
||||
statusText = http.STATUS_CODES[statusCode];
|
||||
statusText = http.STATUS_CODES[statusCode] || 'UNKNOWN_STATUS_CODE';
|
||||
}
|
||||
|
||||
return (result: CmdResult) => {
|
||||
const [headers, body] = result.stdout.
|
||||
split(/(?:\r?\n){2,}/).
|
||||
map(s => s.trim()).
|
||||
slice(-2);
|
||||
slice(-2); // In case of redirect, discard the previous headers.
|
||||
// Only keep the last to sections (final headers and body).
|
||||
|
||||
if (!result.success) {
|
||||
console.log('Stdout:', result.stdout);
|
||||
console.log('Stderr:', result.stderr);
|
||||
console.log('Error:', result.err);
|
||||
this.logger.log('Stdout:', result.stdout);
|
||||
this.logger.error('Stderr:', result.stderr);
|
||||
this.logger.error('Error:', result.err);
|
||||
}
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
@ -143,12 +136,14 @@ class Helper {
|
||||
};
|
||||
}
|
||||
|
||||
public writeBuildFile(pr: string, sha: string, relFilePath: string, content: string): CleanUpFn {
|
||||
const absFilePath = path.join(this.buildsDir, pr, sha, relFilePath);
|
||||
return this.writeFile(absFilePath, {content}, true);
|
||||
public writeBuildFile(pr: number, sha: string, relFilePath: string, content: string, isPublic = true,
|
||||
legacy = false): void {
|
||||
const shaDir = this.getShaDir(this.getPrDir(pr, isPublic), sha, legacy);
|
||||
const absFilePath = path.join(shaDir, relFilePath);
|
||||
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)) {
|
||||
throw new Error(`Refusing to overwrite existing file '${filePath}'.`);
|
||||
}
|
||||
@ -166,13 +161,11 @@ class Helper {
|
||||
// Create a file with the specified content.
|
||||
fs.writeFileSync(filePath, content || '');
|
||||
}
|
||||
shell.exec(`chown ${this.serverUser} ${filePath}`);
|
||||
|
||||
return this.createCleanUpFn(() => shell.rm('-rf', cleanUpTarget));
|
||||
shell.exec(`chown ${AIO_WWW_USER} ${filePath}`);
|
||||
}
|
||||
|
||||
// Methods - Protected
|
||||
protected createCleanUpFn(fn: Function): CleanUpFn {
|
||||
protected createCleanUpFn(fn: () => void): CleanUpFn {
|
||||
const cleanUpFn = () => {
|
||||
const idx = this.cleanUpFns.indexOf(cleanUpFn);
|
||||
if (idx !== -1) {
|
||||
@ -187,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
|
||||
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
|
||||
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 {customMatchers} from './jasmine-custom-matchers';
|
||||
|
||||
// Tests
|
||||
describe(`nginx`, () => {
|
||||
|
||||
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000);
|
||||
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000);
|
||||
beforeEach(() => jasmine.addMatchers(customMatchers));
|
||||
afterEach(() => h.cleanUp());
|
||||
|
||||
|
||||
it('should redirect HTTP to HTTPS', done => {
|
||||
const httpHost = `${h.nginxHostname}:${h.nginxPortHttp}`;
|
||||
const httpsHost = `${h.nginxHostname}:${h.nginxPortHttps}`;
|
||||
const httpHost = `${AIO_NGINX_HOSTNAME}:${AIO_NGINX_PORT_HTTP}`;
|
||||
const httpsHost = `${AIO_NGINX_HOSTNAME}:${AIO_NGINX_PORT_HTTPS}`;
|
||||
const urlMap = {
|
||||
[`http://${httpHost}/`]: `https://${httpsHost}/`,
|
||||
[`http://${httpHost}/foo`]: `https://${httpsHost}/foo`,
|
||||
@ -31,111 +37,193 @@ describe(`nginx`, () => {
|
||||
});
|
||||
|
||||
|
||||
h.runForAllSupportedSchemes((scheme, port) => describe(`nginx (on ${scheme.toUpperCase()})`, () => {
|
||||
const hostname = h.nginxHostname;
|
||||
h.runForAllSupportedSchemes((scheme, port) => describe(`(on ${scheme.toUpperCase()})`, () => {
|
||||
const hostname = AIO_NGINX_HOSTNAME;
|
||||
const host = `${hostname}:${port}`;
|
||||
const pr = '9';
|
||||
const pr = 9;
|
||||
const sha9 = '9'.repeat(40);
|
||||
const sha0 = '0'.repeat(40);
|
||||
const shortSha9 = computeShortSha(sha9);
|
||||
const shortSha0 = computeShortSha(sha0);
|
||||
|
||||
|
||||
describe(`pr<pr>-<sha>.${host}/*`, () => {
|
||||
|
||||
beforeEach(() => {
|
||||
h.createDummyBuild(pr, sha9);
|
||||
h.createDummyBuild(pr, sha0);
|
||||
describe('(for public builds)', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
h.createDummyBuild(pr, sha9);
|
||||
h.createDummyBuild(pr, sha0);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
expect({ prNum: pr, sha: sha9 }).toExistAsABuild();
|
||||
expect({ prNum: pr, sha: sha0 }).toExistAsABuild();
|
||||
});
|
||||
|
||||
|
||||
it('should return /index.html', done => {
|
||||
const origin = `${scheme}://pr${pr}-${shortSha9}.${host}`;
|
||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||
|
||||
Promise.all([
|
||||
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)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should return /index.html (for legacy builds)', async () => {
|
||||
const origin = `${scheme}://pr${pr}-${sha9}.${host}`;
|
||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||
|
||||
h.createDummyBuild(pr, sha9, true, false, true);
|
||||
|
||||
await Promise.all([
|
||||
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)),
|
||||
]);
|
||||
|
||||
expect({ prNum: pr, sha: sha9, isLegacy: true }).toExistAsABuild();
|
||||
});
|
||||
|
||||
|
||||
it('should return /foo/bar.js', done => {
|
||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /foo/bar\\.js$`);
|
||||
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}.${host}/foo/bar.js`).
|
||||
then(h.verifyResponse(200, bodyRegex)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should return /foo/bar.js (for legacy builds)', async () => {
|
||||
const origin = `${scheme}://pr${pr}-${sha9}.${host}`;
|
||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /foo/bar\\.js$`);
|
||||
|
||||
h.createDummyBuild(pr, sha9, true, false, true);
|
||||
|
||||
await h.runCmd(`curl -iL ${origin}/foo/bar.js`).then(h.verifyResponse(200, bodyRegex));
|
||||
|
||||
expect({ prNum: pr, sha: sha9, isLegacy: true }).toExistAsABuild();
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 403 for directories', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}.${host}/foo/`).then(h.verifyResponse(403)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}.${host}/foo`).then(h.verifyResponse(403)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown paths to files', done => {
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}.${host}/foo/baz.css`).
|
||||
then(h.verifyResponse(404)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should rewrite to \'index.html\' for unknown paths that don\'t look like files', done => {
|
||||
const origin = `${scheme}://pr${pr}-${shortSha9}.${host}`;
|
||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${origin}/foo/baz`).then(h.verifyResponse(200, bodyRegex)),
|
||||
h.runCmd(`curl -iL ${origin}/foo/baz/`).then(h.verifyResponse(200, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown PRs/SHAs', done => {
|
||||
const otherPr = 54321;
|
||||
const otherShortSha = computeShortSha('8'.repeat(40));
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}9-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${otherPr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}9.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${otherShortSha}.${host}`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 if the subdomain format is wrong', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://xpr${pr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://prx${pr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://xx${pr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://p${pr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://r${pr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${pr}-${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}_${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject PRs with leading zeros', done => {
|
||||
h.runCmd(`curl -iL ${scheme}://pr0${pr}-${shortSha9}.${host}`).
|
||||
then(h.verifyResponse(404)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should accept SHAs with leading zeros (but not trim the zeros)', done => {
|
||||
const bodyRegex9 = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||
const bodyRegex0 = new RegExp(`^PR: ${pr} | SHA: ${sha0} | File: /index\\.html$`);
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-0${shortSha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha9}.${host}`).then(h.verifyResponse(200, bodyRegex9)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${shortSha0}.${host}`).then(h.verifyResponse(200, bodyRegex0)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
it('should return /index.html', done => {
|
||||
const origin = `${scheme}://pr${pr}-${sha9}.${host}`;
|
||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||
describe('(for hidden builds)', () => {
|
||||
|
||||
Promise.all([
|
||||
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)),
|
||||
]).then(done);
|
||||
});
|
||||
it('should respond with 404 for any file or directory', async () => {
|
||||
const origin = `${scheme}://pr${pr}-${shortSha9}.${host}`;
|
||||
const assert404 = h.verifyResponse(404);
|
||||
|
||||
h.createDummyBuild(pr, sha9, false);
|
||||
|
||||
await Promise.all([
|
||||
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}/foo/bar.js`).then(assert404),
|
||||
h.runCmd(`curl -iL ${origin}/foo/`).then(assert404),
|
||||
h.runCmd(`curl -iL ${origin}/foo`).then(assert404),
|
||||
]);
|
||||
|
||||
expect({ prNum: pr, sha: sha9, isPublic: false }).toExistAsABuild();
|
||||
});
|
||||
|
||||
|
||||
it('should return /foo/bar.js', done => {
|
||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /foo/bar\\.js$`);
|
||||
it('should respond with 404 for any file or directory (for legacy builds)', async () => {
|
||||
const origin = `${scheme}://pr${pr}-${sha9}.${host}`;
|
||||
const assert404 = h.verifyResponse(404);
|
||||
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/bar.js`).
|
||||
then(h.verifyResponse(200, bodyRegex)).
|
||||
then(done);
|
||||
});
|
||||
h.createDummyBuild(pr, sha9, false, false, true);
|
||||
|
||||
await Promise.all([
|
||||
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}/foo/bar.js`).then(assert404),
|
||||
h.runCmd(`curl -iL ${origin}/foo/`).then(assert404),
|
||||
h.runCmd(`curl -iL ${origin}/foo`).then(assert404),
|
||||
]);
|
||||
|
||||
it('should respond with 403 for directories', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/`).then(h.verifyResponse(403)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo`).then(h.verifyResponse(403)),
|
||||
]).then(done);
|
||||
});
|
||||
expect({ prNum: pr, sha: sha9, isPublic: false, isLegacy: true }).toExistAsABuild();
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown paths to files', done => {
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/baz.css`).
|
||||
then(h.verifyResponse(404)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should rewrite to \'index.html\' for unknown paths that don\'t look like files', done => {
|
||||
const bodyRegex = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/baz`).then(h.verifyResponse(200, bodyRegex)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}/foo/baz/`).then(h.verifyResponse(200, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 for unknown PRs/SHAs', done => {
|
||||
const otherPr = 54321;
|
||||
const otherSha = '8'.repeat(40);
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}9-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${otherPr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}9.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${otherSha}.${host}`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 404 if the subdomain format is wrong', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://xpr${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://prx${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://xx${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://p${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://r${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${pr}-${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}_${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject PRs with leading zeros', done => {
|
||||
h.runCmd(`curl -iL ${scheme}://pr0${pr}-${sha9}.${host}`).
|
||||
then(h.verifyResponse(404)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should accept SHAs with leading zeros (but not trim the zeros)', done => {
|
||||
const bodyRegex9 = new RegExp(`^PR: ${pr} | SHA: ${sha9} | File: /index\\.html$`);
|
||||
const bodyRegex0 = new RegExp(`^PR: ${pr} | SHA: ${sha0} | File: /index\\.html$`);
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-0${sha9}.${host}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha9}.${host}`).then(h.verifyResponse(200, bodyRegex9)),
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha0}.${host}`).then(h.verifyResponse(200, bodyRegex0)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
@ -165,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 => {
|
||||
const url = `${scheme}://${host}/create-build/${pr}/${sha9}`;
|
||||
const url = `${scheme}://${host}/circle-build`;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iLX GET ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
@ -179,31 +303,9 @@ describe(`nginx`, () => {
|
||||
});
|
||||
|
||||
|
||||
it(`should reject files larger than ${h.uploadMaxSize}B (according to header)`, done => {
|
||||
const headers = `--header "Content-Length: ${1.5 * h.uploadMaxSize}"`;
|
||||
const url = `${scheme}://${host}/create-build/${pr}/${sha9}`;
|
||||
|
||||
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/)).
|
||||
it('should pass requests through to the preview server', done => {
|
||||
h.runCmd(`curl -iLX POST ${scheme}://${host}/circle-build`).
|
||||
then(h.verifyResponse(400, /Incorrect body content. Expected JSON/)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
@ -212,32 +314,55 @@ describe(`nginx`, () => {
|
||||
const cmdPrefix = `curl -iLX POST ${scheme}://${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)),
|
||||
h.runCmd(`${cmdPrefix}/foo/circle-build/`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/foo-circle-build/`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/fooncircle-build/`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/circle-build/foo/`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/circle-build-foo/`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/circle-buildnfoo/`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/circle-build/pr`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/circle-build/42`).then(h.verifyResponse(404)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe(`${host}/pr-updated`, () => {
|
||||
const url = `${scheme}://${host}/pr-updated`;
|
||||
|
||||
|
||||
it('should disallow non-POST requests', done => {
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iLX GET ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
|
||||
]).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 pass requests through to the preview server', done => {
|
||||
const cmdPrefix = `curl -iLX POST --header "Content-Type: application/json"`;
|
||||
|
||||
const cmd1 = `${cmdPrefix} ${url}`;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(cmd1).then(h.verifyResponse(400, /Missing or empty 'number' field/)),
|
||||
]).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/;
|
||||
it('should respond with 404 for unknown paths', done => {
|
||||
const cmdPrefix = `curl -iLX POST ${scheme}://${host}`;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`${cmdPrefix}/0${sha9}`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`${cmdPrefix}/${sha0}`).then(h.verifyResponse(401, bodyRegex)),
|
||||
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);
|
||||
});
|
||||
|
||||
@ -246,13 +371,15 @@ describe(`nginx`, () => {
|
||||
|
||||
describe(`${host}/*`, () => {
|
||||
|
||||
it('should respond with 404 for unkown URLs (even if the resource exists)', done => {
|
||||
beforeEach(() => {
|
||||
['index.html', 'foo.js', 'foo/index.html'].forEach(relFilePath => {
|
||||
const absFilePath = path.join(h.buildsDir, relFilePath);
|
||||
h.writeFile(absFilePath, {content: `File: /${relFilePath}`});
|
||||
const absFilePath = path.join(AIO_BUILDS_DIR, 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}/`).then(h.verifyResponse(404)),
|
||||
h.runCmd(`curl -iL ${scheme}://${host}`).then(h.verifyResponse(404)),
|
||||
@ -261,7 +388,14 @@ describe(`nginx`, () => {
|
||||
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/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,84 +1,269 @@
|
||||
// Imports
|
||||
import * as path from 'path';
|
||||
import {helper as h} from './helper';
|
||||
import {AIO_NGINX_HOSTNAME} from '../common/env-variables';
|
||||
import {computeShortSha} from '../common/utils';
|
||||
import {ALT_SHA, BuildNums, PrNums, SHA} from './constants';
|
||||
import {helper as h, makeCurl, payload} from './helper';
|
||||
import {customMatchers} from './jasmine-custom-matchers';
|
||||
|
||||
// Tests
|
||||
h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme.toUpperCase()})`, () => {
|
||||
const hostname = h.nginxHostname;
|
||||
const hostname = AIO_NGINX_HOSTNAME;
|
||||
const host = `${hostname}:${port}`;
|
||||
const pr9 = '9';
|
||||
const sha9 = '9'.repeat(40);
|
||||
const sha0 = '0'.repeat(40);
|
||||
const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz');
|
||||
const curlPrUpdated = makeCurl(`${scheme}://${host}/pr-updated`);
|
||||
|
||||
const getFile = (pr: string, sha: string, file: string) =>
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${sha}.${host}/${file}`);
|
||||
const uploadBuild = (pr: string, sha: string, archive: string) => {
|
||||
const curlPost = 'curl -iLX POST --header "Authorization: Token FOO"';
|
||||
return h.runCmd(`${curlPost} --data-binary "@${archive}" ${scheme}://${host}/create-build/${pr}/${sha}`);
|
||||
};
|
||||
const getFile = (pr: number, sha: string, file: string) =>
|
||||
h.runCmd(`curl -iL ${scheme}://pr${pr}-${computeShortSha(sha)}.${host}/${file}`);
|
||||
const prUpdated = (prNum: number, action?: string) => curlPrUpdated({ data: { number: prNum, action } });
|
||||
const circleBuild = makeCurl(`${scheme}://${host}/circle-build`);
|
||||
|
||||
beforeEach(() => {
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000;
|
||||
jasmine.addMatchers(customMatchers);
|
||||
});
|
||||
afterEach(() => h.cleanUp());
|
||||
|
||||
|
||||
describe('for a new/non-existing PR', () => {
|
||||
|
||||
it('should be able to create and serve a public preview', async () => {
|
||||
const BUILD = BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
|
||||
const PR = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
|
||||
|
||||
const regexPrefix = `^BUILD: ${BUILD} \\| PR: ${PR} \\| SHA: ${SHA} \\| File:`;
|
||||
const idxContentRegex = new RegExp(`${regexPrefix} \\/index\\.html$`);
|
||||
const barContentRegex = new RegExp(`${regexPrefix} \\/foo\\/bar\\.js$`);
|
||||
|
||||
await circleBuild(payload(BUILD)).then(h.verifyResponse(201));
|
||||
await Promise.all([
|
||||
getFile(PR, SHA, 'index.html').then(h.verifyResponse(200, idxContentRegex)),
|
||||
getFile(PR, SHA, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex)),
|
||||
]);
|
||||
|
||||
expect({ prNum: PR }).toExistAsABuild();
|
||||
expect({ prNum: PR, isPublic: false }).not.toExistAsABuild();
|
||||
});
|
||||
|
||||
|
||||
it('should be able to create but not serve a hidden preview', async () => {
|
||||
const BUILD = BuildNums.TRUST_CHECK_UNTRUSTED;
|
||||
const PR = PrNums.TRUST_CHECK_UNTRUSTED;
|
||||
|
||||
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)),
|
||||
]);
|
||||
|
||||
expect({ prNum: PR }).not.toExistAsABuild();
|
||||
expect({ prNum: PR, isPublic: false }).toExistAsABuild();
|
||||
});
|
||||
|
||||
|
||||
it('should reject if verification fails', async () => {
|
||||
const BUILD = BuildNums.TRUST_CHECK_ERROR;
|
||||
const PR = PrNums.TRUST_CHECK_ERROR;
|
||||
|
||||
await circleBuild(payload(BUILD)).then(h.verifyResponse(500));
|
||||
expect({ prNum: PR }).toExistAsAnArtifact();
|
||||
expect({ prNum: PR }).not.toExistAsABuild();
|
||||
expect({ prNum: PR, isPublic: false }).not.toExistAsABuild();
|
||||
});
|
||||
|
||||
|
||||
it('should be able to notify that a PR has been updated (and do nothing)', async () => {
|
||||
await prUpdated(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER).then(h.verifyResponse(200));
|
||||
// The PR should still not exist.
|
||||
expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, isPublic: false }).not.toExistAsABuild();
|
||||
expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, isPublic: true }).not.toExistAsABuild();
|
||||
});
|
||||
|
||||
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000);
|
||||
afterEach(() => {
|
||||
h.deletePrDir(pr9);
|
||||
h.cleanUp();
|
||||
});
|
||||
|
||||
|
||||
it('should be able to upload and serve a build for a new PR', done => {
|
||||
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`;
|
||||
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
|
||||
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
|
||||
describe('for an existing PR', () => {
|
||||
|
||||
h.createDummyArchive(pr9, sha9, archivePath);
|
||||
it('should be able to create and serve a public preview', async () => {
|
||||
const BUILD = BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
|
||||
const PR = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
|
||||
|
||||
uploadBuild(pr9, sha9, archivePath).
|
||||
then(() => Promise.all([
|
||||
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)),
|
||||
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)),
|
||||
])).
|
||||
then(done);
|
||||
});
|
||||
const regexPrefix1 = `^PR: ${PR} \\| SHA: ${ALT_SHA} \\| File:`;
|
||||
const idxContentRegex1 = new RegExp(`${regexPrefix1} \\/index\\.html$`);
|
||||
const barContentRegex1 = new RegExp(`${regexPrefix1} \\/foo\\/bar\\.js$`);
|
||||
|
||||
const regexPrefix2 = `^BUILD: ${BUILD} \\| PR: ${PR} \\| SHA: ${SHA} \\| File:`;
|
||||
const idxContentRegex2 = new RegExp(`${regexPrefix2} \\/index\\.html$`);
|
||||
const barContentRegex2 = new RegExp(`${regexPrefix2} \\/foo\\/bar\\.js$`);
|
||||
|
||||
h.createDummyBuild(PR, ALT_SHA);
|
||||
await circleBuild(payload(BUILD)).then(h.verifyResponse(201));
|
||||
await Promise.all([
|
||||
getFile(PR, ALT_SHA, 'index.html').then(h.verifyResponse(200, idxContentRegex1)),
|
||||
getFile(PR, ALT_SHA, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex1)),
|
||||
getFile(PR, SHA, 'index.html').then(h.verifyResponse(200, idxContentRegex2)),
|
||||
getFile(PR, SHA, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex2)),
|
||||
]);
|
||||
|
||||
expect({ prNum: PR, sha: SHA }).toExistAsABuild();
|
||||
expect({ prNum: PR, sha: ALT_SHA }).toExistAsABuild();
|
||||
});
|
||||
|
||||
|
||||
it('should be able to upload and serve a build for an existing PR', done => {
|
||||
const regexPrefix0 = `^PR: ${pr9} \\| SHA: ${sha0} \\| File:`;
|
||||
const idxContentRegex0 = new RegExp(`${regexPrefix0} \\/index\\.html$`);
|
||||
const barContentRegex0 = new RegExp(`${regexPrefix0} \\/foo\\/bar\\.js$`);
|
||||
it('should be able to create but not serve a hidden preview', async () => {
|
||||
const BUILD = BuildNums.TRUST_CHECK_UNTRUSTED;
|
||||
const PR = PrNums.TRUST_CHECK_UNTRUSTED;
|
||||
|
||||
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`;
|
||||
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
|
||||
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
|
||||
h.createDummyBuild(PR, ALT_SHA, false);
|
||||
await circleBuild(payload(BUILD)).then(h.verifyResponse(202));
|
||||
|
||||
h.createDummyBuild(pr9, sha0);
|
||||
h.createDummyArchive(pr9, sha9, archivePath);
|
||||
await Promise.all([
|
||||
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).
|
||||
then(() => Promise.all([
|
||||
getFile(pr9, sha0, 'index.html').then(h.verifyResponse(200, idxContentRegex0)),
|
||||
getFile(pr9, sha0, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex0)),
|
||||
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)),
|
||||
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)),
|
||||
])).
|
||||
then(done);
|
||||
});
|
||||
expect({ prNum: PR, sha: SHA }).not.toExistAsABuild();
|
||||
expect({ prNum: PR, sha: SHA, isPublic: false }).toExistAsABuild();
|
||||
expect({ prNum: PR, sha: ALT_SHA }).not.toExistAsABuild();
|
||||
expect({ prNum: PR, sha: ALT_SHA, isPublic: false }).toExistAsABuild();
|
||||
});
|
||||
|
||||
|
||||
it('should not be able to overwrite a build', done => {
|
||||
const regexPrefix9 = `^PR: ${pr9} \\| SHA: ${sha9} \\| File:`;
|
||||
const idxContentRegex9 = new RegExp(`${regexPrefix9} \\/index\\.html$`);
|
||||
const barContentRegex9 = new RegExp(`${regexPrefix9} \\/foo\\/bar\\.js$`);
|
||||
it('should reject if verification fails', async () => {
|
||||
const BUILD = BuildNums.TRUST_CHECK_ERROR;
|
||||
const PR = PrNums.TRUST_CHECK_ERROR;
|
||||
|
||||
h.createDummyBuild(pr9, sha9);
|
||||
h.createDummyArchive(pr9, sha9, archivePath);
|
||||
h.createDummyBuild(PR, ALT_SHA, false);
|
||||
|
||||
await circleBuild(payload(BUILD)).then(h.verifyResponse(500));
|
||||
|
||||
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 preview', async () => {
|
||||
const BUILD = BuildNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
|
||||
const PR = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
|
||||
|
||||
const regexPrefix = `^PR: ${PR} \\| SHA: ${SHA} \\| File:`;
|
||||
const idxContentRegex = new RegExp(`${regexPrefix} \\/index\\.html$`);
|
||||
const barContentRegex = new RegExp(`${regexPrefix} \\/foo\\/bar\\.js$`);
|
||||
|
||||
h.createDummyBuild(PR, SHA);
|
||||
|
||||
await circleBuild(payload(BUILD)).then(h.verifyResponse(409));
|
||||
await Promise.all([
|
||||
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)),
|
||||
]);
|
||||
|
||||
expect({ prNum: PR }).toExistAsAnArtifact();
|
||||
expect({ prNum: PR }).toExistAsABuild();
|
||||
});
|
||||
|
||||
|
||||
it('should not be able to overwrite an existing hidden preview', async () => {
|
||||
const BUILD = BuildNums.TRUST_CHECK_UNTRUSTED;
|
||||
const PR = PrNums.TRUST_CHECK_UNTRUSTED;
|
||||
h.createDummyBuild(PR, SHA, false);
|
||||
|
||||
await circleBuild(payload(BUILD)).then(h.verifyResponse(409));
|
||||
|
||||
expect({ prNum: PR }).toExistAsAnArtifact();
|
||||
expect({ prNum: PR, isPublic: false }).toExistAsABuild();
|
||||
});
|
||||
|
||||
|
||||
it('should be able to request re-checking visibility (if outdated)', async () => {
|
||||
const publicPr = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
|
||||
const hiddenPr = PrNums.TRUST_CHECK_UNTRUSTED;
|
||||
|
||||
h.createDummyBuild(publicPr, SHA, false);
|
||||
h.createDummyBuild(hiddenPr, SHA, true);
|
||||
|
||||
// PR visibilities are outdated (i.e. the opposte of what the should).
|
||||
expect({ prNum: publicPr, sha: SHA, isPublic: false }).toExistAsABuild(false);
|
||||
expect({ prNum: publicPr, sha: SHA, isPublic: true }).not.toExistAsABuild(false);
|
||||
expect({ prNum: hiddenPr, sha: SHA, isPublic: false }).not.toExistAsABuild(false);
|
||||
expect({ prNum: hiddenPr, sha: SHA, isPublic: true }).toExistAsABuild(false);
|
||||
|
||||
await Promise.all([
|
||||
prUpdated(publicPr).then(h.verifyResponse(200)),
|
||||
prUpdated(hiddenPr).then(h.verifyResponse(200)),
|
||||
]);
|
||||
|
||||
// PR visibilities should have been updated.
|
||||
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 be able to request re-checking visibility (if up-to-date)', async () => {
|
||||
const publicPr = PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER;
|
||||
const hiddenPr = PrNums.TRUST_CHECK_UNTRUSTED;
|
||||
|
||||
h.createDummyBuild(publicPr, SHA, true);
|
||||
h.createDummyBuild(hiddenPr, SHA, false);
|
||||
|
||||
// PR visibilities are already up-to-date.
|
||||
expect({ prNum: publicPr, sha: SHA, isPublic: false }).not.toExistAsABuild(false);
|
||||
expect({ prNum: publicPr, sha: SHA, isPublic: true }).toExistAsABuild(false);
|
||||
expect({ prNum: hiddenPr, sha: SHA, isPublic: false }).toExistAsABuild(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)),
|
||||
]);
|
||||
|
||||
// PR visibilities are still up-to-date.
|
||||
expect({ prNum: publicPr, isPublic: true }).toExistAsABuild();
|
||||
expect({ prNum: publicPr, isPublic: false }).not.toExistAsABuild();
|
||||
expect({ prNum: hiddenPr, isPublic: true }).not.toExistAsABuild();
|
||||
expect({ prNum: hiddenPr, isPublic: false }).toExistAsABuild();
|
||||
});
|
||||
|
||||
|
||||
it('should reject a request if re-checking visibility fails', async () => {
|
||||
const errorPr = PrNums.TRUST_CHECK_ERROR;
|
||||
|
||||
h.createDummyBuild(errorPr, SHA, true);
|
||||
|
||||
expect({ prNum: errorPr, isPublic: false }).not.toExistAsABuild(false);
|
||||
expect({ prNum: errorPr, isPublic: true }).toExistAsABuild(false);
|
||||
|
||||
await prUpdated(errorPr).then(h.verifyResponse(500, /TRUST_CHECK_ERROR/));
|
||||
|
||||
// PR visibility should not have been updated.
|
||||
expect({ prNum: errorPr, isPublic: false }).not.toExistAsABuild();
|
||||
expect({ prNum: errorPr, isPublic: true }).toExistAsABuild();
|
||||
});
|
||||
|
||||
|
||||
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.
|
||||
h.createDummyBuild(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, SHA, false);
|
||||
h.createDummyBuild(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, SHA, true);
|
||||
|
||||
const hiddenPrDir = h.getPrDir(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, false);
|
||||
const publicPrDir = h.getPrDir(PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, true);
|
||||
const bodyRegex = new RegExp(`Request to move '${hiddenPrDir}' to existing directory '${publicPrDir}'`);
|
||||
|
||||
expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, isPublic: false }).toExistAsABuild(false);
|
||||
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));
|
||||
|
||||
// PR visibility should not have been updated.
|
||||
expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, isPublic: false }).toExistAsABuild();
|
||||
expect({ prNum: PrNums.TRUST_CHECK_ACTIVE_TRUSTED_USER, isPublic: true }).toExistAsABuild();
|
||||
});
|
||||
|
||||
uploadBuild(pr9, sha9, archivePath).
|
||||
then(h.verifyResponse(403)).
|
||||
then(() => Promise.all([
|
||||
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(200, idxContentRegex9)),
|
||||
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(200, barContentRegex9)),
|
||||
])).
|
||||
then(done);
|
||||
});
|
||||
|
||||
}));
|
||||
|
@ -0,0 +1,2 @@
|
||||
import '../preview-server';
|
||||
import './mock-external-apis';
|
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,266 +0,0 @@
|
||||
// Imports
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
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 curl = `curl -iL ${authorizationHeader} ${xFileHeader}`;
|
||||
|
||||
|
||||
it('should disallow non-GET requests', done => {
|
||||
const url = `http://${host}/create-build/${pr}/${sha9}`;
|
||||
const bodyRegex = /^Unsupported method/;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
h.runCmd(`curl -iLX POST ${url}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse(405, 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 -iL ${headers1} ${url}`).then(h.verifyResponse(401, bodyRegex)),
|
||||
h.runCmd(`curl -iL ${headers2} ${url}`).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 -iL ${headers1} ${url}`).then(h.verifyResponse(400, bodyRegex)),
|
||||
h.runCmd(`curl -iL ${headers2} ${url}`).then(h.verifyResponse(400, 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);
|
||||
});
|
||||
|
||||
|
||||
it('should not overwrite existing builds', done => {
|
||||
h.createDummyBuild(pr, sha9);
|
||||
expect(h.readBuildFile(pr, sha9, 'index.html')).toContain('index.html');
|
||||
|
||||
h.writeBuildFile(pr, sha9, 'index.html', 'My content');
|
||||
expect(h.readBuildFile(pr, sha9, 'index.html')).toBe('My content');
|
||||
|
||||
h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`).
|
||||
then(h.verifyResponse(403, /^Request to overwrite existing directory/)).
|
||||
then(() => expect(h.readBuildFile(pr, sha9, 'index.html')).toBe('My content')).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should delete the PR directory on error (for new PR)', done => {
|
||||
const prDir = path.join(h.buildsDir, pr);
|
||||
|
||||
h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`).
|
||||
then(h.verifyResponse(500)).
|
||||
then(() => expect(fs.existsSync(prDir)).toBe(false)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should only delete the SHA directory on error (for existing PR)', done => {
|
||||
const prDir = path.join(h.buildsDir, pr);
|
||||
const shaDir = path.join(prDir, sha9);
|
||||
|
||||
h.createDummyBuild(pr, sha0);
|
||||
|
||||
h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`).
|
||||
then(h.verifyResponse(500)).
|
||||
then(() => {
|
||||
expect(fs.existsSync(shaDir)).toBe(false);
|
||||
expect(fs.existsSync(prDir)).toBe(true);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
describe('on successful upload', () => {
|
||||
const archivePath = path.join(h.buildsDir, 'snapshot.tar.gz');
|
||||
let uploadPromise: Promise<CmdResult>;
|
||||
|
||||
beforeEach(() => {
|
||||
h.createDummyArchive(pr, sha9, archivePath);
|
||||
uploadPromise = h.runCmd(`${curl} http://${host}/create-build/${pr}/${sha9}`);
|
||||
});
|
||||
afterEach(() => h.deletePrDir(pr));
|
||||
|
||||
|
||||
it('should respond with 201', done => {
|
||||
uploadPromise.then(h.verifyResponse(201)).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should extract the contents of the uploaded file', done => {
|
||||
uploadPromise.
|
||||
then(() => {
|
||||
expect(h.readBuildFile(pr, sha9, 'index.html')).toContain(`uploaded/${pr}`);
|
||||
expect(h.readBuildFile(pr, sha9, 'foo/bar.js')).toContain(`uploaded/${pr}`);
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it(`should create files/directories owned by '${h.serverUser}'`, done => {
|
||||
const shaDir = path.join(h.buildsDir, pr, 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.serverUser}`),
|
||||
])).
|
||||
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 shaDir = path.join(h.buildsDir, pr, 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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
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}/*`, () => {
|
||||
|
||||
it('should respond with 404 for GET 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)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should respond with 405 for non-GET requests to any URL', done => {
|
||||
const bodyRegex = /^Unsupported method/;
|
||||
|
||||
Promise.all([
|
||||
h.runCmd(`curl -iLX PUT http://${host}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
h.runCmd(`curl -iLX POST http://${host}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
h.runCmd(`curl -iLX PATCH http://${host}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
h.runCmd(`curl -iLX DELETE http://${host}`).then(h.verifyResponse(405, bodyRegex)),
|
||||
]).then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -6,38 +6,50 @@
|
||||
"author": "Angular",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"prebuild": "yarn run clean",
|
||||
"build": "tsc",
|
||||
"build-watch": "yarn run tsc -- --watch",
|
||||
"clean": "node --eval \"require('shelljs').rm('-rf', 'dist')\"",
|
||||
"dev": "concurrently --kill-others --raw --success first \"yarn run build-watch\" \"yarn run test-watch\"",
|
||||
"prebuild": "yarn clean-dist",
|
||||
"build": "yarn ~~build",
|
||||
"prebuild-watch": "yarn prebuild",
|
||||
"build-watch": "yarn ~~build-watch",
|
||||
"clean-dist": "node --eval \"require('shelljs').rm('-rf', 'dist')\"",
|
||||
"predev": "yarn build || true",
|
||||
"dev": "run-p ~~build-watch ~~test-watch",
|
||||
"lint": "tslint --project tsconfig.json",
|
||||
"pre~~test-only": "yarn run lint",
|
||||
"pretest": "yarn build",
|
||||
"test": "yarn ~~test-only",
|
||||
"pretest-watch": "yarn pretest",
|
||||
"test-watch": "yarn ~~test-watch",
|
||||
"~~build": "tsc",
|
||||
"~~build-watch": "yarn ~~build --watch",
|
||||
"pre~~test-only": "yarn lint",
|
||||
"~~test-only": "node dist/test",
|
||||
"pretest": "yarn run build",
|
||||
"test": "yarn run ~~test-only",
|
||||
"pretest-watch": "yarn run build",
|
||||
"test-watch": "nodemon --exec \"yarn run ~~test-only\" --watch dist"
|
||||
"~~test-watch": "nodemon --delay 1 --exec \"yarn ~~test-only\" --watch dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.14.1",
|
||||
"jasmine": "^2.5.3",
|
||||
"jsonwebtoken": "^7.3.0",
|
||||
"shelljs": "^0.7.6"
|
||||
"body-parser": "^1.18.3",
|
||||
"delete-empty": "^2.0.0",
|
||||
"express": "^4.16.3",
|
||||
"jasmine": "^3.2.0",
|
||||
"nock": "^9.6.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": {
|
||||
"@types/express": "^4.0.35",
|
||||
"@types/jasmine": "^2.5.43",
|
||||
"@types/jsonwebtoken": "^7.2.0",
|
||||
"@types/node": "^7.0.5",
|
||||
"@types/shelljs": "^0.7.0",
|
||||
"@types/supertest": "^2.0.0",
|
||||
"concurrently": "^3.3.0",
|
||||
"eslint": "^3.15.0",
|
||||
"eslint-plugin-jasmine": "^2.2.0",
|
||||
"nodemon": "^1.11.0",
|
||||
"supertest": "^3.0.0",
|
||||
"tslint": "^4.4.2",
|
||||
"typescript": "^2.1.6"
|
||||
"@types/body-parser": "^1.17.0",
|
||||
"@types/express": "^4.16.0",
|
||||
"@types/jasmine": "^2.8.8",
|
||||
"@types/nock": "^9.3.0",
|
||||
"@types/node": "^10.9.2",
|
||||
"@types/node-fetch": "^2.1.2",
|
||||
"@types/shelljs": "^0.8.0",
|
||||
"@types/supertest": "^2.0.5",
|
||||
"nodemon": "^1.18.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"supertest": "^3.1.0",
|
||||
"tslint": "^5.11.0",
|
||||
"tslint-jasmine-noSkipOrFocus": "^1.0.9",
|
||||
"typescript": "^3.0.1"
|
||||
}
|
||||
}
|
||||
|
@ -1,133 +1,186 @@
|
||||
// Imports
|
||||
import * as fs from 'fs';
|
||||
import {normalize} from 'path';
|
||||
import * as shell from 'shelljs';
|
||||
import {BuildCleaner} from '../../lib/clean-up/build-cleaner';
|
||||
import {HIDDEN_DIR_PREFIX} from '../../lib/common/constants';
|
||||
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
|
||||
describe('BuildCleaner', () => {
|
||||
let loggerErrorSpy: jasmine.Spy;
|
||||
let loggerLogSpy: jasmine.Spy;
|
||||
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()', () => {
|
||||
|
||||
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\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'repoSlug\' is empty', () => {
|
||||
expect(() => new BuildCleaner('/foo/bar', '', '12345')).
|
||||
toThrowError('Missing or empty required parameter \'repoSlug\'!');
|
||||
it('should throw if \'githubOrg\' is empty', () => {
|
||||
expect(() => new BuildCleaner('/foo/bar', '', 'qux', '12345', 'downloads', 'build.zip')).
|
||||
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', () => {
|
||||
expect(() => new BuildCleaner('/foo/bar', 'baz/qux', '')).
|
||||
expect(() => new BuildCleaner('/foo/bar', 'baz', 'qux', '', 'downloads', 'build.zip')).
|
||||
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()', () => {
|
||||
let cleanerGetExistingBuildNumbersSpy: jasmine.Spy;
|
||||
let cleanerGetOpenPrNumbersSpy: jasmine.Spy;
|
||||
let cleanerGetExistingDownloadsSpy: jasmine.Spy;
|
||||
let cleanerRemoveUnnecessaryBuildsSpy: jasmine.Spy;
|
||||
let existingBuildsDeferred: {resolve: Function, reject: Function};
|
||||
let openPrsDeferred: {resolve: Function, reject: Function};
|
||||
let promise: Promise<void>;
|
||||
let cleanerRemoveUnnecessaryDownloadsSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
cleanerGetExistingBuildNumbersSpy = spyOn(cleaner as any, 'getExistingBuildNumbers').and.callFake(() => {
|
||||
return new Promise((resolve, reject) => existingBuildsDeferred = {resolve, reject});
|
||||
});
|
||||
cleanerGetOpenPrNumbersSpy = spyOn(cleaner as any, 'getOpenPrNumbers').and.callFake(() => {
|
||||
return new Promise((resolve, reject) => openPrsDeferred = {resolve, reject});
|
||||
});
|
||||
cleanerRemoveUnnecessaryBuildsSpy = spyOn(cleaner as any, 'removeUnnecessaryBuilds');
|
||||
cleanerGetExistingBuildNumbersSpy = spyOn(cleaner, 'getExistingBuildNumbers')
|
||||
.and.callFake(() => Promise.resolve(EXISTING_BUILDS));
|
||||
cleanerGetOpenPrNumbersSpy = spyOn(cleaner, 'getOpenPrNumbers')
|
||||
.and.callFake(() => Promise.resolve(OPEN_PRS));
|
||||
cleanerGetExistingDownloadsSpy = spyOn(cleaner, 'getExistingDownloads')
|
||||
.and.callFake(() => Promise.resolve(EXISTING_DOWNLOADS));
|
||||
|
||||
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));
|
||||
|
||||
// Do not complete the test and release the spies synchronously, to avoid running the actual implementations.
|
||||
await promise;
|
||||
});
|
||||
|
||||
|
||||
it('should get the existing builds', () => {
|
||||
expect(cleanerGetExistingBuildNumbersSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it('should get the open PRs', () => {
|
||||
it('should get the open PRs', async () => {
|
||||
await cleaner.cleanUp();
|
||||
expect(cleanerGetOpenPrNumbersSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it('should reject if \'getExistingBuildNumbers()\' rejects', done => {
|
||||
promise.catch(err => {
|
||||
it('should get the existing builds', async () => {
|
||||
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');
|
||||
done();
|
||||
});
|
||||
|
||||
existingBuildsDeferred.reject('Test');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
it('should reject if \'getOpenPrNumbers()\' rejects', done => {
|
||||
promise.catch(err => {
|
||||
it('should reject if \'getExistingBuildNumbers()\' rejects', async () => {
|
||||
try {
|
||||
cleanerGetExistingBuildNumbersSpy.and.callFake(() => Promise.reject('Test'));
|
||||
await cleaner.cleanUp();
|
||||
} catch (err) {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
openPrsDeferred.reject('Test');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
it('should reject if \'removeUnnecessaryBuilds()\' rejects', done => {
|
||||
promise.catch(err => {
|
||||
it('should reject if \'getExistingDownloads()\' rejects', async () => {
|
||||
try {
|
||||
cleanerGetExistingDownloadsSpy.and.callFake(() => Promise.reject('Test'));
|
||||
await cleaner.cleanUp();
|
||||
} catch (err) {
|
||||
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 => {
|
||||
promise.then(() => {
|
||||
expect(cleanerRemoveUnnecessaryBuildsSpy).toHaveBeenCalledWith('foo', 'bar');
|
||||
done();
|
||||
});
|
||||
|
||||
existingBuildsDeferred.resolve('foo');
|
||||
openPrsDeferred.resolve('bar');
|
||||
it('should reject if \'removeUnnecessaryBuilds()\' rejects', async () => {
|
||||
try {
|
||||
cleanerRemoveUnnecessaryBuildsSpy.and.callFake(() => Promise.reject('Test'));
|
||||
await cleaner.cleanUp();
|
||||
} catch (err) {
|
||||
expect(err).toBe('Test');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the value returned by \'removeUnnecessaryBuilds()\'', done => {
|
||||
promise.then(result => {
|
||||
expect(result).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
cleanerRemoveUnnecessaryBuildsSpy.and.returnValue(Promise.resolve('Test'));
|
||||
existingBuildsDeferred.resolve();
|
||||
openPrsDeferred.resolve();
|
||||
it('should reject if \'removeUnnecessaryDownloads()\' rejects', async () => {
|
||||
try {
|
||||
cleanerRemoveUnnecessaryDownloadsSpy.and.callFake(() => Promise.reject('Test'));
|
||||
await cleaner.cleanUp();
|
||||
} catch (err) {
|
||||
expect(err).toBe('Test');
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
// Protected methods
|
||||
|
||||
describe('getExistingBuildNumbers()', () => {
|
||||
let fsReaddirSpy: jasmine.Spy;
|
||||
let readdirCb: (err: any, files?: string[]) => void;
|
||||
@ -135,7 +188,7 @@ describe('BuildCleaner', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
fsReaddirSpy = spyOn(fs, 'readdir').and.callFake((_: string, cb: typeof readdirCb) => readdirCb = cb);
|
||||
promise = (cleaner as any).getExistingBuildNumbers();
|
||||
promise = cleaner.getExistingBuildNumbers();
|
||||
});
|
||||
|
||||
|
||||
@ -170,6 +223,16 @@ describe('BuildCleaner', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should remove `HIDDEN_DIR_PREFIX` from the filenames', done => {
|
||||
promise.then(result => {
|
||||
expect(result).toEqual([12, 34, 56]);
|
||||
done();
|
||||
});
|
||||
|
||||
readdirCb(null, [`${HIDDEN_DIR_PREFIX}12`, '34', `${HIDDEN_DIR_PREFIX}56`]);
|
||||
});
|
||||
|
||||
|
||||
it('should ignore files with non-numeric (or zero) names', done => {
|
||||
promise.then(result => {
|
||||
expect(result).toEqual([12, 34, 56]);
|
||||
@ -183,7 +246,7 @@ describe('BuildCleaner', () => {
|
||||
|
||||
|
||||
describe('getOpenPrNumbers()', () => {
|
||||
let prDeferred: {resolve: Function, reject: Function};
|
||||
let prDeferred: {resolve: (v: any) => void, reject: (v: any) => void};
|
||||
let promise: Promise<number[]>;
|
||||
|
||||
beforeEach(() => {
|
||||
@ -191,7 +254,7 @@ describe('BuildCleaner', () => {
|
||||
return new Promise((resolve, reject) => prDeferred = {resolve, reject});
|
||||
});
|
||||
|
||||
promise = (cleaner as any).getOpenPrNumbers();
|
||||
promise = cleaner.getOpenPrNumbers();
|
||||
});
|
||||
|
||||
|
||||
@ -224,95 +287,215 @@ describe('BuildCleaner', () => {
|
||||
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']);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('removeDir()', () => {
|
||||
let shellChmodSpy: jasmine.Spy;
|
||||
let shellRmSpy: jasmine.Spy;
|
||||
let shellTestSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
shellChmodSpy = spyOn(shell, 'chmod');
|
||||
shellRmSpy = spyOn(shell, 'rm');
|
||||
shellTestSpy = spyOn(shell, 'test').and.returnValue(true);
|
||||
});
|
||||
|
||||
|
||||
it('should test if the directory exists (and return if is does not)', () => {
|
||||
shellTestSpy.and.returnValue(false);
|
||||
cleaner.removeDir('/foo/bar');
|
||||
|
||||
expect(shellTestSpy).toHaveBeenCalledWith('-d', '/foo/bar');
|
||||
expect(shellChmodSpy).not.toHaveBeenCalled();
|
||||
expect(shellRmSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
|
||||
it('should make the directory and its content writable before removing', () => {
|
||||
shellRmSpy.and.callFake(() => expect(shellChmodSpy).toHaveBeenCalledWith('-R', 'a+w', '/foo/bar'));
|
||||
(cleaner as any).removeDir('/foo/bar');
|
||||
cleaner.removeDir('/foo/bar');
|
||||
|
||||
expect(shellRmSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it('should catch errors and log them', () => {
|
||||
const consoleErrorSpy = spyOn(console, 'error');
|
||||
shellRmSpy.and.callFake(() => { throw 'Test'; });
|
||||
shellRmSpy.and.callFake(() => {
|
||||
// tslint:disable-next-line: no-string-throw
|
||||
throw 'Test';
|
||||
});
|
||||
|
||||
(cleaner as any).removeDir('/foo/bar');
|
||||
cleaner.removeDir('/foo/bar');
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalled();
|
||||
expect(consoleErrorSpy.calls.argsFor(0)[0]).toContain('Unable to remove \'/foo/bar\'');
|
||||
expect(consoleErrorSpy.calls.argsFor(0)[1]).toBe('Test');
|
||||
expect(loggerErrorSpy).toHaveBeenCalledWith('ERROR: Unable to remove \'/foo/bar\' due to:', 'Test');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('removeUnnecessaryBuilds()', () => {
|
||||
let consoleLogSpy: jasmine.Spy;
|
||||
let cleanerRemoveDirSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleLogSpy = spyOn(console, 'log');
|
||||
cleanerRemoveDirSpy = spyOn(cleaner as any, 'removeDir');
|
||||
cleanerRemoveDirSpy = spyOn(cleaner, 'removeDir');
|
||||
});
|
||||
|
||||
|
||||
it('should log the number of existing builds, open PRs and builds to be removed', () => {
|
||||
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3], [3, 4, 5, 6]);
|
||||
it('should log the number of existing builds and builds to be removed', () => {
|
||||
cleaner.removeUnnecessaryBuilds([1, 2, 3], [3, 4, 5, 6]);
|
||||
|
||||
expect(console.log).toHaveBeenCalledWith('Existing builds: 3');
|
||||
expect(console.log).toHaveBeenCalledWith('Open pull requests: 4');
|
||||
expect(console.log).toHaveBeenCalledWith('Removing 2 build(s): 1, 2');
|
||||
expect(loggerLogSpy).toHaveBeenCalledWith('Existing builds: 3');
|
||||
expect(loggerLogSpy).toHaveBeenCalledWith('Removing 2 build(s): 1, 2');
|
||||
});
|
||||
|
||||
|
||||
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('/foo/bar/1');
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/2');
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/3');
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/1'));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/2'));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/3'));
|
||||
});
|
||||
|
||||
|
||||
it('should try removing hidden directories as well', () => {
|
||||
cleaner.removeUnnecessaryBuilds([1, 2, 3], []);
|
||||
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}2`));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}3`));
|
||||
});
|
||||
|
||||
|
||||
it('should remove the builds that do not correspond to open PRs', () => {
|
||||
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3, 4], [2, 4]);
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(2);
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/1');
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/3');
|
||||
cleaner.removeUnnecessaryBuilds([1, 2, 3, 4], [2, 4]);
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(4);
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/1'));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/3'));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}3`));
|
||||
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);
|
||||
cleanerRemoveDirSpy.calls.reset();
|
||||
|
||||
(cleaner as any).removeUnnecessaryBuilds([1, 2, 3, 4], []);
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(4);
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/1');
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/2');
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/3');
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith('/foo/bar/4');
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledTimes(8);
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/1'));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/2'));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/3'));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize('/foo/bar/4'));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}1`));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}2`));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}3`));
|
||||
expect(cleanerRemoveDirSpy).toHaveBeenCalledWith(normalize(`/foo/bar/${HIDDEN_DIR_PREFIX}4`));
|
||||
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
|
||||
import {EventEmitter} from 'events';
|
||||
import {ClientRequest, IncomingMessage} from 'http';
|
||||
import * as https from 'https';
|
||||
import * as nock from 'nock';
|
||||
import {GithubApi} from '../../lib/common/github-api';
|
||||
|
||||
// Tests
|
||||
@ -56,7 +54,7 @@ describe('GithubApi', () => {
|
||||
|
||||
|
||||
it('should not pass data to \'request()\'', () => {
|
||||
(api.get as Function)('foo', {}, {});
|
||||
(api.get as any)('foo', {}, {});
|
||||
|
||||
expect(apiRequestSpy).toHaveBeenCalled();
|
||||
expect(apiRequestSpy.calls.argsFor(0)[2]).toBeUndefined();
|
||||
@ -110,41 +108,8 @@ 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()', () => {
|
||||
let deferreds: {resolve: Function, reject: Function}[];
|
||||
let deferreds: {resolve: (v: any) => void, reject: (v: any) => void}[];
|
||||
|
||||
beforeEach(() => {
|
||||
deferreds = [];
|
||||
@ -161,8 +126,8 @@ describe('GithubApi', () => {
|
||||
(api as any).getPaginated('/foo/bar');
|
||||
(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', {baz: 'qux', 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: 1, per_page: 100});
|
||||
});
|
||||
|
||||
|
||||
@ -197,9 +162,9 @@ describe('GithubApi', () => {
|
||||
const paramsForPage = (page: number) => ({baz: 'qux', page, per_page: 100});
|
||||
|
||||
expect(apiGetSpy).toHaveBeenCalledTimes(3);
|
||||
expect(apiGetSpy.calls.argsFor(0)).toEqual(['/foo/bar', paramsForPage(0)]);
|
||||
expect(apiGetSpy.calls.argsFor(1)).toEqual(['/foo/bar', paramsForPage(1)]);
|
||||
expect(apiGetSpy.calls.argsFor(2)).toEqual(['/foo/bar', paramsForPage(2)]);
|
||||
expect(apiGetSpy.calls.argsFor(0)).toEqual(['/foo/bar', paramsForPage(1)]);
|
||||
expect(apiGetSpy.calls.argsFor(1)).toEqual(['/foo/bar', paramsForPage(2)]);
|
||||
expect(apiGetSpy.calls.argsFor(2)).toEqual(['/foo/bar', paramsForPage(3)]);
|
||||
|
||||
expect(data).toEqual(allItems);
|
||||
|
||||
@ -218,191 +183,162 @@ describe('GithubApi', () => {
|
||||
});
|
||||
|
||||
|
||||
describe('request()', () => {
|
||||
let httpsRequestSpy: jasmine.Spy;
|
||||
let latestRequest: ClientRequest;
|
||||
// Protected methods
|
||||
|
||||
beforeEach(() => {
|
||||
const originalRequest = https.request;
|
||||
describe('buildPath()', () => {
|
||||
|
||||
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 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()', () => {
|
||||
it('should return a promise', () => {
|
||||
nock('https://api.github.com').get('').reply(200);
|
||||
expect((api as any).request()).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
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();
|
||||
expect(httpsRequestSpy.calls.argsFor(0)[0]).toEqual(jasmine.objectContaining({
|
||||
headers: jasmine.objectContaining({
|
||||
'User-Agent': `Node/${process.versions.node}`,
|
||||
}),
|
||||
host: 'api.github.com',
|
||||
method: 'method',
|
||||
path: 'path',
|
||||
}));
|
||||
(api as any).request('method', '/path');
|
||||
requestHandler.done();
|
||||
});
|
||||
|
||||
|
||||
it('should call specify an \'Authorization\' header if \'githubToken\' is present', () => {
|
||||
(api as any).request('method', 'path');
|
||||
|
||||
expect(httpsRequestSpy).toHaveBeenCalled();
|
||||
expect(httpsRequestSpy.calls.argsFor(0)[0].headers).toEqual(jasmine.objectContaining({
|
||||
Authorization: 'token 12345',
|
||||
}));
|
||||
it('should add the \'Authorization\' header containing the \'githubToken\'', () => {
|
||||
const requestHandler = nock('https://api.github.com')
|
||||
.intercept('/path', 'method', undefined, {
|
||||
reqheaders: {Authorization: 'token 12345'},
|
||||
})
|
||||
.reply(200);
|
||||
(api as any).request('method', '/path');
|
||||
requestHandler.done();
|
||||
});
|
||||
|
||||
|
||||
it('should reject on request error', done => {
|
||||
(api as any).request('method', 'path').catch((err: any) => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
latestRequest.emit('error', 'Test');
|
||||
});
|
||||
|
||||
|
||||
it('should send the request (i.e. call \'end()\')', () => {
|
||||
(api as any).request('method', 'path');
|
||||
expect(latestRequest.end).toHaveBeenCalled();
|
||||
it('should reject on request error', async () => {
|
||||
nock('https://api.github.com')
|
||||
.intercept('/path', 'method')
|
||||
.replyWithError('Test');
|
||||
let message = 'Failed to reject error';
|
||||
await (api as any).request('method', '/path').catch((err: any) => message = err.message);
|
||||
expect(message).toEqual('Test');
|
||||
});
|
||||
|
||||
|
||||
it('should \'JSON.stringify\' and send the data along with the request', () => {
|
||||
(api as any).request('method', 'path');
|
||||
expect(latestRequest.end).toHaveBeenCalledWith(null);
|
||||
|
||||
(api as any).request('method', 'path', {key: 'value'});
|
||||
expect(latestRequest.end).toHaveBeenCalledWith('{"key":"value"}');
|
||||
const data = {key: 'value'};
|
||||
const requestHandler = nock('https://api.github.com')
|
||||
.intercept('/path', 'method', JSON.stringify(data))
|
||||
.reply(200);
|
||||
(api as any).request('method', '/path', data);
|
||||
requestHandler.done();
|
||||
});
|
||||
|
||||
|
||||
describe('onResponse', () => {
|
||||
let promise: Promise<void>;
|
||||
let respond: (statusCode: number) => IncomingMessage;
|
||||
it('should reject if response statusCode is <200', done => {
|
||||
const requestHandler = nock('https://api.github.com')
|
||||
.intercept('/path', 'method')
|
||||
.reply(199);
|
||||
|
||||
beforeEach(() => {
|
||||
promise = (api as any).request('method', 'path');
|
||||
|
||||
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 => {
|
||||
(api as any).request('method', '/path')
|
||||
.catch((err: string) => {
|
||||
expect(err).toContain('failed');
|
||||
expect(err).toContain('status: 199');
|
||||
done();
|
||||
});
|
||||
|
||||
const res = respond(199);
|
||||
res.emit('end');
|
||||
});
|
||||
requestHandler.done();
|
||||
});
|
||||
|
||||
|
||||
it('should reject if returned statusCode is >=400', done => {
|
||||
promise.catch(err => {
|
||||
it('should reject if response statusCode is >=400', done => {
|
||||
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('status: 400');
|
||||
done();
|
||||
});
|
||||
|
||||
const res = respond(400);
|
||||
res.emit('end');
|
||||
});
|
||||
requestHandler.done();
|
||||
});
|
||||
|
||||
|
||||
it('should include the response text in the rejection message', done => {
|
||||
promise.catch(err => {
|
||||
it('should include the response text in the rejection message', done => {
|
||||
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');
|
||||
done();
|
||||
});
|
||||
requestHandler.done();
|
||||
});
|
||||
|
||||
const res = respond(500);
|
||||
res.emit('data', 'Test');
|
||||
res.emit('end');
|
||||
|
||||
it('should resolve if returned statusCode is >=200 and <400', done => {
|
||||
const requestHandler = nock('https://api.github.com')
|
||||
.intercept('/path', 'method')
|
||||
.reply(200);
|
||||
|
||||
(api as any).request('method', '/path').then(done);
|
||||
requestHandler.done();
|
||||
});
|
||||
|
||||
|
||||
it('should parse the response body into an object using \'JSON.parse\'', done => {
|
||||
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'});
|
||||
done();
|
||||
});
|
||||
requestHandler.done();
|
||||
});
|
||||
|
||||
it('should reject if the response text is malformed JSON', done => {
|
||||
const requestHandler = nock('https://api.github.com')
|
||||
.intercept('/path', 'method')
|
||||
.reply(300, '}');
|
||||
|
||||
it('should resolve if returned statusCode is <=200 <400', done => {
|
||||
promise.then(done);
|
||||
|
||||
const res = respond(200);
|
||||
res.emit('data', '{}');
|
||||
res.emit('end');
|
||||
(api as any).request('method', '/path').catch((err: any) => {
|
||||
expect(err).toEqual(jasmine.any(SyntaxError));
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the response text \'JSON.parsed\'', done => {
|
||||
promise.then(data => {
|
||||
expect(data).toEqual({foo: 'bar'});
|
||||
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 => {
|
||||
promise.catch(err => {
|
||||
expect(err).toEqual(jasmine.any(SyntaxError));
|
||||
done();
|
||||
});
|
||||
|
||||
const res = respond(300);
|
||||
res.emit('data', '}');
|
||||
res.emit('end');
|
||||
});
|
||||
|
||||
requestHandler.done();
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -1,20 +1,27 @@
|
||||
// Imports
|
||||
import {GithubApi} from '../../lib/common/github-api';
|
||||
import {GithubPullRequests} from '../../lib/common/github-pull-requests';
|
||||
|
||||
// Tests
|
||||
describe('GithubPullRequests', () => {
|
||||
let githubApi: jasmine.SpyObj<GithubApi>;
|
||||
|
||||
beforeEach(() => {
|
||||
githubApi = jasmine.createSpyObj('githubApi', ['post', 'get', 'getPaginated']);
|
||||
});
|
||||
|
||||
|
||||
describe('constructor()', () => {
|
||||
|
||||
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 \'githubOrg\' is missing or empty', () => {
|
||||
expect(() => new GithubPullRequests(githubApi, '', 'bar')).
|
||||
toThrowError('Missing or empty required parameter \'githubOrg\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should throw if \'repoSlug\' is missing or empty', () => {
|
||||
expect(() => new GithubPullRequests('12345', '')).
|
||||
toThrowError('Missing or empty required parameter \'repoSlug\'!');
|
||||
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()', () => {
|
||||
let prs: GithubPullRequests;
|
||||
let deferred: {resolve: Function, reject: Function};
|
||||
|
||||
beforeEach(() => {
|
||||
prs = new GithubPullRequests('12345', '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));
|
||||
prs = new GithubPullRequests(githubApi, 'foo', 'bar');
|
||||
});
|
||||
|
||||
|
||||
@ -47,30 +46,54 @@ 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');
|
||||
|
||||
expect(prs.post).toHaveBeenCalledWith('/repos/foo/bar/issues/42/comments', null, {body: 'body'});
|
||||
expect(githubApi.post).toHaveBeenCalledWith('/repos/foo/bar/issues/42/comments', null, {body: 'body'});
|
||||
});
|
||||
|
||||
|
||||
it('should reject if the request fails', done => {
|
||||
githubApi.post.and.callFake(() => Promise.reject('Test'));
|
||||
prs.addComment(42, 'body').catch(err => {
|
||||
expect(err).toBe('Test');
|
||||
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 => {
|
||||
expect(data).toEqual('Test');
|
||||
expect(data).toBe('Test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
deferred.resolve('Test');
|
||||
});
|
||||
|
||||
|
||||
describe('fetch()', () => {
|
||||
let prs: GithubPullRequests;
|
||||
|
||||
beforeEach(() => {
|
||||
prs = new GithubPullRequests(githubApi, 'foo', 'bar');
|
||||
});
|
||||
|
||||
|
||||
it('should make a GET request to GitHub with the correct pathname', () => {
|
||||
prs.fetch(42);
|
||||
expect(githubApi.get).toHaveBeenCalledWith('/repos/foo/bar/issues/42');
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the data returned from GitHub', done => {
|
||||
const expected: any = {number: 42};
|
||||
githubApi.get.and.callFake(() => Promise.resolve(expected));
|
||||
prs.fetch(42).then(data => {
|
||||
expect(data).toEqual(expected);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@ -78,13 +101,8 @@ describe('GithubPullRequests', () => {
|
||||
|
||||
describe('fetchAll()', () => {
|
||||
let prs: GithubPullRequests;
|
||||
let prsGetPaginatedSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
prs = new GithubPullRequests('12345', 'foo/bar');
|
||||
prsGetPaginatedSpy = spyOn(prs as any, 'getPaginated');
|
||||
spyOn(console, 'log');
|
||||
});
|
||||
beforeEach(() => prs = new GithubPullRequests(githubApi, 'foo', 'bar'));
|
||||
|
||||
|
||||
it('should call \'getPaginated()\' with the correct pathname and params', () => {
|
||||
@ -94,22 +112,48 @@ describe('GithubPullRequests', () => {
|
||||
prs.fetchAll('closed');
|
||||
prs.fetchAll('open');
|
||||
|
||||
expect(prsGetPaginatedSpy).toHaveBeenCalledTimes(3);
|
||||
expect(prsGetPaginatedSpy.calls.argsFor(0)).toEqual([expectedPathname, {state: 'all'}]);
|
||||
expect(prsGetPaginatedSpy.calls.argsFor(1)).toEqual([expectedPathname, {state: 'closed'}]);
|
||||
expect(prsGetPaginatedSpy.calls.argsFor(2)).toEqual([expectedPathname, {state: 'open'}]);
|
||||
expect(githubApi.getPaginated).toHaveBeenCalledTimes(3);
|
||||
expect(githubApi.getPaginated.calls.argsFor(0)).toEqual([expectedPathname, {state: 'all'}]);
|
||||
expect(githubApi.getPaginated.calls.argsFor(1)).toEqual([expectedPathname, {state: 'closed'}]);
|
||||
expect(githubApi.getPaginated.calls.argsFor(2)).toEqual([expectedPathname, {state: 'open'}]);
|
||||
});
|
||||
|
||||
|
||||
it('should default to \'all\' if no state is specified', () => {
|
||||
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()\'', () => {
|
||||
prsGetPaginatedSpy.and.returnValue('Test');
|
||||
expect(prs.fetchAll()).toBe('Test');
|
||||
githubApi.getPaginated.and.returnValue('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,44 +1,41 @@
|
||||
// Imports
|
||||
import {GithubApi} from '../../lib/common/github-api';
|
||||
import {GithubTeams} from '../../lib/common/github-teams';
|
||||
|
||||
// Tests
|
||||
describe('GithubTeams', () => {
|
||||
|
||||
let githubApi: jasmine.SpyObj<GithubApi>;
|
||||
|
||||
beforeEach(() => {
|
||||
githubApi = jasmine.createSpyObj('githubApi', ['post', 'get', 'getPaginated']);
|
||||
});
|
||||
|
||||
describe('constructor()', () => {
|
||||
|
||||
it('should throw if \'githubToken\' is missing or empty', () => {
|
||||
expect(() => new GithubTeams('', 'org')).
|
||||
toThrowError('Missing or empty required parameter \'githubToken\'!');
|
||||
it('should throw if \'githubOrg\' is missing or empty', () => {
|
||||
expect(() => new GithubTeams(githubApi, '')).
|
||||
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()', () => {
|
||||
let teams: GithubTeams;
|
||||
let teamsGetPaginatedSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
teams = new GithubTeams('12345', 'foo');
|
||||
teamsGetPaginatedSpy = spyOn(teams as any, 'getPaginated');
|
||||
teams = new GithubTeams(githubApi, 'foo');
|
||||
});
|
||||
|
||||
|
||||
it('should call \'getPaginated()\' with the correct pathname and params', () => {
|
||||
teams.fetchAll();
|
||||
expect(teamsGetPaginatedSpy).toHaveBeenCalledWith('/orgs/foo/teams');
|
||||
expect(githubApi.getPaginated).toHaveBeenCalledWith('/orgs/foo/teams');
|
||||
});
|
||||
|
||||
|
||||
it('should forward the value returned by \'getPaginated()\'', () => {
|
||||
teamsGetPaginatedSpy.and.returnValue('Test');
|
||||
expect(teams.fetchAll()).toBe('Test');
|
||||
githubApi.getPaginated.and.returnValue('Test');
|
||||
expect(teams.fetchAll() as any).toBe('Test');
|
||||
});
|
||||
|
||||
});
|
||||
@ -46,59 +43,59 @@ describe('GithubTeams', () => {
|
||||
|
||||
describe('isMemberById()', () => {
|
||||
let teams: GithubTeams;
|
||||
let teamsGetSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
teams = new GithubTeams('12345', 'foo');
|
||||
teamsGetSpy = spyOn(teams, 'get');
|
||||
teams = new GithubTeams(githubApi, 'foo');
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect(teams.isMemberById('user', [1])).toEqual(jasmine.any(Promise));
|
||||
githubApi.get.and.callFake(() => Promise.resolve());
|
||||
const promise = teams.isMemberById('user', [1]);
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with false if called with an empty array', done => {
|
||||
teams.isMemberById('user', []).then(isMember => {
|
||||
expect(isMember).toBe(false);
|
||||
expect(teamsGetSpy).not.toHaveBeenCalled();
|
||||
expect(githubApi.get).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should call \'get()\' with the correct pathname', done => {
|
||||
teamsGetSpy.and.returnValue(Promise.resolve(null));
|
||||
githubApi.get.and.callFake(() => Promise.resolve());
|
||||
teams.isMemberById('user', [1]).then(() => {
|
||||
expect(teamsGetSpy).toHaveBeenCalledWith('/teams/1/memberships/user');
|
||||
expect(githubApi.get).toHaveBeenCalledWith('/teams/1/memberships/user');
|
||||
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 => {
|
||||
expect(isMember).toBe(false);
|
||||
expect(teamsGetSpy).toHaveBeenCalled();
|
||||
expect(githubApi.get).toHaveBeenCalled();
|
||||
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 => {
|
||||
expect(isMember).toBe(false);
|
||||
expect(teamsGetSpy).toHaveBeenCalled();
|
||||
expect(githubApi.get).toHaveBeenCalled();
|
||||
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 => {
|
||||
expect(isMember).toBe(true);
|
||||
done();
|
||||
@ -112,15 +109,15 @@ describe('GithubTeams', () => {
|
||||
'/teams/2/memberships/user': Promise.reject(null),
|
||||
'/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 => {
|
||||
expect(isMember).toBe(true);
|
||||
|
||||
expect(teamsGetSpy).toHaveBeenCalledTimes(3);
|
||||
expect(teamsGetSpy.calls.argsFor(0)[0]).toBe('/teams/1/memberships/user');
|
||||
expect(teamsGetSpy.calls.argsFor(1)[0]).toBe('/teams/2/memberships/user');
|
||||
expect(teamsGetSpy.calls.argsFor(2)[0]).toBe('/teams/3/memberships/user');
|
||||
expect(githubApi.get).toHaveBeenCalledTimes(3);
|
||||
expect(githubApi.get.calls.argsFor(0)[0]).toBe('/teams/1/memberships/user');
|
||||
expect(githubApi.get.calls.argsFor(1)[0]).toBe('/teams/2/memberships/user');
|
||||
expect(githubApi.get.calls.argsFor(2)[0]).toBe('/teams/3/memberships/user');
|
||||
|
||||
done();
|
||||
});
|
||||
@ -134,16 +131,16 @@ describe('GithubTeams', () => {
|
||||
'/teams/3/memberships/user': Promise.resolve({state: 'not active'}),
|
||||
'/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 => {
|
||||
expect(isMember).toBe(false);
|
||||
|
||||
expect(teamsGetSpy).toHaveBeenCalledTimes(4);
|
||||
expect(teamsGetSpy.calls.argsFor(0)[0]).toBe('/teams/1/memberships/user');
|
||||
expect(teamsGetSpy.calls.argsFor(1)[0]).toBe('/teams/2/memberships/user');
|
||||
expect(teamsGetSpy.calls.argsFor(2)[0]).toBe('/teams/3/memberships/user');
|
||||
expect(teamsGetSpy.calls.argsFor(3)[0]).toBe('/teams/4/memberships/user');
|
||||
expect(githubApi.get).toHaveBeenCalledTimes(4);
|
||||
expect(githubApi.get.calls.argsFor(0)[0]).toBe('/teams/1/memberships/user');
|
||||
expect(githubApi.get.calls.argsFor(1)[0]).toBe('/teams/2/memberships/user');
|
||||
expect(githubApi.get.calls.argsFor(2)[0]).toBe('/teams/3/memberships/user');
|
||||
expect(githubApi.get.calls.argsFor(3)[0]).toBe('/teams/4/memberships/user');
|
||||
|
||||
done();
|
||||
});
|
||||
@ -158,7 +155,7 @@ describe('GithubTeams', () => {
|
||||
let teamsIsMemberByIdSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
teams = new GithubTeams('12345', 'foo');
|
||||
teams = new GithubTeams(githubApi, 'foo');
|
||||
|
||||
const mockResponse = Promise.resolve([{id: 1, slug: 'team1'}, {id: 2, slug: 'team2'}]);
|
||||
teamsFetchAllSpy = spyOn(teams, 'fetchAll').and.returnValue(mockResponse);
|
||||
@ -178,7 +175,7 @@ describe('GithubTeams', () => {
|
||||
|
||||
|
||||
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 => {
|
||||
expect(isMember).toBe(false);
|
||||
done();
|
||||
@ -206,7 +203,7 @@ describe('GithubTeams', () => {
|
||||
|
||||
|
||||
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 => {
|
||||
expect(isMember).toBe(false);
|
||||
expect(teamsIsMemberByIdSpy).toHaveBeenCalled();
|
||||
@ -215,16 +212,17 @@ describe('GithubTeams', () => {
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with the value \'isMemberById()\' resolves with', done => {
|
||||
teamsIsMemberByIdSpy.and.returnValues(Promise.resolve(false), Promise.resolve(true));
|
||||
it('should resolve with the value \'isMemberById()\' resolves with', async () => {
|
||||
|
||||
Promise.all([
|
||||
teams.isMemberBySlug('user', ['team1']).then(isMember => expect(isMember).toBe(false)),
|
||||
teams.isMemberBySlug('user', ['team1']).then(isMember => expect(isMember).toBe(true)),
|
||||
]).then(() => {
|
||||
expect(teamsIsMemberByIdSpy).toHaveBeenCalledTimes(2);
|
||||
done();
|
||||
});
|
||||
teamsIsMemberByIdSpy.and.callFake(() => Promise.resolve(true));
|
||||
const isMember1 = await teams.isMemberBySlug('user', ['team1']);
|
||||
expect(isMember1).toBe(true);
|
||||
expect(teamsIsMemberByIdSpy).toHaveBeenCalledWith('user', [1]);
|
||||
|
||||
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
|
||||
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
|
||||
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()', () => {
|
||||
|
||||
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
|
||||
const specFiles = [`${__dirname}/**/*.spec.js`];
|
||||
const helpers = [`${__dirname}/helpers.js`];
|
||||
runTests(specFiles, helpers);
|
||||
runTests(specFiles);
|
||||
|
@ -0,0 +1,690 @@
|
||||
// Imports
|
||||
import * as cp from 'child_process';
|
||||
import {EventEmitter} from 'events';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as shell from 'shelljs';
|
||||
import {SHORT_SHA_LEN} from '../../lib/common/constants';
|
||||
import {Logger} from '../../lib/common/utils';
|
||||
import {BuildCreator} from '../../lib/preview-server/build-creator';
|
||||
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/preview-server/build-events';
|
||||
import {PreviewServerError} from '../../lib/preview-server/preview-error';
|
||||
import {expectToBePreviewServerError} from './helpers';
|
||||
|
||||
// Tests
|
||||
describe('BuildCreator', () => {
|
||||
const pr = 9;
|
||||
const sha = '9'.repeat(40);
|
||||
const shortSha = sha.substr(0, SHORT_SHA_LEN);
|
||||
const archive = 'snapshot.tar.gz';
|
||||
const buildsDir = 'builds/dir';
|
||||
const hiddenPrDir = path.join(buildsDir, `hidden--${pr}`);
|
||||
const publicPrDir = path.join(buildsDir, `${pr}`);
|
||||
const hiddenShaDir = path.join(hiddenPrDir, shortSha);
|
||||
const publicShaDir = path.join(publicPrDir, shortSha);
|
||||
let bc: BuildCreator;
|
||||
|
||||
beforeEach(() => bc = new BuildCreator(buildsDir));
|
||||
|
||||
|
||||
describe('constructor()', () => {
|
||||
|
||||
it('should throw if \'buildsDir\' is missing or empty', () => {
|
||||
expect(() => new BuildCreator('')).toThrowError('Missing or empty required parameter \'buildsDir\'!');
|
||||
});
|
||||
|
||||
|
||||
it('should extend EventEmitter', () => {
|
||||
expect(bc).toEqual(jasmine.any(BuildCreator));
|
||||
expect(bc).toEqual(jasmine.any(EventEmitter));
|
||||
|
||||
expect(Object.getPrototypeOf(bc)).toBe(BuildCreator.prototype);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('create()', () => {
|
||||
let bcEmitSpy: jasmine.Spy;
|
||||
let bcExistsSpy: jasmine.Spy;
|
||||
let bcExtractArchiveSpy: jasmine.Spy;
|
||||
let bcUpdatePrVisibilitySpy: jasmine.Spy;
|
||||
let shellMkdirSpy: jasmine.Spy;
|
||||
let shellRmSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
bcEmitSpy = spyOn(bc, 'emit');
|
||||
bcExistsSpy = spyOn(bc as any, 'exists');
|
||||
bcExtractArchiveSpy = spyOn(bc as any, 'extractArchive');
|
||||
bcUpdatePrVisibilitySpy = spyOn(bc, 'updatePrVisibility');
|
||||
shellMkdirSpy = spyOn(shell, 'mkdir');
|
||||
shellRmSpy = spyOn(shell, 'rm');
|
||||
});
|
||||
|
||||
|
||||
[true, false].forEach(isPublic => {
|
||||
const prDir = isPublic ? publicPrDir : hiddenPrDir;
|
||||
const shaDir = isPublic ? publicShaDir : hiddenShaDir;
|
||||
|
||||
|
||||
it('should return a promise', done => {
|
||||
const promise = bc.create(pr, sha, archive, isPublic);
|
||||
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
||||
// to avoid running the actual `extractArchive()`.
|
||||
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should update the PR\'s visibility first if necessary', done => {
|
||||
bcUpdatePrVisibilitySpy.and.callFake(() => expect(shellMkdirSpy).not.toHaveBeenCalled());
|
||||
|
||||
bc.create(pr, sha, archive, isPublic).
|
||||
then(() => {
|
||||
expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(pr, isPublic);
|
||||
expect(shellMkdirSpy).toHaveBeenCalled();
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should create the build directory (and any parent directories)', done => {
|
||||
bc.create(pr, sha, archive, isPublic).
|
||||
then(() => expect(shellMkdirSpy).toHaveBeenCalledWith('-p', shaDir)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should extract the archive contents into the build directory', done => {
|
||||
bc.create(pr, sha, archive, isPublic).
|
||||
then(() => expect(bcExtractArchiveSpy).toHaveBeenCalledWith(archive, shaDir)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should emit a CreatedBuildEvent on success', done => {
|
||||
let emitted = false;
|
||||
|
||||
bcEmitSpy.and.callFake((type: string, evt: CreatedBuildEvent) => {
|
||||
expect(type).toBe(CreatedBuildEvent.type);
|
||||
expect(evt).toEqual(jasmine.any(CreatedBuildEvent));
|
||||
expect(evt.pr).toBe(+pr);
|
||||
expect(evt.sha).toBe(shortSha);
|
||||
expect(evt.isPublic).toBe(isPublic);
|
||||
|
||||
emitted = true;
|
||||
});
|
||||
|
||||
bc.create(pr, sha, archive, isPublic).
|
||||
then(() => expect(emitted).toBe(true)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
describe('on error', () => {
|
||||
let existsValues: {[dir: string]: boolean};
|
||||
|
||||
beforeEach(() => {
|
||||
existsValues = {
|
||||
[prDir]: false,
|
||||
[shaDir]: false,
|
||||
};
|
||||
|
||||
bcExistsSpy.and.callFake((dir: string) => existsValues[dir]);
|
||||
});
|
||||
|
||||
|
||||
it('should abort and skip further operations if changing the PR\'s visibility fails', done => {
|
||||
const mockError = new PreviewServerError(543, 'Test');
|
||||
bcUpdatePrVisibilitySpy.and.callFake(() => Promise.reject(mockError));
|
||||
|
||||
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||
expect(err).toBe(mockError);
|
||||
|
||||
expect(bcExistsSpy).not.toHaveBeenCalled();
|
||||
expect(shellMkdirSpy).not.toHaveBeenCalled();
|
||||
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should abort and skip further operations if the build does already exist', done => {
|
||||
existsValues[shaDir] = true;
|
||||
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||
const publicOrNot = isPublic ? 'public' : 'non-public';
|
||||
expectToBePreviewServerError(err, 409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`);
|
||||
expect(shellMkdirSpy).not.toHaveBeenCalled();
|
||||
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should detect existing build directory after visibility change', done => {
|
||||
bcUpdatePrVisibilitySpy.and.callFake(() => existsValues[prDir] = existsValues[shaDir] = true);
|
||||
|
||||
expect(bcExistsSpy(prDir)).toBe(false);
|
||||
expect(bcExistsSpy(shaDir)).toBe(false);
|
||||
|
||||
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||
const publicOrNot = isPublic ? 'public' : 'non-public';
|
||||
expectToBePreviewServerError(err, 409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`);
|
||||
expect(shellMkdirSpy).not.toHaveBeenCalled();
|
||||
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should abort and skip further operations if it fails to create the directories', done => {
|
||||
shellMkdirSpy.and.throwError('');
|
||||
bc.create(pr, sha, archive, isPublic).catch(() => {
|
||||
expect(shellMkdirSpy).toHaveBeenCalled();
|
||||
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should abort and skip further operations if it fails to extract the archive', done => {
|
||||
bcExtractArchiveSpy.and.throwError('');
|
||||
bc.create(pr, sha, archive, isPublic).catch(() => {
|
||||
expect(shellMkdirSpy).toHaveBeenCalled();
|
||||
expect(bcExtractArchiveSpy).toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should delete the PR directory (for new PR)', done => {
|
||||
bcExtractArchiveSpy.and.throwError('');
|
||||
bc.create(pr, sha, archive, isPublic).catch(() => {
|
||||
expect(shellRmSpy).toHaveBeenCalledWith('-rf', prDir);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should delete the SHA directory (for existing PR)', done => {
|
||||
existsValues[prDir] = true;
|
||||
bcExtractArchiveSpy.and.throwError('');
|
||||
|
||||
bc.create(pr, sha, archive, isPublic).catch(() => {
|
||||
expect(shellRmSpy).toHaveBeenCalledWith('-rf', shaDir);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should reject with an PreviewServerError', done => {
|
||||
// tslint:disable-next-line: no-string-throw
|
||||
shellMkdirSpy.and.callFake(() => { throw 'Test'; });
|
||||
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||
expectToBePreviewServerError(err, 500, `Error while creating preview at: ${shaDir}\nTest`);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should pass PreviewServerError instances unmodified', done => {
|
||||
shellMkdirSpy.and.callFake(() => { throw new PreviewServerError(543, 'Test'); });
|
||||
bc.create(pr, sha, archive, isPublic).catch(err => {
|
||||
expectToBePreviewServerError(err, 543, 'Test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('updatePrVisibility()', () => {
|
||||
let bcEmitSpy: jasmine.Spy;
|
||||
let bcExistsSpy: jasmine.Spy;
|
||||
let bcListShasByDate: jasmine.Spy;
|
||||
let shellMvSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
bcEmitSpy = spyOn(bc, 'emit');
|
||||
bcExistsSpy = spyOn(bc as any, 'exists');
|
||||
bcListShasByDate = spyOn(bc as any, 'listShasByDate');
|
||||
shellMvSpy = spyOn(shell, 'mv');
|
||||
|
||||
bcExistsSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
|
||||
bcListShasByDate.and.returnValue([]);
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', done => {
|
||||
const promise = bc.updatePrVisibility(pr, true);
|
||||
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
||||
// to avoid running the actual `extractArchive()`.
|
||||
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
[true, false].forEach(makePublic => {
|
||||
const oldPrDir = makePublic ? hiddenPrDir : publicPrDir;
|
||||
const newPrDir = makePublic ? publicPrDir : hiddenPrDir;
|
||||
|
||||
|
||||
it('should rename the directory', done => {
|
||||
bc.updatePrVisibility(pr, makePublic).
|
||||
then(() => expect(shellMvSpy).toHaveBeenCalledWith(oldPrDir, newPrDir)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
describe('when the visibility is updated', () => {
|
||||
|
||||
it('should resolve to true', done => {
|
||||
bc.updatePrVisibility(pr, makePublic).
|
||||
then(result => expect(result).toBe(true)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should rename the directory', done => {
|
||||
bc.updatePrVisibility(pr, makePublic).
|
||||
then(() => expect(shellMvSpy).toHaveBeenCalledWith(oldPrDir, newPrDir)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should emit a ChangedPrVisibilityEvent on success', done => {
|
||||
let emitted = false;
|
||||
|
||||
bcEmitSpy.and.callFake((type: string, evt: ChangedPrVisibilityEvent) => {
|
||||
expect(type).toBe(ChangedPrVisibilityEvent.type);
|
||||
expect(evt).toEqual(jasmine.any(ChangedPrVisibilityEvent));
|
||||
expect(evt.pr).toBe(+pr);
|
||||
expect(evt.shas).toEqual(jasmine.any(Array));
|
||||
expect(evt.isPublic).toBe(makePublic);
|
||||
|
||||
emitted = true;
|
||||
});
|
||||
|
||||
bc.updatePrVisibility(pr, makePublic).
|
||||
then(() => expect(emitted).toBe(true)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should include all shas in the emitted event', done => {
|
||||
const shas = ['foo', 'bar', 'baz'];
|
||||
let emitted = false;
|
||||
|
||||
bcListShasByDate.and.callFake(() => Promise.resolve(shas));
|
||||
bcEmitSpy.and.callFake((type: string, evt: ChangedPrVisibilityEvent) => {
|
||||
expect(bcListShasByDate).toHaveBeenCalledWith(newPrDir);
|
||||
|
||||
expect(type).toBe(ChangedPrVisibilityEvent.type);
|
||||
expect(evt).toEqual(jasmine.any(ChangedPrVisibilityEvent));
|
||||
expect(evt.pr).toBe(+pr);
|
||||
expect(evt.shas).toBe(shas);
|
||||
expect(evt.isPublic).toBe(makePublic);
|
||||
|
||||
emitted = true;
|
||||
});
|
||||
|
||||
bc.updatePrVisibility(pr, makePublic).
|
||||
then(() => expect(emitted).toBe(true)).
|
||||
then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
it('should do nothing if the visibility is already up-to-date', done => {
|
||||
bcExistsSpy.and.callFake((dir: string) => dir === newPrDir);
|
||||
bc.updatePrVisibility(pr, makePublic).
|
||||
then(result => {
|
||||
expect(result).toBe(false);
|
||||
expect(shellMvSpy).not.toHaveBeenCalled();
|
||||
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should do nothing if the PR directory does not exist', done => {
|
||||
bcExistsSpy.and.returnValue(false);
|
||||
bc.updatePrVisibility(pr, makePublic).
|
||||
then(result => {
|
||||
expect(result).toBe(false);
|
||||
expect(shellMvSpy).not.toHaveBeenCalled();
|
||||
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
describe('on error', () => {
|
||||
|
||||
it('should abort and skip further operations if both directories exist', done => {
|
||||
bcExistsSpy.and.returnValue(true);
|
||||
bc.updatePrVisibility(pr, makePublic).catch(err => {
|
||||
expectToBePreviewServerError(err, 409,
|
||||
`Request to move '${oldPrDir}' to existing directory '${newPrDir}'.`);
|
||||
expect(shellMvSpy).not.toHaveBeenCalled();
|
||||
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should abort and skip further operations if it fails to rename the directory', done => {
|
||||
shellMvSpy.and.throwError('');
|
||||
bc.updatePrVisibility(pr, makePublic).catch(() => {
|
||||
expect(shellMvSpy).toHaveBeenCalled();
|
||||
expect(bcListShasByDate).not.toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should abort and skip further operations if it fails to list the SHAs', done => {
|
||||
bcListShasByDate.and.throwError('');
|
||||
bc.updatePrVisibility(pr, makePublic).catch(() => {
|
||||
expect(shellMvSpy).toHaveBeenCalled();
|
||||
expect(bcListShasByDate).toHaveBeenCalled();
|
||||
expect(bcEmitSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should reject with an PreviewServerError', done => {
|
||||
// tslint:disable-next-line: no-string-throw
|
||||
shellMvSpy.and.callFake(() => { throw 'Test'; });
|
||||
bc.updatePrVisibility(pr, makePublic).catch(err => {
|
||||
expectToBePreviewServerError(err, 500,
|
||||
`Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\nTest`);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should pass PreviewServerError instances unmodified', done => {
|
||||
shellMvSpy.and.callFake(() => { throw new PreviewServerError(543, 'Test'); });
|
||||
bc.updatePrVisibility(pr, makePublic).catch(err => {
|
||||
expectToBePreviewServerError(err, 543, 'Test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
// Protected methods
|
||||
|
||||
describe('exists()', () => {
|
||||
let fsAccessSpy: jasmine.Spy;
|
||||
let fsAccessCbs: ((v?: any) => void)[];
|
||||
|
||||
beforeEach(() => {
|
||||
fsAccessCbs = [];
|
||||
fsAccessSpy = spyOn(fs, 'access').and.callFake((_: string, cb: (v?: any) => void) => fsAccessCbs.push(cb));
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect((bc as any).exists('foo')).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should call \'fs.access()\' with the specified argument', () => {
|
||||
(bc as any).exists('foo');
|
||||
expect(fsAccessSpy).toHaveBeenCalledWith('foo', jasmine.any(Function));
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with \'true\' if \'fs.access()\' succeeds', done => {
|
||||
Promise.
|
||||
all([(bc as any).exists('foo'), (bc as any).exists('bar')]).
|
||||
then(results => expect(results).toEqual([true, true])).
|
||||
then(done);
|
||||
|
||||
fsAccessCbs[0]();
|
||||
fsAccessCbs[1](null);
|
||||
});
|
||||
|
||||
|
||||
it('should resolve with \'false\' if \'fs.access()\' errors', done => {
|
||||
Promise.
|
||||
all([(bc as any).exists('foo'), (bc as any).exists('bar')]).
|
||||
then(results => expect(results).toEqual([false, false])).
|
||||
then(done);
|
||||
|
||||
fsAccessCbs[0]('Error');
|
||||
fsAccessCbs[1](new Error());
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('extractArchive()', () => {
|
||||
let consoleWarnSpy: jasmine.Spy;
|
||||
let shellChmodSpy: jasmine.Spy;
|
||||
let shellRmSpy: jasmine.Spy;
|
||||
let cpExecSpy: jasmine.Spy;
|
||||
let cpExecCbs: ((...args: any[]) => void)[];
|
||||
|
||||
beforeEach(() => {
|
||||
cpExecCbs = [];
|
||||
|
||||
consoleWarnSpy = spyOn(Logger.prototype, 'warn');
|
||||
shellChmodSpy = spyOn(shell, 'chmod');
|
||||
shellRmSpy = spyOn(shell, 'rm');
|
||||
cpExecSpy = spyOn(cp, 'exec').and.callFake((_: string, cb: (...args: any[]) => void) => cpExecCbs.push(cb));
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', () => {
|
||||
expect((bc as any).extractArchive('foo', 'bar')).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should "gunzip" and "untar" the input file into the output directory', () => {
|
||||
const cmd = 'tar --extract --gzip --directory "output/dir" --file "input/file"';
|
||||
|
||||
(bc as any).extractArchive('input/file', 'output/dir');
|
||||
expect(cpExecSpy).toHaveBeenCalledWith(cmd, jasmine.any(Function));
|
||||
});
|
||||
|
||||
|
||||
it('should log (as a warning) any stderr output if extracting succeeded', done => {
|
||||
(bc as any).extractArchive('foo', 'bar').
|
||||
then(() => expect(consoleWarnSpy).toHaveBeenCalledWith('This is stderr')).
|
||||
then(done);
|
||||
|
||||
cpExecCbs[0](null, 'This is stdout', 'This is stderr');
|
||||
});
|
||||
|
||||
|
||||
it('should make the build directory non-writable', done => {
|
||||
(bc as any).extractArchive('foo', 'bar').
|
||||
then(() => expect(shellChmodSpy).toHaveBeenCalledWith('-R', 'a-w', 'bar')).
|
||||
then(done);
|
||||
|
||||
cpExecCbs[0]();
|
||||
});
|
||||
|
||||
|
||||
it('should delete the build artifact file on success', done => {
|
||||
(bc as any).extractArchive('input/file', 'output/dir').
|
||||
then(() => expect(shellRmSpy).toHaveBeenCalledWith('-f', 'input/file')).
|
||||
then(done);
|
||||
|
||||
cpExecCbs[0]();
|
||||
});
|
||||
|
||||
|
||||
describe('on error', () => {
|
||||
|
||||
it('should abort and skip further operations if it fails to extract the archive', done => {
|
||||
(bc as any).extractArchive('foo', 'bar').catch((err: any) => {
|
||||
expect(shellChmodSpy).not.toHaveBeenCalled();
|
||||
expect(shellRmSpy).not.toHaveBeenCalled();
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
cpExecCbs[0]('Test');
|
||||
});
|
||||
|
||||
|
||||
it('should abort and skip further operations if it fails to make non-writable', done => {
|
||||
(bc as any).extractArchive('foo', 'bar').catch((err: any) => {
|
||||
expect(shellChmodSpy).toHaveBeenCalled();
|
||||
expect(shellRmSpy).not.toHaveBeenCalled();
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
shellChmodSpy.and.callFake(() => {
|
||||
// tslint:disable-next-line: no-string-throw
|
||||
throw 'Test';
|
||||
});
|
||||
|
||||
cpExecCbs[0]();
|
||||
});
|
||||
|
||||
|
||||
it('should abort and reject if it fails to remove the build artifact file', done => {
|
||||
(bc as any).extractArchive('foo', 'bar').catch((err: any) => {
|
||||
expect(shellChmodSpy).toHaveBeenCalled();
|
||||
expect(shellRmSpy).toHaveBeenCalled();
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
|
||||
shellRmSpy.and.callFake(() => {
|
||||
// tslint:disable-next-line: no-string-throw
|
||||
throw 'Test';
|
||||
});
|
||||
|
||||
cpExecCbs[0]();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('listShasByDate()', () => {
|
||||
let shellLsSpy: jasmine.Spy;
|
||||
const lsResult = (name: string, mtimeMs: number, isDirectory = true) => ({
|
||||
isDirectory: () => isDirectory,
|
||||
mtime: new Date(mtimeMs),
|
||||
name,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
shellLsSpy = spyOn(shell, 'ls').and.returnValue([]);
|
||||
});
|
||||
|
||||
|
||||
it('should return a promise', done => {
|
||||
const promise = (bc as any).listShasByDate('input/dir');
|
||||
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
||||
// to avoid running the actual `ls()`.
|
||||
|
||||
expect(promise).toEqual(jasmine.any(Promise));
|
||||
});
|
||||
|
||||
|
||||
it('should `ls()` files with their metadata', done => {
|
||||
(bc as any).listShasByDate('input/dir').
|
||||
then(() => expect(shellLsSpy).toHaveBeenCalledWith('-l', 'input/dir')).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should reject if listing files fails', done => {
|
||||
shellLsSpy.and.callFake(() => Promise.reject('Test'));
|
||||
(bc as any).listShasByDate('input/dir').catch((err: string) => {
|
||||
expect(err).toBe('Test');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should return the filenames', done => {
|
||||
shellLsSpy.and.callFake(() => Promise.resolve([
|
||||
lsResult('foo', 100),
|
||||
lsResult('bar', 200),
|
||||
lsResult('baz', 300),
|
||||
]));
|
||||
|
||||
(bc as any).listShasByDate('input/dir').
|
||||
then((shas: string[]) => expect(shas).toEqual(['foo', 'bar', 'baz'])).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should sort by date', done => {
|
||||
shellLsSpy.and.callFake(() => Promise.resolve([
|
||||
lsResult('foo', 300),
|
||||
lsResult('bar', 100),
|
||||
lsResult('baz', 200),
|
||||
]));
|
||||
|
||||
(bc as any).listShasByDate('input/dir').
|
||||
then((shas: string[]) => expect(shas).toEqual(['bar', 'baz', 'foo'])).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should not break with ShellJS\' custom `sort()` method', done => {
|
||||
const mockArray = [
|
||||
lsResult('foo', 300),
|
||||
lsResult('bar', 100),
|
||||
lsResult('baz', 200),
|
||||
];
|
||||
mockArray.sort = jasmine.createSpy('sort');
|
||||
|
||||
shellLsSpy.and.callFake(() => Promise.resolve(mockArray));
|
||||
(bc as any).listShasByDate('input/dir').
|
||||
then((shas: string[]) => {
|
||||
expect(shas).toEqual(['bar', 'baz', 'foo']);
|
||||
expect(mockArray.sort).not.toHaveBeenCalled();
|
||||
}).
|
||||
then(done);
|
||||
});
|
||||
|
||||
|
||||
it('should only include directories', done => {
|
||||
shellLsSpy.and.callFake(() => Promise.resolve([
|
||||
lsResult('foo', 100),
|
||||
lsResult('bar', 200, false),
|
||||
lsResult('baz', 300),
|
||||
]));
|
||||
|
||||
(bc as any).listShasByDate('input/dir').
|
||||
then((shas: string[]) => expect(shas).toEqual(['foo', 'baz'])).
|
||||
then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -0,0 +1,58 @@
|
||||
// Imports
|
||||
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from '../../lib/preview-server/build-events';
|
||||
|
||||
// Tests
|
||||
describe('ChangedPrVisibilityEvent', () => {
|
||||
let evt: ChangedPrVisibilityEvent;
|
||||
|
||||
beforeEach(() => evt = new ChangedPrVisibilityEvent(42, ['foo', 'bar'], true));
|
||||
|
||||
|
||||
it('should have a static \'type\' property', () => {
|
||||
expect(ChangedPrVisibilityEvent.type).toBe('pr.changedVisibility');
|
||||
});
|
||||
|
||||
|
||||
it('should have a \'pr\' property', () => {
|
||||
expect(evt.pr).toBe(42);
|
||||
});
|
||||
|
||||
|
||||
it('should have a \'shas\' property', () => {
|
||||
expect(evt.shas).toEqual(['foo', 'bar']);
|
||||
});
|
||||
|
||||
|
||||
it('should have an \'isPublic\' property', () => {
|
||||
expect(evt.isPublic).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('CreatedBuildEvent', () => {
|
||||
let evt: CreatedBuildEvent;
|
||||
|
||||
beforeEach(() => evt = new CreatedBuildEvent(42, 'bar', true));
|
||||
|
||||
|
||||
it('should have a static \'type\' property', () => {
|
||||
expect(CreatedBuildEvent.type).toBe('build.created');
|
||||
});
|
||||
|
||||
|
||||
it('should have a \'pr\' property', () => {
|
||||
expect(evt.pr).toBe(42);
|
||||
});
|
||||
|
||||
|
||||
it('should have a \'sha\' property', () => {
|
||||
expect(evt.sha).toBe('bar');
|
||||
});
|
||||
|
||||
|
||||
it('should have an \'isPublic\' property', () => {
|
||||
expect(evt.isPublic).toBe(true);
|
||||
});
|
||||
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user