expo-release 2.3.1 → 2.4.0
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/README.md +117 -124
- package/action.yml +63 -12
- package/dist/args.d.ts +17 -15
- package/dist/cli.js +865 -143
- package/dist/decide.d.ts +26 -0
- package/dist/doctor.d.ts +30 -0
- package/dist/eas-json.d.ts +25 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +409 -122
- package/dist/operate.d.ts +8 -1
- package/dist/run.d.ts +42 -2
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +74 -14
- package/dist/runtime/ota.d.ts +23 -3
- package/dist/runtime/push.d.ts +15 -3
- package/dist/source-maps.d.ts +1 -0
- package/package.json +7 -3
- package/src/args.ts +35 -20
- package/src/cli.ts +233 -36
- package/src/decide.ts +236 -22
- package/src/doctor.ts +296 -0
- package/src/eas-json.ts +128 -0
- package/src/index.ts +2 -0
- package/src/operate.ts +97 -53
- package/src/run.ts +259 -37
- package/src/runtime/index.ts +2 -0
- package/src/runtime/ota.ts +92 -15
- package/src/runtime/push.ts +46 -13
- package/src/runtime/react-native.d.ts +10 -0
- package/src/source-maps.ts +4 -1
package/dist/cli.js
CHANGED
|
@@ -2,18 +2,24 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
|
-
import {
|
|
5
|
+
import { relative } from "path";
|
|
6
|
+
import {
|
|
7
|
+
CliUsageError,
|
|
8
|
+
defineCli,
|
|
9
|
+
defineCommand
|
|
10
|
+
} from "hanzio/cli";
|
|
6
11
|
|
|
7
12
|
// src/args.ts
|
|
8
13
|
import { z } from "zod";
|
|
9
14
|
var DEFAULT_IMPACT_PATHS = ["apps/expo/"];
|
|
10
15
|
var text = (env) => z.string().transform((value) => value.trim() || undefined).optional().meta({ env: `EXPO_RELEASE_${env}` });
|
|
11
|
-
var
|
|
16
|
+
var trimmed = (schema) => z.preprocess((value) => typeof value === "string" ? value.trim() : value, schema);
|
|
17
|
+
var trueFalse = (env, fallback) => trimmed(z.enum(["true", "false"])).default(fallback).transform((value) => value === "true").meta({ env: `EXPO_RELEASE_${env}` });
|
|
12
18
|
var releaseFlags = z.object({
|
|
13
19
|
profile: z.string().trim().min(1).meta({ env: "EXPO_RELEASE_PROFILE" }).describe("EAS build profile, also the default update channel"),
|
|
14
|
-
base: text("BASE").describe("Git base SHA for the impact diff (
|
|
15
|
-
head: text("HEAD").describe("Git head SHA for the impact diff
|
|
16
|
-
platform: z.enum(["ios", "android"]).default("ios").meta({ env: "EXPO_RELEASE_PLATFORM" }).describe("Native platform"),
|
|
20
|
+
base: text("BASE").describe("Git base SHA for the impact diff (default: the merge base on a pull request, else github.event.before)"),
|
|
21
|
+
head: text("HEAD").meta({ env: ["EXPO_RELEASE_HEAD", "GITHUB_SHA"] }).describe("Git head SHA for the impact diff"),
|
|
22
|
+
platform: trimmed(z.enum(["ios", "android"])).default("ios").meta({ env: "EXPO_RELEASE_PLATFORM" }).describe("Native platform"),
|
|
17
23
|
impactPaths: z.string().default(DEFAULT_IMPACT_PATHS.join(",")).transform((value, ctx) => {
|
|
18
24
|
const paths = value.split(",").map((path) => path.trim()).filter(Boolean);
|
|
19
25
|
if (paths.length === 0)
|
|
@@ -24,16 +30,16 @@ var releaseFlags = z.object({
|
|
|
24
30
|
return paths;
|
|
25
31
|
}).meta({ env: "EXPO_RELEASE_IMPACT_PATHS" }).describe("Comma-separated repo-root paths that count as an Expo change"),
|
|
26
32
|
ignoreExpoImpact: trueFalse("IGNORE_EXPO_IMPACT", "false").describe("Check the fingerprint even when no impact path changed"),
|
|
27
|
-
operation: z.enum([
|
|
33
|
+
operation: trimmed(z.enum([
|
|
28
34
|
"deploy",
|
|
29
35
|
"force-native",
|
|
30
36
|
"retry-ota",
|
|
31
37
|
"republish",
|
|
32
38
|
"rollback-embedded"
|
|
33
|
-
]).default("deploy").meta({ env: "EXPO_RELEASE_OPERATION" }).describe("What to run"),
|
|
34
|
-
deployType: z.enum(["none", "ota", "native"]).default("none").meta({ env: "EXPO_RELEASE_DEPLOY_TYPE" }).describe("Decision type for operate"),
|
|
35
|
-
channel: text("CHANNEL").describe("EAS Update channel (default: the profile)"),
|
|
36
|
-
environment: text("ENVIRONMENT").describe("EAS environment for eas update (required for Expo SDK 55
|
|
39
|
+
])).default("deploy").meta({ env: "EXPO_RELEASE_OPERATION" }).describe("What to run"),
|
|
40
|
+
deployType: trimmed(z.enum(["none", "ota", "native"])).default("none").meta({ env: "EXPO_RELEASE_DEPLOY_TYPE" }).describe("Decision type for operate"),
|
|
41
|
+
channel: text("CHANNEL").describe("EAS Update channel (default: the eas.json profile channel, else the profile name)"),
|
|
42
|
+
environment: text("ENVIRONMENT").describe("EAS environment for eas update (required for Expo SDK 55+, where it defaults to the eas.json profile environment)"),
|
|
37
43
|
autoSubmit: trueFalse("AUTO_SUBMIT", "true").describe("Pass --auto-submit on native builds"),
|
|
38
44
|
preUpdate: text("PRE_UPDATE").describe("Shell command run before eas update"),
|
|
39
45
|
postUpdate: text("POST_UPDATE").describe("Shell command run after a successful eas update"),
|
|
@@ -43,7 +49,7 @@ var releaseFlags = z.object({
|
|
|
43
49
|
runtimeVersion: text("RUNTIME_VERSION").describe("Runtime version for rollback-embedded (default: the fingerprint)"),
|
|
44
50
|
message: text("MESSAGE").describe("Override the eas update message"),
|
|
45
51
|
fingerprint: text("FINGERPRINT").describe("Fingerprint hash to use instead of generating one"),
|
|
46
|
-
dryRun: z.boolean().meta({ env: "EXPO_RELEASE_DRY_RUN" }).describe("Print mutating EAS commands without running them")
|
|
52
|
+
dryRun: z.preprocess((value) => typeof value === "string" && /^\s*(true|false)\s*$/.test(value) ? value.trim() === "true" : value, z.boolean()).meta({ env: "EXPO_RELEASE_DRY_RUN" }).describe("Print mutating EAS commands without running them")
|
|
47
53
|
}).superRefine((flags, ctx) => {
|
|
48
54
|
if (flags.operation === "republish" && !flags.group)
|
|
49
55
|
ctx.addIssue({
|
|
@@ -52,13 +58,13 @@ var releaseFlags = z.object({
|
|
|
52
58
|
message: "required by --operation republish"
|
|
53
59
|
});
|
|
54
60
|
});
|
|
55
|
-
function toReleaseOptions(command, { context: flags,
|
|
61
|
+
function toReleaseOptions(command, { context: flags }, buildProfile = null) {
|
|
62
|
+
const environment = flags.environment ?? buildProfile?.environment;
|
|
56
63
|
return {
|
|
57
64
|
...flags,
|
|
58
65
|
command,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
channel: flags.channel ?? flags.profile
|
|
66
|
+
channel: flags.channel ?? buildProfile?.channel ?? flags.profile,
|
|
67
|
+
...environment ? { environment } : {}
|
|
62
68
|
};
|
|
63
69
|
}
|
|
64
70
|
function needsDecide(operation) {
|
|
@@ -102,22 +108,119 @@ ${delimiter}`;
|
|
|
102
108
|
}
|
|
103
109
|
|
|
104
110
|
// src/run.ts
|
|
111
|
+
import { existsSync } from "fs";
|
|
112
|
+
import { dirname, join } from "path";
|
|
105
113
|
function easCommand(...args) {
|
|
106
114
|
const bin = process.env.EAS_BIN?.trim();
|
|
107
|
-
return [bin && bin.length > 0 ? bin : "eas", ...args];
|
|
115
|
+
return [bin && bin.length > 0 ? bin : localBin("eas") ?? "eas", ...args];
|
|
108
116
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
var localBins = new Map;
|
|
118
|
+
function localBin(name) {
|
|
119
|
+
const cwd = process.cwd();
|
|
120
|
+
const cacheKey = `${cwd}\x00${name}`;
|
|
121
|
+
const cached = localBins.get(cacheKey);
|
|
122
|
+
if (cached !== undefined)
|
|
123
|
+
return cached;
|
|
124
|
+
let bin = null;
|
|
125
|
+
for (let dir = cwd;; dir = dirname(dir)) {
|
|
126
|
+
const candidate = join(dir, "node_modules", ".bin", name);
|
|
127
|
+
if (existsSync(candidate)) {
|
|
128
|
+
bin = candidate;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
if (existsSync(join(dir, ".git")) || dirname(dir) === dir)
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
localBins.set(cacheKey, bin);
|
|
135
|
+
return bin;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
class CommandError extends Error {
|
|
139
|
+
tail;
|
|
140
|
+
constructor(message, tail) {
|
|
141
|
+
super(message);
|
|
142
|
+
this.tail = tail;
|
|
143
|
+
this.name = "CommandError";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
var ERROR_TAIL_LINES = 20;
|
|
147
|
+
function lastLines(output, count = ERROR_TAIL_LINES) {
|
|
148
|
+
return output.split(`
|
|
149
|
+
`).map((line) => line.trimEnd()).filter((line) => line.trim().length > 0).slice(-count);
|
|
150
|
+
}
|
|
151
|
+
var DRAIN_AFTER_EXIT_MS = 1000;
|
|
152
|
+
function collect(stream, echo, sink) {
|
|
153
|
+
const reader = stream.getReader();
|
|
154
|
+
const decoder = new TextDecoder;
|
|
155
|
+
let text = "";
|
|
156
|
+
const done = (async () => {
|
|
157
|
+
try {
|
|
158
|
+
for (;; ) {
|
|
159
|
+
const { value, done } = await reader.read();
|
|
160
|
+
if (done)
|
|
161
|
+
break;
|
|
162
|
+
echo?.write(value);
|
|
163
|
+
const part = decoder.decode(value, { stream: true });
|
|
164
|
+
text += part;
|
|
165
|
+
sink.push(part);
|
|
166
|
+
}
|
|
167
|
+
} catch {}
|
|
168
|
+
const rest = decoder.decode();
|
|
169
|
+
sink.push(rest);
|
|
170
|
+
text += rest;
|
|
171
|
+
})();
|
|
172
|
+
return {
|
|
173
|
+
done,
|
|
174
|
+
stop: () => reader.cancel().catch(() => {
|
|
175
|
+
return;
|
|
176
|
+
}),
|
|
177
|
+
text: () => text
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
async function spawn(command, echoStdout, echoStderr = true) {
|
|
181
|
+
const child = Bun.spawn(command, { stdout: "pipe", stderr: "pipe" });
|
|
182
|
+
const combined = [];
|
|
183
|
+
const out = collect(child.stdout, echoStdout ? process.stdout : null, combined);
|
|
184
|
+
const err = collect(child.stderr, echoStderr ? process.stderr : null, combined);
|
|
185
|
+
const exitCode = await child.exited;
|
|
186
|
+
const drained = Promise.all([out.done, err.done]);
|
|
187
|
+
let timer;
|
|
188
|
+
const timedOut = await Promise.race([
|
|
189
|
+
drained.then(() => false),
|
|
190
|
+
new Promise((resolve) => {
|
|
191
|
+
timer = setTimeout(() => resolve(true), DRAIN_AFTER_EXIT_MS);
|
|
192
|
+
})
|
|
118
193
|
]);
|
|
194
|
+
clearTimeout(timer);
|
|
195
|
+
if (timedOut) {
|
|
196
|
+
await Promise.all([out.stop(), err.stop()]);
|
|
197
|
+
await drained;
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
stdout: out.text(),
|
|
201
|
+
stderr: err.text(),
|
|
202
|
+
exitCode,
|
|
203
|
+
tail: lastLines(combined.join(""))
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
async function runCaptured(command) {
|
|
207
|
+
const { stdout, stderr, exitCode, tail } = await spawn(command, false);
|
|
208
|
+
if (exitCode !== 0) {
|
|
209
|
+
throw new CommandError(describeFailure(command, exitCode, { stderr, stdout }), tail);
|
|
210
|
+
}
|
|
211
|
+
return stdout;
|
|
212
|
+
}
|
|
213
|
+
async function runSilent(command) {
|
|
214
|
+
const { stdout, stderr, exitCode, tail } = await spawn(command, false, false);
|
|
119
215
|
if (exitCode !== 0) {
|
|
120
|
-
throw new
|
|
216
|
+
throw new CommandError(describeFailure(command, exitCode, { stderr, stdout }), tail);
|
|
217
|
+
}
|
|
218
|
+
return stdout;
|
|
219
|
+
}
|
|
220
|
+
async function runInherited(command) {
|
|
221
|
+
const { stdout, exitCode, tail } = await spawn(command, true);
|
|
222
|
+
if (exitCode !== 0) {
|
|
223
|
+
throw new CommandError(`Command failed with exit code ${exitCode}: ${quoteCommand(command)} (output above)`, tail);
|
|
121
224
|
}
|
|
122
225
|
return stdout;
|
|
123
226
|
}
|
|
@@ -150,21 +253,28 @@ ${tail(output.stdout)}`);
|
|
|
150
253
|
return sections.join(`
|
|
151
254
|
`);
|
|
152
255
|
}
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
256
|
+
function parseJsonDocument(output) {
|
|
257
|
+
const starts = [0];
|
|
258
|
+
for (const match of output.matchAll(/\n(?=[[{])/g)) {
|
|
259
|
+
starts.push(match.index + 1);
|
|
260
|
+
}
|
|
261
|
+
for (const start of starts) {
|
|
262
|
+
try {
|
|
263
|
+
return {
|
|
264
|
+
value: JSON.parse(output.slice(start)),
|
|
265
|
+
preamble: output.slice(0, start).trim()
|
|
266
|
+
};
|
|
267
|
+
} catch {}
|
|
158
268
|
}
|
|
159
|
-
|
|
269
|
+
throw new Error(`Expected JSON output, received:
|
|
270
|
+
${tail(output)}`);
|
|
160
271
|
}
|
|
161
272
|
function parseJson(output) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
273
|
+
const { value, preamble } = parseJsonDocument(output);
|
|
274
|
+
if (preamble)
|
|
275
|
+
process.stderr.write(`${preamble}
|
|
276
|
+
`);
|
|
277
|
+
return value;
|
|
168
278
|
}
|
|
169
279
|
function getRecord(value, context) {
|
|
170
280
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
@@ -173,7 +283,7 @@ function getRecord(value, context) {
|
|
|
173
283
|
return value;
|
|
174
284
|
}
|
|
175
285
|
function parseFingerprintHash(output) {
|
|
176
|
-
const record = getRecord(
|
|
286
|
+
const record = getRecord(parseJsonDocument(output).value, "EAS fingerprint");
|
|
177
287
|
if (typeof record.hash !== "string" || record.hash.length === 0) {
|
|
178
288
|
throw new Error("EAS fingerprint output did not contain a full hash");
|
|
179
289
|
}
|
|
@@ -186,8 +296,85 @@ function dryRunCommand(command) {
|
|
|
186
296
|
process.stderr.write(`[dry-run] ${quoteCommand(command)}
|
|
187
297
|
`);
|
|
188
298
|
}
|
|
189
|
-
|
|
190
|
-
|
|
299
|
+
|
|
300
|
+
class StageError extends Error {
|
|
301
|
+
stage;
|
|
302
|
+
tail;
|
|
303
|
+
constructor(stage, message, tail = []) {
|
|
304
|
+
super(message);
|
|
305
|
+
this.stage = stage;
|
|
306
|
+
this.tail = tail;
|
|
307
|
+
this.name = "StageError";
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
async function stage(name, run) {
|
|
311
|
+
const started = performance.now();
|
|
312
|
+
try {
|
|
313
|
+
return await run();
|
|
314
|
+
} catch (error) {
|
|
315
|
+
if (error instanceof StageError)
|
|
316
|
+
throw error;
|
|
317
|
+
throw new StageError(name, error instanceof Error ? error.message : String(error), error instanceof CommandError ? error.tail : []);
|
|
318
|
+
} finally {
|
|
319
|
+
const seconds = ((performance.now() - started) / 1000).toFixed(1);
|
|
320
|
+
process.stderr.write(`[expo-release] ${name}: ${seconds}s
|
|
321
|
+
`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async function generateFingerprint(input, commandRunner, explainConfig) {
|
|
325
|
+
const output = await commandRunner(easCommand("fingerprint:generate", "--platform", input.platform, "--build-profile", input.profile, "--json", "--non-interactive"));
|
|
326
|
+
const { preamble } = parseJsonDocument(output);
|
|
327
|
+
if (preamble) {
|
|
328
|
+
const reason = explainConfig ? await explainConfig() : "";
|
|
329
|
+
throw new CommandError([
|
|
330
|
+
"eas fingerprint:generate could not load the app config and fell back to a default one, so its fingerprint may not match what EAS builds.",
|
|
331
|
+
`eas-cli printed:
|
|
332
|
+
${preamble}`,
|
|
333
|
+
reason && `expo config said:
|
|
334
|
+
${reason}`
|
|
335
|
+
].filter(Boolean).join(`
|
|
336
|
+
`), lastLines(reason || preamble));
|
|
337
|
+
}
|
|
338
|
+
return parseFingerprintHash(output);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/source-maps.ts
|
|
342
|
+
import { exists } from "fs/promises";
|
|
343
|
+
var SOURCE_MAP_OUTPUT_DIR = "dist";
|
|
344
|
+
function expoCommand(...args) {
|
|
345
|
+
const bin = process.env.EXPO_BIN?.trim();
|
|
346
|
+
if (bin && bin.length > 0)
|
|
347
|
+
return [bin, ...args];
|
|
348
|
+
const local = localBin("expo");
|
|
349
|
+
return local ? [local, ...args] : ["bunx", "expo", ...args];
|
|
350
|
+
}
|
|
351
|
+
function sourceMapExportCommand(platform) {
|
|
352
|
+
return expoCommand("export", "--platform", platform, "--output-dir", SOURCE_MAP_OUTPUT_DIR, "--source-maps", "external");
|
|
353
|
+
}
|
|
354
|
+
function mergeUpdateArgs(extra, exportSourceMaps) {
|
|
355
|
+
const args = extra ? [...extra] : [];
|
|
356
|
+
if (!exportSourceMaps)
|
|
357
|
+
return args;
|
|
358
|
+
if (!args.includes("--input-dir")) {
|
|
359
|
+
args.push("--input-dir", SOURCE_MAP_OUTPUT_DIR);
|
|
360
|
+
}
|
|
361
|
+
if (!args.includes("--skip-bundler")) {
|
|
362
|
+
args.push("--skip-bundler");
|
|
363
|
+
}
|
|
364
|
+
return args;
|
|
365
|
+
}
|
|
366
|
+
async function defaultHasSourceMaps(directory = SOURCE_MAP_OUTPUT_DIR) {
|
|
367
|
+
if (!await exists(directory))
|
|
368
|
+
return false;
|
|
369
|
+
for await (const _ of new Bun.Glob("**/*.map").scan(directory)) {
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
async function assertSourceMaps(hasSourceMaps = defaultHasSourceMaps) {
|
|
375
|
+
if (await hasSourceMaps())
|
|
376
|
+
return;
|
|
377
|
+
throw new Error("Expo export did not produce Hermes source maps in dist; refusing to publish an OTA update.");
|
|
191
378
|
}
|
|
192
379
|
|
|
193
380
|
// src/decide.ts
|
|
@@ -226,8 +413,8 @@ function normalizeBuildStatus(value) {
|
|
|
226
413
|
return null;
|
|
227
414
|
}
|
|
228
415
|
function dashboardBuildUrl(record) {
|
|
229
|
-
const ownerName = nestedString(record, "app", "ownerAccount", "name");
|
|
230
|
-
const slug = nestedString(record, "app", "slug");
|
|
416
|
+
const ownerName = nestedString(record, "project", "ownerAccount", "name") ?? nestedString(record, "app", "ownerAccount", "name");
|
|
417
|
+
const slug = nestedString(record, "project", "slug") ?? nestedString(record, "app", "slug");
|
|
231
418
|
if (typeof record.id !== "string" || !ownerName || !slug) {
|
|
232
419
|
return;
|
|
233
420
|
}
|
|
@@ -252,13 +439,17 @@ function parseBuildList(output) {
|
|
|
252
439
|
}
|
|
253
440
|
const buildUrl = dashboardBuildUrl(record);
|
|
254
441
|
const fingerprint = nestedString(record, "fingerprint", "hash");
|
|
255
|
-
const runtimeVersion = nestedString(record, "runtime", "version");
|
|
442
|
+
const runtimeVersion = nestedString(record, "runtimeVersion") ?? nestedString(record, "runtime", "version");
|
|
443
|
+
const errorCode = nestedString(record, "error", "errorCode");
|
|
444
|
+
const errorMessage = nestedString(record, "error", "message");
|
|
445
|
+
const error = [errorCode, errorMessage].filter(Boolean).join(": ");
|
|
256
446
|
builds.push({
|
|
257
447
|
id: record.id,
|
|
258
448
|
status,
|
|
259
449
|
...buildUrl ? { buildUrl } : {},
|
|
260
450
|
...fingerprint ? { fingerprint } : {},
|
|
261
|
-
...runtimeVersion ? { runtimeVersion } : {}
|
|
451
|
+
...runtimeVersion ? { runtimeVersion } : {},
|
|
452
|
+
...error ? { error } : {}
|
|
262
453
|
});
|
|
263
454
|
}
|
|
264
455
|
return builds;
|
|
@@ -275,26 +466,71 @@ function hasExpoImpact(paths, impactPaths = DEFAULT_IMPACT_PATHS) {
|
|
|
275
466
|
function resolveBuildDecision(builds) {
|
|
276
467
|
const finished = builds.find((build) => build.status === "finished");
|
|
277
468
|
if (finished) {
|
|
278
|
-
return {
|
|
469
|
+
return {
|
|
470
|
+
deployType: "ota",
|
|
471
|
+
reason: "finished-build",
|
|
472
|
+
nativeBuildPending: false,
|
|
473
|
+
build: finished
|
|
474
|
+
};
|
|
279
475
|
}
|
|
280
476
|
const inProgress = builds.find((build) => ACTIVE_BUILD_STATUSES.includes(build.status));
|
|
281
477
|
if (inProgress) {
|
|
282
478
|
return {
|
|
283
479
|
deployType: "ota",
|
|
480
|
+
reason: "build-in-progress",
|
|
284
481
|
nativeBuildPending: true,
|
|
285
482
|
build: inProgress
|
|
286
483
|
};
|
|
287
484
|
}
|
|
288
485
|
const newest = builds[0];
|
|
289
486
|
if (newest?.status === "errored") {
|
|
290
|
-
const
|
|
291
|
-
|
|
487
|
+
const lines = [
|
|
488
|
+
`The newest native build for this fingerprint failed on EAS${newest.error ? `: ${newest.error}` : ` (build ${newest.id})`}`,
|
|
489
|
+
newest.buildUrl,
|
|
490
|
+
"Fix it, then run the workflow with operation force-native."
|
|
491
|
+
].filter((line) => Boolean(line));
|
|
492
|
+
throw new StageError("errored build", lines.join(`
|
|
493
|
+
`), lines);
|
|
292
494
|
}
|
|
293
|
-
return {
|
|
495
|
+
return {
|
|
496
|
+
deployType: "native",
|
|
497
|
+
reason: "no-build-for-fingerprint",
|
|
498
|
+
nativeBuildPending: false,
|
|
499
|
+
build: null
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
async function readPullRequestBase() {
|
|
503
|
+
const path = process.env.GITHUB_EVENT_PATH;
|
|
504
|
+
if (!path)
|
|
505
|
+
return;
|
|
506
|
+
try {
|
|
507
|
+
const event = await Bun.file(path).json();
|
|
508
|
+
const sha = event.pull_request?.base?.sha;
|
|
509
|
+
return typeof sha === "string" && sha ? sha : undefined;
|
|
510
|
+
} catch {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
async function defaultBase(head, commandRunner) {
|
|
515
|
+
const event = process.env.GITHUB_EVENT_NAME;
|
|
516
|
+
if (event === "pull_request" || event === "pull_request_target") {
|
|
517
|
+
const base = await readPullRequestBase();
|
|
518
|
+
if (base && head) {
|
|
519
|
+
try {
|
|
520
|
+
const mergeBase = (await commandRunner(["git", "merge-base", base, head])).trim();
|
|
521
|
+
if (mergeBase)
|
|
522
|
+
return mergeBase;
|
|
523
|
+
} catch {}
|
|
524
|
+
}
|
|
525
|
+
if (base)
|
|
526
|
+
return base;
|
|
527
|
+
}
|
|
528
|
+
return process.env.GITHUB_EVENT_BEFORE?.trim() || undefined;
|
|
294
529
|
}
|
|
295
530
|
async function getChangedFiles(input, commandRunner) {
|
|
296
531
|
const head = input.head;
|
|
297
|
-
|
|
532
|
+
const base = input.base ?? await defaultBase(head, commandRunner);
|
|
533
|
+
if (!base || !head || /^0+$/.test(base)) {
|
|
298
534
|
return { files: [], unreachable: true };
|
|
299
535
|
}
|
|
300
536
|
try {
|
|
@@ -304,7 +540,7 @@ async function getChangedFiles(input, commandRunner) {
|
|
|
304
540
|
"diff.relative=false",
|
|
305
541
|
"diff",
|
|
306
542
|
"--name-only",
|
|
307
|
-
|
|
543
|
+
base,
|
|
308
544
|
head
|
|
309
545
|
]);
|
|
310
546
|
return {
|
|
@@ -319,25 +555,95 @@ async function getChangedFiles(input, commandRunner) {
|
|
|
319
555
|
async function getMatchingBuilds(input, fingerprint, commandRunner) {
|
|
320
556
|
return parseBuildList(await commandRunner(easCommand("build:list", "--platform", input.platform, "--build-profile", input.profile, "--fingerprint-hash", fingerprint, "--limit", "20", "--json", "--non-interactive")));
|
|
321
557
|
}
|
|
558
|
+
function fingerprintSources(value) {
|
|
559
|
+
const sources = value?.sources;
|
|
560
|
+
const map = new Map;
|
|
561
|
+
if (!Array.isArray(sources))
|
|
562
|
+
return map;
|
|
563
|
+
for (const source of sources) {
|
|
564
|
+
const key = source.type === "contents" ? source.id : source.filePath ?? source.id;
|
|
565
|
+
if (typeof key === "string")
|
|
566
|
+
map.set(key, source.hash ?? null);
|
|
567
|
+
}
|
|
568
|
+
return map;
|
|
569
|
+
}
|
|
570
|
+
var MAX_DIFF_SOURCES = 30;
|
|
571
|
+
function diffFingerprintSources(compare) {
|
|
572
|
+
const record = getRecord(compare, "EAS fingerprint:compare");
|
|
573
|
+
const before = fingerprintSources(record.fingerprint1);
|
|
574
|
+
const after = fingerprintSources(record.fingerprint2);
|
|
575
|
+
const lines = [];
|
|
576
|
+
for (const [key, hash] of after) {
|
|
577
|
+
if (!before.has(key))
|
|
578
|
+
lines.push(`added ${key}`);
|
|
579
|
+
else if (before.get(key) !== hash)
|
|
580
|
+
lines.push(`changed ${key}`);
|
|
581
|
+
}
|
|
582
|
+
for (const key of before.keys()) {
|
|
583
|
+
if (!after.has(key))
|
|
584
|
+
lines.push(`removed ${key}`);
|
|
585
|
+
}
|
|
586
|
+
if (lines.length <= MAX_DIFF_SOURCES)
|
|
587
|
+
return lines;
|
|
588
|
+
return [
|
|
589
|
+
...lines.slice(0, MAX_DIFF_SOURCES),
|
|
590
|
+
`... and ${lines.length - MAX_DIFF_SOURCES} more`
|
|
591
|
+
];
|
|
592
|
+
}
|
|
593
|
+
async function explainNativeDecision(input, commandRunner) {
|
|
594
|
+
try {
|
|
595
|
+
return await stage("fingerprint diff", async () => {
|
|
596
|
+
const [last] = parseBuildList(await commandRunner(easCommand("build:list", "--platform", input.platform, "--build-profile", input.profile, "--status", "finished", "--limit", "1", "--json", "--non-interactive")));
|
|
597
|
+
if (!last)
|
|
598
|
+
return null;
|
|
599
|
+
const command = easCommand("fingerprint:compare", "--build-id", last.id, "--json", "--non-interactive");
|
|
600
|
+
if (input.environment)
|
|
601
|
+
command.push("--environment", input.environment);
|
|
602
|
+
const { value } = parseJsonDocument(await commandRunner(command));
|
|
603
|
+
return {
|
|
604
|
+
buildId: last.id,
|
|
605
|
+
...last.buildUrl ? { buildUrl: last.buildUrl } : {},
|
|
606
|
+
sources: diffFingerprintSources(value)
|
|
607
|
+
};
|
|
608
|
+
});
|
|
609
|
+
} catch (error) {
|
|
610
|
+
process.stderr.write(`Warning: could not compare the fingerprint with the last finished build: ${error instanceof Error ? error.message : String(error)}
|
|
611
|
+
`);
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
async function explainConfigFailure(commandRunner) {
|
|
616
|
+
try {
|
|
617
|
+
await commandRunner(expoCommand("config", "--json"));
|
|
618
|
+
return "";
|
|
619
|
+
} catch (error) {
|
|
620
|
+
return error instanceof Error ? error.message : String(error);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
322
623
|
async function decideRelease(input, commandRunner = runCaptured) {
|
|
323
|
-
const { files: changedFiles, unreachable } = await getChangedFiles(input, commandRunner);
|
|
624
|
+
const { files: changedFiles, unreachable } = await stage("diff", () => getChangedFiles(input, commandRunner));
|
|
324
625
|
if (!input.ignoreExpoImpact && !unreachable && !hasExpoImpact(changedFiles, input.impactPaths)) {
|
|
325
626
|
return {
|
|
326
627
|
deployType: "none",
|
|
628
|
+
reason: "no-impact-path",
|
|
327
629
|
nativeBuildPending: false,
|
|
328
630
|
changedFiles,
|
|
329
631
|
fingerprint: null,
|
|
330
632
|
build: null
|
|
331
633
|
};
|
|
332
634
|
}
|
|
333
|
-
|
|
334
|
-
const
|
|
635
|
+
await input.preflight?.();
|
|
636
|
+
const fingerprint = await stage("fingerprint", () => generateFingerprint(input, commandRunner, () => explainConfigFailure(commandRunner)));
|
|
637
|
+
const builds = await stage("build list", () => getMatchingBuilds(input, fingerprint, commandRunner));
|
|
638
|
+
const resolved = resolveBuildDecision(builds);
|
|
335
639
|
return {
|
|
336
640
|
deployType: resolved.deployType,
|
|
641
|
+
reason: resolved.reason,
|
|
337
642
|
nativeBuildPending: resolved.nativeBuildPending,
|
|
338
643
|
changedFiles,
|
|
339
644
|
fingerprint,
|
|
340
|
-
build: resolved.build
|
|
645
|
+
build: resolved.build,
|
|
646
|
+
...resolved.deployType === "native" ? { fingerprintDiff: await explainNativeDecision(input, commandRunner) } : {}
|
|
341
647
|
};
|
|
342
648
|
}
|
|
343
649
|
async function writeDecideGitHubOutput(decision) {
|
|
@@ -347,52 +653,22 @@ async function writeDecideGitHubOutput(decision) {
|
|
|
347
653
|
changed: String(decision.deployType !== "none"),
|
|
348
654
|
fingerprint: decision.fingerprint ?? "",
|
|
349
655
|
build_id: decision.build?.id ?? "",
|
|
350
|
-
build_url: decision.build?.buildUrl ?? ""
|
|
656
|
+
build_url: decision.build?.buildUrl ?? "",
|
|
657
|
+
reason: decision.reason,
|
|
658
|
+
changed_sources: decision.fingerprintDiff?.sources.join(`
|
|
659
|
+
`) ?? "",
|
|
660
|
+
compared_build_url: decision.fingerprintDiff?.buildUrl ?? decision.fingerprintDiff?.buildId ?? ""
|
|
351
661
|
});
|
|
352
662
|
}
|
|
353
663
|
|
|
354
|
-
// src/
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if (bin && bin.length > 0)
|
|
360
|
-
return [bin, ...args];
|
|
361
|
-
return ["bunx", "expo", ...args];
|
|
362
|
-
}
|
|
363
|
-
function sourceMapExportCommand(platform) {
|
|
364
|
-
return expoCommand("export", "--platform", platform, "--output-dir", SOURCE_MAP_OUTPUT_DIR, "--source-maps", "external");
|
|
365
|
-
}
|
|
366
|
-
function mergeUpdateArgs(extra, exportSourceMaps) {
|
|
367
|
-
const args = extra ? [...extra] : [];
|
|
368
|
-
if (!exportSourceMaps)
|
|
369
|
-
return args;
|
|
370
|
-
if (!args.includes("--input-dir")) {
|
|
371
|
-
args.push("--input-dir", SOURCE_MAP_OUTPUT_DIR);
|
|
372
|
-
}
|
|
373
|
-
if (!args.includes("--skip-bundler")) {
|
|
374
|
-
args.push("--skip-bundler");
|
|
664
|
+
// src/operate.ts
|
|
665
|
+
function splitArgs(value) {
|
|
666
|
+
const args = [];
|
|
667
|
+
for (const match of (value ?? "").matchAll(/"((?:\\.|[^"\\])*)"|'([^']*)'|(\S+)/g)) {
|
|
668
|
+
args.push(match[1]?.replace(/\\(.)/g, "$1") ?? match[2] ?? match[3] ?? "");
|
|
375
669
|
}
|
|
376
670
|
return args;
|
|
377
671
|
}
|
|
378
|
-
async function defaultHasSourceMaps(directory = SOURCE_MAP_OUTPUT_DIR) {
|
|
379
|
-
if (!await exists(directory))
|
|
380
|
-
return false;
|
|
381
|
-
for await (const _ of new Bun.Glob("**/*.map").scan(directory)) {
|
|
382
|
-
return true;
|
|
383
|
-
}
|
|
384
|
-
return false;
|
|
385
|
-
}
|
|
386
|
-
async function assertSourceMaps(hasSourceMaps = defaultHasSourceMaps) {
|
|
387
|
-
if (await hasSourceMaps())
|
|
388
|
-
return;
|
|
389
|
-
throw new Error("Expo export did not produce Hermes source maps in dist; refusing to publish an OTA update.");
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// src/operate.ts
|
|
393
|
-
function extraUpdateArgs(value) {
|
|
394
|
-
return value?.split(/\s+/).filter(Boolean) ?? [];
|
|
395
|
-
}
|
|
396
672
|
function updateMessage(input) {
|
|
397
673
|
if (input.message)
|
|
398
674
|
return input.message;
|
|
@@ -417,23 +693,25 @@ async function defaultReadPackageJson() {
|
|
|
417
693
|
return null;
|
|
418
694
|
return file.json();
|
|
419
695
|
}
|
|
696
|
+
async function readExpoSdkMajor(readPackageJson = defaultReadPackageJson) {
|
|
697
|
+
const pkg = await readPackageJson();
|
|
698
|
+
if (!pkg || typeof pkg !== "object")
|
|
699
|
+
return null;
|
|
700
|
+
const record = pkg;
|
|
701
|
+
return parseExpoSdkMajor(record.dependencies?.expo ?? record.devDependencies?.expo);
|
|
702
|
+
}
|
|
420
703
|
async function requireEnvironmentIfNeeded(input) {
|
|
421
704
|
if (input.environment)
|
|
422
705
|
return;
|
|
423
|
-
const
|
|
424
|
-
if (!pkg || typeof pkg !== "object" || pkg === null)
|
|
425
|
-
return;
|
|
426
|
-
const record = pkg;
|
|
427
|
-
const version = record.dependencies?.expo ?? record.devDependencies?.expo;
|
|
428
|
-
const major = parseExpoSdkMajor(version);
|
|
706
|
+
const major = await readExpoSdkMajor(input.readPackageJson);
|
|
429
707
|
if (major !== null && major >= 55) {
|
|
430
|
-
throw new Error("EAS Update requires --environment for Expo SDK 55+. Pass --environment <name>.");
|
|
708
|
+
throw new Error("EAS Update requires --environment for Expo SDK 55+. Pass --environment <name> or set build.<profile>.environment in eas.json.");
|
|
431
709
|
}
|
|
432
710
|
}
|
|
433
|
-
async function runHook(command, commandRunner) {
|
|
711
|
+
async function runHook(name, command, commandRunner) {
|
|
434
712
|
if (!command)
|
|
435
713
|
return;
|
|
436
|
-
await commandRunner(["bash", "-c", command]);
|
|
714
|
+
await stage(name, () => commandRunner(["bash", "-c", command]));
|
|
437
715
|
}
|
|
438
716
|
function mutatingRunner(commandRunner, dryRun) {
|
|
439
717
|
return async (command) => {
|
|
@@ -447,33 +725,44 @@ function mutatingRunner(commandRunner, dryRun) {
|
|
|
447
725
|
async function publishUpdate(input, commandRunner) {
|
|
448
726
|
await requireEnvironmentIfNeeded(input);
|
|
449
727
|
if (input.exportSourceMaps) {
|
|
450
|
-
await
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
728
|
+
await stage("ota export", async () => {
|
|
729
|
+
await commandRunner(sourceMapExportCommand(input.platform));
|
|
730
|
+
if (input.dryRun !== true) {
|
|
731
|
+
await assertSourceMaps(input.hasSourceMaps);
|
|
732
|
+
}
|
|
733
|
+
});
|
|
454
734
|
}
|
|
455
|
-
await runHook(input.preUpdate, commandRunner);
|
|
735
|
+
await runHook("pre-update", input.preUpdate, commandRunner);
|
|
456
736
|
const command = easCommand("update", "--channel", input.channel, "--platform", input.platform, "--message", updateMessage(input), "--non-interactive");
|
|
457
737
|
if (input.environment) {
|
|
458
738
|
command.push("--environment", input.environment);
|
|
459
739
|
}
|
|
460
|
-
command.push(...mergeUpdateArgs(
|
|
461
|
-
await commandRunner(command);
|
|
462
|
-
await runHook(input.postUpdate, commandRunner);
|
|
740
|
+
command.push(...mergeUpdateArgs(splitArgs(input.updateExtraArgs), input.exportSourceMaps === true));
|
|
741
|
+
await stage("ota update", () => commandRunner(command));
|
|
742
|
+
await runHook("post-update", input.postUpdate, commandRunner);
|
|
463
743
|
}
|
|
464
744
|
async function queueNativeBuild(input, commandRunner) {
|
|
465
|
-
const command = easCommand("build", "--platform", input.platform, "--profile", input.profile, "--non-interactive", "--no-wait");
|
|
745
|
+
const command = easCommand("build", "--platform", input.platform, "--profile", input.profile, "--non-interactive", "--no-wait", "--json");
|
|
466
746
|
if (input.autoSubmit) {
|
|
467
747
|
command.push("--auto-submit");
|
|
468
748
|
}
|
|
469
|
-
await commandRunner(command);
|
|
749
|
+
const output = await stage("native build", () => commandRunner(command));
|
|
750
|
+
try {
|
|
751
|
+
const queued = output.trim() ? parseBuildList(output)[0] : undefined;
|
|
752
|
+
if (queued)
|
|
753
|
+
input.onQueuedBuild?.(queued);
|
|
754
|
+
} catch (error) {
|
|
755
|
+
process.stderr.write(`Warning: could not read the queued build from eas build --json: ${error instanceof Error ? error.message : String(error)}
|
|
756
|
+
`);
|
|
757
|
+
}
|
|
758
|
+
return "native-build";
|
|
470
759
|
}
|
|
471
760
|
async function resolveRuntimeVersion(input, commandRunner) {
|
|
472
761
|
if (input.runtimeVersion)
|
|
473
762
|
return input.runtimeVersion;
|
|
474
763
|
if (input.fingerprint)
|
|
475
764
|
return input.fingerprint;
|
|
476
|
-
return generateFingerprint(input, commandRunner);
|
|
765
|
+
return stage("fingerprint", () => generateFingerprint(input, commandRunner));
|
|
477
766
|
}
|
|
478
767
|
async function operateRelease(input, commandRunner = runInherited) {
|
|
479
768
|
const execute = mutatingRunner(commandRunner, input.dryRun === true);
|
|
@@ -482,19 +771,19 @@ async function operateRelease(input, commandRunner = runInherited) {
|
|
|
482
771
|
if (!input.group) {
|
|
483
772
|
throw new Error("Usage: --operation republish requires --group <update-group-id>");
|
|
484
773
|
}
|
|
774
|
+
const group = input.group;
|
|
485
775
|
await requireEnvironmentIfNeeded(input);
|
|
486
|
-
await execute(easCommand("update:republish", "--group",
|
|
776
|
+
await stage("recovery", () => execute(easCommand("update:republish", "--group", group, "--platform", input.platform, "--message", updateMessage(input), "--non-interactive")));
|
|
487
777
|
return "recovery";
|
|
488
778
|
}
|
|
489
779
|
case "rollback-embedded": {
|
|
490
780
|
await requireEnvironmentIfNeeded(input);
|
|
491
781
|
const runtimeVersion = await resolveRuntimeVersion(input, commandRunner);
|
|
492
|
-
await execute(easCommand("update:roll-back-to-embedded", "--channel", input.channel, "--platform", input.platform, "--runtime-version", runtimeVersion, "--message", updateMessage(input), "--non-interactive"));
|
|
782
|
+
await stage("recovery", () => execute(easCommand("update:roll-back-to-embedded", "--channel", input.channel, "--platform", input.platform, "--runtime-version", runtimeVersion, "--message", updateMessage(input), "--non-interactive")));
|
|
493
783
|
return "recovery";
|
|
494
784
|
}
|
|
495
785
|
case "force-native":
|
|
496
|
-
|
|
497
|
-
return "native-build";
|
|
786
|
+
return queueNativeBuild(input, execute);
|
|
498
787
|
case "retry-ota":
|
|
499
788
|
await publishUpdate(input, execute);
|
|
500
789
|
return "ota";
|
|
@@ -509,8 +798,7 @@ async function operateRelease(input, commandRunner = runInherited) {
|
|
|
509
798
|
case "none":
|
|
510
799
|
return "skipped";
|
|
511
800
|
case "native":
|
|
512
|
-
|
|
513
|
-
return "native-build";
|
|
801
|
+
return queueNativeBuild(input, execute);
|
|
514
802
|
case "ota":
|
|
515
803
|
await publishUpdate(input, execute);
|
|
516
804
|
return "ota";
|
|
@@ -520,19 +808,355 @@ async function operateRelease(input, commandRunner = runInherited) {
|
|
|
520
808
|
}
|
|
521
809
|
}
|
|
522
810
|
}
|
|
523
|
-
async function writeOperateGitHubOutput(result) {
|
|
524
|
-
await appendGitHubOutput({
|
|
811
|
+
async function writeOperateGitHubOutput(result, queuedBuild) {
|
|
812
|
+
await appendGitHubOutput({
|
|
813
|
+
result,
|
|
814
|
+
queued_build_id: queuedBuild?.id ?? "",
|
|
815
|
+
queued_build_url: queuedBuild?.buildUrl ?? ""
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// src/doctor.ts
|
|
820
|
+
var CREDENTIAL_FILE = /\.(keystore|jks|p8|p12|mobileprovision)$/;
|
|
821
|
+
async function tryRun(runner, command) {
|
|
822
|
+
try {
|
|
823
|
+
return { ok: true, output: await runner(command) };
|
|
824
|
+
} catch (error) {
|
|
825
|
+
return {
|
|
826
|
+
ok: false,
|
|
827
|
+
error: error instanceof Error ? error.message : String(error)
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
async function defaultReadFile(path) {
|
|
832
|
+
const file = Bun.file(path);
|
|
833
|
+
return await file.exists() ? file.text() : null;
|
|
834
|
+
}
|
|
835
|
+
function versionOf(output) {
|
|
836
|
+
return output.match(/(\d+\.\d+\.\d+)/)?.[1];
|
|
837
|
+
}
|
|
838
|
+
async function checkTools(input, runner, findings) {
|
|
839
|
+
const env = input.env ?? process.env;
|
|
840
|
+
const eas = await tryRun(runner, easCommand("--version"));
|
|
841
|
+
if (!eas.ok) {
|
|
842
|
+
findings.push({
|
|
843
|
+
level: "error",
|
|
844
|
+
message: "eas-cli did not run.",
|
|
845
|
+
fix: "Add eas-cli to the app devDependencies, or set it up with expo/expo-github-action."
|
|
846
|
+
});
|
|
847
|
+
} else {
|
|
848
|
+
const wanted = input.easJson?.cli?.version;
|
|
849
|
+
const actual = versionOf(eas.output);
|
|
850
|
+
if (wanted && actual && !Bun.semver.satisfies(actual, wanted)) {
|
|
851
|
+
findings.push({
|
|
852
|
+
level: "error",
|
|
853
|
+
message: `eas-cli ${actual} does not satisfy eas.json cli.version ${wanted}.`,
|
|
854
|
+
fix: `Pin eas-cli ${wanted} in the app devDependencies or the workflow.`
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (env.CI && !env.EXPO_TOKEN) {
|
|
859
|
+
findings.push({
|
|
860
|
+
level: "error",
|
|
861
|
+
message: "EXPO_TOKEN is not set.",
|
|
862
|
+
fix: "Add the EXPO_TOKEN secret (https://expo.dev/settings/access-tokens) and pass it to the job."
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
const bun = input.buildProfile?.bun;
|
|
866
|
+
const bunVersion = input.bunVersion ?? Bun.version;
|
|
867
|
+
if (bun && bun !== bunVersion) {
|
|
868
|
+
findings.push({
|
|
869
|
+
level: "error",
|
|
870
|
+
message: `This job runs Bun ${bunVersion}, the "${input.profile}" profile builds with Bun ${bun}. The fingerprint and lockfile can differ between them.`,
|
|
871
|
+
fix: `Use Bun ${bun} in CI, or set build.${input.profile}.bun to ${bunVersion}.`
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
const node = input.buildProfile?.node;
|
|
875
|
+
if (node) {
|
|
876
|
+
const actual = await tryRun(runner, ["node", "--version"]);
|
|
877
|
+
const version = actual.ok ? versionOf(actual.output) : undefined;
|
|
878
|
+
if (version && version !== node) {
|
|
879
|
+
findings.push({
|
|
880
|
+
level: "warning",
|
|
881
|
+
message: `This job runs Node ${version}, the "${input.profile}" profile builds with Node ${node}.`,
|
|
882
|
+
fix: `Use Node ${node} in CI (actions/setup-node).`
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
async function checkProfile(input, findings) {
|
|
888
|
+
if (!input.easJson) {
|
|
889
|
+
findings.push({
|
|
890
|
+
level: "error",
|
|
891
|
+
message: "No eas.json in the working directory.",
|
|
892
|
+
fix: "Set working-directory to the Expo app, or run `eas build:configure` there."
|
|
893
|
+
});
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
if (!input.buildProfile) {
|
|
897
|
+
const names = Object.keys(input.easJson.build ?? {}).join(", ") || "none";
|
|
898
|
+
findings.push({
|
|
899
|
+
level: "error",
|
|
900
|
+
message: `eas.json has no build profile "${input.profile}" (profiles: ${names}).`
|
|
901
|
+
});
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
if (!input.buildProfile.channel) {
|
|
905
|
+
findings.push({
|
|
906
|
+
level: "warning",
|
|
907
|
+
message: `Build profile "${input.profile}" has no channel, so its builds receive no EAS updates.`,
|
|
908
|
+
fix: `Set build.${input.profile}.channel in eas.json.`
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
if (input.autoSubmit && !input.easJson.submit?.[input.profile]) {
|
|
912
|
+
findings.push({
|
|
913
|
+
level: "warning",
|
|
914
|
+
message: `auto-submit is on, but eas.json has no submit.${input.profile}.`,
|
|
915
|
+
fix: input.platform === "ios" ? `Set submit.${input.profile}.ios.ascAppId.` : `Set submit.${input.profile}.android.serviceAccountKeyPath, pointing at a file from an EAS file secret.`
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
if (!input.environment) {
|
|
919
|
+
const major = await readExpoSdkMajor(input.readPackageJson);
|
|
920
|
+
if (major !== null && major >= 55) {
|
|
921
|
+
findings.push({
|
|
922
|
+
level: "error",
|
|
923
|
+
message: "Expo SDK 55+ needs an EAS environment for eas update.",
|
|
924
|
+
fix: `Set build.${input.profile}.environment in eas.json, or pass --environment.`
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
async function checkTrackedFiles(runner, findings, readFile) {
|
|
930
|
+
const listed = await tryRun(runner, ["git", "ls-files"]);
|
|
931
|
+
if (!listed.ok)
|
|
932
|
+
return;
|
|
933
|
+
const files = listed.output.split(`
|
|
934
|
+
`).filter(Boolean);
|
|
935
|
+
const credentials = files.filter((file) => CREDENTIAL_FILE.test(file) && !/(^|\/)debug\.keystore$/.test(file));
|
|
936
|
+
for (const file of files) {
|
|
937
|
+
if (!file.endsWith(".json") || credentials.includes(file))
|
|
938
|
+
continue;
|
|
939
|
+
const text = await readFile(file);
|
|
940
|
+
if (text && text.length < 50000 && /"type"\s*:\s*"service_account"/.test(text)) {
|
|
941
|
+
credentials.push(file);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
if (credentials.length > 0) {
|
|
945
|
+
findings.push({
|
|
946
|
+
level: "error",
|
|
947
|
+
message: `Credentials are committed: ${credentials.join(", ")}.`,
|
|
948
|
+
fix: "Remove them from git, rotate them, and keep them in EAS (`eas credentials`, EAS file secrets)."
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
const generated = files.filter((file) => /^\.expo\//.test(file) || /\.log$/.test(file));
|
|
952
|
+
if (generated.length > 0) {
|
|
953
|
+
findings.push({
|
|
954
|
+
level: "warning",
|
|
955
|
+
message: `Generated files are committed: ${generated.slice(0, 5).join(", ")}${generated.length > 5 ? `, and ${generated.length - 5} more` : ""}.`,
|
|
956
|
+
fix: "git rm --cached them and add .expo/ and *.log to .gitignore."
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
const nativeDirs = ["android", "ios"].filter((dir) => files.some((file) => file.startsWith(`${dir}/`)));
|
|
960
|
+
if (nativeDirs.length === 0)
|
|
961
|
+
return;
|
|
962
|
+
const ignore = await readFile(".fingerprintignore") ?? "";
|
|
963
|
+
const lines = ignore.split(`
|
|
964
|
+
`).map((line) => line.trim().replace(/^\//, ""));
|
|
965
|
+
const hidden = nativeDirs.filter((dir) => lines.some((line) => [dir, `${dir}/`, `${dir}/*`, `${dir}/**`, `${dir}/**/*`].includes(line)));
|
|
966
|
+
if (hidden.length > 0) {
|
|
967
|
+
findings.push({
|
|
968
|
+
level: "error",
|
|
969
|
+
message: `${hidden.join(" and ")} are committed, and .fingerprintignore hides them: EAS builds the committed tree, but a change there never asks for a native build.`,
|
|
970
|
+
fix: "Either delete and gitignore the native folders (continuous native generation), or stop ignoring them in .fingerprintignore."
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
return nativeDirs;
|
|
974
|
+
}
|
|
975
|
+
async function checkAppConfig(runner, findings, nativeDirs) {
|
|
976
|
+
const config = await tryRun(runner, expoCommand("config", "--json"));
|
|
977
|
+
if (!config.ok) {
|
|
978
|
+
findings.push({
|
|
979
|
+
level: "error",
|
|
980
|
+
message: `The app config did not load:
|
|
981
|
+
${config.error}`,
|
|
982
|
+
fix: "Run `npx expo config` in the app with the same environment variables."
|
|
983
|
+
});
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
let parsed = {};
|
|
987
|
+
try {
|
|
988
|
+
parsed = JSON.parse(config.output);
|
|
989
|
+
} catch {
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
const policy = parsed.runtimeVersion?.policy;
|
|
993
|
+
if (policy !== "fingerprint") {
|
|
994
|
+
findings.push({
|
|
995
|
+
level: "warning",
|
|
996
|
+
message: `runtimeVersion is ${JSON.stringify(parsed.runtimeVersion ?? null)}, not { policy: 'fingerprint' }: an OTA can reach a binary whose native code differs.`,
|
|
997
|
+
fix: "Set runtimeVersion: { policy: 'fingerprint' } in the app config."
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
if (nativeDirs.length > 0 && Array.isArray(parsed.plugins) && parsed.plugins.length > 0) {
|
|
1001
|
+
findings.push({
|
|
1002
|
+
level: "warning",
|
|
1003
|
+
message: `${nativeDirs.join(" and ")} are committed while the app config has plugins: EAS builds the committed tree and never applies config plugin changes.`,
|
|
1004
|
+
fix: "Delete and gitignore the native folders, or run `npx expo prebuild` and commit after every plugin change."
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
async function runDoctor(input, runner = runSilent) {
|
|
1009
|
+
const findings = [];
|
|
1010
|
+
const readFile = input.readFile ?? defaultReadFile;
|
|
1011
|
+
await checkProfile(input, findings);
|
|
1012
|
+
await checkTools(input, runner, findings);
|
|
1013
|
+
const nativeDirs = await checkTrackedFiles(runner, findings, readFile) ?? [];
|
|
1014
|
+
if (input.full)
|
|
1015
|
+
await checkAppConfig(runner, findings, nativeDirs);
|
|
1016
|
+
return findings;
|
|
1017
|
+
}
|
|
1018
|
+
function escapeAnnotation(text) {
|
|
1019
|
+
return text.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll(`
|
|
1020
|
+
`, "%0A");
|
|
1021
|
+
}
|
|
1022
|
+
function formatFindings(findings, options = {}) {
|
|
1023
|
+
return findings.map((finding) => {
|
|
1024
|
+
const level = options.asWarnings ? "warning" : finding.level;
|
|
1025
|
+
const text = finding.fix ? `${finding.message}
|
|
1026
|
+
Fix: ${finding.fix}` : finding.message;
|
|
1027
|
+
return options.github ? `::${level} title=expo-release doctor::${escapeAnnotation(text)}` : `${level}: ${text}`;
|
|
1028
|
+
}).join(`
|
|
1029
|
+
`);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// src/eas-json.ts
|
|
1033
|
+
async function readEasJson(path = "eas.json") {
|
|
1034
|
+
const file = Bun.file(path);
|
|
1035
|
+
if (!await file.exists())
|
|
1036
|
+
return null;
|
|
1037
|
+
const text = await file.text();
|
|
1038
|
+
let value;
|
|
1039
|
+
try {
|
|
1040
|
+
value = JSON.parse(text);
|
|
1041
|
+
} catch (error) {
|
|
1042
|
+
const json5 = Bun.JSON5;
|
|
1043
|
+
if (!json5)
|
|
1044
|
+
throw error;
|
|
1045
|
+
value = json5.parse(text);
|
|
1046
|
+
}
|
|
1047
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1048
|
+
throw new Error(`${path} must hold an object`);
|
|
1049
|
+
}
|
|
1050
|
+
return value;
|
|
1051
|
+
}
|
|
1052
|
+
function asRecord(value) {
|
|
1053
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
|
|
1054
|
+
}
|
|
1055
|
+
function merge(base, update) {
|
|
1056
|
+
const result = { ...base, ...update };
|
|
1057
|
+
for (const key of ["env", "ios", "android"]) {
|
|
1058
|
+
const a = asRecord(base[key]);
|
|
1059
|
+
const b = asRecord(update[key]);
|
|
1060
|
+
if (a && b)
|
|
1061
|
+
result[key] = key === "env" ? { ...a, ...b } : merge(a, b);
|
|
1062
|
+
}
|
|
1063
|
+
return result;
|
|
1064
|
+
}
|
|
1065
|
+
function resolveRaw(easJson, name, depth) {
|
|
1066
|
+
if (depth >= 5) {
|
|
1067
|
+
throw new Error('eas.json: too long a chain of "extends"; check the build profiles for a cycle');
|
|
1068
|
+
}
|
|
1069
|
+
const profile = asRecord(easJson.build?.[name]);
|
|
1070
|
+
if (!profile) {
|
|
1071
|
+
if (depth === 0)
|
|
1072
|
+
return null;
|
|
1073
|
+
throw new Error(`eas.json: build profile extends missing "${name}"`);
|
|
1074
|
+
}
|
|
1075
|
+
const { extends: parent, ...rest } = profile;
|
|
1076
|
+
return typeof parent === "string" ? merge(resolveRaw(easJson, parent, depth + 1) ?? {}, rest) : rest;
|
|
1077
|
+
}
|
|
1078
|
+
function resolveBuildProfile(easJson, name, platform) {
|
|
1079
|
+
const raw = resolveRaw(easJson, name, 0);
|
|
1080
|
+
if (!raw)
|
|
1081
|
+
return null;
|
|
1082
|
+
const { ios, android, ...base } = raw;
|
|
1083
|
+
const resolved = merge(base, asRecord(platform === "ios" ? ios : android) ?? {});
|
|
1084
|
+
const text = (key) => typeof resolved[key] === "string" && resolved[key] !== "" ? resolved[key] : undefined;
|
|
1085
|
+
const env = asRecord(resolved.env);
|
|
1086
|
+
return {
|
|
1087
|
+
channel: text("channel"),
|
|
1088
|
+
environment: text("environment"),
|
|
1089
|
+
node: text("node"),
|
|
1090
|
+
bun: text("bun"),
|
|
1091
|
+
...env ? {
|
|
1092
|
+
env: Object.fromEntries(Object.entries(env).map(([key, value]) => [key, String(value)]))
|
|
1093
|
+
} : {}
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
function applyProfileEnv(profile, env = process.env) {
|
|
1097
|
+
const applied = [];
|
|
1098
|
+
for (const [key, value] of Object.entries(profile?.env ?? {})) {
|
|
1099
|
+
if (env[key] !== undefined)
|
|
1100
|
+
continue;
|
|
1101
|
+
env[key] = value;
|
|
1102
|
+
applied.push(key);
|
|
1103
|
+
}
|
|
1104
|
+
return applied;
|
|
525
1105
|
}
|
|
526
1106
|
|
|
527
1107
|
// src/cli.ts
|
|
528
|
-
|
|
1108
|
+
var inGitHubActions = process.env.GITHUB_ACTIONS === "true";
|
|
1109
|
+
if (process.env.RUNNER_ENVIRONMENT === "self-hosted" && process.env.RUNNER_TEMP && !process.env.TMPDIR) {
|
|
1110
|
+
process.env.TMPDIR = process.env.RUNNER_TEMP;
|
|
1111
|
+
}
|
|
1112
|
+
var current = null;
|
|
1113
|
+
async function prepare(command, ctx) {
|
|
1114
|
+
const platform = ctx.context.platform;
|
|
1115
|
+
const easJson = await readEasJson();
|
|
1116
|
+
const buildProfile = easJson ? resolveBuildProfile(easJson, ctx.context.profile, platform) : null;
|
|
1117
|
+
const applied = applyProfileEnv(buildProfile);
|
|
1118
|
+
if (applied.length > 0) {
|
|
1119
|
+
process.stderr.write(`[expo-release] env from eas.json build.${ctx.context.profile}: ${applied.join(", ")}
|
|
1120
|
+
`);
|
|
1121
|
+
}
|
|
1122
|
+
const sdk = await readExpoSdkMajor().catch(() => null);
|
|
1123
|
+
const defaults = buildProfile && (sdk === null || sdk < 55) ? { ...buildProfile, environment: undefined } : buildProfile;
|
|
1124
|
+
current = {
|
|
1125
|
+
options: toReleaseOptions(command, ctx, defaults),
|
|
1126
|
+
easJson,
|
|
1127
|
+
buildProfile
|
|
1128
|
+
};
|
|
1129
|
+
return current;
|
|
1130
|
+
}
|
|
1131
|
+
function doctorInput(prepared, full) {
|
|
1132
|
+
const { options } = prepared;
|
|
1133
|
+
return {
|
|
1134
|
+
profile: options.profile,
|
|
1135
|
+
platform: options.platform,
|
|
1136
|
+
environment: options.environment,
|
|
1137
|
+
autoSubmit: options.autoSubmit,
|
|
1138
|
+
easJson: prepared.easJson,
|
|
1139
|
+
buildProfile: prepared.buildProfile,
|
|
1140
|
+
full
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
function toDecisionInput(prepared) {
|
|
1144
|
+
const { options } = prepared;
|
|
529
1145
|
return {
|
|
530
1146
|
base: options.base,
|
|
531
1147
|
head: options.head,
|
|
532
1148
|
profile: options.profile,
|
|
533
1149
|
platform: options.platform,
|
|
534
1150
|
impactPaths: options.impactPaths,
|
|
535
|
-
ignoreExpoImpact: shouldIgnoreExpoImpact(options)
|
|
1151
|
+
ignoreExpoImpact: shouldIgnoreExpoImpact(options),
|
|
1152
|
+
environment: options.environment,
|
|
1153
|
+
preflight: async () => {
|
|
1154
|
+
const findings = await runDoctor(doctorInput(prepared, false)).catch(() => []);
|
|
1155
|
+
if (findings.length > 0) {
|
|
1156
|
+
process.stderr.write(`${formatFindings(findings, { asWarnings: true, github: inGitHubActions })}
|
|
1157
|
+
`);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
536
1160
|
};
|
|
537
1161
|
}
|
|
538
1162
|
function toOperationInput(options, decision) {
|
|
@@ -555,52 +1179,150 @@ function toOperationInput(options, decision) {
|
|
|
555
1179
|
dryRun: options.dryRun
|
|
556
1180
|
};
|
|
557
1181
|
}
|
|
558
|
-
async function decide(
|
|
559
|
-
const decision = await decideRelease(toDecisionInput(
|
|
1182
|
+
async function decide(prepared, ctx) {
|
|
1183
|
+
const decision = await decideRelease(toDecisionInput(prepared));
|
|
560
1184
|
await writeDecideGitHubOutput(decision);
|
|
561
1185
|
ctx.stdout.write(`${JSON.stringify(decision)}
|
|
562
1186
|
`);
|
|
563
1187
|
return decision;
|
|
564
1188
|
}
|
|
565
1189
|
async function operate(options, decision, ctx) {
|
|
566
|
-
|
|
567
|
-
await
|
|
1190
|
+
let queued;
|
|
1191
|
+
const result = await operateRelease({
|
|
1192
|
+
...toOperationInput(options, decision),
|
|
1193
|
+
onQueuedBuild: (build) => {
|
|
1194
|
+
queued = build;
|
|
1195
|
+
}
|
|
1196
|
+
});
|
|
1197
|
+
await writeOperateGitHubOutput(result, queued);
|
|
568
1198
|
ctx.stdout.write(`${result}
|
|
569
1199
|
`);
|
|
570
1200
|
}
|
|
571
|
-
|
|
1201
|
+
function nextStep(stage, options) {
|
|
1202
|
+
const app = relative(process.env.GITHUB_WORKSPACE ?? process.cwd(), process.cwd());
|
|
1203
|
+
const from = app ? `From ${app}: ` : "";
|
|
1204
|
+
const { platform, profile } = options;
|
|
1205
|
+
switch (stage) {
|
|
1206
|
+
case "doctor":
|
|
1207
|
+
return "Fix the findings above; `expo-release doctor` runs the same checks.";
|
|
1208
|
+
case "diff":
|
|
1209
|
+
return "Check out with fetch-depth: 0 so both commits of the diff exist.";
|
|
1210
|
+
case "fingerprint":
|
|
1211
|
+
return `The app config or EAS project did not load. ${from}\`npx expo config\` and \`eas fingerprint:generate --platform ${platform} --build-profile ${profile}\` with the job's environment variables.`;
|
|
1212
|
+
case "build list":
|
|
1213
|
+
case "fingerprint diff":
|
|
1214
|
+
return `Check that EXPO_TOKEN can read this project. ${from}\`eas build:list --platform ${platform} --build-profile ${profile}\`.`;
|
|
1215
|
+
case "errored build":
|
|
1216
|
+
return "Fix the build failure on EAS, then run the workflow with operation force-native.";
|
|
1217
|
+
case "native build":
|
|
1218
|
+
return `A native build that fails before it is queued usually needs one interactive run. ${from}\`eas credentials:configure-build --platform ${platform} --profile ${profile}\`.`;
|
|
1219
|
+
case "ota export":
|
|
1220
|
+
case "ota update":
|
|
1221
|
+
return "Fix the bundle error above, then run the workflow with operation retry-ota.";
|
|
1222
|
+
case "pre-update":
|
|
1223
|
+
case "post-update":
|
|
1224
|
+
return `The ${stage} command failed; it is the workflow's own step.`;
|
|
1225
|
+
case "recovery":
|
|
1226
|
+
return "Check the group or runtime version passed to the recovery operation.";
|
|
1227
|
+
default: {
|
|
1228
|
+
const _exhaustive = stage;
|
|
1229
|
+
return _exhaustive;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
function escapeData(text) {
|
|
1234
|
+
return text.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll(`
|
|
1235
|
+
`, "%0A");
|
|
1236
|
+
}
|
|
1237
|
+
function escapeProperty(text) {
|
|
1238
|
+
return escapeData(text).replaceAll(":", "%3A").replaceAll(",", "%2C");
|
|
1239
|
+
}
|
|
1240
|
+
async function reportFailure(error, io) {
|
|
1241
|
+
const stage = error instanceof StageError ? error.stage : undefined;
|
|
1242
|
+
const options = current?.options;
|
|
1243
|
+
const said = error instanceof StageError ? error.tail : [];
|
|
1244
|
+
const next = stage && options ? nextStep(stage, options) : undefined;
|
|
1245
|
+
io.stderr.write(`${error.message}
|
|
1246
|
+
`);
|
|
1247
|
+
if (inGitHubActions) {
|
|
1248
|
+
const where = options ? ` (${options.profile}, ${options.platform})` : "";
|
|
1249
|
+
const title = `expo-release: ${stage ?? "release"} failed${where}`;
|
|
1250
|
+
const message = said.slice(-3).join(`
|
|
1251
|
+
`) || error.message;
|
|
1252
|
+
io.stderr.write(`::error title=${escapeProperty(title)}::${escapeData(message)}
|
|
1253
|
+
`);
|
|
1254
|
+
}
|
|
1255
|
+
await appendGitHubOutput({
|
|
1256
|
+
result: "failed",
|
|
1257
|
+
failed_stage: stage ?? "",
|
|
1258
|
+
failure: [
|
|
1259
|
+
said.length > 0 ? `EAS said:
|
|
1260
|
+
${said.join(`
|
|
1261
|
+
`)}` : error.message,
|
|
1262
|
+
next ? `Next: ${next}` : ""
|
|
1263
|
+
].filter(Boolean).join(`
|
|
1264
|
+
`)
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
572
1267
|
var cli = defineCli({
|
|
573
1268
|
name: "expo-release",
|
|
574
1269
|
description: "Fingerprint-driven Expo EAS release: OTA update or native store build",
|
|
575
1270
|
globals: releaseFlags,
|
|
576
1271
|
context: (flags) => flags,
|
|
1272
|
+
defaultCommand: (env) => env.EXPO_RELEASE_COMMAND?.trim() || undefined,
|
|
1273
|
+
onError: async (error, io) => {
|
|
1274
|
+
if (error instanceof CliUsageError) {
|
|
1275
|
+
if (error.kind === "missing-command" && process.argv.length <= 2) {
|
|
1276
|
+
io.stdout.write(error.help);
|
|
1277
|
+
return 0;
|
|
1278
|
+
}
|
|
1279
|
+
const code = io.print();
|
|
1280
|
+
return code === 2 ? 1 : code;
|
|
1281
|
+
}
|
|
1282
|
+
if (error instanceof Error) {
|
|
1283
|
+
await reportFailure(error, io);
|
|
1284
|
+
return 1;
|
|
1285
|
+
}
|
|
1286
|
+
return io.print();
|
|
1287
|
+
},
|
|
577
1288
|
commands: {
|
|
578
1289
|
release: defineCommand({
|
|
579
1290
|
description: "Run decide, then operate on its decision",
|
|
580
1291
|
run: async (_input, ctx) => {
|
|
581
|
-
const
|
|
582
|
-
const decision = needsDecide(options.operation) ? await decide(
|
|
583
|
-
await operate(options, decision, ctx);
|
|
1292
|
+
const prepared = await prepare("release", ctx);
|
|
1293
|
+
const decision = needsDecide(prepared.options.operation) ? await decide(prepared, ctx) : null;
|
|
1294
|
+
await operate(prepared.options, decision, ctx);
|
|
584
1295
|
}
|
|
585
1296
|
}),
|
|
586
1297
|
decide: defineCommand({
|
|
587
|
-
description: "Print whether this change needs no deploy, an OTA update or a native build",
|
|
1298
|
+
description: "Print whether this change needs no deploy, an OTA update or a native build, and why",
|
|
588
1299
|
run: async (_input, ctx) => {
|
|
589
|
-
await decide(
|
|
1300
|
+
await decide(await prepare("decide", ctx), ctx);
|
|
590
1301
|
}
|
|
591
1302
|
}),
|
|
592
1303
|
operate: defineCommand({
|
|
593
1304
|
description: "Run --operation for --deploy-type",
|
|
594
1305
|
run: async (_input, ctx) => {
|
|
595
|
-
await operate(
|
|
1306
|
+
await operate((await prepare("operate", ctx)).options, null, ctx);
|
|
1307
|
+
}
|
|
1308
|
+
}),
|
|
1309
|
+
doctor: defineCommand({
|
|
1310
|
+
description: "Check the EAS setup, the app config and the committed files; exit 1 on an error",
|
|
1311
|
+
run: async (_input, ctx) => {
|
|
1312
|
+
const prepared = await prepare("doctor", ctx);
|
|
1313
|
+
const findings = await runDoctor(doctorInput(prepared, true));
|
|
1314
|
+
if (findings.length === 0) {
|
|
1315
|
+
ctx.stdout.write(`No problems found.
|
|
1316
|
+
`);
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
ctx.stdout.write(`${formatFindings(findings, { github: inGitHubActions })}
|
|
1320
|
+
`);
|
|
1321
|
+
if (findings.some((finding) => finding.level === "error")) {
|
|
1322
|
+
throw new StageError("doctor", `${findings.filter((f) => f.level === "error").length} doctor check(s) failed.`);
|
|
1323
|
+
}
|
|
596
1324
|
}
|
|
597
1325
|
})
|
|
598
1326
|
}
|
|
599
1327
|
});
|
|
600
|
-
|
|
601
|
-
var argv = process.argv.slice(2);
|
|
602
|
-
if (!argv.some((arg) => COMMANDS.includes(arg))) {
|
|
603
|
-
argv = env.EXPO_RELEASE_COMMAND ? [env.EXPO_RELEASE_COMMAND, ...argv] : argv.length === 0 ? ["--help"] : argv;
|
|
604
|
-
}
|
|
605
|
-
var code = await cli.run(argv, { env });
|
|
606
|
-
process.exitCode = code === 2 ? 1 : code;
|
|
1328
|
+
process.exitCode = await cli.run();
|