expo-release 2.3.2 → 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 +4 -2
- package/dist/cli.js +848 -131
- 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 +401 -113
- 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 +13 -8
- package/src/cli.ts +224 -26
- 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/index.js
CHANGED
|
@@ -7,7 +7,7 @@ var trimmed = (schema) => z.preprocess((value) => typeof value === "string" ? va
|
|
|
7
7
|
var trueFalse = (env, fallback) => trimmed(z.enum(["true", "false"])).default(fallback).transform((value) => value === "true").meta({ env: `EXPO_RELEASE_${env}` });
|
|
8
8
|
var releaseFlags = z.object({
|
|
9
9
|
profile: z.string().trim().min(1).meta({ env: "EXPO_RELEASE_PROFILE" }).describe("EAS build profile, also the default update channel"),
|
|
10
|
-
base: text("BASE").
|
|
10
|
+
base: text("BASE").describe("Git base SHA for the impact diff (default: the merge base on a pull request, else github.event.before)"),
|
|
11
11
|
head: text("HEAD").meta({ env: ["EXPO_RELEASE_HEAD", "GITHUB_SHA"] }).describe("Git head SHA for the impact diff"),
|
|
12
12
|
platform: trimmed(z.enum(["ios", "android"])).default("ios").meta({ env: "EXPO_RELEASE_PLATFORM" }).describe("Native platform"),
|
|
13
13
|
impactPaths: z.string().default(DEFAULT_IMPACT_PATHS.join(",")).transform((value, ctx) => {
|
|
@@ -28,8 +28,8 @@ var releaseFlags = z.object({
|
|
|
28
28
|
"rollback-embedded"
|
|
29
29
|
])).default("deploy").meta({ env: "EXPO_RELEASE_OPERATION" }).describe("What to run"),
|
|
30
30
|
deployType: trimmed(z.enum(["none", "ota", "native"])).default("none").meta({ env: "EXPO_RELEASE_DEPLOY_TYPE" }).describe("Decision type for operate"),
|
|
31
|
-
channel: text("CHANNEL").describe("EAS Update channel (default: the profile)"),
|
|
32
|
-
environment: text("ENVIRONMENT").describe("EAS environment for eas update (required for Expo SDK 55
|
|
31
|
+
channel: text("CHANNEL").describe("EAS Update channel (default: the eas.json profile channel, else the profile name)"),
|
|
32
|
+
environment: text("ENVIRONMENT").describe("EAS environment for eas update (required for Expo SDK 55+, where it defaults to the eas.json profile environment)"),
|
|
33
33
|
autoSubmit: trueFalse("AUTO_SUBMIT", "true").describe("Pass --auto-submit on native builds"),
|
|
34
34
|
preUpdate: text("PRE_UPDATE").describe("Shell command run before eas update"),
|
|
35
35
|
postUpdate: text("POST_UPDATE").describe("Shell command run after a successful eas update"),
|
|
@@ -48,11 +48,13 @@ var releaseFlags = z.object({
|
|
|
48
48
|
message: "required by --operation republish"
|
|
49
49
|
});
|
|
50
50
|
});
|
|
51
|
-
function toReleaseOptions(command, { context: flags }) {
|
|
51
|
+
function toReleaseOptions(command, { context: flags }, buildProfile = null) {
|
|
52
|
+
const environment = flags.environment ?? buildProfile?.environment;
|
|
52
53
|
return {
|
|
53
54
|
...flags,
|
|
54
55
|
command,
|
|
55
|
-
channel: flags.channel ?? flags.profile
|
|
56
|
+
channel: flags.channel ?? buildProfile?.channel ?? flags.profile,
|
|
57
|
+
...environment ? { environment } : {}
|
|
56
58
|
};
|
|
57
59
|
}
|
|
58
60
|
function needsDecide(operation) {
|
|
@@ -95,22 +97,119 @@ ${delimiter}`;
|
|
|
95
97
|
}
|
|
96
98
|
|
|
97
99
|
// src/run.ts
|
|
100
|
+
import { existsSync } from "fs";
|
|
101
|
+
import { dirname, join } from "path";
|
|
98
102
|
function easCommand(...args) {
|
|
99
103
|
const bin = process.env.EAS_BIN?.trim();
|
|
100
|
-
return [bin && bin.length > 0 ? bin : "eas", ...args];
|
|
104
|
+
return [bin && bin.length > 0 ? bin : localBin("eas") ?? "eas", ...args];
|
|
105
|
+
}
|
|
106
|
+
var localBins = new Map;
|
|
107
|
+
function localBin(name) {
|
|
108
|
+
const cwd = process.cwd();
|
|
109
|
+
const cacheKey = `${cwd}\x00${name}`;
|
|
110
|
+
const cached = localBins.get(cacheKey);
|
|
111
|
+
if (cached !== undefined)
|
|
112
|
+
return cached;
|
|
113
|
+
let bin = null;
|
|
114
|
+
for (let dir = cwd;; dir = dirname(dir)) {
|
|
115
|
+
const candidate = join(dir, "node_modules", ".bin", name);
|
|
116
|
+
if (existsSync(candidate)) {
|
|
117
|
+
bin = candidate;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
if (existsSync(join(dir, ".git")) || dirname(dir) === dir)
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
localBins.set(cacheKey, bin);
|
|
124
|
+
return bin;
|
|
101
125
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
126
|
+
|
|
127
|
+
class CommandError extends Error {
|
|
128
|
+
tail;
|
|
129
|
+
constructor(message, tail) {
|
|
130
|
+
super(message);
|
|
131
|
+
this.tail = tail;
|
|
132
|
+
this.name = "CommandError";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
var ERROR_TAIL_LINES = 20;
|
|
136
|
+
function lastLines(output, count = ERROR_TAIL_LINES) {
|
|
137
|
+
return output.split(`
|
|
138
|
+
`).map((line) => line.trimEnd()).filter((line) => line.trim().length > 0).slice(-count);
|
|
139
|
+
}
|
|
140
|
+
var DRAIN_AFTER_EXIT_MS = 1000;
|
|
141
|
+
function collect(stream, echo, sink) {
|
|
142
|
+
const reader = stream.getReader();
|
|
143
|
+
const decoder = new TextDecoder;
|
|
144
|
+
let text = "";
|
|
145
|
+
const done = (async () => {
|
|
146
|
+
try {
|
|
147
|
+
for (;; ) {
|
|
148
|
+
const { value, done } = await reader.read();
|
|
149
|
+
if (done)
|
|
150
|
+
break;
|
|
151
|
+
echo?.write(value);
|
|
152
|
+
const part = decoder.decode(value, { stream: true });
|
|
153
|
+
text += part;
|
|
154
|
+
sink.push(part);
|
|
155
|
+
}
|
|
156
|
+
} catch {}
|
|
157
|
+
const rest = decoder.decode();
|
|
158
|
+
sink.push(rest);
|
|
159
|
+
text += rest;
|
|
160
|
+
})();
|
|
161
|
+
return {
|
|
162
|
+
done,
|
|
163
|
+
stop: () => reader.cancel().catch(() => {
|
|
164
|
+
return;
|
|
165
|
+
}),
|
|
166
|
+
text: () => text
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
async function spawn(command, echoStdout, echoStderr = true) {
|
|
170
|
+
const child = Bun.spawn(command, { stdout: "pipe", stderr: "pipe" });
|
|
171
|
+
const combined = [];
|
|
172
|
+
const out = collect(child.stdout, echoStdout ? process.stdout : null, combined);
|
|
173
|
+
const err = collect(child.stderr, echoStderr ? process.stderr : null, combined);
|
|
174
|
+
const exitCode = await child.exited;
|
|
175
|
+
const drained = Promise.all([out.done, err.done]);
|
|
176
|
+
let timer;
|
|
177
|
+
const timedOut = await Promise.race([
|
|
178
|
+
drained.then(() => false),
|
|
179
|
+
new Promise((resolve) => {
|
|
180
|
+
timer = setTimeout(() => resolve(true), DRAIN_AFTER_EXIT_MS);
|
|
181
|
+
})
|
|
111
182
|
]);
|
|
183
|
+
clearTimeout(timer);
|
|
184
|
+
if (timedOut) {
|
|
185
|
+
await Promise.all([out.stop(), err.stop()]);
|
|
186
|
+
await drained;
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
stdout: out.text(),
|
|
190
|
+
stderr: err.text(),
|
|
191
|
+
exitCode,
|
|
192
|
+
tail: lastLines(combined.join(""))
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
async function runCaptured(command) {
|
|
196
|
+
const { stdout, stderr, exitCode, tail } = await spawn(command, false);
|
|
112
197
|
if (exitCode !== 0) {
|
|
113
|
-
throw new
|
|
198
|
+
throw new CommandError(describeFailure(command, exitCode, { stderr, stdout }), tail);
|
|
199
|
+
}
|
|
200
|
+
return stdout;
|
|
201
|
+
}
|
|
202
|
+
async function runSilent(command) {
|
|
203
|
+
const { stdout, stderr, exitCode, tail } = await spawn(command, false, false);
|
|
204
|
+
if (exitCode !== 0) {
|
|
205
|
+
throw new CommandError(describeFailure(command, exitCode, { stderr, stdout }), tail);
|
|
206
|
+
}
|
|
207
|
+
return stdout;
|
|
208
|
+
}
|
|
209
|
+
async function runInherited(command) {
|
|
210
|
+
const { stdout, exitCode, tail } = await spawn(command, true);
|
|
211
|
+
if (exitCode !== 0) {
|
|
212
|
+
throw new CommandError(`Command failed with exit code ${exitCode}: ${quoteCommand(command)} (output above)`, tail);
|
|
114
213
|
}
|
|
115
214
|
return stdout;
|
|
116
215
|
}
|
|
@@ -143,21 +242,28 @@ ${tail(output.stdout)}`);
|
|
|
143
242
|
return sections.join(`
|
|
144
243
|
`);
|
|
145
244
|
}
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
245
|
+
function parseJsonDocument(output) {
|
|
246
|
+
const starts = [0];
|
|
247
|
+
for (const match of output.matchAll(/\n(?=[[{])/g)) {
|
|
248
|
+
starts.push(match.index + 1);
|
|
249
|
+
}
|
|
250
|
+
for (const start of starts) {
|
|
251
|
+
try {
|
|
252
|
+
return {
|
|
253
|
+
value: JSON.parse(output.slice(start)),
|
|
254
|
+
preamble: output.slice(0, start).trim()
|
|
255
|
+
};
|
|
256
|
+
} catch {}
|
|
151
257
|
}
|
|
152
|
-
|
|
258
|
+
throw new Error(`Expected JSON output, received:
|
|
259
|
+
${tail(output)}`);
|
|
153
260
|
}
|
|
154
261
|
function parseJson(output) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
262
|
+
const { value, preamble } = parseJsonDocument(output);
|
|
263
|
+
if (preamble)
|
|
264
|
+
process.stderr.write(`${preamble}
|
|
265
|
+
`);
|
|
266
|
+
return value;
|
|
161
267
|
}
|
|
162
268
|
function getRecord(value, context) {
|
|
163
269
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
@@ -166,7 +272,7 @@ function getRecord(value, context) {
|
|
|
166
272
|
return value;
|
|
167
273
|
}
|
|
168
274
|
function parseFingerprintHash(output) {
|
|
169
|
-
const record = getRecord(
|
|
275
|
+
const record = getRecord(parseJsonDocument(output).value, "EAS fingerprint");
|
|
170
276
|
if (typeof record.hash !== "string" || record.hash.length === 0) {
|
|
171
277
|
throw new Error("EAS fingerprint output did not contain a full hash");
|
|
172
278
|
}
|
|
@@ -179,8 +285,85 @@ function dryRunCommand(command) {
|
|
|
179
285
|
process.stderr.write(`[dry-run] ${quoteCommand(command)}
|
|
180
286
|
`);
|
|
181
287
|
}
|
|
182
|
-
|
|
183
|
-
|
|
288
|
+
|
|
289
|
+
class StageError extends Error {
|
|
290
|
+
stage;
|
|
291
|
+
tail;
|
|
292
|
+
constructor(stage, message, tail = []) {
|
|
293
|
+
super(message);
|
|
294
|
+
this.stage = stage;
|
|
295
|
+
this.tail = tail;
|
|
296
|
+
this.name = "StageError";
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async function stage(name, run) {
|
|
300
|
+
const started = performance.now();
|
|
301
|
+
try {
|
|
302
|
+
return await run();
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (error instanceof StageError)
|
|
305
|
+
throw error;
|
|
306
|
+
throw new StageError(name, error instanceof Error ? error.message : String(error), error instanceof CommandError ? error.tail : []);
|
|
307
|
+
} finally {
|
|
308
|
+
const seconds = ((performance.now() - started) / 1000).toFixed(1);
|
|
309
|
+
process.stderr.write(`[expo-release] ${name}: ${seconds}s
|
|
310
|
+
`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async function generateFingerprint(input, commandRunner, explainConfig) {
|
|
314
|
+
const output = await commandRunner(easCommand("fingerprint:generate", "--platform", input.platform, "--build-profile", input.profile, "--json", "--non-interactive"));
|
|
315
|
+
const { preamble } = parseJsonDocument(output);
|
|
316
|
+
if (preamble) {
|
|
317
|
+
const reason = explainConfig ? await explainConfig() : "";
|
|
318
|
+
throw new CommandError([
|
|
319
|
+
"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.",
|
|
320
|
+
`eas-cli printed:
|
|
321
|
+
${preamble}`,
|
|
322
|
+
reason && `expo config said:
|
|
323
|
+
${reason}`
|
|
324
|
+
].filter(Boolean).join(`
|
|
325
|
+
`), lastLines(reason || preamble));
|
|
326
|
+
}
|
|
327
|
+
return parseFingerprintHash(output);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/source-maps.ts
|
|
331
|
+
import { exists } from "fs/promises";
|
|
332
|
+
var SOURCE_MAP_OUTPUT_DIR = "dist";
|
|
333
|
+
function expoCommand(...args) {
|
|
334
|
+
const bin = process.env.EXPO_BIN?.trim();
|
|
335
|
+
if (bin && bin.length > 0)
|
|
336
|
+
return [bin, ...args];
|
|
337
|
+
const local = localBin("expo");
|
|
338
|
+
return local ? [local, ...args] : ["bunx", "expo", ...args];
|
|
339
|
+
}
|
|
340
|
+
function sourceMapExportCommand(platform) {
|
|
341
|
+
return expoCommand("export", "--platform", platform, "--output-dir", SOURCE_MAP_OUTPUT_DIR, "--source-maps", "external");
|
|
342
|
+
}
|
|
343
|
+
function mergeUpdateArgs(extra, exportSourceMaps) {
|
|
344
|
+
const args = extra ? [...extra] : [];
|
|
345
|
+
if (!exportSourceMaps)
|
|
346
|
+
return args;
|
|
347
|
+
if (!args.includes("--input-dir")) {
|
|
348
|
+
args.push("--input-dir", SOURCE_MAP_OUTPUT_DIR);
|
|
349
|
+
}
|
|
350
|
+
if (!args.includes("--skip-bundler")) {
|
|
351
|
+
args.push("--skip-bundler");
|
|
352
|
+
}
|
|
353
|
+
return args;
|
|
354
|
+
}
|
|
355
|
+
async function defaultHasSourceMaps(directory = SOURCE_MAP_OUTPUT_DIR) {
|
|
356
|
+
if (!await exists(directory))
|
|
357
|
+
return false;
|
|
358
|
+
for await (const _ of new Bun.Glob("**/*.map").scan(directory)) {
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
async function assertSourceMaps(hasSourceMaps = defaultHasSourceMaps) {
|
|
364
|
+
if (await hasSourceMaps())
|
|
365
|
+
return;
|
|
366
|
+
throw new Error("Expo export did not produce Hermes source maps in dist; refusing to publish an OTA update.");
|
|
184
367
|
}
|
|
185
368
|
|
|
186
369
|
// src/decide.ts
|
|
@@ -219,8 +402,8 @@ function normalizeBuildStatus(value) {
|
|
|
219
402
|
return null;
|
|
220
403
|
}
|
|
221
404
|
function dashboardBuildUrl(record) {
|
|
222
|
-
const ownerName = nestedString(record, "app", "ownerAccount", "name");
|
|
223
|
-
const slug = nestedString(record, "app", "slug");
|
|
405
|
+
const ownerName = nestedString(record, "project", "ownerAccount", "name") ?? nestedString(record, "app", "ownerAccount", "name");
|
|
406
|
+
const slug = nestedString(record, "project", "slug") ?? nestedString(record, "app", "slug");
|
|
224
407
|
if (typeof record.id !== "string" || !ownerName || !slug) {
|
|
225
408
|
return;
|
|
226
409
|
}
|
|
@@ -245,13 +428,17 @@ function parseBuildList(output) {
|
|
|
245
428
|
}
|
|
246
429
|
const buildUrl = dashboardBuildUrl(record);
|
|
247
430
|
const fingerprint = nestedString(record, "fingerprint", "hash");
|
|
248
|
-
const runtimeVersion = nestedString(record, "runtime", "version");
|
|
431
|
+
const runtimeVersion = nestedString(record, "runtimeVersion") ?? nestedString(record, "runtime", "version");
|
|
432
|
+
const errorCode = nestedString(record, "error", "errorCode");
|
|
433
|
+
const errorMessage = nestedString(record, "error", "message");
|
|
434
|
+
const error = [errorCode, errorMessage].filter(Boolean).join(": ");
|
|
249
435
|
builds.push({
|
|
250
436
|
id: record.id,
|
|
251
437
|
status,
|
|
252
438
|
...buildUrl ? { buildUrl } : {},
|
|
253
439
|
...fingerprint ? { fingerprint } : {},
|
|
254
|
-
...runtimeVersion ? { runtimeVersion } : {}
|
|
440
|
+
...runtimeVersion ? { runtimeVersion } : {},
|
|
441
|
+
...error ? { error } : {}
|
|
255
442
|
});
|
|
256
443
|
}
|
|
257
444
|
return builds;
|
|
@@ -268,26 +455,71 @@ function hasExpoImpact(paths, impactPaths = DEFAULT_IMPACT_PATHS) {
|
|
|
268
455
|
function resolveBuildDecision(builds) {
|
|
269
456
|
const finished = builds.find((build) => build.status === "finished");
|
|
270
457
|
if (finished) {
|
|
271
|
-
return {
|
|
458
|
+
return {
|
|
459
|
+
deployType: "ota",
|
|
460
|
+
reason: "finished-build",
|
|
461
|
+
nativeBuildPending: false,
|
|
462
|
+
build: finished
|
|
463
|
+
};
|
|
272
464
|
}
|
|
273
465
|
const inProgress = builds.find((build) => ACTIVE_BUILD_STATUSES.includes(build.status));
|
|
274
466
|
if (inProgress) {
|
|
275
467
|
return {
|
|
276
468
|
deployType: "ota",
|
|
469
|
+
reason: "build-in-progress",
|
|
277
470
|
nativeBuildPending: true,
|
|
278
471
|
build: inProgress
|
|
279
472
|
};
|
|
280
473
|
}
|
|
281
474
|
const newest = builds[0];
|
|
282
475
|
if (newest?.status === "errored") {
|
|
283
|
-
const
|
|
284
|
-
|
|
476
|
+
const lines = [
|
|
477
|
+
`The newest native build for this fingerprint failed on EAS${newest.error ? `: ${newest.error}` : ` (build ${newest.id})`}`,
|
|
478
|
+
newest.buildUrl,
|
|
479
|
+
"Fix it, then run the workflow with operation force-native."
|
|
480
|
+
].filter((line) => Boolean(line));
|
|
481
|
+
throw new StageError("errored build", lines.join(`
|
|
482
|
+
`), lines);
|
|
285
483
|
}
|
|
286
|
-
return {
|
|
484
|
+
return {
|
|
485
|
+
deployType: "native",
|
|
486
|
+
reason: "no-build-for-fingerprint",
|
|
487
|
+
nativeBuildPending: false,
|
|
488
|
+
build: null
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
async function readPullRequestBase() {
|
|
492
|
+
const path = process.env.GITHUB_EVENT_PATH;
|
|
493
|
+
if (!path)
|
|
494
|
+
return;
|
|
495
|
+
try {
|
|
496
|
+
const event = await Bun.file(path).json();
|
|
497
|
+
const sha = event.pull_request?.base?.sha;
|
|
498
|
+
return typeof sha === "string" && sha ? sha : undefined;
|
|
499
|
+
} catch {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
async function defaultBase(head, commandRunner) {
|
|
504
|
+
const event = process.env.GITHUB_EVENT_NAME;
|
|
505
|
+
if (event === "pull_request" || event === "pull_request_target") {
|
|
506
|
+
const base = await readPullRequestBase();
|
|
507
|
+
if (base && head) {
|
|
508
|
+
try {
|
|
509
|
+
const mergeBase = (await commandRunner(["git", "merge-base", base, head])).trim();
|
|
510
|
+
if (mergeBase)
|
|
511
|
+
return mergeBase;
|
|
512
|
+
} catch {}
|
|
513
|
+
}
|
|
514
|
+
if (base)
|
|
515
|
+
return base;
|
|
516
|
+
}
|
|
517
|
+
return process.env.GITHUB_EVENT_BEFORE?.trim() || undefined;
|
|
287
518
|
}
|
|
288
519
|
async function getChangedFiles(input, commandRunner) {
|
|
289
520
|
const head = input.head;
|
|
290
|
-
|
|
521
|
+
const base = input.base ?? await defaultBase(head, commandRunner);
|
|
522
|
+
if (!base || !head || /^0+$/.test(base)) {
|
|
291
523
|
return { files: [], unreachable: true };
|
|
292
524
|
}
|
|
293
525
|
try {
|
|
@@ -297,7 +529,7 @@ async function getChangedFiles(input, commandRunner) {
|
|
|
297
529
|
"diff.relative=false",
|
|
298
530
|
"diff",
|
|
299
531
|
"--name-only",
|
|
300
|
-
|
|
532
|
+
base,
|
|
301
533
|
head
|
|
302
534
|
]);
|
|
303
535
|
return {
|
|
@@ -312,25 +544,95 @@ async function getChangedFiles(input, commandRunner) {
|
|
|
312
544
|
async function getMatchingBuilds(input, fingerprint, commandRunner) {
|
|
313
545
|
return parseBuildList(await commandRunner(easCommand("build:list", "--platform", input.platform, "--build-profile", input.profile, "--fingerprint-hash", fingerprint, "--limit", "20", "--json", "--non-interactive")));
|
|
314
546
|
}
|
|
547
|
+
function fingerprintSources(value) {
|
|
548
|
+
const sources = value?.sources;
|
|
549
|
+
const map = new Map;
|
|
550
|
+
if (!Array.isArray(sources))
|
|
551
|
+
return map;
|
|
552
|
+
for (const source of sources) {
|
|
553
|
+
const key = source.type === "contents" ? source.id : source.filePath ?? source.id;
|
|
554
|
+
if (typeof key === "string")
|
|
555
|
+
map.set(key, source.hash ?? null);
|
|
556
|
+
}
|
|
557
|
+
return map;
|
|
558
|
+
}
|
|
559
|
+
var MAX_DIFF_SOURCES = 30;
|
|
560
|
+
function diffFingerprintSources(compare) {
|
|
561
|
+
const record = getRecord(compare, "EAS fingerprint:compare");
|
|
562
|
+
const before = fingerprintSources(record.fingerprint1);
|
|
563
|
+
const after = fingerprintSources(record.fingerprint2);
|
|
564
|
+
const lines = [];
|
|
565
|
+
for (const [key, hash] of after) {
|
|
566
|
+
if (!before.has(key))
|
|
567
|
+
lines.push(`added ${key}`);
|
|
568
|
+
else if (before.get(key) !== hash)
|
|
569
|
+
lines.push(`changed ${key}`);
|
|
570
|
+
}
|
|
571
|
+
for (const key of before.keys()) {
|
|
572
|
+
if (!after.has(key))
|
|
573
|
+
lines.push(`removed ${key}`);
|
|
574
|
+
}
|
|
575
|
+
if (lines.length <= MAX_DIFF_SOURCES)
|
|
576
|
+
return lines;
|
|
577
|
+
return [
|
|
578
|
+
...lines.slice(0, MAX_DIFF_SOURCES),
|
|
579
|
+
`... and ${lines.length - MAX_DIFF_SOURCES} more`
|
|
580
|
+
];
|
|
581
|
+
}
|
|
582
|
+
async function explainNativeDecision(input, commandRunner) {
|
|
583
|
+
try {
|
|
584
|
+
return await stage("fingerprint diff", async () => {
|
|
585
|
+
const [last] = parseBuildList(await commandRunner(easCommand("build:list", "--platform", input.platform, "--build-profile", input.profile, "--status", "finished", "--limit", "1", "--json", "--non-interactive")));
|
|
586
|
+
if (!last)
|
|
587
|
+
return null;
|
|
588
|
+
const command = easCommand("fingerprint:compare", "--build-id", last.id, "--json", "--non-interactive");
|
|
589
|
+
if (input.environment)
|
|
590
|
+
command.push("--environment", input.environment);
|
|
591
|
+
const { value } = parseJsonDocument(await commandRunner(command));
|
|
592
|
+
return {
|
|
593
|
+
buildId: last.id,
|
|
594
|
+
...last.buildUrl ? { buildUrl: last.buildUrl } : {},
|
|
595
|
+
sources: diffFingerprintSources(value)
|
|
596
|
+
};
|
|
597
|
+
});
|
|
598
|
+
} catch (error) {
|
|
599
|
+
process.stderr.write(`Warning: could not compare the fingerprint with the last finished build: ${error instanceof Error ? error.message : String(error)}
|
|
600
|
+
`);
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
async function explainConfigFailure(commandRunner) {
|
|
605
|
+
try {
|
|
606
|
+
await commandRunner(expoCommand("config", "--json"));
|
|
607
|
+
return "";
|
|
608
|
+
} catch (error) {
|
|
609
|
+
return error instanceof Error ? error.message : String(error);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
315
612
|
async function decideRelease(input, commandRunner = runCaptured) {
|
|
316
|
-
const { files: changedFiles, unreachable } = await getChangedFiles(input, commandRunner);
|
|
613
|
+
const { files: changedFiles, unreachable } = await stage("diff", () => getChangedFiles(input, commandRunner));
|
|
317
614
|
if (!input.ignoreExpoImpact && !unreachable && !hasExpoImpact(changedFiles, input.impactPaths)) {
|
|
318
615
|
return {
|
|
319
616
|
deployType: "none",
|
|
617
|
+
reason: "no-impact-path",
|
|
320
618
|
nativeBuildPending: false,
|
|
321
619
|
changedFiles,
|
|
322
620
|
fingerprint: null,
|
|
323
621
|
build: null
|
|
324
622
|
};
|
|
325
623
|
}
|
|
326
|
-
|
|
327
|
-
const
|
|
624
|
+
await input.preflight?.();
|
|
625
|
+
const fingerprint = await stage("fingerprint", () => generateFingerprint(input, commandRunner, () => explainConfigFailure(commandRunner)));
|
|
626
|
+
const builds = await stage("build list", () => getMatchingBuilds(input, fingerprint, commandRunner));
|
|
627
|
+
const resolved = resolveBuildDecision(builds);
|
|
328
628
|
return {
|
|
329
629
|
deployType: resolved.deployType,
|
|
630
|
+
reason: resolved.reason,
|
|
330
631
|
nativeBuildPending: resolved.nativeBuildPending,
|
|
331
632
|
changedFiles,
|
|
332
633
|
fingerprint,
|
|
333
|
-
build: resolved.build
|
|
634
|
+
build: resolved.build,
|
|
635
|
+
...resolved.deployType === "native" ? { fingerprintDiff: await explainNativeDecision(input, commandRunner) } : {}
|
|
334
636
|
};
|
|
335
637
|
}
|
|
336
638
|
async function writeDecideGitHubOutput(decision) {
|
|
@@ -340,51 +642,21 @@ async function writeDecideGitHubOutput(decision) {
|
|
|
340
642
|
changed: String(decision.deployType !== "none"),
|
|
341
643
|
fingerprint: decision.fingerprint ?? "",
|
|
342
644
|
build_id: decision.build?.id ?? "",
|
|
343
|
-
build_url: decision.build?.buildUrl ?? ""
|
|
645
|
+
build_url: decision.build?.buildUrl ?? "",
|
|
646
|
+
reason: decision.reason,
|
|
647
|
+
changed_sources: decision.fingerprintDiff?.sources.join(`
|
|
648
|
+
`) ?? "",
|
|
649
|
+
compared_build_url: decision.fingerprintDiff?.buildUrl ?? decision.fingerprintDiff?.buildId ?? ""
|
|
344
650
|
});
|
|
345
651
|
}
|
|
346
|
-
// src/
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if (bin && bin.length > 0)
|
|
352
|
-
return [bin, ...args];
|
|
353
|
-
return ["bunx", "expo", ...args];
|
|
354
|
-
}
|
|
355
|
-
function sourceMapExportCommand(platform) {
|
|
356
|
-
return expoCommand("export", "--platform", platform, "--output-dir", SOURCE_MAP_OUTPUT_DIR, "--source-maps", "external");
|
|
357
|
-
}
|
|
358
|
-
function mergeUpdateArgs(extra, exportSourceMaps) {
|
|
359
|
-
const args = extra ? [...extra] : [];
|
|
360
|
-
if (!exportSourceMaps)
|
|
361
|
-
return args;
|
|
362
|
-
if (!args.includes("--input-dir")) {
|
|
363
|
-
args.push("--input-dir", SOURCE_MAP_OUTPUT_DIR);
|
|
364
|
-
}
|
|
365
|
-
if (!args.includes("--skip-bundler")) {
|
|
366
|
-
args.push("--skip-bundler");
|
|
652
|
+
// src/operate.ts
|
|
653
|
+
function splitArgs(value) {
|
|
654
|
+
const args = [];
|
|
655
|
+
for (const match of (value ?? "").matchAll(/"((?:\\.|[^"\\])*)"|'([^']*)'|(\S+)/g)) {
|
|
656
|
+
args.push(match[1]?.replace(/\\(.)/g, "$1") ?? match[2] ?? match[3] ?? "");
|
|
367
657
|
}
|
|
368
658
|
return args;
|
|
369
659
|
}
|
|
370
|
-
async function defaultHasSourceMaps(directory = SOURCE_MAP_OUTPUT_DIR) {
|
|
371
|
-
if (!await exists(directory))
|
|
372
|
-
return false;
|
|
373
|
-
for await (const _ of new Bun.Glob("**/*.map").scan(directory)) {
|
|
374
|
-
return true;
|
|
375
|
-
}
|
|
376
|
-
return false;
|
|
377
|
-
}
|
|
378
|
-
async function assertSourceMaps(hasSourceMaps = defaultHasSourceMaps) {
|
|
379
|
-
if (await hasSourceMaps())
|
|
380
|
-
return;
|
|
381
|
-
throw new Error("Expo export did not produce Hermes source maps in dist; refusing to publish an OTA update.");
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// src/operate.ts
|
|
385
|
-
function extraUpdateArgs(value) {
|
|
386
|
-
return value?.split(/\s+/).filter(Boolean) ?? [];
|
|
387
|
-
}
|
|
388
660
|
function updateMessage(input) {
|
|
389
661
|
if (input.message)
|
|
390
662
|
return input.message;
|
|
@@ -409,23 +681,25 @@ async function defaultReadPackageJson() {
|
|
|
409
681
|
return null;
|
|
410
682
|
return file.json();
|
|
411
683
|
}
|
|
684
|
+
async function readExpoSdkMajor(readPackageJson = defaultReadPackageJson) {
|
|
685
|
+
const pkg = await readPackageJson();
|
|
686
|
+
if (!pkg || typeof pkg !== "object")
|
|
687
|
+
return null;
|
|
688
|
+
const record = pkg;
|
|
689
|
+
return parseExpoSdkMajor(record.dependencies?.expo ?? record.devDependencies?.expo);
|
|
690
|
+
}
|
|
412
691
|
async function requireEnvironmentIfNeeded(input) {
|
|
413
692
|
if (input.environment)
|
|
414
693
|
return;
|
|
415
|
-
const
|
|
416
|
-
if (!pkg || typeof pkg !== "object" || pkg === null)
|
|
417
|
-
return;
|
|
418
|
-
const record = pkg;
|
|
419
|
-
const version = record.dependencies?.expo ?? record.devDependencies?.expo;
|
|
420
|
-
const major = parseExpoSdkMajor(version);
|
|
694
|
+
const major = await readExpoSdkMajor(input.readPackageJson);
|
|
421
695
|
if (major !== null && major >= 55) {
|
|
422
|
-
throw new Error("EAS Update requires --environment for Expo SDK 55+. Pass --environment <name>.");
|
|
696
|
+
throw new Error("EAS Update requires --environment for Expo SDK 55+. Pass --environment <name> or set build.<profile>.environment in eas.json.");
|
|
423
697
|
}
|
|
424
698
|
}
|
|
425
|
-
async function runHook(command, commandRunner) {
|
|
699
|
+
async function runHook(name, command, commandRunner) {
|
|
426
700
|
if (!command)
|
|
427
701
|
return;
|
|
428
|
-
await commandRunner(["bash", "-c", command]);
|
|
702
|
+
await stage(name, () => commandRunner(["bash", "-c", command]));
|
|
429
703
|
}
|
|
430
704
|
function mutatingRunner(commandRunner, dryRun) {
|
|
431
705
|
return async (command) => {
|
|
@@ -439,33 +713,44 @@ function mutatingRunner(commandRunner, dryRun) {
|
|
|
439
713
|
async function publishUpdate(input, commandRunner) {
|
|
440
714
|
await requireEnvironmentIfNeeded(input);
|
|
441
715
|
if (input.exportSourceMaps) {
|
|
442
|
-
await
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
716
|
+
await stage("ota export", async () => {
|
|
717
|
+
await commandRunner(sourceMapExportCommand(input.platform));
|
|
718
|
+
if (input.dryRun !== true) {
|
|
719
|
+
await assertSourceMaps(input.hasSourceMaps);
|
|
720
|
+
}
|
|
721
|
+
});
|
|
446
722
|
}
|
|
447
|
-
await runHook(input.preUpdate, commandRunner);
|
|
723
|
+
await runHook("pre-update", input.preUpdate, commandRunner);
|
|
448
724
|
const command = easCommand("update", "--channel", input.channel, "--platform", input.platform, "--message", updateMessage(input), "--non-interactive");
|
|
449
725
|
if (input.environment) {
|
|
450
726
|
command.push("--environment", input.environment);
|
|
451
727
|
}
|
|
452
|
-
command.push(...mergeUpdateArgs(
|
|
453
|
-
await commandRunner(command);
|
|
454
|
-
await runHook(input.postUpdate, commandRunner);
|
|
728
|
+
command.push(...mergeUpdateArgs(splitArgs(input.updateExtraArgs), input.exportSourceMaps === true));
|
|
729
|
+
await stage("ota update", () => commandRunner(command));
|
|
730
|
+
await runHook("post-update", input.postUpdate, commandRunner);
|
|
455
731
|
}
|
|
456
732
|
async function queueNativeBuild(input, commandRunner) {
|
|
457
|
-
const command = easCommand("build", "--platform", input.platform, "--profile", input.profile, "--non-interactive", "--no-wait");
|
|
733
|
+
const command = easCommand("build", "--platform", input.platform, "--profile", input.profile, "--non-interactive", "--no-wait", "--json");
|
|
458
734
|
if (input.autoSubmit) {
|
|
459
735
|
command.push("--auto-submit");
|
|
460
736
|
}
|
|
461
|
-
await commandRunner(command);
|
|
737
|
+
const output = await stage("native build", () => commandRunner(command));
|
|
738
|
+
try {
|
|
739
|
+
const queued = output.trim() ? parseBuildList(output)[0] : undefined;
|
|
740
|
+
if (queued)
|
|
741
|
+
input.onQueuedBuild?.(queued);
|
|
742
|
+
} catch (error) {
|
|
743
|
+
process.stderr.write(`Warning: could not read the queued build from eas build --json: ${error instanceof Error ? error.message : String(error)}
|
|
744
|
+
`);
|
|
745
|
+
}
|
|
746
|
+
return "native-build";
|
|
462
747
|
}
|
|
463
748
|
async function resolveRuntimeVersion(input, commandRunner) {
|
|
464
749
|
if (input.runtimeVersion)
|
|
465
750
|
return input.runtimeVersion;
|
|
466
751
|
if (input.fingerprint)
|
|
467
752
|
return input.fingerprint;
|
|
468
|
-
return generateFingerprint(input, commandRunner);
|
|
753
|
+
return stage("fingerprint", () => generateFingerprint(input, commandRunner));
|
|
469
754
|
}
|
|
470
755
|
async function operateRelease(input, commandRunner = runInherited) {
|
|
471
756
|
const execute = mutatingRunner(commandRunner, input.dryRun === true);
|
|
@@ -474,19 +759,19 @@ async function operateRelease(input, commandRunner = runInherited) {
|
|
|
474
759
|
if (!input.group) {
|
|
475
760
|
throw new Error("Usage: --operation republish requires --group <update-group-id>");
|
|
476
761
|
}
|
|
762
|
+
const group = input.group;
|
|
477
763
|
await requireEnvironmentIfNeeded(input);
|
|
478
|
-
await execute(easCommand("update:republish", "--group",
|
|
764
|
+
await stage("recovery", () => execute(easCommand("update:republish", "--group", group, "--platform", input.platform, "--message", updateMessage(input), "--non-interactive")));
|
|
479
765
|
return "recovery";
|
|
480
766
|
}
|
|
481
767
|
case "rollback-embedded": {
|
|
482
768
|
await requireEnvironmentIfNeeded(input);
|
|
483
769
|
const runtimeVersion = await resolveRuntimeVersion(input, commandRunner);
|
|
484
|
-
await execute(easCommand("update:roll-back-to-embedded", "--channel", input.channel, "--platform", input.platform, "--runtime-version", runtimeVersion, "--message", updateMessage(input), "--non-interactive"));
|
|
770
|
+
await stage("recovery", () => execute(easCommand("update:roll-back-to-embedded", "--channel", input.channel, "--platform", input.platform, "--runtime-version", runtimeVersion, "--message", updateMessage(input), "--non-interactive")));
|
|
485
771
|
return "recovery";
|
|
486
772
|
}
|
|
487
773
|
case "force-native":
|
|
488
|
-
|
|
489
|
-
return "native-build";
|
|
774
|
+
return queueNativeBuild(input, execute);
|
|
490
775
|
case "retry-ota":
|
|
491
776
|
await publishUpdate(input, execute);
|
|
492
777
|
return "ota";
|
|
@@ -501,8 +786,7 @@ async function operateRelease(input, commandRunner = runInherited) {
|
|
|
501
786
|
case "none":
|
|
502
787
|
return "skipped";
|
|
503
788
|
case "native":
|
|
504
|
-
|
|
505
|
-
return "native-build";
|
|
789
|
+
return queueNativeBuild(input, execute);
|
|
506
790
|
case "ota":
|
|
507
791
|
await publishUpdate(input, execute);
|
|
508
792
|
return "ota";
|
|
@@ -512,8 +796,12 @@ async function operateRelease(input, commandRunner = runInherited) {
|
|
|
512
796
|
}
|
|
513
797
|
}
|
|
514
798
|
}
|
|
515
|
-
async function writeOperateGitHubOutput(result) {
|
|
516
|
-
await appendGitHubOutput({
|
|
799
|
+
async function writeOperateGitHubOutput(result, queuedBuild) {
|
|
800
|
+
await appendGitHubOutput({
|
|
801
|
+
result,
|
|
802
|
+
queued_build_id: queuedBuild?.id ?? "",
|
|
803
|
+
queued_build_url: queuedBuild?.buildUrl ?? ""
|
|
804
|
+
});
|
|
517
805
|
}
|
|
518
806
|
export {
|
|
519
807
|
DEFAULT_IMPACT_PATHS,
|