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/src/cli.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
+
import { relative } from 'node:path'
|
|
3
4
|
import {
|
|
4
5
|
CliUsageError,
|
|
5
6
|
type CommandContext,
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
defineCommand
|
|
8
9
|
} from 'hanzio/cli'
|
|
9
10
|
import {
|
|
11
|
+
type CommandName,
|
|
10
12
|
needsDecide,
|
|
11
13
|
type ReleaseFlags,
|
|
12
14
|
type ReleaseOptions,
|
|
@@ -17,23 +19,110 @@ import {
|
|
|
17
19
|
import {
|
|
18
20
|
type DecisionInput,
|
|
19
21
|
decideRelease,
|
|
22
|
+
type EASBuild,
|
|
20
23
|
type ReleaseDecision,
|
|
21
24
|
writeDecideGitHubOutput
|
|
22
25
|
} from './decide'
|
|
26
|
+
import { formatFindings, runDoctor } from './doctor'
|
|
27
|
+
import {
|
|
28
|
+
applyProfileEnv,
|
|
29
|
+
type BuildProfile,
|
|
30
|
+
type EasJson,
|
|
31
|
+
readEasJson,
|
|
32
|
+
resolveBuildProfile
|
|
33
|
+
} from './eas-json'
|
|
34
|
+
import { appendGitHubOutput } from './github-output'
|
|
23
35
|
import {
|
|
24
36
|
type OperationInput,
|
|
25
37
|
operateRelease,
|
|
38
|
+
readExpoSdkMajor,
|
|
26
39
|
writeOperateGitHubOutput
|
|
27
40
|
} from './operate'
|
|
41
|
+
import { type Stage, StageError } from './run'
|
|
42
|
+
|
|
43
|
+
const inGitHubActions = process.env.GITHUB_ACTIONS === 'true'
|
|
44
|
+
|
|
45
|
+
// Metro caches in $TMPDIR/metro-cache. On a shared self-hosted machine
|
|
46
|
+
// /tmp/metro-cache can belong to another user (EACCES); the job's temp
|
|
47
|
+
// directory is ours.
|
|
48
|
+
if (
|
|
49
|
+
process.env.RUNNER_ENVIRONMENT === 'self-hosted' &&
|
|
50
|
+
process.env.RUNNER_TEMP &&
|
|
51
|
+
!process.env.TMPDIR
|
|
52
|
+
) {
|
|
53
|
+
process.env.TMPDIR = process.env.RUNNER_TEMP
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type Prepared = {
|
|
57
|
+
options: ReleaseOptions
|
|
58
|
+
easJson: EasJson | null
|
|
59
|
+
buildProfile: BuildProfile | null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Read by onError, which gets no context.
|
|
63
|
+
let current: Prepared | null = null
|
|
64
|
+
|
|
65
|
+
async function prepare(command: CommandName, ctx: Context): Promise<Prepared> {
|
|
66
|
+
const platform = ctx.context.platform
|
|
67
|
+
const easJson = await readEasJson()
|
|
68
|
+
const buildProfile = easJson
|
|
69
|
+
? resolveBuildProfile(easJson, ctx.context.profile, platform)
|
|
70
|
+
: null
|
|
71
|
+
const applied = applyProfileEnv(buildProfile)
|
|
72
|
+
if (applied.length > 0) {
|
|
73
|
+
process.stderr.write(
|
|
74
|
+
`[expo-release] env from eas.json build.${ctx.context.profile}: ${applied.join(', ')}\n`
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
// `eas update --environment` also turns off .env loading, so the profile's
|
|
78
|
+
// environment is a default only where SDK 55+ requires one anyway.
|
|
79
|
+
const sdk = await readExpoSdkMajor().catch(() => null)
|
|
80
|
+
const defaults =
|
|
81
|
+
buildProfile && (sdk === null || sdk < 55)
|
|
82
|
+
? { ...buildProfile, environment: undefined }
|
|
83
|
+
: buildProfile
|
|
84
|
+
current = {
|
|
85
|
+
options: toReleaseOptions(command, ctx, defaults),
|
|
86
|
+
easJson,
|
|
87
|
+
buildProfile
|
|
88
|
+
}
|
|
89
|
+
return current
|
|
90
|
+
}
|
|
28
91
|
|
|
29
|
-
function
|
|
92
|
+
function doctorInput(prepared: Prepared, full: boolean) {
|
|
93
|
+
const { options } = prepared
|
|
94
|
+
return {
|
|
95
|
+
profile: options.profile,
|
|
96
|
+
platform: options.platform,
|
|
97
|
+
environment: options.environment,
|
|
98
|
+
autoSubmit: options.autoSubmit,
|
|
99
|
+
easJson: prepared.easJson,
|
|
100
|
+
buildProfile: prepared.buildProfile,
|
|
101
|
+
full
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function toDecisionInput(prepared: Prepared): DecisionInput {
|
|
106
|
+
const { options } = prepared
|
|
30
107
|
return {
|
|
31
108
|
base: options.base,
|
|
32
109
|
head: options.head,
|
|
33
110
|
profile: options.profile,
|
|
34
111
|
platform: options.platform,
|
|
35
112
|
impactPaths: options.impactPaths,
|
|
36
|
-
ignoreExpoImpact: shouldIgnoreExpoImpact(options)
|
|
113
|
+
ignoreExpoImpact: shouldIgnoreExpoImpact(options),
|
|
114
|
+
environment: options.environment,
|
|
115
|
+
// The quick checks, as warnings: they explain a failure that follows.
|
|
116
|
+
preflight: async () => {
|
|
117
|
+
const findings = await runDoctor(doctorInput(prepared, false)).catch(
|
|
118
|
+
() => []
|
|
119
|
+
)
|
|
120
|
+
if (findings.length > 0) {
|
|
121
|
+
process.stderr.write(
|
|
122
|
+
`${formatFindings(findings, { asWarnings: true, github: inGitHubActions })}\n`
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
37
126
|
}
|
|
38
127
|
}
|
|
39
128
|
|
|
@@ -63,8 +152,8 @@ function toOperationInput(
|
|
|
63
152
|
|
|
64
153
|
type Context = CommandContext<ReleaseFlags>
|
|
65
154
|
|
|
66
|
-
async function decide(
|
|
67
|
-
const decision = await decideRelease(toDecisionInput(
|
|
155
|
+
async function decide(prepared: Prepared, ctx: Context) {
|
|
156
|
+
const decision = await decideRelease(toDecisionInput(prepared))
|
|
68
157
|
await writeDecideGitHubOutput(decision)
|
|
69
158
|
ctx.stdout.write(`${JSON.stringify(decision)}\n`)
|
|
70
159
|
return decision
|
|
@@ -75,56 +164,165 @@ async function operate(
|
|
|
75
164
|
decision: ReleaseDecision | null,
|
|
76
165
|
ctx: Context
|
|
77
166
|
) {
|
|
78
|
-
|
|
79
|
-
await
|
|
167
|
+
let queued: EASBuild | undefined
|
|
168
|
+
const result = await operateRelease({
|
|
169
|
+
...toOperationInput(options, decision),
|
|
170
|
+
onQueuedBuild: (build) => {
|
|
171
|
+
queued = build
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
await writeOperateGitHubOutput(result, queued)
|
|
80
175
|
ctx.stdout.write(`${result}\n`)
|
|
81
176
|
}
|
|
82
177
|
|
|
178
|
+
/** A fixed next step per stage, never guessed from EAS's wording. */
|
|
179
|
+
function nextStep(stage: Stage, options: ReleaseOptions): string {
|
|
180
|
+
const app = relative(
|
|
181
|
+
process.env.GITHUB_WORKSPACE ?? process.cwd(),
|
|
182
|
+
process.cwd()
|
|
183
|
+
)
|
|
184
|
+
const from = app ? `From ${app}: ` : ''
|
|
185
|
+
const { platform, profile } = options
|
|
186
|
+
switch (stage) {
|
|
187
|
+
case 'doctor':
|
|
188
|
+
return 'Fix the findings above; `expo-release doctor` runs the same checks.'
|
|
189
|
+
case 'diff':
|
|
190
|
+
return 'Check out with fetch-depth: 0 so both commits of the diff exist.'
|
|
191
|
+
case 'fingerprint':
|
|
192
|
+
return `The app config or EAS project did not load. ${from}\`npx expo config\` and \`eas fingerprint:generate --platform ${platform} --build-profile ${profile}\` with the job's environment variables.`
|
|
193
|
+
case 'build list':
|
|
194
|
+
case 'fingerprint diff':
|
|
195
|
+
return `Check that EXPO_TOKEN can read this project. ${from}\`eas build:list --platform ${platform} --build-profile ${profile}\`.`
|
|
196
|
+
case 'errored build':
|
|
197
|
+
return 'Fix the build failure on EAS, then run the workflow with operation force-native.'
|
|
198
|
+
case 'native build':
|
|
199
|
+
return `A native build that fails before it is queued usually needs one interactive run. ${from}\`eas credentials:configure-build --platform ${platform} --profile ${profile}\`.`
|
|
200
|
+
case 'ota export':
|
|
201
|
+
case 'ota update':
|
|
202
|
+
return 'Fix the bundle error above, then run the workflow with operation retry-ota.'
|
|
203
|
+
case 'pre-update':
|
|
204
|
+
case 'post-update':
|
|
205
|
+
return `The ${stage} command failed; it is the workflow's own step.`
|
|
206
|
+
case 'recovery':
|
|
207
|
+
return 'Check the group or runtime version passed to the recovery operation.'
|
|
208
|
+
default: {
|
|
209
|
+
const _exhaustive: never = stage
|
|
210
|
+
return _exhaustive
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function escapeData(text: string) {
|
|
216
|
+
return text
|
|
217
|
+
.replaceAll('%', '%25')
|
|
218
|
+
.replaceAll('\r', '%0D')
|
|
219
|
+
.replaceAll('\n', '%0A')
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function escapeProperty(text: string) {
|
|
223
|
+
return escapeData(text).replaceAll(':', '%3A').replaceAll(',', '%2C')
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function reportFailure(
|
|
227
|
+
error: Error,
|
|
228
|
+
io: { stderr: { write(text: string): unknown } }
|
|
229
|
+
) {
|
|
230
|
+
const stage = error instanceof StageError ? error.stage : undefined
|
|
231
|
+
const options = current?.options
|
|
232
|
+
const said = error instanceof StageError ? error.tail : []
|
|
233
|
+
const next = stage && options ? nextStep(stage, options) : undefined
|
|
234
|
+
|
|
235
|
+
io.stderr.write(`${error.message}\n`)
|
|
236
|
+
if (inGitHubActions) {
|
|
237
|
+
const where = options ? ` (${options.profile}, ${options.platform})` : ''
|
|
238
|
+
const title = `expo-release: ${stage ?? 'release'} failed${where}`
|
|
239
|
+
const message = said.slice(-3).join('\n') || error.message
|
|
240
|
+
io.stderr.write(
|
|
241
|
+
`::error title=${escapeProperty(title)}::${escapeData(message)}\n`
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
await appendGitHubOutput({
|
|
245
|
+
result: 'failed',
|
|
246
|
+
failed_stage: stage ?? '',
|
|
247
|
+
failure: [
|
|
248
|
+
said.length > 0 ? `EAS said:\n${said.join('\n')}` : error.message,
|
|
249
|
+
next ? `Next: ${next}` : ''
|
|
250
|
+
]
|
|
251
|
+
.filter(Boolean)
|
|
252
|
+
.join('\n')
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
83
256
|
const cli = defineCli({
|
|
84
257
|
name: 'expo-release',
|
|
85
258
|
description:
|
|
86
259
|
'Fingerprint-driven Expo EAS release: OTA update or native store build',
|
|
87
|
-
//
|
|
260
|
+
// Every command takes the same flags, before or after the command name.
|
|
88
261
|
globals: releaseFlags,
|
|
89
262
|
context: (flags) => flags,
|
|
90
263
|
// The composite action names the command in EXPO_RELEASE_COMMAND and passes no argv.
|
|
91
264
|
defaultCommand: (env) => env.EXPO_RELEASE_COMMAND?.trim() || undefined,
|
|
92
|
-
onError: (error, io) => {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
error
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
265
|
+
onError: async (error, io) => {
|
|
266
|
+
if (error instanceof CliUsageError) {
|
|
267
|
+
// With no arguments at all, print help and exit 0.
|
|
268
|
+
if (error.kind === 'missing-command' && process.argv.length <= 2) {
|
|
269
|
+
io.stdout.write(error.help)
|
|
270
|
+
return 0
|
|
271
|
+
}
|
|
272
|
+
// Bad arguments exit 1, as before hanzio/cli, which uses 2.
|
|
273
|
+
const code = io.print()
|
|
274
|
+
return code === 2 ? 1 : code
|
|
101
275
|
}
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
276
|
+
// An expected failure: the message, an annotation and outputs; no stack.
|
|
277
|
+
if (error instanceof Error) {
|
|
278
|
+
await reportFailure(error, io)
|
|
279
|
+
return 1
|
|
280
|
+
}
|
|
281
|
+
return io.print()
|
|
105
282
|
},
|
|
106
283
|
commands: {
|
|
107
284
|
release: defineCommand({
|
|
108
285
|
description: 'Run decide, then operate on its decision',
|
|
109
286
|
run: async (_input, ctx: Context) => {
|
|
110
|
-
const
|
|
111
|
-
const decision = needsDecide(options.operation)
|
|
112
|
-
? await decide(
|
|
287
|
+
const prepared = await prepare('release', ctx)
|
|
288
|
+
const decision = needsDecide(prepared.options.operation)
|
|
289
|
+
? await decide(prepared, ctx)
|
|
113
290
|
: null
|
|
114
|
-
await operate(options, decision, ctx)
|
|
291
|
+
await operate(prepared.options, decision, ctx)
|
|
115
292
|
}
|
|
116
293
|
}),
|
|
117
294
|
decide: defineCommand({
|
|
118
295
|
description:
|
|
119
|
-
'Print whether this change needs no deploy, an OTA update or a native build',
|
|
296
|
+
'Print whether this change needs no deploy, an OTA update or a native build, and why',
|
|
120
297
|
run: async (_input, ctx: Context) => {
|
|
121
|
-
await decide(
|
|
298
|
+
await decide(await prepare('decide', ctx), ctx)
|
|
122
299
|
}
|
|
123
300
|
}),
|
|
124
301
|
operate: defineCommand({
|
|
125
302
|
description: 'Run --operation for --deploy-type',
|
|
126
303
|
run: async (_input, ctx: Context) => {
|
|
127
|
-
await operate(
|
|
304
|
+
await operate((await prepare('operate', ctx)).options, null, ctx)
|
|
305
|
+
}
|
|
306
|
+
}),
|
|
307
|
+
doctor: defineCommand({
|
|
308
|
+
description:
|
|
309
|
+
'Check the EAS setup, the app config and the committed files; exit 1 on an error',
|
|
310
|
+
run: async (_input, ctx: Context) => {
|
|
311
|
+
const prepared = await prepare('doctor', ctx)
|
|
312
|
+
const findings = await runDoctor(doctorInput(prepared, true))
|
|
313
|
+
if (findings.length === 0) {
|
|
314
|
+
ctx.stdout.write('No problems found.\n')
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
ctx.stdout.write(
|
|
318
|
+
`${formatFindings(findings, { github: inGitHubActions })}\n`
|
|
319
|
+
)
|
|
320
|
+
if (findings.some((finding) => finding.level === 'error')) {
|
|
321
|
+
throw new StageError(
|
|
322
|
+
'doctor',
|
|
323
|
+
`${findings.filter((f) => f.level === 'error').length} doctor check(s) failed.`
|
|
324
|
+
)
|
|
325
|
+
}
|
|
128
326
|
}
|
|
129
327
|
})
|
|
130
328
|
}
|