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/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").meta({ env: ["EXPO_RELEASE_BASE", "GITHUB_EVENT_BEFORE"] }).describe("Git base SHA for the impact diff"),
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
- async function runCaptured(command) {
103
- const child = Bun.spawn(command, {
104
- stdout: "pipe",
105
- stderr: "pipe"
106
- });
107
- const [stdout, stderr, exitCode] = await Promise.all([
108
- new Response(child.stdout).text(),
109
- new Response(child.stderr).text(),
110
- child.exited
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 Error(describeFailure(command, exitCode, { stderr, stdout }));
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
- async function runInherited(command) {
147
- const child = Bun.spawn(command, { stdout: "inherit", stderr: "inherit" });
148
- const exitCode = await child.exited;
149
- if (exitCode !== 0) {
150
- throw new Error(`Command failed with exit code ${exitCode}: ${quoteCommand(command)} (output above)`);
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
- return "";
258
+ throw new Error(`Expected JSON output, received:
259
+ ${tail(output)}`);
153
260
  }
154
261
  function parseJson(output) {
155
- try {
156
- return JSON.parse(output);
157
- } catch {
158
- throw new Error(`Expected JSON output, received:
159
- ${output}`);
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(parseJson(output), "EAS fingerprint");
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
- async function generateFingerprint(input, commandRunner) {
183
- return parseFingerprintHash(await commandRunner(easCommand("fingerprint:generate", "--platform", input.platform, "--build-profile", input.profile, "--json", "--non-interactive")));
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 { deployType: "ota", nativeBuildPending: false, build: finished };
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 location = newest.buildUrl ?? newest.id;
284
- throw new Error(`The most recent native build for this fingerprint failed (${location}). Fix the failure or use --operation force-native to queue another build.`);
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 { deployType: "native", nativeBuildPending: false, build: null };
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
- if (!input.base || !head || /^0+$/.test(input.base)) {
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
- input.base,
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
- const fingerprint = await generateFingerprint(input, commandRunner);
327
- const resolved = resolveBuildDecision(await getMatchingBuilds(input, fingerprint, commandRunner));
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/source-maps.ts
347
- import { exists } from "fs/promises";
348
- var SOURCE_MAP_OUTPUT_DIR = "dist";
349
- function expoCommand(...args) {
350
- const bin = process.env.EXPO_BIN?.trim();
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 pkg = await (input.readPackageJson ?? defaultReadPackageJson)();
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 commandRunner(sourceMapExportCommand(input.platform));
443
- if (input.dryRun !== true) {
444
- await assertSourceMaps(input.hasSourceMaps);
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(extraUpdateArgs(input.updateExtraArgs), input.exportSourceMaps === true));
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", input.group, "--platform", input.platform, "--message", updateMessage(input), "--non-interactive"));
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
- await queueNativeBuild(input, execute);
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
- await queueNativeBuild(input, execute);
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({ result });
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,