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/index.js
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
var DEFAULT_IMPACT_PATHS = ["apps/expo/"];
|
|
5
5
|
var text = (env) => z.string().transform((value) => value.trim() || undefined).optional().meta({ env: `EXPO_RELEASE_${env}` });
|
|
6
|
-
var
|
|
6
|
+
var trimmed = (schema) => z.preprocess((value) => typeof value === "string" ? value.trim() : value, schema);
|
|
7
|
+
var trueFalse = (env, fallback) => trimmed(z.enum(["true", "false"])).default(fallback).transform((value) => value === "true").meta({ env: `EXPO_RELEASE_${env}` });
|
|
7
8
|
var releaseFlags = z.object({
|
|
8
9
|
profile: z.string().trim().min(1).meta({ env: "EXPO_RELEASE_PROFILE" }).describe("EAS build profile, also the default update channel"),
|
|
9
|
-
base: text("BASE").describe("Git base SHA for the impact diff (
|
|
10
|
-
head: text("HEAD").describe("Git head SHA for the impact diff
|
|
11
|
-
platform: z.enum(["ios", "android"]).default("ios").meta({ env: "EXPO_RELEASE_PLATFORM" }).describe("Native platform"),
|
|
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
|
+
head: text("HEAD").meta({ env: ["EXPO_RELEASE_HEAD", "GITHUB_SHA"] }).describe("Git head SHA for the impact diff"),
|
|
12
|
+
platform: trimmed(z.enum(["ios", "android"])).default("ios").meta({ env: "EXPO_RELEASE_PLATFORM" }).describe("Native platform"),
|
|
12
13
|
impactPaths: z.string().default(DEFAULT_IMPACT_PATHS.join(",")).transform((value, ctx) => {
|
|
13
14
|
const paths = value.split(",").map((path) => path.trim()).filter(Boolean);
|
|
14
15
|
if (paths.length === 0)
|
|
@@ -19,16 +20,16 @@ var releaseFlags = z.object({
|
|
|
19
20
|
return paths;
|
|
20
21
|
}).meta({ env: "EXPO_RELEASE_IMPACT_PATHS" }).describe("Comma-separated repo-root paths that count as an Expo change"),
|
|
21
22
|
ignoreExpoImpact: trueFalse("IGNORE_EXPO_IMPACT", "false").describe("Check the fingerprint even when no impact path changed"),
|
|
22
|
-
operation: z.enum([
|
|
23
|
+
operation: trimmed(z.enum([
|
|
23
24
|
"deploy",
|
|
24
25
|
"force-native",
|
|
25
26
|
"retry-ota",
|
|
26
27
|
"republish",
|
|
27
28
|
"rollback-embedded"
|
|
28
|
-
]).default("deploy").meta({ env: "EXPO_RELEASE_OPERATION" }).describe("What to run"),
|
|
29
|
-
deployType: z.enum(["none", "ota", "native"]).default("none").meta({ env: "EXPO_RELEASE_DEPLOY_TYPE" }).describe("Decision type for operate"),
|
|
30
|
-
channel: text("CHANNEL").describe("EAS Update channel (default: the profile)"),
|
|
31
|
-
environment: text("ENVIRONMENT").describe("EAS environment for eas update (required for Expo SDK 55
|
|
29
|
+
])).default("deploy").meta({ env: "EXPO_RELEASE_OPERATION" }).describe("What to run"),
|
|
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 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)"),
|
|
32
33
|
autoSubmit: trueFalse("AUTO_SUBMIT", "true").describe("Pass --auto-submit on native builds"),
|
|
33
34
|
preUpdate: text("PRE_UPDATE").describe("Shell command run before eas update"),
|
|
34
35
|
postUpdate: text("POST_UPDATE").describe("Shell command run after a successful eas update"),
|
|
@@ -38,7 +39,7 @@ var releaseFlags = z.object({
|
|
|
38
39
|
runtimeVersion: text("RUNTIME_VERSION").describe("Runtime version for rollback-embedded (default: the fingerprint)"),
|
|
39
40
|
message: text("MESSAGE").describe("Override the eas update message"),
|
|
40
41
|
fingerprint: text("FINGERPRINT").describe("Fingerprint hash to use instead of generating one"),
|
|
41
|
-
dryRun: z.boolean().meta({ env: "EXPO_RELEASE_DRY_RUN" }).describe("Print mutating EAS commands without running them")
|
|
42
|
+
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")
|
|
42
43
|
}).superRefine((flags, ctx) => {
|
|
43
44
|
if (flags.operation === "republish" && !flags.group)
|
|
44
45
|
ctx.addIssue({
|
|
@@ -47,13 +48,13 @@ var releaseFlags = z.object({
|
|
|
47
48
|
message: "required by --operation republish"
|
|
48
49
|
});
|
|
49
50
|
});
|
|
50
|
-
function toReleaseOptions(command, { context: flags,
|
|
51
|
+
function toReleaseOptions(command, { context: flags }, buildProfile = null) {
|
|
52
|
+
const environment = flags.environment ?? buildProfile?.environment;
|
|
51
53
|
return {
|
|
52
54
|
...flags,
|
|
53
55
|
command,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
channel: flags.channel ?? flags.profile
|
|
56
|
+
channel: flags.channel ?? buildProfile?.channel ?? flags.profile,
|
|
57
|
+
...environment ? { environment } : {}
|
|
57
58
|
};
|
|
58
59
|
}
|
|
59
60
|
function needsDecide(operation) {
|
|
@@ -96,22 +97,119 @@ ${delimiter}`;
|
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
// src/run.ts
|
|
100
|
+
import { existsSync } from "fs";
|
|
101
|
+
import { dirname, join } from "path";
|
|
99
102
|
function easCommand(...args) {
|
|
100
103
|
const bin = process.env.EAS_BIN?.trim();
|
|
101
|
-
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;
|
|
102
125
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
+
})
|
|
112
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);
|
|
113
197
|
if (exitCode !== 0) {
|
|
114
|
-
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);
|
|
115
213
|
}
|
|
116
214
|
return stdout;
|
|
117
215
|
}
|
|
@@ -144,21 +242,28 @@ ${tail(output.stdout)}`);
|
|
|
144
242
|
return sections.join(`
|
|
145
243
|
`);
|
|
146
244
|
}
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
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 {}
|
|
152
257
|
}
|
|
153
|
-
|
|
258
|
+
throw new Error(`Expected JSON output, received:
|
|
259
|
+
${tail(output)}`);
|
|
154
260
|
}
|
|
155
261
|
function parseJson(output) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
262
|
+
const { value, preamble } = parseJsonDocument(output);
|
|
263
|
+
if (preamble)
|
|
264
|
+
process.stderr.write(`${preamble}
|
|
265
|
+
`);
|
|
266
|
+
return value;
|
|
162
267
|
}
|
|
163
268
|
function getRecord(value, context) {
|
|
164
269
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
@@ -167,7 +272,7 @@ function getRecord(value, context) {
|
|
|
167
272
|
return value;
|
|
168
273
|
}
|
|
169
274
|
function parseFingerprintHash(output) {
|
|
170
|
-
const record = getRecord(
|
|
275
|
+
const record = getRecord(parseJsonDocument(output).value, "EAS fingerprint");
|
|
171
276
|
if (typeof record.hash !== "string" || record.hash.length === 0) {
|
|
172
277
|
throw new Error("EAS fingerprint output did not contain a full hash");
|
|
173
278
|
}
|
|
@@ -180,8 +285,85 @@ function dryRunCommand(command) {
|
|
|
180
285
|
process.stderr.write(`[dry-run] ${quoteCommand(command)}
|
|
181
286
|
`);
|
|
182
287
|
}
|
|
183
|
-
|
|
184
|
-
|
|
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.");
|
|
185
367
|
}
|
|
186
368
|
|
|
187
369
|
// src/decide.ts
|
|
@@ -220,8 +402,8 @@ function normalizeBuildStatus(value) {
|
|
|
220
402
|
return null;
|
|
221
403
|
}
|
|
222
404
|
function dashboardBuildUrl(record) {
|
|
223
|
-
const ownerName = nestedString(record, "app", "ownerAccount", "name");
|
|
224
|
-
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");
|
|
225
407
|
if (typeof record.id !== "string" || !ownerName || !slug) {
|
|
226
408
|
return;
|
|
227
409
|
}
|
|
@@ -246,13 +428,17 @@ function parseBuildList(output) {
|
|
|
246
428
|
}
|
|
247
429
|
const buildUrl = dashboardBuildUrl(record);
|
|
248
430
|
const fingerprint = nestedString(record, "fingerprint", "hash");
|
|
249
|
-
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(": ");
|
|
250
435
|
builds.push({
|
|
251
436
|
id: record.id,
|
|
252
437
|
status,
|
|
253
438
|
...buildUrl ? { buildUrl } : {},
|
|
254
439
|
...fingerprint ? { fingerprint } : {},
|
|
255
|
-
...runtimeVersion ? { runtimeVersion } : {}
|
|
440
|
+
...runtimeVersion ? { runtimeVersion } : {},
|
|
441
|
+
...error ? { error } : {}
|
|
256
442
|
});
|
|
257
443
|
}
|
|
258
444
|
return builds;
|
|
@@ -269,26 +455,71 @@ function hasExpoImpact(paths, impactPaths = DEFAULT_IMPACT_PATHS) {
|
|
|
269
455
|
function resolveBuildDecision(builds) {
|
|
270
456
|
const finished = builds.find((build) => build.status === "finished");
|
|
271
457
|
if (finished) {
|
|
272
|
-
return {
|
|
458
|
+
return {
|
|
459
|
+
deployType: "ota",
|
|
460
|
+
reason: "finished-build",
|
|
461
|
+
nativeBuildPending: false,
|
|
462
|
+
build: finished
|
|
463
|
+
};
|
|
273
464
|
}
|
|
274
465
|
const inProgress = builds.find((build) => ACTIVE_BUILD_STATUSES.includes(build.status));
|
|
275
466
|
if (inProgress) {
|
|
276
467
|
return {
|
|
277
468
|
deployType: "ota",
|
|
469
|
+
reason: "build-in-progress",
|
|
278
470
|
nativeBuildPending: true,
|
|
279
471
|
build: inProgress
|
|
280
472
|
};
|
|
281
473
|
}
|
|
282
474
|
const newest = builds[0];
|
|
283
475
|
if (newest?.status === "errored") {
|
|
284
|
-
const
|
|
285
|
-
|
|
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);
|
|
286
483
|
}
|
|
287
|
-
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;
|
|
288
518
|
}
|
|
289
519
|
async function getChangedFiles(input, commandRunner) {
|
|
290
520
|
const head = input.head;
|
|
291
|
-
|
|
521
|
+
const base = input.base ?? await defaultBase(head, commandRunner);
|
|
522
|
+
if (!base || !head || /^0+$/.test(base)) {
|
|
292
523
|
return { files: [], unreachable: true };
|
|
293
524
|
}
|
|
294
525
|
try {
|
|
@@ -298,7 +529,7 @@ async function getChangedFiles(input, commandRunner) {
|
|
|
298
529
|
"diff.relative=false",
|
|
299
530
|
"diff",
|
|
300
531
|
"--name-only",
|
|
301
|
-
|
|
532
|
+
base,
|
|
302
533
|
head
|
|
303
534
|
]);
|
|
304
535
|
return {
|
|
@@ -313,25 +544,95 @@ async function getChangedFiles(input, commandRunner) {
|
|
|
313
544
|
async function getMatchingBuilds(input, fingerprint, commandRunner) {
|
|
314
545
|
return parseBuildList(await commandRunner(easCommand("build:list", "--platform", input.platform, "--build-profile", input.profile, "--fingerprint-hash", fingerprint, "--limit", "20", "--json", "--non-interactive")));
|
|
315
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
|
+
}
|
|
316
612
|
async function decideRelease(input, commandRunner = runCaptured) {
|
|
317
|
-
const { files: changedFiles, unreachable } = await getChangedFiles(input, commandRunner);
|
|
613
|
+
const { files: changedFiles, unreachable } = await stage("diff", () => getChangedFiles(input, commandRunner));
|
|
318
614
|
if (!input.ignoreExpoImpact && !unreachable && !hasExpoImpact(changedFiles, input.impactPaths)) {
|
|
319
615
|
return {
|
|
320
616
|
deployType: "none",
|
|
617
|
+
reason: "no-impact-path",
|
|
321
618
|
nativeBuildPending: false,
|
|
322
619
|
changedFiles,
|
|
323
620
|
fingerprint: null,
|
|
324
621
|
build: null
|
|
325
622
|
};
|
|
326
623
|
}
|
|
327
|
-
|
|
328
|
-
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);
|
|
329
628
|
return {
|
|
330
629
|
deployType: resolved.deployType,
|
|
630
|
+
reason: resolved.reason,
|
|
331
631
|
nativeBuildPending: resolved.nativeBuildPending,
|
|
332
632
|
changedFiles,
|
|
333
633
|
fingerprint,
|
|
334
|
-
build: resolved.build
|
|
634
|
+
build: resolved.build,
|
|
635
|
+
...resolved.deployType === "native" ? { fingerprintDiff: await explainNativeDecision(input, commandRunner) } : {}
|
|
335
636
|
};
|
|
336
637
|
}
|
|
337
638
|
async function writeDecideGitHubOutput(decision) {
|
|
@@ -341,51 +642,21 @@ async function writeDecideGitHubOutput(decision) {
|
|
|
341
642
|
changed: String(decision.deployType !== "none"),
|
|
342
643
|
fingerprint: decision.fingerprint ?? "",
|
|
343
644
|
build_id: decision.build?.id ?? "",
|
|
344
|
-
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 ?? ""
|
|
345
650
|
});
|
|
346
651
|
}
|
|
347
|
-
// src/
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
if (bin && bin.length > 0)
|
|
353
|
-
return [bin, ...args];
|
|
354
|
-
return ["bunx", "expo", ...args];
|
|
355
|
-
}
|
|
356
|
-
function sourceMapExportCommand(platform) {
|
|
357
|
-
return expoCommand("export", "--platform", platform, "--output-dir", SOURCE_MAP_OUTPUT_DIR, "--source-maps", "external");
|
|
358
|
-
}
|
|
359
|
-
function mergeUpdateArgs(extra, exportSourceMaps) {
|
|
360
|
-
const args = extra ? [...extra] : [];
|
|
361
|
-
if (!exportSourceMaps)
|
|
362
|
-
return args;
|
|
363
|
-
if (!args.includes("--input-dir")) {
|
|
364
|
-
args.push("--input-dir", SOURCE_MAP_OUTPUT_DIR);
|
|
365
|
-
}
|
|
366
|
-
if (!args.includes("--skip-bundler")) {
|
|
367
|
-
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] ?? "");
|
|
368
657
|
}
|
|
369
658
|
return args;
|
|
370
659
|
}
|
|
371
|
-
async function defaultHasSourceMaps(directory = SOURCE_MAP_OUTPUT_DIR) {
|
|
372
|
-
if (!await exists(directory))
|
|
373
|
-
return false;
|
|
374
|
-
for await (const _ of new Bun.Glob("**/*.map").scan(directory)) {
|
|
375
|
-
return true;
|
|
376
|
-
}
|
|
377
|
-
return false;
|
|
378
|
-
}
|
|
379
|
-
async function assertSourceMaps(hasSourceMaps = defaultHasSourceMaps) {
|
|
380
|
-
if (await hasSourceMaps())
|
|
381
|
-
return;
|
|
382
|
-
throw new Error("Expo export did not produce Hermes source maps in dist; refusing to publish an OTA update.");
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// src/operate.ts
|
|
386
|
-
function extraUpdateArgs(value) {
|
|
387
|
-
return value?.split(/\s+/).filter(Boolean) ?? [];
|
|
388
|
-
}
|
|
389
660
|
function updateMessage(input) {
|
|
390
661
|
if (input.message)
|
|
391
662
|
return input.message;
|
|
@@ -410,23 +681,25 @@ async function defaultReadPackageJson() {
|
|
|
410
681
|
return null;
|
|
411
682
|
return file.json();
|
|
412
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
|
+
}
|
|
413
691
|
async function requireEnvironmentIfNeeded(input) {
|
|
414
692
|
if (input.environment)
|
|
415
693
|
return;
|
|
416
|
-
const
|
|
417
|
-
if (!pkg || typeof pkg !== "object" || pkg === null)
|
|
418
|
-
return;
|
|
419
|
-
const record = pkg;
|
|
420
|
-
const version = record.dependencies?.expo ?? record.devDependencies?.expo;
|
|
421
|
-
const major = parseExpoSdkMajor(version);
|
|
694
|
+
const major = await readExpoSdkMajor(input.readPackageJson);
|
|
422
695
|
if (major !== null && major >= 55) {
|
|
423
|
-
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.");
|
|
424
697
|
}
|
|
425
698
|
}
|
|
426
|
-
async function runHook(command, commandRunner) {
|
|
699
|
+
async function runHook(name, command, commandRunner) {
|
|
427
700
|
if (!command)
|
|
428
701
|
return;
|
|
429
|
-
await commandRunner(["bash", "-c", command]);
|
|
702
|
+
await stage(name, () => commandRunner(["bash", "-c", command]));
|
|
430
703
|
}
|
|
431
704
|
function mutatingRunner(commandRunner, dryRun) {
|
|
432
705
|
return async (command) => {
|
|
@@ -440,33 +713,44 @@ function mutatingRunner(commandRunner, dryRun) {
|
|
|
440
713
|
async function publishUpdate(input, commandRunner) {
|
|
441
714
|
await requireEnvironmentIfNeeded(input);
|
|
442
715
|
if (input.exportSourceMaps) {
|
|
443
|
-
await
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
716
|
+
await stage("ota export", async () => {
|
|
717
|
+
await commandRunner(sourceMapExportCommand(input.platform));
|
|
718
|
+
if (input.dryRun !== true) {
|
|
719
|
+
await assertSourceMaps(input.hasSourceMaps);
|
|
720
|
+
}
|
|
721
|
+
});
|
|
447
722
|
}
|
|
448
|
-
await runHook(input.preUpdate, commandRunner);
|
|
723
|
+
await runHook("pre-update", input.preUpdate, commandRunner);
|
|
449
724
|
const command = easCommand("update", "--channel", input.channel, "--platform", input.platform, "--message", updateMessage(input), "--non-interactive");
|
|
450
725
|
if (input.environment) {
|
|
451
726
|
command.push("--environment", input.environment);
|
|
452
727
|
}
|
|
453
|
-
command.push(...mergeUpdateArgs(
|
|
454
|
-
await commandRunner(command);
|
|
455
|
-
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);
|
|
456
731
|
}
|
|
457
732
|
async function queueNativeBuild(input, commandRunner) {
|
|
458
|
-
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");
|
|
459
734
|
if (input.autoSubmit) {
|
|
460
735
|
command.push("--auto-submit");
|
|
461
736
|
}
|
|
462
|
-
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";
|
|
463
747
|
}
|
|
464
748
|
async function resolveRuntimeVersion(input, commandRunner) {
|
|
465
749
|
if (input.runtimeVersion)
|
|
466
750
|
return input.runtimeVersion;
|
|
467
751
|
if (input.fingerprint)
|
|
468
752
|
return input.fingerprint;
|
|
469
|
-
return generateFingerprint(input, commandRunner);
|
|
753
|
+
return stage("fingerprint", () => generateFingerprint(input, commandRunner));
|
|
470
754
|
}
|
|
471
755
|
async function operateRelease(input, commandRunner = runInherited) {
|
|
472
756
|
const execute = mutatingRunner(commandRunner, input.dryRun === true);
|
|
@@ -475,19 +759,19 @@ async function operateRelease(input, commandRunner = runInherited) {
|
|
|
475
759
|
if (!input.group) {
|
|
476
760
|
throw new Error("Usage: --operation republish requires --group <update-group-id>");
|
|
477
761
|
}
|
|
762
|
+
const group = input.group;
|
|
478
763
|
await requireEnvironmentIfNeeded(input);
|
|
479
|
-
await execute(easCommand("update:republish", "--group",
|
|
764
|
+
await stage("recovery", () => execute(easCommand("update:republish", "--group", group, "--platform", input.platform, "--message", updateMessage(input), "--non-interactive")));
|
|
480
765
|
return "recovery";
|
|
481
766
|
}
|
|
482
767
|
case "rollback-embedded": {
|
|
483
768
|
await requireEnvironmentIfNeeded(input);
|
|
484
769
|
const runtimeVersion = await resolveRuntimeVersion(input, commandRunner);
|
|
485
|
-
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")));
|
|
486
771
|
return "recovery";
|
|
487
772
|
}
|
|
488
773
|
case "force-native":
|
|
489
|
-
|
|
490
|
-
return "native-build";
|
|
774
|
+
return queueNativeBuild(input, execute);
|
|
491
775
|
case "retry-ota":
|
|
492
776
|
await publishUpdate(input, execute);
|
|
493
777
|
return "ota";
|
|
@@ -502,8 +786,7 @@ async function operateRelease(input, commandRunner = runInherited) {
|
|
|
502
786
|
case "none":
|
|
503
787
|
return "skipped";
|
|
504
788
|
case "native":
|
|
505
|
-
|
|
506
|
-
return "native-build";
|
|
789
|
+
return queueNativeBuild(input, execute);
|
|
507
790
|
case "ota":
|
|
508
791
|
await publishUpdate(input, execute);
|
|
509
792
|
return "ota";
|
|
@@ -513,8 +796,12 @@ async function operateRelease(input, commandRunner = runInherited) {
|
|
|
513
796
|
}
|
|
514
797
|
}
|
|
515
798
|
}
|
|
516
|
-
async function writeOperateGitHubOutput(result) {
|
|
517
|
-
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
|
+
});
|
|
518
805
|
}
|
|
519
806
|
export {
|
|
520
807
|
DEFAULT_IMPACT_PATHS,
|