mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 17:50:32 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f46063cca5 | ||
|
|
c0cfa93b69 | ||
|
|
7d6f620f91 | ||
|
|
c973a1f603 | ||
|
|
1b8f60fad4 | ||
|
|
7a30de9ad7 | ||
|
|
4247b65e09 | ||
|
|
8c388d5173 | ||
|
|
fce945679f | ||
|
|
7772e199b4 | ||
|
|
755e105252 | ||
|
|
611d0c98ee | ||
|
|
949a90187b | ||
|
|
bb14d9255a | ||
|
|
98e35926ff | ||
|
|
cd433405cb | ||
|
|
3ad54b0fc9 | ||
|
|
5184117ed6 | ||
|
|
868df87146 | ||
|
|
2f0cc2361b | ||
|
|
4be6c51418 | ||
|
|
99c4c618c9 | ||
|
|
742a1a05e5 | ||
|
|
6f806cabb1 | ||
|
|
4c84794a31 | ||
|
|
a1ab5a4fb5 | ||
|
|
dc11c1c487 | ||
|
|
805865356c | ||
|
|
5fc0757fb0 | ||
|
|
2c6d92745f | ||
|
|
f677e038cf | ||
|
|
ef13c2bf0e | ||
|
|
68e552010e | ||
|
|
c5143fabad | ||
|
|
64efdad61d | ||
|
|
8bc07ca452 | ||
|
|
ea4d28e7d9 | ||
|
|
e1fce4a066 | ||
|
|
76d75e9c17 | ||
|
|
9278b3c17d | ||
|
|
ef9fbe84c0 | ||
|
|
b3a69e8863 | ||
|
|
5e4663ac44 | ||
|
|
ca18a555c1 | ||
|
|
c446bbb016 | ||
|
|
d5254434c6 | ||
|
|
e2277b9482 | ||
|
|
739502aef9 | ||
|
|
de42624992 | ||
|
|
0ccfbe5dd6 | ||
|
|
3ffad12681 | ||
|
|
565867b5a8 | ||
|
|
303cc82a06 | ||
|
|
4c2abc0693 | ||
|
|
b63167fbae | ||
|
|
8adb6434bf | ||
|
|
4c9e407fef | ||
|
|
ef566b3f9c | ||
|
|
895231ea98 | ||
|
|
c7000a754e | ||
|
|
9ac8404681 | ||
|
|
6c0b7d5951 | ||
|
|
ddfe60a7b0 | ||
|
|
7dedeb904b | ||
|
|
4c02e0102b | ||
|
|
e91516fcc7 | ||
|
|
d01f6c0d8e | ||
|
|
1ce69cd7c9 | ||
|
|
83c10793ac | ||
|
|
a344c11ba1 | ||
|
|
c7d5a9a819 | ||
|
|
2f43ed94ab | ||
|
|
4a4ebf3cd6 | ||
|
|
18b832dbda | ||
|
|
1200e7fecd | ||
|
|
3cc6b73d3a | ||
|
|
4831ff186a | ||
|
|
a2d60a4a76 | ||
|
|
9f604777b9 | ||
|
|
fce9bda9bb | ||
|
|
4cc6745a0d | ||
|
|
67f0b806f4 | ||
|
|
9280dfb3ad | ||
|
|
2befbb0eba | ||
|
|
41359b7194 | ||
|
|
c57861a67d | ||
|
|
d31ece8793 | ||
|
|
4f7148b756 | ||
|
|
bc8a781d86 | ||
|
|
153a70fb85 | ||
|
|
2f93492fc3 | ||
|
|
0b39ff1457 | ||
|
|
f6817037ae | ||
|
|
216f97ea8a | ||
|
|
11c194a168 | ||
|
|
3c74c111a7 | ||
|
|
b69b8b87a7 | ||
|
|
825baa9f68 | ||
|
|
9f5736cfaa | ||
|
|
ae24ffbe86 | ||
|
|
487316370b | ||
|
|
8bc3565270 | ||
|
|
07450f6bf7 | ||
|
|
71d48c3cf2 | ||
|
|
7343e81065 | ||
|
|
1f1310bef6 | ||
|
|
27c69ec8e1 | ||
|
|
b81b896bc6 | ||
|
|
ef79db2ca5 | ||
|
|
88565eec62 | ||
|
|
e8a520e55f | ||
|
|
9a33f7bd93 | ||
|
|
23c70e237b | ||
|
|
7fcf41db78 | ||
|
|
36f4a4bd38 | ||
|
|
87541db80d | ||
|
|
80112c682f | ||
|
|
eb29d3dadb | ||
|
|
558c367038 | ||
|
|
4b88d00379 | ||
|
|
aad3b5ccd5 | ||
|
|
8b50806093 | ||
|
|
45c9bda2e9 | ||
|
|
23237210d1 | ||
|
|
e34382e6a3 | ||
|
|
b8ad2ee29a | ||
|
|
6bd62bac64 | ||
|
|
8b959fc6d8 | ||
|
|
6e615e20d6 | ||
|
|
efbc5fc4bc | ||
|
|
79c77ae835 | ||
|
|
618b6b1caf | ||
|
|
864658df80 | ||
|
|
4510e86b64 | ||
|
|
4af4490abc | ||
|
|
37c4040203 | ||
|
|
55f9be3b5c | ||
|
|
152b25b93c | ||
|
|
ea0d0bb4dc | ||
|
|
37e360aead | ||
|
|
20211e6265 | ||
|
|
226c888cc9 | ||
|
|
0e9f8ec272 | ||
|
|
8eb5d8028d | ||
|
|
2fb296d116 | ||
|
|
010c1ee439 | ||
|
|
dddc4a0e95 | ||
|
|
679ccf19d9 | ||
|
|
5ba3cc6623 | ||
|
|
c1a4731762 | ||
|
|
767577e386 | ||
|
|
d2f620dbc6 | ||
|
|
0aa556c29e | ||
|
|
10cf003758 | ||
|
|
0e6254453a | ||
|
|
587a29892f | ||
|
|
db8245d954 | ||
|
|
0088306bca | ||
|
|
b6fc718713 | ||
|
|
18e55dafd4 | ||
|
|
8e013ab793 | ||
|
|
b88e79f0f3 | ||
|
|
35b505120d | ||
|
|
6807d3080b | ||
|
|
0d1cfb3286 | ||
|
|
0b9a5621fe | ||
|
|
9bd15af02a | ||
|
|
aab3bf1330 | ||
|
|
9e95218523 | ||
|
|
bb98e6de03 | ||
|
|
2604228233 | ||
|
|
a7035d3469 | ||
|
|
f6dce0e17c | ||
|
|
ccfa6f9fc3 | ||
|
|
0ee8e2f967 | ||
|
|
94dc4c6e93 | ||
|
|
4144e387d6 | ||
|
|
de4e05e1e0 | ||
|
|
36118b700f | ||
|
|
2fb947b47a | ||
|
|
0f1156e951 | ||
|
|
e9f8be92b7 | ||
|
|
6360ea9f8e | ||
|
|
ad2d8e80f0 | ||
|
|
bc0a5fdaa6 | ||
|
|
d6cfc887c3 | ||
|
|
ce41c436aa | ||
|
|
ec096eaaa2 | ||
|
|
9c32dd5308 | ||
|
|
69d7ee9547 | ||
|
|
e2e4cb67ca | ||
|
|
4a08ad4997 | ||
|
|
107a1c12bd | ||
|
|
8fe5268a73 | ||
|
|
faba1ad8e3 | ||
|
|
ea4885c710 | ||
|
|
4d85002d2b | ||
|
|
535b17f592 | ||
|
|
e556b45d4d | ||
|
|
48130dcbdc | ||
|
|
1419ca6325 | ||
|
|
8513dc0645 | ||
|
|
70e8f8370b | ||
|
|
135c90a8b8 | ||
|
|
fa588c2d24 | ||
|
|
b4ce7a91c2 | ||
|
|
8ca9d66c32 | ||
|
|
a86fabde59 | ||
|
|
40a896675f | ||
|
|
cf7848e3c5 | ||
|
|
3d4394a9bc | ||
|
|
05f4c08d09 | ||
|
|
a4107a86f9 | ||
|
|
a5c695dd88 | ||
|
|
7576d9195d | ||
|
|
838c1508be | ||
|
|
7941683435 | ||
|
|
91f2311a50 | ||
|
|
45d10d9b83 | ||
|
|
f5d5093922 | ||
|
|
fa4687b0f8 | ||
|
|
e78c475118 | ||
|
|
def91af01f | ||
|
|
5eb0fb75ac | ||
|
|
de8c69f766 | ||
|
|
5879ac424d | ||
|
|
e30f62ec12 | ||
|
|
dc4e36e8a8 | ||
|
|
12ded8c9a9 | ||
|
|
7378c6fd0e | ||
|
|
77a8b08834 | ||
|
|
79db9eb482 | ||
|
|
59f6d36114 | ||
|
|
c36c26f5a9 | ||
|
|
d6692f20b5 | ||
|
|
6e37f596df | ||
|
|
d326891ef8 | ||
|
|
46a34327d9 | ||
|
|
97242af30f | ||
|
|
ca0a8f79cd | ||
|
|
67eac59585 | ||
|
|
8cf1f0dc22 | ||
|
|
51702e4bda | ||
|
|
426a3d1ef3 | ||
|
|
e8d71a52ec | ||
|
|
ccd0cb19a4 | ||
|
|
9f85178d71 | ||
|
|
085ee52305 | ||
|
|
22a71275b2 | ||
|
|
5a730d70d6 | ||
|
|
41f58e5e52 | ||
|
|
34af57f585 | ||
|
|
4674cbebc5 | ||
|
|
69add6f523 | ||
|
|
b70e874135 | ||
|
|
03fa29dd5a | ||
|
|
9e467b900e | ||
|
|
6e1c7dd91e | ||
|
|
9812406c18 | ||
|
|
cf11ae93af | ||
|
|
14756ad39b | ||
|
|
1486b481f6 | ||
|
|
84d82e9a59 | ||
|
|
c7ce4a8670 | ||
|
|
fe0a3b4c41 | ||
|
|
0bce278bd4 | ||
|
|
fedfdb3364 | ||
|
|
028f0ab74b | ||
|
|
cd278155e2 | ||
|
|
1e419ab49d | ||
|
|
2f57fd199d | ||
|
|
5932775cd4 | ||
|
|
d68d6c38d6 | ||
|
|
d690e10caf | ||
|
|
99ea613b39 | ||
|
|
13715317bd | ||
|
|
18d1ed441b | ||
|
|
db067c4757 | ||
|
|
51388f6adb | ||
|
|
fcbf363538 | ||
|
|
b4d7795006 | ||
|
|
2364ed74d7 | ||
|
|
87fb74dd5a | ||
|
|
eef0c6ae6e | ||
|
|
5ee905a977 | ||
|
|
ca3e083995 | ||
|
|
b7ae91892c | ||
|
|
508ca745c1 | ||
|
|
95e8a38c85 | ||
|
|
441f3eaf4c | ||
|
|
9edb10ca55 | ||
|
|
7f094bc605 | ||
|
|
2463e958aa | ||
|
|
390e6fef9e | ||
|
|
6b416c3dfb | ||
|
|
60d2daf4c8 | ||
|
|
9528f25ff2 | ||
|
|
5da03dfca7 | ||
|
|
8b3b0376c3 | ||
|
|
5f3c4756d2 | ||
|
|
3b34a64893 | ||
|
|
243cf8d630 | ||
|
|
671ce30ed4 | ||
|
|
9021887072 | ||
|
|
6a80d4c8e9 | ||
|
|
471e26817a | ||
|
|
14bbd1ee0f | ||
|
|
22b06b444d | ||
|
|
cebd29e40d | ||
|
|
acf7e55b5b | ||
|
|
7c29aa3b38 | ||
|
|
dee5b4cf60 | ||
|
|
896e14b1e4 | ||
|
|
42c20d6ef5 | ||
|
|
0c94f80f68 | ||
|
|
95f308737e | ||
|
|
2a2a0392cd | ||
|
|
bcbdae8278 | ||
|
|
805aee67ee | ||
|
|
0477984d7a | ||
|
|
4e96196e07 | ||
|
|
25ee2abdaa | ||
|
|
75b71ae457 | ||
|
|
7548fc35fd | ||
|
|
5bc14b5b30 | ||
|
|
bee98ce25e | ||
|
|
0e6a5315ab | ||
|
|
91b51528ef | ||
|
|
5e3962afc2 | ||
|
|
f07c52d2fb | ||
|
|
50846a1a96 | ||
|
|
4cb726f374 | ||
|
|
da9d1b4984 | ||
|
|
64ad52dc45 | ||
|
|
6169e5ff5b | ||
|
|
ef33973f7f | ||
|
|
33a5254f2a | ||
|
|
0d737ecf6d | ||
|
|
951eea2396 | ||
|
|
75968bf449 | ||
|
|
32aa46ac69 | ||
|
|
339cae2532 | ||
|
|
e6ec3bfe6a | ||
|
|
012518c375 | ||
|
|
cc8c8584d9 | ||
|
|
d1910b3c42 | ||
|
|
4a1fa3d4fa | ||
|
|
61178c9d06 | ||
|
|
094c04c7f0 | ||
|
|
37cca4878d | ||
|
|
2b40e1ec05 | ||
|
|
6e95d3afcc | ||
|
|
6b9b9f0be2 | ||
|
|
f14b963b29 | ||
|
|
ddd2814e8f | ||
|
|
36f15a81a9 | ||
|
|
ece93dac20 | ||
|
|
0226dbba5a | ||
|
|
e773e77185 | ||
|
|
893cbb2ec5 | ||
|
|
1898de2609 | ||
|
|
fe03cc28c4 | ||
|
|
5309b7b6ce | ||
|
|
bc9b8442d7 | ||
|
|
a84762b00d | ||
|
|
2d117e1d7e | ||
|
|
986674942d | ||
|
|
13c224589b | ||
|
|
20b091fbfa | ||
|
|
168b6afdfa | ||
|
|
3c6320b929 | ||
|
|
4f1dc582bd | ||
|
|
daf2ef1e30 | ||
|
|
f703bd8b5a | ||
|
|
997cd7058a | ||
|
|
e896c2f6c5 | ||
|
|
3d723ca426 | ||
|
|
c17e1b3fa2 | ||
|
|
d70fc36485 | ||
|
|
47784ca6d3 | ||
|
|
3e5a3aca27 | ||
|
|
89b0227594 | ||
|
|
b98a30e7c6 | ||
|
|
5c7862a685 | ||
|
|
82833a9eb4 | ||
|
|
66171c3c4b | ||
|
|
179def91c7 | ||
|
|
9f63ad4527 | ||
|
|
878028271a | ||
|
|
d4e15bbcbf | ||
|
|
13bb056df1 | ||
|
|
14d56904d2 | ||
|
|
c2ffd05763 | ||
|
|
46e394fbef | ||
|
|
2613f0c2f9 | ||
|
|
e5fd7c01f4 | ||
|
|
73a0dd6e23 | ||
|
|
a9dc7f9eeb | ||
|
|
c3836f23d1 | ||
|
|
bfd73cbb19 | ||
|
|
6a1753edaa | ||
|
|
558a73086d | ||
|
|
15966fbd26 | ||
|
|
1e8b55383e | ||
|
|
6920ebb0ba | ||
|
|
55f35949e1 | ||
|
|
ccb998a221 | ||
|
|
30bc27b4c4 | ||
|
|
da3c95ccaf | ||
|
|
bbcbe1dceb | ||
|
|
f15beea454 | ||
|
|
16e384be76 | ||
|
|
e74e6d8fc5 | ||
|
|
b6f4603107 | ||
|
|
0e7d7f765a | ||
|
|
33b4d8d719 | ||
|
|
0d765748e3 | ||
|
|
93953f1424 | ||
|
|
e9b67db05c | ||
|
|
09bf912b9e | ||
|
|
e701bacdaa | ||
|
|
cc22542031 | ||
|
|
6c53262b5b | ||
|
|
7d7d4995d8 | ||
|
|
977546c8c9 | ||
|
|
53dcd4cb43 | ||
|
|
9e18e738eb | ||
|
|
252c7187d5 | ||
|
|
90852b9557 | ||
|
|
0df16236a2 | ||
|
|
ecaf5a4318 | ||
|
|
86e5e1a7b1 | ||
|
|
ab10094071 | ||
|
|
2d536d73fe | ||
|
|
80ea8ee50b | ||
|
|
b49c5f47d1 | ||
|
|
a9da78946b | ||
|
|
b6d9accea4 | ||
|
|
4611a65bce | ||
|
|
db29b3fc18 | ||
|
|
2534a46bd3 | ||
|
|
7def4b502e | ||
|
|
4ea26d7ca3 | ||
|
|
c4c8075b1f | ||
|
|
266545d2a0 | ||
|
|
ddd7046513 | ||
|
|
7a44eeb22b | ||
|
|
5bcf8263cf | ||
|
|
f380ba0106 | ||
|
|
1a6d81f68e | ||
|
|
522818113b | ||
|
|
f93d347665 | ||
|
|
e96cee1c3b | ||
|
|
4d46e0a8ac | ||
|
|
80e35c7524 | ||
|
|
186c26bc99 | ||
|
|
be6a7f54f9 | ||
|
|
fff7aac5b6 | ||
|
|
0d90fbd3ff | ||
|
|
e71779049e | ||
|
|
7da909e3c1 | ||
|
|
e439fd14a9 | ||
|
|
ffed1b446a | ||
|
|
7b65412ec6 | ||
|
|
f1cbdcc094 | ||
|
|
ec5573efa6 | ||
|
|
242da62271 | ||
|
|
23d1221ef9 | ||
|
|
5402d8c74f | ||
|
|
176026645c | ||
|
|
b0d4ed6c0e | ||
|
|
2f7ac71921 | ||
|
|
b599315a85 | ||
|
|
e86d8aa77a | ||
|
|
ab4ac87d52 | ||
|
|
af98fb1621 | ||
|
|
0ce7c1b3b4 | ||
|
|
2bdaa0d428 | ||
|
|
2486a4de97 | ||
|
|
6137b215e9 | ||
|
|
265ba16594 | ||
|
|
a7bc3d0c98 | ||
|
|
ee618880d7 | ||
|
|
2c95c8c310 | ||
|
|
b90e37d9fc | ||
|
|
34563070f4 | ||
|
|
66b269f4bf | ||
|
|
168cb4d4a0 | ||
|
|
91b531da43 | ||
|
|
2e0d567361 | ||
|
|
81e6280cff | ||
|
|
e2fda7b9d3 | ||
|
|
ede30807f5 | ||
|
|
9f354013fa | ||
|
|
30552f134c | ||
|
|
e4beba9a21 | ||
|
|
b71ab7ec84 | ||
|
|
596cfc372b | ||
|
|
d5f6c5680c | ||
|
|
28c508ff62 | ||
|
|
97ff6f406d | ||
|
|
bd39b296fa | ||
|
|
bfbe1e930b | ||
|
|
3fe45176cc | ||
|
|
bffa6fbf03 | ||
|
|
161808bc37 | ||
|
|
ac42b15acd | ||
|
|
47d21de4eb | ||
|
|
e12ffa9ebc | ||
|
|
2944023986 | ||
|
|
49c1e7670d | ||
|
|
b157b901e3 | ||
|
|
1a1f88f202 | ||
|
|
d02d70da91 | ||
|
|
c37f2957bb | ||
|
|
a32f5fe57a | ||
|
|
b7f6d36a63 | ||
|
|
2975a025ac | ||
|
|
f4e396dde2 | ||
|
|
ef54911323 | ||
|
|
18b1074232 | ||
|
|
8468e0c5ea | ||
|
|
436d5c9ec9 | ||
|
|
1f88f7e1a3 | ||
|
|
09682754f8 | ||
|
|
8e3d84b1e5 | ||
|
|
8bb2631192 | ||
|
|
38a7fb6266 | ||
|
|
0ac2fb77a1 | ||
|
|
f12774b539 | ||
|
|
d50bdedcec | ||
|
|
8d443e5992 | ||
|
|
2b5fb6de53 | ||
|
|
dc86f27199 | ||
|
|
d97ed80867 | ||
|
|
3601b448fe | ||
|
|
68aab13d51 | ||
|
|
6cef13d756 | ||
|
|
1d45941f5d | ||
|
|
5e5badf11f | ||
|
|
840c65946b | ||
|
|
fdbd786126 | ||
|
|
d5365275e0 | ||
|
|
886e48d3ee | ||
|
|
342c78a4f6 | ||
|
|
a483d94e26 | ||
|
|
b40412a7a1 | ||
|
|
d652e04f7b | ||
|
|
85326d1104 | ||
|
|
28c087138b | ||
|
|
8badf14ec3 | ||
|
|
4f1db29ef8 | ||
|
|
2354dc1b71 | ||
|
|
f106bfa6d7 | ||
|
|
1a7edbea06 | ||
|
|
5bfaee9e4d | ||
|
|
0cb2309d7d | ||
|
|
0415a1d97f | ||
|
|
249c60f4d1 | ||
|
|
c357cc378c | ||
|
|
f7c0d731b6 | ||
|
|
e852e3edac | ||
|
|
2a283b74a3 | ||
|
|
c749bc2372 | ||
|
|
9c60f7190e | ||
|
|
8126b2276e | ||
|
|
e1de21015f | ||
|
|
61e505f1f8 | ||
|
|
2e3611161e | ||
|
|
95f5eaaacf | ||
|
|
87a3ea99e2 | ||
|
|
ddab65a55d | ||
|
|
0cf8f8ce0d | ||
|
|
2b531ec70c | ||
|
|
6cd7966904 | ||
|
|
ad72ab990f | ||
|
|
b53412fc00 | ||
|
|
cbb2de9e11 | ||
|
|
593c42f601 | ||
|
|
84b10b9dad | ||
|
|
0f286061e4 | ||
|
|
c8aad01480 | ||
|
|
d70a6bee69 | ||
|
|
67799b5c54 | ||
|
|
05b8eef622 | ||
|
|
50e56b5bc4 | ||
|
|
30ab4e62ec | ||
|
|
a1d901294b | ||
|
|
eed818cfe5 | ||
|
|
db0b6bfcf5 | ||
|
|
da81c1c6df | ||
|
|
42d3296f07 | ||
|
|
f9b4b1a6e8 | ||
|
|
d03603a75f | ||
|
|
ffa9e5ef66 | ||
|
|
e7f3cf9c74 | ||
|
|
e82a4a0a57 | ||
|
|
175f07fac9 | ||
|
|
2869744a5d | ||
|
|
99f898a483 | ||
|
|
1448db96e6 | ||
|
|
e02036a46a | ||
|
|
09f33263cb | ||
|
|
412b1ec74c | ||
|
|
9a1538fb4d | ||
|
|
0ad9b31ee3 | ||
|
|
4cbb7bb2f2 | ||
|
|
ae109cd8c6 | ||
|
|
fbaba774a6 | ||
|
|
38bc8062a4 | ||
|
|
d41d184b55 | ||
|
|
d6d32a7a7d | ||
|
|
15a9532fa7 | ||
|
|
3b9fb724fe | ||
|
|
33767b5635 | ||
|
|
175fc2b30e | ||
|
|
aec9347d69 | ||
|
|
91500d07c0 | ||
|
|
62ca14b504 | ||
|
|
9158a95d57 | ||
|
|
d0f3e387e7 | ||
|
|
158856f8ec | ||
|
|
1e59957753 | ||
|
|
f97d46040c | ||
|
|
d7485cb5d2 | ||
|
|
071fdb7f4a | ||
|
|
6dd96ffb20 | ||
|
|
f56fb0f7de | ||
|
|
7d28f6add9 | ||
|
|
eebef3e461 | ||
|
|
8336f630ea | ||
|
|
7d6bdbb6ed | ||
|
|
403b5c237f | ||
|
|
2367ed7911 | ||
|
|
b650f76979 | ||
|
|
96e59c198d | ||
|
|
bf1704d268 | ||
|
|
279dfd00c8 | ||
|
|
4d2577865a | ||
|
|
9d88fba40e | ||
|
|
9d2353b4f7 | ||
|
|
44b338d50a | ||
|
|
62114cab3b | ||
|
|
d426fb0355 | ||
|
|
2a8d6683ff | ||
|
|
7b18165390 | ||
|
|
814a44ac27 | ||
|
|
f5483c8996 | ||
|
|
acdbed4762 | ||
|
|
f09e4b54cf | ||
|
|
859fdf5e46 | ||
|
|
6aa72d5b7d | ||
|
|
886900df29 | ||
|
|
a450fed1f8 | ||
|
|
4c78100adb | ||
|
|
0317e6fe71 | ||
|
|
6825877344 | ||
|
|
a47ea88d28 | ||
|
|
a9e7f304ec | ||
|
|
d92a2ae3f4 | ||
|
|
b7dea35da4 | ||
|
|
5a5f11752d | ||
|
|
b6c300b5c2 | ||
|
|
0117dbfa5f | ||
|
|
c106f8a1a4 | ||
|
|
8b9741a70c | ||
|
|
271b85fd74 | ||
|
|
21a481790b | ||
|
|
ab5d9237f1 | ||
|
|
756853f2f3 | ||
|
|
5eb80d15f6 | ||
|
|
a98043b4ad | ||
|
|
d0818932e7 | ||
|
|
23b60ed385 | ||
|
|
5d2811bf21 | ||
|
|
7b202f2aa7 | ||
|
|
fa6e9c1735 | ||
|
|
c3300036d8 | ||
|
|
5efcdb2940 | ||
|
|
8113450ea4 | ||
|
|
d10b0c3c65 | ||
|
|
e8de0e8712 | ||
|
|
e1da1099da | ||
|
|
264aa8fefa | ||
|
|
941d0294b7 | ||
|
|
366f4e8914 | ||
|
|
1460c3c5a2 | ||
|
|
6271472827 | ||
|
|
3bc4a054ae | ||
|
|
dd85d559d7 | ||
|
|
ab10b082e5 | ||
|
|
60b60066f9 | ||
|
|
9e9e517698 | ||
|
|
020c0f2b98 | ||
|
|
dd74ebdc51 | ||
|
|
ddf7381efb | ||
|
|
3b67b912e4 | ||
|
|
37d7fa4510 | ||
|
|
096601473a | ||
|
|
2e25ed81ca | ||
|
|
4e11ea1d77 | ||
|
|
d9d470426b | ||
|
|
119b4ad8b2 | ||
|
|
ae1e9f000d | ||
|
|
eb0505f8f8 | ||
|
|
c8f7800ab5 | ||
|
|
bc4e7becdd | ||
|
|
71ece19c40 | ||
|
|
e9053c0d15 | ||
|
|
cf76296142 | ||
|
|
5e5d3626f4 | ||
|
|
5808c5b412 | ||
|
|
cd64392d44 | ||
|
|
79f889e16c | ||
|
|
595bfecf1c | ||
|
|
ced2e192eb | ||
|
|
ee91612c07 | ||
|
|
4d815c1fd9 | ||
|
|
d2687a55b7 | ||
|
|
41142d33dc | ||
|
|
52ab6107c5 | ||
|
|
56ac348ed4 | ||
|
|
1ac0af53ab | ||
|
|
f9361b471c | ||
|
|
8f39e0dcde | ||
|
|
81de402d2d | ||
|
|
9d6e3f52d8 | ||
|
|
6a0a9bbaaa | ||
|
|
24bc29b3cd | ||
|
|
aac5ebff73 | ||
|
|
61cc5b0a4a | ||
|
|
1383783c33 | ||
|
|
971129be6b | ||
|
|
f4fb10483a | ||
|
|
256a0e1f22 | ||
|
|
b82f0b04fe | ||
|
|
9714420f90 | ||
|
|
0054f0bc26 | ||
|
|
38f565d849 | ||
|
|
1aff7ae2a3 | ||
|
|
31020047ca | ||
|
|
26778ec52b | ||
|
|
b27590f4f5 | ||
|
|
ea2af5af04 | ||
|
|
3f1a7a0da0 | ||
|
|
8184a13bf3 | ||
|
|
2f7be56bef | ||
|
|
5c166bb3b2 | ||
|
|
4e76526843 | ||
|
|
0e6fae0632 | ||
|
|
62a8fd1b02 | ||
|
|
5a8e252790 | ||
|
|
78613cd7c0 | ||
|
|
8f6f7f7e1c | ||
|
|
f46ad6b898 | ||
|
|
f153b08a1e | ||
|
|
6b57f00d7d | ||
|
|
de65ab702b | ||
|
|
a87b735832 | ||
|
|
afc6022529 | ||
|
|
2d3bb21f60 | ||
|
|
c26f478b45 | ||
|
|
f958a0db3e | ||
|
|
6df1130140 | ||
|
|
260a771070 | ||
|
|
6659ed97ca | ||
|
|
4eae49fe58 | ||
|
|
41fef5c0e4 | ||
|
|
8c4b45a439 | ||
|
|
e35b5e78d2 | ||
|
|
b04c1a2c20 | ||
|
|
25d75b6f94 | ||
|
|
a2adacf5a3 |
No files matched your search
+40
-13
@@ -1,15 +1,9 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['universe/node'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/__tests__/*'],
|
||||
},
|
||||
],
|
||||
globals: {
|
||||
jasmine: false,
|
||||
},
|
||||
plugins: ['async-protect', 'node'],
|
||||
rules: {
|
||||
'no-console': 'warn',
|
||||
'no-constant-condition': ['warn', { checkLoops: false }],
|
||||
'sort-imports': [
|
||||
'warn',
|
||||
@@ -17,11 +11,44 @@ module.exports = {
|
||||
ignoreDeclarationSort: true,
|
||||
},
|
||||
],
|
||||
'flowtype/no-types-missing-file-annotation': 'off',
|
||||
curly: 'warn',
|
||||
'import/no-cycle': 'error',
|
||||
'import/no-extraneous-dependencies': [
|
||||
'error',
|
||||
{ devDependencies: ['**/__tests__/**/*', '**/__mocks__/**/*'] },
|
||||
],
|
||||
'import/no-relative-packages': 'error',
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'lodash',
|
||||
message: "Don't use lodash, it's heavy!",
|
||||
},
|
||||
{
|
||||
name: 'node-fetch',
|
||||
message: 'Use got instead, node-fetch is used only for the GraphQL client.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'no-underscore-dangle': ['error', { allow: ['__typename'] }],
|
||||
'@typescript-eslint/explicit-function-return-type': [
|
||||
'warn',
|
||||
{
|
||||
allowExpressions: true,
|
||||
},
|
||||
],
|
||||
'async-protect/async-suffix': 'error',
|
||||
'node/no-sync': 'error',
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/__tests__/**/*.ts', '**/__mocks__/**/*.ts'],
|
||||
rules: {
|
||||
'async-protect/async-suffix': 'off',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
**/yarn.lock linguist-generated
|
||||
@@ -0,0 +1,58 @@
|
||||
name: "\U0001F41B Bug Report in EAS CLI"
|
||||
labels: 'bug'
|
||||
description: 'You want to report a reproducible bug or regression in EAS CLI.'
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "We're looking to keep questions on our forums and bug reports on the GitHub repo. For questions and help using EAS CLI, post in our [community forums](https://forums.expo.dev/c/help-expo-application-services) (log in with your Expo developer account)."
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Found a bug in the Expo SDK? Open a bug report [here](https://github.com/expo/expo/issues/new?assignees=&labels=needs+review&template=bug_report.yml) instead.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Thanks for taking the time to file a bug report! Please fill out this form as completely as possible.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: If you leave out sections there is a high likelihood your issue will be closed. If you have a question, not a bug report, please post it on our [forums](https://forums.expo.dev/) instead.
|
||||
- type: input
|
||||
attributes:
|
||||
label: Build/Submit details page URL
|
||||
description: If you're reporting an issue with EAS Build and/or EAS Submit, please provide the URL to the details page URL. The page can be only viewed by the Expo team members. Providing the URL is optional but it makes debugging the issue easier.
|
||||
placeholder: https://expo.dev/accounts/ACCOUNT_NAME/projects/PROJECT_NAME/builds/BUILD_ID
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Summary
|
||||
description: Describe the issue in 1 or 2 sentences
|
||||
placeholder: Clearly describe what the expected behavior is vs. what is actually happening. This should be as short as possible, while still communicating all the necessary information. If your summary is just 'X library/method isn't working', then you need to continue debugging yourself and provide more information.
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Managed or bare?
|
||||
description: Is your project managed or bare? Bare - Android and/or iOS native projects exist in your repo; Managed - you don't have native code (custom or 3rd party) in your project. See the [Workflows](https://docs.expo.dev/introduction/managed-vs-bare/) page for more information.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Environment
|
||||
description: Run `eas diagnostics` and paste the output here.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: false
|
||||
attributes:
|
||||
label: Error output
|
||||
placeholder: If you're running into an error message, please run the same command with `EXPO_DEBUG=true <your command here>` and paste output here.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Reproducible demo or steps to reproduce from a blank project
|
||||
description: 'This should include as little code as possible, do not simply link your entire project. Sharing a link to a [Snack](https://snack.expo.dev/) is a GREAT way to provide a reproducible demo :) If a reproducible demo, or a complete list of steps from blank project to bug, are not provided, it is very likely your issue will be closed. Read [here more guidance](https://stackoverflow.com/help/mcve).'
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Please make sure contributors can follow the steps your provided in order to reproduce the bug.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "**Realize that it is up to you to debug your code and be as certain as possible that the bug is with EAS CLI, not with your own app.** [Here's an excellent guide to debugging you can follow](https://github.com/expo/fyi/blob/master/manual-debugging.md#how-to-narrow-down-the-source-of-an-error-a-step-by-step-guide-to-good-old-fashioned-manual-debugging)."
|
||||
@@ -0,0 +1,14 @@
|
||||
name: "\U0001F64B Enhancement"
|
||||
labels: 'enhancement'
|
||||
description: 'Want us to add something to EAS CLI?'
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Thanks for helping us make EAS even better! 😀 Unfortunately, this is not the right place for feature requests. Please find and upvote your feature (or create a new one) at https://expo.canny.io instead.
|
||||
- type: dropdown
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Do you understand that any feature requests opened in this repository will be closed?
|
||||
options:
|
||||
- 'Yes'
|
||||
@@ -1,3 +1,9 @@
|
||||
<!-- Thanks for contributing to _EAS CLI_! Before you submit your pull request, please make sure to check the following boxes by putting an x in the [ ] (don't: [x ], [ x], do: [x]) -->
|
||||
|
||||
# Checklist
|
||||
|
||||
- [ ] I've added an entry to [CHANGELOG.md](https://github.com/expo/eas-cli/blob/main/CHANGELOG.md#main) if necessary. You can comment this pull request with `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]` and CHANGELOG.md will be updated automatically.
|
||||
|
||||
# Why
|
||||
|
||||
Please describe the motivation for this PR, and link to relevant GitHub issues, forums posts, or feature requests.
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
name: Changelog
|
||||
|
||||
jobs:
|
||||
pr-commented:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.issue.pull_request && github.event.issue.user.login == github.event.comment.user.login && startsWith(github.event.comment.body, '/changelog-entry ') }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Checkout Pull Request
|
||||
run: hub pr checkout ${{ github.event.issue.number }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Install dependencies
|
||||
working-directory: ./scripts
|
||||
run: yarn install --frozen-lockfile --check-files
|
||||
- name: Add changelog entry
|
||||
working-directory: ./scripts
|
||||
run: |
|
||||
PARAMS=${PR_COMMENT_BODY#/changelog-entry }
|
||||
if [[ ! "$PARAMS" =~ " " ]]; then
|
||||
echo "Invalid /changelog-entry command"
|
||||
exit 1
|
||||
fi
|
||||
CATEGORY=$( cut -d ' ' -f 1 <<< "$PARAMS" | sed -e 's/^[[:space:]]*//' )
|
||||
MESSAGE=$( cut -d ' ' -f 2- <<< "$PARAMS" | sed -e 's/^[[:space:]]*//' )
|
||||
yarn changelog-entry "$CATEGORY" "$MESSAGE"
|
||||
env:
|
||||
GITHUB_PR_AUTHOR: ${{ github.event.issue.user.login }}
|
||||
GITHUB_PR_NUMBER: ${{ github.event.issue.number }}
|
||||
PR_COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
- name: Commit and push
|
||||
run: |
|
||||
# use the last commit author name and email
|
||||
git config --global user.name "$(git --no-pager log --format=format:'%an' -n 1)"
|
||||
git config --global user.email "$(git --no-pager log --format=format:'%ae' -n 1)"
|
||||
|
||||
git add CHANGELOG.md
|
||||
git commit -m "update CHANGELOG.md"
|
||||
git push
|
||||
@@ -15,4 +15,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: bash bin/install.sh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- run: eas --version
|
||||
+103
-21
@@ -15,7 +15,7 @@ jobs:
|
||||
- id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
draft: true
|
||||
@@ -25,22 +25,30 @@ jobs:
|
||||
needs: release
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
node-version: '16'
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: yarn build
|
||||
- name: Build tarballs
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli
|
||||
run: yarn oclif-dev pack --targets linux-x64,linux-arm
|
||||
run: |
|
||||
yarn pretarball-ci
|
||||
yarn oclif pack:tarballs --no-xz --targets linux-x64,linux-arm
|
||||
- id: x64
|
||||
run: echo "::set-output name=filename::$(ls eas-*-x64.tar.gz)"
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
|
||||
- id: arm
|
||||
run: echo "::set-output name=filename::$(ls eas-*-arm.tar.gz)"
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
|
||||
- name: Upload linux-x64 tarball
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||
asset_path: packages/eas-cli/dist/eas-linux-x64.tar.gz
|
||||
asset_name: eas-linux-x64.tar.gz
|
||||
asset_path: packages/eas-cli/dist/${{ steps.x64.outputs.filename }}
|
||||
asset_name: ${{ steps.x64.outputs.filename }}
|
||||
asset_content_type: application/gzip
|
||||
- name: Upload linux-arm tarball
|
||||
uses: actions/upload-release-asset@v1
|
||||
@@ -48,8 +56,8 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||
asset_path: packages/eas-cli/dist/eas-linux-arm.tar.gz
|
||||
asset_name: eas-linux-arm.tar.gz
|
||||
asset_path: packages/eas-cli/dist/${{ steps.arm.outputs.filename }}
|
||||
asset_name: ${{ steps.arm.outputs.filename }}
|
||||
asset_content_type: application/gzip
|
||||
build-mac:
|
||||
name: Build for macOS
|
||||
@@ -58,22 +66,27 @@ jobs:
|
||||
needs: release
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
node-version: '16'
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: yarn build
|
||||
- name: Build tarballs
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli
|
||||
run: yarn oclif-dev pack --targets darwin-x64
|
||||
run: |
|
||||
yarn pretarball-ci
|
||||
yarn oclif pack:tarballs --no-xz --targets darwin-x64
|
||||
- id: x64
|
||||
run: echo "::set-output name=filename::$(ls eas-*-x64.tar.gz)"
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
|
||||
- name: Upload darwin-x64 tarball
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||
asset_path: packages/eas-cli/dist/eas-darwin-x64.tar.gz
|
||||
asset_name: eas-darwin-x64.tar.gz
|
||||
asset_path: packages/eas-cli/dist/${{ steps.x64.outputs.filename }}
|
||||
asset_name: ${{ steps.x64.outputs.filename }}
|
||||
asset_content_type: application/gzip
|
||||
build-windows:
|
||||
name: Build for Windows
|
||||
@@ -81,27 +94,29 @@ jobs:
|
||||
needs: release
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
node-version: '16'
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: sudo apt-get install nsis
|
||||
- run: yarn build
|
||||
- run: yarn oclif-dev pack:win
|
||||
- run: |
|
||||
yarn pretarball-ci
|
||||
yarn oclif pack:win
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli
|
||||
- id: x64
|
||||
run: echo "::set-output name=filename::$(ls eas-*-x64.exe)"
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win32
|
||||
- id: x86
|
||||
run: echo "::set-output name=filename::$(ls eas-*-x86.exe)"
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win32
|
||||
- name: Upload x64 installer
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||
asset_path: packages/eas-cli/dist/win/${{ steps.x64.outputs.filename }}
|
||||
asset_path: packages/eas-cli/dist/win32/${{ steps.x64.outputs.filename }}
|
||||
asset_name: ${{ steps.x64.outputs.filename }}
|
||||
asset_content_type: application/gzip
|
||||
- name: Upload x86 installer
|
||||
@@ -110,6 +125,73 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||
asset_path: packages/eas-cli/dist/win/${{ steps.x86.outputs.filename }}
|
||||
asset_path: packages/eas-cli/dist/win32/${{ steps.x86.outputs.filename }}
|
||||
asset_name: ${{ steps.x86.outputs.filename }}
|
||||
asset_content_type: application/gzip
|
||||
asset_content_type: application/gzip
|
||||
publish-to-npm:
|
||||
name: Publish to npm
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-linux, build-mac, build-windows]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org/'
|
||||
scope: 'expo'
|
||||
node-version: '16'
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile --check-files
|
||||
- name: Build
|
||||
run: yarn build
|
||||
- name: Publish packages to npm
|
||||
run: yarn lerna publish from-package --yes --no-verify-access
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
release-changelog:
|
||||
name: Update changelog and publish release
|
||||
runs-on: ubuntu-latest
|
||||
needs: publish-to-npm
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Install dependencies
|
||||
working-directory: ./scripts
|
||||
run: yarn install --frozen-lockfile --check-files
|
||||
- name: Resolve version
|
||||
run: echo "EAS_CLI_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||
- name: Update changelog
|
||||
working-directory: ./scripts
|
||||
run: yarn --silent release-changelog $EAS_CLI_VERSION > /tmp/current-changelog
|
||||
- name: Publish release
|
||||
run: hub release edit v$EAS_CLI_VERSION --draft=false -F /tmp/current-changelog
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Commit and push
|
||||
run: |
|
||||
git fetch
|
||||
git checkout main
|
||||
git config --global user.email "[email protected]"
|
||||
git config --global user.name "Expo CI"
|
||||
git add CHANGELOG.md
|
||||
git commit -m "update CHANGELOG.md"
|
||||
git push
|
||||
- name: Prepare changelog for Slack
|
||||
run: |
|
||||
tail -n +3 /tmp/current-changelog > /tmp/changelog.md
|
||||
npx slackmd /tmp/changelog.md
|
||||
echo 'SLACK_CHANGELOG<<EOF' >> $GITHUB_ENV
|
||||
cat /tmp/changelog.slack.md >> $GITHUB_ENV
|
||||
echo 'EOF' >> $GITHUB_ENV
|
||||
- name: Slack Notification
|
||||
uses: rtCamp/action-slack-notify@v2
|
||||
env:
|
||||
SLACK_CHANNEL: eas-cli
|
||||
SLACK_COLOR: ${{ job.status }}
|
||||
SLACK_ICON: https://github.com/expo-ci.png?size=48
|
||||
SLACK_TITLE: 'Released eas-cli@${{ env.EAS_CLI_VERSION }}'
|
||||
SLACK_MESSAGE: ${{ env.SLACK_CHANGELOG }}
|
||||
SLACK_USERNAME: 'Expo CI'
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||
SLACK_FOOTER: ''
|
||||
@@ -7,12 +7,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
node-version: 16
|
||||
- uses: preactjs/compressed-size-action@v2
|
||||
with:
|
||||
build-script: 'build:tarballs:linux'
|
||||
repo-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
pattern: './packages/eas-cli/dist/*.tar.gz'
|
||||
compression: 'none'
|
||||
strip-hash: "v\\d+.\\d+.\\d+-\\w+-"
|
||||
clean-script: 'clean'
|
||||
@@ -10,19 +10,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: ['12', '14']
|
||||
node: ['12', '14', '16']
|
||||
include:
|
||||
- node: '14'
|
||||
- node: '16'
|
||||
coverage: true
|
||||
name: Test with Node ${{ matrix.node }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: yarn lerna run build
|
||||
- run: yarn typecheck
|
||||
- run: yarn test
|
||||
if: ${{ !matrix.coverage }}
|
||||
- run: yarn test --coverage
|
||||
|
||||
+15
-9
@@ -1,13 +1,19 @@
|
||||
# Build directories
|
||||
packages/*/build
|
||||
packages/eas-cli/dist
|
||||
packages/eas-cli/tmp
|
||||
|
||||
# Code editors
|
||||
.idea
|
||||
.history
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Node
|
||||
node_modules
|
||||
*-debug.log
|
||||
*-error.log
|
||||
|
||||
**/build/**
|
||||
!packages/eas-cli/src/commands/build/**
|
||||
!packages/eas-cli/src/build/**
|
||||
|
||||
dist
|
||||
/package-lock.json
|
||||
tmp
|
||||
node_modules
|
||||
# Tests
|
||||
coverage
|
||||
.history
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"jsxBracketSameLine": true,
|
||||
"bracketSameLine": true,
|
||||
"trailingComma": "es5",
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
+957
@@ -0,0 +1,957 @@
|
||||
# Changelog
|
||||
|
||||
This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
## main
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- No longer enable APNS (iOS Push Notifications) capability by default anymore. ([#797](https://github.com/expo/eas-cli/pull/797) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Don't configure Android projects locally. Clean up old Gradle signing config. Streamline `expo-updates` configuration. ([#888](https://github.com/expo/eas-cli/pull/888) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add `update:list` command. ([#884](https://github.com/expo/eas-cli/pull/884) by [@jkhales](https://github.com/jkhales))
|
||||
- Improve error message for outdated Apple PLA error. ([#889](https://github.com/expo/eas-cli/pull/889) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Add `--output` flag to the build command. ([#885](https://github.com/expo/eas-cli/pull/885) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix creating project archive with symlink cycle. ([#891](https://github.com/expo/eas-cli/pull/891) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Sign Android debug builds with the correct keystore. Previously, all debug builds would be signed with a default debug keystore. ([#888](https://github.com/expo/eas-cli/pull/888) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [0.43.0](https://github.com/expo/eas-cli/releases/tag/v0.43.0) - 2022-01-03
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add `eas channel:delete`. ([#846](https://github.com/expo/eas-cli/pull/846), [#869](https://github.com/expo/eas-cli/pull/869) by [@jkhales](https://github.com/jkhales))
|
||||
- Add `eas autocomplete`. ([#870](https://github.com/expo/eas-cli/pull/870) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix saving `build:inspect` results to project subdirectory. ([#863](https://github.com/expo/eas-cli/pull/863) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.42.4](https://github.com/expo/eas-cli/releases/tag/v0.42.4) - 2021-12-21
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Add missing dependency - `@oclif/plugin-help`. ([#865](https://github.com/expo/eas-cli/pull/865) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.42.3](https://github.com/expo/eas-cli/releases/tag/v0.42.3) - 2021-12-21
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Allow select eas commands to be run outside of a project. ([#851](https://github.com/expo/eas-cli/pull/851) by [@jkhales](https://github.com/jkhales))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Replace all oclif dependencies with `@oclif/core`. ([#858](https://github.com/expo/eas-cli/pull/858) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.41.1](https://github.com/expo/eas-cli/releases/tag/v0.41.1) - 2021-12-16
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Adds commands for EAS Update, which is now in preview for subscribers. EAS Update makes fixing small bugs and pushing quick fixes a snap in between app store submissions. It accomplishes this by allowing an end-user's app to swap out the non-native parts of their app (for example, JS, styling, and image changes) with a new update that contains bug fixes and other updates.
|
||||
- Adds `eas update`, which can bundle and publish updates on a branch.
|
||||
- Adds `eas branch`, which manages branches. Branches contain a list of updates and are linked to channels.
|
||||
- Adds `eas channel`, which manages channels. Channels are specified inside builds and are linked to branches, allowing us to link specific updates with specific builds.
|
||||
- Read more in our [feature preview docs](https://docs.expo.dev/eas-update/introduction/).
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix `eas submit` displaying a prompt in non-interactive mode when some ASC API credentials are missing in `eas.json`. ([#841](https://github.com/expo/eas-cli/pull/841) by [@barthap](https://github.com/barthap))
|
||||
|
||||
## [0.41.0](https://github.com/expo/eas-cli/releases/tag/v0.41.0) - 2021-12-13
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Ask user to select profile if `release` does not exist. ([#829](https://github.com/expo/eas-cli/pull/829) by [@dsokal](https://github.com/dsokal))
|
||||
- Add `build:inspect` command. ([#834](https://github.com/expo/eas-cli/pull/834) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix validating tool versions in eas.json. ([#832](https://github.com/expo/eas-cli/pull/832) by [@dsokal](https://github.com/dsokal))
|
||||
- Fix submission URL. ([#845](https://github.com/expo/eas-cli/pull/845) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Warn when building managed project with SDK < 41. ([#833](https://github.com/expo/eas-cli/pull/833) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.40.0](https://github.com/expo/eas-cli/releases/tag/v0.40.0) - 2021-12-08
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- [eas-cli] change eas update:publish to eas update. ([#830](https://github.com/expo/eas-cli/pull/830) by [@jkhales](https://github.com/jkhales))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Pass build profile environment to credentials command. ([#828](https://github.com/expo/eas-cli/pull/828) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.39.0](https://github.com/expo/eas-cli/releases/tag/v0.39.0) - 2021-12-06
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Set runtime version in `eas update:configure`. ([#811](https://github.com/expo/eas-cli/pull/811) by [@jkhales](https://github.com/jkhales))
|
||||
- Print project workflow in `eas diagnostics`. ([#822](https://github.com/expo/eas-cli/pull/822) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix submit for multi-target projects where ascAppId is not specified. ([#809](https://github.com/expo/eas-cli/pull/809) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Fix building iOS projects with Apple Watch companion app. ([#812](https://github.com/expo/eas-cli/pull/812), [#817](https://github.com/expo/eas-cli/pull/817), [#821](https://github.com/expo/eas-cli/pull/821) by [@jkhales](https://github.com/jkhales) + [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Improve error message for missing ascAppId. ([#813](https://github.com/expo/eas-cli/pull/813) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Add `react-native` version to metadata. ([#823](https://github.com/expo/eas-cli/pull/823) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.38.3](https://github.com/expo/eas-cli/releases/tag/v0.38.3) - 2021-11-29
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Print warning about outdated version to stderr. ([#805](https://github.com/expo/eas-cli/pull/805) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Fix Apple sign in capabilities. ([#806](https://github.com/expo/eas-cli/pull/806) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [0.38.2](https://github.com/expo/eas-cli/releases/tag/v0.38.2) - 2021-11-26
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add support for extending submit profiles. ([#794](https://github.com/expo/eas-cli/pull/794) by [@dsokal](https://github.com/dsokal))
|
||||
- Add command: `eas update:configure`. ([#800](https://github.com/expo/eas-cli/pull/800) by [@jkhales](https://github.com/jkhales))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix reading app version and app build version of iOS projects. ([#798](https://github.com/expo/eas-cli/pull/798) by [@dsokal](https://github.com/dsokal))
|
||||
- Fix push notifications entitlements. ([#801](https://github.com/expo/eas-cli/pull/801) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [0.38.1](https://github.com/expo/eas-cli/releases/tag/v0.38.1) - 2021-11-25
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix building iOS projects with whitespace in the project name. ([#792](https://github.com/expo/eas-cli/pull/792) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.38.0](https://github.com/expo/eas-cli/releases/tag/v0.38.0) - 2021-11-24
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Improve unknown capability syncing error message. ([#775](https://github.com/expo/eas-cli/pulls/775) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Add submit webhooks. ([#777](https://github.com/expo/eas-cli/pull/777) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Compute runtime version policies. ([#783](https://github.com/expo/eas-cli/pull/783), [#785](https://github.com/expo/eas-cli/pull/785) by [@jkhales](https://github.com/jkhales))
|
||||
- Fix local builds with npm < 7. ([#787](https://github.com/expo/eas-cli/pull/787) by [@dsokal](https://github.com/dsokal))
|
||||
- Override `applicationId`/`bundleIdentifier` when auto-submitting after build. ([#780](https://github.com/expo/eas-cli/pull/780) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Upgrade `typescript` to 4.5.2. Upgrade oclif dependencies. ([#781](https://github.com/expo/eas-cli/pull/781) by [@dsokal](https://github.com/dsokal))
|
||||
- Make missing `git` command error message more descriptive. ([#784](https://github.com/expo/eas-cli/pull/784) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.37.0](https://github.com/expo/eas-cli/releases/tag/v0.37.0) - 2021-11-18
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Require explicitly defined applicationId/bundleIdentifier for EAS Submit in case of bare projects consisting of multiple product flavors on Android or multiple schemes/targets on iOS. ([#765](https://github.com/expo/eas-cli/pull/765) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Auto create channel on publish. ([#766](https://github.com/expo/eas-cli/pull/766) by [@jkhales](https://github.com/jkhales))
|
||||
- Interactively configure Git `user.name` and `user.email`. ([#772](https://github.com/expo/eas-cli/pull/772) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Validate release channel in eas.json. ([#764](https://github.com/expo/eas-cli/pull/764) by [@dsokal](https://github.com/dsokal))
|
||||
- Make the missing profile error message more descriptive. ([#761](https://github.com/expo/eas-cli/pull/761) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Better error message when missing eas.json. ([#770](https://github.com/expo/eas-cli/pull/770) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.36.1](https://github.com/expo/eas-cli/releases/tag/v0.36.1) - 2021-11-15
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix `dateformat` import issue when building with `--local` flag. ([a520d](https://github.com/expo/eas-cli/commit/a520d1750736d61d6489da6487879f35ed3deb23) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.36.0](https://github.com/expo/eas-cli/releases/tag/v0.36.0) - 2021-11-15
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Do not require manual `eas-cli-local-build-plugin` installation. An existing global installation (either with `yarn global add` or `npm install -g`) will not be used anymore. ([#753](https://github.com/expo/eas-cli/pull/753) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Use Keystore Service when `keytool` is not installed. ([#754](https://github.com/expo/eas-cli/pull/754) by [@dsokal](https://github.com/dsokal))
|
||||
- Add applicationId/bundleIdentifier fields to the submit profile. ([#765](https://github.com/expo/eas-cli/pull/765) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix auto-submitting with `--auto-submit-with-profile`. ([#748](https://github.com/expo/eas-cli/pull/748) by [@dsokal](https://github.com/dsokal))
|
||||
- Pass env from build profile when resolving entitlements. ([#751](https://github.com/expo/eas-cli/pull/751) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Only prompt for Apple Id username if authenticating with an App Specific Password. ([#745](https://github.com/expo/eas-cli/pull/745) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Use same URL paths for local development of EAS CLI as production. ([#750](https://github.com/expo/eas-cli/pull/750) by [@ide](https://github.com/ide))
|
||||
- Grammar: Api -> API. ([#755](https://github.com/expo/eas-cli/pull/755) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add analytics for EAS Submit. ([#752](https://github.com/expo/eas-cli/pull/752) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add an 'attempt' event to build and submit analytics. ([#757](https://github.com/expo/eas-cli/pull/757) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.35.0](https://github.com/expo/eas-cli/releases/tag/v0.35.0) - 2021-11-08
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add ASC API Key generation workflow. ([#718](https://github.com/expo/eas-cli/pull/718) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add support for removal of ASC API Keys. ([#721](https://github.com/expo/eas-cli/pull/721) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Allow users to assign an ASC API Key to their project. ([#719](https://github.com/expo/eas-cli/pull/719) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add setup support for ASC API Keys. ([#733](https://github.com/expo/eas-cli/pull/733) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Show initiating user display name when selecting a build to submit. ([#730](https://github.com/expo/eas-cli/pull/730) by [@barthap](https://github.com/barthap))
|
||||
- Handle Apple servers maintenance error in `eas submit`. ([#738](https://github.com/expo/eas-cli/pull/738) by [@barthap](https://github.com/barthap))
|
||||
- Integrate ASC API Key with submissions workflow. ([#737](https://github.com/expo/eas-cli/pull/737) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Change EAS API server domain. ([#744](https://github.com/expo/eas-cli/pull/744) by [@ide](https://github.com/ide))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Clean up credentials prompt method. ([#728](https://github.com/expo/eas-cli/pull/728) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Bump `@expo/apple-utils` to 0.0.0-alpha.26. ([#723](https://github.com/expo/eas-cli/pull/723) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Grammar: replace setup with set up. ([#735](https://github.com/expo/eas-cli/pull/735) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Improve VCS workflow migration experience. ([#732](https://github.com/expo/eas-cli/pull/732) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.34.1](https://github.com/expo/eas-cli/releases/tag/v0.34.1) - 2021-11-02
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Don't show commit prompt in no-commit workflow after installing `expo-dev-client`. ([#722](https://github.com/expo/eas-cli/pull/722) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Add App Store Connect API Key graphql type and print support. ([#717](https://github.com/expo/eas-cli/pull/717) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.34.0](https://github.com/expo/eas-cli/releases/tag/v0.34.0) - 2021-11-01
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Use new build job format. ([#701](https://github.com/expo/eas-cli/pull/701) and [#711](https://github.com/expo/eas-cli/pull/711) by [@dsokal](https://github.com/dsokal))
|
||||
- Don't print logs to `stderr`. ([#708](https://github.com/expo/eas-cli/pull/708) by [@dsokal](https://github.com/dsokal))
|
||||
- Remove automatic migration for legacy `eas.json` format introduced in [v0.22.2](https://github.com/expo/eas-cli/releases/tag/v0.22.2). ([#695](https://github.com/expo/eas-cli/pull/695) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Implement no-commit build workflow. Add required `cli` field in root of `eas.json`. ([#695](https://github.com/expo/eas-cli/pull/695) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Better error message when eas.json is invalid. ([#707](https://github.com/expo/eas-cli/pull/707) by [@dsokal](https://github.com/dsokal))
|
||||
- Fix credentials workflow for new users: add additionalTypenames for credentials. ([#703](https://github.com/expo/eas-cli/pull/703) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Add additionalTypenames for other queries. ([#704](https://github.com/expo/eas-cli/pull/704) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Fix errors after pulling down most recent graphql schema changes. ([#713](https://github.com/expo/eas-cli/pull/713) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Refactor credentials manager. ([#712](https://github.com/expo/eas-cli/pull/712) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Remove unneeded parameters. ([#710](https://github.com/expo/eas-cli/pull/710) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Enforce additionalTypenames for graphql queries. ([#709](https://github.com/expo/eas-cli/pull/709) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.33.1](https://github.com/expo/eas-cli/releases/tag/v0.33.1) - 2021-10-22
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Compute runtime version using config-plugins. ([#697](https://github.com/expo/eas-cli/pull/697) by [@jkhales](https://github.com/jkhales))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- [eas-cli] Skip validating updates scripts integration, which no longer exist in SDK 43+ ([#706](https://github.com/expo/eas-cli/pull/706) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [0.33.0](https://github.com/expo/eas-cli/releases/tag/v0.33.0) - 2021-10-20
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Make "production" the default profile for building and submitting. ([#677](https://github.com/expo/eas-cli/pull/677)) by [@jonsamp](https://github.com/jonsamp)
|
||||
- Previously, the default profile when running `eas build` or `eas submit` was "release". We're changing it to a more recognizable name that is consistent with our docs, which is now "production". You can always specify a profile manually with the `--profile` flag. For this major version, if you do not have a profile named "production", and still have a profile named "release", we will fall back to the "release" profile as the default, however you'll see a warning as we're going to remove that behavior in the next major release of EAS CLI.
|
||||
- To upgrade, update **eas.json** to have a "production" profile in both the `build` and `submit` objects. If you already have a project set up, this will replace the existing "release" profile. After the change, eas.json should have the following profiles:
|
||||
```json
|
||||
{
|
||||
"build": {
|
||||
"production": { ... }
|
||||
},
|
||||
"submit": {
|
||||
"production": { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Skip the second prompt for Apple ID if the user is already signed in with Apple. ([#691](https://github.com/expo/eas-cli/pull/691) by [@dsokal](https://github.com/dsokal))
|
||||
- Unify reading `app.json`. ([#692](https://github.com/expo/eas-cli/pull/692) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.32.0](https://github.com/expo/eas-cli/releases/tag/v0.32.0) - 2021-10-15
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add App Store Connect API Key support to iOS submissions. ([#678](https://github.com/expo/eas-cli/pull/678) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Create/list/revoke App Store Connect Api keys via Apple apis. ([#687](https://github.com/expo/eas-cli/pull/687) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add ability to select a build from a list in `eas submit` interactive mode. ([#688](https://github.com/expo/eas-cli/pull/688) by [@barthap](https://github.com/barthap))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix printing App Store Connect URL after submission. ([#683](https://github.com/expo/eas-cli/pull/683) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Add App Store Connect API Key fields to `eas.json`. ([#684](https://github.com/expo/eas-cli/pull/684) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Enable no-underscore-dangle eslint rule. ([#686](https://github.com/expo/eas-cli/pull/686) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.31.1](https://github.com/expo/eas-cli/releases/tag/v0.31.1) - 2021-10-08
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Google Service Account Keys: Fix non-interactive bug in credentials service workflow ([#682](https://github.com/expo/eas-cli/pull/682) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.31.0](https://github.com/expo/eas-cli/releases/tag/v0.31.0) - 2021-10-08
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Don't resolve the iOS builder image on the client side. EAS Build will use the appropriate iOS image for a given Expo SDK version unless the `image` is defined. This only applies to managed projects. ([#675](https://github.com/expo/eas-cli/pull/675) by [@wkozyra95](https://github.com/wkozyra95) + [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Integrate credentials service with Android submissions. ([#664](https://github.com/expo/eas-cli/pull/664) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add option to review ad-hoc devices when reusing provisioning profile. ([#673](https://github.com/expo/eas-cli/pull/673) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.30.1](https://github.com/expo/eas-cli/releases/tag/v0.30.1) - 2021-10-06
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix `--json` flag when running EAS CLI on GitHub actions. ([#669](https://github.com/expo/eas-cli/pull/669) by [@dsokal](https://github.com/dsokal))
|
||||
- Fix `"ios: mods.ios.infoPlist: Failed to find Info.plist linked to Xcode project."` warning when running `eas build` in a managed project.([#670](https://github.com/expo/eas-cli/pull/670) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Fix building monorepo projects on Windows. ([#671](https://github.com/expo/eas-cli/pull/671) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.30.0](https://github.com/expo/eas-cli/releases/tag/v0.30.0) - 2021-10-05
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Enable auto-incrementing on Android. ([#645](https://github.com/expo/eas-cli/pull/645) by [@jkhales](https://github.com/jkhales))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Ports detection for Google Service Account Keys into the credentials service. ([#660](https://github.com/expo/eas-cli/pull/660) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Improve iOS credentials printing. ([#657](https://github.com/expo/eas-cli/pull/657) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Automate `eas-cli` releases. ([#668](https://github.com/expo/eas-cli/pull/668) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.29.1](https://github.com/expo/eas-cli/releases/tag/v0.29.1) - 2021-09-29
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- More upload support for Google Service Account Keys. ([#649](https://github.com/expo/eas-cli/pull/649) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Allow the user to assign an existing Google Service Account Key to their project. ([#650](https://github.com/expo/eas-cli/pull/650) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Allow the user to remove a Google Service Account Key from their account. ([#658](https://github.com/expo/eas-cli/pull/658) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Adds setup support for Google Service Account Keys. ([#659](https://github.com/expo/eas-cli/pull/659) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix double prompt for Apple credentials when running `eas build --auto-submit`. ([#654](https://github.com/expo/eas-cli/pull/654) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Always use async `fs` functions. ([#652](https://github.com/expo/eas-cli/pull/652) by [@dsokal](https://github.com/dsokal))
|
||||
- Improve Android credentials printing. ([#656](https://github.com/expo/eas-cli/pull/656) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.29.0](https://github.com/expo/eas-cli/releases/tag/v0.29.0) - 2021-09-28
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Added upload support for Google Service Account Keys. ([#642](https://github.com/expo/eas-cli/pull/642) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add Xcode 13 image. ([#651](https://github.com/expo/eas-cli/pull/651) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Credentials manager: stop prompting for an Android build profile every iteration. ([#641](https://github.com/expo/eas-cli/pull/641) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Don't display prompt in non-interactive mode when the metro config seems invalid. ([#644](https://github.com/expo/eas-cli/pull/644) by [@dsokal](https://github.com/dsokal))
|
||||
- Read versions from correct `Info.plist`. ([#635](https://github.com/expo/eas-cli/pull/635) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Move away from weird convention of exports. ([#647](https://github.com/expo/eas-cli/pull/647) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.28.2](https://github.com/expo/eas-cli/releases/tag/v0.28.2) - 2021-09-23
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Use `log-symbols` to unify green ticks style. ([#639](https://github.com/expo/eas-cli/pull/639) by [@dsokal](https://github.com/dsokal))
|
||||
- Fix `eas build --auto-submit` in fresh projects. ([#640](https://github.com/expo/eas-cli/pull/640) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.28.1](https://github.com/expo/eas-cli/releases/tag/v0.28.1) - 2021-09-22
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Prevent ora spinners breaking when debug logs are enabled. ([#575](https://github.com/expo/eas-cli/pull/575) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Do not fail `eas build` when transient network errors occur. ([#638](https://github.com/expo/eas-cli/pull/638) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Add GraphQL support for GoogleServiceAccountKey. ([#632](https://github.com/expo/eas-cli/pull/632) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Remove GraphQL support for the App Specific Password. ([#633](https://github.com/expo/eas-cli/pull/633) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.28.0](https://github.com/expo/eas-cli/releases/tag/v0.28.0) - 2021-09-20
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Use envs from build profile to resolve app config when auto-submitting. ([#614](https://github.com/expo/eas-cli/pull/614) by [@dsokal](https://github.com/dsokal))
|
||||
- Support multi flavor Android projects. ([#595](https://github.com/expo/eas-cli/pull/595) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Improve experience when using the build details page as a build artifact URL in `eas submit`. ([#620](https://github.com/expo/eas-cli/pull/620) by [@dsokal](https://github.com/dsokal))
|
||||
- Better error message when eas.json is invalid JSON. ([#618](https://github.com/expo/eas-cli/pull/618) by [@dsokal](https://github.com/dsokal))
|
||||
- Add warning about the legacy build service IDs in `eas submit`. ([#624](https://github.com/expo/eas-cli/pull/624) by [@dsokal](https://github.com/dsokal))
|
||||
- Validate that project includes `expo-dev-client` when building with `developmentClient` flag. ([#627](https://github.com/expo/eas-cli/pull/627) by [@dsokal](https://github.com/dsokal))
|
||||
- Better experience when not logged in in non-interactive mode. ([#628](https://github.com/expo/eas-cli/pull/628) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Reduce `eas-cli` size by almost getting rid of `lodash` from dependencies. ([#605](https://github.com/expo/eas-cli/pull/605) by [@dsokal](https://github.com/dsokal))
|
||||
- Enforce explicit return type in all functions. ([#619](https://github.com/expo/eas-cli/pull/619) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Enforce `Async` suffix for async functions. ([#623](https://github.com/expo/eas-cli/pull/623) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.27.1](https://github.com/expo/eas-cli/releases/tag/v0.27.1) - 2021-09-10
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fixed force-creation, app secret creation, and secret deletion mutations. ([#606](https://github.com/expo/eas-cli/pull/606) by [@fiberjw](https://github.com/fiberjw))
|
||||
|
||||
## [0.27.0](https://github.com/expo/eas-cli/releases/tag/v0.27.0) - 2021-09-10
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Implement support for `--platform all` in `eas submit`. ([#598](https://github.com/expo/eas-cli/pull/598) by [@dsokal](https://github.com/dsokal))
|
||||
- Implement support for `--non-interactive` in `eas submit`. ([#600](https://github.com/expo/eas-cli/pull/600) by [@dsokal](https://github.com/dsokal))
|
||||
- Add auto-submit feature. Run `eas build --auto-submit` to submit automatically on build complete. ([#603](https://github.com/expo/eas-cli/pull/603) by [@dsokal](https://github.com/dsokal))
|
||||
- Allow using env var for `android.serviceAccountKeyPath` in submit profiles. ([#604](https://github.com/expo/eas-cli/pull/604) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix detecting `googleServicesFile` on Windows. ([#602](https://github.com/expo/eas-cli/pull/602) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.26.0](https://github.com/expo/eas-cli/releases/tag/v0.26.0) - 2021-09-08
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Require `eas submit` to be configured with eas.json submit profiles (see [https://docs.expo.dev/submit/eas-json/](https://docs.expo.dev/submit/eas-json/) for details). Drop support for CLI params. ([#590](https://github.com/expo/eas-cli/pull/590) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add `eas diagnostics` command. ([#594](https://github.com/expo/eas-cli/pull/594) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix `eas build` from throwing an exception when detecting the git `core.ignorecase` setting. ([#592](https://github.com/expo/eas-cli/pull/592) by [@mwillbanks](https://github.com/mwillbanks))
|
||||
- Support app names that consist only of unicode characters. ([#596](https://github.com/expo/eas-cli/pull/596) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.25.0](https://github.com/expo/eas-cli/releases/tag/v0.25.0) - 2021-09-01
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Drop support for node < 12. ([#581](https://github.com/expo/eas-cli/pull/581) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Support `--no-wait` in `eas submit`. ([#578](https://github.com/expo/eas-cli/pull/578) by [@dsokal](https://github.com/dsokal))
|
||||
- Detect changes when `core.ignorecase` is set to true. ([#570](https://github.com/expo/eas-cli/pull/570) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix wrong warning for `metro.config.js` check on Windows. ([#587](https://github.com/expo/eas-cli/pull/588) by [@louisgv](https://github.com/louisgv))
|
||||
- Fix detecting `googleServicesFile` with `EAS_NO_VCS=1`. ([#583](https://github.com/expo/eas-cli/pull/583) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Replace `@hapi/joi` with `joi`. Upgrade typescript to 4.4.2. Upgrade dependencies. ([#582](https://github.com/expo/eas-cli/pull/582) by [@dsokal](https://github.com/dsokal))
|
||||
- Change typescript target from `ES2017` to `ES2019`. ([#584](https://github.com/expo/eas-cli/pull/584) by [@dsokal](https://github.com/dsokal))
|
||||
- Use `ts-jest` instead of `babel-jest`. ([#585](https://github.com/expo/eas-cli/pull/585) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.24.1](https://github.com/expo/eas-cli/releases/tag/v0.24.1) - 2021-08-25
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix wrong `EasJsonReader` import causing `eas submit` to fail. ([951ee](https://github.com/expo/eas-cli/commit/951eea2396591bd612aa7cb3038da35e45c33bb8) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.24.0](https://github.com/expo/eas-cli/releases/tag/v0.24.0) - 2021-08-25
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Allow publishing of multiple update groups. ([#566](https://github.com/expo/eas-cli/pull/566) by [@jkhales](https://github.com/jkhales))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Retry status code 500-600 server errors from Apple servers with cookies auth. ([#574](https://github.com/expo/eas-cli/pull/574) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Throw internal server errors when Apple servers fail to authenticate. ([#574](https://github.com/expo/eas-cli/pull/574) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Add --auto flag for eas branch:publish. ([#549](https://github.com/expo/eas-cli/pull/549) by [@jkhales](https://github.com/jkhales))
|
||||
- Add submissions link in output of `eas submit`. ([#553](https://github.com/expo/eas-cli/pull/553) by [@ajsmth](https://github.com/ajsmth))
|
||||
- Add submit profiles. ([#555](https://github.com/expo/eas-cli/pull/555) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Add `changesNotSentForReview` option to android submissions. ([#560](https://github.com/expo/eas-cli/pull/560) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Support `--json` flag in build commands. ([#567](https://github.com/expo/eas-cli/pull/567) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Add link to https://expo.fyi/eas-build-archive in `eas build` output to make it easier to understand what is going on in the archive/upload phase of the build. ([#562](https://github.com/expo/eas-cli/pull/562) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Update QR code URL for Android internal distribution build and generate smaller QR code. ([#573](https://github.com/expo/eas-cli/pull/573) by [@axeldelafosse](https://github.com/axeldelafosse))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix `--sku` flag being ignored when running `eas submit -p ios`. ([#559](https://github.com/expo/eas-cli/pull/559) by [@barthap](https://github.com/barthap))
|
||||
- Set correct distribution for simulator builds. ([#556](https://github.com/expo/eas-cli/pull/556) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Add warnings around common issues (`NODE_ENV=production`, git ignored `googleServicesFile`). ([#569](https://github.com/expo/eas-cli/pull/569) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.23.0](https://github.com/expo/eas-cli/releases/tag/v0.23.0) - 2021-08-05
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Stop including deprecated storageBucket field when publishing. ([#547](https://github.com/expo/eas-cli/pull/547) by [@jkhales](https://github.com/jkhales))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Pass through metadata context to app version resolver. ([#550](https://github.com/expo/eas-cli/pull/550) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [0.22.4](https://github.com/expo/eas-cli/releases/tag/v0.22.4) - 2021-08-02
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix build profile schema validation when `simulator` key is present for iOS. ([#546](https://github.com/expo/eas-cli/pull/546) by [@sallar](https://github.com/sallar))
|
||||
|
||||
## [0.22.2](https://github.com/expo/eas-cli/releases/tag/v0.22.2) - 2021-08-02
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Introduce new format of eas.json (old eas.json will be migrated automatically). ([#537](https://github.com/expo/eas-cli/pull/537) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Validate metro config for managed workflow projects. ([#534](https://github.com/expo/eas-cli/pull/534) by [@dsokal](https://github.com/dsokal))
|
||||
- Add more filter params to `build:list`. ([#540](https://github.com/expo/eas-cli/pull/540) by [@dsokal](https://github.com/dsokal))
|
||||
- Evaluate interpolated iOS version strings for metadata. ([#541](https://github.com/expo/eas-cli/pull/541) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Validate metadata client side to print better errors. ([#542](https://github.com/expo/eas-cli/pull/542) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Fix `new` build status when building both paltforms. ([#543](https://github.com/expo/eas-cli/pull/543) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Fix link to build details page where previously we used `project.name` instead of `project.slug` - leave out project segment entirely and depend on redirect. ([#554](https://github.com/expo/eas-cli/pull/554) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [0.22.1](https://github.com/expo/eas-cli/releases/tag/v0.22.1) - 2021-07-28
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Include the file extension for update's assets in the manifest fragment. ([#532](https://github.com/expo/eas-cli/pull/535) by [@jkhales](https://github.com/jkhales))
|
||||
- Use env variables from `eas.json` when evaluating `app.config.js`. ([#532](https://github.com/expo/eas-cli/pull/532) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix workflow detection when xcodeproj is an empty directory. ([#531](https://github.com/expo/eas-cli/pull/531) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Fix Android Keystore upload. ([#538](https://github.com/expo/eas-cli/pull/538) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Upgrade typescript to 4.3.5. ([#533](https://github.com/expo/eas-cli/pull/533) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.22.0](https://github.com/expo/eas-cli/releases/tag/v0.22.0) - 2021-07-23
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Added support for automatically creating and linking iOS capability identifiers (Apple Pay, iCloud Containers, App Groups). ([#524](https://github.com/expo/eas-cli/pull/524) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Add `secret:create --force` command to overwrite existing secrets. ([#513](https://github.com/expo/eas-cli/pull/513) by [@bycedric](https://github.com/bycedric))
|
||||
- Fall back to APK when building for internal distribution. ([#527](https://github.com/expo/eas-cli/pull/527) by [@dsokal](https://github.com/dsokal))
|
||||
- Add `iosEnterpriseProvisioning` to build metadata. ([#522](https://github.com/expo/eas-cli/pull/522) by [@dsokal](https://github.com/dsokal))
|
||||
- Autodetect Google Services JSON key path in `eas submit -p android`. ([#520](https://github.com/expo/eas-cli/pull/297) by [@barthap](https://github.com/barthap))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix iOS capability syncing on build. ([#521](https://github.com/expo/eas-cli/pull/521) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Fix unhandled error when amplitude domains are blocked. ([#512](https://github.com/expo/eas-cli/pull/512) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Use default value for `appBuildVersion` in build metadata when building an Android managed project. ([#526](https://github.com/expo/eas-cli/pull/526) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.21.0](https://github.com/expo/eas-cli/releases/tag/v0.21.0) - 2021-07-12
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add `project:*` commands. ([#500](https://github.com/expo/eas-cli/pull/500) by [@jkhales](https://github.com/jkhales))
|
||||
- Added support for iOS 15 capabilities: Communication Notifications, Time Sensitive Notifications, Group Activities, and Family Controls. ([#499](https://github.com/expo/eas-cli/pull/499) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Show more build metadata in `build:view` and `build:list`. ([#504](https://github.com/expo/eas-cli/pull/504) and [#508](https://github.com/expo/eas-cli/pull/508) by [@dsokal](https://github.com/dsokal))
|
||||
- Add runtime version to build metadata. ([#493](https://github.com/expo/eas-cli/pull/493) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.20.0](https://github.com/expo/eas-cli/releases/tag/v0.20.0) - 2021-07-09
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Unifify generic and managed workflow, deprecate `workflow` field. ([#497](https://github.com/expo/eas-cli/pull/497) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix runtime version checks. ([#495](https://github.com/expo/eas-cli/pull/495) by [@dsokal](https://github.com/dsokal))
|
||||
- Resolve `--android-package` correctly in `eas submit` command. ([#494](https://github.com/expo/eas-cli/pull/494) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.19.1](https://github.com/expo/eas-cli/releases/tag/v0.19.1) - 2021-07-02
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Improve project workflow detection (fixes the case where the `android` and/or `ios` directories are git-ignored). ([#490](https://github.com/expo/eas-cli/pull/490) by [@dsokal](https://github.com/dsokal))
|
||||
- Improve credentials workflow with project creation and current working directories ([#491](https://github.com/expo/eas-cli/pull/491) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Better debugging experience ([#492](https://github.com/expo/eas-cli/pull/492) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.19.0](https://github.com/expo/eas-cli/releases/tag/v0.19.0) - 2021-07-01
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Auto-suggest application id and bundle identifier when running `eas build:configure` for a managed project. ([#487](https://github.com/expo/eas-cli/pull/487) by [@dsokal](https://github.com/dsokal))
|
||||
- Support configuring one platform at a time when running `eas build`. ([#483](https://github.com/expo/eas-cli/pull/483) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [0.18.3](https://github.com/expo/eas-cli/releases/tag/v0.18.3) - 2021-06-28
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Support bundle identifiers that begin with `.`. ([#459](https://github.com/expo/eas-cli/pull/459) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Push Key setup integrated with ios build command ([#473](https://github.com/expo/eas-cli/pull/473) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix android builds for PKCS12 keystores. ([#484](https://github.com/expo/eas-cli/pull/484) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.18.2](https://github.com/expo/eas-cli/releases/tag/v0.18.2) - 2021-06-25
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Change default iOS image for projects with Expo SDK <= 41 (SDK 41 won't build with Xcode 12.5). ([#479](https://github.com/expo/eas-cli/pull/479) by [@dsokal](https://github.com/dsokal))
|
||||
- Make Apple team optional in appropriate cases. ([#468](https://github.com/expo/eas-cli/pull/468) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Show projects that depend on a push key. ([#472](https://github.com/expo/eas-cli/pull/472) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix inverted conditional so we actually only prompt about commit when index is dirty. ([#481](https://github.com/expo/eas-cli/pull/481) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [0.18.1](https://github.com/expo/eas-cli/releases/tag/v0.18.1) - 2021-06-24
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add basic printing support for Apple App Specific Password. ([#474](https://github.com/expo/eas-cli/pull/474) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix CLI UI getting blocked on credentials migration, leading to partially migrated state. ([#477](https://github.com/expo/eas-cli/pull/477) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [0.18.0](https://github.com/expo/eas-cli/releases/tag/v0.18.0) - 2021-06-24
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Drop support for `experimental.npmToken` in credentials.json, EAS Secrets can be used instead. ([#444](https://github.com/expo/eas-cli/pull/444) by [@dsokal](https://github.com/dsokal))
|
||||
- Remove `--allow-experimental` flag from `eas build:configure` as it has no effect now. ([#444](https://github.com/expo/eas-cli/pull/444) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Make credentials manager work with multi-target iOS projects. ([#441](https://github.com/expo/eas-cli/pull/441) by [@dsokal](https://github.com/dsokal))
|
||||
- Copy over credentials from Expo Classic to EAS. ([#445](https://github.com/expo/eas-cli/pull/445) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add Big Sur image for iOS builds. ([#457](https://github.com/expo/eas-cli/pull/457) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- New version of Android credentials manager. ([#460](https://github.com/expo/eas-cli/pull/460) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add an internal distribution with dev client to `build:configure` defaults. ([#465](https://github.com/expo/eas-cli/pull/465) by [@fson](https://github.com/fson))
|
||||
- Add `updates.channel` to build metadata. ([#461](https://github.com/expo/eas-cli/pull/461) by [@jkhales](https://github.com/jkhales))
|
||||
- iOS push key setup and management now available in `eas-cli credentials`. ([#469](https://github.com/expo/eas-cli/pull/469) [#470](https://github.com/expo/eas-cli/pull/470) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Support new build status: `new`. ([#475](https://github.com/expo/eas-cli/pull/475) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Deprecate `--skip-credentials-check` flag because it doesn't do anything and is no longer needed. ([#442](https://github.com/expo/eas-cli/pull/442) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Move android credentials code to new Graphql API. ([#439](https://github.com/expo/eas-cli/pull/439) [#440](https://github.com/expo/eas-cli/pull/440) [#438](https://github.com/expo/eas-cli/pull/438) [#443](https://github.com/expo/eas-cli/pull/443) [#447](https://github.com/expo/eas-cli/pull/447) [#451](https://github.com/expo/eas-cli/pull/451) [#455](https://github.com/expo/eas-cli/pull/455) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Prepare Graphql infra to support iOS push keys. ([#456](https://github.com/expo/eas-cli/pull/456) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Improve credentials DX ([#448](https://github.com/expo/eas-cli/pull/448) [#449](https://github.com/expo/eas-cli/pull/449) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add analytics on dev client builds. ([#454](https://github.com/expo/eas-cli/pull/454) by [@fson](https://github.com/fson))
|
||||
- Support non-git projects. ([#462](https://github.com/expo/eas-cli/pull/462) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.17.0](https://github.com/expo/eas-cli/releases/tag/v0.17.0) - 2021-06-02
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix bundle identifier resolution when native target is not provided. ([#434](https://github.com/expo/eas-cli/pull/434) by [@dsokal](https://github.com/dsokal))
|
||||
- Fix git repo root path getter on Windows. ([#429](https://github.com/expo/eas-cli/pull/429) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Fix resolving Android application identifier. ([#431](https://github.com/expo/eas-cli/pull/431) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Android credentials setup now on Graphql API. ([#434](https://github.com/expo/eas-cli/pull/427) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.16.0](https://github.com/expo/eas-cli/releases/tag/v0.16.0) - 2021-05-26
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Opt out of capability syncing with `EXPO_NO_CAPABILITY_SYNC=1`. ([#426](https://github.com/expo/eas-cli/pull/426) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Add more verbose logging around capability syncing to help debug reported issues. ([#426](https://github.com/expo/eas-cli/pull/426) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Add managed credentials support for multi-target iOS projects. ([#414](https://github.com/expo/eas-cli/pull/414) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.15.1](https://github.com/expo/eas-cli/releases/tag/v0.15.1) - 2021-05-25
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Support auto capabilities in managed workflow using Expo config plugin introspection. ([#419](https://github.com/expo/eas-cli/pull/419) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix sending `appBuildVersion` as part of the build metadata. ([#423](https://github.com/expo/eas-cli/pull/423) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.15.0](https://github.com/expo/eas-cli/releases/tag/v0.15.0) - 2021-05-20
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Remove "Auto" option for `schemeBuildConfiguration` and make the old "Auto" behaviour the default. ([#394](https://github.com/expo/eas-cli/pull/394) by [@randomhajile](https://github.com/randomhajile))
|
||||
- Remove `experimental.disableIosBundleIdentifierValidation` flag from eas.json. ([#407](https://github.com/expo/eas-cli/pull/407) by [@dsokal](https://github.com/dsokal))
|
||||
- Deprecate `android.package` and `ios.bundleIdentifier` in app config for generic projects. EAS CLI depends on the values in native code now. ([#407](https://github.com/expo/eas-cli/pull/407) by [@dsokal](https://github.com/dsokal))
|
||||
- Remove application id synchronization (`android.package` and `ios.bundleIdentifier`) between app.json and native code when running builds. ([#407](https://github.com/expo/eas-cli/pull/407) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Auto sync associated domains capability before building. ([#384](https://github.com/expo/eas-cli/pull/384) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Add eas init command. [#402](https://github.com/expo/eas-cli/pull/402) by [@jkhales](https://github.com/jkhales))
|
||||
- Allow for arbitrary string values in `schemeBuildConfiguration`. ([#394](https://github.com/expo/eas-cli/pull/394) by [@randomhajile](https://github.com/randomhajile))
|
||||
- Allow for installing custom `expo-cli` version on EAS Build. ([#409](https://github.com/expo/eas-cli/pull/409) by [@randomhajile](https://github.com/randomhajile))
|
||||
- Support PKCS kesytores for Android. ([#398](https://github.com/expo/eas-cli/pull/398) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Support empty passwords in Android and iOS keystores. ([#398](https://github.com/expo/eas-cli/pull/398) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Add more build metadata - `appBuildVersion`. ([#413](https://github.com/expo/eas-cli/pull/413) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix failure when publishing without the platform flag. ([#415](https://github.com/expo/eas-cli/pull/415) by [@jkhales](https://github.com/jkhales))
|
||||
- Pin versions in package.json. ([#399](https://github.com/expo/eas-cli/pull/399) by [@dsokal](https://github.com/dsokal))
|
||||
- Revert [0ac2f](https://github.com/expo/eas-cli/commit/0ac2fb77a118df609381c4e350aa68609340c3cd) as the root cause of the issue has been fixed in [#399](https://github.com/expo/eas-cli/pull/399).
|
||||
- Include development-client in valid buildType for internal distribution. ([#410](https://github.com/expo/eas-cli/pull/410) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [0.14.1](https://github.com/expo/eas-cli/releases/tag/v0.14.1) - 2021-05-11
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Make the default urql requestPolicy 'network-only'. ([#397](https://github.com/expo/eas-cli/pull/397) by [@jkhales](https://github.com/jkhales))
|
||||
|
||||
## [0.14.0](https://github.com/expo/eas-cli/releases/tag/v0.14.0) - 2021-05-10
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Make eas branch:publish work with expo-cli >= 4.4.3 ([#366](https://github.com/expo/eas-cli/pull/366) by [@jkhales](https://github.com/jkhales))
|
||||
- Create asset keys without an extension. ([#366](https://github.com/expo/eas-cli/pull/366) by [@jkhales](https://github.com/jkhales))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Display platforms supported by an update group ([#391](https://github.com/expo/eas-cli/pull/391) by [@jkhales](https://github.com/jkhales))
|
||||
- Add a --platform flag to branch:publish command ([#389](https://github.com/expo/eas-cli/pull/389) by [@jkhales](https://github.com/jkhales))
|
||||
- Release new credentials manager. ([#363](https://github.com/expo/eas-cli/pull/363) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add QR code to install internal distribution build on device. ([#371](https://github.com/expo/eas-cli/pull/371) by [@axeldelafosse](https://github.com/axeldelafosse))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Cleanup unused code. ([#364](https://github.com/expo/eas-cli/pull/364), [#365](https://github.com/expo/eas-cli/pull/365) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Redesign UX in beta credentials manager. ([#360](https://github.com/expo/eas-cli/pull/360) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Port more options to the beta credentials manager. ([#352](https://github.com/expo/eas-cli/pull/352), [357](https://github.com/expo/eas-cli/pull/357), [361](https://github.com/expo/eas-cli/pull/361) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add getBuildProfileNamesAsync helper to EasJsonReader. ([#351](https://github.com/expo/eas-cli/pull/351) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Increase build timeout to 1 hour. ([#370](https://github.com/expo/eas-cli/pull/370) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Remove check for pending builds. ([#373](https://github.com/expo/eas-cli/pull/373) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.13.0](https://github.com/expo/eas-cli/releases/tag/v0.13.0) - 2021-04-22
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Implement offline distribution certificate validation when running a build in non-interactive mode. ([#344](https://github.com/expo/eas-cli/pull/344) by [@dsokal](https://github.com/dsokal))
|
||||
- Add support for building internal distribution apps for Apple Enterprise Teams. ([#344](https://github.com/expo/eas-cli/pull/344) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Display descriptive error message when API for EAS Build changes. ([#359](https://github.com/expo/eas-cli/pull/359) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.12.0](https://github.com/expo/eas-cli/releases/tag/v0.12.0) - 2021-04-22
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add the initiating user's username to the build metadata. ([#354](https://github.com/expo/eas-cli/pull/354) by [@dsokal](https://github.com/dsokal))
|
||||
- Add `--clear-cache` flag for `eas build`. ([#355](https://github.com/expo/eas-cli/pull/355) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix the bug where the account name was used as the username. ([#354](https://github.com/expo/eas-cli/pull/354) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.11.1](https://github.com/expo/eas-cli/releases/tag/v0.11.1) - 2021-04-20
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Resolve ios distribution types. ([#348](https://github.com/expo/eas-cli/pull/348) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Bump version of `@expo/apple-utils` to fix capabilities sync immediately after initial identifier registration. ([593c4](https://github.com/expo/eas-cli/commit/593c42f601396deba1751489343ec07b0a8876da) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [0.11.0](https://github.com/expo/eas-cli/releases/tag/v0.11.0) - 2021-04-20
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- `"credentialsSource": "auto"` is now deprecated. The option will use `"remote"` as the default value. ([#345](https://github.com/expo/eas-cli/pull/345) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add validation when setting up ad-hoc provisioning profile in non-interactive mode. ([#338](https://github.com/expo/eas-cli/pull/338) by [@dsokal](https://github.com/dsokal))
|
||||
- Allow for registering devices when running an ad-hoc build for the first time. ([#340](https://github.com/expo/eas-cli/pull/340) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix the bug where all Android managed builds produced AAB archives. The `buildType` property from `eas.json` is not ignored now. ([#349](https://github.com/expo/eas-cli/pull/349) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.10.0](https://github.com/expo/eas-cli/releases/tag/v0.10.0) - 2021-04-16
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- `secrets:<COMMAND>` is renamed to `secret:<COMMAND>` ([#315](https://github.com/expo/eas-cli/pull/315) by [@fiberjw](https://github.com/fiberjw))
|
||||
- `secret:delete` now accepts `id` through a flag rather than an argument ([#315](https://github.com/expo/eas-cli/pull/315) by [@fiberjw](https://github.com/fiberjw))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Use special Expo SDK runtime version for managed projects ([#336](https://github.com/expo/eas-cli/pull/336)) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix the behavior where the provisioning profile was invalidated after syncing bundle id capabilities. ([#334](https://github.com/expo/eas-cli/pull/334) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Change build credentials summary format. ([#321](https://github.com/expo/eas-cli/pull/321) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.9.1](https://github.com/expo/eas-cli/releases/tag/v0.9.1) - 2021-04-09
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix provisioning profile validation to actually check status. ([#318](https://github.com/expo/eas-cli/pull/318) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Replace Credentials REST API calls with GraphQL counterparts, also improved DX. ([#293](https://github.com/expo/eas-cli/pull/293), [#299](https://github.com/expo/eas-cli/pull/299), [#301](https://github.com/expo/eas-cli/pull/301), [#317](https://github.com/expo/eas-cli/pull/317) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [0.9.0](https://github.com/expo/eas-cli/releases/tag/v0.9.0) - 2021-04-09
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Retry graphQL call on network or transient server errors. ([#320](https://github.com/expo/eas-cli/pull/320) by [@jkhales](https://github.com/jkhales))
|
||||
- Display more friendly error messages when `eas submit` fails. ([#311](https://github.com/expo/eas-cli/pull/297) by [@barthap](https://github.com/barthap))
|
||||
- Add support for managing webhooks (new commands: `webhook:create`, `webhook:view`, `webhook:list`, `webhook:update`, and `webhook:delete`). ([#314](https://github.com/expo/eas-cli/pull/314) by [@dsokal](https://github.com/dsokal))
|
||||
- Support for local builds [experimental]. ([#305](https://github.com/expo/eas-cli/pull/305) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix the issue where the first iOS build fails for a project without `ios.bundleIdentifier` set in `app.json`. ([#319](https://github.com/expo/eas-cli/pull/319) by [@dsokal](https://github.com/dsokal))
|
||||
- Bump @expo/config-plugins to fix `eas build:configure` on Windows. ([d0f3e](https://github.com/expo/eas-cli/commit/d0f3e387e7f31dbce4c3b93b8d083c7d20d9e30d) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Replace REST API calls with GraphQL counterparts. ([#333](https://github.com/expo/eas-cli/pull/333) by [@barthap](https://github.com/barthap))
|
||||
|
||||
## [0.8.1](https://github.com/expo/eas-cli/releases/tag/v0.8.1) - 2021-04-06
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix the issue where the build status never got updated when running `eas build`. ([#310](https://github.com/expo/eas-cli/pull/310) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.8.0](https://github.com/expo/eas-cli/releases/tag/v0.8.0) - 2021-04-06
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Change the way of disabling cache - add `cache.disabled` field. ([#295](https://github.com/expo/eas-cli/pull/295) by [@dsokal](https://github.com/dsokal))
|
||||
- `secrets:create` now uses flags rather than positional arguments ([#300](https://github.com/expo/eas-cli/pull/300) by [@fiberjw](https://github.com/fiberjw))
|
||||
- `secrets:create`'s `target` arg is now called `scope` ([#300](https://github.com/expo/eas-cli/pull/300) by [@fiberjw](https://github.com/fiberjw))
|
||||
- `secrets:list`'s `target` property is now called `scope` ([#300](https://github.com/expo/eas-cli/pull/300) by [@fiberjw](https://github.com/fiberjw))
|
||||
- `secrets:delete`'s `ID` arg is now optional ([#309](https://github.com/expo/eas-cli/pull/309) by [@fiberjw](https://github.com/fiberjw))
|
||||
- `secrets:delete`'s now allows users to choose secrets from a list ([#309](https://github.com/expo/eas-cli/pull/309) by [@fiberjw](https://github.com/fiberjw))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- `build:view` and `build:list` now showing the distribution type (store / internal) and release channel. ([#284](https://github.com/expo/eas-cli/pull/284) by [@vthibault](https://github.com/vthibault))
|
||||
- Add analytics to EAS Build. ([#162](https://github.com/expo/eas-cli/pull/162) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Improve tar archive support in EAS Submit. ([#297](https://github.com/expo/eas-cli/pull/297) by [@barthap](https://github.com/barthap))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix environment secret creation prompt. ([#298](https://github.com/expo/eas-cli/pull/298) by [@fiberjw](https://github.com/fiberjw))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Replace REST API calls with GraphQL counterparts. ([#286](https://github.com/expo/eas-cli/pull/286), [#288](https://github.com/expo/eas-cli/pull/288), [#303](https://github.com/expo/eas-cli/pull/303), [#306](https://github.com/expo/eas-cli/pull/306), [#307](https://github.com/expo/eas-cli/pull/307) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.7.0](https://github.com/expo/eas-cli/releases/tag/v0.7.0) - 2021-03-22
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Print common error messages when builds fail. ([#272](https://github.com/expo/eas-cli/pull/272) by [@dsokal](https://github.com/dsokal))
|
||||
- Commit automatically if `EAS_BUILD_AUTOCOMMIT` is set. ([#271](https://github.com/expo/eas-cli/pull/271) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Allow for installing custom `bundler` version on EAS Build. ([#277](https://github.com/expo/eas-cli/pull/277) by [@dsokal](https://github.com/dsokal))
|
||||
- Add support for managing environment secrets. ([#275](https://github.com/expo/eas-cli/pull/275) by [@fiberjw](https://github.com/fiberjw))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix `eas submit` local archive prompt for `.aab` files when submitting for iOS. ([#273](https://github.com/expo/eas-cli/pull/273) by [@barthap](https://github.com/barthap))
|
||||
- Verify whether "name" field in app.json contains any alphanumeric characters. ([#280](https://github.com/expo/eas-cli/pull/280) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Detect dependency cycles in eas.json build profiles. ([#283](https://github.com/expo/eas-cli/pull/283) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.6.0](https://github.com/expo/eas-cli/releases/tag/v0.6.0) - 2021-03-09
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Generic iOS projects: build release builds by default. ([#266](https://github.com/expo/eas-cli/pull/266) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Log the size of the archived project when uploading. ([#264](https://github.com/expo/eas-cli/pull/264) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Add more build metadata (release channel, build profile name, git commit hash). ([#265](https://github.com/expo/eas-cli/pull/265) by [@dsokal](https://github.com/dsokal))
|
||||
- Display App Store link after successful submission. ([#144](https://github.com/expo/eas-cli/pull/144) by [@barthap](https://github.com/barthap))
|
||||
- Add `experimental.disableIosBundleIdentifierValidation` flag to eas.json. ([#263](https://github.com/expo/eas-cli/pull/263) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Support internal distribution in non-interactive builds. ([#269](https://github.com/expo/eas-cli/pull/269) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Print Apple Team ID in the output of `device:list` when the team name is unknown. ([#268](https://github.com/expo/eas-cli/pull/268) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.5.0](https://github.com/expo/eas-cli/releases/tag/v0.5.0) - 2021-03-02
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add support for build cache. ([#247](https://github.com/expo/eas-cli/pull/247) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Enable internal distribution support for self-managed credentials. ([#256](https://github.com/expo/eas-cli/pull/256) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Make sure all files are committed before build. ([#251](https://github.com/expo/eas-cli/pull/251) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Fix `eas submit` support for tar.gz files. ([#257](https://github.com/expo/eas-cli/pull/257)) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Show untracked files when checking `git status`. ([#259](https://github.com/expo/eas-cli/pull/259) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Upgrade `@expo/eas-build-job` from `0.2.12` to `0.2.13`. ([#245](https://github.com/expo/eas-cli/pull/245) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.4.3](https://github.com/expo/eas-cli/releases/tag/v0.4.3) - 2021-02-23
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add support for iOS simulator builds. ([#240](https://github.com/expo/eas-cli/pull/240) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Use fixed version of `@expo/eas-json`. ([#243](https://github.com/expo/eas-cli/pull/243) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.4.2](https://github.com/expo/eas-cli/releases/tag/v0.4.2) - 2021-02-18
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix detecting application target (iOS builds). ([#238](https://github.com/expo/eas-cli/pull/238) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.4.1](https://github.com/expo/eas-cli/releases/tag/v0.4.1) - 2021-02-16
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix `"buildType" is not allowed` error. ([#595bf](https://github.com/expo/eas-cli/commit/595bfecf1cbff0f76e7fd2049fe16f6f38bbe150) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.4.0](https://github.com/expo/eas-cli/releases/tag/v0.4.0) - 2021-02-16
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add build:cancel command. ([#219](https://github.com/expo/eas-cli/pull/219) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Implement version auto increment for iOS builds. ([#231](https://github.com/expo/eas-cli/pull/231) by [@dsokal](https://github.com/dsokal))
|
||||
- Add support for builder environment customizations. ([#230](https://github.com/expo/eas-cli/pull/230) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Add `schemeBuildConfiguration` option for generic iOS builds. ([#234](https://github.com/expo/eas-cli/pull/234) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix `--no-wait` flag for `eas build`. ([#226](https://github.com/expo/eas-cli/pull/226) by [@paul-ridgway](https://github.com/paul-ridgway))
|
||||
- Fix running builds from project subdirectories. ([#229](https://github.com/expo/eas-cli/pull/229) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
@@ -0,0 +1,7 @@
|
||||
packages/eas-cli/src/build @dsokal @wkozyra95
|
||||
packages/eas-cli/src/commands/build @dsokal @wkozyra95
|
||||
|
||||
packages/eas-cli/src/submit @barthap @dsokal
|
||||
packages/eas-cli/src/commands/submit.ts @barthap @dsokal
|
||||
|
||||
packages/eas-json @dsokal @wkozyra95
|
||||
@@ -0,0 +1,7 @@
|
||||
# Community Code of Conduct
|
||||
|
||||
The Expo team is committed to fostering an open and welcoming environment.
|
||||
|
||||
**Our Community Guidelines can be found here:**
|
||||
|
||||
https://expo.dev/guidelines
|
||||
+2
-6
@@ -1,8 +1,4 @@
|
||||
## Releasing a new version
|
||||
|
||||
1. Add release notes to `CHANGELOG.md`.
|
||||
1. Run `yarn release` in the repository root folder. This will create a new tag
|
||||
like `v1.0.0`.
|
||||
1. Wait until the `release` GitHub workflow has finished.
|
||||
1. Once the release ready to go, you can go to
|
||||
https://github.com/expo/eas-cli/releases and mark it as not draft.
|
||||
1. Run `yarn release` in the repository root folder.
|
||||
2. That's it! GitHub Actions is going to take care of the rest. Watch the #eas-cli Slack channel for a successful release notification.
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2020-present 650 Industries, Inc. (aka Expo)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+4
-5
@@ -11,7 +11,7 @@
|
||||
|
||||
prefix="${EAS_PREFIX:-/usr/local}"
|
||||
temp_dir="$(mktemp -d)"
|
||||
|
||||
|
||||
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]
|
||||
then
|
||||
# Use colors when stdout is a terminal and NO_COLOR isn't set.
|
||||
@@ -67,7 +67,7 @@
|
||||
if [[ ! ":$PATH:" == *":$prefix/bin:"* ]]; then
|
||||
abort "Your path is missing $prefix/bin, you need to add this to \$PATH use this installer."
|
||||
fi
|
||||
|
||||
|
||||
hardware="$(uname -m)"
|
||||
case "$hardware" in
|
||||
x86_64)
|
||||
@@ -95,13 +95,12 @@
|
||||
abort "This installer is supported only on macOS, Linux, and Windows Subsystem for Linux."
|
||||
;;
|
||||
esac
|
||||
|
||||
url="https://github.com/expo/eas-cli/releases/latest/download/eas-$platform-$arch.tar.gz"
|
||||
url=$(hub release -L 1 | xargs hub release show --show-downloads | grep "${platform}-${arch}.tar.gz")
|
||||
echo "Installing EAS CLI from $url"
|
||||
|
||||
mkdir -p "$temp_dir"
|
||||
cd "$temp_dir"
|
||||
|
||||
|
||||
echo
|
||||
echo "Downloading..."
|
||||
if [ "$(command -v curl)" ]
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
comment: false
|
||||
coverage:
|
||||
round: up
|
||||
precision: 2
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
// Only use this when running tests
|
||||
env: {
|
||||
test: {
|
||||
presets: ['@expo/babel-preset-cli'],
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
+1
-3
@@ -1,9 +1,7 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testRegex: '/__tests__/.*(test|spec)\\.[jt]sx?$',
|
||||
transform: {
|
||||
'^.+\\.[jt]sx?$': ['babel-jest', { configFile: require.resolve('./babel.config.js') }],
|
||||
},
|
||||
watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'],
|
||||
collectCoverageFrom: ['<rootDir>/src/**/*.{ts,tsx,js,jsx}'],
|
||||
};
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.1.2"
|
||||
"version": "0.44.0"
|
||||
}
|
||||
+26
-12
@@ -5,20 +5,34 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "lerna run build",
|
||||
"build:tarballs:linux": "yarn build && yarn workspace eas-cli oclif-dev pack --targets linux-x64",
|
||||
"build:tarballs:linux": "yarn build-for-size-check",
|
||||
"build-for-size-check": "yarn build && yarn workspace eas-cli pretarball-ci && CLI_SIZE_CHECK=1 yarn workspace eas-cli oclif pack:tarballs --no-xz --targets linux-x64",
|
||||
"typecheck": "lerna run typecheck",
|
||||
"start": "lerna run watch --parallel",
|
||||
"watch": "yarn start",
|
||||
"eas": "packages/eas-cli/bin/run",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"release": "lerna version",
|
||||
"test": "jest"
|
||||
"lint": "eslint .",
|
||||
"release": "./scripts/bin/run update-local-plugin && lerna version --exact",
|
||||
"test": "jest",
|
||||
"clean": "lerna run clean && rm -rf node_modules coverage"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/oclif-dev-cli": "1.24.0-expo",
|
||||
"@types/jest": "^26.0.13",
|
||||
"eslint": "^6.6.0",
|
||||
"eslint-config-universe": "^4.0.0",
|
||||
"jest": "^26.4.2",
|
||||
"jest-watch-typeahead": "^0.6.1",
|
||||
"lerna": "^3.22.1",
|
||||
"prettier": "^2.0.5"
|
||||
"@types/jest": "27.0.1",
|
||||
"@types/node": "16.7.8",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-config-universe": "8.0.0",
|
||||
"eslint-plugin-async-protect": "1.1.0",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"jest": "27.1.0",
|
||||
"jest-watch-typeahead": "0.6.4",
|
||||
"lerna": "4.0.0",
|
||||
"oclif": "2.0.1",
|
||||
"prettier": "2.4.1",
|
||||
"ts-jest": "27.0.5",
|
||||
"typescript": "4.5.2"
|
||||
},
|
||||
"volta": {
|
||||
"node": "16.10.0",
|
||||
"yarn": "1.22.15"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,46 @@
|
||||
const rootEslintrc = require('../../.eslintrc.js');
|
||||
|
||||
module.exports = {
|
||||
rules: {
|
||||
'import/no-cycle': 'error',
|
||||
'import/no-extraneous-dependencies': ['error', { devDependencies: [
|
||||
'**/__tests__/**/*',
|
||||
'**/__mocks__/**/*'
|
||||
] }],
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
...rootEslintrc.rules['no-restricted-imports'][1].paths,
|
||||
{
|
||||
name: 'ora',
|
||||
message: 'Import ora from src/ora.ts instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'graphql/template-strings': [
|
||||
'error',
|
||||
{
|
||||
env: 'apollo',
|
||||
schemaJson: require('./graphql.schema.json'),
|
||||
},
|
||||
],
|
||||
'graphql/named-operations': [
|
||||
'error',
|
||||
{
|
||||
schemaJson: require('./graphql.schema.json'),
|
||||
},
|
||||
],
|
||||
'graphql/required-fields': [
|
||||
'error',
|
||||
{
|
||||
env: 'apollo',
|
||||
schemaJson: require('./graphql.schema.json'),
|
||||
requiredFields: ['id'],
|
||||
},
|
||||
],
|
||||
'graphql/capitalized-type-name': [
|
||||
'error',
|
||||
{
|
||||
schemaJson: require('./graphql.schema.json'),
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: ['graphql'],
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
graphql.schema.json linguist-generated
|
||||
src/graphql/generated.ts linguist-generated
|
||||
+724
-107
@@ -7,27 +7,28 @@ EAS command line tool
|
||||
[](https://npmjs.org/package/eas-cli)
|
||||
[](https://github.com/expo/eas-cli/blob/main/package.json)
|
||||
|
||||
<!-- toc -->
|
||||
* [eas-cli](#eas-cli)
|
||||
* [Usage](#usage)
|
||||
* [Commands](#commands)
|
||||
<!-- tocstop -->
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Commands](#commands)
|
||||
|
||||
# Installation
|
||||
|
||||
```sh
|
||||
npm install -g eas-cli
|
||||
# or
|
||||
yarn global add eas-cli
|
||||
```
|
||||
|
||||
# Usage
|
||||
|
||||
<!-- usage -->
|
||||
```sh-session
|
||||
$ npm install -g eas-cli
|
||||
$ eas COMMAND
|
||||
running command...
|
||||
$ eas (-v|--version|version)
|
||||
eas-cli/0.1.2 darwin-x64 node-v14.15.0
|
||||
$ eas --help [COMMAND]
|
||||
USAGE
|
||||
$ eas COMMAND
|
||||
...
|
||||
```sh
|
||||
eas COMMAND
|
||||
# runs the command
|
||||
eas (-v|--version|version)
|
||||
# prints the version
|
||||
eas --help COMMAND
|
||||
# outputs help for specific command
|
||||
```
|
||||
<!-- usagestop -->
|
||||
|
||||
# Commands
|
||||
|
||||
@@ -35,16 +36,46 @@ USAGE
|
||||
* [`eas account:login`](#eas-accountlogin)
|
||||
* [`eas account:logout`](#eas-accountlogout)
|
||||
* [`eas account:view`](#eas-accountview)
|
||||
* [`eas analytics [STATUS]`](#eas-analytics-status)
|
||||
* [`eas autocomplete [SHELL]`](#eas-autocomplete-shell)
|
||||
* [`eas branch:create [NAME]`](#eas-branchcreate-name)
|
||||
* [`eas branch:delete [NAME]`](#eas-branchdelete-name)
|
||||
* [`eas branch:list`](#eas-branchlist)
|
||||
* [`eas branch:rename`](#eas-branchrename)
|
||||
* [`eas branch:view [NAME]`](#eas-branchview-name)
|
||||
* [`eas build`](#eas-build)
|
||||
* [`eas build:cancel [BUILD_ID]`](#eas-buildcancel-build_id)
|
||||
* [`eas build:configure`](#eas-buildconfigure)
|
||||
* [`eas build:inspect`](#eas-buildinspect)
|
||||
* [`eas build:list`](#eas-buildlist)
|
||||
* [`eas build:view [BUILDID]`](#eas-buildview-buildid)
|
||||
* [`eas build:view [BUILD_ID]`](#eas-buildview-build_id)
|
||||
* [`eas channel:create [NAME]`](#eas-channelcreate-name)
|
||||
* [`eas channel:edit [NAME]`](#eas-channeledit-name)
|
||||
* [`eas channel:list`](#eas-channellist)
|
||||
* [`eas channel:view [NAME]`](#eas-channelview-name)
|
||||
* [`eas config`](#eas-config)
|
||||
* [`eas credentials`](#eas-credentials)
|
||||
* [`eas device:create`](#eas-devicecreate)
|
||||
* [`eas device:list`](#eas-devicelist)
|
||||
* [`eas device:view [UDID]`](#eas-deviceview-udid)
|
||||
* [`eas diagnostics`](#eas-diagnostics)
|
||||
* [`eas help [COMMAND]`](#eas-help-command)
|
||||
* [`eas submit --platform=(android|ios)`](#eas-submit---platformandroidios)
|
||||
* [`eas project:info`](#eas-projectinfo)
|
||||
* [`eas project:init`](#eas-projectinit)
|
||||
* [`eas secret:create`](#eas-secretcreate)
|
||||
* [`eas secret:delete`](#eas-secretdelete)
|
||||
* [`eas secret:list`](#eas-secretlist)
|
||||
* [`eas submit`](#eas-submit)
|
||||
* [`eas update`](#eas-update)
|
||||
* [`eas update:configure`](#eas-updateconfigure)
|
||||
* [`eas update:delete GROUPID`](#eas-updatedelete-groupid)
|
||||
* [`eas update:list`](#eas-updatelist)
|
||||
* [`eas update:view GROUPID`](#eas-updateview-groupid)
|
||||
* [`eas webhook:create`](#eas-webhookcreate)
|
||||
* [`eas webhook:delete [ID]`](#eas-webhookdelete-id)
|
||||
* [`eas webhook:list`](#eas-webhooklist)
|
||||
* [`eas webhook:update`](#eas-webhookupdate)
|
||||
* [`eas webhook:view ID`](#eas-webhookview-id)
|
||||
|
||||
## `eas account:login`
|
||||
|
||||
@@ -54,11 +85,14 @@ log in with your Expo account
|
||||
USAGE
|
||||
$ eas account:login
|
||||
|
||||
DESCRIPTION
|
||||
log in with your Expo account
|
||||
|
||||
ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.1.2/build/commands/account/login.ts)_
|
||||
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -68,11 +102,14 @@ log out
|
||||
USAGE
|
||||
$ eas account:logout
|
||||
|
||||
DESCRIPTION
|
||||
log out
|
||||
|
||||
ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.1.2/build/commands/account/logout.ts)_
|
||||
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -82,11 +119,153 @@ show the username you are logged in as
|
||||
USAGE
|
||||
$ eas account:view
|
||||
|
||||
DESCRIPTION
|
||||
show the username you are logged in as
|
||||
|
||||
ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.1.2/build/commands/account/view.ts)_
|
||||
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
view or change analytics settings
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas analytics [STATUS]
|
||||
|
||||
DESCRIPTION
|
||||
view or change analytics settings
|
||||
```
|
||||
|
||||
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
|
||||
## `eas autocomplete [SHELL]`
|
||||
|
||||
display autocomplete installation instructions
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas autocomplete [SHELL] [-r]
|
||||
|
||||
ARGUMENTS
|
||||
SHELL shell type
|
||||
|
||||
FLAGS
|
||||
-r, --refresh-cache Refresh cache (ignores displaying instructions)
|
||||
|
||||
DESCRIPTION
|
||||
display autocomplete installation instructions
|
||||
|
||||
EXAMPLES
|
||||
$ eas autocomplete
|
||||
|
||||
$ eas autocomplete bash
|
||||
|
||||
$ eas autocomplete zsh
|
||||
|
||||
$ eas autocomplete --refresh-cache
|
||||
```
|
||||
|
||||
_See code: [@expo/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v1.0.2/src/commands/autocomplete/index.ts)_
|
||||
|
||||
## `eas branch:create [NAME]`
|
||||
|
||||
Create a branch on the current project.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas branch:create [NAME] [--json]
|
||||
|
||||
ARGUMENTS
|
||||
NAME Name of the branch to create
|
||||
|
||||
FLAGS
|
||||
--json return a json with the new branch ID and name.
|
||||
|
||||
DESCRIPTION
|
||||
Create a branch on the current project.
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
|
||||
## `eas branch:delete [NAME]`
|
||||
|
||||
Delete a branch on the current project
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas branch:delete [NAME] [--json]
|
||||
|
||||
ARGUMENTS
|
||||
NAME Name of the branch to delete
|
||||
|
||||
FLAGS
|
||||
--json return JSON with the edited branch's ID and name.
|
||||
|
||||
DESCRIPTION
|
||||
Delete a branch on the current project
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
|
||||
## `eas branch:list`
|
||||
|
||||
List all branches on this project.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas branch:list [--json]
|
||||
|
||||
FLAGS
|
||||
--json return output as JSON
|
||||
|
||||
DESCRIPTION
|
||||
List all branches on this project.
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
|
||||
## `eas branch:rename`
|
||||
|
||||
Rename a branch.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas branch:rename [--from <value>] [--to <value>] [--json]
|
||||
|
||||
FLAGS
|
||||
--from=<value> current name of the branch.
|
||||
--json return a json with the edited branch's ID and name.
|
||||
--to=<value> new name of the branch.
|
||||
|
||||
DESCRIPTION
|
||||
Rename a branch.
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
|
||||
## `eas branch:view [NAME]`
|
||||
|
||||
View a branch.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas branch:view [NAME] [--json]
|
||||
|
||||
ARGUMENTS
|
||||
NAME Name of the branch to view
|
||||
|
||||
FLAGS
|
||||
--json return a json with the branch's ID name and recent update groups.
|
||||
|
||||
DESCRIPTION
|
||||
View a branch.
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -94,18 +273,44 @@ Start a build
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas build
|
||||
$ eas build [-p android|ios|all] [--json] [--skip-project-configuration] [--profile <value>]
|
||||
[--non-interactive] [--local] [--output <value>] [--wait] [--clear-cache] [--auto-submit |
|
||||
--auto-submit-with-profile <value>]
|
||||
|
||||
OPTIONS
|
||||
FLAGS
|
||||
-p, --platform=(android|ios|all)
|
||||
--non-interactive Run command in --non-interactive mode
|
||||
--profile=profile [default: release] Name of the build profile from eas.json
|
||||
--skip-credentials-check Skip validation of build credentials
|
||||
--skip-project-configuration Skip project configuration
|
||||
--wait Wait for build(s) to complete
|
||||
--auto-submit Submit on build complete using the submit profile with the same name as the
|
||||
build profile
|
||||
--auto-submit-with-profile=PROFILE_NAME Submit on build complete using the submit profile with provided name
|
||||
--clear-cache Clear cache before the build
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr
|
||||
--local Run build locally [experimental]
|
||||
--non-interactive Run command in non-interactive mode
|
||||
--output=<value> Output path for local build
|
||||
--profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to "production" if defined
|
||||
in eas.json.
|
||||
--skip-project-configuration Skip project configuration
|
||||
--[no-]wait Wait for build(s) to complete
|
||||
|
||||
DESCRIPTION
|
||||
Start a build
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.1.2/build/commands/build/index.ts)_
|
||||
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
Cancel a build.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas build:cancel [BUILD_ID]
|
||||
|
||||
DESCRIPTION
|
||||
Cancel a build.
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -113,14 +318,51 @@ Configure the project to support EAS Build.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas build:configure
|
||||
$ eas build:configure [-p android|ios|all]
|
||||
|
||||
OPTIONS
|
||||
FLAGS
|
||||
-p, --platform=(android|ios|all) Platform to configure
|
||||
--allow-experimental Enable experimental configuration steps.
|
||||
|
||||
DESCRIPTION
|
||||
Configure the project to support EAS Build.
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.1.2/build/commands/build/configure.ts)_
|
||||
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:inspect`
|
||||
|
||||
Inspect the state of the project at specific build stages. Useful for troubleshooting.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas build:inspect -p android|ios -s archive|pre-build|post-build -o <value> [--profile <value>] [--force] [-v]
|
||||
|
||||
FLAGS
|
||||
-o, --output=OUTPUT_DIRECTORY
|
||||
(required) Output directory.
|
||||
|
||||
-p, --platform=(android|ios)
|
||||
(required)
|
||||
|
||||
-s, --stage=(archive|pre-build|post-build)
|
||||
(required) Stage of the build you want to inspect.
|
||||
archive - builds the project archive that would be uploaded to EAS when building
|
||||
pre-build - prepares the project to be built with Gradle/Xcode. Does not run the native build.
|
||||
post-build - builds the native project and leaves the output directory for inspection
|
||||
|
||||
-v, --verbose
|
||||
|
||||
--force
|
||||
Delete OUTPUT_DIRECTORY if it already exists.
|
||||
|
||||
--profile=PROFILE_NAME
|
||||
Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.
|
||||
|
||||
DESCRIPTION
|
||||
Inspect the state of the project at specific build stages. Useful for troubleshooting.
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -128,37 +370,159 @@ list all builds for your project
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas build:list
|
||||
$ eas build:list [--platform all|android|ios] [--json] [--status
|
||||
new|in-queue|in-progress|errored|finished|canceled] [--distribution store|internal|simulator] [--channel <value>]
|
||||
[--appVersion <value>] [--appBuildVersion <value>] [--sdkVersion <value>] [--runtimeVersion <value>]
|
||||
[--appIdentifier <value>] [--buildProfile <value>] [--gitCommitHash <value>] [--limit <value>]
|
||||
|
||||
OPTIONS
|
||||
--limit=limit
|
||||
FLAGS
|
||||
--appBuildVersion=<value>
|
||||
--appIdentifier=<value>
|
||||
--appVersion=<value>
|
||||
--buildProfile=<value>
|
||||
--channel=<value>
|
||||
--distribution=(store|internal|simulator)
|
||||
--gitCommitHash=<value>
|
||||
--json Enable JSON output, non-JSON messages will be printed
|
||||
to stderr
|
||||
--limit=<value>
|
||||
--platform=(all|android|ios)
|
||||
--status=(in-queue|in-progress|errored|finished)
|
||||
--runtimeVersion=<value>
|
||||
--sdkVersion=<value>
|
||||
--status=(new|in-queue|in-progress|errored|finished|canceled)
|
||||
|
||||
DESCRIPTION
|
||||
list all builds for your project
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.1.2/build/commands/build/list.ts)_
|
||||
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/build/list.ts)_
|
||||
|
||||
## `eas build:view [BUILDID]`
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
view a build for your project
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas build:view [BUILDID]
|
||||
$ eas build:view [BUILD_ID] [--json]
|
||||
|
||||
FLAGS
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr
|
||||
|
||||
DESCRIPTION
|
||||
view a build for your project
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.1.2/build/commands/build/view.ts)_
|
||||
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/build/view.ts)_
|
||||
|
||||
## `eas channel:create [NAME]`
|
||||
|
||||
Create a channel on the current project.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas channel:create [NAME] [--json]
|
||||
|
||||
ARGUMENTS
|
||||
NAME Name of the channel to create
|
||||
|
||||
FLAGS
|
||||
--json print output as a JSON object with the new channel ID, name and branch mapping.
|
||||
|
||||
DESCRIPTION
|
||||
Create a channel on the current project.
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
|
||||
## `eas channel:edit [NAME]`
|
||||
|
||||
Point a channel at a new branch.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas channel:edit [NAME] [--branch <value>] [--json]
|
||||
|
||||
ARGUMENTS
|
||||
NAME Name of the channel to edit
|
||||
|
||||
FLAGS
|
||||
--branch=<value> Name of the branch to point to
|
||||
--json print output as a JSON object with the channel ID, name and branch mapping.
|
||||
|
||||
DESCRIPTION
|
||||
Point a channel at a new branch.
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
|
||||
## `eas channel:list`
|
||||
|
||||
List all channels on the current project.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas channel:list [--json]
|
||||
|
||||
FLAGS
|
||||
--json print output as a JSON object with the channel ID, name and branch mapping.
|
||||
|
||||
DESCRIPTION
|
||||
List all channels on the current project.
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
|
||||
## `eas channel:view [NAME]`
|
||||
|
||||
View a channel on the current project.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas channel:view [NAME] [--json]
|
||||
|
||||
ARGUMENTS
|
||||
NAME Name of the channel to view
|
||||
|
||||
FLAGS
|
||||
--json print output as a JSON object with the channel ID, name and branch mapping.
|
||||
|
||||
DESCRIPTION
|
||||
View a channel on the current project.
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
show the eas.json config
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas config [-p android|ios] [--profile <value>]
|
||||
|
||||
FLAGS
|
||||
-p, --platform=(android|ios)
|
||||
--profile=<value>
|
||||
|
||||
DESCRIPTION
|
||||
show the eas.json config
|
||||
```
|
||||
|
||||
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/config.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
Manage your credentials
|
||||
manage your credentials
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas credentials
|
||||
|
||||
DESCRIPTION
|
||||
manage your credentials
|
||||
```
|
||||
|
||||
_See code: [build/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.1.2/build/commands/credentials.ts)_
|
||||
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/credentials.ts)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
@@ -167,9 +531,12 @@ register new Apple Devices to use for internal distribution
|
||||
```
|
||||
USAGE
|
||||
$ eas device:create
|
||||
|
||||
DESCRIPTION
|
||||
register new Apple Devices to use for internal distribution
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.1.2/build/commands/device/create.ts)_
|
||||
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/device/create.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -177,13 +544,16 @@ list all registered devices for your account
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas device:list
|
||||
$ eas device:list [--apple-team-id <value>]
|
||||
|
||||
OPTIONS
|
||||
--apple-team-id=apple-team-id
|
||||
FLAGS
|
||||
--apple-team-id=<value>
|
||||
|
||||
DESCRIPTION
|
||||
list all registered devices for your account
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.1.2/build/commands/device/list.ts)_
|
||||
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/device/list.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -192,104 +562,351 @@ view a device for your project
|
||||
```
|
||||
USAGE
|
||||
$ eas device:view [UDID]
|
||||
|
||||
DESCRIPTION
|
||||
view a device for your project
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.1.2/build/commands/device/view.ts)_
|
||||
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/device/view.ts)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
log environment info to the console
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas diagnostics
|
||||
|
||||
DESCRIPTION
|
||||
log environment info to the console
|
||||
```
|
||||
|
||||
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
display help for eas
|
||||
Display help for eas.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas help [COMMAND]
|
||||
$ eas help [COMMAND] [-n]
|
||||
|
||||
ARGUMENTS
|
||||
COMMAND command to show help for
|
||||
COMMAND Command to show help for.
|
||||
|
||||
OPTIONS
|
||||
--all see all commands in CLI
|
||||
FLAGS
|
||||
-n, --nested-commands Include all nested commands in the output.
|
||||
|
||||
DESCRIPTION
|
||||
Display help for eas.
|
||||
```
|
||||
|
||||
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.0/src/commands/help.ts)_
|
||||
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v5.1.10/src/commands/help.ts)_
|
||||
|
||||
## `eas submit --platform=(android|ios)`
|
||||
## `eas project:info`
|
||||
|
||||
Submits build artifact to app store
|
||||
information about the current project
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas submit --platform=(android|ios)
|
||||
$ eas project:info
|
||||
|
||||
OPTIONS
|
||||
-p, --platform=(android|ios) For which platform you want to submit a build
|
||||
DESCRIPTION
|
||||
information about the current project
|
||||
```
|
||||
|
||||
Android specific options=android-package Android package name (default: expo.android.package
|
||||
from app config)
|
||||
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/project/info.ts)_
|
||||
|
||||
iOS specific options=app-name The name of your app as it will appear on the App
|
||||
Store (default: expo.name from app config)
|
||||
## `eas project:init`
|
||||
|
||||
iOS specific options=apple-id Your Apple ID username (you can also set
|
||||
EXPO_APPLE_ID env variable)
|
||||
create or link an EAS project
|
||||
|
||||
iOS specific options=apple-team-id Your Apple Developer Team ID
|
||||
```
|
||||
USAGE
|
||||
$ eas project:init
|
||||
|
||||
iOS specific options=asc-app-id App Store Connect unique application Apple ID
|
||||
number. Providing this param results in skipping
|
||||
app creation step. Learn more:
|
||||
https://expo.fyi/asc-app-id
|
||||
DESCRIPTION
|
||||
create or link an EAS project
|
||||
|
||||
iOS specific options=bundle-identifier Your iOS Bundle Identifier (default:
|
||||
expo.ios.bundleIdentifier from app config)
|
||||
ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
iOS specific options=company-name The name of your company, needed only for the first
|
||||
upload of any app to App Store
|
||||
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/project/init.ts)_
|
||||
|
||||
--id=id ID of the build to submit
|
||||
## `eas secret:create`
|
||||
|
||||
Android specific options=key Path to the JSON key used to authenticate with
|
||||
Google Play
|
||||
Create an environment secret on the current project or owner account.
|
||||
|
||||
iOS specific options=language [default: en-US] Primary language (e.g. English,
|
||||
German, ...)
|
||||
```
|
||||
USAGE
|
||||
$ eas secret:create [--scope account|project] [--name <value>] [--value <value>] [--force]
|
||||
|
||||
--latest Submit the latest build for specified platform
|
||||
FLAGS
|
||||
--force Delete and recreate existing secrets
|
||||
--name=<value> Name of the secret
|
||||
--scope=(account|project) [default: project] Scope for the secret
|
||||
--value=<value> Value of the secret
|
||||
|
||||
--path=path Path to the .apk/.aab file
|
||||
DESCRIPTION
|
||||
Create an environment secret on the current project or owner account.
|
||||
```
|
||||
|
||||
Android specific options=(completed|draft|halted|inProgress) [default: completed] Release status (used when
|
||||
uploading new APKs/AABs)
|
||||
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/secret/create.ts)_
|
||||
|
||||
iOS specific options=sku An unique ID for your app that is not visible on
|
||||
the App Store, will be generated unless provided
|
||||
## `eas secret:delete`
|
||||
|
||||
Android specific options=(production|beta|alpha|internal|rollout) [default: internal] The track of the application to
|
||||
use
|
||||
Delete an environment secret by ID.
|
||||
|
||||
Android specific options=(apk|aab) Android archive type
|
||||
```
|
||||
USAGE
|
||||
$ eas secret:delete [--id <value>]
|
||||
|
||||
--url=url App archive url
|
||||
FLAGS
|
||||
--id=<value> ID of the secret to delete
|
||||
|
||||
--verbose Always print logs from Submission Service
|
||||
DESCRIPTION
|
||||
Delete an environment secret by ID.
|
||||
|
||||
Unsure where to find the secret's ID? Run eas secret:list
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/secret/delete.ts)_
|
||||
|
||||
## `eas secret:list`
|
||||
|
||||
Lists environment secrets available for your current app
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas secret:list
|
||||
|
||||
DESCRIPTION
|
||||
Lists environment secrets available for your current app
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/secret/list.ts)_
|
||||
|
||||
## `eas submit`
|
||||
|
||||
Submit build archive to App Store Connect
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas submit [-p android|ios|all] [--profile <value>] [--latest | --id <value> | --path <value> | --url
|
||||
<value>] [--verbose] [--wait] [--non-interactive]
|
||||
|
||||
FLAGS
|
||||
-p, --platform=(android|ios|all)
|
||||
--id=<value> ID of the build to submit
|
||||
--latest Submit the latest build for specified platform
|
||||
--non-interactive Run command in non-interactive mode
|
||||
--path=<value> Path to the .apk/.aab/.ipa file
|
||||
--profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in
|
||||
eas.json.
|
||||
--url=<value> App archive url
|
||||
--verbose Always print logs from Submission Service
|
||||
--[no-]wait Wait for submission to complete
|
||||
|
||||
DESCRIPTION
|
||||
Submit build archive to App Store Connect
|
||||
|
||||
See how to configure submits with eas.json: https://docs.expo.dev/submit/eas-json/
|
||||
|
||||
ALIASES
|
||||
$ eas build:submit
|
||||
|
||||
EXAMPLES
|
||||
$ eas submit --platform=ios
|
||||
- Fully interactive iOS submission
|
||||
|
||||
$ eas submit --platform=android
|
||||
- Fully interactive Android submission
|
||||
|
||||
$ eas submit -p android --latest --key=/path/to/google-services.json
|
||||
- Minimal non-interactive Android submission, however it can ask you for other params if not specified
|
||||
|
||||
$ EXPO_APPLE_APP_SPECIFIC_PASSWORD=xxx eas submit -p ios --latest [email protected] --asc-app-id=1234567890,
|
||||
- Minimal non-interactive iOS submission, assuming you already have an app in App Store Connect
|
||||
and provide its App ID
|
||||
```
|
||||
|
||||
_See code: [build/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.1.2/build/commands/submit.ts)_
|
||||
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
|
||||
## `eas update`
|
||||
|
||||
Publish an update group.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas update [--branch <value>] [--message <value>] [--republish | --input-dir <value> | --skip-bundler]
|
||||
[--group <value> | | ] [-p android|ios|all] [--json] [--auto]
|
||||
|
||||
FLAGS
|
||||
-p, --platform=(android|ios|all) [default: all]
|
||||
--auto Use the current git branch and commit message for the EAS branch and update message
|
||||
--branch=<value> Branch to publish the update group on
|
||||
--group=<value> Update group to republish
|
||||
--input-dir=<value> [default: dist] Location of the bundle
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr
|
||||
--message=<value> A short message describing the update
|
||||
--republish Republish an update group
|
||||
--skip-bundler Skip running Expo CLI to bundle the app before publishing
|
||||
|
||||
DESCRIPTION
|
||||
Publish an update group.
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/update/index.ts)_
|
||||
|
||||
## `eas update:configure`
|
||||
|
||||
Configure the project to support EAS Update.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas update:configure
|
||||
|
||||
DESCRIPTION
|
||||
Configure the project to support EAS Update.
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
|
||||
## `eas update:delete GROUPID`
|
||||
|
||||
Delete all the updates in an update Group.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas update:delete [GROUPID] [--json]
|
||||
|
||||
ARGUMENTS
|
||||
GROUPID The ID of an update group to delete.
|
||||
|
||||
FLAGS
|
||||
--json Return a json with the group ID of the deleted updates.
|
||||
|
||||
DESCRIPTION
|
||||
Delete all the updates in an update Group.
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
|
||||
## `eas update:list`
|
||||
|
||||
View the recent updates for a branch
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas update:list [--branch <value> | --all] [--json]
|
||||
|
||||
FLAGS
|
||||
--all List all updates associated with this project
|
||||
--branch=<value> List all updates on this branch
|
||||
--json Return a json with all of the recent update groups.
|
||||
|
||||
DESCRIPTION
|
||||
View the recent updates for a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/update/list.ts)_
|
||||
|
||||
## `eas update:view GROUPID`
|
||||
|
||||
Update group details.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas update:view [GROUPID] [--json]
|
||||
|
||||
ARGUMENTS
|
||||
GROUPID The ID of an update group.
|
||||
|
||||
FLAGS
|
||||
--json Return a json with the updates belonging to the group.
|
||||
|
||||
DESCRIPTION
|
||||
Update group details.
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/update/view.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
Create a webhook on the current project.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas webhook:create [--event BUILD|SUBMIT] [--url <value>] [--secret <value>]
|
||||
|
||||
FLAGS
|
||||
--event=(BUILD|SUBMIT) Event type that triggers the webhook
|
||||
--secret=<value> Secret used to create a hash signature of the request payload, provided in the
|
||||
'Expo-Signature' header.
|
||||
--url=<value> Webhook URL
|
||||
|
||||
DESCRIPTION
|
||||
Create a webhook on the current project.
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
Delete a webhook on the current project.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas webhook:delete [ID]
|
||||
|
||||
ARGUMENTS
|
||||
ID ID of the webhook to delete
|
||||
|
||||
DESCRIPTION
|
||||
Delete a webhook on the current project.
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
List webhooks on the current project.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas webhook:list [--event BUILD|SUBMIT]
|
||||
|
||||
FLAGS
|
||||
--event=(BUILD|SUBMIT) Event type that triggers the webhook
|
||||
|
||||
DESCRIPTION
|
||||
List webhooks on the current project.
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
Create a webhook on the current project.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas webhook:update --id <value> [--event BUILD|SUBMIT] [--url <value>] [--secret <value>]
|
||||
|
||||
FLAGS
|
||||
--event=(BUILD|SUBMIT) Event type that triggers the webhook
|
||||
--id=<value> (required) Webhook ID
|
||||
--secret=<value> Secret used to create a hash signature of the request payload, provided in the
|
||||
'Expo-Signature' header.
|
||||
--url=<value> Webhook URL
|
||||
|
||||
DESCRIPTION
|
||||
Create a webhook on the current project.
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
View a webhook on the current project.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas webhook:view [ID]
|
||||
|
||||
ARGUMENTS
|
||||
ID ID of the webhook to view
|
||||
|
||||
DESCRIPTION
|
||||
View a webhook on the current project.
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.44.0/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
<!-- commandsstop -->
|
||||
@@ -1,3 +0,0 @@
|
||||
const getConfig = jest.fn(() => ({ exp: {} }));
|
||||
|
||||
export { getConfig };
|
||||
@@ -1,2 +1,14 @@
|
||||
import { fs } from 'memfs';
|
||||
|
||||
// needed because of a weird bug with tempy (dependency of @expo/config-plugins)
|
||||
// eslint-disable-next-line node/no-sync
|
||||
fs.mkdirSync('/tmp');
|
||||
if (process.env.TMPDIR) {
|
||||
// eslint-disable-next-line node/no-sync
|
||||
fs.mkdirSync(process.env.TMPDIR, { recursive: true });
|
||||
}
|
||||
|
||||
// fs-extra@10 is not compatible with memfs, the following line fixes tests
|
||||
(fs.realpath as any).native = jest.fn();
|
||||
|
||||
module.exports = fs;
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const oclif = require('@oclif/core');
|
||||
const path = require('path');
|
||||
|
||||
const project = path.join(__dirname, '..', 'tsconfig.json');
|
||||
|
||||
// In dev mode -> use ts-node and dev plugins
|
||||
process.env.NODE_ENV = 'development';
|
||||
|
||||
require('ts-node').register({ project });
|
||||
|
||||
// In dev mode, always show stack traces
|
||||
oclif.settings.disableJsonFlag = true
|
||||
|
||||
// Start the CLI
|
||||
oclif.run().then(oclif.flush).catch(oclif.Errors.handle);
|
||||
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
|
||||
node "%~dp0\dev" %*
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
require('@oclif/command')
|
||||
require('@oclif/core')
|
||||
.run()
|
||||
.then(require('@oclif/command/flush'))
|
||||
.catch(require('@oclif/errors/handle'));
|
||||
.then(require('@oclif/core/flush'))
|
||||
.catch(require('@oclif/core/handle'));
|
||||
@@ -1,12 +1,20 @@
|
||||
overwrite: true
|
||||
schema: 'https://staging.exp.host/--/graphql'
|
||||
schema: 'https://staging-api.expo.dev/graphql'
|
||||
documents:
|
||||
- 'src/graphql/**/!(*.d).{ts,tsx}'
|
||||
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
|
||||
- 'src/credentials/android/api/graphql/**/!(*.d).{ts,tsx}'
|
||||
- 'src/commands/**/*.ts'
|
||||
generates:
|
||||
src/graphql/generated.ts:
|
||||
plugins:
|
||||
- 'typescript'
|
||||
- 'typescript-operations'
|
||||
config:
|
||||
dedupeOperationSuffix: true
|
||||
hooks:
|
||||
afterOneFileWrite:
|
||||
- ./annotate-graphql-codegen.sh
|
||||
./graphql.schema.json:
|
||||
plugins:
|
||||
- 'introspection'
|
||||
Generated
+25346
File diff suppressed because it is too large.
Load diff
@@ -2,4 +2,6 @@ module.exports = {
|
||||
...require('../../jest/jest.config.js'),
|
||||
rootDir: __dirname,
|
||||
roots: ['src', '__mocks__'],
|
||||
globalSetup: './jest/global-setup.ts',
|
||||
setupFilesAfterEnv: ['./jest/setup-after-env.ts'],
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = async () => {
|
||||
process.env.TZ = 'UTC';
|
||||
process.env.EAS_NO_VCS = '1';
|
||||
process.env.EAS_PROJECT_ROOT = '/app';
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
/* eslint-disable no-console */
|
||||
beforeAll(() => {
|
||||
console.log = jest.fn();
|
||||
console.warn = jest.fn();
|
||||
console.error = jest.fn();
|
||||
});
|
||||
+110
-83
@@ -1,97 +1,104 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "0.1.2",
|
||||
"author": "Expo <support@expo.io>",
|
||||
"version": "0.44.0",
|
||||
"author": "Expo <support@expo.dev>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
},
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@expo/apple-utils": "0.0.0-alpha.16",
|
||||
"@expo/config": "~3.3.19",
|
||||
"@expo/config-plugins": "~1.0.9",
|
||||
"@expo/eas-build-job": "0.1.4",
|
||||
"@expo/eas-json": "^0.1.0-alpha.20",
|
||||
"@expo/json-file": "^8.2.24",
|
||||
"@expo/plist": "^0.0.11",
|
||||
"@expo/plugin-warn-if-update-available": "^1.7.0",
|
||||
"@expo/results": "^1.0.0",
|
||||
"@expo/spawn-async": "^1.5.0",
|
||||
"@hapi/joi": "^17.1.1",
|
||||
"@oclif/command": "^1",
|
||||
"@oclif/config": "^1",
|
||||
"@oclif/plugin-help": "^3",
|
||||
"@urql/core": "^1.13.0",
|
||||
"chalk": "^4.1.0",
|
||||
"cli-table3": "^0.6.0",
|
||||
"dateformat": "^3.0.3",
|
||||
"env-paths": "^2.2.0",
|
||||
"fast-glob": "^3.2.4",
|
||||
"figures": "^3.2.0",
|
||||
"form-data": "^3.0.0",
|
||||
"fs-extra": "^9.0.1",
|
||||
"getenv": "^1.0.0",
|
||||
"got": "^11.5.2",
|
||||
"graphql": "^15.3.0",
|
||||
"graphql-tag": "^2.11.0",
|
||||
"indent-string": "^4.0.0",
|
||||
"keychain": "^1.3.0",
|
||||
"lodash": "^4.17.20",
|
||||
"md5-file": "^5.0.0",
|
||||
"mime": "^2.4.7",
|
||||
"minimatch": "^3.0.4",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-forge": "^0.10.0",
|
||||
"nullthrows": "^1.1.1",
|
||||
"ora": "^5.1.0",
|
||||
"pkg-dir": "^4.2.0",
|
||||
"progress": "^2.0.3",
|
||||
"prompts": "^2.3.2",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"tar": "^6.0.5",
|
||||
"tempy": "^0.7.0",
|
||||
"tslib": "^1",
|
||||
"untildify": "^4.0.0",
|
||||
"uuid": "^8.3.0",
|
||||
"wordwrap": "^1.0.0"
|
||||
"@expo/apple-utils": "0.0.0-alpha.27",
|
||||
"@expo/config": "6.0.11",
|
||||
"@expo/config-plugins": "4.0.11",
|
||||
"@expo/eas-build-job": "0.2.61",
|
||||
"@expo/eas-json": "0.44.0",
|
||||
"@expo/json-file": "8.2.34",
|
||||
"@expo/pkcs12": "0.0.6",
|
||||
"@expo/plist": "0.0.16",
|
||||
"@expo/plugin-autocomplete": "1.0.2",
|
||||
"@expo/plugin-warn-if-update-available": "2.0.1",
|
||||
"@expo/prebuild-config": "3.0.11",
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.5.0",
|
||||
"@expo/timeago.js": "1.0.0",
|
||||
"@oclif/core": "1.0.10",
|
||||
"@oclif/plugin-help": "5.1.10",
|
||||
"@urql/core": "2.3.1",
|
||||
"@urql/exchange-retry": "0.3.0",
|
||||
"chalk": "4.1.2",
|
||||
"cli-table3": "0.6.0",
|
||||
"dateformat": "4.5.1",
|
||||
"env-paths": "2.2.0",
|
||||
"envinfo": "7.8.1",
|
||||
"fast-glob": "3.2.7",
|
||||
"figures": "3.2.0",
|
||||
"form-data": "3.0.0",
|
||||
"fs-extra": "10.0.0",
|
||||
"getenv": "1.0.0",
|
||||
"got": "11.8.2",
|
||||
"gradle-to-js": "2.0.0",
|
||||
"graphql": "15.5.1",
|
||||
"graphql-tag": "2.12.5",
|
||||
"ignore": "5.1.8",
|
||||
"indent-string": "4.0.0",
|
||||
"jks-js": "1.0.1",
|
||||
"joi": "17.4.2",
|
||||
"keychain": "1.3.0",
|
||||
"log-symbols": "4.1.0",
|
||||
"mime": "2.5.2",
|
||||
"minimatch": "3.0.4",
|
||||
"nanoid": "3.1.25",
|
||||
"node-fetch": "2.6.1",
|
||||
"node-forge": "0.10.0",
|
||||
"nullthrows": "1.1.1",
|
||||
"ora": "5.1.0",
|
||||
"pkg-dir": "4.2.0",
|
||||
"prompts": "2.3.2",
|
||||
"qrcode-terminal": "0.12.0",
|
||||
"resolve-from": "5.0.0",
|
||||
"semver": "7.3.5",
|
||||
"slash": "3.0.0",
|
||||
"strip-ansi": "6.0.0",
|
||||
"tar": "6.1.11",
|
||||
"terminal-link": "2.1.1",
|
||||
"tslib": "2.3.1",
|
||||
"turndown": "7.1.1",
|
||||
"untildify": "4.0.0",
|
||||
"uuid": "8.3.2",
|
||||
"wrap-ansi": "7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.6",
|
||||
"@babel/preset-env": "^7.11.5",
|
||||
"@babel/preset-typescript": "^7.10.4",
|
||||
"@expo/babel-preset-cli": "^0.2.17",
|
||||
"@graphql-codegen/cli": "^1.19.2",
|
||||
"@graphql-codegen/introspection": "^1.18.0",
|
||||
"@graphql-codegen/typescript": "^1.17.11",
|
||||
"@graphql-codegen/typescript-operations": "^1.17.10",
|
||||
"@types/color-string": "^1.5.0",
|
||||
"@types/dateformat": "^3.0.1",
|
||||
"@types/dateformat": "3.0.1",
|
||||
"@types/envinfo": "7.8.1",
|
||||
"@types/form-data": "^2.5.0",
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/fs-extra": "9.0.12",
|
||||
"@types/getenv": "^1.0.0",
|
||||
"@types/lodash": "^4.14.161",
|
||||
"@types/mime": "^2.0.3",
|
||||
"@types/node": "^12",
|
||||
"@types/mime": "2.0.3",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/node-forge": "^0.9.5",
|
||||
"@types/progress": "^2.0.3",
|
||||
"@types/prompts": "^2.0.9",
|
||||
"@types/tar": "^4.0.3",
|
||||
"@types/tough-cookie": "^4.0.0",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@types/wordwrap": "^1.0.0",
|
||||
"@vercel/ncc": "^0.23.0",
|
||||
"axios": "^0.21.0",
|
||||
"babel-jest": "^26.3.0",
|
||||
"memfs": "^3.2.0",
|
||||
"@types/semver": "7.3.8",
|
||||
"@types/tar": "4.0.5",
|
||||
"@types/tough-cookie": "4.0.1",
|
||||
"@types/uuid": "8.3.1",
|
||||
"@types/wrap-ansi": "3.0.0",
|
||||
"axios": "0.21.4",
|
||||
"eslint-plugin-graphql": "4.0.0",
|
||||
"memfs": "3.2.2",
|
||||
"mockdate": "^3.0.2",
|
||||
"ts-node": "^8",
|
||||
"typescript": "^3.9.7",
|
||||
"wonka": "^4.0.14"
|
||||
"nock": "^13.0.5",
|
||||
"tempy": "0.7.0",
|
||||
"ts-deepmerge": "1.0.8",
|
||||
"typescript": "4.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"files": [
|
||||
"/bin",
|
||||
@@ -105,25 +112,36 @@
|
||||
"cli"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "build/index.js",
|
||||
"oclif": {
|
||||
"bin": "eas",
|
||||
"commands": "./build/commands",
|
||||
"plugins": [
|
||||
"@oclif/plugin-help",
|
||||
"@expo/plugin-warn-if-update-available"
|
||||
"@expo/plugin-warn-if-update-available",
|
||||
"@expo/plugin-autocomplete"
|
||||
],
|
||||
"helpClass": "./build/help",
|
||||
"topics": {
|
||||
"account": {
|
||||
"description": "manage your account"
|
||||
},
|
||||
"build": {
|
||||
"description": "build app binaries"
|
||||
},
|
||||
"channel": {
|
||||
"description": "manage update channels"
|
||||
},
|
||||
"device": {
|
||||
"description": "manage your Apple devices for internal distribution"
|
||||
},
|
||||
"project": {
|
||||
"description": "manage your project"
|
||||
},
|
||||
"release": {
|
||||
"description": "manage update releases"
|
||||
},
|
||||
"secret": {
|
||||
"description": "manage project and account secrets"
|
||||
},
|
||||
"update": {
|
||||
"description": "manage individual updates"
|
||||
},
|
||||
@@ -147,16 +165,25 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalHelpFlags": [
|
||||
"-h"
|
||||
],
|
||||
"additionalVersionFlags": [
|
||||
"-v"
|
||||
]
|
||||
},
|
||||
"repository": "expo/eas-cli",
|
||||
"scripts": {
|
||||
"postpack": "rm -f oclif.manifest.json",
|
||||
"prepack": "rm -rf build && yarn build && yarn oclif-dev manifest && yarn oclif-dev readme",
|
||||
"build": "tsc",
|
||||
"prepack": "./scripts/prepack.sh",
|
||||
"pretarball-ci": "./scripts/pretarball-ci.sh",
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"watch": "yarn build --watch --preserveWatchOutput",
|
||||
"typecheck": "tsc",
|
||||
"test": "jest",
|
||||
"version": "yarn oclif-dev readme && git add README.md",
|
||||
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml"
|
||||
},
|
||||
"types": "build/index.d.ts"
|
||||
"version": "yarn oclif readme && node scripts/patch-readme && git add README.md",
|
||||
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml",
|
||||
"clean": "rm -rf dist build tmp node_modules yarn-error.log"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
const { promises: fs } = require('fs');
|
||||
|
||||
(async () => {
|
||||
// Patch `oclif readme` path and link generation
|
||||
let readmeContent = await fs.readFile('README.md', 'utf8');
|
||||
readmeContent = readmeContent.replace(/\[build\//g, '[src/');
|
||||
readmeContent = readmeContent.replace(/build\/commands/g, 'packages/eas-cli/src/commands');
|
||||
await fs.writeFile('README.md', readmeContent);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Patched README path generation');
|
||||
})();
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
rm -rf build
|
||||
yarn build
|
||||
|
||||
if [ -z "$CLI_SIZE_CHECK" ]; then
|
||||
yarn oclif manifest
|
||||
fi
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
EAS_CLI_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
|
||||
PACKAGE_JSON_PATH="$EAS_CLI_DIR/package.json"
|
||||
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
sed -i '' "s/\\\"@expo\/eas-json\\\": \\\".*\\\"/\\\"@expo\/eas-json\\\": \\\"file:..\/..\/..\/eas-json\\\"/g" $PACKAGE_JSON_PATH
|
||||
else
|
||||
sed -i "s/\\\"@expo\/eas-json\\\": \\\".*\\\"/\\\"@expo\/eas-json\\\": \\\"file:..\/..\/..\/eas-json\\\"/g" $PACKAGE_JSON_PATH
|
||||
fi
|
||||
@@ -0,0 +1,27 @@
|
||||
import { JSONValue } from '@expo/json-file';
|
||||
import { HTTPError, RequestError } from 'got/dist/source';
|
||||
|
||||
export default class ApiV2Error extends RequestError {
|
||||
readonly name = 'ApiV2Error';
|
||||
readonly expoApiV2ErrorCode: string;
|
||||
readonly expoApiV2ErrorDetails?: JSONValue;
|
||||
readonly expoApiV2ErrorServerStack?: string;
|
||||
readonly expoApiV2ErrorMetadata?: object;
|
||||
|
||||
constructor(
|
||||
originalError: HTTPError,
|
||||
response: {
|
||||
message: string;
|
||||
code: string;
|
||||
stack?: string;
|
||||
details?: JSONValue;
|
||||
metadata?: object;
|
||||
}
|
||||
) {
|
||||
super(response.message, originalError, originalError.request);
|
||||
this.expoApiV2ErrorCode = response.code;
|
||||
this.expoApiV2ErrorDetails = response.details;
|
||||
this.expoApiV2ErrorServerStack = response.stack;
|
||||
this.expoApiV2ErrorMetadata = response.metadata;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
export default jest.fn().mockReturnValue({
|
||||
export const ora = jest.fn().mockReturnValue({
|
||||
start: jest.fn().mockReturnValue({
|
||||
fail: jest.fn(),
|
||||
succeed: jest.fn(),
|
||||
}),
|
||||
stop: jest.fn(),
|
||||
stopAndPersist: jest.fn(),
|
||||
succeed: jest.fn(),
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import assert from 'assert';
|
||||
import { RequestError } from 'got/dist/source';
|
||||
import nock from 'nock';
|
||||
|
||||
import ApiV2Error from '../ApiV2Error';
|
||||
import { apiClient, getExpoApiBaseUrl } from '../api';
|
||||
|
||||
describe('apiClient', () => {
|
||||
it('converts Expo APIv2 error to ApiV2Error', async () => {
|
||||
nock(getExpoApiBaseUrl())
|
||||
.post('/v2/test')
|
||||
.reply(400, {
|
||||
errors: [
|
||||
{
|
||||
message: 'hellomessage',
|
||||
code: 'TEST_CODE',
|
||||
stack: 'line 1: hello',
|
||||
details: { who: 'world' },
|
||||
metadata: { an: 'object' },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
await apiClient.post('test');
|
||||
} catch (e: any) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeInstanceOf(ApiV2Error);
|
||||
assert(error instanceof ApiV2Error);
|
||||
|
||||
expect(error.message).toEqual('hellomessage');
|
||||
expect(error.expoApiV2ErrorCode).toEqual('TEST_CODE');
|
||||
expect(error.expoApiV2ErrorDetails).toEqual({ who: 'world' });
|
||||
expect(error.expoApiV2ErrorMetadata).toEqual({ an: 'object' });
|
||||
expect(error.expoApiV2ErrorServerStack).toEqual('line 1: hello');
|
||||
});
|
||||
|
||||
it('does not convert non-APIv2 error to ApiV2Error', async () => {
|
||||
nock(getExpoApiBaseUrl()).post('/v2/test').reply(500, 'Something went wrong');
|
||||
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
await apiClient.post('test');
|
||||
} catch (e: any) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeInstanceOf(RequestError);
|
||||
expect(error).not.toBeInstanceOf(ApiV2Error);
|
||||
});
|
||||
});
|
||||
@@ -25,7 +25,7 @@ interface AppJSON {
|
||||
};
|
||||
}
|
||||
|
||||
function createTestProject(
|
||||
export function createTestProject(
|
||||
user: Actor,
|
||||
appJSONExtraData?: Record<string, object | string>
|
||||
): MockProject {
|
||||
@@ -58,5 +58,3 @@ function createTestProject(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export { createTestProject };
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Analytics, Event } from './events';
|
||||
|
||||
export type TrackingContext = Record<string, string | number | boolean>;
|
||||
|
||||
export async function withAnalyticsAsync<Result>(
|
||||
fn: () => Promise<Result>,
|
||||
analytics: {
|
||||
attemptEvent: Event;
|
||||
successEvent: Event;
|
||||
failureEvent: Event;
|
||||
trackingCtx: TrackingContext;
|
||||
}
|
||||
): Promise<Result> {
|
||||
try {
|
||||
const result = await fn();
|
||||
Analytics.logEvent(analytics.successEvent, analytics.trackingCtx);
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
Analytics.logEvent(analytics.failureEvent, {
|
||||
...analytics.trackingCtx,
|
||||
reason: error.message,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { logEvent } from './rudderstackClient';
|
||||
export type Event = BuildEvent | SubmissionEvent;
|
||||
|
||||
export enum SubmissionEvent {
|
||||
SUBMIT_COMMAND = 'submit cli submit command',
|
||||
SUBMIT_COMMAND_ATTEMPT = 'submit cli attempt',
|
||||
SUBMIT_COMMAND_SUCCESS = 'submit cli success',
|
||||
SUBMIT_COMMAND_FAIL = 'submit cli fail',
|
||||
GATHER_CREDENTIALS_ATTEMPT = 'submit cli gather credentials attempt',
|
||||
GATHER_CREDENTIALS_SUCCESS = 'submit cli gather credentials success',
|
||||
GATHER_CREDENTIALS_FAIL = 'submit cli gather credentials fail',
|
||||
GATHER_ARCHIVE_ATTEMPT = 'submit cli gather archive attempt',
|
||||
GATHER_ARCHIVE_SUCCESS = 'submit cli gather archive success',
|
||||
GATHER_ARCHIVE_FAIL = 'submit cli gather archive fail',
|
||||
SUBMIT_REQUEST_ATTEMPT = 'submit cli request attempt',
|
||||
SUBMIT_REQUEST_SUCCESS = 'submit cli request success',
|
||||
SUBMIT_REQUEST_FAIL = 'submit cli request fail',
|
||||
}
|
||||
|
||||
export enum BuildEvent {
|
||||
BUILD_COMMAND = 'build cli build command',
|
||||
PROJECT_UPLOAD_ATTEMPT = 'build cli project upload attempt',
|
||||
PROJECT_UPLOAD_SUCCESS = 'build cli project upload success',
|
||||
PROJECT_UPLOAD_FAIL = 'build cli project upload fail',
|
||||
GATHER_CREDENTIALS_ATTEMPT = 'build cli gather credentials attempt',
|
||||
GATHER_CREDENTIALS_SUCCESS = 'build cli gather credentials success',
|
||||
GATHER_CREDENTIALS_FAIL = 'build cli gather credentials fail',
|
||||
CONFIGURE_PROJECT_ATTEMPT = 'build cli configure project attempt',
|
||||
CONFIGURE_PROJECT_SUCCESS = 'build cli configure project success',
|
||||
CONFIGURE_PROJECT_FAIL = 'build cli configure project fail',
|
||||
BUILD_REQUEST_ATTEMPT = 'build cli build request attempt',
|
||||
BUILD_REQUEST_SUCCESS = 'build cli build request success',
|
||||
BUILD_REQUEST_FAIL = 'build cli build request fail',
|
||||
|
||||
BUILD_STATUS_COMMAND = 'build cli build status',
|
||||
|
||||
CREDENTIALS_SYNC_COMMAND = 'build cli credentials sync command',
|
||||
CREDENTIALS_SYNC_UPDATE_LOCAL_ATTEMPT = 'build cli credentials sync update local attempt',
|
||||
CREDENTIALS_SYNC_UPDATE_LOCAL_SUCCESS = 'build cli credentials sync update local success',
|
||||
CREDENTIALS_SYNC_UPDATE_LOCAL_FAIL = 'build cli credentials sync update local fail',
|
||||
CREDENTIALS_SYNC_UPDATE_REMOTE_ATTEMPT = 'build cli credentials sync update remote attempt',
|
||||
CREDENTIALS_SYNC_UPDATE_REMOTE_SUCCESS = 'build cli credentials sync update remote success',
|
||||
CREDENTIALS_SYNC_UPDATE_REMOTE_FAIL = 'build cli credentials sync update remote fail',
|
||||
|
||||
ANDROID_KEYSTORE_CREATE = 'build cli credentials keystore create',
|
||||
}
|
||||
|
||||
export class Analytics {
|
||||
static logEvent(name: Event, properties: Record<string, any>): void {
|
||||
logEvent(name, properties);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import RudderAnalytics from '@expo/rudder-sdk-node';
|
||||
import os from 'os';
|
||||
import { URL } from 'url';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import UserSettings from '../user/UserSettings';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
|
||||
const PLATFORM_TO_ANALYTICS_PLATFORM: { [platform: string]: string } = {
|
||||
darwin: 'Mac',
|
||||
win32: 'Windows',
|
||||
linux: 'Linux',
|
||||
};
|
||||
|
||||
let rudderstackClient: RudderAnalytics | null = null;
|
||||
let userIdentified = false;
|
||||
let identifyData: {
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
traits: Record<string, any>;
|
||||
} | null = null;
|
||||
|
||||
export async function initAsync(): Promise<void> {
|
||||
// TODO: remove after some time
|
||||
const amplitudeEnabled = await UserSettings.getAsync('amplitudeEnabled', null);
|
||||
if (amplitudeEnabled !== null) {
|
||||
await UserSettings.setAsync('analyticsEnabled', amplitudeEnabled);
|
||||
await UserSettings.deleteKeyAsync('amplitudeEnabled');
|
||||
}
|
||||
const amplitudeDeviceId = await UserSettings.getAsync('amplitudeDeviceId', null);
|
||||
if (amplitudeDeviceId !== null) {
|
||||
await UserSettings.setAsync('analyticsDeviceId', amplitudeDeviceId);
|
||||
await UserSettings.deleteKeyAsync('amplitudeDeviceId');
|
||||
}
|
||||
// TODO: cut here
|
||||
if (process.env.DISABLE_EAS_ANALYTICS) {
|
||||
await UserSettings.setAsync('analyticsEnabled', false);
|
||||
}
|
||||
|
||||
const analyticsEnabled = await UserSettings.getAsync('analyticsEnabled', true);
|
||||
if (analyticsEnabled) {
|
||||
const config =
|
||||
process.env.EXPO_STAGING || process.env.EXPO_LOCAL
|
||||
? {
|
||||
// staging environment
|
||||
rudderstackWriteKey: '1wpX20Da4ltFGSXbPFYUL00Chb7',
|
||||
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
|
||||
}
|
||||
: {
|
||||
// prod environment
|
||||
rudderstackWriteKey: '1wpXLFxmujq86etH6G6cc90hPcC',
|
||||
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
|
||||
};
|
||||
|
||||
rudderstackClient = new RudderAnalytics(
|
||||
config.rudderstackWriteKey,
|
||||
new URL('/v1/batch', config.rudderstackDataPlaneURL).toString(),
|
||||
{
|
||||
flushInterval: 300,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function setUserDataAsync(userId: string, traits: Record<string, any>): Promise<void> {
|
||||
const savedDeviceId = await UserSettings.getAsync('analyticsDeviceId', null);
|
||||
const deviceId = savedDeviceId ?? uuidv4();
|
||||
if (!savedDeviceId) {
|
||||
await UserSettings.setAsync('analyticsDeviceId', deviceId);
|
||||
}
|
||||
|
||||
identifyData = {
|
||||
userId,
|
||||
deviceId,
|
||||
traits,
|
||||
};
|
||||
|
||||
ensureUserIdentified();
|
||||
}
|
||||
|
||||
export async function flushAsync(): Promise<void> {
|
||||
if (rudderstackClient) {
|
||||
await rudderstackClient.flush();
|
||||
}
|
||||
}
|
||||
|
||||
export function logEvent(name: string, properties: Record<string, any> = {}): void {
|
||||
if (!rudderstackClient) {
|
||||
return;
|
||||
}
|
||||
ensureUserIdentified();
|
||||
|
||||
const { userId, deviceId } = identifyData ?? {};
|
||||
const commonEventProperties = { source_version: easCliVersion, source: 'eas cli' };
|
||||
|
||||
const identity = { userId: userId ?? undefined, anonymousId: deviceId ?? uuidv4() };
|
||||
rudderstackClient.track({
|
||||
event: name,
|
||||
properties: { ...properties, ...commonEventProperties },
|
||||
...identity,
|
||||
context: getRudderStackContext(),
|
||||
});
|
||||
}
|
||||
|
||||
function ensureUserIdentified(): void {
|
||||
if (!rudderstackClient || userIdentified || !identifyData) {
|
||||
return;
|
||||
}
|
||||
|
||||
rudderstackClient.identify({
|
||||
userId: identifyData.userId,
|
||||
anonymousId: identifyData.deviceId,
|
||||
traits: identifyData.traits,
|
||||
});
|
||||
userIdentified = true;
|
||||
}
|
||||
|
||||
function getRudderStackContext(): Record<string, any> {
|
||||
const platform = PLATFORM_TO_ANALYTICS_PLATFORM[os.platform()] || os.platform();
|
||||
return {
|
||||
os: { name: platform, version: os.release() },
|
||||
device: { type: platform, model: platform },
|
||||
app: { name: 'eas cli', version: easCliVersion ?? undefined },
|
||||
};
|
||||
}
|
||||
|
||||
export enum AnalyticsEvent {
|
||||
ACTION = 'action', // generic event type which is used to determine the 'daily active user' stat, include an `action: eas ${subcommand}` property inside of the event properties object
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import got, { NormalizedOptions } from 'got';
|
||||
import got, { HTTPError, NormalizedOptions, RequestError } from 'got';
|
||||
|
||||
import ApiV2Error from './ApiV2Error';
|
||||
import { getAccessToken, getSessionSecret } from './user/sessionStorage';
|
||||
|
||||
export const apiClient = got.extend({
|
||||
prefixUrl: getExpoApiBaseUrl() + '/--/api/v2/',
|
||||
prefixUrl: getExpoApiBaseUrl() + '/v2/',
|
||||
hooks: {
|
||||
beforeRequest: [
|
||||
(options: NormalizedOptions) => {
|
||||
@@ -18,25 +19,51 @@ export const apiClient = got.extend({
|
||||
}
|
||||
},
|
||||
],
|
||||
beforeError: [
|
||||
(error: RequestError): RequestError => {
|
||||
if (error instanceof HTTPError) {
|
||||
let result: { [key: string]: any };
|
||||
try {
|
||||
result = JSON.parse(error.response.body as string);
|
||||
} catch (e2) {
|
||||
return error;
|
||||
}
|
||||
if (result.errors?.length) {
|
||||
return new ApiV2Error(error, result.errors[0]);
|
||||
}
|
||||
}
|
||||
return error;
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
export function getExpoApiBaseUrl(): string {
|
||||
if (process.env.EXPO_STAGING) {
|
||||
return `https://staging.exp.host`;
|
||||
return `https://staging-api.expo.dev`;
|
||||
} else if (process.env.EXPO_LOCAL) {
|
||||
return `http://127.0.0.1:3000`;
|
||||
} else {
|
||||
return `https://exp.host`;
|
||||
return `https://api.expo.dev`;
|
||||
}
|
||||
}
|
||||
|
||||
export function getExpoWebsiteBaseUrl(): string {
|
||||
if (process.env.EXPO_STAGING) {
|
||||
return `https://staging.expo.io`;
|
||||
return `https://staging.expo.dev`;
|
||||
} else if (process.env.EXPO_LOCAL) {
|
||||
return `http://expo.test`;
|
||||
} else {
|
||||
return `https://expo.io`;
|
||||
return `https://expo.dev`;
|
||||
}
|
||||
}
|
||||
|
||||
export function getEASUpdateURL(projectId: string): string {
|
||||
if (process.env.EXPO_STAGING) {
|
||||
return new URL(projectId, `https://staging-u.expo.dev`).href;
|
||||
} else if (process.env.EXPO_LOCAL) {
|
||||
return new URL(`expo-updates/${projectId}`, `http://127.0.0.1:3000`).href;
|
||||
} else {
|
||||
return new URL(projectId, `https://u.expo.dev`).href;
|
||||
}
|
||||
}
|
||||
@@ -1,294 +0,0 @@
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import prompts from 'prompts';
|
||||
|
||||
import { asMock } from '../../__tests__/utils';
|
||||
import { CredentialsProvider } from '../../credentials/CredentialsProvider';
|
||||
import { ensureCredentialsAsync } from '../credentials';
|
||||
|
||||
jest.mock('prompts');
|
||||
|
||||
function createMockCredentialsProvider({
|
||||
hasRemote,
|
||||
hasLocal,
|
||||
isLocalSynced,
|
||||
}: any): CredentialsProvider {
|
||||
return {
|
||||
platform: 'android',
|
||||
hasRemoteAsync: jest.fn().mockImplementation(() => hasRemote || false),
|
||||
hasLocalAsync: jest.fn().mockImplementation(() => hasLocal || false),
|
||||
isLocalSyncedAsync: jest.fn().mockImplementation(() => isLocalSynced || false),
|
||||
} as CredentialsProvider;
|
||||
}
|
||||
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleWarn = console.warn;
|
||||
beforeAll(() => {
|
||||
console.log = jest.fn();
|
||||
console.warn = jest.fn();
|
||||
});
|
||||
afterAll(() => {
|
||||
console.log = originalConsoleLog;
|
||||
console.warn = originalConsoleWarn;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
asMock(prompts).mockReset();
|
||||
asMock(prompts).mockImplementation(() => {
|
||||
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
|
||||
});
|
||||
});
|
||||
|
||||
describe(ensureCredentialsAsync, () => {
|
||||
describe('for generic builds', () => {
|
||||
it("should use credentials.json if it's the only available credentials", async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: false,
|
||||
hasLocal: true,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Generic,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
it("should use credentials from server if it's the only available credentials", async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: false,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Generic,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(src).toBe('remote');
|
||||
});
|
||||
it('should use credentials.json if credentials are the same in credentials.json and on server', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: true,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Generic,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
it('should ask which credentials to use when local and remote crednitals have different values', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
asMock(prompts).mockImplementationOnce(() => {
|
||||
return { select: 'local' };
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Generic,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(1);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
it('should ask when local and remote are not the same (select remote)', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
asMock(prompts).mockImplementationOnce(() => {
|
||||
return { select: 'remote' };
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Generic,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(1);
|
||||
expect(src).toBe('remote');
|
||||
});
|
||||
it('should should throw an error when local and remote are not the same (in non interactive mode)', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
|
||||
try {
|
||||
await ensureCredentialsAsync(provider, Workflow.Generic, CredentialsSource.AUTO, true);
|
||||
throw new Error('ensureCredentialsAsync should throw an Error');
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(
|
||||
'Contents of your local credentials.json for Android are not the same as credentials on Expo servers'
|
||||
);
|
||||
}
|
||||
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
it('should ask when no credentials are present and return remote if confirm=true', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: false,
|
||||
hasLocal: false,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
asMock(prompts).mockImplementationOnce(() => {
|
||||
return { value: true }; //confirm
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Generic,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(1);
|
||||
expect(src).toBe('remote');
|
||||
});
|
||||
it('should ask when no credentials are present and abort if confimr=false', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: false,
|
||||
hasLocal: false,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
asMock(prompts).mockImplementationOnce(() => {
|
||||
return { value: false }; //confirm
|
||||
});
|
||||
|
||||
try {
|
||||
await ensureCredentialsAsync(provider, Workflow.Generic, CredentialsSource.AUTO, false);
|
||||
throw new Error('ensureCredentialsAsync should throw an Error');
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(
|
||||
'Aborting build process, credentials are not configured for Android'
|
||||
);
|
||||
}
|
||||
|
||||
expect(prompts).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('should throw an error when no credentials are present in non interactive mode', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: false,
|
||||
hasLocal: false,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
asMock(prompts).mockImplementation(() => {
|
||||
return { value: true }; // confirm
|
||||
});
|
||||
|
||||
try {
|
||||
await ensureCredentialsAsync(provider, Workflow.Generic, CredentialsSource.AUTO, true);
|
||||
throw new Error('ensureCredentialsAsync should throw an Error');
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch('Credentials for this app are not configured');
|
||||
}
|
||||
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('for managed builds', () => {
|
||||
it("should use credentials.json if it's the only available credentials", async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: false,
|
||||
hasLocal: true,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Managed,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
it("should use credentials from server if it's the only available credentials", async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: false,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Managed,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(src).toBe('remote');
|
||||
});
|
||||
it('should use credentials.json if credentials are the same in credentials.json and on server', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: true,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Managed,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
|
||||
it('should use credentials.json even if credentials on server are different', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Managed,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generic builds with credentialsSource set to local', () => {
|
||||
it('should use credentials.json without asking even if credentials on server are different', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Generic,
|
||||
CredentialsSource.LOCAL,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generic builds with credentialsSource set to remote', () => {
|
||||
it('should use credentials from server even if credentials.json exists and have different values', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Generic,
|
||||
CredentialsSource.REMOTE,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
expect(src).toBe('remote');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,93 +1,63 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AndroidConfig } from '@expo/config-plugins';
|
||||
import fs from 'fs-extra';
|
||||
import { AndroidConfig, AndroidManifest } from '@expo/config-plugins';
|
||||
|
||||
import log from '../../log';
|
||||
import { getProjectAccountName } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { ensureValidVersions } from '../utils/updates';
|
||||
|
||||
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
|
||||
ensureValidVersions(exp);
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradle(projectDir);
|
||||
const buildGradleContent = await fs.readFile(buildGradlePath, 'utf8');
|
||||
|
||||
if (!AndroidConfig.Updates.isBuildGradleConfigured(buildGradleContent, projectDir, exp)) {
|
||||
const gradleScriptApply = AndroidConfig.Updates.formatApplyLineForBuildGradle(projectDir, exp);
|
||||
|
||||
await fs.writeFile(
|
||||
buildGradlePath,
|
||||
`${buildGradleContent}\n// Integration with Expo updates\n${gradleScriptApply}\n`
|
||||
);
|
||||
}
|
||||
|
||||
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
|
||||
if (!androidManifestPath) {
|
||||
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
|
||||
}
|
||||
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(
|
||||
androidManifestPath
|
||||
);
|
||||
|
||||
if (!AndroidConfig.Updates.isMainApplicationMetaDataSynced(exp, androidManifest, accountName)) {
|
||||
const result = await AndroidConfig.Updates.setUpdatesConfig(exp, androidManifest, accountName);
|
||||
|
||||
await AndroidConfig.Manifest.writeAndroidManifestAsync(androidManifestPath, result);
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncUpdatesConfigurationAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
): Promise<void> {
|
||||
ensureValidVersions(exp);
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
try {
|
||||
await ensureUpdatesConfiguredAsync(projectDir, exp);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
'expo-updates module is not configured. Please run "eas build:configure" first to configure the project'
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
|
||||
if (!androidManifestPath) {
|
||||
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
|
||||
}
|
||||
let androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(androidManifestPath);
|
||||
const androidManifest = await getAndroidManifestAsync(projectDir);
|
||||
|
||||
if (!AndroidConfig.Updates.areVersionsSynced(exp, androidManifest)) {
|
||||
androidManifest = AndroidConfig.Updates.setVersionsConfig(exp, androidManifest);
|
||||
await AndroidConfig.Manifest.writeAndroidManifestAsync(androidManifestPath, androidManifest);
|
||||
}
|
||||
|
||||
if (!AndroidConfig.Updates.isMainApplicationMetaDataSynced(exp, androidManifest, accountName)) {
|
||||
log.warn(
|
||||
'Native project configuration is not synced with values present in your app.json, run "eas build:configure" to make sure all values are applied in the native project'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradle(projectDir);
|
||||
const buildGradleContent = await fs.readFile(buildGradlePath, 'utf8');
|
||||
|
||||
if (!AndroidConfig.Updates.isBuildGradleConfigured(buildGradleContent, projectDir, exp)) {
|
||||
const gradleScriptApply = AndroidConfig.Updates.formatApplyLineForBuildGradle(projectDir, exp);
|
||||
throw new Error(`Missing ${gradleScriptApply} in ${buildGradlePath}`);
|
||||
}
|
||||
|
||||
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
|
||||
if (!androidManifestPath) {
|
||||
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
|
||||
}
|
||||
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(
|
||||
androidManifestPath
|
||||
const updatedAndroidManifest = AndroidConfig.Updates.setUpdatesConfig(
|
||||
exp,
|
||||
androidManifest,
|
||||
accountName
|
||||
);
|
||||
await AndroidConfig.Manifest.writeAndroidManifestAsync(
|
||||
androidManifestPath,
|
||||
updatedAndroidManifest
|
||||
);
|
||||
}
|
||||
|
||||
if (!AndroidConfig.Updates.isMainApplicationMetaDataSet(androidManifest)) {
|
||||
throw new Error('Missing values in AndroidManifest.xml');
|
||||
export async function readReleaseChannelSafelyAsync(projectDir: string): Promise<string | null> {
|
||||
try {
|
||||
const androidManifest = await getAndroidManifestAsync(projectDir);
|
||||
return AndroidConfig.Manifest.getMainApplicationMetaDataValue(
|
||||
androidManifest,
|
||||
AndroidConfig.Updates.Config.RELEASE_CHANNEL
|
||||
);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function readChannelSafelyAsync(projectDir: string): Promise<string | null> {
|
||||
try {
|
||||
const androidManifest = await getAndroidManifestAsync(projectDir);
|
||||
const stringifiedRequestHeaders = AndroidConfig.Manifest.getMainApplicationMetaDataValue(
|
||||
androidManifest,
|
||||
AndroidConfig.Updates.Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY
|
||||
);
|
||||
if (!stringifiedRequestHeaders) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(stringifiedRequestHeaders)['expo-channel-name'] ?? null;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getAndroidManifestAsync(projectDir: string): Promise<AndroidManifest> {
|
||||
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
|
||||
if (!androidManifestPath) {
|
||||
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
|
||||
}
|
||||
return AndroidConfig.Manifest.readAndroidManifestAsync(androidManifestPath);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
|
||||
import { readChannelSafelyAsync } from '../UpdatesModule';
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
afterAll(async () => {
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
await fs.remove(os.tmpdir());
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
vol.reset();
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
await fs.mkdirp(os.tmpdir());
|
||||
});
|
||||
|
||||
const testChannel = 'test-channel';
|
||||
describe(readChannelSafelyAsync, () => {
|
||||
it('retrieves the channel when it is defined natively', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./android/app/src/main/AndroidManifest.xml': AndroidManifestWithChannel,
|
||||
},
|
||||
'/expo-project'
|
||||
);
|
||||
const channel = await readChannelSafelyAsync('/expo-project');
|
||||
expect(channel).toBe(testChannel);
|
||||
});
|
||||
it('returns null if the channel is not defined natively', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./android/app/src/main/AndroidManifest.xml': AndroidManifestNoChannel,
|
||||
},
|
||||
'/expo-project'
|
||||
);
|
||||
const channel = await readChannelSafelyAsync('/expo-project');
|
||||
expect(channel).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
const AndroidManifestWithChannel = `
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.expo.mycoolapp">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:allowBackup="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<meta-data android:name="expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY" android:value='{"expo-channel-name":"${testChannel}"}'/>
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||
</application>
|
||||
|
||||
|
||||
</manifest>
|
||||
`;
|
||||
const AndroidManifestNoChannel = `
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.expo.mycoolapp">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:allowBackup="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||
</application>
|
||||
|
||||
|
||||
</manifest>
|
||||
`;
|
||||
@@ -0,0 +1,280 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import assert from 'assert';
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import { getAppBuildGradleAsync, resolveConfigValue } from '../../../project/android/gradleUtils';
|
||||
import {
|
||||
BumpStrategy,
|
||||
bumpVersionAsync,
|
||||
bumpVersionInAppJsonAsync,
|
||||
maybeResolveVersionsAsync,
|
||||
} from '../version';
|
||||
|
||||
const fsReal = jest.requireActual('fs').promises as typeof fs;
|
||||
jest.mock('fs');
|
||||
|
||||
afterAll(async () => {
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
await fs.remove(os.tmpdir());
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
vol.reset();
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
await fs.mkdirp(os.tmpdir());
|
||||
});
|
||||
|
||||
// bare workflow
|
||||
describe(bumpVersionAsync, () => {
|
||||
it('throws an informative error when multiple flavors are detected in the android project', async () => {
|
||||
await initProjectWithGradleFileAsync(
|
||||
/*buildGradlePath*/ path.join(
|
||||
__dirname,
|
||||
'../../../project/android/__tests__/fixtures/multiflavor-build.gradle'
|
||||
)
|
||||
);
|
||||
await expect(
|
||||
bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.VERSION_CODE,
|
||||
projectDir: '/multiflavor',
|
||||
exp: {} as any,
|
||||
})
|
||||
).rejects.toThrow(
|
||||
'Automatic version bumping is not supported for multi-flavor Android projects.'
|
||||
);
|
||||
});
|
||||
it('throws an informative error when multiple flavor dimensions are defined in an android project', async () => {
|
||||
await initProjectWithGradleFileAsync(
|
||||
/*buildGradlePath*/ path.join(
|
||||
__dirname,
|
||||
'../../../project/android/__tests__/fixtures/multiflavor-with-dimensions-build.gradle'
|
||||
)
|
||||
);
|
||||
await expect(
|
||||
bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.VERSION_CODE,
|
||||
projectDir: '/multiflavor',
|
||||
exp: {} as any,
|
||||
})
|
||||
).rejects.toThrow(
|
||||
'Automatic version bumping is not supported for multi-flavor Android projects.'
|
||||
);
|
||||
});
|
||||
it('bumps expo.android.versionCode and buildGradle versionCode when strategy = BumpStrategy.VERSION_CODE', async () => {
|
||||
const nativeVersionCode = 100; // this should be overwritten by bumpVersionAsync
|
||||
const fakeExp = initBareWorkflowProject({ versionCode: nativeVersionCode });
|
||||
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.VERSION_CODE,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('3.0.0');
|
||||
expect(fakeExp.android?.versionCode).toBe(124);
|
||||
expect(appJSON.expo.version).toBe('3.0.0');
|
||||
expect(appJSON.expo.android.versionCode).toBe(124);
|
||||
|
||||
const buildGradle = await getAppBuildGradleAsync('/repo');
|
||||
assert(buildGradle);
|
||||
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('124');
|
||||
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.0');
|
||||
});
|
||||
|
||||
it('bumps expo.version and gradle versionCode when strategy = BumpStrategy.SHORT_VERSION', async () => {
|
||||
const nativeVersionName = '1.0.0'; // this should be overwritten by bumpVersionAsync
|
||||
const fakeExp = initBareWorkflowProject({ versionName: nativeVersionName });
|
||||
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.APP_VERSION,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('3.0.1');
|
||||
expect(fakeExp.android?.versionCode).toBe(123);
|
||||
expect(appJSON.expo.version).toBe('3.0.1');
|
||||
expect(appJSON.expo.android.versionCode).toBe(123);
|
||||
|
||||
const buildGradle = await getAppBuildGradleAsync('/repo');
|
||||
assert(buildGradle);
|
||||
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('123');
|
||||
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.1');
|
||||
});
|
||||
|
||||
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
|
||||
const fakeExp = initBareWorkflowProject();
|
||||
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.NOOP,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('3.0.0');
|
||||
expect(fakeExp.android?.versionCode).toBe(123);
|
||||
expect(appJSON.expo.version).toBe('3.0.0');
|
||||
expect(appJSON.expo.android.versionCode).toBe(123);
|
||||
|
||||
const buildGradle = await getAppBuildGradleAsync('/repo');
|
||||
assert(buildGradle);
|
||||
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('123');
|
||||
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.0');
|
||||
});
|
||||
});
|
||||
|
||||
// managed workflow
|
||||
describe(bumpVersionInAppJsonAsync, () => {
|
||||
it('bumps expo.android.versionCode when strategy = BumpStrategy.VERSION_CODE', async () => {
|
||||
const fakeExp = initManagedProject();
|
||||
|
||||
await bumpVersionInAppJsonAsync({
|
||||
bumpStrategy: BumpStrategy.VERSION_CODE,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('5.0.0');
|
||||
expect(fakeExp.android?.versionCode).toBe(127);
|
||||
expect(appJSON.expo.version).toBe('5.0.0');
|
||||
expect(appJSON.expo.android.versionCode).toBe(127);
|
||||
});
|
||||
|
||||
it('bumps expo.version when strategy = BumpStrategy.SHORT_VERSION', async () => {
|
||||
const fakeExp = initManagedProject();
|
||||
|
||||
await bumpVersionInAppJsonAsync({
|
||||
bumpStrategy: BumpStrategy.APP_VERSION,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('5.0.1');
|
||||
expect(fakeExp.android?.versionCode).toBe(126);
|
||||
expect(appJSON.expo.version).toBe('5.0.1');
|
||||
expect(appJSON.expo.android.versionCode).toBe(126);
|
||||
});
|
||||
|
||||
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
|
||||
const fakeExp = initManagedProject();
|
||||
|
||||
await bumpVersionInAppJsonAsync({
|
||||
bumpStrategy: BumpStrategy.NOOP,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('5.0.0');
|
||||
expect(fakeExp.android?.versionCode).toBe(126);
|
||||
expect(appJSON.expo.version).toBe('5.0.0');
|
||||
expect(appJSON.expo.android.versionCode).toBe(126);
|
||||
});
|
||||
});
|
||||
|
||||
describe(maybeResolveVersionsAsync, () => {
|
||||
describe('bare project', () => {
|
||||
it('reads the versions from native code', async () => {
|
||||
const exp = initBareWorkflowProject();
|
||||
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
|
||||
'/repo',
|
||||
exp,
|
||||
{} as BuildProfile<Platform.ANDROID>
|
||||
);
|
||||
expect(appVersion).toBe('3.0.0');
|
||||
expect(appBuildVersion).toBe('123');
|
||||
});
|
||||
});
|
||||
describe('managed project', () => {
|
||||
it('reads the versions from expo config', async () => {
|
||||
const exp = initManagedProject();
|
||||
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
|
||||
'/repo',
|
||||
exp,
|
||||
{} as BuildProfile<Platform.ANDROID>
|
||||
);
|
||||
expect(appVersion).toBe('5.0.0');
|
||||
expect(appBuildVersion).toBe('126');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function initBareWorkflowProject({
|
||||
versionCode = 123,
|
||||
versionName = '3.0.0',
|
||||
}: {
|
||||
versionCode?: number;
|
||||
versionName?: string;
|
||||
} = {}): ExpoConfig {
|
||||
const fakeExp: ExpoConfig = {
|
||||
name: 'myproject',
|
||||
slug: 'myproject',
|
||||
version: '3.0.0',
|
||||
android: {
|
||||
versionCode: 123,
|
||||
},
|
||||
};
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./app.json': JSON.stringify({
|
||||
expo: fakeExp,
|
||||
}),
|
||||
'./android/app/build.gradle': `android {
|
||||
defaultConfig {
|
||||
applicationId "com.expo.testapp"
|
||||
versionCode ${versionCode}
|
||||
versionName "${versionName}"
|
||||
}
|
||||
}`,
|
||||
'./android/app/src/main/AndroidManifest.xml': 'fake',
|
||||
},
|
||||
'/repo'
|
||||
);
|
||||
|
||||
return fakeExp;
|
||||
}
|
||||
|
||||
function initManagedProject(): ExpoConfig {
|
||||
const fakeExp: ExpoConfig = {
|
||||
name: 'myproject',
|
||||
slug: 'myproject',
|
||||
version: '5.0.0',
|
||||
android: {
|
||||
versionCode: 126,
|
||||
},
|
||||
};
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./app.json': JSON.stringify({
|
||||
expo: fakeExp,
|
||||
}),
|
||||
},
|
||||
'/repo'
|
||||
);
|
||||
|
||||
return fakeExp;
|
||||
}
|
||||
|
||||
async function initProjectWithGradleFileAsync(gradleFilePath: string): Promise<void> {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'android/app/build.gradle': await fsReal.readFile(gradleFilePath, 'utf-8'),
|
||||
'./app.json': JSON.stringify({
|
||||
expo: {} as any,
|
||||
}),
|
||||
},
|
||||
'/multiflavor'
|
||||
);
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
|
||||
import { AndroidConfig } from '@expo/config-plugins';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import log from '../../log';
|
||||
import {
|
||||
ensureAppIdentifierIsDefinedAsync,
|
||||
getAndroidApplicationIdAsync,
|
||||
getProjectConfigDescription,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { gitAddAsync } from '../../utils/git';
|
||||
import { Platform } from '../types';
|
||||
|
||||
enum ApplicationIdSource {
|
||||
AndroidProject,
|
||||
AppJson,
|
||||
}
|
||||
|
||||
function isApplicationIdValid(applicationId: string): boolean {
|
||||
return /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/.test(applicationId);
|
||||
}
|
||||
|
||||
export async function ensureApplicationIdIsValidAsync(projectDir: string) {
|
||||
const applicationId = await ensureAppIdentifierIsDefinedAsync(projectDir, Platform.Android);
|
||||
if (!isApplicationIdValid(applicationId)) {
|
||||
const configDescription = getProjectConfigDescription(projectDir);
|
||||
log.error(
|
||||
`Invalid format of Android applicationId. Only alphanumeric characters, '.' and '_' are allowed, and each '.' must be followed by a letter.`
|
||||
);
|
||||
log.error(`Update "android.package" in ${configDescription} and run this command again.`);
|
||||
throw new Error('Invalid applicationId');
|
||||
}
|
||||
}
|
||||
|
||||
export async function configureApplicationIdAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
allowExperimental: boolean
|
||||
): Promise<void> {
|
||||
const configDescription = getProjectConfigDescription(projectDir);
|
||||
const applicationIdFromConfig = AndroidConfig.Package.getPackage(exp);
|
||||
const applicationIdFromAndroidProject = await getAndroidApplicationIdAsync(projectDir);
|
||||
|
||||
if (applicationIdFromAndroidProject && applicationIdFromConfig) {
|
||||
if (applicationIdFromConfig !== applicationIdFromAndroidProject) {
|
||||
log.newLine();
|
||||
log.warn(
|
||||
`We detected that your Android project is configured with a different application id than the one defined in ${configDescription}.`
|
||||
);
|
||||
const hasApplicationIdInStaticConfig = await hasApplicationIdInStaticConfigAsync(
|
||||
projectDir,
|
||||
exp
|
||||
);
|
||||
if (!hasApplicationIdInStaticConfig) {
|
||||
log(`If you choose the one defined in ${configDescription} we'll automatically configure your Android project with it.
|
||||
However, if you choose the one defined in the Android project you'll have to update ${configDescription} on your own.`);
|
||||
}
|
||||
log.newLine();
|
||||
const { applicationIdSource } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'applicationIdSource',
|
||||
message: 'Which application id should we use?',
|
||||
choices: [
|
||||
{
|
||||
title: `${chalk.bold(
|
||||
applicationIdFromAndroidProject
|
||||
)} - In build.gradle in Android project`,
|
||||
value: ApplicationIdSource.AndroidProject,
|
||||
},
|
||||
{
|
||||
title: `${chalk.bold(applicationIdFromConfig)} - In your ${configDescription}`,
|
||||
value: ApplicationIdSource.AppJson,
|
||||
},
|
||||
],
|
||||
});
|
||||
switch (applicationIdSource) {
|
||||
case ApplicationIdSource.AppJson: {
|
||||
await updateApplicationIdInBuildGradleAsync(projectDir, exp);
|
||||
break;
|
||||
}
|
||||
case ApplicationIdSource.AndroidProject: {
|
||||
if (hasApplicationIdInStaticConfig) {
|
||||
await updateAppJsonConfigAsync(projectDir, exp, applicationIdFromAndroidProject);
|
||||
} else {
|
||||
throw new Error(missingPackageMessage(configDescription));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!applicationIdFromAndroidProject && !applicationIdFromConfig) {
|
||||
throw new Error(missingPackageMessage(configDescription));
|
||||
} else if (applicationIdFromAndroidProject && !applicationIdFromConfig) {
|
||||
if (getConfigFilePaths(projectDir).staticConfigPath) {
|
||||
await updateAppJsonConfigAsync(projectDir, exp, applicationIdFromAndroidProject);
|
||||
} else {
|
||||
throw new Error(missingPackageMessage(configDescription));
|
||||
}
|
||||
} else if (!applicationIdFromAndroidProject && applicationIdFromConfig) {
|
||||
// This should never happen, adding warning just in case
|
||||
log.warn(
|
||||
'applicationId is not specified in your ./android/app/build.gradle file. Make sure your project is configured correctly before building.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (allowExperimental) {
|
||||
// this step is optional, the Play store is using applicationId value from build.gradle
|
||||
// to identify the app, so package name does not need to be updated
|
||||
await updatePackageNameAsync(projectDir, exp);
|
||||
await gitAddAsync(path.join(projectDir, 'android'), { intentToAdd: true });
|
||||
}
|
||||
}
|
||||
|
||||
function missingPackageMessage(configDescription: string): string {
|
||||
return `Please define "android.package" in ${configDescription} and run "eas build:configure" again.`;
|
||||
}
|
||||
|
||||
async function updateAppJsonConfigAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
newApplicationId: string
|
||||
): Promise<void> {
|
||||
const paths = getConfigFilePaths(projectDir);
|
||||
assert(paths.staticConfigPath, "can't update dynamic configs");
|
||||
|
||||
const rawStaticConfig = await fs.readJSON(paths.staticConfigPath);
|
||||
rawStaticConfig.expo = {
|
||||
...rawStaticConfig.expo,
|
||||
android: { ...rawStaticConfig.expo?.android, package: newApplicationId },
|
||||
};
|
||||
await fs.writeJson(paths.staticConfigPath, rawStaticConfig, { spaces: 2 });
|
||||
|
||||
exp.android = { ...exp.android, package: newApplicationId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if static config exists and if expo.android.package is defined there.
|
||||
* It will return false if the value in static config is different than "android.package" in ExpoConfig
|
||||
*/
|
||||
async function hasApplicationIdInStaticConfigAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
): Promise<boolean> {
|
||||
if (!exp.android?.package) {
|
||||
return false;
|
||||
}
|
||||
const paths = getConfigFilePaths(projectDir);
|
||||
if (!paths.staticConfigPath) {
|
||||
return false;
|
||||
}
|
||||
const rawStaticConfig = await fs.readJson(paths.staticConfigPath);
|
||||
return rawStaticConfig?.expo?.android?.package === exp.android.package;
|
||||
}
|
||||
|
||||
async function updateApplicationIdInBuildGradleAsync(projectDir: string, exp: ExpoConfig) {
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradle(projectDir);
|
||||
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf8');
|
||||
const updatedBuildGradleContents = AndroidConfig.Package.setPackageInBuildGradle(
|
||||
exp,
|
||||
buildGradleContents
|
||||
);
|
||||
await fs.writeFile(buildGradlePath, updatedBuildGradleContents);
|
||||
}
|
||||
|
||||
async function updatePackageNameAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
|
||||
await AndroidConfig.Package.renamePackageOnDisk(exp, projectDir);
|
||||
|
||||
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
|
||||
if (!androidManifestPath) {
|
||||
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
|
||||
}
|
||||
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(
|
||||
androidManifestPath
|
||||
);
|
||||
const updatedAndroidManifest = AndroidConfig.Package.setPackageInAndroidManifest(
|
||||
exp,
|
||||
androidManifest
|
||||
);
|
||||
await AndroidConfig.Manifest.writeAndroidManifestAsync(
|
||||
androidManifestPath,
|
||||
updatedAndroidManifest
|
||||
);
|
||||
}
|
||||
@@ -1,111 +1,132 @@
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { EasConfig } from '@expo/eas-json';
|
||||
import { Android, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import AndroidCredentialsProvider, {
|
||||
AndroidCredentials,
|
||||
} from '../../credentials/android/AndroidCredentialsProvider';
|
||||
import { createCredentialsContextAsync } from '../../credentials/context';
|
||||
import log from '../../log';
|
||||
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
ensureApplicationIdIsDefinedForManagedProjectAsync,
|
||||
getApplicationIdAsync,
|
||||
} from '../../project/android/applicationId';
|
||||
import { resolveGradleBuildContextAsync } from '../../project/android/gradle';
|
||||
import { toggleConfirmAsync } from '../../prompts';
|
||||
import { CredentialsResult, prepareBuildRequestForPlatformAsync } from '../build';
|
||||
import { BuildContext, CommandContext, createBuildContext } from '../context';
|
||||
import { ensureCredentialsAsync } from '../credentials';
|
||||
import { Platform } from '../types';
|
||||
import { ensureApplicationIdIsValidAsync } from './applicationId';
|
||||
import { validateAndSyncProjectConfigurationAsync } from './configure';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import {
|
||||
BuildRequestSender,
|
||||
CredentialsResult,
|
||||
JobData,
|
||||
prepareBuildRequestForPlatformAsync,
|
||||
} from '../build';
|
||||
import { AndroidBuildContext, BuildContext, CommonContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { logCredentialsSource } from '../utils/credentials';
|
||||
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
|
||||
import { transformJob } from './graphql';
|
||||
import { prepareJobAsync } from './prepareJob';
|
||||
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
|
||||
|
||||
export async function prepareAndroidBuildAsync(
|
||||
commandCtx: CommandContext,
|
||||
easConfig: EasConfig
|
||||
): Promise<() => Promise<string>> {
|
||||
const buildCtx = createBuildContext<Platform.Android>({
|
||||
commandCtx,
|
||||
platform: Platform.Android,
|
||||
easConfig,
|
||||
});
|
||||
const { buildProfile } = buildCtx;
|
||||
export async function createAndroidContextAsync(
|
||||
ctx: CommonContext<Platform.ANDROID>
|
||||
): Promise<AndroidBuildContext> {
|
||||
const { buildProfile } = ctx;
|
||||
|
||||
if (
|
||||
buildProfile.workflow === Workflow.Generic &&
|
||||
!(await fs.pathExists(path.join(commandCtx.projectDir, 'android')))
|
||||
) {
|
||||
throw new Error(
|
||||
`"android" directory not found. If you're trying to build a managed project, set ${chalk.bold(
|
||||
`builds.android.${commandCtx.profile}.workflow`
|
||||
)} in "eas.json" to "managed".`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
buildProfile.workflow === Workflow.Generic &&
|
||||
buildProfile.distribution === 'internal' &&
|
||||
buildProfile.gradleCommand?.match(/bundle/)
|
||||
) {
|
||||
log.addNewLineIfNone();
|
||||
log.warn(
|
||||
if (buildProfile.distribution === 'internal' && buildProfile.gradleCommand?.match(/bundle/)) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
`You're building your Android app for internal distribution. However, we've detected that the Gradle command you defined (${chalk.underline(
|
||||
buildProfile.gradleCommand
|
||||
)}) includes string 'bundle'.
|
||||
This means that it will most likely produce an AAB and you will not be able to install it on your Android devices straight from the Expo website.`
|
||||
);
|
||||
log.newLine();
|
||||
Log.newLine();
|
||||
const confirmed = await toggleConfirmAsync({ message: 'Would you like to proceed?' });
|
||||
if (!confirmed) {
|
||||
log.error('Please update eas.json and come back again.');
|
||||
Log.error('Please update eas.json and come back again.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
await ensureApplicationIdIsValidAsync(commandCtx.projectDir);
|
||||
checkNodeEnvVariable(ctx);
|
||||
await checkGoogleServicesFileAsync(ctx);
|
||||
|
||||
const gradleContext = await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile);
|
||||
|
||||
if (ctx.workflow === Workflow.MANAGED) {
|
||||
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
|
||||
const applicationId = await getApplicationIdAsync(ctx.projectDir, ctx.exp, gradleContext);
|
||||
|
||||
return { applicationId, gradleContext };
|
||||
}
|
||||
|
||||
export async function prepareAndroidBuildAsync(
|
||||
ctx: BuildContext<Platform.ANDROID>
|
||||
): Promise<BuildRequestSender> {
|
||||
return await prepareBuildRequestForPlatformAsync({
|
||||
ctx: buildCtx,
|
||||
projectConfiguration: {},
|
||||
ensureCredentialsAsync: ensureAndroidCredentialsAsync,
|
||||
ensureProjectConfiguredAsync: async () => {
|
||||
if (buildCtx.buildProfile.workflow === Workflow.Generic) {
|
||||
await validateAndSyncProjectConfigurationAsync(commandCtx.projectDir, commandCtx.exp);
|
||||
}
|
||||
ctx,
|
||||
ensureCredentialsAsync: async (ctx: BuildContext<Platform.ANDROID>) => {
|
||||
return await ensureAndroidCredentialsAsync(ctx);
|
||||
},
|
||||
syncProjectConfigurationAsync: async () => {
|
||||
await syncProjectConfigurationAsync({
|
||||
projectDir: ctx.projectDir,
|
||||
exp: ctx.exp,
|
||||
buildProfile: ctx.buildProfile,
|
||||
});
|
||||
},
|
||||
prepareJobAsync: async (
|
||||
ctx: BuildContext<Platform.ANDROID>,
|
||||
jobData: JobData<AndroidCredentials>
|
||||
): Promise<Job> => {
|
||||
return await prepareJobAsync(ctx, jobData);
|
||||
},
|
||||
sendBuildRequestAsync: async (
|
||||
appId: string,
|
||||
job: Android.Job,
|
||||
metadata: Metadata
|
||||
): Promise<BuildResult> => {
|
||||
const graphqlMetadata = transformMetadata(metadata);
|
||||
const graphqlJob = transformJob(job);
|
||||
return await BuildMutation.createAndroidBuildAsync({
|
||||
appId,
|
||||
job: graphqlJob,
|
||||
metadata: graphqlMetadata,
|
||||
});
|
||||
},
|
||||
prepareJobAsync,
|
||||
});
|
||||
}
|
||||
|
||||
function shouldProvideCredentials(ctx: BuildContext<Platform.Android>): boolean {
|
||||
return (
|
||||
ctx.buildProfile.workflow === Workflow.Managed ||
|
||||
(ctx.buildProfile.workflow === Workflow.Generic && !ctx.buildProfile.withoutCredentials)
|
||||
);
|
||||
function shouldProvideCredentials(ctx: BuildContext<Platform.ANDROID>): boolean {
|
||||
return !ctx.buildProfile.withoutCredentials;
|
||||
}
|
||||
|
||||
async function ensureAndroidCredentialsAsync(
|
||||
ctx: BuildContext<Platform.Android>
|
||||
ctx: BuildContext<Platform.ANDROID>
|
||||
): Promise<CredentialsResult<AndroidCredentials> | undefined> {
|
||||
if (!shouldProvideCredentials(ctx)) {
|
||||
return;
|
||||
}
|
||||
const provider = new AndroidCredentialsProvider(
|
||||
await createCredentialsContextAsync(ctx.commandCtx.projectDir, {
|
||||
nonInteractive: ctx.commandCtx.nonInteractive,
|
||||
}),
|
||||
{
|
||||
app: {
|
||||
projectName: ctx.commandCtx.projectName,
|
||||
accountName: ctx.commandCtx.accountName,
|
||||
},
|
||||
skipCredentialsCheck: ctx.commandCtx.skipCredentialsCheck,
|
||||
}
|
||||
);
|
||||
const credentialsSource = await ensureCredentialsAsync(
|
||||
provider,
|
||||
ctx.buildProfile.workflow,
|
||||
ctx.buildProfile.credentialsSource,
|
||||
ctx.commandCtx.nonInteractive
|
||||
const androidApplicationIdentifier = await getApplicationIdAsync(
|
||||
ctx.projectDir,
|
||||
ctx.exp,
|
||||
ctx.android.gradleContext
|
||||
);
|
||||
const provider = new AndroidCredentialsProvider(ctx.credentialsCtx, {
|
||||
app: {
|
||||
account: nullthrows(
|
||||
findAccountByName(ctx.user.accounts, ctx.accountName),
|
||||
`You do not have access to account: ${ctx.accountName}`
|
||||
),
|
||||
projectName: ctx.projectName,
|
||||
androidApplicationIdentifier,
|
||||
},
|
||||
});
|
||||
const { credentialsSource } = ctx.buildProfile;
|
||||
logCredentialsSource(credentialsSource, Platform.ANDROID);
|
||||
return {
|
||||
credentials: await provider.getCredentialsAsync(credentialsSource),
|
||||
source: credentialsSource,
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AndroidConfig } from '@expo/config-plugins';
|
||||
|
||||
import log from '../../log';
|
||||
import { getAndroidApplicationIdAsync } from '../../project/projectUtils';
|
||||
import { gitAddAsync } from '../../utils/git';
|
||||
import { ConfigureContext } from '../context';
|
||||
import { isExpoUpdatesInstalled } from '../utils/updates';
|
||||
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
|
||||
import { configureApplicationIdAsync, ensureApplicationIdIsValidAsync } from './applicationId';
|
||||
|
||||
export async function configureAndroidAsync(ctx: ConfigureContext): Promise<void> {
|
||||
if (!ctx.hasAndroidNativeProject) {
|
||||
return;
|
||||
}
|
||||
await AndroidConfig.EasBuild.configureEasBuildAsync(ctx.projectDir);
|
||||
await configureApplicationIdAsync(ctx.projectDir, ctx.exp, ctx.allowExperimental);
|
||||
await ensureApplicationIdIsValidAsync(ctx.projectDir);
|
||||
|
||||
const easGradlePath = AndroidConfig.EasBuild.getEasBuildGradlePath(ctx.projectDir);
|
||||
await gitAddAsync(easGradlePath, { intentToAdd: true });
|
||||
|
||||
if (isExpoUpdatesInstalled(ctx.projectDir)) {
|
||||
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
log.withTick('Android project configured');
|
||||
}
|
||||
|
||||
export async function validateAndSyncProjectConfigurationAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
): Promise<void> {
|
||||
const applicationIdFromAndroidProject = await getAndroidApplicationIdAsync(projectDir);
|
||||
const packageNameFromConfig = AndroidConfig.Package.getPackage(exp);
|
||||
const isApplicationIdConfigured =
|
||||
applicationIdFromAndroidProject && applicationIdFromAndroidProject === packageNameFromConfig;
|
||||
|
||||
const isEasBuildGradleConfigured = await AndroidConfig.EasBuild.isEasBuildGradleConfiguredAsync(
|
||||
projectDir
|
||||
);
|
||||
if (!isEasBuildGradleConfigured || !isApplicationIdConfigured) {
|
||||
throw new Error(
|
||||
'Project is not configured. Please run "eas build:configure" to configure the project.'
|
||||
);
|
||||
}
|
||||
|
||||
if (isExpoUpdatesInstalled(projectDir)) {
|
||||
await syncUpdatesConfigurationAsync(projectDir, exp);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Android } from '@expo/eas-build-job';
|
||||
|
||||
import { AndroidBuildType, AndroidJobInput } from '../../graphql/generated';
|
||||
import { transformProjectArchive, transformWorkflow } from '../graphql';
|
||||
|
||||
export function transformJob(job: Android.Job): AndroidJobInput {
|
||||
return {
|
||||
type: transformWorkflow(job.type),
|
||||
projectArchive: transformProjectArchive(job.projectArchive),
|
||||
projectRootDirectory: job.projectRootDirectory,
|
||||
releaseChannel: job.releaseChannel,
|
||||
updates: job.updates,
|
||||
secrets: job.secrets,
|
||||
builderEnvironment: job.builderEnvironment,
|
||||
cache: job.cache,
|
||||
gradleCommand: job.gradleCommand,
|
||||
artifactPath: job.artifactPath,
|
||||
username: job.username,
|
||||
buildType: job.buildType && transformBuildType(job.buildType),
|
||||
developmentClient: job.developmentClient,
|
||||
};
|
||||
}
|
||||
|
||||
function transformBuildType(buildType: Android.BuildType): AndroidBuildType {
|
||||
if (buildType === Android.BuildType.APK) {
|
||||
return AndroidBuildType.Apk;
|
||||
} else {
|
||||
return AndroidBuildType.AppBundle;
|
||||
}
|
||||
}
|
||||
@@ -1,58 +1,34 @@
|
||||
import {
|
||||
Android,
|
||||
ArchiveSource,
|
||||
ArchiveSourceType,
|
||||
Job,
|
||||
Workflow,
|
||||
sanitizeJob,
|
||||
} from '@expo/eas-build-job';
|
||||
import { AndroidGenericBuildProfile, AndroidManagedBuildProfile } from '@expo/eas-json';
|
||||
import { Android, ArchiveSource, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import path from 'path';
|
||||
import slash from 'slash';
|
||||
|
||||
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
|
||||
import { readSecretEnvsAsync } from '../../credentials/credentialsJson/read';
|
||||
import { getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import { gitRootDirectoryAsync } from '../../utils/git';
|
||||
import { getUsername } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { BuildContext } from '../context';
|
||||
import { Platform } from '../types';
|
||||
|
||||
interface JobData {
|
||||
archiveBucketKey: string;
|
||||
projectArchive: ArchiveSource;
|
||||
credentials?: AndroidCredentials;
|
||||
}
|
||||
|
||||
const cacheDefaults = {
|
||||
disabled: false,
|
||||
customPaths: [],
|
||||
cacheDefaultPaths: true,
|
||||
};
|
||||
|
||||
export async function prepareJobAsync(
|
||||
ctx: BuildContext<Platform.Android>,
|
||||
ctx: BuildContext<Platform.ANDROID>,
|
||||
jobData: JobData
|
||||
): Promise<Job> {
|
||||
if (ctx.buildProfile.workflow === Workflow.Generic) {
|
||||
const partialJob = await prepareGenericJobAsync(ctx, jobData, ctx.buildProfile);
|
||||
return sanitizeJob(partialJob);
|
||||
} else if (ctx.buildProfile.workflow === Workflow.Managed) {
|
||||
const partialJob = await prepareManagedJobAsync(ctx, jobData, ctx.buildProfile);
|
||||
return sanitizeJob(partialJob);
|
||||
} else {
|
||||
throw new Error("Unknown workflow. Shouldn't happen");
|
||||
}
|
||||
}
|
||||
|
||||
interface CommonJobProperties {
|
||||
platform: Platform.Android;
|
||||
projectArchive: ArchiveSource;
|
||||
secrets: {
|
||||
buildCredentials?: {
|
||||
keystore: Android.Keystore;
|
||||
};
|
||||
secretEnvs?: Record<string, string>;
|
||||
};
|
||||
}
|
||||
|
||||
async function prepareJobCommonAsync(
|
||||
ctx: BuildContext<Platform.Android>,
|
||||
jobData: JobData
|
||||
): Promise<Partial<CommonJobProperties>> {
|
||||
const secretEnvs = await readSecretEnvsAsync(ctx.commandCtx.projectDir);
|
||||
const credentials = jobData.credentials;
|
||||
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
|
||||
const buildProfile: BuildProfile<Platform.ANDROID> = ctx.buildProfile;
|
||||
const projectRootDirectory =
|
||||
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
|
||||
const { credentials } = jobData;
|
||||
const buildCredentials = credentials
|
||||
? {
|
||||
buildCredentials: {
|
||||
@@ -66,48 +42,40 @@ async function prepareJobCommonAsync(
|
||||
}
|
||||
: {};
|
||||
|
||||
return {
|
||||
platform: Platform.Android,
|
||||
projectArchive: {
|
||||
type: ArchiveSourceType.S3,
|
||||
bucketKey: jobData.archiveBucketKey,
|
||||
let buildType = ctx.buildProfile.buildType;
|
||||
if (!buildType && !buildProfile.gradleCommand && ctx.buildProfile.distribution === 'internal') {
|
||||
buildType = Android.BuildType.APK;
|
||||
}
|
||||
|
||||
const job: Android.Job = {
|
||||
type: ctx.workflow,
|
||||
platform: Platform.ANDROID,
|
||||
projectRootDirectory,
|
||||
projectArchive: jobData.projectArchive,
|
||||
builderEnvironment: {
|
||||
image: buildProfile.image,
|
||||
node: buildProfile.node,
|
||||
yarn: buildProfile.yarn,
|
||||
ndk: buildProfile.ndk,
|
||||
expoCli: buildProfile.expoCli,
|
||||
env: buildProfile.env,
|
||||
},
|
||||
cache: {
|
||||
...cacheDefaults,
|
||||
...buildProfile.cache,
|
||||
clear: ctx.clearCache,
|
||||
},
|
||||
secrets: {
|
||||
...(secretEnvs ? { secretEnvs } : {}),
|
||||
...buildCredentials,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function prepareGenericJobAsync(
|
||||
ctx: BuildContext<Platform.Android>,
|
||||
jobData: JobData,
|
||||
buildProfile: AndroidGenericBuildProfile
|
||||
): Promise<Partial<Android.GenericJob>> {
|
||||
const projectRootDirectory = path.relative(await gitRootDirectoryAsync(), process.cwd()) || '.';
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, jobData)),
|
||||
type: Workflow.Generic,
|
||||
releaseChannel: ctx.buildProfile.releaseChannel,
|
||||
updates: { channel: ctx.buildProfile.channel },
|
||||
developmentClient: buildProfile.developmentClient,
|
||||
gradleCommand: buildProfile.gradleCommand,
|
||||
artifactPath: buildProfile.artifactPath,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
projectRootDirectory,
|
||||
buildType,
|
||||
username,
|
||||
};
|
||||
}
|
||||
|
||||
async function prepareManagedJobAsync(
|
||||
ctx: BuildContext<Platform.Android>,
|
||||
jobData: JobData,
|
||||
buildProfile: AndroidManagedBuildProfile
|
||||
): Promise<Partial<Android.ManagedJob>> {
|
||||
const projectRootDirectory = path.relative(await gitRootDirectoryAsync(), process.cwd()) || '.';
|
||||
const accountName = await getProjectAccountNameAsync(ctx.commandCtx.projectDir);
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, jobData)),
|
||||
type: Workflow.Managed,
|
||||
username: accountName,
|
||||
buildType: buildProfile.buildType,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
projectRootDirectory,
|
||||
};
|
||||
return sanitizeJob(job);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AndroidConfig } from '@expo/config-plugins';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { AndroidVersionAutoIncrement, BuildProfile } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import Log from '../../log';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { isExpoUpdatesInstalled } from '../utils/updates';
|
||||
import { syncUpdatesConfigurationAsync } from './UpdatesModule';
|
||||
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
|
||||
|
||||
export async function syncProjectConfigurationAsync({
|
||||
projectDir,
|
||||
exp,
|
||||
buildProfile,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
buildProfile: BuildProfile<Platform.ANDROID>;
|
||||
}): Promise<void> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
|
||||
const { autoIncrement } = buildProfile;
|
||||
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement ?? false);
|
||||
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
await cleanUpOldEasBuildGradleScriptAsync(projectDir);
|
||||
if (isExpoUpdatesInstalled(projectDir)) {
|
||||
await syncUpdatesConfigurationAsync(projectDir, exp);
|
||||
}
|
||||
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
|
||||
} else {
|
||||
await bumpVersionInAppJsonAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
|
||||
}
|
||||
}
|
||||
|
||||
function resolveVersionBumpStrategy(autoIncrement: AndroidVersionAutoIncrement): BumpStrategy {
|
||||
if (autoIncrement === true) {
|
||||
return BumpStrategy.VERSION_CODE;
|
||||
} else if (autoIncrement === false) {
|
||||
return BumpStrategy.NOOP;
|
||||
} else if (autoIncrement === 'versionCode') {
|
||||
return BumpStrategy.VERSION_CODE;
|
||||
} else {
|
||||
return BumpStrategy.APP_VERSION;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove this after a few months
|
||||
export async function cleanUpOldEasBuildGradleScriptAsync(projectDir: string): Promise<void> {
|
||||
const easBuildGradlePath = path.join(projectDir, 'android', 'app', 'eas-build.gradle');
|
||||
if (await fs.pathExists(easBuildGradlePath)) {
|
||||
Log.withTick(`Removing ${chalk.bold('eas-build.gradle')} as it's not longer necessary`);
|
||||
await fs.remove(easBuildGradlePath);
|
||||
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
|
||||
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf-8');
|
||||
const buildGradleContentsWithoutApply = buildGradleContents.replace(
|
||||
/apply from: ["'].\/eas-build.gradle["']\n/,
|
||||
''
|
||||
);
|
||||
if (buildGradleContentsWithoutApply !== buildGradleContents) {
|
||||
await fs.writeFile(buildGradlePath, buildGradleContentsWithoutApply);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AndroidConfig } from '@expo/config-plugins';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import Log from '../../log';
|
||||
import {
|
||||
getAppBuildGradleAsync,
|
||||
parseGradleCommand,
|
||||
resolveConfigValue,
|
||||
} from '../../project/android/gradleUtils';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { updateAppJsonConfigAsync } from '../utils/appJson';
|
||||
import { bumpAppVersionAsync, ensureStaticConfigExists } from '../utils/version';
|
||||
|
||||
export enum BumpStrategy {
|
||||
APP_VERSION,
|
||||
VERSION_CODE,
|
||||
NOOP,
|
||||
}
|
||||
|
||||
export async function bumpVersionAsync({
|
||||
bumpStrategy,
|
||||
projectDir,
|
||||
exp,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
bumpStrategy: BumpStrategy;
|
||||
}): Promise<void> {
|
||||
if (bumpStrategy === BumpStrategy.NOOP) {
|
||||
return;
|
||||
}
|
||||
ensureStaticConfigExists(projectDir);
|
||||
|
||||
const buildGradle = await getAppBuildGradleAsync(projectDir);
|
||||
const isMultiFlavor =
|
||||
buildGradle.android?.productFlavors || buildGradle.android?.flavorDimensions;
|
||||
if (isMultiFlavor) {
|
||||
throw new Error(
|
||||
'Automatic version bumping is not supported for multi-flavor Android projects.'
|
||||
);
|
||||
}
|
||||
|
||||
await bumpVersionInAppJsonAsync({ bumpStrategy, projectDir, exp });
|
||||
Log.log('Updated versions in app.json');
|
||||
await writeVersionsToBuildGradleAsync({
|
||||
projectDir,
|
||||
exp,
|
||||
});
|
||||
Log.log('Synchronized versions with build gradle');
|
||||
}
|
||||
|
||||
export async function bumpVersionInAppJsonAsync({
|
||||
bumpStrategy,
|
||||
projectDir,
|
||||
exp,
|
||||
}: {
|
||||
bumpStrategy: BumpStrategy;
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
}): Promise<void> {
|
||||
if (bumpStrategy === BumpStrategy.NOOP) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureStaticConfigExists(projectDir);
|
||||
Log.addNewLineIfNone();
|
||||
|
||||
if (bumpStrategy === BumpStrategy.APP_VERSION) {
|
||||
const appVersion = AndroidConfig.Version.getVersionName(exp) ?? '1.0.0';
|
||||
await bumpAppVersionAsync({ appVersion, projectDir, exp });
|
||||
} else {
|
||||
const versionCode = AndroidConfig.Version.getVersionCode(exp);
|
||||
const bumpedVersionCode = versionCode + 1;
|
||||
Log.log(
|
||||
`Bumping ${chalk.bold('expo.android.versionCode')} from ${chalk.bold(
|
||||
versionCode
|
||||
)} to ${chalk.bold(bumpedVersionCode)}`
|
||||
);
|
||||
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
|
||||
config.android = { ...config.android, versionCode: bumpedVersionCode };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function maybeResolveVersionsAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
buildProfile: BuildProfile<Platform.ANDROID>
|
||||
): Promise<{ appVersion?: string; appBuildVersion?: string }> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
const buildGradle = await getAppBuildGradleAsync(projectDir);
|
||||
try {
|
||||
const parsedGradleCommand = buildProfile.gradleCommand
|
||||
? parseGradleCommand(buildProfile.gradleCommand, buildGradle)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
appVersion:
|
||||
resolveConfigValue(buildGradle, 'versionName', parsedGradleCommand?.flavor) ?? '1.0.0',
|
||||
appBuildVersion:
|
||||
resolveConfigValue(buildGradle, 'versionCode', parsedGradleCommand?.flavor) ?? '1',
|
||||
};
|
||||
} catch (err: any) {
|
||||
return {};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
appBuildVersion: String(AndroidConfig.Version.getVersionCode(exp)),
|
||||
appVersion: exp.version,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function writeVersionsToBuildGradleAsync({
|
||||
projectDir,
|
||||
exp,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
}): Promise<string> {
|
||||
const buildGradle = await readBuildGradleAsync(projectDir);
|
||||
if (!buildGradle) {
|
||||
throw new Error('This project is missing a build.gradle file.');
|
||||
}
|
||||
let updatedBuildGradle = AndroidConfig.Version.setVersionName(exp, buildGradle);
|
||||
updatedBuildGradle = AndroidConfig.Version.setVersionCode(exp, updatedBuildGradle);
|
||||
await writeBuildGradleAsync({ projectDir, buildGradle: updatedBuildGradle });
|
||||
return updatedBuildGradle;
|
||||
}
|
||||
|
||||
async function readBuildGradleAsync(projectDir: string): Promise<string | undefined> {
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
|
||||
if (!(await fs.pathExists(buildGradlePath))) {
|
||||
return undefined;
|
||||
}
|
||||
return await fs.readFile(buildGradlePath, 'utf8');
|
||||
}
|
||||
|
||||
async function writeBuildGradleAsync({
|
||||
projectDir,
|
||||
buildGradle,
|
||||
}: {
|
||||
projectDir: string;
|
||||
buildGradle: string;
|
||||
}): Promise<void> {
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
|
||||
await fs.writeFile(buildGradlePath, buildGradle);
|
||||
}
|
||||
+239
-148
@@ -1,121 +1,138 @@
|
||||
import { Job } from '@expo/eas-build-job';
|
||||
import { ArchiveSource, ArchiveSourceType, Job, Metadata, Platform } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { apiClient } from '../api';
|
||||
import log from '../log';
|
||||
import { promptAsync } from '../prompts';
|
||||
import { UploadType, uploadAsync } from '../uploads';
|
||||
import { withAnalyticsAsync } from '../analytics/common';
|
||||
import { BuildEvent } from '../analytics/events';
|
||||
import { BuildFragment, BuildStatus, UploadSessionType } from '../graphql/generated';
|
||||
import { BuildResult } from '../graphql/mutations/BuildMutation';
|
||||
import { BuildQuery } from '../graphql/queries/BuildQuery';
|
||||
import Log, { learnMore } from '../log';
|
||||
import { ora } from '../ora';
|
||||
import { requestedPlatformDisplayNames } from '../platform';
|
||||
import { uploadAsync } from '../uploads';
|
||||
import { formatBytes } from '../utils/files';
|
||||
import { createProgressTracker } from '../utils/progress';
|
||||
import { platformDisplayNames } from './constants';
|
||||
import { sleepAsync } from '../utils/promise';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { BuildContext } from './context';
|
||||
import { collectMetadata } from './metadata';
|
||||
import { AnalyticsEvent, Platform, TrackingContext } from './types';
|
||||
import Analytics from './utils/analytics';
|
||||
import { runLocalBuildAsync } from './local';
|
||||
import { collectMetadataAsync } from './metadata';
|
||||
import { printDeprecationWarnings } from './utils/printBuildInfo';
|
||||
import {
|
||||
commitPromptAsync,
|
||||
isGitStatusCleanAsync,
|
||||
makeProjectTarballAsync,
|
||||
showDiffAsync,
|
||||
} from './utils/repository';
|
||||
import { makeProjectTarballAsync, reviewAndCommitChangesAsync } from './utils/repository';
|
||||
|
||||
export interface CredentialsResult<Credentials> {
|
||||
source: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
|
||||
credentials: Credentials;
|
||||
}
|
||||
|
||||
interface Builder<TPlatform extends Platform, Credentials, ProjectConfiguration> {
|
||||
export interface JobData<Credentials> {
|
||||
credentials?: Credentials;
|
||||
projectArchive: ArchiveSource;
|
||||
}
|
||||
|
||||
interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
|
||||
ctx: BuildContext<TPlatform>;
|
||||
projectConfiguration: ProjectConfiguration;
|
||||
|
||||
ensureCredentialsAsync(
|
||||
ctx: BuildContext<TPlatform>
|
||||
): Promise<CredentialsResult<Credentials> | undefined>;
|
||||
ensureProjectConfiguredAsync(ctx: BuildContext<TPlatform>): Promise<void>;
|
||||
prepareJobAsync(
|
||||
ctx: BuildContext<TPlatform>,
|
||||
jobData: {
|
||||
archiveBucketKey: string;
|
||||
credentials?: Credentials;
|
||||
projectConfiguration?: ProjectConfiguration;
|
||||
}
|
||||
): Promise<Job>;
|
||||
syncProjectConfigurationAsync(ctx: BuildContext<TPlatform>): Promise<void>;
|
||||
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<Job>;
|
||||
sendBuildRequestAsync(appId: string, job: TJob, metadata: Metadata): Promise<BuildResult>;
|
||||
}
|
||||
|
||||
export type BuildRequestSender = () => Promise<BuildFragment | undefined>;
|
||||
|
||||
export async function prepareBuildRequestForPlatformAsync<
|
||||
TPlatform extends Platform,
|
||||
Credentials,
|
||||
ProjectConfiguration
|
||||
>(builder: Builder<TPlatform, Credentials, ProjectConfiguration>): Promise<() => Promise<string>> {
|
||||
TJob extends Job
|
||||
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<BuildRequestSender> {
|
||||
const { ctx } = builder;
|
||||
const credentialsResult = await withAnalyticsAsync(
|
||||
async () => await builder.ensureCredentialsAsync(builder.ctx),
|
||||
async () => await builder.ensureCredentialsAsync(ctx),
|
||||
{
|
||||
successEvent: AnalyticsEvent.GATHER_CREDENTIALS_SUCCESS,
|
||||
failureEvent: AnalyticsEvent.GATHER_CREDENTIALS_FAIL,
|
||||
trackingCtx: builder.ctx.trackingCtx,
|
||||
attemptEvent: BuildEvent.GATHER_CREDENTIALS_ATTEMPT,
|
||||
successEvent: BuildEvent.GATHER_CREDENTIALS_SUCCESS,
|
||||
failureEvent: BuildEvent.GATHER_CREDENTIALS_FAIL,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
}
|
||||
);
|
||||
if (!builder.ctx.commandCtx.skipProjectConfiguration) {
|
||||
await withAnalyticsAsync(async () => await builder.ensureProjectConfiguredAsync(builder.ctx), {
|
||||
successEvent: AnalyticsEvent.CONFIGURE_PROJECT_SUCCESS,
|
||||
failureEvent: AnalyticsEvent.CONFIGURE_PROJECT_FAIL,
|
||||
trackingCtx: builder.ctx.trackingCtx,
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await isGitStatusCleanAsync())) {
|
||||
log.addNewLineIfNone();
|
||||
const projectType = builder.ctx.platform === Platform.Android ? 'Android' : 'Xcode';
|
||||
// Currently we are only updaing runtime version durring build, but if it changes in a future
|
||||
// this message should also contain more info on that (or be more generic)
|
||||
await reviewAndCommitChangesAsync(`Update runtime version in the ${projectType} project`, {
|
||||
nonInteractive: builder.ctx.commandCtx.nonInteractive,
|
||||
});
|
||||
}
|
||||
|
||||
const archiveBucketKey = await uploadProjectAsync(builder.ctx);
|
||||
|
||||
const metadata = await collectMetadata(builder.ctx, {
|
||||
credentialsSource: credentialsResult?.source,
|
||||
await withAnalyticsAsync(async () => await builder.syncProjectConfigurationAsync(ctx), {
|
||||
attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT,
|
||||
successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS,
|
||||
failureEvent: BuildEvent.CONFIGURE_PROJECT_FAIL,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
});
|
||||
const job = await builder.prepareJobAsync(builder.ctx, {
|
||||
archiveBucketKey,
|
||||
|
||||
if (await getVcsClient().isCommitRequiredAsync()) {
|
||||
Log.addNewLineIfNone();
|
||||
await reviewAndCommitChangesAsync(
|
||||
`[EAS Build] Run EAS Build for ${requestedPlatformDisplayNames[ctx.platform as Platform]}`,
|
||||
{ nonInteractive: ctx.nonInteractive }
|
||||
);
|
||||
}
|
||||
|
||||
const projectArchive = ctx.localBuildOptions.enable
|
||||
? ({
|
||||
type: ArchiveSourceType.PATH,
|
||||
path: (await makeProjectTarballAsync()).path,
|
||||
} as const)
|
||||
: ({
|
||||
type: ArchiveSourceType.S3,
|
||||
bucketKey: await uploadProjectAsync(ctx),
|
||||
} as const);
|
||||
|
||||
const metadata = await collectMetadataAsync(ctx);
|
||||
const job = await builder.prepareJobAsync(ctx, {
|
||||
projectArchive,
|
||||
credentials: credentialsResult?.credentials,
|
||||
projectConfiguration: builder.projectConfiguration,
|
||||
});
|
||||
|
||||
return async () => {
|
||||
try {
|
||||
return await withAnalyticsAsync(
|
||||
async () => {
|
||||
if (log.isDebug) {
|
||||
log(`Starting ${platformDisplayNames[job.platform]} build`);
|
||||
}
|
||||
const {
|
||||
data: { buildId, deprecationInfo },
|
||||
} = await apiClient
|
||||
.post(`projects/${builder.ctx.commandCtx.projectId}/builds`, {
|
||||
json: {
|
||||
job,
|
||||
metadata,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
printDeprecationWarnings(deprecationInfo);
|
||||
return buildId;
|
||||
},
|
||||
{
|
||||
successEvent: AnalyticsEvent.BUILD_REQUEST_SUCCESS,
|
||||
failureEvent: AnalyticsEvent.BUILD_REQUEST_FAIL,
|
||||
trackingCtx: builder.ctx.trackingCtx,
|
||||
if (ctx.localBuildOptions.enable) {
|
||||
await runLocalBuildAsync(job, ctx.localBuildOptions);
|
||||
return undefined;
|
||||
} else {
|
||||
try {
|
||||
return await sendBuildRequestAsync(builder, job, metadata);
|
||||
} catch (error: any) {
|
||||
if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'TURTLE_DEPRECATED_JOB_FORMAT') {
|
||||
Log.error('EAS Build API has changed, please upgrade to the latest eas-cli version.');
|
||||
throw new Error('Build request failed.');
|
||||
} else if (
|
||||
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_DOWN_FOR_MAINTENANCE'
|
||||
) {
|
||||
Log.error(
|
||||
'EAS Build is down for maintenance, please try again later. Check https://status.expo.dev/ for updates.'
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (
|
||||
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_FREE_TIER_DISABLED'
|
||||
) {
|
||||
Log.error(
|
||||
'EAS Build free tier is temporarily disabled, please try again later. Check https://status.expo.dev/ for updates.'
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (
|
||||
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_TOO_MANY_PENDING_BUILDS'
|
||||
) {
|
||||
Log.error(
|
||||
`You have already reached the maximum number of pending ${
|
||||
requestedPlatformDisplayNames[job.platform]
|
||||
} builds for your account. Try again later.`
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (error?.graphQLErrors) {
|
||||
Log.error(
|
||||
'Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, please report the issue.'
|
||||
);
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
if (error.code === 'TURTLE_DEPRECATED_JOB_FORMAT') {
|
||||
log.error('EAS Build API changed, upgrade to latest expo-cli');
|
||||
throw error;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -131,19 +148,26 @@ async function uploadProjectAsync<TPlatform extends Platform>(
|
||||
projectTarballPath = projectTarball.path;
|
||||
|
||||
const { bucketKey } = await uploadAsync(
|
||||
UploadType.TURTLE_PROJECT_SOURCES,
|
||||
UploadSessionType.EasBuildProjectSources,
|
||||
projectTarball.path,
|
||||
createProgressTracker({
|
||||
total: projectTarball.size,
|
||||
message: 'Uploading to EAS Build',
|
||||
completedMessage: `Uploaded to EAS`,
|
||||
message: ratio =>
|
||||
`Uploading to EAS Build (${formatBytes(projectTarball.size * ratio)} / ${formatBytes(
|
||||
projectTarball.size
|
||||
)})`,
|
||||
completedMessage: (duration: string) =>
|
||||
`Uploaded to EAS ${chalk.dim(duration)} ${learnMore(
|
||||
'https://expo.fyi/eas-build-archive'
|
||||
)}`,
|
||||
})
|
||||
);
|
||||
return bucketKey;
|
||||
},
|
||||
{
|
||||
successEvent: AnalyticsEvent.PROJECT_UPLOAD_SUCCESS,
|
||||
failureEvent: AnalyticsEvent.PROJECT_UPLOAD_FAIL,
|
||||
attemptEvent: BuildEvent.PROJECT_UPLOAD_ATTEMPT,
|
||||
successEvent: BuildEvent.PROJECT_UPLOAD_SUCCESS,
|
||||
failureEvent: BuildEvent.PROJECT_UPLOAD_FAIL,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
}
|
||||
);
|
||||
@@ -154,67 +178,134 @@ async function uploadProjectAsync<TPlatform extends Platform>(
|
||||
}
|
||||
}
|
||||
|
||||
async function withAnalyticsAsync<Result>(
|
||||
fn: () => Promise<Result>,
|
||||
analytics: {
|
||||
successEvent: AnalyticsEvent;
|
||||
failureEvent: AnalyticsEvent;
|
||||
trackingCtx: TrackingContext;
|
||||
}
|
||||
): Promise<Result> {
|
||||
try {
|
||||
const result = await fn();
|
||||
Analytics.logEvent(analytics.successEvent, analytics.trackingCtx);
|
||||
return result;
|
||||
} catch (error) {
|
||||
Analytics.logEvent(analytics.failureEvent, {
|
||||
...analytics.trackingCtx,
|
||||
reason: error.message,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJob extends Job>(
|
||||
builder: Builder<TPlatform, Credentials, TJob>,
|
||||
job: TJob,
|
||||
metadata: Metadata
|
||||
): Promise<BuildFragment> {
|
||||
const { ctx } = builder;
|
||||
return await withAnalyticsAsync(
|
||||
async () => {
|
||||
if (Log.isDebug) {
|
||||
Log.log(`Starting ${requestedPlatformDisplayNames[job.platform]} build`);
|
||||
}
|
||||
|
||||
const { build, deprecationInfo } = await builder.sendBuildRequestAsync(
|
||||
ctx.projectId,
|
||||
job,
|
||||
metadata
|
||||
);
|
||||
|
||||
printDeprecationWarnings(deprecationInfo);
|
||||
return build;
|
||||
},
|
||||
{
|
||||
attemptEvent: BuildEvent.BUILD_REQUEST_ATTEMPT,
|
||||
successEvent: BuildEvent.BUILD_REQUEST_SUCCESS,
|
||||
failureEvent: BuildEvent.BUILD_REQUEST_FAIL,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
enum ShouldCommitChanges {
|
||||
Yes,
|
||||
ShowDiffFirst,
|
||||
Abort,
|
||||
}
|
||||
|
||||
async function reviewAndCommitChangesAsync(
|
||||
commitMessage: string,
|
||||
{ nonInteractive, askedFirstTime = true }: { nonInteractive: boolean; askedFirstTime?: boolean }
|
||||
): Promise<void> {
|
||||
if (nonInteractive) {
|
||||
throw new Error(
|
||||
'Cannot commit changes when --non-interactive is specified. Run the command in interactive mode to review and commit changes.'
|
||||
export async function waitForBuildEndAsync(
|
||||
buildIds: string[],
|
||||
{ timeoutSec = 3600, intervalSec = 30 } = {}
|
||||
): Promise<(BuildFragment | null)[]> {
|
||||
Log.log(
|
||||
`Waiting for build${buildIds.length > 1 ? 's' : ''} to complete. You can press Ctrl+C to exit.`
|
||||
);
|
||||
const spinner = ora().start();
|
||||
let time = new Date().getTime();
|
||||
const endTime = time + timeoutSec * 1000;
|
||||
while (time <= endTime) {
|
||||
const builds: (BuildFragment | null)[] = await Promise.all(
|
||||
buildIds.map(async buildId => {
|
||||
try {
|
||||
return await BuildQuery.byIdAsync(buildId, { useCache: false });
|
||||
} catch (err) {
|
||||
Log.debug('Failed to fetch the build status', err);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
if (builds.length === 1) {
|
||||
const [build] = builds;
|
||||
if (build !== null) {
|
||||
switch (build.status) {
|
||||
case BuildStatus.Finished:
|
||||
spinner.succeed('Build finished');
|
||||
return builds;
|
||||
case BuildStatus.New:
|
||||
spinner.text = 'Build created';
|
||||
break;
|
||||
case BuildStatus.InQueue:
|
||||
spinner.text = 'Build queued...';
|
||||
break;
|
||||
case BuildStatus.Canceled:
|
||||
spinner.text = 'Build canceled';
|
||||
spinner.stopAndPersist();
|
||||
return builds;
|
||||
case BuildStatus.InProgress:
|
||||
spinner.text = 'Build in progress...';
|
||||
break;
|
||||
case BuildStatus.Errored:
|
||||
spinner.fail('Build failed');
|
||||
if (build.error) {
|
||||
return builds;
|
||||
} else {
|
||||
throw new Error(`Standalone build failed!`);
|
||||
}
|
||||
default:
|
||||
spinner.warn('Unknown status');
|
||||
throw new Error(`Unknown build status: ${build.status} - aborting!`);
|
||||
}
|
||||
} else {
|
||||
if (!spinner.text) {
|
||||
spinner.text = 'Could not fetch the build status. Check your network connection.';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (builds.filter(build => build?.status === BuildStatus.Finished).length === builds.length) {
|
||||
spinner.succeed('All builds have finished');
|
||||
return builds;
|
||||
} else if (
|
||||
builds.filter(build =>
|
||||
build?.status
|
||||
? [BuildStatus.Finished, BuildStatus.Errored, BuildStatus.Canceled].includes(
|
||||
build.status
|
||||
)
|
||||
: false
|
||||
).length === builds.length
|
||||
) {
|
||||
spinner.fail('Some of the builds were canceled or failed.');
|
||||
return builds;
|
||||
} else {
|
||||
const newBuilds = builds.filter(build => build?.status === BuildStatus.New).length;
|
||||
const inQueue = builds.filter(build => build?.status === BuildStatus.InQueue).length;
|
||||
const inProgress = builds.filter(build => build?.status === BuildStatus.InProgress).length;
|
||||
const errored = builds.filter(build => build?.status === BuildStatus.Errored).length;
|
||||
const finished = builds.filter(build => build?.status === BuildStatus.Finished).length;
|
||||
const canceled = builds.filter(build => build?.status === BuildStatus.Canceled).length;
|
||||
const unknown = builds.length - newBuilds - inQueue - inProgress - errored - finished;
|
||||
spinner.text = [
|
||||
newBuilds && `Builds created: ${newBuilds}`,
|
||||
inQueue && `Builds in queue: ${inQueue}`,
|
||||
inProgress && `Builds in progress: ${inProgress}`,
|
||||
canceled && `Builds canceled: ${canceled}`,
|
||||
errored && chalk.red(`Builds failed: ${errored}`),
|
||||
finished && chalk.green(`Builds finished: ${finished}`),
|
||||
unknown && chalk.red(`Builds with unknown status: ${unknown}`),
|
||||
]
|
||||
.filter(i => i)
|
||||
.join('\t');
|
||||
}
|
||||
}
|
||||
time = new Date().getTime();
|
||||
await sleepAsync(intervalSec * 1000);
|
||||
}
|
||||
const { selected } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'selected',
|
||||
message: 'Can we commit these changes to git for you?',
|
||||
choices: [
|
||||
{ title: 'Yes', value: ShouldCommitChanges.Yes },
|
||||
...(askedFirstTime
|
||||
? [{ title: 'Show the diff and ask me again', value: ShouldCommitChanges.ShowDiffFirst }]
|
||||
: []),
|
||||
{
|
||||
title: 'Abort build process',
|
||||
value: ShouldCommitChanges.Abort,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (selected === ShouldCommitChanges.Abort) {
|
||||
throw new Error(
|
||||
"Aborting, run the command again once you're ready. Make sure to commit any changes you've made."
|
||||
);
|
||||
} else if (selected === ShouldCommitChanges.Yes) {
|
||||
await commitPromptAsync(commitMessage);
|
||||
log.withTick('Committed changes.');
|
||||
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
|
||||
await showDiffAsync();
|
||||
await reviewAndCommitChangesAsync(commitMessage, { nonInteractive, askedFirstTime: false });
|
||||
}
|
||||
spinner.warn('Timed out');
|
||||
throw new Error(
|
||||
'Timeout reached! It is taking longer than expected to finish the build, aborting...'
|
||||
);
|
||||
}
|
||||
@@ -1,161 +1,106 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { EasJsonReader } from '@expo/eas-json';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { EasJson, EasJsonReader } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import log from '../log';
|
||||
import { promptAsync } from '../prompts';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { gitAddAsync } from '../utils/git';
|
||||
import { configureAndroidAsync } from './android/configure';
|
||||
import { ConfigureContext } from './context';
|
||||
import { configureIosAsync } from './ios/configure';
|
||||
import { RequestedPlatform } from './types';
|
||||
import {
|
||||
commitPromptAsync,
|
||||
ensureGitRepoExistsAsync,
|
||||
isGitStatusCleanAsync,
|
||||
maybeBailOnGitStatusAsync,
|
||||
showDiffAsync,
|
||||
} from './utils/repository';
|
||||
import Log from '../log';
|
||||
import { resolveWorkflowAsync } from '../project/workflow';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { maybeBailOnRepoStatusAsync, reviewAndCommitChangesAsync } from './utils/repository';
|
||||
|
||||
const configureCommitMessage = {
|
||||
[RequestedPlatform.Android]: 'Configure EAS Build for Android',
|
||||
[RequestedPlatform.iOS]: 'Configure EAS Build for iOS',
|
||||
[RequestedPlatform.All]: 'Configure EAS Build',
|
||||
interface ConfigureParams {
|
||||
projectDir: string;
|
||||
nonInteractive: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates eas.json if it does not exist.
|
||||
*
|
||||
* Returns:
|
||||
* - false - if eas.json already exists
|
||||
* - true - if eas.json was created by the function
|
||||
*/
|
||||
export async function ensureProjectConfiguredAsync(
|
||||
configureParams: ConfigureParams
|
||||
): Promise<boolean> {
|
||||
if (await fs.pathExists(EasJsonReader.formatEasJsonPath(configureParams.projectDir))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await configureAsync(configureParams);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function configureAsync({ projectDir, nonInteractive }: ConfigureParams): Promise<void> {
|
||||
await maybeBailOnRepoStatusAsync();
|
||||
|
||||
await createEasJsonAsync(projectDir);
|
||||
|
||||
if (await getVcsClient().isCommitRequiredAsync()) {
|
||||
Log.newLine();
|
||||
await reviewAndCommitChangesAsync('Configure EAS Build', {
|
||||
nonInteractive,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const EAS_JSON_MANAGED_DEFAULT: EasJson = {
|
||||
cli: {
|
||||
version: `>= ${easCliVersion}`,
|
||||
},
|
||||
build: {
|
||||
development: {
|
||||
developmentClient: true,
|
||||
distribution: 'internal',
|
||||
},
|
||||
preview: {
|
||||
distribution: 'internal',
|
||||
},
|
||||
production: {},
|
||||
},
|
||||
submit: {
|
||||
production: {},
|
||||
},
|
||||
};
|
||||
|
||||
export async function configureAsync(options: {
|
||||
platform: RequestedPlatform;
|
||||
projectDir: string;
|
||||
allowExperimental: boolean;
|
||||
}): Promise<void> {
|
||||
await ensureGitRepoExistsAsync();
|
||||
await maybeBailOnGitStatusAsync();
|
||||
|
||||
const { exp } = getConfig(options.projectDir, { skipSDKVersionRequirement: true });
|
||||
|
||||
const ctx: ConfigureContext = {
|
||||
user: await ensureLoggedInAsync(),
|
||||
projectDir: options.projectDir,
|
||||
exp,
|
||||
allowExperimental: options.allowExperimental,
|
||||
requestedPlatform: options.platform,
|
||||
shouldConfigureAndroid: [RequestedPlatform.All, RequestedPlatform.Android].includes(
|
||||
options.platform
|
||||
),
|
||||
shouldConfigureIos: [RequestedPlatform.All, RequestedPlatform.iOS].includes(options.platform),
|
||||
hasAndroidNativeProject: await fs.pathExists(path.join(options.projectDir, 'android')),
|
||||
hasIosNativeProject: await fs.pathExists(path.join(options.projectDir, 'ios')),
|
||||
};
|
||||
|
||||
log.newLine();
|
||||
await ensureEasJsonExistsAsync(ctx);
|
||||
if (ctx.shouldConfigureAndroid) {
|
||||
await configureAndroidAsync(ctx);
|
||||
}
|
||||
if (ctx.shouldConfigureIos) {
|
||||
await configureIosAsync(ctx);
|
||||
}
|
||||
|
||||
if (!(await isGitStatusCleanAsync())) {
|
||||
log.newLine();
|
||||
await reviewAndCommitChangesAsync(configureCommitMessage[options.platform]);
|
||||
} else {
|
||||
log.newLine();
|
||||
log.withTick('No changes were necessary, the project is already configured correctly.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<void> {
|
||||
const easJsonPath = path.join(ctx.projectDir, 'eas.json');
|
||||
let existingEasJson;
|
||||
|
||||
if (await fs.pathExists(easJsonPath)) {
|
||||
const reader = new EasJsonReader(ctx.projectDir, ctx.requestedPlatform);
|
||||
await reader.validateAsync();
|
||||
|
||||
existingEasJson = await reader.readRawAsync();
|
||||
log.withTick('Validated eas.json');
|
||||
|
||||
// If we have already populated eas.json with the default fields for the
|
||||
// platform then proceed
|
||||
if (ctx.requestedPlatform !== 'all' && existingEasJson.builds[ctx.requestedPlatform]) {
|
||||
return;
|
||||
} else if (
|
||||
ctx.requestedPlatform === 'all' &&
|
||||
existingEasJson.builds.ios &&
|
||||
existingEasJson.builds.android
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const shouldInitIOS =
|
||||
['all', 'ios'].includes(ctx.requestedPlatform) && !existingEasJson?.builds.ios;
|
||||
const shouldInitAndroid =
|
||||
['all', 'android'].includes(ctx.requestedPlatform) && !existingEasJson?.builds.android;
|
||||
|
||||
const easJson = {
|
||||
builds: {
|
||||
...existingEasJson,
|
||||
...(shouldInitAndroid
|
||||
? {
|
||||
android: {
|
||||
release: {
|
||||
workflow: ctx.hasAndroidNativeProject ? 'generic' : 'managed',
|
||||
},
|
||||
},
|
||||
}
|
||||
: null),
|
||||
...(shouldInitIOS
|
||||
? {
|
||||
ios: {
|
||||
release: {
|
||||
workflow: ctx.hasIosNativeProject ? 'generic' : 'managed',
|
||||
},
|
||||
},
|
||||
}
|
||||
: null),
|
||||
const EAS_JSON_BARE_DEFAULT: EasJson = {
|
||||
cli: {
|
||||
version: `>= ${easCliVersion}`,
|
||||
},
|
||||
build: {
|
||||
development: {
|
||||
distribution: 'internal',
|
||||
android: {
|
||||
gradleCommand: ':app:assembleDebug',
|
||||
},
|
||||
ios: {
|
||||
buildConfiguration: 'Debug',
|
||||
},
|
||||
},
|
||||
};
|
||||
preview: {
|
||||
distribution: 'internal',
|
||||
},
|
||||
production: {},
|
||||
},
|
||||
submit: {
|
||||
production: {},
|
||||
},
|
||||
};
|
||||
|
||||
async function createEasJsonAsync(projectDir: string): Promise<void> {
|
||||
const easJsonPath = EasJsonReader.formatEasJsonPath(projectDir);
|
||||
|
||||
const hasAndroidNativeProject =
|
||||
(await resolveWorkflowAsync(projectDir, Platform.ANDROID)) === Workflow.GENERIC;
|
||||
const hasIosNativeProject =
|
||||
(await resolveWorkflowAsync(projectDir, Platform.IOS)) === Workflow.GENERIC;
|
||||
const easJson =
|
||||
hasAndroidNativeProject || hasIosNativeProject
|
||||
? EAS_JSON_BARE_DEFAULT
|
||||
: EAS_JSON_MANAGED_DEFAULT;
|
||||
|
||||
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
|
||||
await gitAddAsync(easJsonPath, { intentToAdd: true });
|
||||
log.withTick(`${existingEasJson ? 'Updated' : 'Generated'} eas.json`);
|
||||
}
|
||||
|
||||
enum ShouldCommitChanges {
|
||||
Yes,
|
||||
ShowDiffFirst,
|
||||
Skip,
|
||||
}
|
||||
|
||||
async function reviewAndCommitChangesAsync(
|
||||
commitMessage: string,
|
||||
askedFirstTime: boolean = true
|
||||
): Promise<void> {
|
||||
const { selected } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'selected',
|
||||
message: 'Can we commit these changes to git for you?',
|
||||
choices: [
|
||||
{ title: 'Yes', value: ShouldCommitChanges.Yes },
|
||||
...(askedFirstTime
|
||||
? [{ title: 'Show the diff and ask me again', value: ShouldCommitChanges.ShowDiffFirst }]
|
||||
: []),
|
||||
{
|
||||
title: 'Skip committing changes, I will do it later on my own',
|
||||
value: ShouldCommitChanges.Skip,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (selected === ShouldCommitChanges.Yes) {
|
||||
await commitPromptAsync(commitMessage);
|
||||
log.withTick('Committed changes');
|
||||
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
|
||||
await showDiffAsync();
|
||||
await reviewAndCommitChangesAsync(commitMessage, false);
|
||||
}
|
||||
await getVcsClient().trackFileAsync(easJsonPath);
|
||||
Log.withTick(`Generated ${chalk.bold('eas.json')}`);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Platform, RequestedPlatform } from './types';
|
||||
|
||||
export const platformDisplayNames = {
|
||||
[RequestedPlatform.iOS]: 'iOS',
|
||||
[RequestedPlatform.Android]: 'Android',
|
||||
[RequestedPlatform.All]: 'Android and iOS',
|
||||
};
|
||||
|
||||
export const platformEmojis = {
|
||||
[Platform.iOS]: '🍎',
|
||||
[Platform.Android]: '🤖',
|
||||
};
|
||||
@@ -1,114 +1,47 @@
|
||||
import { ExpoConfig, getConfig } from '@expo/config';
|
||||
import { AndroidBuildProfile, EasConfig, iOSBuildProfile } from '@expo/eas-json';
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
|
||||
import { getProjectAccountName } from '../project/projectUtils';
|
||||
import { TrackingContext } from '../analytics/common';
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import { Target } from '../credentials/ios/types';
|
||||
import { GradleBuildContext } from '../project/android/gradle';
|
||||
import { XcodeBuildContext } from '../project/ios/scheme';
|
||||
import { Actor } from '../user/User';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { Platform, RequestedPlatform, TrackingContext } from './types';
|
||||
import { LocalBuildOptions } from './local';
|
||||
|
||||
export interface CommandContext {
|
||||
requestedPlatform: RequestedPlatform;
|
||||
profile: string;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
user: Actor;
|
||||
accountName: string;
|
||||
projectName: string;
|
||||
exp: ExpoConfig;
|
||||
trackingCtx: TrackingContext;
|
||||
nonInteractive: boolean;
|
||||
skipCredentialsCheck: boolean;
|
||||
skipProjectConfiguration: boolean;
|
||||
waitForBuildEnd: boolean;
|
||||
export type CommonContext<T extends Platform> = Omit<BuildContext<T>, 'android' | 'ios'>;
|
||||
|
||||
export interface AndroidBuildContext {
|
||||
applicationId: string;
|
||||
gradleContext?: GradleBuildContext;
|
||||
}
|
||||
|
||||
export async function createCommandContextAsync({
|
||||
requestedPlatform,
|
||||
profile,
|
||||
projectDir,
|
||||
projectId,
|
||||
trackingCtx,
|
||||
nonInteractive = false,
|
||||
skipCredentialsCheck = false,
|
||||
skipProjectConfiguration = false,
|
||||
waitForBuildEnd,
|
||||
}: {
|
||||
requestedPlatform: RequestedPlatform;
|
||||
profile: string;
|
||||
projectId: string;
|
||||
projectDir: string;
|
||||
trackingCtx: TrackingContext;
|
||||
nonInteractive: boolean;
|
||||
skipCredentialsCheck: boolean;
|
||||
skipProjectConfiguration: boolean;
|
||||
waitForBuildEnd: boolean;
|
||||
}): Promise<CommandContext> {
|
||||
const user = await ensureLoggedInAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = getProjectAccountName(exp, user);
|
||||
const projectName = exp.slug;
|
||||
|
||||
return {
|
||||
requestedPlatform,
|
||||
profile,
|
||||
projectDir,
|
||||
projectId,
|
||||
user,
|
||||
accountName,
|
||||
projectName,
|
||||
exp,
|
||||
trackingCtx,
|
||||
nonInteractive,
|
||||
skipCredentialsCheck,
|
||||
skipProjectConfiguration,
|
||||
waitForBuildEnd,
|
||||
};
|
||||
export interface IosBuildContext {
|
||||
bundleIdentifier: string;
|
||||
applicationTargetBuildSettings: XCBuildConfiguration['buildSettings'];
|
||||
applicationTarget: Target;
|
||||
targets: Target[];
|
||||
xcodeBuildContext: XcodeBuildContext;
|
||||
}
|
||||
|
||||
export interface ConfigureContext {
|
||||
user: Actor;
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
allowExperimental: boolean;
|
||||
requestedPlatform: RequestedPlatform;
|
||||
shouldConfigureAndroid: boolean;
|
||||
shouldConfigureIos: boolean;
|
||||
hasAndroidNativeProject: boolean;
|
||||
hasIosNativeProject: boolean;
|
||||
}
|
||||
|
||||
type PlatformBuildProfile<T extends Platform> = T extends Platform.Android
|
||||
? AndroidBuildProfile
|
||||
: iOSBuildProfile;
|
||||
|
||||
export interface BuildContext<T extends Platform> {
|
||||
commandCtx: CommandContext;
|
||||
accountName: string;
|
||||
buildProfile: BuildProfile<T>;
|
||||
buildProfileName: string;
|
||||
clearCache: boolean;
|
||||
credentialsCtx: CredentialsContext;
|
||||
exp: ExpoConfig;
|
||||
localBuildOptions: LocalBuildOptions;
|
||||
nonInteractive: boolean;
|
||||
platform: T;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
trackingCtx: TrackingContext;
|
||||
platform: T;
|
||||
buildProfile: PlatformBuildProfile<T>;
|
||||
}
|
||||
|
||||
export function createBuildContext<T extends Platform>({
|
||||
platform,
|
||||
easConfig,
|
||||
commandCtx,
|
||||
}: {
|
||||
platform: T;
|
||||
easConfig: EasConfig;
|
||||
commandCtx: CommandContext;
|
||||
}): BuildContext<T> {
|
||||
const buildProfile = easConfig.builds[platform] as PlatformBuildProfile<T> | undefined;
|
||||
if (!buildProfile) {
|
||||
throw new Error(`${platform} build profile does not exist`);
|
||||
}
|
||||
const builderTrackingCtx = {
|
||||
...commandCtx.trackingCtx,
|
||||
platform,
|
||||
};
|
||||
return {
|
||||
commandCtx,
|
||||
trackingCtx: builderTrackingCtx,
|
||||
platform,
|
||||
buildProfile,
|
||||
};
|
||||
user: Actor;
|
||||
workflow: Workflow;
|
||||
android: T extends Platform.ANDROID ? AndroidBuildContext : undefined;
|
||||
ios: T extends Platform.IOS ? IosBuildContext : undefined;
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
import { EasJsonReader } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
|
||||
import { apiClient } from '../api';
|
||||
import log from '../log';
|
||||
import { sleep } from '../utils/promise';
|
||||
import { prepareAndroidBuildAsync } from './android/build';
|
||||
import { CommandContext } from './context';
|
||||
import { prepareIosBuildAsync } from './ios/build';
|
||||
import { Build, BuildStatus, Platform, RequestedPlatform } from './types';
|
||||
import { printBuildResults, printLogsUrls } from './utils/printBuildInfo';
|
||||
import { ensureGitRepoExistsAsync, ensureGitStatusIsCleanAsync } from './utils/repository';
|
||||
|
||||
export async function buildAsync(commandCtx: CommandContext): Promise<void> {
|
||||
await ensureGitRepoExistsAsync();
|
||||
await ensureGitStatusIsCleanAsync(commandCtx.nonInteractive);
|
||||
|
||||
const scheduledBuilds = await startBuildsAsync(commandCtx);
|
||||
log.newLine();
|
||||
printLogsUrls(commandCtx.accountName, scheduledBuilds);
|
||||
log.newLine();
|
||||
|
||||
if (commandCtx.waitForBuildEnd) {
|
||||
const builds = await waitForBuildEndAsync(
|
||||
commandCtx,
|
||||
scheduledBuilds.map(i => i.buildId)
|
||||
);
|
||||
log.newLine();
|
||||
printBuildResults(commandCtx.accountName, builds);
|
||||
}
|
||||
}
|
||||
|
||||
async function startBuildsAsync(
|
||||
commandCtx: CommandContext
|
||||
): Promise<{ platform: Platform; buildId: string }[]> {
|
||||
const shouldBuildAndroid = [RequestedPlatform.Android, RequestedPlatform.All].includes(
|
||||
commandCtx.requestedPlatform
|
||||
);
|
||||
const shouldBuildiOS = [RequestedPlatform.iOS, RequestedPlatform.All].includes(
|
||||
commandCtx.requestedPlatform
|
||||
);
|
||||
const easConfig = await new EasJsonReader(
|
||||
commandCtx.projectDir,
|
||||
commandCtx.requestedPlatform
|
||||
).readAsync(commandCtx.profile);
|
||||
|
||||
const builds: {
|
||||
platform: Platform;
|
||||
sendBuildRequestAsync: () => Promise<string>;
|
||||
}[] = [];
|
||||
if (shouldBuildAndroid) {
|
||||
const sendBuildRequestAsync = await prepareAndroidBuildAsync(commandCtx, easConfig);
|
||||
builds.push({ platform: Platform.Android, sendBuildRequestAsync });
|
||||
}
|
||||
if (shouldBuildiOS) {
|
||||
const sendBuildRequestAsync = await prepareIosBuildAsync(commandCtx, easConfig);
|
||||
builds.push({ platform: Platform.iOS, sendBuildRequestAsync });
|
||||
}
|
||||
return Promise.all(
|
||||
builds.map(async ({ platform, sendBuildRequestAsync }) => ({
|
||||
platform,
|
||||
buildId: await sendBuildRequestAsync(),
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForBuildEndAsync(
|
||||
commandCtx: CommandContext,
|
||||
buildIds: string[],
|
||||
{ timeoutSec = 1800, intervalSec = 30 } = {}
|
||||
): Promise<(Build | null)[]> {
|
||||
log('Waiting for build to complete. You can press Ctrl+C to exit.');
|
||||
const spinner = ora().start();
|
||||
let time = new Date().getTime();
|
||||
const endTime = time + timeoutSec * 1000;
|
||||
while (time <= endTime) {
|
||||
const builds: (Build | null)[] = await Promise.all(
|
||||
buildIds.map(async buildId => {
|
||||
try {
|
||||
const { data } = await apiClient
|
||||
.get(`projects/${commandCtx.projectId}/builds/${buildId}`)
|
||||
.json();
|
||||
return data;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
if (builds.length === 1) {
|
||||
switch (builds[0]?.status) {
|
||||
case BuildStatus.FINISHED:
|
||||
spinner.succeed('Build finished');
|
||||
return builds;
|
||||
case BuildStatus.IN_QUEUE:
|
||||
spinner.text = 'Build queued...';
|
||||
break;
|
||||
case BuildStatus.IN_PROGRESS:
|
||||
spinner.text = 'Build in progress...';
|
||||
break;
|
||||
case BuildStatus.ERRORED:
|
||||
spinner.fail('Build failed');
|
||||
throw new Error(`Standalone build failed!`);
|
||||
default:
|
||||
spinner.warn('Unknown status.');
|
||||
throw new Error(`Unknown status: ${builds} - aborting!`);
|
||||
}
|
||||
} else {
|
||||
if (builds.filter(build => build?.status === BuildStatus.FINISHED).length === builds.length) {
|
||||
spinner.succeed('All builds have finished');
|
||||
return builds;
|
||||
} else if (
|
||||
builds.filter(build =>
|
||||
build?.status ? [BuildStatus.FINISHED, BuildStatus.ERRORED].includes(build.status) : false
|
||||
).length === builds.length
|
||||
) {
|
||||
spinner.fail('Some of the builds failed');
|
||||
return builds;
|
||||
} else {
|
||||
const inQueue = builds.filter(build => build?.status === BuildStatus.IN_QUEUE).length;
|
||||
const inProgress = builds.filter(build => build?.status === BuildStatus.IN_PROGRESS).length;
|
||||
const errored = builds.filter(build => build?.status === BuildStatus.ERRORED).length;
|
||||
const finished = builds.filter(build => build?.status === BuildStatus.FINISHED).length;
|
||||
const unknownState = builds.length - inQueue - inProgress - errored - finished;
|
||||
spinner.text = [
|
||||
inQueue && `Builds in queue: ${inQueue}`,
|
||||
inProgress && `Builds in progress: ${inProgress}`,
|
||||
errored && chalk.red(`Builds failed: ${errored}`),
|
||||
finished && chalk.green(`Builds finished: ${finished}`),
|
||||
unknownState && chalk.red(`Builds in unknown state: ${unknownState}`),
|
||||
]
|
||||
.filter(i => i)
|
||||
.join('\t');
|
||||
}
|
||||
}
|
||||
time = new Date().getTime();
|
||||
await sleep(intervalSec * 1000);
|
||||
}
|
||||
spinner.warn('Timed out');
|
||||
throw new Error(
|
||||
'Timeout reached! It is taking longer than expected to finish the build, aborting...'
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import JsonFile from '@expo/json-file';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { TrackingContext } from '../analytics/common';
|
||||
import { Analytics, BuildEvent } from '../analytics/events';
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { getProjectAccountName, getProjectIdAsync } from '../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../project/workflow';
|
||||
import { findAccountByName } from '../user/Account';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { createAndroidContextAsync } from './android/build';
|
||||
import { BuildContext, CommonContext } from './context';
|
||||
import { createIosContextAsync } from './ios/build';
|
||||
import { LocalBuildOptions } from './local';
|
||||
|
||||
export async function createBuildContextAsync<T extends Platform>({
|
||||
buildProfileName,
|
||||
buildProfile,
|
||||
clearCache = false,
|
||||
localBuildOptions,
|
||||
nonInteractive = false,
|
||||
platform,
|
||||
projectDir,
|
||||
}: {
|
||||
buildProfileName: string;
|
||||
buildProfile: BuildProfile<T>;
|
||||
clearCache: boolean;
|
||||
localBuildOptions: LocalBuildOptions;
|
||||
nonInteractive: boolean;
|
||||
platform: T;
|
||||
projectDir: string;
|
||||
}): Promise<BuildContext<T>> {
|
||||
const exp = getExpoConfig(projectDir, { env: buildProfile.env });
|
||||
|
||||
const user = await ensureLoggedInAsync();
|
||||
const accountName = getProjectAccountName(exp, user);
|
||||
const projectName = exp.slug;
|
||||
const projectId = await getProjectIdAsync(exp, { env: buildProfile.env });
|
||||
const workflow = await resolveWorkflowAsync(projectDir, platform);
|
||||
const accountId = findAccountByName(user.accounts, accountName)?.id;
|
||||
|
||||
const credentialsCtx = new CredentialsContext({
|
||||
exp,
|
||||
nonInteractive,
|
||||
projectDir,
|
||||
user,
|
||||
env: buildProfile.env,
|
||||
});
|
||||
|
||||
const devClientProperties = getDevClientEventProperties({
|
||||
platform,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
});
|
||||
const trackingCtx = {
|
||||
tracking_id: uuidv4(),
|
||||
platform,
|
||||
...(accountId && { account_id: accountId }),
|
||||
project_id: projectId,
|
||||
project_type: workflow,
|
||||
...devClientProperties,
|
||||
};
|
||||
Analytics.logEvent(BuildEvent.BUILD_COMMAND, trackingCtx);
|
||||
|
||||
const commonContext: CommonContext<T> = {
|
||||
accountName,
|
||||
buildProfile,
|
||||
buildProfileName,
|
||||
clearCache,
|
||||
credentialsCtx,
|
||||
exp,
|
||||
localBuildOptions,
|
||||
nonInteractive,
|
||||
platform,
|
||||
projectDir,
|
||||
projectId,
|
||||
projectName,
|
||||
trackingCtx,
|
||||
user,
|
||||
workflow,
|
||||
};
|
||||
if (platform === Platform.ANDROID) {
|
||||
const common = commonContext as CommonContext<Platform.ANDROID>;
|
||||
return {
|
||||
...common,
|
||||
android: await createAndroidContextAsync(common),
|
||||
} as BuildContext<T>;
|
||||
} else {
|
||||
const common = commonContext as CommonContext<Platform.IOS>;
|
||||
return {
|
||||
...common,
|
||||
ios: await createIosContextAsync(common),
|
||||
} as BuildContext<T>;
|
||||
}
|
||||
}
|
||||
|
||||
function getDevClientEventProperties({
|
||||
platform,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
}: {
|
||||
platform: Platform;
|
||||
projectDir: string;
|
||||
buildProfile: BuildProfile;
|
||||
}): Partial<TrackingContext> {
|
||||
let includesDevClient;
|
||||
const version = tryGetDevClientVersion(projectDir);
|
||||
if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) {
|
||||
includesDevClient = Boolean(version && buildProfile.gradleCommand?.includes('Debug'));
|
||||
} else if (platform === Platform.IOS && 'buildConfiguration' in buildProfile) {
|
||||
includesDevClient = Boolean(version && buildProfile.buildConfiguration === 'Debug');
|
||||
} else if (buildProfile.developmentClient) {
|
||||
includesDevClient = true;
|
||||
} else {
|
||||
includesDevClient = false;
|
||||
}
|
||||
|
||||
if (version) {
|
||||
return { dev_client: includesDevClient, dev_client_version: version };
|
||||
} else {
|
||||
return { dev_client: includesDevClient };
|
||||
}
|
||||
}
|
||||
|
||||
function tryGetDevClientVersion(projectDir: string): string | null {
|
||||
try {
|
||||
const pkg = JsonFile.read(resolveFrom(projectDir, 'expo-dev-client/package.json'));
|
||||
return pkg.version?.toString() ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { CredentialsProvider } from '../credentials/CredentialsProvider';
|
||||
import log from '../log';
|
||||
import { confirmAsync, promptAsync } from '../prompts';
|
||||
import { platformDisplayNames } from './constants';
|
||||
|
||||
function logCredentials(target: 'local' | 'remote', platform: Platform) {
|
||||
let message = `Using ${target} ${platformDisplayNames[platform]} credentials`;
|
||||
if (target === 'local') message += ` ${chalk.dim('(credentials.json)')}`;
|
||||
if (target === 'remote') message += ` ${chalk.dim('(Expo server)')}`;
|
||||
log.succeed(message);
|
||||
}
|
||||
|
||||
async function ensureCredentialsAutoAsync(
|
||||
provider: CredentialsProvider,
|
||||
workflow: Workflow,
|
||||
nonInteractive: boolean
|
||||
): Promise<CredentialsSource.LOCAL | CredentialsSource.REMOTE> {
|
||||
const platform = platformDisplayNames[provider.platform];
|
||||
switch (workflow) {
|
||||
case Workflow.Managed:
|
||||
if (await provider.hasLocalAsync()) {
|
||||
return CredentialsSource.LOCAL;
|
||||
} else {
|
||||
return CredentialsSource.REMOTE;
|
||||
}
|
||||
case Workflow.Generic: {
|
||||
const hasLocal = await provider.hasLocalAsync();
|
||||
const hasRemote = await provider.hasRemoteAsync();
|
||||
if (hasRemote && hasLocal) {
|
||||
if (!(await provider.isLocalSyncedAsync())) {
|
||||
if (nonInteractive) {
|
||||
throw new Error(
|
||||
`Contents of your local credentials.json for ${platform} are not the same as credentials on Expo servers. To use the desired credentials, set the "builds.${platform}.{profile}.credentialsSource" field in the credentials.json file to one of the following: "local", "remote".`
|
||||
);
|
||||
} else {
|
||||
log(
|
||||
`Contents of your local credentials.json for ${platform} are not the same as credentials on Expo servers`
|
||||
);
|
||||
}
|
||||
|
||||
const { select } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'select',
|
||||
message: 'Which credentials do you want to use for this build?',
|
||||
choices: [
|
||||
{ title: 'Local credentials.json', value: CredentialsSource.LOCAL },
|
||||
{ title: 'Credentials stored on Expo servers.', value: CredentialsSource.REMOTE },
|
||||
],
|
||||
});
|
||||
return select;
|
||||
} else {
|
||||
return CredentialsSource.LOCAL;
|
||||
}
|
||||
} else if (hasLocal) {
|
||||
logCredentials('local', provider.platform);
|
||||
return CredentialsSource.LOCAL;
|
||||
} else if (hasRemote) {
|
||||
logCredentials('remote', provider.platform);
|
||||
return CredentialsSource.REMOTE;
|
||||
} else {
|
||||
if (nonInteractive) {
|
||||
throw new Error(
|
||||
`Credentials for this app are not configured and there is no entry in credentials.json for ${platform}. Either configure credentials.json, or launch the build without "--non-interactive" flag to get a prompt to generate credentials automatically.`
|
||||
);
|
||||
} else if (log.isDebug) {
|
||||
log.warn(
|
||||
`Credentials for this app are not configured and there is no entry in credentials.json for ${platform}`
|
||||
);
|
||||
}
|
||||
|
||||
const confirm = await confirmAsync({
|
||||
message: `${platform} credentials not found, generate now?`,
|
||||
});
|
||||
if (confirm) {
|
||||
return CredentialsSource.REMOTE;
|
||||
} else {
|
||||
throw new Error(`Aborting build process, credentials are not configured for ${platform}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureCredentialsAsync(
|
||||
provider: CredentialsProvider,
|
||||
workflow: Workflow,
|
||||
src: CredentialsSource,
|
||||
nonInteractive: boolean
|
||||
): Promise<CredentialsSource.LOCAL | CredentialsSource.REMOTE> {
|
||||
switch (src) {
|
||||
case CredentialsSource.LOCAL:
|
||||
logCredentials('local', provider.platform);
|
||||
return CredentialsSource.LOCAL;
|
||||
case CredentialsSource.REMOTE:
|
||||
logCredentials('remote', provider.platform);
|
||||
return CredentialsSource.REMOTE;
|
||||
case CredentialsSource.AUTO:
|
||||
return await ensureCredentialsAutoAsync(provider, workflow, nonInteractive);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { ArchiveSource, ArchiveSourceType, Metadata, Workflow } from '@expo/eas-build-job';
|
||||
|
||||
import {
|
||||
BuildCredentialsSource,
|
||||
BuildIosEnterpriseProvisioning,
|
||||
BuildMetadataInput,
|
||||
BuildWorkflow,
|
||||
DistributionType,
|
||||
ProjectArchiveSourceInput,
|
||||
ProjectArchiveSourceType,
|
||||
} from '../graphql/generated';
|
||||
|
||||
export function transformProjectArchive(archiveSource: ArchiveSource): ProjectArchiveSourceInput {
|
||||
if (archiveSource.type === ArchiveSourceType.S3) {
|
||||
return {
|
||||
type: ProjectArchiveSourceType.S3,
|
||||
bucketKey: archiveSource.bucketKey,
|
||||
};
|
||||
} else if (archiveSource.type === ArchiveSourceType.URL) {
|
||||
return {
|
||||
type: ProjectArchiveSourceType.Url,
|
||||
url: archiveSource.url,
|
||||
};
|
||||
} else {
|
||||
throw new Error(`Unsupported project archive source type: '${archiveSource.type}'`);
|
||||
}
|
||||
}
|
||||
|
||||
export function transformMetadata(metadata: Metadata): BuildMetadataInput {
|
||||
return {
|
||||
...metadata,
|
||||
credentialsSource:
|
||||
metadata.credentialsSource && transformCredentialsSource(metadata.credentialsSource),
|
||||
distribution: metadata.distribution && transformDistribution(metadata.distribution),
|
||||
workflow: metadata.workflow && transformWorkflow(metadata.workflow),
|
||||
iosEnterpriseProvisioning:
|
||||
metadata.iosEnterpriseProvisioning &&
|
||||
transformIosEnterpriseProvisioning(metadata.iosEnterpriseProvisioning),
|
||||
};
|
||||
}
|
||||
|
||||
function transformCredentialsSource(
|
||||
credentialsSource: Metadata['credentialsSource']
|
||||
): BuildCredentialsSource {
|
||||
if (credentialsSource === 'local') {
|
||||
return BuildCredentialsSource.Local;
|
||||
} else {
|
||||
return BuildCredentialsSource.Remote;
|
||||
}
|
||||
}
|
||||
|
||||
function transformDistribution(distribution: Metadata['distribution']): DistributionType {
|
||||
if (distribution === 'internal') {
|
||||
return DistributionType.Internal;
|
||||
} else if (distribution === 'simulator') {
|
||||
return DistributionType.Simulator;
|
||||
} else {
|
||||
return DistributionType.Store;
|
||||
}
|
||||
}
|
||||
|
||||
export function transformWorkflow(workflow: Workflow): BuildWorkflow {
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
return BuildWorkflow.Generic;
|
||||
} else {
|
||||
return BuildWorkflow.Managed;
|
||||
}
|
||||
}
|
||||
|
||||
export function transformIosEnterpriseProvisioning(
|
||||
enterpriseProvisioning: Metadata['iosEnterpriseProvisioning']
|
||||
): BuildIosEnterpriseProvisioning {
|
||||
if (enterpriseProvisioning === 'adhoc') {
|
||||
return BuildIosEnterpriseProvisioning.Adhoc;
|
||||
} else {
|
||||
return BuildIosEnterpriseProvisioning.Universal;
|
||||
}
|
||||
}
|
||||
@@ -1,89 +1,26 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import plist from '@expo/plist';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import log from '../../log';
|
||||
import { getProjectAccountName } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { gitAddAsync } from '../../utils/git';
|
||||
import { readPlistAsync, writePlistAsync } from '../../utils/plist';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { ensureValidVersions } from '../utils/updates';
|
||||
|
||||
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
|
||||
ensureValidVersions(exp);
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
let xcodeProject = IOSConfig.XcodeUtils.getPbxproj(projectDir);
|
||||
|
||||
if (!IOSConfig.Updates.isShellScriptBuildPhaseConfigured(projectDir, exp, xcodeProject)) {
|
||||
xcodeProject = IOSConfig.Updates.ensureBundleReactNativePhaseContainsConfigurationScript(
|
||||
projectDir,
|
||||
exp,
|
||||
xcodeProject
|
||||
);
|
||||
await fs.writeFile(IOSConfig.Paths.getPBXProjectPath(projectDir), xcodeProject.writeSync());
|
||||
}
|
||||
|
||||
let expoPlist = await readExpoPlistAsync(projectDir);
|
||||
if (!IOSConfig.Updates.isPlistConfigurationSynced(exp, expoPlist, accountName)) {
|
||||
expoPlist = IOSConfig.Updates.setUpdatesConfig(exp, expoPlist, accountName);
|
||||
await writeExpoPlistAsync(projectDir, expoPlist);
|
||||
}
|
||||
// TODO: ensure ExpoPlist in pbxproj
|
||||
}
|
||||
|
||||
export async function syncUpdatesConfigurationAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
): Promise<void> {
|
||||
ensureValidVersions(exp);
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
try {
|
||||
await ensureUpdatesConfiguredAsync(projectDir, exp);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
'expo-updates module is not configured. Please run "eas build:configure" first to configure the project'
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
let expoPlist = await readExpoPlistAsync(projectDir);
|
||||
if (!IOSConfig.Updates.isPlistVersionConfigurationSynced(exp, expoPlist)) {
|
||||
expoPlist = IOSConfig.Updates.setVersionsConfig(exp, expoPlist);
|
||||
await writeExpoPlistAsync(projectDir, expoPlist);
|
||||
}
|
||||
|
||||
if (!IOSConfig.Updates.isPlistConfigurationSynced(exp, expoPlist, accountName)) {
|
||||
log.warn(
|
||||
'Native project configuration is not synced with values present in you app.json, run "eas build:configure" to make sure all values are applied in the native project'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
|
||||
const xcodeProject = IOSConfig.XcodeUtils.getPbxproj(projectDir);
|
||||
|
||||
if (!IOSConfig.Updates.isShellScriptBuildPhaseConfigured(projectDir, exp, xcodeProject)) {
|
||||
const script = 'expo-updates/scripts/create-manifest-ios.sh';
|
||||
const buildPhase = '"Bundle React Native code and images"';
|
||||
throw new Error(`Path to ${script} is missing in a ${buildPhase} build phase.`);
|
||||
}
|
||||
|
||||
const expoPlist = await readExpoPlistAsync(projectDir);
|
||||
if (!IOSConfig.Updates.isPlistConfigurationSet(expoPlist)) {
|
||||
throw new Error('Missing values in Expo.plist');
|
||||
}
|
||||
const updatedExpoPlist = IOSConfig.Updates.setUpdatesConfig(exp, expoPlist, accountName);
|
||||
await writeExpoPlistAsync(projectDir, updatedExpoPlist);
|
||||
}
|
||||
|
||||
async function readExpoPlistAsync(projectDir: string): Promise<IOSConfig.ExpoPlist> {
|
||||
const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(projectDir);
|
||||
|
||||
let expoPlist = {};
|
||||
if (await fs.pathExists(expoPlistPath)) {
|
||||
const expoPlistContent = await fs.readFile(expoPlistPath, 'utf8');
|
||||
expoPlist = plist.parse(expoPlistContent);
|
||||
}
|
||||
return expoPlist;
|
||||
return ((await readPlistAsync(expoPlistPath)) ?? {}) as IOSConfig.ExpoPlist;
|
||||
}
|
||||
|
||||
async function writeExpoPlistAsync(
|
||||
@@ -91,9 +28,27 @@ async function writeExpoPlistAsync(
|
||||
expoPlist: IOSConfig.ExpoPlist
|
||||
): Promise<void> {
|
||||
const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(projectDir);
|
||||
const expoPlistContent = plist.build(expoPlist);
|
||||
await writePlistAsync(expoPlistPath, expoPlist);
|
||||
await getVcsClient().trackFileAsync(expoPlistPath);
|
||||
}
|
||||
|
||||
await fs.mkdirp(path.dirname(expoPlistPath));
|
||||
await fs.writeFile(expoPlistPath, expoPlistContent);
|
||||
await gitAddAsync(expoPlistPath, { intentToAdd: true });
|
||||
export async function readReleaseChannelSafelyAsync(projectDir: string): Promise<string | null> {
|
||||
try {
|
||||
const expoPlist = await readExpoPlistAsync(projectDir);
|
||||
return expoPlist[IOSConfig.Updates.Config.RELEASE_CHANNEL] ?? null;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function readChannelSafelyAsync(projectDir: string): Promise<string | null> {
|
||||
try {
|
||||
// TODO-JJ remove any once IOSConfig.ExpoPlist is updated to include `EXUpdatesRequestHeaders : Record<string,string>`
|
||||
const expoPlist: any = await readExpoPlistAsync(projectDir);
|
||||
//TODO-JJ 'EXUpdatesRequestHeaders' IosConfig.Updates.Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY once https://github.com/expo/expo-cli/pull/3571 is published
|
||||
const updatesRequestHeaders = expoPlist['EXUpdatesRequestHeaders'] ?? {};
|
||||
return updatesRequestHeaders['expo-channel-name'] ?? null;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
|
||||
import { readChannelSafelyAsync } from '../UpdatesModule';
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
afterAll(async () => {
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
await fs.remove(os.tmpdir());
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
vol.reset();
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
await fs.mkdirp(os.tmpdir());
|
||||
});
|
||||
|
||||
const testChannel = 'test-channel';
|
||||
describe(readChannelSafelyAsync, () => {
|
||||
it('retrieves the channel when it is defined natively', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'ios/expo-project/Supporting/Expo.plist': ExpoPlistWithChannel,
|
||||
'ios/expo-project/AppDelegate.m': '',
|
||||
},
|
||||
'/expo-project'
|
||||
);
|
||||
const channel = await readChannelSafelyAsync('/expo-project');
|
||||
expect(channel).toBe(testChannel);
|
||||
});
|
||||
it('returns null if the channel is not defined natively', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'ios/expo-project/Supporting/Expo.plist': ExpoPlistWithOutChannel,
|
||||
'ios/expo-project/AppDelegate.m': '',
|
||||
},
|
||||
'/expo-project'
|
||||
);
|
||||
const channel = await readChannelSafelyAsync('/expo-project');
|
||||
expect(channel).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
const ExpoPlistWithChannel = `
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>EXUpdatesCheckOnLaunch</key>
|
||||
<string>ALWAYS</string>
|
||||
<key>EXUpdatesEnabled</key>
|
||||
<true/>
|
||||
<key>EXUpdatesLaunchWaitMs</key>
|
||||
<integer>0</integer>
|
||||
<key>EXUpdatesReleaseChannel</key>
|
||||
<string>default</string>
|
||||
<key>EXUpdatesSDKVersion</key>
|
||||
<string>40.0.0</string>
|
||||
<key>EXUpdatesURL</key>
|
||||
<string>https://exp.host/@user/expo-project</string>
|
||||
<key>EXUpdatesRequestHeaders</key>
|
||||
<dict>
|
||||
<key>expo-channel-name</key>
|
||||
<string>${testChannel}</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
`;
|
||||
const ExpoPlistWithOutChannel = `
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>EXUpdatesCheckOnLaunch</key>
|
||||
<string>ALWAYS</string>
|
||||
<key>EXUpdatesEnabled</key>
|
||||
<true/>
|
||||
<key>EXUpdatesLaunchWaitMs</key>
|
||||
<integer>0</integer>
|
||||
<key>EXUpdatesReleaseChannel</key>
|
||||
<string>default</string>
|
||||
<key>EXUpdatesSDKVersion</key>
|
||||
<string>40.0.0</string>
|
||||
<key>EXUpdatesURL</key>
|
||||
<string>https://exp.host/@user/expo-project</string>
|
||||
</dict>
|
||||
</plist>
|
||||
`;
|
||||
@@ -0,0 +1,344 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
|
||||
import { readPlistAsync } from '../../../utils/plist';
|
||||
import {
|
||||
BumpStrategy,
|
||||
bumpVersionAsync,
|
||||
bumpVersionInAppJsonAsync,
|
||||
evaluateTemplateString,
|
||||
getInfoPlistPath,
|
||||
readBuildNumberAsync,
|
||||
readShortVersionAsync,
|
||||
} from '../version';
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
afterAll(async () => {
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
await fs.remove(os.tmpdir());
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
vol.reset();
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
await fs.mkdirp(os.tmpdir());
|
||||
});
|
||||
|
||||
describe(evaluateTemplateString, () => {
|
||||
it('evaluates the template string when value is a number', () => {
|
||||
expect(evaluateTemplateString('$(BLAH_BLAH)', { BLAH_BLAH: 123 })).toBe('123');
|
||||
});
|
||||
it('evaluates the template string when value is a string', () => {
|
||||
expect(evaluateTemplateString('$(BLAH_BLAH)', { BLAH_BLAH: '123' })).toBe('123');
|
||||
});
|
||||
it('evaluates the template string when template is not the only element', () => {
|
||||
expect(evaluateTemplateString('before$(BLAH_BLAH)after', { BLAH_BLAH: '123' })).toBe(
|
||||
'before123after'
|
||||
);
|
||||
});
|
||||
it('evaluates the template string when template value is double quoted', () => {
|
||||
expect(evaluateTemplateString('before$(BLAH_BLAH)after', { BLAH_BLAH: '"123"' })).toBe(
|
||||
'before123after'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// bare workflow
|
||||
describe(bumpVersionAsync, () => {
|
||||
it('bumps expo.ios.buildNumber and CFBundleVersion when strategy = BumpStrategy.BUILD_NUMBER', async () => {
|
||||
const fakeExp = initBareWorkflowProject();
|
||||
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.BUILD_NUMBER,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
buildSettings: {},
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
const infoPlist = (await readPlistAsync(
|
||||
'/repo/ios/myproject/Info.plist'
|
||||
)) as IOSConfig.InfoPlist;
|
||||
expect(fakeExp.version).toBe('1.0.0');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('2');
|
||||
expect(appJSON.expo.version).toBe('1.0.0');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('2');
|
||||
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.0');
|
||||
expect(infoPlist['CFBundleVersion']).toBe('2');
|
||||
});
|
||||
|
||||
it('bumps expo.ios.buildNumber and CFBundleVersion for non default Info.plist location', async () => {
|
||||
const fakeExp = initBareWorkflowProject({ infoPlistName: 'Info2.plist' });
|
||||
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.BUILD_NUMBER,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
buildSettings: {
|
||||
INFOPLIST_FILE: '$(SRCROOT)/myproject/Info2.plist',
|
||||
},
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
const infoPlist = (await readPlistAsync(
|
||||
'/repo/ios/myproject/Info2.plist'
|
||||
)) as IOSConfig.InfoPlist;
|
||||
expect(fakeExp.version).toBe('1.0.0');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('2');
|
||||
expect(appJSON.expo.version).toBe('1.0.0');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('2');
|
||||
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.0');
|
||||
expect(infoPlist['CFBundleVersion']).toBe('2');
|
||||
});
|
||||
|
||||
it('bumps expo.version and CFBundleShortVersionString when strategy = BumpStrategy.SHORT_VERSION', async () => {
|
||||
const fakeExp = initBareWorkflowProject();
|
||||
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.APP_VERSION,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
buildSettings: {},
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
const infoPlist = (await readPlistAsync(
|
||||
'/repo/ios/myproject/Info.plist'
|
||||
)) as IOSConfig.InfoPlist;
|
||||
expect(fakeExp.version).toBe('1.0.1');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('1');
|
||||
expect(appJSON.expo.version).toBe('1.0.1');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('1');
|
||||
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.1');
|
||||
expect(infoPlist['CFBundleVersion']).toBe('1');
|
||||
});
|
||||
|
||||
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
|
||||
const fakeExp = initBareWorkflowProject();
|
||||
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.NOOP,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
buildSettings: {},
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
const infoPlist = (await readPlistAsync(
|
||||
'/repo/ios/myproject/Info.plist'
|
||||
)) as IOSConfig.InfoPlist;
|
||||
expect(fakeExp.version).toBe('1.0.0');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('1');
|
||||
expect(appJSON.expo.version).toBe('1.0.0');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('1');
|
||||
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.0');
|
||||
expect(infoPlist['CFBundleVersion']).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
// managed workflow
|
||||
describe(bumpVersionInAppJsonAsync, () => {
|
||||
it('bumps expo.ios.buildNumber when strategy = BumpStrategy.BUILD_NUMBER', async () => {
|
||||
const fakeExp = initManagedProject();
|
||||
|
||||
await bumpVersionInAppJsonAsync({
|
||||
bumpStrategy: BumpStrategy.BUILD_NUMBER,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('1.0.0');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('2');
|
||||
expect(appJSON.expo.version).toBe('1.0.0');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('2');
|
||||
});
|
||||
|
||||
it('bumps expo.version when strategy = BumpStrategy.SHORT_VERSION', async () => {
|
||||
const fakeExp = initManagedProject();
|
||||
|
||||
await bumpVersionInAppJsonAsync({
|
||||
bumpStrategy: BumpStrategy.APP_VERSION,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('1.0.1');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('1');
|
||||
expect(appJSON.expo.version).toBe('1.0.1');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('1');
|
||||
});
|
||||
|
||||
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
|
||||
const fakeExp = initManagedProject();
|
||||
|
||||
await bumpVersionInAppJsonAsync({
|
||||
bumpStrategy: BumpStrategy.NOOP,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('1.0.0');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('1');
|
||||
expect(appJSON.expo.version).toBe('1.0.0');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe(readBuildNumberAsync, () => {
|
||||
describe('bare project', () => {
|
||||
it('reads the build number from native code', async () => {
|
||||
const exp = initBareWorkflowProject();
|
||||
const buildNumber = await readBuildNumberAsync('/repo', exp, {});
|
||||
expect(buildNumber).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('managed project', () => {
|
||||
it('reads the build number from expo config', async () => {
|
||||
const exp = initManagedProject();
|
||||
const buildNumber = await readBuildNumberAsync('/repo', exp, {});
|
||||
expect(buildNumber).toBe('1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(readShortVersionAsync, () => {
|
||||
describe('bare project', () => {
|
||||
it('reads the short version from native code', async () => {
|
||||
const exp = initBareWorkflowProject();
|
||||
const appVersion = await readShortVersionAsync('/repo', exp, {});
|
||||
expect(appVersion).toBe('1.0.0');
|
||||
});
|
||||
it('evaluates interpolated build number', async () => {
|
||||
const exp = initBareWorkflowProject({
|
||||
appVersion: '$(CURRENT_PROJECT_VERSION)',
|
||||
});
|
||||
const buildNumber = await readShortVersionAsync('/repo', exp, {
|
||||
CURRENT_PROJECT_VERSION: '1.0.0',
|
||||
});
|
||||
expect(buildNumber).toBe('1.0.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('managed project', () => {
|
||||
it('reads the version from app config', async () => {
|
||||
const exp = initBareWorkflowProject();
|
||||
const appVersion = await readShortVersionAsync('/repo', exp, {});
|
||||
expect(appVersion).toBe('1.0.0');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(getInfoPlistPath, () => {
|
||||
it('returns default path if INFOPLIST_FILE is not specified', () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./ios/testapp/Info.plist': '',
|
||||
},
|
||||
'/repo'
|
||||
);
|
||||
const plistPath = getInfoPlistPath('/repo', {});
|
||||
expect(plistPath).toBe('/repo/ios/testapp/Info.plist');
|
||||
});
|
||||
it('returns INFOPLIST_FILE if specified', () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./ios/testapp/Info.plist': '',
|
||||
},
|
||||
'/repo'
|
||||
);
|
||||
const plistPath = getInfoPlistPath('/repo', { INFOPLIST_FILE: './qwert/NotInfo.plist' });
|
||||
expect(plistPath).toBe('/repo/ios/qwert/NotInfo.plist');
|
||||
});
|
||||
it('evaluates SRCROOT in Info.plist', () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./ios/testapp/Info.plist': '',
|
||||
},
|
||||
'/repo'
|
||||
);
|
||||
const plistPath = getInfoPlistPath('/repo', {
|
||||
INFOPLIST_FILE: '$(SRCROOT)/qwert/NotInfo.plist',
|
||||
});
|
||||
expect(plistPath).toBe('/repo/ios/qwert/NotInfo.plist');
|
||||
});
|
||||
it('evaluates BuildSettings in Info.plist', () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./ios/testapp/Info.plist': '',
|
||||
},
|
||||
'/repo'
|
||||
);
|
||||
const plistPath = getInfoPlistPath('/repo', {
|
||||
INFOPLIST_FILE: '$(SRCROOT)/qwert/$(TARGET_NAME).plist',
|
||||
TARGET_NAME: 'NotInfo',
|
||||
});
|
||||
expect(plistPath).toBe('/repo/ios/qwert/NotInfo.plist');
|
||||
});
|
||||
});
|
||||
|
||||
function initBareWorkflowProject({
|
||||
appVersion = '1.0.0',
|
||||
version = '1',
|
||||
infoPlistName = 'Info.plist',
|
||||
}: { appVersion?: string; version?: string; infoPlistName?: string } = {}): ExpoConfig {
|
||||
const fakeExp: ExpoConfig = {
|
||||
name: 'myproject',
|
||||
slug: 'myproject',
|
||||
version: '1.0.0',
|
||||
ios: {
|
||||
buildNumber: '1',
|
||||
},
|
||||
};
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./app.json': JSON.stringify({
|
||||
expo: fakeExp,
|
||||
}),
|
||||
[`./ios/myproject/${infoPlistName}`]: `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>${appVersion}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${version}</string>
|
||||
</dict>
|
||||
</plist>`,
|
||||
},
|
||||
'/repo'
|
||||
);
|
||||
|
||||
return fakeExp;
|
||||
}
|
||||
|
||||
function initManagedProject(): ExpoConfig {
|
||||
const fakeExp: ExpoConfig = {
|
||||
name: 'myproject',
|
||||
slug: 'myproject',
|
||||
version: '1.0.0',
|
||||
ios: {
|
||||
buildNumber: '1',
|
||||
},
|
||||
};
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./app.json': JSON.stringify({
|
||||
expo: fakeExp,
|
||||
}),
|
||||
},
|
||||
'/repo'
|
||||
);
|
||||
|
||||
return fakeExp;
|
||||
}
|
||||
@@ -1,93 +1,112 @@
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { EasConfig } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import path from 'path';
|
||||
import { Ios, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
|
||||
import log from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { prepareBuildRequestForPlatformAsync } from '../build';
|
||||
import { BuildContext, CommandContext, createBuildContext } from '../context';
|
||||
import { Platform } from '../types';
|
||||
import { ensureBundleIdentifierIsValidAsync } from './bundleIdentifer';
|
||||
import { validateAndSyncProjectConfigurationAsync } from './configure';
|
||||
import { IosCredentials, Target } from '../../credentials/ios/types';
|
||||
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
|
||||
import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier';
|
||||
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
|
||||
import { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/target';
|
||||
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
|
||||
import { BuildContext, CommonContext, IosBuildContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
|
||||
import { ensureIosCredentialsAsync } from './credentials';
|
||||
import { transformJob } from './graphql';
|
||||
import { prepareJobAsync } from './prepareJob';
|
||||
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
|
||||
|
||||
export async function createIosContextAsync(
|
||||
ctx: CommonContext<Platform.IOS>
|
||||
): Promise<IosBuildContext> {
|
||||
const { buildProfile } = ctx;
|
||||
|
||||
if (ctx.workflow === Workflow.MANAGED) {
|
||||
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
|
||||
checkNodeEnvVariable(ctx);
|
||||
await checkGoogleServicesFileAsync(ctx);
|
||||
|
||||
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
|
||||
{
|
||||
projectDir: ctx.projectDir,
|
||||
nonInteractive: ctx.nonInteractive,
|
||||
exp: ctx.exp,
|
||||
},
|
||||
buildProfile
|
||||
);
|
||||
const targets = await resolveTargetsAsync(
|
||||
{
|
||||
projectDir: ctx.projectDir,
|
||||
exp: ctx.exp,
|
||||
},
|
||||
xcodeBuildContext
|
||||
);
|
||||
const applicationTarget = findApplicationTarget(targets);
|
||||
const applicationTargetBuildSettings = resolveBuildSettings(ctx, applicationTarget);
|
||||
|
||||
return {
|
||||
bundleIdentifier: applicationTarget.bundleIdentifier,
|
||||
applicationTarget,
|
||||
applicationTargetBuildSettings,
|
||||
targets,
|
||||
xcodeBuildContext,
|
||||
};
|
||||
}
|
||||
|
||||
export async function prepareIosBuildAsync(
|
||||
commandCtx: CommandContext,
|
||||
easConfig: EasConfig
|
||||
): Promise<() => Promise<string>> {
|
||||
const buildCtx = createBuildContext<Platform.iOS>({
|
||||
commandCtx,
|
||||
platform: Platform.iOS,
|
||||
easConfig,
|
||||
});
|
||||
|
||||
if (
|
||||
buildCtx.buildProfile.workflow === Workflow.Generic &&
|
||||
!(await fs.pathExists(path.join(commandCtx.projectDir, 'ios')))
|
||||
) {
|
||||
throw new Error(
|
||||
`"ios" directory not found. If you're trying to build a managed project, set ${chalk.bold(
|
||||
`builds.ios.${commandCtx.profile}.workflow`
|
||||
)} in "eas.json" to "managed".`
|
||||
);
|
||||
}
|
||||
|
||||
let iosNativeProjectScheme: string | undefined;
|
||||
if (buildCtx.buildProfile.workflow === Workflow.Generic) {
|
||||
iosNativeProjectScheme = buildCtx.buildProfile.scheme ?? (await resolveSchemeAsync(buildCtx));
|
||||
}
|
||||
await ensureBundleIdentifierIsValidAsync(commandCtx.projectDir);
|
||||
|
||||
ctx: BuildContext<Platform.IOS>
|
||||
): Promise<BuildRequestSender> {
|
||||
return await prepareBuildRequestForPlatformAsync({
|
||||
ctx: buildCtx,
|
||||
projectConfiguration: {
|
||||
iosNativeProjectScheme,
|
||||
ctx,
|
||||
ensureCredentialsAsync: async (ctx: BuildContext<Platform.IOS>) => {
|
||||
return ensureIosCredentialsAsync(ctx, ctx.ios.targets);
|
||||
},
|
||||
ensureCredentialsAsync: ensureIosCredentialsAsync,
|
||||
ensureProjectConfiguredAsync: async () => {
|
||||
if (buildCtx.buildProfile.workflow === Workflow.Generic) {
|
||||
await validateAndSyncProjectConfigurationAsync(commandCtx.projectDir, commandCtx.exp);
|
||||
}
|
||||
syncProjectConfigurationAsync: async () => {
|
||||
await syncProjectConfigurationAsync({
|
||||
projectDir: ctx.projectDir,
|
||||
exp: ctx.exp,
|
||||
buildProfile: ctx.buildProfile,
|
||||
buildSettings: ctx.ios.applicationTargetBuildSettings,
|
||||
});
|
||||
},
|
||||
prepareJobAsync: async (
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData<IosCredentials>
|
||||
): Promise<Job> => {
|
||||
return await prepareJobAsync(ctx, {
|
||||
...jobData,
|
||||
buildScheme: ctx.ios.xcodeBuildContext.buildScheme,
|
||||
});
|
||||
},
|
||||
sendBuildRequestAsync: async (
|
||||
appId: string,
|
||||
job: Ios.Job,
|
||||
metadata: Metadata
|
||||
): Promise<BuildResult> => {
|
||||
const graphqlMetadata = transformMetadata(metadata);
|
||||
const graphqlJob = transformJob(job);
|
||||
return await BuildMutation.createIosBuildAsync({
|
||||
appId,
|
||||
job: graphqlJob,
|
||||
metadata: graphqlMetadata,
|
||||
});
|
||||
},
|
||||
prepareJobAsync,
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveSchemeAsync(ctx: BuildContext<Platform.iOS>): Promise<string> {
|
||||
const schemes = IOSConfig.Scheme.getSchemesFromXcodeproj(ctx.commandCtx.projectDir);
|
||||
if (schemes.length === 1) {
|
||||
return schemes[0];
|
||||
}
|
||||
|
||||
const sortedSchemes = sortBy(schemes);
|
||||
log.newLine();
|
||||
log(
|
||||
`We've found multiple schemes in your Xcode project: ${chalk.bold(sortedSchemes.join(', '))}`
|
||||
);
|
||||
log(
|
||||
`You can specify the scheme you want to build at ${chalk.bold(
|
||||
`builds.ios.${ctx.commandCtx.profile}.scheme`
|
||||
)} in eas.json.`
|
||||
);
|
||||
if (ctx.commandCtx.nonInteractive) {
|
||||
const withoutTvOS = sortedSchemes.filter(i => !i.includes('tvOS'));
|
||||
const scheme = withoutTvOS.length > 0 ? withoutTvOS[0] : sortedSchemes[0];
|
||||
log(`You've run Expo CLI in non-interactive mode, choosing the ${chalk.bold(scheme)} scheme.`);
|
||||
log.newLine();
|
||||
return scheme;
|
||||
} else {
|
||||
const { selectedScheme } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'selectedScheme',
|
||||
message: 'Which scheme would you like to build now?',
|
||||
choices: sortedSchemes.map(scheme => ({ title: scheme, value: scheme })),
|
||||
});
|
||||
log.newLine();
|
||||
return selectedScheme as string;
|
||||
function resolveBuildSettings(
|
||||
ctx: CommonContext<Platform.IOS>,
|
||||
applicationTarget: Target
|
||||
): XCBuildConfiguration['buildSettings'] {
|
||||
if (ctx.workflow === Workflow.MANAGED) {
|
||||
return {};
|
||||
}
|
||||
const project = IOSConfig.XcodeUtils.getPbxproj(ctx.projectDir);
|
||||
const xcBuildConfiguration = IOSConfig.Target.getXCBuildConfigurationFromPbxproj(project, {
|
||||
targetName: applicationTarget.targetName,
|
||||
buildConfiguration: applicationTarget.buildConfiguration,
|
||||
});
|
||||
return xcBuildConfiguration?.buildSettings ?? {};
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import log from '../../log';
|
||||
import {
|
||||
ensureAppIdentifierIsDefinedAsync,
|
||||
getProjectConfigDescription,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { Platform } from '../types';
|
||||
|
||||
enum BundleIdentiferSource {
|
||||
XcodeProject,
|
||||
AppJson,
|
||||
}
|
||||
|
||||
function isBundleIdentifierValid(bundleIdentifier: string): boolean {
|
||||
return /^[a-zA-Z][a-zA-Z0-9\-.]+$/.test(bundleIdentifier);
|
||||
}
|
||||
|
||||
export async function ensureBundleIdentifierIsValidAsync(projectDir: string) {
|
||||
const bundleIdentifier = await ensureAppIdentifierIsDefinedAsync(projectDir, Platform.iOS);
|
||||
if (!isBundleIdentifierValid(bundleIdentifier)) {
|
||||
const configDescription = getProjectConfigDescription(projectDir);
|
||||
log.error(
|
||||
`Invalid format of iOS bundleId. Only alphanumeric characters, '.' and '-' are allowed, and each '.' must be followed by a letter.`
|
||||
);
|
||||
log.error(`Update "ios.bundleIdentifier" in ${configDescription} and run this command again.`);
|
||||
throw new Error('Invalid bundleIdentifier');
|
||||
}
|
||||
}
|
||||
|
||||
export async function configureBundleIdentifierAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
): Promise<void> {
|
||||
const configDescription = getProjectConfigDescription(projectDir);
|
||||
const bundleIdentifierFromPbxproj = IOSConfig.BundleIdenitifer.getBundleIdentifierFromPbxproj(
|
||||
projectDir
|
||||
);
|
||||
const bundleIdentifierFromConfig = IOSConfig.BundleIdenitifer.getBundleIdentifier(exp);
|
||||
if (bundleIdentifierFromPbxproj && bundleIdentifierFromConfig) {
|
||||
if (bundleIdentifierFromPbxproj !== bundleIdentifierFromConfig) {
|
||||
log.addNewLineIfNone();
|
||||
log.warn(
|
||||
`We detected that your Xcode project is configured with a different bundle identifier than the one defined in ${configDescription}.`
|
||||
);
|
||||
const hasBundleIdentifierInStaticConfig = await hasBundleIdentifierInStaticConfigAsync(
|
||||
projectDir,
|
||||
exp
|
||||
);
|
||||
if (!hasBundleIdentifierInStaticConfig) {
|
||||
log(`If you choose the one defined in ${configDescription} we'll automatically configure your Xcode project with it.
|
||||
However, if you choose the one defined in the Xcode project you'll have to update ${configDescription} on your own.`);
|
||||
}
|
||||
log.newLine();
|
||||
const { bundleIdentifierSource } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'bundleIdentifierSource',
|
||||
message: 'Which bundle identifier should we use?',
|
||||
choices: [
|
||||
{
|
||||
title: `${chalk.bold(bundleIdentifierFromPbxproj)} - In Xcode project`,
|
||||
value: BundleIdentiferSource.XcodeProject,
|
||||
},
|
||||
{
|
||||
title: `${chalk.bold(bundleIdentifierFromConfig)} - In your ${configDescription}`,
|
||||
value: BundleIdentiferSource.AppJson,
|
||||
},
|
||||
],
|
||||
});
|
||||
switch (bundleIdentifierSource) {
|
||||
case BundleIdentiferSource.AppJson: {
|
||||
IOSConfig.BundleIdenitifer.setBundleIdentifierForPbxproj(
|
||||
projectDir,
|
||||
bundleIdentifierFromConfig
|
||||
);
|
||||
break;
|
||||
}
|
||||
case BundleIdentiferSource.XcodeProject: {
|
||||
if (hasBundleIdentifierInStaticConfig) {
|
||||
await updateAppJsonConfigAsync(projectDir, exp, bundleIdentifierFromPbxproj);
|
||||
} else {
|
||||
throw new Error(missingBundleIdentifierMessage(configDescription));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!bundleIdentifierFromPbxproj && !bundleIdentifierFromConfig) {
|
||||
throw new Error(missingBundleIdentifierMessage(configDescription));
|
||||
} else if (bundleIdentifierFromPbxproj && !bundleIdentifierFromConfig) {
|
||||
if (getConfigFilePaths(projectDir).staticConfigPath) {
|
||||
await updateAppJsonConfigAsync(projectDir, exp, bundleIdentifierFromPbxproj);
|
||||
} else {
|
||||
throw new Error(missingBundleIdentifierMessage(configDescription));
|
||||
}
|
||||
} else if (!bundleIdentifierFromPbxproj && bundleIdentifierFromConfig) {
|
||||
IOSConfig.BundleIdenitifer.setBundleIdentifierForPbxproj(
|
||||
projectDir,
|
||||
bundleIdentifierFromConfig
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function missingBundleIdentifierMessage(configDescription: string): string {
|
||||
return `Please define "ios.bundleIdentifier" in ${configDescription} and run "eas build:configure" again.`;
|
||||
}
|
||||
|
||||
async function updateAppJsonConfigAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
newBundleIdentifier: string
|
||||
): Promise<void> {
|
||||
const paths = getConfigFilePaths(projectDir);
|
||||
assert(paths.staticConfigPath, "can't update dynamic configs");
|
||||
|
||||
const rawStaticConfig = await fs.readJSON(paths.staticConfigPath);
|
||||
rawStaticConfig.expo = {
|
||||
...rawStaticConfig.expo,
|
||||
ios: { ...rawStaticConfig.expo?.ios, bundleIdentifier: newBundleIdentifier },
|
||||
};
|
||||
await fs.writeJson(paths.staticConfigPath, rawStaticConfig, { spaces: 2 });
|
||||
|
||||
exp.ios = { ...exp.ios, bundleIdentifier: newBundleIdentifier };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if static config exists and if ios.bundleIdentifier is defined there.
|
||||
* It will return false if the value in static config is different than "ios.bundleIdentifier" in ExpoConfig
|
||||
*/
|
||||
async function hasBundleIdentifierInStaticConfigAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
): Promise<boolean> {
|
||||
if (!exp.ios?.bundleIdentifier) {
|
||||
return false;
|
||||
}
|
||||
const paths = getConfigFilePaths(projectDir);
|
||||
if (!paths.staticConfigPath) {
|
||||
return false;
|
||||
}
|
||||
const rawStaticConfig = await fs.readJson(paths.staticConfigPath);
|
||||
return rawStaticConfig?.expo?.ios?.bundleIdentifier === exp.ios.bundleIdentifier;
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
|
||||
import log from '../../log';
|
||||
import { ConfigureContext } from '../context';
|
||||
import { isExpoUpdatesInstalled } from '../utils/updates';
|
||||
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
|
||||
import {
|
||||
configureBundleIdentifierAsync,
|
||||
ensureBundleIdentifierIsValidAsync,
|
||||
} from './bundleIdentifer';
|
||||
|
||||
export async function configureIosAsync(ctx: ConfigureContext): Promise<void> {
|
||||
if (!ctx.hasIosNativeProject) {
|
||||
return;
|
||||
}
|
||||
await configureBundleIdentifierAsync(ctx.projectDir, ctx.exp);
|
||||
await ensureBundleIdentifierIsValidAsync(ctx.projectDir);
|
||||
|
||||
if (isExpoUpdatesInstalled(ctx.projectDir)) {
|
||||
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
log.withTick('iOS project configured');
|
||||
}
|
||||
|
||||
export async function validateAndSyncProjectConfigurationAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
): Promise<void> {
|
||||
const bundleIdentifierFromPbxproj = IOSConfig.BundleIdenitifer.getBundleIdentifierFromPbxproj(
|
||||
projectDir
|
||||
);
|
||||
if (!bundleIdentifierFromPbxproj || bundleIdentifierFromPbxproj !== exp.ios?.bundleIdentifier) {
|
||||
throw new Error(
|
||||
'Bundle identifier is not configured correctly in your Xcode project. Please run "eas build:configure" to configure it.'
|
||||
);
|
||||
}
|
||||
if (isExpoUpdatesInstalled(projectDir)) {
|
||||
await syncUpdatesConfigurationAsync(projectDir, exp);
|
||||
}
|
||||
}
|
||||
@@ -1,73 +1,44 @@
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { CredentialsSource, DistributionType } from '@expo/eas-json';
|
||||
import assert from 'assert';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
|
||||
import { createCredentialsContextAsync } from '../../credentials/context';
|
||||
import IosCredentialsProvider, {
|
||||
IosCredentials,
|
||||
} from '../../credentials/ios/IosCredentialsProvider';
|
||||
import { AppLookupParams } from '../../credentials/ios/credentials';
|
||||
import IosCredentialsProvider from '../../credentials/ios/IosCredentialsProvider';
|
||||
import { getAppFromContext } from '../../credentials/ios/actions/BuildCredentialsUtils';
|
||||
import { resolveEntitlementsJsonAsync } from '../../credentials/ios/appstore/entitlements';
|
||||
import { IosCredentials, Target } from '../../credentials/ios/types';
|
||||
import { CredentialsResult } from '../build';
|
||||
import { BuildContext } from '../context';
|
||||
import { ensureCredentialsAsync } from '../credentials';
|
||||
import { Platform } from '../types';
|
||||
import { logCredentialsSource } from '../utils/credentials';
|
||||
|
||||
export async function ensureIosCredentialsAsync(
|
||||
ctx: BuildContext<Platform.iOS>
|
||||
buildCtx: BuildContext<Platform.IOS>,
|
||||
targets: Target[]
|
||||
): Promise<CredentialsResult<IosCredentials> | undefined> {
|
||||
if (!shouldProvideCredentials(ctx)) {
|
||||
if (!shouldProvideCredentials(buildCtx)) {
|
||||
return;
|
||||
}
|
||||
assert(ctx.commandCtx.exp?.ios?.bundleIdentifier, 'ios.bundleIdentifier is required');
|
||||
return await resolveIosCredentialsAsync(ctx.commandCtx.projectDir, {
|
||||
app: {
|
||||
accountName: ctx.commandCtx.accountName,
|
||||
projectName: ctx.commandCtx.projectName,
|
||||
bundleIdentifier: ctx.commandCtx.exp.ios.bundleIdentifier,
|
||||
|
||||
const provider = new IosCredentialsProvider(buildCtx.credentialsCtx, {
|
||||
app: getAppFromContext(buildCtx.credentialsCtx),
|
||||
targets,
|
||||
iosCapabilitiesOptions: {
|
||||
entitlements: await resolveEntitlementsJsonAsync(
|
||||
buildCtx.projectDir,
|
||||
buildCtx.workflow,
|
||||
buildCtx.buildProfile.env ?? {}
|
||||
),
|
||||
},
|
||||
workflow: ctx.buildProfile.workflow,
|
||||
credentialsSource: ctx.buildProfile.credentialsSource,
|
||||
distribution: ctx.buildProfile.distribution ?? DistributionType.STORE,
|
||||
nonInteractive: ctx.commandCtx.nonInteractive,
|
||||
skipCredentialsCheck: ctx.commandCtx.skipCredentialsCheck,
|
||||
distribution: buildCtx.buildProfile.distribution ?? 'store',
|
||||
enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning,
|
||||
});
|
||||
}
|
||||
|
||||
interface ResolveCredentialsParams {
|
||||
app: AppLookupParams;
|
||||
workflow: Workflow;
|
||||
credentialsSource: CredentialsSource;
|
||||
distribution: DistributionType;
|
||||
nonInteractive: boolean;
|
||||
skipCredentialsCheck: boolean;
|
||||
}
|
||||
const { credentialsSource } = buildCtx.buildProfile;
|
||||
|
||||
export async function resolveIosCredentialsAsync(
|
||||
projectDir: string,
|
||||
params: ResolveCredentialsParams
|
||||
): Promise<CredentialsResult<IosCredentials>> {
|
||||
const provider = new IosCredentialsProvider(
|
||||
await createCredentialsContextAsync(projectDir, {
|
||||
nonInteractive: params.nonInteractive,
|
||||
}),
|
||||
{
|
||||
app: params.app,
|
||||
distribution: params.distribution,
|
||||
skipCredentialsCheck: params.skipCredentialsCheck,
|
||||
}
|
||||
);
|
||||
const credentialsSource = await ensureCredentialsAsync(
|
||||
provider,
|
||||
params.workflow,
|
||||
params.credentialsSource,
|
||||
params.nonInteractive
|
||||
);
|
||||
logCredentialsSource(credentialsSource, Platform.IOS);
|
||||
return {
|
||||
credentials: await provider.getCredentialsAsync(credentialsSource),
|
||||
source: credentialsSource,
|
||||
};
|
||||
}
|
||||
|
||||
function shouldProvideCredentials(ctx: BuildContext<Platform.iOS>): boolean {
|
||||
return true;
|
||||
function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
|
||||
return !buildCtx.buildProfile.simulator;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Env, Ios } from '@expo/eas-build-job';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { IosJobInput, IosJobSecretsInput } from '../../graphql/generated';
|
||||
import { transformProjectArchive, transformWorkflow } from '../graphql';
|
||||
|
||||
export function transformJob(job: Ios.Job): IosJobInput {
|
||||
return {
|
||||
type: transformWorkflow(job.type),
|
||||
projectArchive: transformProjectArchive(job.projectArchive),
|
||||
projectRootDirectory: job.projectRootDirectory,
|
||||
releaseChannel: job.releaseChannel,
|
||||
updates: job.updates,
|
||||
secrets: transformIosSecrets(job.secrets),
|
||||
builderEnvironment: job.builderEnvironment,
|
||||
cache: job.cache,
|
||||
scheme: job.scheme,
|
||||
buildConfiguration: job.buildConfiguration,
|
||||
artifactPath: job.artifactPath,
|
||||
username: job.username,
|
||||
developmentClient: job.developmentClient,
|
||||
simulator: job.simulator,
|
||||
};
|
||||
}
|
||||
|
||||
function transformIosSecrets(secrets: {
|
||||
buildCredentials?: Ios.BuildCredentials;
|
||||
env?: Env;
|
||||
}): IosJobSecretsInput {
|
||||
const buildCredentials: IosJobSecretsInput['buildCredentials'] = [];
|
||||
|
||||
for (const targetName of Object.keys(secrets.buildCredentials ?? {})) {
|
||||
buildCredentials.push({
|
||||
targetName,
|
||||
provisioningProfileBase64: nullthrows(secrets.buildCredentials?.[targetName])
|
||||
.provisioningProfileBase64,
|
||||
distributionCertificate: nullthrows(secrets.buildCredentials?.[targetName])
|
||||
.distributionCertificate,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
buildCredentials,
|
||||
environmentSecrets: secrets.env,
|
||||
};
|
||||
}
|
||||
@@ -1,118 +1,81 @@
|
||||
import {
|
||||
ArchiveSource,
|
||||
ArchiveSourceType,
|
||||
Job,
|
||||
Workflow,
|
||||
iOS,
|
||||
sanitizeJob,
|
||||
} from '@expo/eas-build-job';
|
||||
import { iOSGenericBuildProfile, iOSManagedBuildProfile } from '@expo/eas-json';
|
||||
import { ArchiveSource, Ios, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
|
||||
import path from 'path';
|
||||
import slash from 'slash';
|
||||
|
||||
import { readSecretEnvsAsync } from '../../credentials/credentialsJson/read';
|
||||
import { IosCredentials } from '../../credentials/ios/IosCredentialsProvider';
|
||||
import { getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import { gitRootDirectoryAsync } from '../../utils/git';
|
||||
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
|
||||
import { getUsername } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { BuildContext } from '../context';
|
||||
import { Platform } from '../types';
|
||||
|
||||
interface JobData {
|
||||
archiveBucketKey: string;
|
||||
projectArchive: ArchiveSource;
|
||||
credentials?: IosCredentials;
|
||||
projectConfiguration: {
|
||||
iosNativeProjectScheme?: string;
|
||||
};
|
||||
buildScheme: string;
|
||||
}
|
||||
|
||||
const cacheDefaults = {
|
||||
disabled: false,
|
||||
customPaths: [],
|
||||
cacheDefaultPaths: true,
|
||||
};
|
||||
|
||||
export async function prepareJobAsync(
|
||||
ctx: BuildContext<Platform.iOS>,
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData
|
||||
): Promise<Job> {
|
||||
if (ctx.buildProfile.workflow === Workflow.Generic) {
|
||||
const partialJob = await prepareGenericJobAsync(ctx, jobData, ctx.buildProfile);
|
||||
return sanitizeJob(partialJob);
|
||||
} else if (ctx.buildProfile.workflow === Workflow.Managed) {
|
||||
const partialJob = await prepareManagedJobAsync(ctx, jobData, ctx.buildProfile);
|
||||
return sanitizeJob(partialJob);
|
||||
} else {
|
||||
throw new Error("Unknown workflow. Shouldn't happen");
|
||||
const projectRootDirectory =
|
||||
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
|
||||
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
|
||||
const buildCredentials: Ios.Job['secrets']['buildCredentials'] = {};
|
||||
if (jobData.credentials) {
|
||||
const targetNames = Object.keys(jobData.credentials);
|
||||
for (const targetName of targetNames) {
|
||||
buildCredentials[targetName] = prepareTargetCredentials(jobData.credentials[targetName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface CommonJobProperties {
|
||||
platform: Platform.iOS;
|
||||
projectArchive: ArchiveSource;
|
||||
releaseChannel: string;
|
||||
secrets: {
|
||||
buildCredentials?: {
|
||||
provisioningProfileBase64: string;
|
||||
distributionCertificate: {
|
||||
dataBase64: string;
|
||||
password: string;
|
||||
};
|
||||
};
|
||||
secretEnvs?: Record<string, string>;
|
||||
};
|
||||
}
|
||||
|
||||
async function prepareJobCommonAsync(
|
||||
ctx: BuildContext<Platform.iOS>,
|
||||
jobData: JobData
|
||||
): Promise<Partial<CommonJobProperties>> {
|
||||
const secretEnvs = await readSecretEnvsAsync(ctx.commandCtx.projectDir);
|
||||
const buildCredentials = jobData.credentials
|
||||
? {
|
||||
buildCredentials: {
|
||||
provisioningProfileBase64: jobData.credentials.provisioningProfile,
|
||||
distributionCertificate: {
|
||||
dataBase64: jobData.credentials.distributionCertificate.certP12,
|
||||
password: jobData.credentials.distributionCertificate.certPassword,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {};
|
||||
|
||||
return {
|
||||
platform: Platform.iOS,
|
||||
projectArchive: {
|
||||
type: ArchiveSourceType.S3,
|
||||
bucketKey: jobData.archiveBucketKey,
|
||||
const job: Ios.Job = {
|
||||
type: ctx.workflow,
|
||||
platform: Platform.IOS,
|
||||
projectArchive: jobData.projectArchive,
|
||||
projectRootDirectory,
|
||||
builderEnvironment: {
|
||||
image: ctx.buildProfile.image,
|
||||
node: ctx.buildProfile.node,
|
||||
yarn: ctx.buildProfile.yarn,
|
||||
bundler: ctx.buildProfile.bundler,
|
||||
cocoapods: ctx.buildProfile.cocoapods,
|
||||
fastlane: ctx.buildProfile.fastlane,
|
||||
expoCli: ctx.buildProfile.expoCli,
|
||||
env: ctx.buildProfile.env,
|
||||
},
|
||||
cache: {
|
||||
...cacheDefaults,
|
||||
...ctx.buildProfile.cache,
|
||||
clear: ctx.clearCache,
|
||||
},
|
||||
secrets: {
|
||||
...(secretEnvs ? { secretEnvs } : {}),
|
||||
...buildCredentials,
|
||||
buildCredentials,
|
||||
},
|
||||
releaseChannel: ctx.buildProfile.releaseChannel,
|
||||
updates: { channel: ctx.buildProfile.channel },
|
||||
developmentClient: ctx.buildProfile.developmentClient,
|
||||
simulator: ctx.buildProfile.simulator,
|
||||
scheme: jobData.buildScheme,
|
||||
buildConfiguration: ctx.buildProfile.buildConfiguration,
|
||||
artifactPath: ctx.buildProfile.artifactPath,
|
||||
username,
|
||||
};
|
||||
return sanitizeJob(job);
|
||||
}
|
||||
|
||||
function prepareTargetCredentials(targetCredentials: TargetCredentials): Ios.TargetCredentials {
|
||||
return {
|
||||
provisioningProfileBase64: targetCredentials.provisioningProfile,
|
||||
distributionCertificate: {
|
||||
dataBase64: targetCredentials.distributionCertificate.certificateP12,
|
||||
password: targetCredentials.distributionCertificate.certificatePassword,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function prepareGenericJobAsync(
|
||||
ctx: BuildContext<Platform.iOS>,
|
||||
jobData: JobData,
|
||||
buildProfile: iOSGenericBuildProfile
|
||||
): Promise<Partial<iOS.GenericJob>> {
|
||||
const projectRootDirectory = path.relative(await gitRootDirectoryAsync(), process.cwd()) || '.';
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, jobData)),
|
||||
type: Workflow.Generic,
|
||||
scheme: jobData.projectConfiguration.iosNativeProjectScheme,
|
||||
artifactPath: buildProfile.artifactPath,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
projectRootDirectory,
|
||||
};
|
||||
}
|
||||
|
||||
async function prepareManagedJobAsync(
|
||||
ctx: BuildContext<Platform.iOS>,
|
||||
jobData: JobData,
|
||||
buildProfile: iOSManagedBuildProfile
|
||||
): Promise<Partial<iOS.ManagedJob>> {
|
||||
const projectRootDirectory = path.relative(await gitRootDirectoryAsync(), process.cwd()) || '.';
|
||||
const accountName = await getProjectAccountNameAsync(ctx.commandCtx.projectDir);
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, jobData)),
|
||||
type: Workflow.Managed,
|
||||
username: accountName,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
projectRootDirectory,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { BuildProfile, IosVersionAutoIncrement } from '@expo/eas-json';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { isExpoUpdatesInstalled } from '../utils/updates';
|
||||
import { syncUpdatesConfigurationAsync } from './UpdatesModule';
|
||||
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
|
||||
|
||||
export async function syncProjectConfigurationAsync({
|
||||
projectDir,
|
||||
exp,
|
||||
buildProfile,
|
||||
buildSettings,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
buildProfile: BuildProfile<Platform.IOS>;
|
||||
buildSettings: XCBuildConfiguration['buildSettings'];
|
||||
}): Promise<void> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||
const { autoIncrement } = buildProfile;
|
||||
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement ?? false);
|
||||
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
if (isExpoUpdatesInstalled(projectDir)) {
|
||||
await syncUpdatesConfigurationAsync(projectDir, exp);
|
||||
}
|
||||
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy, buildSettings });
|
||||
} else {
|
||||
await bumpVersionInAppJsonAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
|
||||
}
|
||||
}
|
||||
|
||||
function resolveVersionBumpStrategy(autoIncrement: IosVersionAutoIncrement): BumpStrategy {
|
||||
if (autoIncrement === true) {
|
||||
return BumpStrategy.BUILD_NUMBER;
|
||||
} else if (autoIncrement === false) {
|
||||
return BumpStrategy.NOOP;
|
||||
} else if (autoIncrement === 'buildNumber') {
|
||||
return BumpStrategy.BUILD_NUMBER;
|
||||
} else {
|
||||
return BumpStrategy.APP_VERSION;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import chalk from 'chalk';
|
||||
import path from 'path';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
|
||||
import Log from '../../log';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { readPlistAsync, writePlistAsync } from '../../utils/plist';
|
||||
import { updateAppJsonConfigAsync } from '../utils/appJson';
|
||||
import { bumpAppVersionAsync } from '../utils/version';
|
||||
|
||||
export enum BumpStrategy {
|
||||
APP_VERSION,
|
||||
BUILD_NUMBER,
|
||||
NOOP,
|
||||
}
|
||||
|
||||
export async function bumpVersionAsync({
|
||||
bumpStrategy,
|
||||
projectDir,
|
||||
exp,
|
||||
buildSettings,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
bumpStrategy: BumpStrategy;
|
||||
buildSettings: XCBuildConfiguration['buildSettings'];
|
||||
}): Promise<void> {
|
||||
if (bumpStrategy === BumpStrategy.NOOP) {
|
||||
return;
|
||||
}
|
||||
ensureStaticConfigExists(projectDir);
|
||||
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
|
||||
await bumpVersionInAppJsonAsync({ bumpStrategy, projectDir, exp });
|
||||
Log.log('Updated versions in app.json');
|
||||
await writeVersionsToInfoPlistAsync({ projectDir, exp, infoPlist, buildSettings });
|
||||
Log.log('Synchronized versions with Info.plist');
|
||||
}
|
||||
|
||||
export async function bumpVersionInAppJsonAsync({
|
||||
bumpStrategy,
|
||||
projectDir,
|
||||
exp,
|
||||
}: {
|
||||
bumpStrategy: BumpStrategy;
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
}): Promise<void> {
|
||||
if (bumpStrategy === BumpStrategy.NOOP) {
|
||||
return;
|
||||
}
|
||||
ensureStaticConfigExists(projectDir);
|
||||
Log.addNewLineIfNone();
|
||||
if (bumpStrategy === BumpStrategy.APP_VERSION) {
|
||||
const appVersion = IOSConfig.Version.getVersion(exp);
|
||||
await bumpAppVersionAsync({ appVersion, projectDir, exp });
|
||||
} else {
|
||||
const buildNumber = IOSConfig.Version.getBuildNumber(exp);
|
||||
if (buildNumber.match(/^\d+(\.\d+)*$/)) {
|
||||
const comps = buildNumber.split('.');
|
||||
comps[comps.length - 1] = String(Number(comps[comps.length - 1]) + 1);
|
||||
const bumpedBuildNumber = comps.join('.');
|
||||
Log.log(
|
||||
`Bumping ${chalk.bold('expo.ios.buildNumber')} from ${chalk.bold(
|
||||
buildNumber
|
||||
)} to ${chalk.bold(bumpedBuildNumber)}`
|
||||
);
|
||||
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
|
||||
config.ios = { ...config.ios, buildNumber: String(bumpedBuildNumber) };
|
||||
});
|
||||
} else {
|
||||
Log.log(`${chalk.bold('expo.ios.buildNumber')} = ${chalk.bold(buildNumber)} is not a number`);
|
||||
const { bumpedBuildNumber } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'bumpedBuildNumber',
|
||||
message: 'What is the next build number?',
|
||||
});
|
||||
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
|
||||
config.ios = { ...config.ios, buildNumber: String(bumpedBuildNumber) };
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function readShortVersionAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
buildSettings: XCBuildConfiguration['buildSettings']
|
||||
): Promise<string | undefined> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
|
||||
return (
|
||||
infoPlist.CFBundleShortVersionString &&
|
||||
evaluateTemplateString(infoPlist.CFBundleShortVersionString, buildSettings)
|
||||
);
|
||||
} else {
|
||||
return exp.version;
|
||||
}
|
||||
}
|
||||
|
||||
export async function readBuildNumberAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
buildSettings: XCBuildConfiguration['buildSettings']
|
||||
): Promise<string | undefined> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
|
||||
return (
|
||||
infoPlist.CFBundleVersion && evaluateTemplateString(infoPlist.CFBundleVersion, buildSettings)
|
||||
);
|
||||
} else {
|
||||
return IOSConfig.Version.getBuildNumber(exp);
|
||||
}
|
||||
}
|
||||
|
||||
export async function maybeResolveVersionsAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
buildSettings: XCBuildConfiguration['buildSettings']
|
||||
): Promise<{ appVersion?: string; appBuildVersion?: string }> {
|
||||
try {
|
||||
return {
|
||||
appBuildVersion: await readBuildNumberAsync(projectDir, exp, buildSettings),
|
||||
appVersion: await readShortVersionAsync(projectDir, exp, buildSettings),
|
||||
};
|
||||
} catch (err: any) {
|
||||
Log.warn('Failed to read app versions.');
|
||||
Log.debug(err);
|
||||
Log.warn(err.message);
|
||||
Log.warn('Proceeding anyway...');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function writeVersionsToInfoPlistAsync({
|
||||
projectDir,
|
||||
exp,
|
||||
infoPlist,
|
||||
buildSettings,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
infoPlist: IOSConfig.InfoPlist;
|
||||
buildSettings: XCBuildConfiguration['buildSettings'];
|
||||
}): Promise<IOSConfig.InfoPlist> {
|
||||
let updatedInfoPlist = IOSConfig.Version.setVersion(exp, infoPlist);
|
||||
updatedInfoPlist = IOSConfig.Version.setBuildNumber(exp, updatedInfoPlist);
|
||||
await writeInfoPlistAsync({ projectDir, infoPlist: updatedInfoPlist, buildSettings });
|
||||
return updatedInfoPlist;
|
||||
}
|
||||
|
||||
export function getInfoPlistPath(
|
||||
projectDir: string,
|
||||
buildSettings: XCBuildConfiguration['buildSettings']
|
||||
): string {
|
||||
if (buildSettings.INFOPLIST_FILE) {
|
||||
const infoPlistFile = buildSettings.INFOPLIST_FILE.startsWith('"')
|
||||
? buildSettings.INFOPLIST_FILE.slice(1, -1)
|
||||
: buildSettings.INFOPLIST_FILE;
|
||||
const iosDir = path.join(projectDir, 'ios');
|
||||
const plistPath = evaluateTemplateString(infoPlistFile, {
|
||||
...buildSettings,
|
||||
SRCROOT: iosDir,
|
||||
});
|
||||
return path.isAbsolute(plistPath) ? plistPath : path.resolve(iosDir, plistPath);
|
||||
} else {
|
||||
return IOSConfig.Paths.getInfoPlistPath(projectDir);
|
||||
}
|
||||
}
|
||||
|
||||
async function readInfoPlistAsync(
|
||||
projectDir: string,
|
||||
buildSettings: XCBuildConfiguration['buildSettings']
|
||||
): Promise<IOSConfig.InfoPlist> {
|
||||
const infoPlistPath = getInfoPlistPath(projectDir, buildSettings);
|
||||
return ((await readPlistAsync(infoPlistPath)) ?? {}) as IOSConfig.InfoPlist;
|
||||
}
|
||||
|
||||
async function writeInfoPlistAsync({
|
||||
projectDir,
|
||||
infoPlist,
|
||||
buildSettings,
|
||||
}: {
|
||||
projectDir: string;
|
||||
infoPlist: IOSConfig.InfoPlist;
|
||||
buildSettings: XCBuildConfiguration['buildSettings'];
|
||||
}): Promise<void> {
|
||||
const infoPlistPath = getInfoPlistPath(projectDir, buildSettings);
|
||||
await writePlistAsync(infoPlistPath, infoPlist);
|
||||
}
|
||||
|
||||
function ensureStaticConfigExists(projectDir: string): void {
|
||||
const paths = getConfigFilePaths(projectDir);
|
||||
if (!paths.staticConfigPath) {
|
||||
throw new Error('autoIncrement option is not supported when using app.config.js');
|
||||
}
|
||||
}
|
||||
|
||||
export function evaluateTemplateString(
|
||||
s: string,
|
||||
buildSettings: XCBuildConfiguration['buildSettings']
|
||||
): string {
|
||||
// necessary because XCBuildConfiguration['buildSettings'] is not a plain object
|
||||
const vars = { ...buildSettings };
|
||||
return s.replace(/\$\((\w+)\)/g, (match, key) => {
|
||||
if (vars.hasOwnProperty(key)) {
|
||||
const value = String(vars[key]);
|
||||
return value.startsWith('"') ? value.slice(1, -1) : value;
|
||||
} else {
|
||||
return match;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Job } from '@expo/eas-build-job';
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import { ChildProcess } from 'child_process';
|
||||
import semver from 'semver';
|
||||
|
||||
import { ora } from '../ora';
|
||||
|
||||
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
|
||||
const PLUGIN_PACKAGE_VERSION = '0.0.58';
|
||||
|
||||
export interface LocalBuildOptions {
|
||||
enable: boolean;
|
||||
skipCleanup?: boolean;
|
||||
skipNativeBuild?: boolean;
|
||||
artifactsDir?: string;
|
||||
artifactPath?: string;
|
||||
workingdir?: string;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
export async function runLocalBuildAsync(job: Job, options: LocalBuildOptions): Promise<void> {
|
||||
const { command, args } = await getCommandAndArgsAsync(job);
|
||||
let spinner;
|
||||
if (!options.verbose) {
|
||||
spinner = ora().start(options.skipNativeBuild ? 'Preparing project' : 'Building project');
|
||||
}
|
||||
let childProcess: ChildProcess | undefined;
|
||||
const interruptHandler = (): void => {
|
||||
if (childProcess) {
|
||||
childProcess.kill();
|
||||
}
|
||||
};
|
||||
process.on('SIGINT', interruptHandler);
|
||||
try {
|
||||
const spawnPromise = spawnAsync(command, args, {
|
||||
stdio: options.verbose ? 'inherit' : 'pipe',
|
||||
env: {
|
||||
...process.env,
|
||||
EAS_LOCAL_BUILD_WORKINGDIR: options.workingdir ?? process.env.EAS_LOCAL_BUILD_WORKINGDIR,
|
||||
...(options.skipCleanup || options.skipNativeBuild
|
||||
? { EAS_LOCAL_BUILD_SKIP_CLEANUP: '1' }
|
||||
: {}),
|
||||
...(options.skipNativeBuild ? { EAS_LOCAL_BUILD_SKIP_NATIVE_BUILD: '1' } : {}),
|
||||
...(options.artifactsDir ? { EAS_LOCAL_BUILD_ARTIFACTS_DIR: options.artifactsDir } : {}),
|
||||
...(options.artifactPath ? { EAS_LOCAL_BUILD_ARTIFACT_PATH: options.artifactPath } : {}),
|
||||
},
|
||||
});
|
||||
childProcess = spawnPromise.child;
|
||||
await spawnPromise;
|
||||
} finally {
|
||||
process.removeListener('SIGINT', interruptHandler);
|
||||
spinner?.stop();
|
||||
}
|
||||
}
|
||||
|
||||
async function getCommandAndArgsAsync(job: Job): Promise<{ command: string; args: string[] }> {
|
||||
const jobBase64 = Buffer.from(JSON.stringify({ job })).toString('base64');
|
||||
if (process.env.EAS_LOCAL_BUILD_PLUGIN_PATH) {
|
||||
return {
|
||||
command: process.env.EAS_LOCAL_BUILD_PLUGIN_PATH,
|
||||
args: [jobBase64],
|
||||
};
|
||||
} else {
|
||||
const args = [`${PLUGIN_PACKAGE_NAME}@${PLUGIN_PACKAGE_VERSION}`, jobBase64];
|
||||
if (await isAtLeastNpm7Async()) {
|
||||
// npx shipped with npm >= 7.0.0 requires the "-y" flag to run commands without
|
||||
// prompting the user to install a package that is used for the first time
|
||||
args.unshift('-y');
|
||||
}
|
||||
return {
|
||||
command: 'npx',
|
||||
args,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function isAtLeastNpm7Async(): Promise<boolean> {
|
||||
const version = (await spawnAsync('npm', ['--version'])).stdout.trim();
|
||||
return semver.gte(version, '7.0.0');
|
||||
}
|
||||
@@ -1,34 +1,164 @@
|
||||
import { CredentialsSource, DistributionType } from '@expo/eas-json';
|
||||
import { Updates } from '@expo/config-plugins';
|
||||
import { Metadata, Platform, sanitizeMetadata } from '@expo/eas-build-job';
|
||||
import { IosEnterpriseProvisioning } from '@expo/eas-json';
|
||||
import fs from 'fs-extra';
|
||||
import resolveFrom from 'resolve-from';
|
||||
|
||||
import { getAppIdentifierAsync } from '../project/projectUtils';
|
||||
import Log from '../log';
|
||||
import { getUsername } from '../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import {
|
||||
readChannelSafelyAsync as readAndroidChannelSafelyAsync,
|
||||
readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync,
|
||||
} from './android/UpdatesModule';
|
||||
import { maybeResolveVersionsAsync as maybeResolveAndroidVersionsAsync } from './android/version';
|
||||
import { BuildContext } from './context';
|
||||
import { BuildMetadata, Platform } from './types';
|
||||
import {
|
||||
readChannelSafelyAsync as readIosChannelSafelyAsync,
|
||||
readReleaseChannelSafelyAsync as readIosReleaseChannelSafelyAsync,
|
||||
} from './ios/UpdatesModule';
|
||||
import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios/version';
|
||||
import { isExpoUpdatesInstalled } from './utils/updates';
|
||||
|
||||
/**
|
||||
* We use require() to exclude package.json from TypeScript's analysis since it lives outside
|
||||
* the src directory and would change the directory structure of the emitted files
|
||||
* under the build directory
|
||||
*/
|
||||
const packageJSON = require('../../package.json');
|
||||
|
||||
export async function collectMetadata<T extends Platform>(
|
||||
ctx: BuildContext<T>,
|
||||
{
|
||||
credentialsSource,
|
||||
}: {
|
||||
credentialsSource?: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
|
||||
// TODO(JJ): Replace this with the getRuntimeVersionNullable function in @expo/config-plugins
|
||||
function getRuntimeVersionNullable(
|
||||
...[config, platform]: Parameters<typeof Updates.getRuntimeVersion>
|
||||
): string | null {
|
||||
try {
|
||||
return Updates.getRuntimeVersion(config, platform);
|
||||
} catch (e) {
|
||||
Log.debug(e);
|
||||
return null;
|
||||
}
|
||||
): Promise<BuildMetadata> {
|
||||
return {
|
||||
appVersion: ctx.commandCtx.exp.version!,
|
||||
cliVersion: packageJSON.version,
|
||||
workflow: ctx.buildProfile.workflow,
|
||||
credentialsSource,
|
||||
sdkVersion: ctx.commandCtx.exp.sdkVersion,
|
||||
}
|
||||
|
||||
export async function collectMetadataAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<Metadata> {
|
||||
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
|
||||
const distribution =
|
||||
('simulator' in ctx.buildProfile && ctx.buildProfile.simulator
|
||||
? 'simulator'
|
||||
: ctx.buildProfile.distribution) ?? 'store';
|
||||
const metadata = {
|
||||
trackingContext: ctx.trackingCtx,
|
||||
distribution: ctx.buildProfile.distribution ?? DistributionType.STORE,
|
||||
appName: ctx.commandCtx.exp.name,
|
||||
appIdentifier:
|
||||
(await getAppIdentifierAsync(ctx.commandCtx.projectDir, ctx.platform)) ?? undefined,
|
||||
...(await maybeResolveVersionsAsync(ctx)),
|
||||
cliVersion: easCliVersion,
|
||||
workflow: ctx.workflow,
|
||||
credentialsSource: ctx.buildProfile.credentialsSource,
|
||||
sdkVersion: ctx.exp.sdkVersion,
|
||||
runtimeVersion: getRuntimeVersionNullable(ctx.exp, ctx.platform) ?? undefined,
|
||||
reactNativeVersion: await getReactNativeVersionAsync(ctx),
|
||||
...channelOrReleaseChannel,
|
||||
distribution,
|
||||
appName: ctx.exp.name,
|
||||
appIdentifier: resolveAppIdentifier(ctx),
|
||||
buildProfile: ctx.buildProfileName,
|
||||
gitCommitHash: await getVcsClient().getCommitHashAsync(),
|
||||
isGitWorkingTreeDirty: await getVcsClient().hasUncommittedChangesAsync(),
|
||||
username: getUsername(ctx.exp, await ensureLoggedInAsync()),
|
||||
...(ctx.platform === Platform.IOS && {
|
||||
iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning(
|
||||
ctx as BuildContext<Platform.IOS>
|
||||
),
|
||||
}),
|
||||
};
|
||||
return sanitizeMetadata(metadata);
|
||||
}
|
||||
|
||||
async function maybeResolveVersionsAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<{ appBuildVersion?: string; appVersion?: string }> {
|
||||
if (ctx.platform === Platform.IOS) {
|
||||
const iosContext = ctx as BuildContext<Platform.IOS>;
|
||||
return await maybeResolveIosVersionsAsync(
|
||||
ctx.projectDir,
|
||||
ctx.exp,
|
||||
iosContext.ios.applicationTargetBuildSettings
|
||||
);
|
||||
} else if (ctx.platform === Platform.ANDROID) {
|
||||
const androidCtx = ctx as BuildContext<Platform.ANDROID>;
|
||||
return await maybeResolveAndroidVersionsAsync(ctx.projectDir, ctx.exp, androidCtx.buildProfile);
|
||||
} else {
|
||||
throw new Error(`Unsupported platform ${ctx.platform}`);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveAppIdentifier<T extends Platform>(ctx: BuildContext<T>): string {
|
||||
if (ctx.platform === Platform.IOS) {
|
||||
return (ctx as BuildContext<Platform.IOS>).ios.bundleIdentifier;
|
||||
} else {
|
||||
return (ctx as BuildContext<Platform.ANDROID>).android.applicationId;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveChannelOrReleaseChannelAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<{ channel: string } | { releaseChannel: string } | null> {
|
||||
if (!isExpoUpdatesInstalled(ctx.projectDir)) {
|
||||
return null;
|
||||
}
|
||||
if (ctx.buildProfile.channel) {
|
||||
return { channel: ctx.buildProfile.channel };
|
||||
}
|
||||
if (ctx.buildProfile.releaseChannel) {
|
||||
return { releaseChannel: ctx.buildProfile.releaseChannel };
|
||||
}
|
||||
const channel = await getNativeChannelAsync(ctx);
|
||||
if (channel) {
|
||||
return { channel };
|
||||
}
|
||||
const releaseChannel = await getNativeReleaseChannelAsync(ctx);
|
||||
return { releaseChannel };
|
||||
}
|
||||
|
||||
async function getNativeReleaseChannelAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<string> {
|
||||
switch (ctx.platform) {
|
||||
case Platform.ANDROID: {
|
||||
return (await readAndroidReleaseChannelSafelyAsync(ctx.projectDir)) ?? 'default';
|
||||
}
|
||||
case Platform.IOS: {
|
||||
return (await readIosReleaseChannelSafelyAsync(ctx.projectDir)) ?? 'default';
|
||||
}
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
}
|
||||
|
||||
async function getNativeChannelAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<string | undefined> {
|
||||
switch (ctx.platform) {
|
||||
case Platform.ANDROID: {
|
||||
return (await readAndroidChannelSafelyAsync(ctx.projectDir)) ?? undefined;
|
||||
}
|
||||
case Platform.IOS: {
|
||||
return (await readIosChannelSafelyAsync(ctx.projectDir)) ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function getReactNativeVersionAsync(
|
||||
ctx: BuildContext<Platform>
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const reactNativePackageJsonPath = resolveFrom(ctx.projectDir, 'react-native/package.json');
|
||||
return (await fs.readJson(reactNativePackageJsonPath)).version;
|
||||
} catch (err) {
|
||||
Log.debug('Failed to resolve react-native version:');
|
||||
Log.debug(err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveIosEnterpriseProvisioning(
|
||||
ctx: BuildContext<Platform.IOS>
|
||||
): IosEnterpriseProvisioning | undefined {
|
||||
return ctx.buildProfile.enterpriseProvisioning;
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { BuildProfile, SubmitProfile } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { AppPlatform, BuildFragment, BuildStatus, SubmissionFragment } from '../graphql/generated';
|
||||
import { toAppPlatform, toPlatform } from '../graphql/types/AppPlatform';
|
||||
import Log from '../log';
|
||||
import {
|
||||
RequestedPlatform,
|
||||
appPlatformDisplayNames,
|
||||
appPlatformEmojis,
|
||||
toPlatforms,
|
||||
} from '../platform';
|
||||
import { checkExpoSdkIsSupportedAsync } from '../project/expoSdk';
|
||||
import { validateMetroConfigForManagedWorkflowAsync } from '../project/metroConfig';
|
||||
import { createSubmissionContextAsync } from '../submit/context';
|
||||
import {
|
||||
submitAsync,
|
||||
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
|
||||
} from '../submit/submit';
|
||||
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
|
||||
import { ProfileData, getProfilesAsync } from '../utils/profiles';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { prepareAndroidBuildAsync } from './android/build';
|
||||
import { BuildRequestSender, waitForBuildEndAsync } from './build';
|
||||
import { ensureProjectConfiguredAsync } from './configure';
|
||||
import { BuildContext } from './context';
|
||||
import { createBuildContextAsync } from './createContext';
|
||||
import { prepareIosBuildAsync } from './ios/build';
|
||||
import { LocalBuildOptions } from './local';
|
||||
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from './utils/devClient';
|
||||
import { printBuildResults, printLogsUrls } from './utils/printBuildInfo';
|
||||
import { ensureRepoIsCleanAsync } from './utils/repository';
|
||||
|
||||
let metroConfigValidated = false;
|
||||
let sdkVersionChecked = false;
|
||||
|
||||
export interface BuildFlags {
|
||||
requestedPlatform: RequestedPlatform;
|
||||
profile?: string;
|
||||
nonInteractive: boolean;
|
||||
wait: boolean;
|
||||
clearCache: boolean;
|
||||
json: boolean;
|
||||
autoSubmit: boolean;
|
||||
submitProfile?: string;
|
||||
localBuildOptions: LocalBuildOptions;
|
||||
}
|
||||
|
||||
export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFlags): Promise<void> {
|
||||
await getVcsClient().ensureRepoExistsAsync();
|
||||
await ensureRepoIsCleanAsync(flags.nonInteractive);
|
||||
|
||||
await ensureProjectConfiguredAsync({
|
||||
projectDir,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
});
|
||||
|
||||
const platforms = toPlatforms(flags.requestedPlatform);
|
||||
const buildProfiles = await getProfilesAsync({
|
||||
type: 'build',
|
||||
projectDir,
|
||||
platforms,
|
||||
profileName: flags.profile ?? undefined,
|
||||
});
|
||||
|
||||
await ensureExpoDevClientInstalledForDevClientBuildsAsync({
|
||||
projectDir,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
buildProfiles,
|
||||
});
|
||||
|
||||
const startedBuilds: {
|
||||
build: BuildFragment;
|
||||
buildProfile: ProfileData<'build'>;
|
||||
}[] = [];
|
||||
const buildCtxByPlatform: { [p in AppPlatform]?: BuildContext<Platform> } = {};
|
||||
|
||||
for (const buildProfile of buildProfiles) {
|
||||
const { build: maybeBuild, buildCtx } = await prepareAndStartBuildAsync({
|
||||
projectDir,
|
||||
flags,
|
||||
moreBuilds: platforms.length > 1,
|
||||
buildProfile,
|
||||
});
|
||||
if (maybeBuild) {
|
||||
startedBuilds.push({ build: maybeBuild, buildProfile });
|
||||
}
|
||||
buildCtxByPlatform[toAppPlatform(buildProfile.platform)] = buildCtx;
|
||||
}
|
||||
|
||||
if (flags.localBuildOptions.enable) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
printLogsUrls(startedBuilds.map(startedBuild => startedBuild.build));
|
||||
Log.newLine();
|
||||
|
||||
const submissions: SubmissionFragment[] = [];
|
||||
if (flags.autoSubmit) {
|
||||
const submitProfiles = await getProfilesAsync({
|
||||
projectDir,
|
||||
platforms,
|
||||
profileName: flags.submitProfile,
|
||||
type: 'submit',
|
||||
});
|
||||
for (const startedBuild of startedBuilds) {
|
||||
const submitProfile = nullthrows(
|
||||
submitProfiles.find(
|
||||
({ platform }) => toAppPlatform(platform) === startedBuild.build.platform
|
||||
)
|
||||
).profile;
|
||||
const submission = await prepareAndStartSubmissionAsync({
|
||||
build: startedBuild.build,
|
||||
buildCtx: nullthrows(buildCtxByPlatform[startedBuild.build.platform]),
|
||||
moreBuilds: startedBuilds.length > 1,
|
||||
projectDir,
|
||||
buildProfile: startedBuild.buildProfile.profile,
|
||||
submitProfile,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
});
|
||||
submissions.push(submission);
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
printSubmissionDetailsUrls(submissions);
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
if (!flags.wait) {
|
||||
return;
|
||||
}
|
||||
|
||||
const builds = await waitForBuildEndAsync(startedBuilds.map(({ build }) => build.id));
|
||||
printBuildResults(builds, flags.json);
|
||||
|
||||
const haveAllBuildsFailedOrCanceled = builds.every(
|
||||
build => build?.status && [BuildStatus.Errored, BuildStatus.Canceled].includes(build?.status)
|
||||
);
|
||||
if (haveAllBuildsFailedOrCanceled || !flags.autoSubmit) {
|
||||
exitWithNonZeroCodeIfSomeBuildsFailed(builds);
|
||||
} else {
|
||||
// the following function also exits with non zero code if any of the submissions failed
|
||||
await waitForSubmissionsToCompleteAsync(submissions);
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareAndStartBuildAsync({
|
||||
projectDir,
|
||||
flags,
|
||||
moreBuilds,
|
||||
buildProfile,
|
||||
}: {
|
||||
projectDir: string;
|
||||
flags: BuildFlags;
|
||||
moreBuilds: boolean;
|
||||
buildProfile: ProfileData<'build'>;
|
||||
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
|
||||
const buildCtx = await createBuildContextAsync({
|
||||
buildProfileName: buildProfile.profileName,
|
||||
clearCache: flags.clearCache,
|
||||
buildProfile: buildProfile.profile,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
platform: buildProfile.platform,
|
||||
projectDir,
|
||||
localBuildOptions: flags.localBuildOptions,
|
||||
});
|
||||
|
||||
if (moreBuilds) {
|
||||
Log.newLine();
|
||||
const appPlatform = toAppPlatform(buildProfile.platform);
|
||||
Log.log(
|
||||
`${appPlatformEmojis[appPlatform]} ${chalk.bold(
|
||||
`${appPlatformDisplayNames[appPlatform]} build`
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (buildCtx.workflow === Workflow.MANAGED) {
|
||||
if (!sdkVersionChecked) {
|
||||
await checkExpoSdkIsSupportedAsync(buildCtx);
|
||||
sdkVersionChecked = true;
|
||||
}
|
||||
if (!metroConfigValidated) {
|
||||
await validateMetroConfigForManagedWorkflowAsync(buildCtx);
|
||||
metroConfigValidated = true;
|
||||
}
|
||||
}
|
||||
|
||||
const build = await startBuildAsync(buildCtx);
|
||||
return {
|
||||
build,
|
||||
buildCtx,
|
||||
};
|
||||
}
|
||||
|
||||
async function startBuildAsync(ctx: BuildContext<Platform>): Promise<BuildFragment | undefined> {
|
||||
let sendBuildRequestAsync: BuildRequestSender;
|
||||
if (ctx.platform === Platform.ANDROID) {
|
||||
sendBuildRequestAsync = await prepareAndroidBuildAsync(ctx as BuildContext<Platform.ANDROID>);
|
||||
} else {
|
||||
sendBuildRequestAsync = await prepareIosBuildAsync(ctx as BuildContext<Platform.IOS>);
|
||||
}
|
||||
return await sendBuildRequestAsync();
|
||||
}
|
||||
|
||||
async function prepareAndStartSubmissionAsync({
|
||||
build,
|
||||
buildCtx,
|
||||
moreBuilds,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
submitProfile,
|
||||
nonInteractive,
|
||||
}: {
|
||||
build: BuildFragment;
|
||||
buildCtx: BuildContext<Platform>;
|
||||
moreBuilds: boolean;
|
||||
projectDir: string;
|
||||
buildProfile: BuildProfile;
|
||||
submitProfile: SubmitProfile;
|
||||
nonInteractive: boolean;
|
||||
}): Promise<SubmissionFragment> {
|
||||
const platform = toPlatform(build.platform);
|
||||
const submissionCtx = await createSubmissionContextAsync({
|
||||
platform,
|
||||
projectDir,
|
||||
projectId: build.project.id,
|
||||
profile: submitProfile,
|
||||
archiveFlags: { id: build.id },
|
||||
nonInteractive,
|
||||
env: buildProfile.env,
|
||||
credentialsCtx: buildCtx.credentialsCtx,
|
||||
applicationIdentifier: buildCtx.android?.applicationId ?? buildCtx.ios?.bundleIdentifier,
|
||||
});
|
||||
|
||||
if (moreBuilds) {
|
||||
Log.newLine();
|
||||
Log.log(
|
||||
`${appPlatformEmojis[build.platform]} ${chalk.bold(
|
||||
`${appPlatformDisplayNames[build.platform]} submission`
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
return await submitAsync(submissionCtx);
|
||||
}
|
||||
|
||||
function exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | null)[]): void {
|
||||
const failedBuilds = (maybeBuilds.filter(i => i) as BuildFragment[]).filter(
|
||||
i => i.status === BuildStatus.Errored
|
||||
);
|
||||
if (failedBuilds.length > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -1,124 +1,14 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import {
|
||||
AndroidBuildProfile,
|
||||
CredentialsSource,
|
||||
DistributionType,
|
||||
iOSBuildProfile,
|
||||
} from '@expo/eas-json';
|
||||
|
||||
export enum RequestedPlatform {
|
||||
Android = 'android',
|
||||
iOS = 'ios',
|
||||
All = 'all',
|
||||
}
|
||||
|
||||
export { Platform };
|
||||
|
||||
export enum BuildStatus {
|
||||
NEW = 'new',
|
||||
IN_QUEUE = 'in-queue',
|
||||
IN_PROGRESS = 'in-progress',
|
||||
ERRORED = 'errored',
|
||||
FINISHED = 'finished',
|
||||
CANCELED = 'canceled',
|
||||
}
|
||||
|
||||
export type TrackingContext = Record<string, string | number>;
|
||||
|
||||
export interface Build {
|
||||
id: string;
|
||||
status: BuildStatus;
|
||||
platform: Platform;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
artifacts?: BuildArtifacts;
|
||||
metadata?: Partial<BuildMetadata>;
|
||||
}
|
||||
|
||||
interface BuildArtifacts {
|
||||
buildUrl?: string;
|
||||
logsUrl: string;
|
||||
}
|
||||
|
||||
export type BuildMetadata = {
|
||||
/**
|
||||
* Application version (the expo.version key in app.json/app.config.js)
|
||||
*/
|
||||
appVersion: string;
|
||||
|
||||
/**
|
||||
* EAS CLI version
|
||||
*/
|
||||
cliVersion: string;
|
||||
|
||||
/**
|
||||
* Build workflow
|
||||
* It's either 'generic' or 'managed'
|
||||
*/
|
||||
workflow: Workflow;
|
||||
|
||||
/**
|
||||
* Credentials source
|
||||
* Credentials could be obtained either from credential.json or Expo servers.
|
||||
*/
|
||||
credentialsSource?: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
|
||||
|
||||
/**
|
||||
* Expo SDK version
|
||||
* It's determined by the expo package version in package.json.
|
||||
* It's undefined if the expo package is not installed for the project.
|
||||
*/
|
||||
sdkVersion?: string;
|
||||
|
||||
/**
|
||||
* Release channel (for expo-updates)
|
||||
* It's undefined if the expo-updates package is not installed for the project.
|
||||
*/
|
||||
releaseChannel?: string;
|
||||
|
||||
/**
|
||||
* Tracking context
|
||||
* It's used to track build process across different Expo services and tools.
|
||||
*/
|
||||
trackingContext: TrackingContext;
|
||||
|
||||
/**
|
||||
* Distribution type
|
||||
* Indicates whether this is a build for store or internal distribution.
|
||||
*/
|
||||
distribution: DistributionType;
|
||||
|
||||
/**
|
||||
* App name (expo.name in app.json/app.config.js)
|
||||
*/
|
||||
appName?: string;
|
||||
|
||||
/**
|
||||
* App identifier:
|
||||
* - iOS builds: the bundle identifier (expo.ios.bundleIdentifier in app.json/app.config.js)
|
||||
* - Android builds: the application id (expo.android.package in app.json/app.config.js)
|
||||
*/
|
||||
appIdentifier?: string;
|
||||
};
|
||||
|
||||
export type PlatformBuildProfile<T extends Platform> = T extends Platform.Android
|
||||
? AndroidBuildProfile
|
||||
: iOSBuildProfile;
|
||||
|
||||
export enum AnalyticsEvent {
|
||||
BUILD_COMMAND = 'builds cli build command',
|
||||
PROJECT_UPLOAD_SUCCESS = 'builds cli project upload success',
|
||||
PROJECT_UPLOAD_FAIL = 'builds cli project upload fail',
|
||||
GATHER_CREDENTIALS_SUCCESS = 'builds cli gather credentials success',
|
||||
GATHER_CREDENTIALS_FAIL = 'builds cli gather credentials fail',
|
||||
CONFIGURE_PROJECT_SUCCESS = 'builds cli configure project success',
|
||||
CONFIGURE_PROJECT_FAIL = 'builds cli configure project fail',
|
||||
BUILD_REQUEST_SUCCESS = 'build cli build request success',
|
||||
BUILD_REQUEST_FAIL = 'builds cli build request fail',
|
||||
|
||||
BUILD_STATUS_COMMAND = 'builds cli build status',
|
||||
|
||||
CREDENTIALS_SYNC_COMMAND = 'builds cli credentials sync command',
|
||||
CREDENTIALS_SYNC_UPDATE_LOCAL_SUCCESS = 'builds cli credentials sync update local success',
|
||||
CREDENTIALS_SYNC_UPDATE_LOCAL_FAIL = 'builds cli credentials sync update local fail',
|
||||
CREDENTIALS_SYNC_UPDATE_REMOTE_SUCCESS = 'builds cli credentials sync update remote success',
|
||||
CREDENTIALS_SYNC_UPDATE_REMOTE_FAIL = 'builds cli credentials sync update remote fail',
|
||||
export enum BuildDistributionType {
|
||||
STORE = 'store',
|
||||
INTERNAL = 'internal',
|
||||
SIMULATOR = 'simulator',
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default {
|
||||
logEvent: (a: any, b: any) => {},
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as ExpoConfig from '@expo/config';
|
||||
import JsonFile from '@expo/json-file';
|
||||
import assert from 'assert';
|
||||
|
||||
export async function updateAppJsonConfigAsync(
|
||||
{
|
||||
projectDir,
|
||||
exp,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig.ExpoConfig;
|
||||
},
|
||||
modifyConfig: (config: any) => void
|
||||
): Promise<void> {
|
||||
const paths = ExpoConfig.getConfigFilePaths(projectDir);
|
||||
assert(paths.staticConfigPath, "can't update dynamic config");
|
||||
|
||||
const rawStaticConfig = readAppJson(paths.staticConfigPath);
|
||||
rawStaticConfig.expo = rawStaticConfig.expo ?? {};
|
||||
modifyConfig(rawStaticConfig.expo);
|
||||
await JsonFile.writeAsync(paths.staticConfigPath, rawStaticConfig, { json5: false });
|
||||
|
||||
modifyConfig(exp);
|
||||
}
|
||||
|
||||
// TODO: remove this once @expo/config exports getStaticConfig
|
||||
export function readAppJson(appJsonPath: string): any {
|
||||
const config = JsonFile.read(appJsonPath, { json5: true });
|
||||
if (config) {
|
||||
return config as any;
|
||||
}
|
||||
throw new Error(`Failed to read app.json`);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log from '../../log';
|
||||
import { requestedPlatformDisplayNames } from '../../platform';
|
||||
|
||||
export function logCredentialsSource(
|
||||
credentialsSource: CredentialsSource,
|
||||
platform: Platform
|
||||
): void {
|
||||
let message = `Using ${credentialsSource} ${requestedPlatformDisplayNames[platform]} credentials`;
|
||||
if (credentialsSource === CredentialsSource.LOCAL) {
|
||||
message += ` ${chalk.dim('(credentials.json)')}`;
|
||||
} else if (credentialsSource === CredentialsSource.REMOTE) {
|
||||
message += ` ${chalk.dim('(Expo server)')}`;
|
||||
}
|
||||
Log.succeed(message);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { Errors } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import resolveFrom from 'resolve-from';
|
||||
|
||||
import { toAppPlatform } from '../../graphql/types/AppPlatform';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { appPlatformDisplayNames } from '../../platform';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { confirmAsync } from '../../prompts';
|
||||
import { expoCommandAsync } from '../../utils/expoCli';
|
||||
import { ProfileData } from '../../utils/profiles';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { reviewAndCommitChangesAsync } from './repository';
|
||||
|
||||
export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
|
||||
projectDir,
|
||||
nonInteractive = false,
|
||||
buildProfiles = [],
|
||||
}: {
|
||||
projectDir: string;
|
||||
nonInteractive?: boolean;
|
||||
buildProfiles?: ProfileData<'build'>[];
|
||||
}): Promise<void> {
|
||||
if (await isExpoDevClientInstalledAsync(projectDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const buildProfilesWithDevelopmentClientRequired = buildProfiles.filter(
|
||||
buildProfile => buildProfile.profile.developmentClient
|
||||
);
|
||||
|
||||
const isDevelopmentClientRequired = buildProfilesWithDevelopmentClientRequired.some(Boolean);
|
||||
|
||||
if (!isDevelopmentClientRequired) {
|
||||
return;
|
||||
}
|
||||
|
||||
const platformsToCheck = buildProfilesWithDevelopmentClientRequired.map(
|
||||
({ platform }) => platform
|
||||
);
|
||||
|
||||
const workflowPerPlatformList = await Promise.all(
|
||||
platformsToCheck.map(platform => resolveWorkflowAsync(projectDir, platform))
|
||||
);
|
||||
|
||||
Log.newLine();
|
||||
Log.error(
|
||||
`You want to build a development client build for platforms: ${platformsToCheck
|
||||
.map(i => chalk.bold(appPlatformDisplayNames[toAppPlatform(i)]))
|
||||
.join(', ')}`
|
||||
);
|
||||
Log.error(
|
||||
`However, we detected that you don't have ${chalk.bold(
|
||||
'expo-dev-client'
|
||||
)} installed for your project.`
|
||||
);
|
||||
const areAllManaged = workflowPerPlatformList.every(i => i === Workflow.MANAGED);
|
||||
if (areAllManaged) {
|
||||
const install = await confirmAsync({
|
||||
message: 'Do you want EAS CLI to install expo-dev-client for you?',
|
||||
instructions: 'The command will abort unless you agree.',
|
||||
});
|
||||
if (install) {
|
||||
await installExpoDevClientAsync(projectDir, { nonInteractive });
|
||||
} else {
|
||||
Errors.error(`Install ${chalk.bold('expo-dev-client')} manually and come back later.`, {
|
||||
exit: 1,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
Log.warn(`You'll need to install ${chalk.bold('expo-dev-client')} manually.`);
|
||||
Log.warn(
|
||||
learnMore('https://docs.expo.dev/clients/installation/', {
|
||||
learnMoreMessage: 'See installation instructions on how to do it.',
|
||||
dim: false,
|
||||
})
|
||||
);
|
||||
Log.warn('If you proceed anyway, you might not get the build you want.');
|
||||
Log.newLine();
|
||||
const shouldContinue = await confirmAsync({
|
||||
message: 'Do you want to proceed anyway?',
|
||||
initial: false,
|
||||
});
|
||||
if (!shouldContinue) {
|
||||
Errors.error('Come back later', { exit: 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function isExpoDevClientInstalledAsync(projectDir: string): Promise<boolean> {
|
||||
try {
|
||||
resolveFrom(projectDir, 'expo-dev-client/package.json');
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
Log.debug(err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function installExpoDevClientAsync(
|
||||
projectDir: string,
|
||||
{ nonInteractive }: { nonInteractive: boolean }
|
||||
): Promise<void> {
|
||||
Log.newLine();
|
||||
Log.log(`Running ${chalk.bold('expo install expo-dev-client')}`);
|
||||
Log.newLine();
|
||||
await expoCommandAsync(projectDir, ['install', 'expo-dev-client']);
|
||||
Log.newLine();
|
||||
if (await getVcsClient().isCommitRequiredAsync()) {
|
||||
await reviewAndCommitChangesAsync('Install expo-dev-client', {
|
||||
nonInteractive,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,69 +1,124 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildFragment,
|
||||
BuildStatus as GraphQLBuildStatus,
|
||||
} from '../../graphql/generated';
|
||||
import { appPlatformDisplayNames } from '../../platform';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { platformDisplayNames } from '../constants';
|
||||
import { Build, BuildStatus } from '../types';
|
||||
import { getBuildLogsUrl } from './url';
|
||||
|
||||
interface Options {
|
||||
accountName: string;
|
||||
}
|
||||
|
||||
export default function formatBuild(build: Build, { accountName }: Options) {
|
||||
const fields: { label: string; value: string }[] = [
|
||||
export function formatGraphQLBuild(build: BuildFragment): string {
|
||||
const actor = getActorName(build);
|
||||
const fields: { label: string; value?: string | null }[] = [
|
||||
{ label: 'ID', value: build.id },
|
||||
{
|
||||
label: 'Platform',
|
||||
value: platformDisplayNames[build.platform],
|
||||
value: appPlatformDisplayNames[build.platform],
|
||||
},
|
||||
{
|
||||
label: 'Status',
|
||||
get value() {
|
||||
switch (build.status) {
|
||||
case BuildStatus.IN_QUEUE:
|
||||
case GraphQLBuildStatus.New:
|
||||
return chalk.blue('new');
|
||||
case GraphQLBuildStatus.InQueue:
|
||||
return chalk.blue('in queue');
|
||||
case BuildStatus.IN_PROGRESS:
|
||||
case GraphQLBuildStatus.InProgress:
|
||||
return chalk.blue('in progress');
|
||||
case BuildStatus.FINISHED:
|
||||
case GraphQLBuildStatus.Canceled:
|
||||
return chalk.gray('canceled');
|
||||
case GraphQLBuildStatus.Finished:
|
||||
return chalk.green('finished');
|
||||
case BuildStatus.ERRORED:
|
||||
case GraphQLBuildStatus.Errored:
|
||||
return chalk.red('errored');
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Distribution',
|
||||
value: build.distribution?.toLowerCase(),
|
||||
},
|
||||
{
|
||||
label: 'Enterprise Provisioning',
|
||||
value: build.iosEnterpriseProvisioning?.toLowerCase(),
|
||||
},
|
||||
{
|
||||
label: 'Release Channel',
|
||||
value: build.releaseChannel,
|
||||
},
|
||||
{
|
||||
label: 'Channel',
|
||||
value: build.channel,
|
||||
},
|
||||
{
|
||||
label: 'SDK Version',
|
||||
value: build.sdkVersion,
|
||||
},
|
||||
{
|
||||
label: 'Runtime Version',
|
||||
value: build.runtimeVersion,
|
||||
},
|
||||
{
|
||||
label: 'Version',
|
||||
value: build.appVersion,
|
||||
},
|
||||
{
|
||||
label: build.platform === AppPlatform.Android ? 'Version code' : 'Build number',
|
||||
value: build.appBuildVersion,
|
||||
},
|
||||
{
|
||||
label: 'Commit',
|
||||
value: build.gitCommitHash,
|
||||
},
|
||||
{
|
||||
label: 'Logs',
|
||||
value: getBuildLogsUrl({ buildId: build.id, account: accountName }),
|
||||
value: getBuildLogsUrl(build),
|
||||
},
|
||||
{
|
||||
label: 'Artifact',
|
||||
get value() {
|
||||
if (build.status === BuildStatus.IN_QUEUE || build.status === BuildStatus.IN_PROGRESS) {
|
||||
return '<in progress>';
|
||||
switch (build.status) {
|
||||
case GraphQLBuildStatus.New:
|
||||
case GraphQLBuildStatus.InQueue:
|
||||
case GraphQLBuildStatus.InProgress:
|
||||
return '<in progress>';
|
||||
case GraphQLBuildStatus.Canceled:
|
||||
case GraphQLBuildStatus.Errored:
|
||||
return null;
|
||||
case GraphQLBuildStatus.Finished: {
|
||||
const url = build.artifacts?.buildUrl;
|
||||
return url ? url : chalk.red('not found');
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
const url = build.artifacts?.buildUrl;
|
||||
|
||||
if (!url) {
|
||||
return chalk.red('not found');
|
||||
}
|
||||
|
||||
return url;
|
||||
},
|
||||
},
|
||||
{ label: 'Started at', value: new Date(build.createdAt).toLocaleString() },
|
||||
{
|
||||
label: 'Finished at',
|
||||
value:
|
||||
build.status === BuildStatus.IN_QUEUE || build.status === BuildStatus.IN_PROGRESS
|
||||
? '<in progress>'
|
||||
: new Date(build.updatedAt).toLocaleString(),
|
||||
value: [
|
||||
GraphQLBuildStatus.New,
|
||||
GraphQLBuildStatus.InQueue,
|
||||
GraphQLBuildStatus.InProgress,
|
||||
].includes(build.status)
|
||||
? '<in progress>'
|
||||
: new Date(build.updatedAt).toLocaleString(),
|
||||
},
|
||||
// TODO: we can do it once we migrate to GraphQL
|
||||
// { label: 'Started by', get: build => 'TODO' },
|
||||
{ label: 'Started by', value: actor ?? 'unknown' },
|
||||
];
|
||||
|
||||
return formatFields(fields);
|
||||
const filteredFields = fields.filter(({ value }) => value !== undefined && value !== null) as {
|
||||
label: string;
|
||||
value: string;
|
||||
}[];
|
||||
return formatFields(filteredFields);
|
||||
}
|
||||
|
||||
const getActorName = (build: BuildFragment): string => {
|
||||
return build.initiatingActor?.displayName || 'unknown';
|
||||
};
|
||||
@@ -1,90 +1,122 @@
|
||||
import { DistributionType } from '@expo/eas-json';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import indentString from 'indent-string';
|
||||
import qrcodeTerminal from 'qrcode-terminal';
|
||||
|
||||
import log from '../../log';
|
||||
import { platformDisplayNames, platformEmojis } from '../constants';
|
||||
import { Build } from '../types';
|
||||
import { getBuildLogsUrl } from './url';
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildError,
|
||||
BuildFragment,
|
||||
BuildStatus,
|
||||
DistributionType,
|
||||
EasBuildDeprecationInfo,
|
||||
EasBuildDeprecationInfoType,
|
||||
} from '../../graphql/generated';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { appPlatformDisplayNames, appPlatformEmojis } from '../../platform';
|
||||
import { printJsonOnlyOutput } from '../../utils/json';
|
||||
import { getBuildLogsUrl, getInternalDistributionInstallUrl } from './url';
|
||||
|
||||
export interface DeprecationInfo {
|
||||
type: 'user-facing' | 'internal';
|
||||
message: string;
|
||||
}
|
||||
|
||||
export function printLogsUrls(
|
||||
accountName: string,
|
||||
builds: { platform: 'android' | 'ios'; buildId: string }[]
|
||||
): void {
|
||||
export function printLogsUrls(builds: BuildFragment[]): void {
|
||||
if (builds.length === 1) {
|
||||
const { buildId } = builds[0];
|
||||
const logsUrl = getBuildLogsUrl({
|
||||
buildId,
|
||||
account: accountName,
|
||||
});
|
||||
log(`Build details: ${chalk.underline(logsUrl)}`);
|
||||
Log.log(`Build details: ${chalk.underline(getBuildLogsUrl(builds[0]))}`);
|
||||
} else {
|
||||
builds.forEach(({ buildId, platform }) => {
|
||||
const logsUrl = getBuildLogsUrl({
|
||||
buildId,
|
||||
account: accountName,
|
||||
});
|
||||
log(`${platformDisplayNames[platform]} build details: ${chalk.underline(logsUrl)}`);
|
||||
builds.forEach(build => {
|
||||
const logsUrl = getBuildLogsUrl(build);
|
||||
Log.log(
|
||||
`${appPlatformEmojis[build.platform]} ${
|
||||
appPlatformDisplayNames[build.platform]
|
||||
} build details: ${chalk.underline(logsUrl)}`
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function printBuildResults(accountName: string, builds: (Build | null)[]): void {
|
||||
if (builds.length === 1) {
|
||||
const [build] = builds;
|
||||
assert(build, 'Build should be defined');
|
||||
printBuildResult(accountName, build);
|
||||
export function printBuildResults(builds: (BuildFragment | null)[], json: boolean): void {
|
||||
if (json) {
|
||||
printJsonOnlyOutput(builds);
|
||||
} else {
|
||||
(builds.filter(i => i) as Build[])
|
||||
.filter(build => build.status === 'finished')
|
||||
.forEach(build => printBuildResult(accountName, build));
|
||||
Log.newLine();
|
||||
if (builds.length === 1) {
|
||||
const [build] = builds;
|
||||
assert(build, 'Build should be defined');
|
||||
printBuildResult(build);
|
||||
} else {
|
||||
(builds.filter(i => i) as BuildFragment[]).forEach(build => printBuildResult(build));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function printBuildResult(accountName: string, build: Build): void {
|
||||
if (build.metadata?.distribution === DistributionType.INTERNAL) {
|
||||
const logsUrl = getBuildLogsUrl({
|
||||
buildId: build.id,
|
||||
account: accountName,
|
||||
});
|
||||
log(
|
||||
`${platformEmojis[build.platform]} Open this link on your ${
|
||||
platformDisplayNames[build.platform]
|
||||
} devices to install the app:`
|
||||
function printBuildResult(build: BuildFragment): void {
|
||||
Log.addNewLineIfNone();
|
||||
if (build.status === BuildStatus.Errored) {
|
||||
const userError = build.error;
|
||||
Log.error(
|
||||
`${appPlatformEmojis[build.platform]} ${
|
||||
appPlatformDisplayNames[build.platform]
|
||||
} build failed${userError ? ':' : ''}`
|
||||
);
|
||||
log(`${chalk.underline(logsUrl)}`);
|
||||
log.newLine();
|
||||
if (userError) {
|
||||
printUserError(userError);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (build.status === BuildStatus.Canceled) {
|
||||
Log.error(
|
||||
`${appPlatformEmojis[build.platform]} ${
|
||||
appPlatformDisplayNames[build.platform]
|
||||
} build was canceled`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (build.distribution === DistributionType.Internal) {
|
||||
const logsUrl = getBuildLogsUrl(build);
|
||||
// It's tricky to install the .apk file directly on Android so let's fallback
|
||||
// to the build details page and let people press the button to download there
|
||||
const qrcodeUrl =
|
||||
build.platform === AppPlatform.Ios ? getInternalDistributionInstallUrl(build) : logsUrl;
|
||||
qrcodeTerminal.generate(qrcodeUrl, { small: true }, code =>
|
||||
Log.log(`${indentString(code, 2)}\n`)
|
||||
);
|
||||
Log.log(
|
||||
`${appPlatformEmojis[build.platform]} Open this link on your ${
|
||||
appPlatformDisplayNames[build.platform]
|
||||
} devices (or scan the QR code) to install the app:`
|
||||
);
|
||||
Log.log(`${chalk.underline(logsUrl)}`);
|
||||
} else {
|
||||
// TODO: it looks like buildUrl could possibly be undefined, based on the code below.
|
||||
// we should account for this case better if it is possible
|
||||
const url = build.artifacts?.buildUrl ?? '';
|
||||
log(`${platformEmojis[build.platform]} ${platformDisplayNames[build.platform]} app:`);
|
||||
log(`${chalk.underline(url)}`);
|
||||
log.newLine();
|
||||
Log.log(`${appPlatformEmojis[build.platform]} ${appPlatformDisplayNames[build.platform]} app:`);
|
||||
Log.log(`${chalk.underline(url)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function printDeprecationWarnings(deprecationInfo?: DeprecationInfo): void {
|
||||
export function printDeprecationWarnings(deprecationInfo?: EasBuildDeprecationInfo | null): void {
|
||||
if (!deprecationInfo) {
|
||||
return;
|
||||
}
|
||||
if (deprecationInfo.type === 'internal') {
|
||||
log.warn('This command is using API that soon will be deprecated, please update expo-cli.');
|
||||
log.warn("Changes won't affect your project config.");
|
||||
log.warn(deprecationInfo.message);
|
||||
} else if (deprecationInfo.type === 'user-facing') {
|
||||
log.warn('This command is using API that soon will be deprecated, please update expo-cli.');
|
||||
log.warn(
|
||||
'There might be some changes necessary to your project config, latest expo-cli will provide more specific error messages.'
|
||||
if (deprecationInfo.type === EasBuildDeprecationInfoType.Internal) {
|
||||
Log.warn('This command is using API that soon will be deprecated, please update eas-cli.');
|
||||
Log.warn("Changes won't affect your project config.");
|
||||
Log.warn(deprecationInfo.message);
|
||||
} else if (deprecationInfo.type === EasBuildDeprecationInfoType.UserFacing) {
|
||||
Log.warn('This command is using API that soon will be deprecated, please update eas-cli.');
|
||||
Log.warn(
|
||||
'There might be some changes necessary to your project config, latest eas-cli will provide more specific error messages.'
|
||||
);
|
||||
log.warn(deprecationInfo.message);
|
||||
Log.warn(deprecationInfo.message);
|
||||
} else {
|
||||
log.warn('An unexpected warning was encountered. Please report it as a bug:');
|
||||
log.warn(deprecationInfo);
|
||||
Log.warn('An unexpected warning was encountered. Please report it as a bug:');
|
||||
Log.warn(deprecationInfo);
|
||||
}
|
||||
}
|
||||
|
||||
export function printUserError(error: BuildError): void {
|
||||
Log.error(error.message);
|
||||
if (error.docsUrl) {
|
||||
Log.error(learnMore(error.docsUrl, { dim: false }));
|
||||
}
|
||||
}
|
||||
@@ -1,63 +1,24 @@
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import ora from 'ora';
|
||||
import path from 'path';
|
||||
import tar from 'tar';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import log from '../../log';
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import {
|
||||
doesGitRepoExistAsync,
|
||||
getGitDiffOutputAsync,
|
||||
gitDiffAsync,
|
||||
gitRootDirectoryAsync,
|
||||
gitStatusAsync,
|
||||
isGitInstalledAsync,
|
||||
} from '../../utils/git';
|
||||
import { formatBytes } from '../../utils/files';
|
||||
import { getTmpDirectory } from '../../utils/paths';
|
||||
import { endTimer, formatMilliseconds, startTimer } from '../../utils/timer';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
|
||||
async function ensureGitRepoExistsAsync(): Promise<void> {
|
||||
if (!(await isGitInstalledAsync())) {
|
||||
throw new Error('git command not found, install it before proceeding');
|
||||
}
|
||||
|
||||
if (await doesGitRepoExistAsync()) {
|
||||
export async function maybeBailOnRepoStatusAsync(): Promise<void> {
|
||||
if (!(await getVcsClient().isCommitRequiredAsync())) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.warn("It looks like you haven't initialized the git repository yet.");
|
||||
log.warn('EAS Build requires you to use a git repository for your project.');
|
||||
|
||||
const confirmInit = await confirmAsync({
|
||||
message: `Would you like to run 'git init' in the current directory?`,
|
||||
});
|
||||
if (!confirmInit) {
|
||||
throw new Error(
|
||||
'A git repository is required for building your project. Initialize it and run this command again.'
|
||||
);
|
||||
}
|
||||
await spawnAsync('git', ['init']);
|
||||
|
||||
log("We're going to make an initial commit for you repository.");
|
||||
|
||||
await spawnAsync('git', ['add', '-A']);
|
||||
await commitPromptAsync('Initial commit');
|
||||
}
|
||||
|
||||
async function isGitStatusCleanAsync(): Promise<boolean> {
|
||||
const changes = await gitStatusAsync();
|
||||
return changes.length === 0;
|
||||
}
|
||||
|
||||
async function maybeBailOnGitStatusAsync(): Promise<void> {
|
||||
if (await isGitStatusCleanAsync()) {
|
||||
return;
|
||||
}
|
||||
log.addNewLineIfNone();
|
||||
log.warn(`${chalk.bold('Warning!')} Your git working tree is dirty.`);
|
||||
log(
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(`${chalk.bold('Warning!')} Your repository working tree is dirty.`);
|
||||
Log.log(
|
||||
`It's recommended to ${chalk.bold(
|
||||
'commit all your changes before proceeding'
|
||||
)}, so you can revert the changes made by this command if necessary.`
|
||||
@@ -71,13 +32,13 @@ async function maybeBailOnGitStatusAsync(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureGitStatusIsCleanAsync(nonInteractive = false): Promise<void> {
|
||||
if (await isGitStatusCleanAsync()) {
|
||||
export async function ensureRepoIsCleanAsync(nonInteractive = false): Promise<void> {
|
||||
if (!(await getVcsClient().isCommitRequiredAsync())) {
|
||||
return;
|
||||
}
|
||||
log.addNewLineIfNone();
|
||||
log.warn(`${chalk.bold('Warning!')} Your git working tree is dirty.`);
|
||||
log(
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(`${chalk.bold('Warning!')} Your repository working tree is dirty.`);
|
||||
Log.log(
|
||||
`This operation needs to be run on a clean working tree, please ${chalk.bold(
|
||||
'commit all your changes before proceeding'
|
||||
)}.`
|
||||
@@ -89,16 +50,34 @@ async function ensureGitStatusIsCleanAsync(nonInteractive = false): Promise<void
|
||||
message: `Commit changes to git?`,
|
||||
});
|
||||
if (answer) {
|
||||
await commitPromptAsync();
|
||||
await commitPromptAsync({ commitAllFiles: true });
|
||||
} else {
|
||||
throw new Error('Please commit all changes. Aborting...');
|
||||
}
|
||||
}
|
||||
|
||||
async function makeProjectTarballAsync(): Promise<{ path: string; size: number }> {
|
||||
export async function commitPromptAsync({
|
||||
initialCommitMessage,
|
||||
commitAllFiles,
|
||||
}: {
|
||||
initialCommitMessage?: string;
|
||||
commitAllFiles?: boolean;
|
||||
} = {}): Promise<void> {
|
||||
const { message } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'message',
|
||||
message: 'Commit message:',
|
||||
initial: initialCommitMessage,
|
||||
validate: (input: string) => input !== '',
|
||||
});
|
||||
await getVcsClient().commitAsync({ commitAllFiles, commitMessage: message });
|
||||
}
|
||||
|
||||
export async function makeProjectTarballAsync(): Promise<{ path: string; size: number }> {
|
||||
const spinner = ora('Compressing project files');
|
||||
|
||||
await fs.mkdirp(getTmpDirectory());
|
||||
const shallowClonePath = path.join(getTmpDirectory(), `${uuidv4()}-shallow-clone`);
|
||||
const tarPath = path.join(getTmpDirectory(), `${uuidv4()}.tar.gz`);
|
||||
|
||||
// If the compression takes longer then a second, show the spinner.
|
||||
@@ -109,58 +88,92 @@ async function makeProjectTarballAsync(): Promise<{ path: string; size: number }
|
||||
() => {
|
||||
spinner.start();
|
||||
},
|
||||
log.isDebug ? 1 : 1000
|
||||
Log.isDebug ? 1 : 1000
|
||||
);
|
||||
// TODO: Possibly warn after more time about unoptimized assets.
|
||||
const compressTimerLabel = 'makeProjectTarballAsync';
|
||||
startTimer(compressTimerLabel);
|
||||
|
||||
try {
|
||||
await spawnAsync(
|
||||
'git',
|
||||
['archive', '--format=tar.gz', '--prefix', 'project/', '-o', tarPath, 'HEAD'],
|
||||
{ cwd: await gitRootDirectoryAsync() }
|
||||
);
|
||||
} finally {
|
||||
// Stop the timer
|
||||
await getVcsClient().makeShallowCopyAsync(shallowClonePath);
|
||||
await tar.create({ cwd: shallowClonePath, file: tarPath, prefix: 'project', gzip: true }, [
|
||||
'.',
|
||||
]);
|
||||
} catch (err) {
|
||||
clearTimeout(timer);
|
||||
|
||||
const duration = endTimer(compressTimerLabel);
|
||||
if (spinner.isSpinning) {
|
||||
const prettyTime = formatMilliseconds(duration);
|
||||
spinner.succeed(`Compressed project files ${chalk.dim(prettyTime)}`);
|
||||
spinner.fail();
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
await fs.remove(shallowClonePath);
|
||||
}
|
||||
clearTimeout(timer);
|
||||
|
||||
const { size } = await fs.stat(tarPath);
|
||||
const duration = endTimer(compressTimerLabel);
|
||||
if (spinner.isSpinning) {
|
||||
const prettyTime = formatMilliseconds(duration);
|
||||
spinner.succeed(
|
||||
`Compressed project files ${chalk.dim(`${prettyTime} (${formatBytes(size)})`)}`
|
||||
);
|
||||
}
|
||||
|
||||
return { size, path: tarPath };
|
||||
}
|
||||
|
||||
async function showDiffAsync() {
|
||||
const outputTooLarge = (await getGitDiffOutputAsync()).split(/\r\n|\r|\n/).length > 100;
|
||||
await gitDiffAsync({ withPager: outputTooLarge });
|
||||
enum ShouldCommitChanges {
|
||||
Yes,
|
||||
ShowDiffFirst,
|
||||
Abort,
|
||||
}
|
||||
|
||||
async function commitPromptAsync(initialCommitMessage?: string): Promise<void> {
|
||||
const { message } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'message',
|
||||
message: 'Commit message:',
|
||||
initial: initialCommitMessage,
|
||||
validate: (input: string) => input !== '',
|
||||
export async function reviewAndCommitChangesAsync(
|
||||
initialCommitMessage: string,
|
||||
{ nonInteractive, askedFirstTime = true }: { nonInteractive: boolean; askedFirstTime?: boolean }
|
||||
): Promise<void> {
|
||||
if (process.env.EAS_BUILD_AUTOCOMMIT) {
|
||||
await getVcsClient().commitAsync({
|
||||
commitMessage: initialCommitMessage,
|
||||
commitAllFiles: false,
|
||||
nonInteractive,
|
||||
});
|
||||
Log.withTick('Committed changes.');
|
||||
return;
|
||||
}
|
||||
if (nonInteractive) {
|
||||
throw new Error(
|
||||
'Cannot commit changes when --non-interactive is specified. Run the command in interactive mode or set EAS_BUILD_AUTOCOMMIT=1 in your environment.'
|
||||
);
|
||||
}
|
||||
const { selected } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'selected',
|
||||
message: 'Can we commit these changes to git for you?',
|
||||
choices: [
|
||||
{ title: 'Yes', value: ShouldCommitChanges.Yes },
|
||||
...(askedFirstTime
|
||||
? [{ title: 'Show the diff and ask me again', value: ShouldCommitChanges.ShowDiffFirst }]
|
||||
: []),
|
||||
{
|
||||
title: 'Abort build process',
|
||||
value: ShouldCommitChanges.Abort,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Add changed files only
|
||||
await spawnAsync('git', ['add', '-u']);
|
||||
await spawnAsync('git', ['commit', '-m', message]);
|
||||
if (selected === ShouldCommitChanges.Abort) {
|
||||
throw new Error(
|
||||
"Aborting, run the command again once you're ready. Make sure to commit any changes you've made."
|
||||
);
|
||||
} else if (selected === ShouldCommitChanges.Yes) {
|
||||
await commitPromptAsync({ initialCommitMessage });
|
||||
Log.withTick('Committed changes.');
|
||||
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
|
||||
await getVcsClient().showDiffAsync();
|
||||
await reviewAndCommitChangesAsync(initialCommitMessage, {
|
||||
nonInteractive,
|
||||
askedFirstTime: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
isGitStatusCleanAsync,
|
||||
showDiffAsync,
|
||||
commitPromptAsync,
|
||||
ensureGitRepoExistsAsync,
|
||||
ensureGitStatusIsCleanAsync,
|
||||
maybeBailOnGitStatusAsync,
|
||||
makeProjectTarballAsync,
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ExpoConfig, getPackageJson } from '@expo/config';
|
||||
|
||||
export function isExpoUpdatesInstalled(projectDir: string) {
|
||||
export function isExpoUpdatesInstalled(projectDir: string): boolean {
|
||||
const packageJson = getPackageJson(projectDir);
|
||||
return packageJson.dependencies && 'expo-updates' in packageJson.dependencies;
|
||||
return !!(packageJson.dependencies && 'expo-updates' in packageJson.dependencies);
|
||||
}
|
||||
|
||||
export function ensureValidVersions(exp: ExpoConfig): void {
|
||||
|
||||
@@ -1,15 +1,33 @@
|
||||
import { getExpoWebsiteBaseUrl } from '../../api';
|
||||
import assert from 'assert';
|
||||
|
||||
export function getBuildLogsUrl({
|
||||
buildId,
|
||||
account,
|
||||
}: {
|
||||
buildId: string;
|
||||
account: string;
|
||||
}): string {
|
||||
return `${getExpoWebsiteBaseUrl()}/accounts/${account}/builds/${buildId}`;
|
||||
import { getExpoApiBaseUrl, getExpoWebsiteBaseUrl } from '../../api';
|
||||
import { AppPlatform, BuildFragment } from '../../graphql/generated';
|
||||
|
||||
export function getProjectDashboardUrl(accountName: string, projectName: string): string {
|
||||
return `${getExpoWebsiteBaseUrl()}/accounts/${accountName}/projects/${projectName}`;
|
||||
}
|
||||
|
||||
export function getBuildLogsUrl(build: BuildFragment): string {
|
||||
const { project } = build;
|
||||
if (project.__typename === 'App') {
|
||||
return `${getExpoWebsiteBaseUrl()}/accounts/${project.ownerAccount.name}/builds/${build.id}`;
|
||||
} else {
|
||||
return `${getExpoWebsiteBaseUrl()}/builds/${build.id}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function getArtifactUrl(artifactId: string): string {
|
||||
return `${getExpoWebsiteBaseUrl()}/artifacts/${artifactId}`;
|
||||
}
|
||||
|
||||
export function getInternalDistributionInstallUrl(build: BuildFragment): string {
|
||||
if (build.platform === AppPlatform.Ios) {
|
||||
return `itms-services://?action=download-manifest;url=${getExpoApiBaseUrl()}/v2/projects/${
|
||||
build.project.id
|
||||
}/builds/${build.id}/manifest.plist`;
|
||||
}
|
||||
|
||||
assert(build.artifacts?.buildUrl, 'buildUrl is missing');
|
||||
|
||||
return build.artifacts.buildUrl;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
import semver from 'semver';
|
||||
|
||||
import Log from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { updateAppJsonConfigAsync } from './appJson';
|
||||
|
||||
export function ensureStaticConfigExists(projectDir: string): void {
|
||||
const paths = getConfigFilePaths(projectDir);
|
||||
if (!paths.staticConfigPath) {
|
||||
throw new Error('autoIncrement option is not supported when using app.config.js');
|
||||
}
|
||||
}
|
||||
|
||||
export async function bumpAppVersionAsync({
|
||||
appVersion,
|
||||
projectDir,
|
||||
exp,
|
||||
}: {
|
||||
appVersion: string;
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
}): Promise<void> {
|
||||
let bumpedAppVersion: string;
|
||||
if (semver.valid(appVersion)) {
|
||||
bumpedAppVersion = nullthrows(semver.inc(appVersion, 'patch'));
|
||||
Log.log(
|
||||
`Bumping ${chalk.bold('expo.version')} from ${chalk.bold(appVersion)} to ${chalk.bold(
|
||||
bumpedAppVersion
|
||||
)}`
|
||||
);
|
||||
} else {
|
||||
Log.log(`${chalk.bold('expo.version')} = ${chalk.bold(appVersion)} is not a valid semver`);
|
||||
bumpedAppVersion = (
|
||||
await promptAsync({
|
||||
type: 'text',
|
||||
name: 'bumpedAppVersion',
|
||||
message: 'What is the next version?',
|
||||
})
|
||||
).bumpedAppVersion;
|
||||
}
|
||||
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
|
||||
config.version = bumpedAppVersion;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import Log, { learnMore } from '../log';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { CommonContext } from './context';
|
||||
|
||||
export function checkNodeEnvVariable(ctx: CommonContext<Platform>): void {
|
||||
if (ctx.buildProfile.env?.NODE_ENV === 'production') {
|
||||
Log.warn(
|
||||
'You set NODE_ENV=production in the build profile. Remember that it will be available during the entire build process. In particular, it will make yarn/npm install only production packages.'
|
||||
);
|
||||
Log.newLine();
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkGoogleServicesFileAsync<T extends Platform>(
|
||||
ctx: CommonContext<T>
|
||||
): Promise<void> {
|
||||
if (ctx.workflow === Workflow.GENERIC || ctx.buildProfile?.env?.GOOGLE_SERVICES_FILE) {
|
||||
return;
|
||||
}
|
||||
const googleServicesFilePath = ctx.exp[ctx.platform]?.googleServicesFile;
|
||||
if (!googleServicesFilePath) {
|
||||
return;
|
||||
}
|
||||
const rootDir = path.normalize(await getVcsClient().getRootPathAsync());
|
||||
const absGoogleServicesFilePath = path.resolve(ctx.projectDir, googleServicesFilePath);
|
||||
if (
|
||||
(await fs.pathExists(absGoogleServicesFilePath)) &&
|
||||
(!isInsideDirectory(absGoogleServicesFilePath, rootDir) ||
|
||||
(await getVcsClient().isFileIgnoredAsync(path.relative(rootDir, absGoogleServicesFilePath))))
|
||||
) {
|
||||
Log.warn(
|
||||
`File specified via "${ctx.platform}.googleServicesFile" field in your app.json is not checked in to your repository and won't be uploaded to the builder.`
|
||||
);
|
||||
Log.warn(
|
||||
`Use EAS Secret to pass all values that you don't want to include in your version control. ${learnMore(
|
||||
'https://docs.expo.dev/build-reference/variables/#using-secrets-in-environment-variables'
|
||||
)}`
|
||||
);
|
||||
Log.warn(
|
||||
`If you are using that file for compatibility with the classic build service (expo build) you can silence this warning by setting your build profile's env.GOOGLE_SERVICES_FILE in eas.json to any non-empty string.`
|
||||
);
|
||||
Log.newLine();
|
||||
}
|
||||
}
|
||||
|
||||
function isInsideDirectory(file: string, directory: string): boolean {
|
||||
return file.startsWith(directory);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { EasJsonReader } from '@expo/eas-json';
|
||||
import { Command } from '@oclif/core';
|
||||
import semver from 'semver';
|
||||
|
||||
import {
|
||||
AnalyticsEvent,
|
||||
flushAsync as flushAnalyticsAsync,
|
||||
initAsync as initAnalyticsAsync,
|
||||
logEvent,
|
||||
} from '../analytics/rudderstackClient';
|
||||
import { findProjectRootAsync } from '../project/projectUtils';
|
||||
import { getUserAsync } from '../user/User';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
import { setVcsClient } from '../vcs';
|
||||
import GitClient from '../vcs/clients/git';
|
||||
|
||||
export default abstract class EasCommand extends Command {
|
||||
/**
|
||||
* When user data is unavailable locally, determines if the command will
|
||||
* force the user to log in
|
||||
*/
|
||||
protected requiresAuthentication = true;
|
||||
protected mustBeRunInsideProject = true;
|
||||
|
||||
protected abstract runAsync(): Promise<any>;
|
||||
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
async run(): Promise<any> {
|
||||
await initAnalyticsAsync();
|
||||
if (this.mustBeRunInsideProject) {
|
||||
await this.applyCliConfigAsync();
|
||||
}
|
||||
|
||||
if (this.requiresAuthentication) {
|
||||
const { flags } = await this.parse();
|
||||
const nonInteractive = (flags as any)['non-interactive'] ?? false;
|
||||
await ensureLoggedInAsync({ nonInteractive });
|
||||
} else {
|
||||
await getUserAsync();
|
||||
}
|
||||
logEvent(AnalyticsEvent.ACTION, {
|
||||
// id is assigned by oclif in constructor based on the filepath:
|
||||
// commands/submit === submit, commands/build/list === build:list
|
||||
action: `eas ${this.id}`,
|
||||
});
|
||||
return this.runAsync();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
async finally(err: Error): Promise<any> {
|
||||
await flushAnalyticsAsync();
|
||||
return super.finally(err);
|
||||
}
|
||||
|
||||
private async applyCliConfigAsync(): Promise<void> {
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
const config = await easJsonReader.getCliConfigAsync();
|
||||
if (config?.version && !semver.satisfies(easCliVersion, config.version)) {
|
||||
throw new Error(
|
||||
`You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint in eas.json (${config.version})`
|
||||
);
|
||||
}
|
||||
if (config?.requireCommit) {
|
||||
setVcsClient(new GitClient());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { asMock } from '../../__tests__/utils';
|
||||
import { flushAsync, initAsync, logEvent } from '../../analytics/rudderstackClient';
|
||||
import { jester as mockJester } from '../../credentials/__tests__/fixtures-constants';
|
||||
import { getUserAsync } from '../../user/User';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import EasCommand from '../EasCommand';
|
||||
import TestEasCommand from './TestEasCommand';
|
||||
|
||||
TestEasCommand.prototype.authValue = jest.fn().mockImplementation(() => true);
|
||||
|
||||
jest.mock('../../user/User');
|
||||
jest.mock('../../user/actions', () => ({ ensureLoggedInAsync: jest.fn() }));
|
||||
jest.mock('../../analytics/rudderstackClient', () => {
|
||||
const { AnalyticsEvent } = jest.requireActual('../../analytics/rudderstackClient');
|
||||
return {
|
||||
AnalyticsEvent,
|
||||
logEvent: jest.fn(),
|
||||
initAsync: jest.fn(),
|
||||
flushAsync: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
let originalProcessArgv: string[];
|
||||
|
||||
beforeAll(() => {
|
||||
originalProcessArgv = process.argv;
|
||||
process.argv = [];
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.argv = originalProcessArgv;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
asMock(getUserAsync)
|
||||
.mockReset()
|
||||
.mockImplementation(() => mockJester);
|
||||
asMock(ensureLoggedInAsync)
|
||||
.mockReset()
|
||||
.mockImplementation(() => mockJester);
|
||||
asMock(initAsync).mockReset();
|
||||
asMock(flushAsync).mockReset();
|
||||
asMock(logEvent).mockReset();
|
||||
});
|
||||
|
||||
describe(EasCommand.name, () => {
|
||||
describe('without exceptions', () => {
|
||||
// The first test in this suite should have an increased timeout
|
||||
// because of the implementation of Command from @oclif/command.
|
||||
// It seems that loading config takes significant amount of time
|
||||
// and I'm not sure how to mock it.
|
||||
//
|
||||
// See https://github.com/oclif/command/blob/master/src/command.ts#L80
|
||||
// and look for "Config.load"
|
||||
it('ensures the user is logged in', async () => {
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(ensureLoggedInAsync).toHaveBeenCalled();
|
||||
}, 15_000);
|
||||
|
||||
it('ensures the user data is read from cache', async () => {
|
||||
asMock(TestEasCommand.prototype.authValue).mockImplementationOnce(() => false);
|
||||
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(getUserAsync).toHaveReturnedWith(mockJester);
|
||||
});
|
||||
|
||||
it('initializes analytics', async () => {
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(initAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('flushes analytics', async () => {
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(flushAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('logs events', async () => {
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(logEvent).toHaveBeenCalledWith('action', {
|
||||
action: `eas ${TestEasCommand.id}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('after exceptions', () => {
|
||||
it('flushes analytics', async () => {
|
||||
try {
|
||||
await TestEasCommand.run().then(() => {
|
||||
throw new Error('foo');
|
||||
});
|
||||
} catch (error) {}
|
||||
|
||||
expect(flushAsync).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import EasCommand from '../EasCommand';
|
||||
|
||||
export default class TestEasCommand extends EasCommand {
|
||||
requiresAuthentication = this.authValue();
|
||||
|
||||
authValue(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
async runAsync(): Promise<void> {}
|
||||
}
|
||||
|
||||
TestEasCommand.id = 'TestEasCommand'; // normally oclif will assign ids, but b/c this is located outside the commands folder it will not
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Command } from '@oclif/command';
|
||||
|
||||
import log from '../../log';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import Log from '../../log';
|
||||
import { showLoginPromptAsync } from '../../user/actions';
|
||||
|
||||
export default class AccountLogin extends Command {
|
||||
export default class AccountLogin extends EasCommand {
|
||||
static description = 'log in with your Expo account';
|
||||
|
||||
static aliases = ['login'];
|
||||
|
||||
async run() {
|
||||
log('Log in to EAS');
|
||||
protected mustBeRunInsideProject = false;
|
||||
protected requiresAuthentication = false;
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
await showLoginPromptAsync();
|
||||
log('Logged in');
|
||||
Log.log('Logged in');
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Command } from '@oclif/command';
|
||||
|
||||
import log from '../../log';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import Log from '../../log';
|
||||
import { logoutAsync } from '../../user/User';
|
||||
|
||||
export default class AccountLogout extends Command {
|
||||
export default class AccountLogout extends EasCommand {
|
||||
static description = 'log out';
|
||||
|
||||
static aliases = ['logout'];
|
||||
|
||||
async run() {
|
||||
protected mustBeRunInsideProject = false;
|
||||
protected requiresAuthentication = false;
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
await logoutAsync();
|
||||
log('Logged out');
|
||||
Log.log('Logged out');
|
||||
}
|
||||
}
|
||||
Loaded 100 of 599 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user