@hot-updater/apple-helper 1.2.0-rc.1 → 1.2.0-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1053 -530
- package/dist/index.d.cts +24 -12
- package/dist/index.d.mts +24 -12
- package/dist/index.mjs +1129 -602
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -23,209 +23,572 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
//#endregion
|
|
24
24
|
let node_crypto = require("node:crypto");
|
|
25
25
|
node_crypto = __toESM(node_crypto, 1);
|
|
26
|
-
let
|
|
27
|
-
let node_fs_promises = require("node:fs/promises");
|
|
28
|
-
node_fs_promises = __toESM(node_fs_promises, 1);
|
|
29
|
-
let node_net = require("node:net");
|
|
30
|
-
node_net = __toESM(node_net, 1);
|
|
31
|
-
let node_os = require("node:os");
|
|
32
|
-
node_os = __toESM(node_os, 1);
|
|
33
|
-
let node_path = require("node:path");
|
|
34
|
-
node_path = __toESM(node_path, 1);
|
|
26
|
+
let node_timers_promises = require("node:timers/promises");
|
|
35
27
|
let _hot_updater_cli_tools = require("@hot-updater/cli-tools");
|
|
36
|
-
let execa = require("execa");
|
|
37
28
|
let fs = require("fs");
|
|
38
29
|
fs = __toESM(fs, 1);
|
|
39
30
|
let path = require("path");
|
|
40
31
|
path = __toESM(path, 1);
|
|
32
|
+
let execa = require("execa");
|
|
33
|
+
let node_fs = require("node:fs");
|
|
34
|
+
let node_fs_promises = require("node:fs/promises");
|
|
35
|
+
let node_os = require("node:os");
|
|
36
|
+
let node_path = require("node:path");
|
|
41
37
|
let child_process = require("child_process");
|
|
42
38
|
let crypto = require("crypto");
|
|
43
39
|
crypto = __toESM(crypto, 1);
|
|
44
40
|
let os = require("os");
|
|
45
41
|
os = __toESM(os, 1);
|
|
46
|
-
//#region src/asc/
|
|
42
|
+
//#region src/asc/appleSrp.ts
|
|
47
43
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
44
|
+
* Apple's sign-in proof: SRP-6a over the RFC 5054 2048-bit group with
|
|
45
|
+
* SHA-256, the password stretched with PBKDF2, as Apple's sign-in page and
|
|
46
|
+
* asc, the open-source App Store Connect CLI
|
|
47
|
+
* (https://github.com/rorkai/App-Store-Connect-CLI, MIT), compute it.
|
|
52
48
|
*/
|
|
53
|
-
|
|
49
|
+
/** RFC 5054, Appendix A, the 2048-bit group. */
|
|
50
|
+
const N = BigInt("0xAC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF6095179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B9078717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB3786160279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DBFBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73");
|
|
51
|
+
const G = 2n;
|
|
52
|
+
/** The hex length every padded value has: N's byte length, twice. */
|
|
53
|
+
const PADDED_HEX_LENGTH = 512;
|
|
54
|
+
const modPow = (base, exponent, modulus) => {
|
|
55
|
+
let result = 1n;
|
|
56
|
+
let b = (base % modulus + modulus) % modulus;
|
|
57
|
+
let e = exponent;
|
|
58
|
+
while (e > 0n) {
|
|
59
|
+
if (e & 1n) result = result * b % modulus;
|
|
60
|
+
b = b * b % modulus;
|
|
61
|
+
e >>= 1n;
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
};
|
|
65
|
+
/** An even-length lowercase hex of `value`, as the proof hashes it. */
|
|
66
|
+
const toHex = (value) => {
|
|
67
|
+
const hex = value.toString(16);
|
|
68
|
+
return hex.length % 2 === 1 ? `0${hex}` : hex;
|
|
69
|
+
};
|
|
70
|
+
const fromBytes = (bytes) => bytes.length === 0 ? 0n : BigInt(`0x${Buffer.from(bytes).toString("hex")}`);
|
|
71
|
+
const sha256Hex = (hex) => node_crypto.default.createHash("sha256").update(Buffer.from(hex, "hex")).digest("hex");
|
|
72
|
+
/** SHA-256 of each value left-padded to N's length, as a number mod N. */
|
|
73
|
+
const paddedHash = (...values) => BigInt(`0x${sha256Hex(values.map((value) => value.padStart(PADDED_HEX_LENGTH, "0")).join(""))}`) % N;
|
|
74
|
+
/** The password as the protocol Apple named feeds it to PBKDF2. */
|
|
75
|
+
const preparePassword = (password, protocol) => {
|
|
76
|
+
const digest = node_crypto.default.createHash("sha256").update(password).digest();
|
|
77
|
+
if (protocol === "s2k") return digest;
|
|
78
|
+
if (protocol === "s2k_fo") return Buffer.from(digest.toString("hex"));
|
|
79
|
+
throw new Error(`Apple asked for the sign-in protocol ${protocol}.`);
|
|
80
|
+
};
|
|
81
|
+
/** A new SRP client; `secret` is the private value a, random unless set. */
|
|
82
|
+
const createSrpClient = (secret = fromBytes(node_crypto.default.randomBytes(256))) => {
|
|
83
|
+
const A = modPow(G, secret, N);
|
|
84
|
+
const aHex = toHex(A);
|
|
85
|
+
return {
|
|
86
|
+
a: Buffer.from(aHex, "hex").toString("base64"),
|
|
87
|
+
proof: ({ accountName, password, protocol, iterations, salt, b }) => {
|
|
88
|
+
const saltBytes = Buffer.from(salt, "base64");
|
|
89
|
+
const saltHex = saltBytes.toString("hex");
|
|
90
|
+
const bHex = Buffer.from(b, "base64").toString("hex");
|
|
91
|
+
const B = BigInt(`0x${bHex || "0"}`);
|
|
92
|
+
if (B % N === 0n) throw new Error("Apple answered an invalid SRP value.");
|
|
93
|
+
const derived = node_crypto.default.pbkdf2Sync(preparePassword(password, protocol), saltBytes, iterations, 32, "sha256");
|
|
94
|
+
const x = BigInt(`0x${sha256Hex(saltHex + sha256Hex(`3a${derived.toString("hex")}`))}`);
|
|
95
|
+
const k = paddedHash(toHex(N), toHex(G));
|
|
96
|
+
const u = paddedHash(aHex, bHex);
|
|
97
|
+
if (u === 0n) throw new Error("Apple answered an invalid SRP value.");
|
|
98
|
+
const S = modPow(B - k * modPow(G, x, N) % N, secret + u * x, N);
|
|
99
|
+
const K = sha256Hex(toHex(S));
|
|
100
|
+
const m1 = sha256Hex(toHex(paddedHash(toHex(N)) ^ paddedHash(toHex(G))) + node_crypto.default.createHash("sha256").update(accountName).digest("hex") + saltHex + aHex + bHex + K);
|
|
101
|
+
const m2 = toHex(BigInt(`0x${sha256Hex(aHex + m1 + K)}`));
|
|
102
|
+
return {
|
|
103
|
+
m1: Buffer.from(m1, "hex").toString("base64"),
|
|
104
|
+
m2: Buffer.from(m2, "hex").toString("base64")
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
const hasLeadingZeroBits = (digest, bits) => {
|
|
110
|
+
const full = Math.floor(bits / 8);
|
|
111
|
+
for (let index = 0; index < full; index++) if (digest[index] !== 0) return false;
|
|
112
|
+
const rest = bits % 8;
|
|
113
|
+
return rest === 0 || (digest[full] & 255 << 8 - rest & 255) === 0;
|
|
114
|
+
};
|
|
54
115
|
/**
|
|
55
|
-
* The
|
|
56
|
-
*
|
|
57
|
-
*
|
|
116
|
+
* The hashcash stamp Apple's sign-in asks for in `X-Apple-HC`: the first
|
|
117
|
+
* `1:<bits>:<date>:<challenge>::<counter>` whose SHA-1 starts with `bits`
|
|
118
|
+
* zero bits.
|
|
58
119
|
*/
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
|
|
120
|
+
const makeHashcash = (bits, challenge, now = /* @__PURE__ */ new Date()) => {
|
|
121
|
+
const date = now.toISOString().replace(/\.\d+Z$/u, "").replaceAll(/[-:T]/gu, "");
|
|
122
|
+
for (let counter = 0;; counter++) {
|
|
123
|
+
const stamp = `1:${bits}:${date}:${challenge}::${counter}`;
|
|
124
|
+
const digest = node_crypto.default.createHash("sha1").update(stamp).digest();
|
|
125
|
+
if (hasLeadingZeroBits(digest, bits)) return stamp;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/asc/appleSignIn.ts
|
|
130
|
+
/**
|
|
131
|
+
* Signing in to App Store Connect with an Apple Account, as its web page
|
|
132
|
+
* does: Apple's SRP sign-in at idmsa.apple.com, its two-factor check, then
|
|
133
|
+
* App Store Connect's session. The endpoints and their headers follow asc,
|
|
134
|
+
* the open-source App Store Connect CLI
|
|
135
|
+
* (https://github.com/rorkai/App-Store-Connect-CLI, MIT), 5.16.1, whose
|
|
136
|
+
* internal/web/auth.go has signed in this way against Apple.
|
|
137
|
+
*/
|
|
138
|
+
const AUTH_URL = "https://idmsa.apple.com/appleauth/auth";
|
|
139
|
+
const ASC_URL = "https://appstoreconnect.apple.com";
|
|
140
|
+
const SESSION_URL = `${ASC_URL}/olympus/v1/session`;
|
|
141
|
+
/**
|
|
142
|
+
* The public widget key App Store Connect's sign-in page ships, used when
|
|
143
|
+
* neither place Apple publishes the current one answers. Not a credential.
|
|
144
|
+
*/
|
|
145
|
+
const ASC_WIDGET_KEY = "e0b80c3bf78523bfe80974d320935bfa30add02e1bff88ec2166c6bd5a706c42";
|
|
146
|
+
/** Signing in, or a request in the signed-in session, failed. */
|
|
147
|
+
var AppleSignInError = class extends Error {
|
|
148
|
+
status;
|
|
149
|
+
/** The response body, for messages that depend on what Apple said. */
|
|
150
|
+
body;
|
|
71
151
|
constructor(message, options) {
|
|
72
|
-
super(message, options);
|
|
73
|
-
this.name = "
|
|
152
|
+
super(message, options?.cause ? { cause: options.cause } : void 0);
|
|
153
|
+
this.name = "AppleSignInError";
|
|
154
|
+
this.status = options?.status;
|
|
155
|
+
this.body = options?.body;
|
|
74
156
|
}
|
|
75
157
|
};
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
158
|
+
/**
|
|
159
|
+
* A cookie jar for the sign-in's requests, as a browser keeps one: `fetch`
|
|
160
|
+
* keeps none, and Apple's session is its cookies.
|
|
161
|
+
*/
|
|
162
|
+
const createCookieJar = () => {
|
|
163
|
+
let cookies = [];
|
|
164
|
+
const live = () => {
|
|
165
|
+
const now = Date.now();
|
|
166
|
+
cookies = cookies.filter((cookie) => cookie.expires === void 0 || cookie.expires > now);
|
|
167
|
+
return cookies;
|
|
168
|
+
};
|
|
169
|
+
return {
|
|
170
|
+
store(url, response) {
|
|
171
|
+
for (const header of response.headers.getSetCookie()) {
|
|
172
|
+
const [pair = "", ...attributes] = header.split(";");
|
|
173
|
+
const separator = pair.indexOf("=");
|
|
174
|
+
if (separator <= 0) continue;
|
|
175
|
+
const name = pair.slice(0, separator).trim();
|
|
176
|
+
const value = pair.slice(separator + 1).trim();
|
|
177
|
+
let domain = url.hostname;
|
|
178
|
+
let hostOnly = true;
|
|
179
|
+
let path = "/";
|
|
180
|
+
let expires;
|
|
181
|
+
let maxAge;
|
|
182
|
+
for (const attribute of attributes) {
|
|
183
|
+
const index = attribute.indexOf("=");
|
|
184
|
+
const key = (index === -1 ? attribute : attribute.slice(0, index)).trim().toLowerCase();
|
|
185
|
+
const raw = index === -1 ? "" : attribute.slice(index + 1).trim();
|
|
186
|
+
if (key === "domain" && raw) {
|
|
187
|
+
const candidate = raw.replace(/^\./u, "").toLowerCase();
|
|
188
|
+
if (url.hostname !== candidate && !url.hostname.endsWith(`.${candidate}`)) {
|
|
189
|
+
domain = "";
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
domain = candidate;
|
|
193
|
+
hostOnly = false;
|
|
194
|
+
} else if (key === "path" && raw.startsWith("/")) path = raw;
|
|
195
|
+
else if (key === "expires") {
|
|
196
|
+
const time = Date.parse(raw);
|
|
197
|
+
if (!Number.isNaN(time)) expires = time;
|
|
198
|
+
} else if (key === "max-age" && /^-?\d+$/u.test(raw)) maxAge = Number(raw);
|
|
199
|
+
}
|
|
200
|
+
if (!domain) continue;
|
|
201
|
+
if (maxAge !== void 0) expires = Date.now() + maxAge * 1e3;
|
|
202
|
+
cookies = cookies.filter((cookie) => !(cookie.name === name && cookie.domain === domain && cookie.path === path));
|
|
203
|
+
if (expires === void 0 || expires > Date.now()) cookies.push({
|
|
204
|
+
name,
|
|
205
|
+
value,
|
|
206
|
+
domain,
|
|
207
|
+
hostOnly,
|
|
208
|
+
path,
|
|
209
|
+
expires
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
header(url) {
|
|
214
|
+
const matching = live().filter((cookie) => (cookie.hostOnly ? url.hostname === cookie.domain : url.hostname === cookie.domain || url.hostname.endsWith(`.${cookie.domain}`)) && (url.pathname === cookie.path || url.pathname.startsWith(cookie.path.endsWith("/") ? cookie.path : `${cookie.path}/`)));
|
|
215
|
+
return matching.length === 0 ? void 0 : matching.map((cookie) => `${cookie.name}=${cookie.value}`).join("; ");
|
|
216
|
+
},
|
|
217
|
+
clear() {
|
|
218
|
+
cookies = [];
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
const REDIRECTS = /* @__PURE__ */ new Set([
|
|
223
|
+
301,
|
|
224
|
+
302,
|
|
225
|
+
303,
|
|
226
|
+
307,
|
|
227
|
+
308
|
|
228
|
+
]);
|
|
229
|
+
/** `fetch` with the jar's cookies, following redirects itself to keep them. */
|
|
230
|
+
const createClient = (fetch) => {
|
|
231
|
+
const jar = createCookieJar();
|
|
232
|
+
const request = async (url, init = {}) => {
|
|
233
|
+
let current = new URL(url);
|
|
234
|
+
let method = init.method ?? "GET";
|
|
235
|
+
let body = init.body;
|
|
236
|
+
for (let hop = 0; hop < 10; hop++) {
|
|
237
|
+
const headers = new Headers(init.headers);
|
|
238
|
+
const cookie = jar.header(current);
|
|
239
|
+
if (cookie) headers.set("Cookie", cookie);
|
|
240
|
+
const response = await fetch(current, {
|
|
241
|
+
method,
|
|
242
|
+
headers,
|
|
243
|
+
...body === void 0 ? {} : { body },
|
|
244
|
+
redirect: "manual"
|
|
245
|
+
});
|
|
246
|
+
jar.store(current, response);
|
|
247
|
+
const location = response.headers.get("location");
|
|
248
|
+
if (!REDIRECTS.has(response.status) || !location) return response;
|
|
249
|
+
await response.text();
|
|
250
|
+
current = new URL(location, current);
|
|
251
|
+
if (response.status === 303 || response.status !== 307 && response.status !== 308 && method === "POST") {
|
|
252
|
+
method = "GET";
|
|
253
|
+
body = void 0;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
throw new AppleSignInError(`${url} redirected too many times.`);
|
|
257
|
+
};
|
|
258
|
+
return {
|
|
259
|
+
request,
|
|
260
|
+
clear: jar.clear
|
|
261
|
+
};
|
|
79
262
|
};
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
263
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
264
|
+
const text$1 = (value) => typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
265
|
+
const parseJson = (body) => {
|
|
266
|
+
try {
|
|
267
|
+
return JSON.parse(body);
|
|
268
|
+
} catch {
|
|
269
|
+
return;
|
|
84
270
|
}
|
|
85
|
-
return true;
|
|
86
271
|
};
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
272
|
+
/** What Apple's sign-in said went wrong, from its `serviceErrors`. */
|
|
273
|
+
const serviceErrors = (body) => {
|
|
274
|
+
const parsed = parseJson(body);
|
|
275
|
+
const errors = isRecord$1(parsed) && Array.isArray(parsed.serviceErrors) ? parsed.serviceErrors.filter(isRecord$1) : [];
|
|
276
|
+
return {
|
|
277
|
+
codes: errors.map((error) => text$1(error.code)).filter(Boolean),
|
|
278
|
+
message: errors.map((error) => text$1(error.message)).find(Boolean)
|
|
279
|
+
};
|
|
280
|
+
};
|
|
281
|
+
const failure = (step, response, body) => new AppleSignInError(`${step} failed (HTTP ${response.status}).${serviceErrors(body).message ? `\n${serviceErrors(body).message}` : ""}`, {
|
|
282
|
+
status: response.status,
|
|
283
|
+
body
|
|
90
284
|
});
|
|
91
|
-
/** Where a downloaded asc is kept, per user, so every project shares it. */
|
|
92
|
-
const defaultAscCacheDir = () => node_path.default.join(node_os.default.homedir(), "Library", "Caches", "hot-updater");
|
|
93
285
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* one: TestFlight builds need Xcode.
|
|
286
|
+
* The widget key Apple's sign-in wants: from the sign-out redirect App Store
|
|
287
|
+
* Connect answers without cookies, else its config, else the one its page
|
|
288
|
+
* ships.
|
|
98
289
|
*/
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
reject: false,
|
|
104
|
-
env: { ASC_TELEMETRY_DISABLED: "1" },
|
|
105
|
-
timeout: 15e3
|
|
290
|
+
const findWidgetKey = async (fetch, client) => {
|
|
291
|
+
const signOut = await fetch(`${ASC_URL}/logout`, {
|
|
292
|
+
method: "HEAD",
|
|
293
|
+
redirect: "manual"
|
|
106
294
|
}).catch(() => void 0);
|
|
107
|
-
const
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
295
|
+
const location = signOut?.headers.get("location");
|
|
296
|
+
if (signOut && REDIRECTS.has(signOut.status) && location) {
|
|
297
|
+
const url = (() => {
|
|
298
|
+
try {
|
|
299
|
+
return new URL(location);
|
|
300
|
+
} catch {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
})();
|
|
304
|
+
const keys = url?.searchParams.getAll("widgetKey") ?? [];
|
|
305
|
+
if (url?.protocol === "https:" && url.hostname === "idmsa.apple.com" && url.pathname === "/appleauth/signout" && keys.length === 1 && keys[0].trim()) return keys[0].trim();
|
|
306
|
+
}
|
|
307
|
+
const config = await client.request(`${ASC_URL}/olympus/v1/app/config?hostname=itunesconnect.apple.com`);
|
|
308
|
+
const body = await config.text();
|
|
309
|
+
if (config.status === 404) return ASC_WIDGET_KEY;
|
|
310
|
+
const parsed = parseJson(body);
|
|
311
|
+
const key = isRecord$1(parsed) ? text$1(parsed.authServiceKey) ?? text$1(parsed.serviceKey) : void 0;
|
|
312
|
+
if (!config.ok || !key) throw new AppleSignInError(`Couldn't load Apple's sign-in configuration (HTTP ${config.status}).`, { status: config.status });
|
|
313
|
+
return key;
|
|
314
|
+
};
|
|
315
|
+
const hashcash = async (client, widgetKey) => {
|
|
316
|
+
const response = await client.request(`${AUTH_URL}/signin?widgetKey=${encodeURIComponent(widgetKey)}`, { headers: { Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" } });
|
|
317
|
+
await response.text();
|
|
318
|
+
if (!response.ok) throw new AppleSignInError(`Loading Apple's sign-in page failed (HTTP ${response.status}).`, { status: response.status });
|
|
319
|
+
const bits = response.headers.get("x-apple-hc-bits")?.trim();
|
|
320
|
+
const challenge = response.headers.get("x-apple-hc-challenge")?.trim();
|
|
321
|
+
if (!bits || !challenge) return void 0;
|
|
322
|
+
if (!/^\d+$/u.test(bits) || Number(bits) > 32) throw new AppleSignInError(`Apple asked for a hashcash of ${bits} bits.`);
|
|
323
|
+
return makeHashcash(Number(bits), challenge);
|
|
324
|
+
};
|
|
325
|
+
const pendingHeaders = (pending) => ({
|
|
326
|
+
"X-Apple-ID-Session-Id": pending.sessionId,
|
|
327
|
+
"X-Apple-Widget-Key": pending.widgetKey,
|
|
328
|
+
scnt: pending.scnt,
|
|
329
|
+
Accept: "application/json"
|
|
330
|
+
});
|
|
331
|
+
/** Apple rotates `scnt` and the session ID; each request sends the latest. */
|
|
332
|
+
const track = (pending, response) => {
|
|
333
|
+
const scnt = response.headers.get("scnt")?.trim();
|
|
334
|
+
const sessionId = response.headers.get("x-apple-id-session-id")?.trim();
|
|
335
|
+
if (scnt) pending.scnt = scnt;
|
|
336
|
+
if (sessionId) pending.sessionId = sessionId;
|
|
337
|
+
};
|
|
338
|
+
const CANCELLED = "Signing in was cancelled.";
|
|
339
|
+
const twoFactor = async (client, pending, prompts) => {
|
|
340
|
+
const send = async (url, method, payload) => {
|
|
341
|
+
const response = await client.request(url, {
|
|
342
|
+
method,
|
|
343
|
+
headers: {
|
|
344
|
+
...pendingHeaders(pending),
|
|
345
|
+
...payload === void 0 ? {} : { "Content-Type": "application/json" }
|
|
346
|
+
},
|
|
347
|
+
...payload === void 0 ? {} : { body: JSON.stringify(payload) }
|
|
348
|
+
});
|
|
349
|
+
track(pending, response);
|
|
350
|
+
return {
|
|
351
|
+
response,
|
|
352
|
+
body: await response.text()
|
|
353
|
+
};
|
|
112
354
|
};
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
355
|
+
const options = await send(AUTH_URL, "GET");
|
|
356
|
+
if (!options.response.ok) throw failure("Loading the two-factor options", options.response, options.body);
|
|
357
|
+
const parsed = parseJson(options.body);
|
|
358
|
+
const phone = (isRecord$1(parsed) && Array.isArray(parsed.trustedPhoneNumbers) ? parsed.trustedPhoneNumbers.filter(isRecord$1) : []).filter((phone) => typeof phone.id === "number")[0];
|
|
359
|
+
const noDevices = isRecord$1(parsed) && parsed.noTrustedDevices === true;
|
|
360
|
+
if (noDevices && !phone) throw new AppleSignInError("The Apple Account has no trusted device or phone number for two-factor codes.");
|
|
361
|
+
const phoneMode = text$1(phone?.pushMode) ?? "sms";
|
|
362
|
+
const destination = text$1(phone?.numberWithDialCode);
|
|
363
|
+
const securityCode = isRecord$1(parsed) && isRecord$1(parsed.securityCode) ? parsed.securityCode.length : void 0;
|
|
364
|
+
const length = typeof securityCode === "number" && securityCode > 0 ? securityCode : 6;
|
|
365
|
+
const textCode = async () => {
|
|
366
|
+
const requested = await send(`${AUTH_URL}/verify/phone`, "PUT", {
|
|
367
|
+
phoneNumber: { id: phone.id },
|
|
368
|
+
mode: phoneMode
|
|
369
|
+
});
|
|
370
|
+
if (!requested.response.ok) throw failure("Texting a two-factor code", requested.response, requested.body);
|
|
121
371
|
};
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
372
|
+
const verify = async (via, code) => {
|
|
373
|
+
const verified = via === "device" ? await send(`${AUTH_URL}/verify/trusteddevice/securitycode`, "POST", { securityCode: { code } }) : await send(`${AUTH_URL}/verify/phone/securitycode`, "POST", {
|
|
374
|
+
securityCode: { code },
|
|
375
|
+
phoneNumber: { id: phone.id },
|
|
376
|
+
mode: phoneMode
|
|
377
|
+
});
|
|
378
|
+
if (verified.response.ok) return true;
|
|
379
|
+
if ([
|
|
380
|
+
400,
|
|
381
|
+
401,
|
|
382
|
+
403
|
|
383
|
+
].includes(verified.response.status)) return false;
|
|
384
|
+
throw failure("Checking the two-factor code", verified.response, verified.body);
|
|
385
|
+
};
|
|
386
|
+
const ask = async (via, afterWrongCode) => {
|
|
387
|
+
const code = await prompts.twoFactorCode({
|
|
388
|
+
via,
|
|
389
|
+
destination,
|
|
390
|
+
textsOnWrongCode: via === "device" && phone !== void 0,
|
|
391
|
+
afterWrongCode,
|
|
392
|
+
length
|
|
393
|
+
});
|
|
394
|
+
if (code === void 0) throw new AppleSignInError(CANCELLED);
|
|
395
|
+
return code;
|
|
396
|
+
};
|
|
397
|
+
let accepted;
|
|
398
|
+
if (noDevices) {
|
|
399
|
+
await textCode();
|
|
400
|
+
accepted = await verify("sms", await ask("sms", false));
|
|
401
|
+
} else {
|
|
402
|
+
accepted = await verify("device", await ask("device", false));
|
|
403
|
+
if (!accepted && phone) {
|
|
404
|
+
await textCode();
|
|
405
|
+
accepted = await verify("sms", await ask("sms", true));
|
|
130
406
|
}
|
|
131
|
-
if (!response.ok || !response.body) throw new AscCliUnavailableError(`Could not download asc ${ASC_CLI_VERSION} from GitHub (HTTP ${response.status}). ${INSTALL_HINT}`);
|
|
132
|
-
await node_fs_promises.default.writeFile(partial, Buffer.from(await response.arrayBuffer()), { mode: 448 });
|
|
133
|
-
const actual = await sha256Of(partial);
|
|
134
|
-
if (actual !== expected) throw new AscCliUnavailableError(`The downloaded asc ${ASC_CLI_VERSION} does not match its published SHA-256 (expected ${expected}, got ${actual}), so it was deleted and not run. ${INSTALL_HINT}`);
|
|
135
|
-
await node_fs_promises.default.chmod(partial, 493);
|
|
136
|
-
await node_fs_promises.default.rename(partial, binary);
|
|
137
|
-
} finally {
|
|
138
|
-
await node_fs_promises.default.rm(partial, { force: true });
|
|
139
407
|
}
|
|
408
|
+
if (!accepted) throw new AppleSignInError("Apple rejected the verification code.");
|
|
409
|
+
const trust = await send(`${AUTH_URL}/2sv/trust`, "GET");
|
|
410
|
+
if (!trust.response.ok) throw failure("Finishing the two-factor check", trust.response, trust.body);
|
|
411
|
+
};
|
|
412
|
+
const parseProvider = (value) => {
|
|
413
|
+
if (!isRecord$1(value) || typeof value.providerId !== "number") return void 0;
|
|
414
|
+
const publicProviderId = text$1(value.publicProviderId);
|
|
415
|
+
if (!publicProviderId) return void 0;
|
|
140
416
|
return {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
417
|
+
providerId: value.providerId,
|
|
418
|
+
publicProviderId,
|
|
419
|
+
name: text$1(value.name)
|
|
144
420
|
};
|
|
145
421
|
};
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
422
|
+
const readSession = async (client) => {
|
|
423
|
+
const response = await client.request(SESSION_URL, { headers: { Accept: "application/json" } });
|
|
424
|
+
const body = await response.text();
|
|
425
|
+
if (!response.ok) throw new AppleSignInError(response.status === 401 || response.status === 403 ? "App Store Connect didn't accept the sign-in. Check that the Apple Account is in an Apple Developer team." : `Loading the App Store Connect session failed (HTTP ${response.status}).`, {
|
|
426
|
+
status: response.status,
|
|
427
|
+
body
|
|
428
|
+
});
|
|
429
|
+
const parsed = parseJson(body);
|
|
430
|
+
const provider = isRecord$1(parsed) ? parseProvider(parsed.provider) : void 0;
|
|
431
|
+
if (!isRecord$1(parsed) || !provider) throw new AppleSignInError("App Store Connect's session names no team for the Apple Account.");
|
|
432
|
+
const providers = (Array.isArray(parsed.availableProviders) ? parsed.availableProviders : []).map(parseProvider).filter((value) => value !== void 0);
|
|
433
|
+
return {
|
|
434
|
+
provider,
|
|
435
|
+
providers: providers.some((value) => value.providerId === provider.providerId) ? providers : [provider, ...providers]
|
|
436
|
+
};
|
|
156
437
|
};
|
|
157
|
-
const TERMINATION_SIGNALS$3 = [
|
|
158
|
-
"SIGHUP",
|
|
159
|
-
"SIGINT",
|
|
160
|
-
"SIGTERM"
|
|
161
|
-
];
|
|
162
438
|
/**
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
439
|
+
* Signs in to App Store Connect with `appleId` and `password`, asking
|
|
440
|
+
* `prompts` for the two-factor code. The session lives only in the returned
|
|
441
|
+
* object's memory.
|
|
166
442
|
*/
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
"
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
443
|
+
const signInToAppStoreConnect = async ({ appleId, password, prompts, fetch = globalThis.fetch }) => {
|
|
444
|
+
const client = createClient(fetch);
|
|
445
|
+
try {
|
|
446
|
+
const widgetKey = await findWidgetKey(fetch, client);
|
|
447
|
+
const json = {
|
|
448
|
+
"Content-Type": "application/json",
|
|
449
|
+
"X-Apple-Widget-Key": widgetKey,
|
|
450
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
451
|
+
Accept: "application/json, text/javascript"
|
|
452
|
+
};
|
|
453
|
+
const srp = createSrpClient();
|
|
454
|
+
const init = await client.request(`${AUTH_URL}/signin/init`, {
|
|
455
|
+
method: "POST",
|
|
456
|
+
headers: json,
|
|
457
|
+
body: JSON.stringify({
|
|
458
|
+
accountName: appleId,
|
|
459
|
+
protocols: ["s2k", "s2k_fo"],
|
|
460
|
+
a: srp.a
|
|
461
|
+
})
|
|
462
|
+
});
|
|
463
|
+
const initBody = await init.text();
|
|
464
|
+
if (!init.ok) throw failure("Starting the sign-in", init, initBody);
|
|
465
|
+
const started = parseJson(initBody);
|
|
466
|
+
if (!isRecord$1(started) || typeof started.iteration !== "number" || !text$1(started.salt) || !text$1(started.b) || !text$1(started.protocol)) throw new AppleSignInError("Apple's sign-in answered no SRP challenge.");
|
|
467
|
+
const { m1, m2 } = srp.proof({
|
|
468
|
+
accountName: appleId,
|
|
469
|
+
password,
|
|
470
|
+
protocol: text$1(started.protocol),
|
|
471
|
+
iterations: started.iteration,
|
|
472
|
+
salt: text$1(started.salt),
|
|
473
|
+
b: text$1(started.b)
|
|
474
|
+
});
|
|
475
|
+
const stamp = await hashcash(client, widgetKey);
|
|
476
|
+
const complete = await client.request(`${AUTH_URL}/signin/complete?isRememberMeEnabled=false`, {
|
|
477
|
+
method: "POST",
|
|
478
|
+
headers: {
|
|
479
|
+
...json,
|
|
480
|
+
...stamp ? { "X-Apple-HC": stamp } : {}
|
|
481
|
+
},
|
|
482
|
+
body: JSON.stringify({
|
|
483
|
+
accountName: appleId,
|
|
484
|
+
rememberMe: false,
|
|
485
|
+
m1,
|
|
486
|
+
m2,
|
|
487
|
+
c: started.c
|
|
488
|
+
})
|
|
489
|
+
});
|
|
490
|
+
const completeBody = await complete.text();
|
|
491
|
+
if (complete.status === 409) {
|
|
492
|
+
const sessionId = complete.headers.get("x-apple-id-session-id")?.trim();
|
|
493
|
+
const scnt = complete.headers.get("scnt")?.trim();
|
|
494
|
+
if (!sessionId || !scnt) throw new AppleSignInError("Apple asked for a two-factor code without a sign-in session.");
|
|
495
|
+
await twoFactor(client, {
|
|
496
|
+
widgetKey,
|
|
497
|
+
sessionId,
|
|
498
|
+
scnt
|
|
499
|
+
}, prompts);
|
|
500
|
+
} else if (!complete.ok) {
|
|
501
|
+
const { codes } = serviceErrors(completeBody);
|
|
502
|
+
const authType = (() => {
|
|
503
|
+
const parsed = parseJson(completeBody);
|
|
504
|
+
return isRecord$1(parsed) ? text$1(parsed.authType) : void 0;
|
|
200
505
|
})();
|
|
201
|
-
|
|
506
|
+
if (complete.status === 401 && codes.includes("-20101")) throw new AppleSignInError("Apple rejected the email or password.", { status: 401 });
|
|
507
|
+
if (complete.status === 412 && authType && [
|
|
508
|
+
"sa",
|
|
509
|
+
"hsa",
|
|
510
|
+
"non-sa",
|
|
511
|
+
"hsa2"
|
|
512
|
+
].includes(authType)) throw new AppleSignInError("The Apple Account has a pending prompt. Sign in at https://appstoreconnect.apple.com once, then run this again.", { status: 412 });
|
|
513
|
+
throw failure("Signing in", complete, completeBody);
|
|
202
514
|
}
|
|
203
|
-
|
|
515
|
+
let { provider, providers } = await readSession(client);
|
|
516
|
+
return {
|
|
517
|
+
appleId,
|
|
518
|
+
get provider() {
|
|
519
|
+
return provider;
|
|
520
|
+
},
|
|
521
|
+
get providers() {
|
|
522
|
+
return providers;
|
|
523
|
+
},
|
|
524
|
+
selectProvider: async (publicProviderId) => {
|
|
525
|
+
if (provider.publicProviderId === publicProviderId) return provider;
|
|
526
|
+
const selected = providers.find((value) => value.publicProviderId === publicProviderId);
|
|
527
|
+
if (!selected) throw new AppleSignInError(`The Apple Account is in no team ${publicProviderId}.`);
|
|
528
|
+
const response = await client.request(SESSION_URL, {
|
|
529
|
+
method: "POST",
|
|
530
|
+
headers: {
|
|
531
|
+
"Content-Type": "application/json",
|
|
532
|
+
"X-Requested-With": "olympus-ui"
|
|
533
|
+
},
|
|
534
|
+
body: JSON.stringify({ provider: { providerId: selected.providerId } })
|
|
535
|
+
});
|
|
536
|
+
const body = await response.text();
|
|
537
|
+
if (!response.ok) throw new AppleSignInError(`Switching to the team ${selected.name ?? publicProviderId} failed (HTTP ${response.status}).`, {
|
|
538
|
+
status: response.status,
|
|
539
|
+
body
|
|
540
|
+
});
|
|
541
|
+
({provider, providers} = await readSession(client));
|
|
542
|
+
if (provider.providerId !== selected.providerId) throw new AppleSignInError(`App Store Connect stayed in the team ${provider.name ?? provider.publicProviderId}.`);
|
|
543
|
+
return provider;
|
|
544
|
+
},
|
|
545
|
+
request: client.request,
|
|
546
|
+
clear: client.clear
|
|
547
|
+
};
|
|
548
|
+
} catch (error) {
|
|
549
|
+
client.clear();
|
|
550
|
+
throw error;
|
|
551
|
+
}
|
|
204
552
|
};
|
|
553
|
+
//#endregion
|
|
554
|
+
//#region src/asc/programLicenseAgreement.ts
|
|
205
555
|
/**
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
* stored, the keychain is bypassed, and telemetry is off.
|
|
556
|
+
* What to do when Apple holds a team's requests until the Account Holder
|
|
557
|
+
* accepts the latest Apple Developer Program License Agreement: App Store
|
|
558
|
+
* Connect answers 403, and xcodebuild prints "PLA Update available".
|
|
210
559
|
*/
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
560
|
+
const PROGRAM_LICENSE_AGREEMENT_HINT = ["The Account Holder must accept the latest Program License Agreement:", "https://developer.apple.com/account"].join("\n");
|
|
561
|
+
/** The code App Store Connect answers a team with a pending agreement. */
|
|
562
|
+
const AGREEMENT_ERROR_CODE = "FORBIDDEN.REQUIRED_AGREEMENTS_MISSING_OR_EXPIRED";
|
|
563
|
+
/**
|
|
564
|
+
* Whether App Store Connect's JSON:API errors say the team has a Program
|
|
565
|
+
* License Agreement to accept, by its code or its detail.
|
|
566
|
+
*/
|
|
567
|
+
const isProgramLicenseAgreementError = (errors) => errors.some(({ code, detail, title }) => code === AGREEMENT_ERROR_CODE || /Program License Agreement/iu.test(`${detail ?? ""} ${title ?? ""}`));
|
|
568
|
+
/** Whether xcodebuild's output says the team has an agreement to accept. */
|
|
569
|
+
const mentionsProgramLicenseAgreement = (output) => output.includes("PLA Update available");
|
|
570
|
+
//#endregion
|
|
571
|
+
//#region src/asc/ascKeyIssuer.ts
|
|
572
|
+
const IRIS_URL = "https://appstoreconnect.apple.com/iris/v1";
|
|
573
|
+
/**
|
|
574
|
+
* The headers App Store Connect's Integrations page sends its API with. A
|
|
575
|
+
* function: `ascApiKey` imports this module, so its constants aren't set
|
|
576
|
+
* yet when this module loads.
|
|
577
|
+
*/
|
|
578
|
+
const irisHeaders = () => ({
|
|
579
|
+
Accept: "application/vnd.api+json, application/json, text/csv",
|
|
580
|
+
"Content-Type": "application/json",
|
|
581
|
+
"X-CSRF-ITC": "[asc-ui]",
|
|
582
|
+
Origin: "https://appstoreconnect.apple.com",
|
|
583
|
+
Referer: ASC_INTEGRATIONS_URL
|
|
584
|
+
});
|
|
228
585
|
const answer = (value) => _hot_updater_cli_tools.p.isCancel(value) ? void 0 : value;
|
|
586
|
+
/** One line: where the code went, and where a wrong one sends the next. */
|
|
587
|
+
const twoFactorMessage = (request) => {
|
|
588
|
+
const phone = request.destination ?? "your trusted phone";
|
|
589
|
+
if (request.via === "sms") return `${request.afterWrongCode ? "Wrong code. " : ""}Code texted to ${phone}`;
|
|
590
|
+
return request.textsOnWrongCode ? `Code on your Apple device (wrong code texts ${phone})` : "Code on your Apple device";
|
|
591
|
+
};
|
|
229
592
|
/** Hot Updater's own prompts, so the sign-in looks like the rest of the CLI. */
|
|
230
593
|
const cliPrompts = {
|
|
231
594
|
password: async (appleId) => answer(await _hot_updater_cli_tools.p.password({
|
|
@@ -233,65 +596,53 @@ const cliPrompts = {
|
|
|
233
596
|
mask: "•",
|
|
234
597
|
validate: (value) => value ? void 0 : "Enter the password."
|
|
235
598
|
})),
|
|
236
|
-
twoFactorCode: async (
|
|
237
|
-
message:
|
|
238
|
-
validate: (value) =>
|
|
599
|
+
twoFactorCode: async (request) => answer(await _hot_updater_cli_tools.p.text({
|
|
600
|
+
message: twoFactorMessage(request),
|
|
601
|
+
validate: (value) => new RegExp(`^\\d{${request.length}}$`, "u").test(value?.trim() ?? "") ? void 0 : `Enter the ${request.length}-digit code.`
|
|
239
602
|
}))?.trim()
|
|
240
603
|
};
|
|
241
|
-
/** `value` as one word for `sh`. */
|
|
242
|
-
const shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
243
|
-
/**
|
|
244
|
-
* A two-factor code command for asc that asks `prompts` in this process:
|
|
245
|
-
* asc runs it when Apple wants a code, and it prints what the developer
|
|
246
|
-
* typed, which it gets over a socket in the 0700 work directory. A
|
|
247
|
-
* cancelled prompt makes it print nothing and fail, which stops the sign-in.
|
|
248
|
-
*/
|
|
249
|
-
const listenForTwoFactorCodes = async (root, prompts) => {
|
|
250
|
-
const socketPath = node_path.default.join(root, "2fa.sock");
|
|
251
|
-
const scriptPath = node_path.default.join(root, "two-factor-code.cjs");
|
|
252
|
-
await node_fs_promises.default.writeFile(scriptPath, [
|
|
253
|
-
"const socket = require(\"node:net\").connect(" + JSON.stringify(socketPath) + ");",
|
|
254
|
-
"let code = \"\";",
|
|
255
|
-
"socket.on(\"data\", (chunk) => { code += chunk; });",
|
|
256
|
-
"socket.on(\"end\", () => {",
|
|
257
|
-
" code = code.trim();",
|
|
258
|
-
" if (!code) process.exit(1);",
|
|
259
|
-
" process.stdout.write(code + \"\\n\");",
|
|
260
|
-
"});",
|
|
261
|
-
"socket.on(\"error\", () => process.exit(1));",
|
|
262
|
-
""
|
|
263
|
-
].join("\n"), { mode: 384 });
|
|
264
|
-
let attempts = 0;
|
|
265
|
-
let cancelled = false;
|
|
266
|
-
const server = node_net.default.createServer((socket) => {
|
|
267
|
-
attempts += 1;
|
|
268
|
-
prompts.twoFactorCode(attempts).then((code) => {
|
|
269
|
-
if (!code) cancelled = true;
|
|
270
|
-
socket.end(code ? `${code}\n` : "");
|
|
271
|
-
}, () => {
|
|
272
|
-
cancelled = true;
|
|
273
|
-
socket.destroy();
|
|
274
|
-
});
|
|
275
|
-
});
|
|
276
|
-
await new Promise((resolve, reject) => {
|
|
277
|
-
server.once("error", reject);
|
|
278
|
-
server.listen(socketPath, resolve);
|
|
279
|
-
});
|
|
280
|
-
return {
|
|
281
|
-
command: `${shellQuote(process.execPath)} ${shellQuote(scriptPath)}`,
|
|
282
|
-
cancelled: () => cancelled,
|
|
283
|
-
close: () => new Promise((resolve) => server.close(() => resolve()))
|
|
284
|
-
};
|
|
285
|
-
};
|
|
286
604
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
287
605
|
const text = (value) => typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
606
|
+
const toTeam = (appleId, provider) => ({
|
|
607
|
+
appleId,
|
|
608
|
+
teamId: provider.publicProviderId,
|
|
609
|
+
...provider.name ? { name: provider.name } : {}
|
|
610
|
+
});
|
|
611
|
+
/** What App Store Connect's API said went wrong, from its JSON:API errors. */
|
|
612
|
+
const irisDetail = (body) => {
|
|
613
|
+
try {
|
|
614
|
+
const parsed = JSON.parse(body);
|
|
615
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.errors)) return void 0;
|
|
616
|
+
return parsed.errors.filter(isRecord).map((error) => text(error.detail) ?? text(error.title)).find(Boolean);
|
|
617
|
+
} catch {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
/** What to do about a key App Store Connect would not create. */
|
|
622
|
+
const createFailureHint = (status, body) => {
|
|
623
|
+
if (/agreement/iu.test(body)) return PROGRAM_LICENSE_AGREEMENT_HINT;
|
|
624
|
+
if (/limit|maximum/iu.test(body)) return `The team has the most API keys App Store Connect allows. Revoke one at:\n${ASC_INTEGRATIONS_URL}`;
|
|
625
|
+
if (status === 403 || /account holder|admin|permission/iu.test(body)) return "Creating an API key needs the Account Holder or Admin role in the team.";
|
|
626
|
+
};
|
|
627
|
+
const irisRequest = async (session, step, method, path, payload) => {
|
|
628
|
+
const response = await session.request(`${IRIS_URL}${path}`, {
|
|
629
|
+
method,
|
|
630
|
+
headers: irisHeaders(),
|
|
631
|
+
...payload === void 0 ? {} : { body: JSON.stringify(payload) }
|
|
632
|
+
});
|
|
633
|
+
const body = await response.text();
|
|
634
|
+
if (!response.ok) {
|
|
635
|
+
const detail = irisDetail(body);
|
|
636
|
+
throw new AppleSignInError(`${step} failed (HTTP ${response.status}).${detail ? `\n${detail}` : ""}`, {
|
|
637
|
+
status: response.status,
|
|
638
|
+
body
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
try {
|
|
642
|
+
const parsed = JSON.parse(body);
|
|
643
|
+
if (isRecord(parsed) && isRecord(parsed.data)) return parsed.data;
|
|
644
|
+
} catch {}
|
|
645
|
+
throw new AppleSignInError(`${step} answered no API key.`, { status: response.status });
|
|
295
646
|
};
|
|
296
647
|
/** A P-256 private key, as App Store Connect issues one, or a thrown error. */
|
|
297
648
|
const assertP8 = (p8) => {
|
|
@@ -299,144 +650,108 @@ const assertP8 = (p8) => {
|
|
|
299
650
|
try {
|
|
300
651
|
key = node_crypto.default.createPrivateKey(p8);
|
|
301
652
|
} catch {
|
|
302
|
-
throw new Error("
|
|
653
|
+
throw new Error("App Store Connect sent a .p8 that is not a private key.");
|
|
303
654
|
}
|
|
304
|
-
if (key.asymmetricKeyType !== "ec") throw new Error("
|
|
655
|
+
if (key.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1") throw new Error("App Store Connect sent a .p8 that is not a P-256 private key.");
|
|
656
|
+
};
|
|
657
|
+
/** The one-time `.p8`; a key App Store Connect hasn't settled yet is retried. */
|
|
658
|
+
const downloadP8 = async (session, keyId, wait) => {
|
|
659
|
+
for (let attempt = 1;; attempt++) try {
|
|
660
|
+
const data = await irisRequest(session, "Downloading the key's .p8", "GET", `/apiKeys/${encodeURIComponent(keyId)}?${new URLSearchParams({ "fields[apiKeys]": "privateKey" })}`);
|
|
661
|
+
const encoded = isRecord(data.attributes) ? text(data.attributes.privateKey) : void 0;
|
|
662
|
+
if (text(data.id) !== keyId || !encoded) throw new Error("App Store Connect sent no .p8 for the key.");
|
|
663
|
+
return Buffer.from(encoded, "base64").toString("utf-8").trim();
|
|
664
|
+
} catch (error) {
|
|
665
|
+
if (!(error instanceof AppleSignInError && error.status !== void 0 && (error.status === 404 || error.status === 409 || error.status === 429 || error.status >= 500)) || attempt === 3) throw error;
|
|
666
|
+
await wait(attempt * 1e3);
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
const issuerOf = (data) => {
|
|
670
|
+
const attributes = isRecord(data.attributes) ? data.attributes : {};
|
|
671
|
+
const relationships = isRecord(data.relationships) ? data.relationships : {};
|
|
672
|
+
const related = isRecord(relationships.provider) ? relationships.provider.data : void 0;
|
|
673
|
+
return (isRecord(attributes.provider) ? text(attributes.provider.id) : void 0) ?? (isRecord(related) ? text(related.id) : void 0);
|
|
305
674
|
};
|
|
306
675
|
/**
|
|
307
|
-
* `IssueAscApiKey`
|
|
308
|
-
* (
|
|
676
|
+
* `IssueAscApiKey` by signing in as App Store Connect's web page does
|
|
677
|
+
* (`signInToAppStoreConnect`) and creating the key with the API its
|
|
678
|
+
* Integrations page uses:
|
|
309
679
|
*
|
|
310
|
-
* 1.
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
* 3.
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
* 4. `revoke` runs `asc web api-keys revoke` in the same session.
|
|
680
|
+
* 1. The password typed into Hot Updater's prompt goes only to Apple's SRP
|
|
681
|
+
* sign-in, which sends a proof of it, never the password; the two-factor
|
|
682
|
+
* code goes only to Apple's check. Neither is kept or printed.
|
|
683
|
+
* 2. `chooseTeam` sees the teams the Apple Account is in, unless `teamId`
|
|
684
|
+
* is set.
|
|
685
|
+
* 3. The key is created, its one-time `.p8` downloaded and checked; a key
|
|
686
|
+
* that fails the check is revoked at once.
|
|
687
|
+
* 4. `revoke` deactivates the key in the same session.
|
|
319
688
|
*
|
|
320
|
-
*
|
|
321
|
-
* temporary 0700 directory, which `dispose` deletes, as does the process
|
|
322
|
-
* exiting first.
|
|
689
|
+
* The session is cookies in this process's memory, which `dispose` drops.
|
|
323
690
|
*
|
|
324
|
-
* @param
|
|
325
|
-
* @param env The environment asc's own is derived from.
|
|
691
|
+
* @param fetch Where requests go, Apple unless set.
|
|
326
692
|
* @param prompts Where the password and two-factor codes are asked.
|
|
693
|
+
* @param wait How a retried download waits.
|
|
327
694
|
*/
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
_hot_updater_cli_tools.p.log.info("
|
|
695
|
+
const createAscKeyIssuer = ({ fetch = globalThis.fetch, prompts = cliPrompts, wait = node_timers_promises.setTimeout } = {}) => async ({ name, role, appleId, teamId, chooseTeam }) => {
|
|
696
|
+
if (!appleId) throw new AppleSignInError("Signing in needs the Apple Account's email.");
|
|
697
|
+
_hot_updater_cli_tools.p.log.info("Signing in to App Store Connect. Only the issued key is kept.");
|
|
331
698
|
const password = await prompts.password(appleId);
|
|
332
|
-
if (password === void 0) throw new
|
|
699
|
+
if (password === void 0) throw new AppleSignInError("Signing in was cancelled.");
|
|
333
700
|
(0, _hot_updater_cli_tools.registerSecretValue)(password);
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
stdout: "pipe",
|
|
345
|
-
stderr: "pipe",
|
|
346
|
-
reject: false
|
|
347
|
-
});
|
|
348
|
-
const stderr = String(result.stderr ?? "");
|
|
349
|
-
if (result.exitCode !== 0 || result.failed) throw new AscCommandError(`${step} failed: ${lastLine(stderr) ?? (result.signal ? `stopped by ${result.signal}` : void 0) ?? `exit code ${result.exitCode}`}`, result.exitCode, { cause: { stderr } });
|
|
350
|
-
let parsed;
|
|
351
|
-
try {
|
|
352
|
-
parsed = JSON.parse(String(result.stdout));
|
|
353
|
-
} catch {
|
|
354
|
-
parsed = void 0;
|
|
355
|
-
}
|
|
356
|
-
if (!isRecord(parsed)) throw new AscCommandError(`${step} printed no JSON this version of Hot Updater reads.`, result.exitCode);
|
|
357
|
-
return parsed;
|
|
701
|
+
const session = await signInToAppStoreConnect({
|
|
702
|
+
appleId,
|
|
703
|
+
password,
|
|
704
|
+
prompts,
|
|
705
|
+
fetch
|
|
706
|
+
});
|
|
707
|
+
let disposed = false;
|
|
708
|
+
const dispose = async () => {
|
|
709
|
+
disposed = true;
|
|
710
|
+
session.clear();
|
|
358
711
|
};
|
|
359
712
|
try {
|
|
360
|
-
const
|
|
361
|
-
|
|
713
|
+
const teams = session.providers.map((provider) => toTeam(appleId, provider));
|
|
714
|
+
const chosen = teamId ?? await chooseTeam?.(teams, toTeam(appleId, session.provider));
|
|
715
|
+
const provider = chosen === void 0 ? session.provider : await session.selectProvider(chosen);
|
|
716
|
+
let data;
|
|
362
717
|
try {
|
|
363
|
-
|
|
364
|
-
"
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
], { ASC_WEB_PASSWORD: password });
|
|
718
|
+
data = await irisRequest(session, "Creating the App Store Connect API key", "POST", "/apiKeys", { data: {
|
|
719
|
+
type: "apiKeys",
|
|
720
|
+
attributes: {
|
|
721
|
+
nickname: name,
|
|
722
|
+
roles: [role],
|
|
723
|
+
allAppsVisible: true,
|
|
724
|
+
keyType: "PUBLIC_API"
|
|
725
|
+
}
|
|
726
|
+
} });
|
|
373
727
|
} catch (error) {
|
|
374
|
-
|
|
728
|
+
const hint = error instanceof AppleSignInError && error.status !== void 0 ? createFailureHint(error.status, error.body ?? "") : void 0;
|
|
729
|
+
if (hint) throw new AppleSignInError(`${error.message}\n${hint}`, {
|
|
730
|
+
...error.status === void 0 ? {} : { status: error.status },
|
|
731
|
+
cause: error
|
|
732
|
+
});
|
|
375
733
|
throw error;
|
|
376
|
-
} finally {
|
|
377
|
-
await twoFactor.close();
|
|
378
734
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
teamId: chosen
|
|
735
|
+
const keyId = text(data.id);
|
|
736
|
+
if (!keyId) throw new AppleSignInError("App Store Connect created a key without an ID.");
|
|
737
|
+
const revoke = async () => {
|
|
738
|
+
if (disposed) return false;
|
|
739
|
+
return irisRequest(session, "Revoking the key", "PATCH", `/apiKeys/${encodeURIComponent(keyId)}`, { data: {
|
|
740
|
+
id: keyId,
|
|
741
|
+
type: "apiKeys",
|
|
742
|
+
attributes: { isActive: false }
|
|
743
|
+
} }).then(() => true, () => false);
|
|
389
744
|
};
|
|
390
|
-
const sessionArgs = [...signedIn.appleId ? ["--apple-id", signedIn.appleId] : [], ...chosen === void 0 ? [] : ["--public-provider-id", chosen]];
|
|
391
|
-
let created;
|
|
392
|
-
try {
|
|
393
|
-
created = await run("Creating the App Store Connect API key", [
|
|
394
|
-
"web",
|
|
395
|
-
"api-keys",
|
|
396
|
-
"create",
|
|
397
|
-
"--name",
|
|
398
|
-
name,
|
|
399
|
-
"--role",
|
|
400
|
-
role,
|
|
401
|
-
"--output-dir",
|
|
402
|
-
node_path.default.join(work.root, "keys"),
|
|
403
|
-
...sessionArgs,
|
|
404
|
-
"--output",
|
|
405
|
-
"json"
|
|
406
|
-
]);
|
|
407
|
-
} catch (error) {
|
|
408
|
-
const stderr = error instanceof AscCommandError && isRecord(error.cause) ? String(error.cause.stderr ?? "") : "";
|
|
409
|
-
const hint = createFailureHint(stderr);
|
|
410
|
-
if (hint && error instanceof AscCommandError) throw new AscCommandError(`${error.message}\n${hint}`, error.exitCode, { cause: error });
|
|
411
|
-
throw error;
|
|
412
|
-
}
|
|
413
|
-
const keyId = text(created.keyId);
|
|
414
|
-
if (!keyId) throw new AscCommandError("asc web api-keys create printed no key ID.", 0);
|
|
415
|
-
const revoke = () => run("Revoking the key", [
|
|
416
|
-
"web",
|
|
417
|
-
"api-keys",
|
|
418
|
-
"revoke",
|
|
419
|
-
"--key-id",
|
|
420
|
-
keyId,
|
|
421
|
-
"--type",
|
|
422
|
-
"team",
|
|
423
|
-
"--confirm",
|
|
424
|
-
...sessionArgs,
|
|
425
|
-
"--output",
|
|
426
|
-
"json"
|
|
427
|
-
]).then((json) => json.active === false || json.status === "revoked", () => false);
|
|
428
745
|
let p8;
|
|
429
746
|
let issuerId;
|
|
430
747
|
try {
|
|
431
|
-
|
|
432
|
-
const p8Path = text(created.p8Path);
|
|
433
|
-
const keys = node_path.default.join(work.root, "keys");
|
|
434
|
-
if (!issuer) throw new Error("asc printed no issuer ID for the key.");
|
|
435
|
-
if (!p8Path || await node_fs_promises.default.realpath(node_path.default.dirname(node_path.default.resolve(p8Path))) !== await node_fs_promises.default.realpath(keys)) throw new Error("asc saved the key's .p8 outside its output directory.");
|
|
436
|
-
p8 = (await node_fs_promises.default.readFile(p8Path, "utf-8")).trim();
|
|
748
|
+
p8 = await downloadP8(session, keyId, wait);
|
|
437
749
|
(0, _hot_updater_cli_tools.registerSecretValue)(p8);
|
|
438
750
|
(0, _hot_updater_cli_tools.registerSecretValue)(Buffer.from(p8).toString("base64"));
|
|
439
751
|
assertP8(p8);
|
|
752
|
+
const details = await irisRequest(session, "Reading the key's issuer ID", "GET", `/apiKeys/${encodeURIComponent(keyId)}?include=provider`);
|
|
753
|
+
const issuer = issuerOf(details) ?? issuerOf(data);
|
|
754
|
+
if (!issuer) throw new Error("App Store Connect named no issuer ID for the key.");
|
|
440
755
|
issuerId = issuer;
|
|
441
756
|
} catch (error) {
|
|
442
757
|
const revoked = await revoke();
|
|
@@ -446,13 +761,13 @@ const createAscCliKeyIssuer = ({ asc, env = process.env, prompts = cliPrompts }
|
|
|
446
761
|
p8,
|
|
447
762
|
keyId,
|
|
448
763
|
issuerId,
|
|
449
|
-
name: text(
|
|
450
|
-
team,
|
|
764
|
+
name: text(isRecord(data.attributes) ? data.attributes.nickname : "") ?? name,
|
|
765
|
+
team: toTeam(appleId, provider),
|
|
451
766
|
revoke,
|
|
452
|
-
dispose
|
|
767
|
+
dispose
|
|
453
768
|
};
|
|
454
769
|
} catch (error) {
|
|
455
|
-
await
|
|
770
|
+
await dispose();
|
|
456
771
|
throw error;
|
|
457
772
|
}
|
|
458
773
|
};
|
|
@@ -461,18 +776,121 @@ const createAscCliKeyIssuer = ({ asc, env = process.env, prompts = cliPrompts }
|
|
|
461
776
|
/** Where team API keys are listed and revoked in App Store Connect. */
|
|
462
777
|
const ASC_INTEGRATIONS_URL = "https://appstoreconnect.apple.com/access/integrations/api";
|
|
463
778
|
/**
|
|
464
|
-
* Hot Updater's sign-in, the one place that picks how it is done:
|
|
465
|
-
*
|
|
779
|
+
* Hot Updater's sign-in, the one place that picks how it is done: App Store
|
|
780
|
+
* Connect's own web sign-in, with no other tool. Another implementation of
|
|
466
781
|
* `IssueAscApiKey` replaces it here without touching its callers.
|
|
467
782
|
*/
|
|
468
|
-
const issueAscApiKey = (request) =>
|
|
783
|
+
const issueAscApiKey = (request) => createAscKeyIssuer()(request);
|
|
784
|
+
//#endregion
|
|
785
|
+
//#region src/utils/formatCommand.ts
|
|
786
|
+
/**
|
|
787
|
+
* A command to print, broken at spaces with ` \` once a line would pass
|
|
788
|
+
* `width`, each continuation indented two spaces, so a terminal never wraps
|
|
789
|
+
* it mid-word and it still pastes as one command.
|
|
790
|
+
*/
|
|
791
|
+
const formatCommand = (words, width = 74) => {
|
|
792
|
+
const lines = [];
|
|
793
|
+
let line = "";
|
|
794
|
+
for (const word of words) {
|
|
795
|
+
const next = line ? `${line} ${word}` : word;
|
|
796
|
+
if (line && next.length > width - 2) {
|
|
797
|
+
lines.push(`${line} \\`);
|
|
798
|
+
line = ` ${word}`;
|
|
799
|
+
} else line = next;
|
|
800
|
+
}
|
|
801
|
+
lines.push(line);
|
|
802
|
+
return lines.join("\n");
|
|
803
|
+
};
|
|
469
804
|
//#endregion
|
|
470
805
|
//#region src/utils/cocoapods.ts
|
|
806
|
+
/**
|
|
807
|
+
* A failed command's error: the command, then the last lines of its
|
|
808
|
+
* output, stdout and stderr as printed, without execa's full message.
|
|
809
|
+
*/
|
|
810
|
+
const commandFailure = (command, error) => {
|
|
811
|
+
const { all, stderr, stdout } = error ?? {};
|
|
812
|
+
const detail = String(all || [stdout, stderr].filter(Boolean).join("\n") || "").trimEnd().split("\n").filter((line) => line.trim()).slice(-12).join("\n") || (error instanceof Error ? error.message : String(error));
|
|
813
|
+
return new Error(`${command} failed.\n${detail}`, { cause: error });
|
|
814
|
+
};
|
|
815
|
+
/**
|
|
816
|
+
* A line of the progress meter curl prints on stderr for each pod it
|
|
817
|
+
* downloads, such as React Native's prebuilt core.
|
|
818
|
+
*/
|
|
819
|
+
const DOWNLOAD_PROGRESS = [
|
|
820
|
+
/^% Total +% Received/,
|
|
821
|
+
/^Dload +Upload/,
|
|
822
|
+
/^\d{1,3} +[\d.]+[kMGT]? +\d{1,3} +[\d.]+[kMGT]? /
|
|
823
|
+
];
|
|
824
|
+
/**
|
|
825
|
+
* `pod install`'s output: its live line shows the pod it installs, its log
|
|
826
|
+
* keeps the whole output, and a failure prints CocoaPods' `[!]` errors and a
|
|
827
|
+
* crash's `SomeError - …` line with its last lines, without the download
|
|
828
|
+
* progress CocoaPods' curl prints on stderr.
|
|
829
|
+
*/
|
|
830
|
+
const createPodInstallLogger = () => new _hot_updater_cli_tools.BuildLogger({
|
|
831
|
+
logPrefix: "pod-install",
|
|
832
|
+
createPhaseReader: () => (line) => {
|
|
833
|
+
const installing = /^Installing (\S+)/.exec(line);
|
|
834
|
+
if (installing) return `Installing ${installing[1]}`;
|
|
835
|
+
return /^(Analyzing dependencies|Downloading dependencies|Generating Pods project|Integrating client project)$/.test(line) ? line : void 0;
|
|
836
|
+
},
|
|
837
|
+
failureSummary: {
|
|
838
|
+
lines: [/^[A-Z]\w*(::\w+)*Error - /],
|
|
839
|
+
blocks: [{
|
|
840
|
+
start: /^\[!\] /,
|
|
841
|
+
end: /^$/,
|
|
842
|
+
includeEnd: false
|
|
843
|
+
}],
|
|
844
|
+
ignore: DOWNLOAD_PROGRESS,
|
|
845
|
+
tail: 4,
|
|
846
|
+
stopAfter: /^\[!\] Oh no, an error occurred\.$/,
|
|
847
|
+
maxWidth: 76
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
/**
|
|
851
|
+
* Runs `pod install` as a step whose failure prints CocoaPods' errors and
|
|
852
|
+
* the log's path, then throws a `BuildFailedError`.
|
|
853
|
+
*/
|
|
854
|
+
const runPodInstall = async ({ command, env, sourceDir }) => {
|
|
855
|
+
const logger = createPodInstallLogger();
|
|
856
|
+
const failureMessage = `${command.join(" ")} failed`;
|
|
857
|
+
await logger.start({
|
|
858
|
+
title: "Installing CocoaPods dependencies",
|
|
859
|
+
header: [`$ ${formatCommand([...Object.entries(env).map(([name, value]) => `${name}=${value}`), ...command])}`, ""]
|
|
860
|
+
});
|
|
861
|
+
try {
|
|
862
|
+
const podProcess = (0, execa.execa)(command[0], command.slice(1), {
|
|
863
|
+
cwd: sourceDir,
|
|
864
|
+
env,
|
|
865
|
+
all: true,
|
|
866
|
+
buffer: false
|
|
867
|
+
});
|
|
868
|
+
const logging = podProcess.all ? logger.processStream(podProcess.all) : Promise.resolve();
|
|
869
|
+
const [podResult, loggingResult] = await Promise.allSettled([podProcess, logging]);
|
|
870
|
+
if (podResult.status === "rejected") throw podResult.reason;
|
|
871
|
+
if (loggingResult.status === "rejected") throw loggingResult.reason;
|
|
872
|
+
logger.stop("CocoaPods dependencies installed");
|
|
873
|
+
} catch (error) {
|
|
874
|
+
logger.writeError(error);
|
|
875
|
+
const reason = error instanceof execa.ExecaError && error.exitCode !== void 0 ? `exited with code ${error.exitCode}` : error instanceof Error ? error.message : String(error);
|
|
876
|
+
logger.stop(`${failureMessage} · ${reason}`, false);
|
|
877
|
+
throw new _hot_updater_cli_tools.BuildFailedError(`${failureMessage}: ${reason}`, {
|
|
878
|
+
logPath: logger.logPath,
|
|
879
|
+
summary: logger.failureSummary,
|
|
880
|
+
cause: error
|
|
881
|
+
});
|
|
882
|
+
} finally {
|
|
883
|
+
await logger.close();
|
|
884
|
+
}
|
|
885
|
+
};
|
|
471
886
|
const installPodsIfNeeded = async (sourceDir) => {
|
|
472
887
|
const podfilePath = path.default.join(sourceDir, "Podfile");
|
|
473
888
|
if (!fs.default.existsSync(podfilePath)) {
|
|
474
889
|
_hot_updater_cli_tools.p.log.info("No Podfile found, skipping CocoaPods installation");
|
|
475
|
-
return
|
|
890
|
+
return {
|
|
891
|
+
codegen: false,
|
|
892
|
+
pods: false
|
|
893
|
+
};
|
|
476
894
|
}
|
|
477
895
|
const gemfilePaths = [path.default.join(sourceDir, "Gemfile"), path.default.join((0, _hot_updater_cli_tools.getCwd)(), "Gemfile")];
|
|
478
896
|
const shouldUseBundler = (await Promise.all(gemfilePaths.map(checkShouldUseBundler))).some(Boolean);
|
|
@@ -486,36 +904,34 @@ const installPodsIfNeeded = async (sourceDir) => {
|
|
|
486
904
|
reactNativePath: reactNative.packagePath,
|
|
487
905
|
sourceDir
|
|
488
906
|
});
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
await (0, execa.execa)("bundle", ["install"], { cwd: sourceDir });
|
|
495
|
-
bundleSpinner.stop("Ruby gems installed");
|
|
496
|
-
const podSpinner = _hot_updater_cli_tools.p.spinner();
|
|
497
|
-
podSpinner.start("Installing CocoaPods dependencies");
|
|
498
|
-
await (0, execa.execa)("bundle", [
|
|
499
|
-
"exec",
|
|
500
|
-
"pod",
|
|
501
|
-
"install"
|
|
502
|
-
], {
|
|
503
|
-
cwd: sourceDir,
|
|
504
|
-
env: plan.env
|
|
505
|
-
});
|
|
506
|
-
podSpinner.stop("CocoaPods dependencies installed");
|
|
507
|
-
} else {
|
|
508
|
-
const spinner = _hot_updater_cli_tools.p.spinner();
|
|
509
|
-
spinner.start("Installing CocoaPods dependencies");
|
|
510
|
-
await (0, execa.execa)("pod", ["install"], {
|
|
907
|
+
if (shouldUseBundler) {
|
|
908
|
+
const bundleSpinner = _hot_updater_cli_tools.p.spinner();
|
|
909
|
+
bundleSpinner.start("Installing Ruby gems");
|
|
910
|
+
try {
|
|
911
|
+
await (0, execa.execa)("bundle", ["install"], {
|
|
511
912
|
cwd: sourceDir,
|
|
512
|
-
|
|
913
|
+
all: true
|
|
513
914
|
});
|
|
514
|
-
|
|
915
|
+
} catch (error) {
|
|
916
|
+
bundleSpinner.error("bundle install failed");
|
|
917
|
+
throw commandFailure("bundle install", error);
|
|
515
918
|
}
|
|
516
|
-
|
|
517
|
-
throw new Error(`pod install failed: ${error}`);
|
|
919
|
+
bundleSpinner.stop("Ruby gems installed");
|
|
518
920
|
}
|
|
921
|
+
await runPodInstall({
|
|
922
|
+
command: shouldUseBundler ? [
|
|
923
|
+
"bundle",
|
|
924
|
+
"exec",
|
|
925
|
+
"pod",
|
|
926
|
+
"install"
|
|
927
|
+
] : ["pod", "install"],
|
|
928
|
+
env: plan.env,
|
|
929
|
+
sourceDir
|
|
930
|
+
});
|
|
931
|
+
return {
|
|
932
|
+
codegen: plan.runCodegen,
|
|
933
|
+
pods: true
|
|
934
|
+
};
|
|
519
935
|
};
|
|
520
936
|
const checkShouldUseBundler = async (gemfilePath) => {
|
|
521
937
|
try {
|
|
@@ -546,10 +962,13 @@ const runReactNativeCodegen = async ({ projectRoot, reactNativePath, sourceDir }
|
|
|
546
962
|
sourceDir,
|
|
547
963
|
"-t",
|
|
548
964
|
"ios"
|
|
549
|
-
], {
|
|
965
|
+
], {
|
|
966
|
+
cwd: projectRoot,
|
|
967
|
+
all: true
|
|
968
|
+
});
|
|
550
969
|
} catch (error) {
|
|
551
|
-
spinner.
|
|
552
|
-
throw
|
|
970
|
+
spinner.error("React Native codegen failed");
|
|
971
|
+
throw commandFailure("React Native codegen", error);
|
|
553
972
|
}
|
|
554
973
|
spinner.stop("React Native codegen done");
|
|
555
974
|
};
|
|
@@ -701,80 +1120,113 @@ const parseXcodeProjectInfo = async (sourceDir) => {
|
|
|
701
1120
|
const prettifyXcodebuildError = (error) => {
|
|
702
1121
|
if (error instanceof _hot_updater_cli_tools.BuildFailedError) {
|
|
703
1122
|
const errorLines = error.summary.join("\n");
|
|
704
|
-
const
|
|
705
|
-
return new _hot_updater_cli_tools.BuildFailedError(
|
|
1123
|
+
const hint = mentionsProgramLicenseAgreement(errorLines) ? PROGRAM_LICENSE_AGREEMENT_HINT : errorLines.includes("error: No profiles for") || errorLines.includes("error: Provisioning profile") ? "No valid provisioning profile is set in the project.\nCheck the Signing & Capabilities tab in Xcode." : null;
|
|
1124
|
+
return new _hot_updater_cli_tools.BuildFailedError(hint ?? error.message, {
|
|
706
1125
|
logPath: error.logPath,
|
|
707
1126
|
summary: error.summary,
|
|
1127
|
+
hint,
|
|
708
1128
|
cause: error.cause
|
|
709
1129
|
});
|
|
710
1130
|
}
|
|
711
1131
|
return /* @__PURE__ */ new Error(`Xcodebuild failed: ${error}`);
|
|
712
1132
|
};
|
|
713
1133
|
//#endregion
|
|
1134
|
+
//#region src/utils/xcodebuildPhase.ts
|
|
1135
|
+
/** The `(in target 'X' from project 'Y')` that ends an action's line. */
|
|
1136
|
+
const TARGET = /\(in target '([^']+)' from project '[^']+'\)\s*$/;
|
|
1137
|
+
/** A `PhaseScriptExecution` line's script phase, its spaces escaped. */
|
|
1138
|
+
const SCRIPT = /^PhaseScriptExecution ((?:\\.|[^\s\\])+)/;
|
|
1139
|
+
/** A batched `SwiftCompile`'s files: `Compiling\ A.swift,\ B.swift`. */
|
|
1140
|
+
const SWIFT_BATCH = /^SwiftCompile \S+ \S+ Compiling\\ ((?:\\.|[^\s\\])+)/;
|
|
1141
|
+
const unescape = (value) => value.replace(/\\(.)/g, "$1");
|
|
1142
|
+
/** Actions whose lines say what xcodebuild is doing, by their first word. */
|
|
1143
|
+
const ACTIONS = {
|
|
1144
|
+
CompileAssetCatalog: "Compiling assets",
|
|
1145
|
+
CompileAssetCatalogVariant: "Compiling assets",
|
|
1146
|
+
LinkAssetCatalog: "Compiling assets",
|
|
1147
|
+
CompileStoryboard: "Compiling storyboards",
|
|
1148
|
+
CompileXIB: "Compiling storyboards",
|
|
1149
|
+
LinkStoryboards: "Compiling storyboards",
|
|
1150
|
+
Ld: "Linking",
|
|
1151
|
+
Libtool: "Linking",
|
|
1152
|
+
CodeSign: "Signing",
|
|
1153
|
+
ProcessInfoPlistFile: "Processing",
|
|
1154
|
+
ProcessProductPackaging: "Processing",
|
|
1155
|
+
ProcessProductPackagingDER: "Processing",
|
|
1156
|
+
CopySwiftLibs: "Copying",
|
|
1157
|
+
CpResource: "Copying",
|
|
1158
|
+
GenerateDSYMFile: "Generating dSYM",
|
|
1159
|
+
Strip: "Stripping",
|
|
1160
|
+
Validate: "Validating"
|
|
1161
|
+
};
|
|
1162
|
+
const COMPILES = /* @__PURE__ */ new Set([
|
|
1163
|
+
"CompileC",
|
|
1164
|
+
"SwiftCompile",
|
|
1165
|
+
"CompileSwift"
|
|
1166
|
+
]);
|
|
1167
|
+
const PRECOMPILES = /* @__PURE__ */ new Set([
|
|
1168
|
+
"PrecompileModule",
|
|
1169
|
+
"ScanDependencies",
|
|
1170
|
+
"SwiftExplicitDependencyGeneratePcm",
|
|
1171
|
+
"SwiftExplicitDependencyCompileModuleFromInterface"
|
|
1172
|
+
]);
|
|
1173
|
+
const plural = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
1174
|
+
/**
|
|
1175
|
+
* Reads the phase of an `xcodebuild` build, archive, or export from its raw
|
|
1176
|
+
* output: `Compiling React-Core · 412 files`, `Linking CodexRelay`,
|
|
1177
|
+
* `Signing CodexRelay`, `Bundling JavaScript`, with the target each
|
|
1178
|
+
* action's line names.
|
|
1179
|
+
*/
|
|
1180
|
+
const createXcodebuildPhaseReader = () => {
|
|
1181
|
+
let compiled = 0;
|
|
1182
|
+
let precompiled = 0;
|
|
1183
|
+
return (line) => {
|
|
1184
|
+
const space = line.indexOf(" ");
|
|
1185
|
+
const action = space === -1 ? line : line.slice(0, space);
|
|
1186
|
+
const target = TARGET.exec(line)?.[1];
|
|
1187
|
+
const withTarget = (phase) => target ? `${phase} ${target}` : phase;
|
|
1188
|
+
if (COMPILES.has(action)) {
|
|
1189
|
+
const batch = SWIFT_BATCH.exec(line)?.[1];
|
|
1190
|
+
compiled += batch ? unescape(batch).split(",").length : 1;
|
|
1191
|
+
return `${withTarget("Compiling")} · ${plural(compiled, "file")}`;
|
|
1192
|
+
}
|
|
1193
|
+
if (PRECOMPILES.has(action)) {
|
|
1194
|
+
precompiled += 1;
|
|
1195
|
+
return `Precompiling modules · ${precompiled}`;
|
|
1196
|
+
}
|
|
1197
|
+
if (action === "PhaseScriptExecution") {
|
|
1198
|
+
const script = unescape(SCRIPT.exec(line)?.[1] ?? "");
|
|
1199
|
+
if (script === "Bundle React Native code and images") return "Bundling JavaScript";
|
|
1200
|
+
return script ? `Running ${script}` : withTarget("Running a script");
|
|
1201
|
+
}
|
|
1202
|
+
const phase = ACTIONS[action];
|
|
1203
|
+
if (phase) return withTarget(phase);
|
|
1204
|
+
if (line === "Prepare packages" || line.startsWith("Resolve Package")) return "Resolving packages";
|
|
1205
|
+
if (line === "ComputeTargetDependencyGraph") return "Planning the build";
|
|
1206
|
+
};
|
|
1207
|
+
};
|
|
1208
|
+
//#endregion
|
|
714
1209
|
//#region src/utils/createXcodebuildLogger.ts
|
|
715
|
-
const createXcodebuildLogger = ({ logPrefix }) => new _hot_updater_cli_tools.BuildLogger({
|
|
1210
|
+
const createXcodebuildLogger = ({ logPrefix, logFilePath }) => new _hot_updater_cli_tools.BuildLogger({
|
|
716
1211
|
logPrefix,
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
"** BUILD FAILED **",
|
|
720
|
-
"** ARCHIVE FAILED **",
|
|
721
|
-
"The following build commands failed:",
|
|
722
|
-
"note: Building targets in dependency order",
|
|
723
|
-
"note: Target dependency graph",
|
|
724
|
-
"Running script",
|
|
725
|
-
"[CP-User]",
|
|
726
|
-
"[CP] Check Pods Manifest.lock",
|
|
727
|
-
"Build Succeeded"
|
|
728
|
-
],
|
|
729
|
-
progressStages: [
|
|
730
|
-
["Building targets in dependency order", "Target dependency graph"],
|
|
731
|
-
["Write Auxiliary File", "[CP] Check Pods Manifest.lock"],
|
|
732
|
-
[
|
|
733
|
-
"Running script",
|
|
734
|
-
"[CP-User] [RN]Check rncore",
|
|
735
|
-
"[CP-User] [Hermes] Replace Hermes",
|
|
736
|
-
"[CP-User] [RN]Check FBReactNativeSpec"
|
|
737
|
-
],
|
|
738
|
-
[
|
|
739
|
-
"Processing",
|
|
740
|
-
"ProcessProductPackaging",
|
|
741
|
-
"ProcessProductPackagingDER"
|
|
742
|
-
],
|
|
743
|
-
["Compiling"],
|
|
744
|
-
["RegisterExecutionPolicyException"],
|
|
745
|
-
["Touching"],
|
|
746
|
-
["Building library", "Create Universal Binary"],
|
|
747
|
-
["Copying"],
|
|
748
|
-
[
|
|
749
|
-
"Build Succeeded",
|
|
750
|
-
"BUILD SUCCEEDED",
|
|
751
|
-
"ARCHIVE SUCCEEDED"
|
|
752
|
-
]
|
|
753
|
-
],
|
|
1212
|
+
logFilePath,
|
|
1213
|
+
createPhaseReader: createXcodebuildPhaseReader,
|
|
754
1214
|
failureSummary: {
|
|
755
1215
|
lines: [/(^|:\s)error: /, /^\*\* [A-Z ]+ FAILED \*\*$/],
|
|
756
1216
|
blocks: [{
|
|
757
1217
|
start: "The following build commands failed:",
|
|
758
1218
|
end: /^\(\d+ failures?\)$/,
|
|
759
1219
|
includeEnd: true
|
|
1220
|
+
}, {
|
|
1221
|
+
start: /^error: exportArchive /,
|
|
1222
|
+
end: /^$/,
|
|
1223
|
+
includeEnd: false
|
|
760
1224
|
}]
|
|
761
1225
|
}
|
|
762
1226
|
});
|
|
763
1227
|
//#endregion
|
|
764
1228
|
//#region src/utils/runXcodebuildWithLogging.ts
|
|
765
|
-
let cachedXcbeautifyAvailability = null;
|
|
766
1229
|
const TERMINATION_SIGNALS$1 = ["SIGINT", "SIGTERM"];
|
|
767
|
-
const isXcbeautifyAvailable = async () => {
|
|
768
|
-
if (cachedXcbeautifyAvailability !== null) return cachedXcbeautifyAvailability;
|
|
769
|
-
try {
|
|
770
|
-
await (0, execa.execa)("which", ["xcbeautify"]);
|
|
771
|
-
cachedXcbeautifyAvailability = true;
|
|
772
|
-
return true;
|
|
773
|
-
} catch {
|
|
774
|
-
cachedXcbeautifyAvailability = false;
|
|
775
|
-
return false;
|
|
776
|
-
}
|
|
777
|
-
};
|
|
778
1230
|
const createRawXcodebuildProcess = ({ args, env, sourceDir }) => {
|
|
779
1231
|
return (0, execa.execa)("xcodebuild", args, {
|
|
780
1232
|
cwd: sourceDir,
|
|
@@ -787,13 +1239,6 @@ const createRawXcodebuildProcess = ({ args, env, sourceDir }) => {
|
|
|
787
1239
|
}
|
|
788
1240
|
});
|
|
789
1241
|
};
|
|
790
|
-
const createXcbeautifyProcess = ({ cwd }) => {
|
|
791
|
-
return (0, execa.execa)("xcbeautify", [], {
|
|
792
|
-
cwd,
|
|
793
|
-
all: true,
|
|
794
|
-
buffer: false
|
|
795
|
-
});
|
|
796
|
-
};
|
|
797
1242
|
/** Why xcodebuild failed, without its command line. */
|
|
798
1243
|
const describeFailure = (error) => {
|
|
799
1244
|
if (error instanceof execa.ExecaError) {
|
|
@@ -802,10 +1247,24 @@ const describeFailure = (error) => {
|
|
|
802
1247
|
}
|
|
803
1248
|
return error instanceof Error ? error.message : String(error);
|
|
804
1249
|
};
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
1250
|
+
/**
|
|
1251
|
+
* Runs xcodebuild as a step titled `title`: its live line shows the build's
|
|
1252
|
+
* phase, the log file keeps the command and the whole output, and a failure
|
|
1253
|
+
* prints the output's errors and the log's path on stderr.
|
|
1254
|
+
*/
|
|
1255
|
+
const runXcodebuildWithLogging = async ({ args, env, sourceDir, title, successMessage, failureMessage, logPrefix, logFilePath, header = [] }) => {
|
|
1256
|
+
const logger = createXcodebuildLogger({
|
|
1257
|
+
logPrefix,
|
|
1258
|
+
logFilePath
|
|
1259
|
+
});
|
|
1260
|
+
await logger.start({
|
|
1261
|
+
title,
|
|
1262
|
+
header: [
|
|
1263
|
+
...header,
|
|
1264
|
+
`$ ${formatCommand(["xcodebuild", ...args])}`,
|
|
1265
|
+
""
|
|
1266
|
+
]
|
|
1267
|
+
});
|
|
809
1268
|
const activeProcesses = [];
|
|
810
1269
|
const terminateActiveProcesses = () => {
|
|
811
1270
|
for (const activeProcess of activeProcesses) activeProcess.kill();
|
|
@@ -819,31 +1278,17 @@ const runXcodebuildWithLogging = async ({ args, env, sourceDir, successMessage,
|
|
|
819
1278
|
});
|
|
820
1279
|
activeProcesses.push(xcodebuildProcess);
|
|
821
1280
|
const xcodebuildOutput = xcodebuildProcess.all ?? xcodebuildProcess.stdout;
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
xcbeautifyProcess = createXcbeautifyProcess({ cwd: sourceDir });
|
|
825
|
-
activeProcesses.push(xcbeautifyProcess);
|
|
826
|
-
}
|
|
827
|
-
const logging = xcodebuildOutput ? logger.processStream(xcodebuildOutput, { display: xcbeautifyProcess ? xcbeautifyProcess.all ?? xcbeautifyProcess.stdout ?? void 0 : void 0 }) : Promise.resolve();
|
|
828
|
-
const xcbeautifyInput = xcbeautifyProcess?.stdin;
|
|
829
|
-
if (xcodebuildOutput && xcbeautifyInput) {
|
|
830
|
-
xcbeautifyInput.on("error", () => {
|
|
831
|
-
xcodebuildOutput.unpipe(xcbeautifyInput);
|
|
832
|
-
});
|
|
833
|
-
xcodebuildOutput.pipe(xcbeautifyInput);
|
|
834
|
-
}
|
|
835
|
-
const [xcodebuildResult, loggingResult] = await Promise.allSettled([
|
|
836
|
-
xcodebuildProcess,
|
|
837
|
-
logging,
|
|
838
|
-
xcbeautifyProcess
|
|
839
|
-
]);
|
|
1281
|
+
const logging = xcodebuildOutput ? logger.processStream(xcodebuildOutput) : Promise.resolve();
|
|
1282
|
+
const [xcodebuildResult, loggingResult] = await Promise.allSettled([xcodebuildProcess, logging]);
|
|
840
1283
|
if (xcodebuildResult.status === "rejected") throw xcodebuildResult.reason;
|
|
841
1284
|
if (loggingResult.status === "rejected") throw loggingResult.reason;
|
|
842
1285
|
logger.stop(successMessage);
|
|
1286
|
+
return { logFilePath: logger.logFilePath };
|
|
843
1287
|
} catch (error) {
|
|
844
1288
|
logger.writeError(error);
|
|
845
|
-
|
|
846
|
-
|
|
1289
|
+
const reason = describeFailure(error);
|
|
1290
|
+
logger.stop(`${failureMessage} · ${reason.replace(/\.$/, "")}`, false);
|
|
1291
|
+
throw new _hot_updater_cli_tools.BuildFailedError(`${failureMessage}: ${reason}`, {
|
|
847
1292
|
logPath: logger.logPath,
|
|
848
1293
|
summary: logger.failureSummary,
|
|
849
1294
|
cause: error
|
|
@@ -935,7 +1380,7 @@ const buildSettingsOverScheme = ({ minBundleId, authenticationArgs }) => ({
|
|
|
935
1380
|
* written to a temporary directory, removed whether the archive succeeds or
|
|
936
1381
|
* not.
|
|
937
1382
|
*/
|
|
938
|
-
const archiveXcodeProject = async ({ sourceDir, archiveDirectory, platform, installPods, destination, extraParams, configuration, xcodeScheme, xcconfig, logPrefix, minBundleId, authenticationArgs }) => {
|
|
1383
|
+
const archiveXcodeProject = async ({ sourceDir, archiveDirectory, platform, installPods, destination, extraParams, configuration, xcodeScheme, xcconfig, logPrefix, header = [], minBundleId, authenticationArgs }) => {
|
|
939
1384
|
if (installPods) await installPodsIfNeeded(sourceDir);
|
|
940
1385
|
const xcodeProject = await parseXcodeProjectInfo(sourceDir);
|
|
941
1386
|
const archiveName = `${xcodeProject.name.replace(".xcworkspace", "").replace(".xcodeproj", "")}.xcarchive`;
|
|
@@ -965,22 +1410,26 @@ const archiveXcodeProject = async ({ sourceDir, archiveDirectory, platform, inst
|
|
|
965
1410
|
xcconfig: build.xcconfig,
|
|
966
1411
|
xcodeScheme
|
|
967
1412
|
});
|
|
968
|
-
_hot_updater_cli_tools.p.log.info(`Xcode Archive Settings:
|
|
969
|
-
Project ${xcodeProject.name}
|
|
970
|
-
Scheme ${xcodeScheme}
|
|
971
|
-
Platform ${platform}
|
|
972
|
-
Command xcodebuild ${archiveArgs.join(" ")}
|
|
973
|
-
`);
|
|
974
1413
|
try {
|
|
975
|
-
await runXcodebuildWithLogging({
|
|
1414
|
+
const { logFilePath } = await runXcodebuildWithLogging({
|
|
976
1415
|
args: archiveArgs,
|
|
977
1416
|
env: { HOT_UPDATER_MIN_BUNDLE_ID: minBundleId },
|
|
1417
|
+
title: `Archiving ${xcodeScheme}`,
|
|
1418
|
+
successMessage: `Archived ${xcodeScheme}`,
|
|
978
1419
|
failureMessage: "Archive failed",
|
|
979
1420
|
logPrefix,
|
|
980
|
-
|
|
981
|
-
|
|
1421
|
+
header: [
|
|
1422
|
+
...header,
|
|
1423
|
+
`Project ${xcodeProject.name}`,
|
|
1424
|
+
`Scheme ${xcodeScheme}`,
|
|
1425
|
+
`Platform ${platform}`
|
|
1426
|
+
],
|
|
1427
|
+
sourceDir
|
|
982
1428
|
});
|
|
983
|
-
return {
|
|
1429
|
+
return {
|
|
1430
|
+
archivePath,
|
|
1431
|
+
logFilePath
|
|
1432
|
+
};
|
|
984
1433
|
} catch (error) {
|
|
985
1434
|
throw prettifyXcodebuildError(error);
|
|
986
1435
|
}
|
|
@@ -1023,7 +1472,7 @@ const prepareArchiveArgs = ({ xcodeScheme, archivePath, platform, sourceDir, xco
|
|
|
1023
1472
|
* `.app` to `outputDirectory`, and removes DerivedData, which takes 10 GB or
|
|
1024
1473
|
* more, whether the build succeeds or not.
|
|
1025
1474
|
*/
|
|
1026
|
-
const buildXcodeProject = async ({ sourceDir, outputDirectory, platform, xcodeScheme, configuration, deviceType, logPrefix, destination = [], useGenericDestination = false, installPods, extraParams, minBundleId, authenticationArgs }) => {
|
|
1475
|
+
const buildXcodeProject = async ({ sourceDir, outputDirectory, platform, xcodeScheme, configuration, deviceType, logPrefix, destination = [], useGenericDestination = false, installPods, extraParams, minBundleId, authenticationArgs, header = [] }) => {
|
|
1027
1476
|
if (installPods) await installPodsIfNeeded(sourceDir);
|
|
1028
1477
|
const xcodeProject = await parseXcodeProjectInfo(sourceDir);
|
|
1029
1478
|
const tmpDir = await createRandomTmpDir();
|
|
@@ -1061,24 +1510,26 @@ const buildXcodeProject = async ({ sourceDir, outputDirectory, platform, xcodeSc
|
|
|
1061
1510
|
sourceDir,
|
|
1062
1511
|
xcodeProject
|
|
1063
1512
|
});
|
|
1064
|
-
_hot_updater_cli_tools.p.log.info(`Xcode Build Settings:
|
|
1065
|
-
Project ${xcodeProject.name}
|
|
1066
|
-
Scheme ${xcodeScheme}
|
|
1067
|
-
Configuration ${configuration}
|
|
1068
|
-
Platform ${platform}
|
|
1069
|
-
Device Type ${deviceType}
|
|
1070
|
-
Command xcodebuild ${buildArgs.join(" ")}
|
|
1071
|
-
`);
|
|
1072
1513
|
let builtAppPath;
|
|
1514
|
+
let logFilePath;
|
|
1073
1515
|
try {
|
|
1074
|
-
await runXcodebuildWithLogging({
|
|
1516
|
+
({logFilePath} = await runXcodebuildWithLogging({
|
|
1075
1517
|
args: buildArgs,
|
|
1076
1518
|
env: { HOT_UPDATER_MIN_BUNDLE_ID: minBundleId },
|
|
1077
1519
|
sourceDir,
|
|
1078
1520
|
logPrefix,
|
|
1079
|
-
|
|
1521
|
+
header: [
|
|
1522
|
+
...header,
|
|
1523
|
+
`Project ${xcodeProject.name}`,
|
|
1524
|
+
`Scheme ${xcodeScheme}`,
|
|
1525
|
+
`Configuration ${configuration}`,
|
|
1526
|
+
`Platform ${platform}`,
|
|
1527
|
+
`Device type ${deviceType}`
|
|
1528
|
+
],
|
|
1529
|
+
title: `Building ${xcodeScheme}`,
|
|
1530
|
+
successMessage: `Built ${xcodeScheme}`,
|
|
1080
1531
|
failureMessage: "Build failed"
|
|
1081
|
-
});
|
|
1532
|
+
}));
|
|
1082
1533
|
({appPath: builtAppPath} = await getBuildSettings({
|
|
1083
1534
|
configuration,
|
|
1084
1535
|
derivedDataPath,
|
|
@@ -1092,7 +1543,10 @@ Command xcodebuild ${buildArgs.join(" ")}
|
|
|
1092
1543
|
}
|
|
1093
1544
|
const appPath = path.default.join(outputDirectory, path.default.basename(builtAppPath));
|
|
1094
1545
|
await fs.default.promises.cp(builtAppPath, appPath, { recursive: true });
|
|
1095
|
-
return {
|
|
1546
|
+
return {
|
|
1547
|
+
appPath,
|
|
1548
|
+
logFilePath
|
|
1549
|
+
};
|
|
1096
1550
|
} finally {
|
|
1097
1551
|
await removeTmpDir(tmpDir);
|
|
1098
1552
|
}
|
|
@@ -1152,7 +1606,7 @@ const getBuildSettings = async ({ xcodeProject, sourceDir, xcodeScheme, configur
|
|
|
1152
1606
|
//#endregion
|
|
1153
1607
|
//#region src/builder/exportXcodeArchive.ts
|
|
1154
1608
|
/** Exports the archive into `exportPath`. */
|
|
1155
|
-
const exportXcodeArchive = async ({ archivePath, exportPath, sourceDir, exportExtraParams, exportOptionsPlist, logPrefix, authenticationArgs }) => {
|
|
1609
|
+
const exportXcodeArchive = async ({ archivePath, exportPath, sourceDir, exportExtraParams, exportOptionsPlist, logPrefix, logFilePath, title, authenticationArgs }) => {
|
|
1156
1610
|
const exportArgs = prepareExportArgs({
|
|
1157
1611
|
authenticationArgs,
|
|
1158
1612
|
archivePath,
|
|
@@ -1164,9 +1618,11 @@ const exportXcodeArchive = async ({ archivePath, exportPath, sourceDir, exportEx
|
|
|
1164
1618
|
await runXcodebuildWithLogging({
|
|
1165
1619
|
args: exportArgs,
|
|
1166
1620
|
sourceDir,
|
|
1167
|
-
|
|
1621
|
+
title: `Exporting ${title}`,
|
|
1622
|
+
successMessage: `Exported ${title}`,
|
|
1168
1623
|
failureMessage: "Export failed",
|
|
1169
|
-
logPrefix
|
|
1624
|
+
logPrefix,
|
|
1625
|
+
logFilePath
|
|
1170
1626
|
});
|
|
1171
1627
|
return { exportPath };
|
|
1172
1628
|
} catch (error) {
|
|
@@ -1349,7 +1805,7 @@ var TestFlightBuildExistsError = class extends Error {
|
|
|
1349
1805
|
bundleVersion;
|
|
1350
1806
|
name = "TestFlightBuildExistsError";
|
|
1351
1807
|
constructor(shortVersion, bundleVersion, options) {
|
|
1352
|
-
super(`App Store Connect already has build ${bundleVersion} of version ${shortVersion}
|
|
1808
|
+
super(`App Store Connect already has build ${bundleVersion} of version ${shortVersion}.\nUse a new build number (CFBundleVersion).`, options);
|
|
1353
1809
|
this.shortVersion = shortVersion;
|
|
1354
1810
|
this.bundleVersion = bundleVersion;
|
|
1355
1811
|
}
|
|
@@ -1536,15 +1992,15 @@ const codeSigningOff = async (iosScheme) => {
|
|
|
1536
1992
|
* it (`assertTestFlightExportOptions`).
|
|
1537
1993
|
*/
|
|
1538
1994
|
const resolveTestFlightUploadKey = async ({ scheme, iosScheme }) => {
|
|
1539
|
-
if (iosScheme.simulator) throw new Error(`
|
|
1995
|
+
if (iosScheme.simulator) throw new Error(`The scheme ${scheme} builds a simulator .app, not an .ipa for TestFlight.`);
|
|
1540
1996
|
const configuration = iosScheme.configuration ?? "Release";
|
|
1541
|
-
if (/debug$/i.test(configuration)) throw new Error(`
|
|
1997
|
+
if (/debug$/i.test(configuration)) throw new Error(`The scheme ${scheme} builds ${configuration}; TestFlight takes a release build.`);
|
|
1542
1998
|
const unsigned = await codeSigningOff(iosScheme);
|
|
1543
|
-
if (unsigned) throw new Error(`
|
|
1544
|
-
if (!iosScheme.exportOptionsPlist && !iosScheme.signing) throw new Error(`The scheme ${scheme} archives without exporting an .ipa
|
|
1999
|
+
if (unsigned) throw new Error(`The scheme ${scheme} turns code signing off:\n${unsigned}`);
|
|
2000
|
+
if (!iosScheme.exportOptionsPlist && !iosScheme.signing) throw new Error(`The scheme ${scheme} archives without exporting an .ipa.\nSet signing.ascApiKey, or exportOptionsPlist with method app-store-connect.`);
|
|
1545
2001
|
if (iosScheme.exportOptionsPlist && fs.default.existsSync(schemeFile(iosScheme.exportOptionsPlist))) {
|
|
1546
2002
|
const method = await readExportMethod(schemeFile(iosScheme.exportOptionsPlist));
|
|
1547
|
-
if (!APP_STORE_EXPORT_METHODS.has(method)) throw new Error(`
|
|
2003
|
+
if (!APP_STORE_EXPORT_METHODS.has(method)) throw new Error(`The scheme ${scheme}'s exportOptionsPlist uses method ${method || "(none)"}.\nSet its method to app-store-connect for TestFlight.`);
|
|
1548
2004
|
}
|
|
1549
2005
|
return {
|
|
1550
2006
|
kind: "asc-api-key",
|
|
@@ -1559,7 +2015,7 @@ const resolveTestFlightUploadKey = async ({ scheme, iosScheme }) => {
|
|
|
1559
2015
|
const assertTestFlightExportOptions = async (schemeConfig) => {
|
|
1560
2016
|
if (!schemeConfig.exportOptionsPlist) return;
|
|
1561
2017
|
const method = await readExportMethod(schemeConfig.exportOptionsPlist);
|
|
1562
|
-
if (!APP_STORE_EXPORT_METHODS.has(method)) throw new Error(`
|
|
2018
|
+
if (!APP_STORE_EXPORT_METHODS.has(method)) throw new Error(`The scheme ${schemeConfig.hotUpdaterSchemeName}'s exportOptionsPlist uses method ${method || "(none)"}.\nSet its method to app-store-connect for TestFlight.`);
|
|
1563
2019
|
};
|
|
1564
2020
|
/**
|
|
1565
2021
|
* Requests to the App Store Connect API with `apiKey`: each carries a token
|
|
@@ -1594,8 +2050,8 @@ const createAppStoreConnectRequest = ({ apiKey, privateKey, fetch }) => {
|
|
|
1594
2050
|
});
|
|
1595
2051
|
if (!response.ok) {
|
|
1596
2052
|
const error = new AppStoreConnectError(action, response.status, await readApiErrors(response), response.headers.get("retry-after"));
|
|
1597
|
-
if (response.status === 401) throw new Error("App Store Connect refused the API key
|
|
1598
|
-
if (response.status === 403) throw new Error(`${error.message}
|
|
2053
|
+
if (response.status === 401) throw new Error("App Store Connect refused the API key.\nCheck its key ID and issuer ID, and that it isn't revoked.", { cause: error });
|
|
2054
|
+
if (response.status === 403) throw new Error(isProgramLicenseAgreementError(error.errors) ? `${action} failed (HTTP 403).\n${PROGRAM_LICENSE_AGREEMENT_HINT}` : `${error.message}\nUploads need a key with the Admin, App Manager, or Developer role.`, { cause: error });
|
|
1599
2055
|
throw error;
|
|
1600
2056
|
}
|
|
1601
2057
|
return response.status === 204 ? void 0 : await response.json();
|
|
@@ -1608,7 +2064,7 @@ const findApp = async (request, bundleId) => {
|
|
|
1608
2064
|
"fields[apps]": "bundleId"
|
|
1609
2065
|
})}`));
|
|
1610
2066
|
const app = apps.find((app) => app.attributes?.bundleId === bundleId);
|
|
1611
|
-
if (!app) throw new Error(`App Store Connect has no app
|
|
2067
|
+
if (!app) throw new Error(`App Store Connect has no app ${bundleId} that this API key can see.\nCreate the app in App Store Connect, or use a key of its team.`);
|
|
1612
2068
|
return {
|
|
1613
2069
|
app,
|
|
1614
2070
|
testFlightUrl: `https://appstoreconnect.apple.com/apps/${app.id}/testflight/ios`
|
|
@@ -1680,14 +2136,18 @@ const uploadToTestFlight = async ({ ipaPath, apiKey, fetch = globalThis.fetch, p
|
|
|
1680
2136
|
} }));
|
|
1681
2137
|
} catch (error) {
|
|
1682
2138
|
if (error instanceof AppStoreConnectError && error.status === 409 && error.errors.some((apiError) => apiError.code === DUPLICATE_ERROR_CODE)) {
|
|
1683
|
-
if (unanswered) throw new Error(
|
|
2139
|
+
if (unanswered) throw new Error([
|
|
2140
|
+
`App Store Connect already has build ${info.bundleVersion} of version ${info.shortVersion}.`,
|
|
2141
|
+
"An unanswered request of this upload may have reserved it.",
|
|
2142
|
+
"Upload with a new build number (CFBundleVersion)."
|
|
2143
|
+
].join("\n"), { cause: error });
|
|
1684
2144
|
throw new TestFlightBuildExistsError(info.shortVersion, info.bundleVersion, { cause: error });
|
|
1685
2145
|
}
|
|
1686
2146
|
if (!isRetryable(error) || attempt === COMMIT_ATTEMPTS) throw error;
|
|
1687
2147
|
if (!isRateLimited(error)) unanswered = true;
|
|
1688
2148
|
await sleep(retryDelayMs(error, attempt));
|
|
1689
2149
|
}
|
|
1690
|
-
_hot_updater_cli_tools.p.log.info(`Uploading ${ipaName}
|
|
2150
|
+
_hot_updater_cli_tools.p.log.info(`Uploading ${ipaName} ${info.shortVersion} (${info.bundleVersion}) to App Store Connect`);
|
|
1691
2151
|
const removeUpload = async () => {
|
|
1692
2152
|
for (let attempt = 1;; attempt++) try {
|
|
1693
2153
|
await request("Removing the build upload", "DELETE", `/v1/buildUploads/${buildUploadId}`);
|
|
@@ -1695,7 +2155,11 @@ const uploadToTestFlight = async ({ ipaPath, apiKey, fetch = globalThis.fetch, p
|
|
|
1695
2155
|
} catch (error) {
|
|
1696
2156
|
if (error instanceof AppStoreConnectError && error.status === 404) return;
|
|
1697
2157
|
if (!isRetryable(error) || attempt === COMMIT_ATTEMPTS) {
|
|
1698
|
-
_hot_updater_cli_tools.p.log.warn(
|
|
2158
|
+
_hot_updater_cli_tools.p.log.warn([
|
|
2159
|
+
`App Store Connect kept the unfinished upload of build ${info.bundleVersion}.`,
|
|
2160
|
+
error.message,
|
|
2161
|
+
"Upload with a new build number (CFBundleVersion)."
|
|
2162
|
+
].join("\n"));
|
|
1699
2163
|
return;
|
|
1700
2164
|
}
|
|
1701
2165
|
await sleep(retryDelayMs(error, attempt));
|
|
@@ -1737,7 +2201,12 @@ const uploadToTestFlight = async ({ ipaPath, apiKey, fetch = globalThis.fetch, p
|
|
|
1737
2201
|
} });
|
|
1738
2202
|
break;
|
|
1739
2203
|
} catch (error) {
|
|
1740
|
-
const unconfirmed = () => new Error(
|
|
2204
|
+
const unconfirmed = () => new Error([
|
|
2205
|
+
`App Store Connect didn't confirm the upload of build ${info.bundleVersion}.`,
|
|
2206
|
+
error.message,
|
|
2207
|
+
"Check TestFlight before you upload it again:",
|
|
2208
|
+
testFlightUrl
|
|
2209
|
+
].join("\n"), { cause: error });
|
|
1741
2210
|
if (!isRetryable(error)) {
|
|
1742
2211
|
if (commitUnanswered) throw unconfirmed();
|
|
1743
2212
|
await removeUpload();
|
|
@@ -1763,8 +2232,13 @@ const uploadToTestFlight = async ({ ipaPath, apiKey, fetch = globalThis.fetch, p
|
|
|
1763
2232
|
if (state.state === "PROCESSING") reportedProcessing = true;
|
|
1764
2233
|
if (!state.state || IN_PROGRESS_STATES.has(state.state)) {
|
|
1765
2234
|
if (Date.now() >= deadline) {
|
|
1766
|
-
if (!reportedProcessing) throw new Error(
|
|
1767
|
-
|
|
2235
|
+
if (!reportedProcessing) throw new Error([
|
|
2236
|
+
`App Store Connect didn't report build ${info.bundleVersion} processing.`,
|
|
2237
|
+
lastError instanceof Error ? lastError.message : state.state ? `State: ${state.state}` : void 0,
|
|
2238
|
+
"Check the build in TestFlight:",
|
|
2239
|
+
testFlightUrl
|
|
2240
|
+
].filter(Boolean).join("\n"), { cause: lastError });
|
|
2241
|
+
_hot_updater_cli_tools.p.log.warn(`App Store Connect is still processing build ${info.bundleVersion}.\nTestFlight lists it once processing finishes:\n${testFlightUrl}`);
|
|
1768
2242
|
return {
|
|
1769
2243
|
appId: app.id,
|
|
1770
2244
|
buildUploadId,
|
|
@@ -1895,7 +2369,7 @@ const acquireWithLockf = async (lockPath) => {
|
|
|
1895
2369
|
await fs.default.promises.mkdir(path.default.dirname(lockPath), { recursive: true });
|
|
1896
2370
|
const release = await holdWithLockf(lockPath, false);
|
|
1897
2371
|
if (release) return release;
|
|
1898
|
-
_hot_updater_cli_tools.p.log.info("Waiting for another build
|
|
2372
|
+
_hot_updater_cli_tools.p.log.info("Waiting for another build's signing keychain under this macOS user.");
|
|
1899
2373
|
return await holdWithLockf(lockPath, true);
|
|
1900
2374
|
};
|
|
1901
2375
|
/**
|
|
@@ -1966,7 +2440,12 @@ const createSigningKeychain = async ({ security = runSecurity, directory = os.de
|
|
|
1966
2440
|
live = void 0;
|
|
1967
2441
|
process.off("exit", restoreLive);
|
|
1968
2442
|
await releaseLock().catch(() => void 0);
|
|
1969
|
-
if (failures.length > 0) _hot_updater_cli_tools.p.log.warn(
|
|
2443
|
+
if (failures.length > 0) _hot_updater_cli_tools.p.log.warn([
|
|
2444
|
+
"Couldn't restore the keychains after the build.",
|
|
2445
|
+
...failures,
|
|
2446
|
+
"Check security list-keychains -d user and default-keychain -d user,",
|
|
2447
|
+
`and delete ${keychainPath} if it is left.`
|
|
2448
|
+
].join("\n"));
|
|
1970
2449
|
})();
|
|
1971
2450
|
return disposal;
|
|
1972
2451
|
};
|
|
@@ -2011,38 +2490,62 @@ const createSigningKeychain = async ({ security = runSecurity, directory = os.de
|
|
|
2011
2490
|
return {
|
|
2012
2491
|
path: keychainPath,
|
|
2013
2492
|
certificates: async () => {
|
|
2493
|
+
const identities = new Set([...(await security([
|
|
2494
|
+
"find-identity",
|
|
2495
|
+
"-p",
|
|
2496
|
+
"codesigning",
|
|
2497
|
+
keychainPath
|
|
2498
|
+
])).matchAll(/^\s*\d+\)\s+([0-9A-F]{40})\s/gm)].map((match) => match[1]));
|
|
2499
|
+
if (identities.size === 0) return [];
|
|
2014
2500
|
return ((await security([
|
|
2015
2501
|
"find-certificate",
|
|
2016
2502
|
"-a",
|
|
2017
2503
|
"-p",
|
|
2018
2504
|
keychainPath
|
|
2019
|
-
])).match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g) ?? []).
|
|
2505
|
+
])).match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g) ?? []).flatMap((pem) => {
|
|
2020
2506
|
const certificate = new crypto.default.X509Certificate(pem);
|
|
2021
|
-
return
|
|
2507
|
+
if (!identities.has(certificate.fingerprint.replaceAll(":", ""))) return [];
|
|
2508
|
+
return [{
|
|
2022
2509
|
serialNumber: normalizeSerial(certificate.serialNumber),
|
|
2023
2510
|
commonName: certificate.subject.match(/^CN=(.*)$/m)?.[1] ?? certificate.subject
|
|
2024
|
-
};
|
|
2511
|
+
}];
|
|
2025
2512
|
});
|
|
2026
2513
|
},
|
|
2027
2514
|
dispose
|
|
2028
2515
|
};
|
|
2029
2516
|
};
|
|
2517
|
+
/** A request App Store Connect refused, in one line. */
|
|
2518
|
+
var AppStoreConnectRefusal = class extends Error {
|
|
2519
|
+
/** Whether the team has a Program License Agreement to accept. */
|
|
2520
|
+
agreement;
|
|
2521
|
+
constructor(message, agreement) {
|
|
2522
|
+
super(message);
|
|
2523
|
+
this.agreement = agreement;
|
|
2524
|
+
}
|
|
2525
|
+
};
|
|
2526
|
+
const refusal = async (action, response) => {
|
|
2527
|
+
const body = await response.json().catch(() => void 0);
|
|
2528
|
+
return new AppStoreConnectRefusal(`${action} failed (HTTP ${response.status}).`, isProgramLicenseAgreementError(Array.isArray(body?.errors) ? body.errors : []));
|
|
2529
|
+
};
|
|
2030
2530
|
/**
|
|
2031
2531
|
* Revokes the certificates Xcode created in the build's keychain, whose
|
|
2032
2532
|
* private keys the keychain's deletion destroys: on a runner that starts
|
|
2033
2533
|
* empty, Xcode creates a development certificate for every build, and App
|
|
2034
2534
|
* Store Connect refuses new ones once a team has its maximum. Each is found
|
|
2035
2535
|
* in the team's certificates by serial number. A revocation that fails is
|
|
2036
|
-
* reported with
|
|
2536
|
+
* reported with each certificate's name and serial and where to revoke it
|
|
2537
|
+
* by hand, and never fails the build.
|
|
2037
2538
|
*/
|
|
2038
2539
|
const revokeKeychainCertificates = async ({ certificates, apiKey, privateKey, fetch = globalThis.fetch }) => {
|
|
2039
2540
|
const revoked = [];
|
|
2040
2541
|
const failed = [];
|
|
2542
|
+
/** Why none could be revoked: the token or the listing failed. */
|
|
2543
|
+
let reason;
|
|
2544
|
+
let agreement = false;
|
|
2041
2545
|
if (certificates.length === 0) return {
|
|
2042
|
-
revoked,
|
|
2043
|
-
failed
|
|
2546
|
+
revoked: [],
|
|
2547
|
+
failed: []
|
|
2044
2548
|
};
|
|
2045
|
-
const describe = ({ commonName, serialNumber }) => `${commonName} (serial ${serialNumber})`;
|
|
2046
2549
|
try {
|
|
2047
2550
|
const token = createAppStoreConnectToken({
|
|
2048
2551
|
privateKey: privateKey ?? crypto.default.createPrivateKey(await fs.default.promises.readFile(apiKey.keyPath, "utf-8")),
|
|
@@ -2058,7 +2561,7 @@ const revokeKeychainCertificates = async ({ certificates, apiKey, privateKey, fe
|
|
|
2058
2561
|
})}`;
|
|
2059
2562
|
while (next) {
|
|
2060
2563
|
const response = await fetch(next, { headers });
|
|
2061
|
-
if (!response.ok) throw
|
|
2564
|
+
if (!response.ok) throw await refusal("Listing the team's certificates", response);
|
|
2062
2565
|
const body = await response.json();
|
|
2063
2566
|
for (const { id, attributes } of body.data ?? []) if (attributes?.serialNumber) idsBySerial.set(normalizeSerial(attributes.serialNumber), id);
|
|
2064
2567
|
next = body.links?.next;
|
|
@@ -2070,18 +2573,32 @@ const revokeKeychainCertificates = async ({ certificates, apiKey, privateKey, fe
|
|
|
2070
2573
|
method: "DELETE",
|
|
2071
2574
|
headers
|
|
2072
2575
|
});
|
|
2073
|
-
if (response.ok || response.status === 404) revoked.push(
|
|
2074
|
-
else
|
|
2576
|
+
if (response.ok || response.status === 404) revoked.push(certificate);
|
|
2577
|
+
else {
|
|
2578
|
+
agreement ||= (await refusal("Revoking", response)).agreement;
|
|
2579
|
+
failed.push({
|
|
2580
|
+
certificate,
|
|
2581
|
+
status: response.status
|
|
2582
|
+
});
|
|
2583
|
+
}
|
|
2075
2584
|
}
|
|
2076
2585
|
} catch (error) {
|
|
2077
|
-
|
|
2078
|
-
|
|
2586
|
+
reason = error instanceof Error ? error.message : String(error);
|
|
2587
|
+
agreement ||= error instanceof AppStoreConnectRefusal && error.agreement;
|
|
2588
|
+
for (const certificate of certificates) if (!revoked.includes(certificate)) failed.push({ certificate });
|
|
2079
2589
|
}
|
|
2080
|
-
|
|
2081
|
-
if (
|
|
2590
|
+
const describe = ({ commonName, serialNumber }) => `${commonName} (serial ${serialNumber})`;
|
|
2591
|
+
if (revoked.length > 0) _hot_updater_cli_tools.p.log.info(["Revoked the certificates Xcode created for this build:", ...revoked.flatMap(({ commonName, serialNumber }) => [commonName, `Serial ${serialNumber}`])].join("\n"));
|
|
2592
|
+
if (failed.length > 0) _hot_updater_cli_tools.p.log.warn([
|
|
2593
|
+
"Couldn't revoke the certificates Xcode created for this build.",
|
|
2594
|
+
...reason === void 0 ? [] : [reason],
|
|
2595
|
+
...agreement ? [PROGRAM_LICENSE_AGREEMENT_HINT] : [],
|
|
2596
|
+
...failed.flatMap(({ certificate, status }) => [certificate.commonName, `Serial ${certificate.serialNumber}${status === void 0 ? "" : ` (HTTP ${status})`}`]),
|
|
2597
|
+
CERTIFICATES_URL
|
|
2598
|
+
].join("\n"));
|
|
2082
2599
|
return {
|
|
2083
|
-
revoked,
|
|
2084
|
-
failed
|
|
2600
|
+
revoked: revoked.map(describe),
|
|
2601
|
+
failed: failed.map(({ certificate }) => describe(certificate))
|
|
2085
2602
|
};
|
|
2086
2603
|
};
|
|
2087
2604
|
const TERMINATION_SIGNALS = [
|
|
@@ -2106,7 +2623,7 @@ const withSigningKeychain = async ({ enabled, apiKey, run, security, lockPath, a
|
|
|
2106
2623
|
lockPath,
|
|
2107
2624
|
acquireLock
|
|
2108
2625
|
});
|
|
2109
|
-
_hot_updater_cli_tools.p.log.info("Signing in a keychain of this build's own
|
|
2626
|
+
_hot_updater_cli_tools.p.log.info("Signing in a keychain of this build's own (CI is set).");
|
|
2110
2627
|
let cleanup;
|
|
2111
2628
|
const release = () => {
|
|
2112
2629
|
cleanup ??= (async () => {
|
|
@@ -2187,7 +2704,7 @@ const readBuiltApp = async (appPath) => {
|
|
|
2187
2704
|
* once the caller has copied it out. A failed build removes it, and every
|
|
2188
2705
|
* other directory the build used, itself.
|
|
2189
2706
|
*/
|
|
2190
|
-
const buildIos = async ({ schemeConfig, minBundleId = (0, _hot_updater_cli_tools.createMinBundleId)(), credentials }) => {
|
|
2707
|
+
const buildIos = async ({ schemeConfig, minBundleId = (0, _hot_updater_cli_tools.createMinBundleId)(), credentials, logHeader = [] }) => {
|
|
2191
2708
|
await assertXcodebuildExist();
|
|
2192
2709
|
if (schemeConfig.simulator && schemeConfig.destination.length) {
|
|
2193
2710
|
_hot_updater_cli_tools.p.log.error("The scheme option 'simulator' is not compatible with the 'destination' option.");
|
|
@@ -2200,7 +2717,8 @@ const buildIos = async ({ schemeConfig, minBundleId = (0, _hot_updater_cli_tools
|
|
|
2200
2717
|
schemeConfig,
|
|
2201
2718
|
minBundleId,
|
|
2202
2719
|
credentials,
|
|
2203
|
-
buildDirectory
|
|
2720
|
+
buildDirectory,
|
|
2721
|
+
logHeader
|
|
2204
2722
|
}),
|
|
2205
2723
|
dispose: () => removeTmpDir(buildDirectory)
|
|
2206
2724
|
};
|
|
@@ -2209,15 +2727,16 @@ const buildIos = async ({ schemeConfig, minBundleId = (0, _hot_updater_cli_tools
|
|
|
2209
2727
|
throw error;
|
|
2210
2728
|
}
|
|
2211
2729
|
};
|
|
2212
|
-
const buildInto = async ({ schemeConfig, minBundleId, credentials, buildDirectory }) => {
|
|
2730
|
+
const buildInto = async ({ schemeConfig, minBundleId, credentials, buildDirectory, logHeader }) => {
|
|
2213
2731
|
const iosProjectRoot = path.default.join((0, _hot_updater_cli_tools.getCwd)(), "ios");
|
|
2214
2732
|
if (schemeConfig.simulator) {
|
|
2215
2733
|
const appDir = path.default.join(buildDirectory, "app");
|
|
2216
2734
|
await fs.default.promises.mkdir(appDir, { recursive: true });
|
|
2217
|
-
const { appPath } = await buildXcodeProject({
|
|
2735
|
+
const { appPath, logFilePath } = await buildXcodeProject({
|
|
2218
2736
|
sourceDir: iosProjectRoot,
|
|
2219
2737
|
outputDirectory: appDir,
|
|
2220
2738
|
logPrefix: `ios-${schemeConfig.hotUpdaterSchemeName}-build`,
|
|
2739
|
+
header: logHeader,
|
|
2221
2740
|
platform: schemeConfig.platform,
|
|
2222
2741
|
xcodeScheme: schemeConfig.scheme,
|
|
2223
2742
|
configuration: schemeConfig.configuration,
|
|
@@ -2227,10 +2746,10 @@ const buildInto = async ({ schemeConfig, minBundleId, credentials, buildDirector
|
|
|
2227
2746
|
useGenericDestination: true,
|
|
2228
2747
|
minBundleId
|
|
2229
2748
|
});
|
|
2230
|
-
_hot_updater_cli_tools.p.log.success(`.app file created: ${path.default.basename(appPath)}`);
|
|
2231
2749
|
return {
|
|
2232
2750
|
buildDirectory,
|
|
2233
2751
|
buildArtifactPath: appPath,
|
|
2752
|
+
logFilePath,
|
|
2234
2753
|
builtApp: await readBuiltApp(appPath)
|
|
2235
2754
|
};
|
|
2236
2755
|
} else {
|
|
@@ -2241,10 +2760,11 @@ const buildInto = async ({ schemeConfig, minBundleId, credentials, buildDirector
|
|
|
2241
2760
|
if (!signing) return archiveAndExport({
|
|
2242
2761
|
schemeConfig,
|
|
2243
2762
|
minBundleId,
|
|
2244
|
-
buildDirectory
|
|
2763
|
+
buildDirectory,
|
|
2764
|
+
logHeader
|
|
2245
2765
|
});
|
|
2246
2766
|
const appStoreConnect = await exportsForAppStoreConnect(schemeConfig);
|
|
2247
|
-
if ((0, _hot_updater_cli_tools.isCI)() && !appStoreConnect) _hot_updater_cli_tools.p.log.info("
|
|
2767
|
+
if ((0, _hot_updater_cli_tools.isCI)() && !appStoreConnect) _hot_updater_cli_tools.p.log.info("Signing in the runner's own keychains (not an App Store Connect export).");
|
|
2248
2768
|
return withSigningKeychain({
|
|
2249
2769
|
enabled: (0, _hot_updater_cli_tools.isCI)() && appStoreConnect,
|
|
2250
2770
|
apiKey: signing.apiKey,
|
|
@@ -2252,6 +2772,7 @@ const buildInto = async ({ schemeConfig, minBundleId, credentials, buildDirector
|
|
|
2252
2772
|
schemeConfig,
|
|
2253
2773
|
minBundleId,
|
|
2254
2774
|
buildDirectory,
|
|
2775
|
+
logHeader,
|
|
2255
2776
|
authenticationArgs: signing.authenticationArgs
|
|
2256
2777
|
})
|
|
2257
2778
|
});
|
|
@@ -2261,12 +2782,12 @@ const buildInto = async ({ schemeConfig, minBundleId, credentials, buildDirector
|
|
|
2261
2782
|
* Archives a device build and exports its `.ipa`, or returns the
|
|
2262
2783
|
* `.xcarchive` of a scheme it cannot export.
|
|
2263
2784
|
*/
|
|
2264
|
-
const archiveAndExport = async ({ schemeConfig, minBundleId, buildDirectory, authenticationArgs }) => {
|
|
2785
|
+
const archiveAndExport = async ({ schemeConfig, minBundleId, buildDirectory, logHeader, authenticationArgs }) => {
|
|
2265
2786
|
const iosProjectRoot = path.default.join((0, _hot_updater_cli_tools.getCwd)(), "ios");
|
|
2266
2787
|
const archiveDir = path.default.join(buildDirectory, "archive");
|
|
2267
2788
|
const exportDir = path.default.join(buildDirectory, "export");
|
|
2268
2789
|
await fs.default.promises.mkdir(archiveDir, { recursive: true });
|
|
2269
|
-
const { archivePath } = await archiveXcodeProject({
|
|
2790
|
+
const { archivePath, logFilePath } = await archiveXcodeProject({
|
|
2270
2791
|
archiveDirectory: archiveDir,
|
|
2271
2792
|
authenticationArgs,
|
|
2272
2793
|
minBundleId,
|
|
@@ -2278,17 +2799,16 @@ const archiveAndExport = async ({ schemeConfig, minBundleId, buildDirectory, aut
|
|
|
2278
2799
|
extraParams: schemeConfig.extraParams,
|
|
2279
2800
|
configuration: schemeConfig.configuration,
|
|
2280
2801
|
installPods: schemeConfig.installPods,
|
|
2281
|
-
logPrefix: `ios-${schemeConfig.hotUpdaterSchemeName}-build
|
|
2802
|
+
logPrefix: `ios-${schemeConfig.hotUpdaterSchemeName}-build`,
|
|
2803
|
+
header: logHeader
|
|
2282
2804
|
});
|
|
2283
2805
|
const appFromArchive = extractAppFromXcarchive(archivePath);
|
|
2284
2806
|
const archivedAppPath = appFromArchive ? path.default.join(archiveDir, path.default.basename(appFromArchive)) : void 0;
|
|
2285
|
-
if (appFromArchive && archivedAppPath) {
|
|
2286
|
-
await fs.default.promises.cp(appFromArchive, archivedAppPath, { recursive: true });
|
|
2287
|
-
_hot_updater_cli_tools.p.log.success(".app extracted from .xcarchive");
|
|
2288
|
-
}
|
|
2807
|
+
if (appFromArchive && archivedAppPath) await fs.default.promises.cp(appFromArchive, archivedAppPath, { recursive: true });
|
|
2289
2808
|
if (!schemeConfig.exportOptionsPlist && !authenticationArgs) return {
|
|
2290
2809
|
buildDirectory,
|
|
2291
2810
|
buildArtifactPath: archivePath,
|
|
2811
|
+
logFilePath,
|
|
2292
2812
|
builtApp: archivedAppPath ? await readBuiltApp(archivedAppPath) : void 0
|
|
2293
2813
|
};
|
|
2294
2814
|
await fs.default.promises.mkdir(exportDir, { recursive: true });
|
|
@@ -2299,7 +2819,9 @@ const archiveAndExport = async ({ schemeConfig, minBundleId, buildDirectory, aut
|
|
|
2299
2819
|
exportExtraParams: schemeConfig.exportExtraParams,
|
|
2300
2820
|
exportOptionsPlist,
|
|
2301
2821
|
sourceDir: iosProjectRoot,
|
|
2302
|
-
logPrefix: `ios-${schemeConfig.hotUpdaterSchemeName}-build
|
|
2822
|
+
logPrefix: `ios-${schemeConfig.hotUpdaterSchemeName}-build`,
|
|
2823
|
+
logFilePath: logFilePath ?? void 0,
|
|
2824
|
+
title: schemeConfig.scheme
|
|
2303
2825
|
});
|
|
2304
2826
|
if (schemeConfig.exportOptionsPlist) await exportArchive(schemeConfig.exportOptionsPlist);
|
|
2305
2827
|
else {
|
|
@@ -2315,6 +2837,7 @@ const archiveAndExport = async ({ schemeConfig, minBundleId, buildDirectory, aut
|
|
|
2315
2837
|
return {
|
|
2316
2838
|
buildDirectory,
|
|
2317
2839
|
buildArtifactPath: path.default.join(exportDir, ipaFile),
|
|
2840
|
+
logFilePath,
|
|
2318
2841
|
builtApp: archivedAppPath ? await readBuiltApp(archivedAppPath) : void 0
|
|
2319
2842
|
};
|
|
2320
2843
|
};
|