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/src/cli.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { relative } from 'node:path'
|
|
4
|
+
import {
|
|
5
|
+
CliUsageError,
|
|
6
|
+
type CommandContext,
|
|
7
|
+
defineCli,
|
|
8
|
+
defineCommand
|
|
9
|
+
} from 'hanzio/cli'
|
|
4
10
|
import {
|
|
5
11
|
type CommandName,
|
|
6
12
|
needsDecide,
|
|
@@ -13,23 +19,110 @@ import {
|
|
|
13
19
|
import {
|
|
14
20
|
type DecisionInput,
|
|
15
21
|
decideRelease,
|
|
22
|
+
type EASBuild,
|
|
16
23
|
type ReleaseDecision,
|
|
17
24
|
writeDecideGitHubOutput
|
|
18
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'
|
|
19
35
|
import {
|
|
20
36
|
type OperationInput,
|
|
21
37
|
operateRelease,
|
|
38
|
+
readExpoSdkMajor,
|
|
22
39
|
writeOperateGitHubOutput
|
|
23
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
|
+
}
|
|
24
55
|
|
|
25
|
-
|
|
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
|
+
}
|
|
91
|
+
|
|
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
|
|
26
107
|
return {
|
|
27
108
|
base: options.base,
|
|
28
109
|
head: options.head,
|
|
29
110
|
profile: options.profile,
|
|
30
111
|
platform: options.platform,
|
|
31
112
|
impactPaths: options.impactPaths,
|
|
32
|
-
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
|
+
}
|
|
33
126
|
}
|
|
34
127
|
}
|
|
35
128
|
|
|
@@ -59,8 +152,8 @@ function toOperationInput(
|
|
|
59
152
|
|
|
60
153
|
type Context = CommandContext<ReleaseFlags>
|
|
61
154
|
|
|
62
|
-
async function decide(
|
|
63
|
-
const decision = await decideRelease(toDecisionInput(
|
|
155
|
+
async function decide(prepared: Prepared, ctx: Context) {
|
|
156
|
+
const decision = await decideRelease(toDecisionInput(prepared))
|
|
64
157
|
await writeDecideGitHubOutput(decision)
|
|
65
158
|
ctx.stdout.write(`${JSON.stringify(decision)}\n`)
|
|
66
159
|
return decision
|
|
@@ -71,64 +164,168 @@ async function operate(
|
|
|
71
164
|
decision: ReleaseDecision | null,
|
|
72
165
|
ctx: Context
|
|
73
166
|
) {
|
|
74
|
-
|
|
75
|
-
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)
|
|
76
175
|
ctx.stdout.write(`${result}\n`)
|
|
77
176
|
}
|
|
78
177
|
|
|
79
|
-
|
|
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
|
+
}
|
|
80
255
|
|
|
81
256
|
const cli = defineCli({
|
|
82
257
|
name: 'expo-release',
|
|
83
258
|
description:
|
|
84
259
|
'Fingerprint-driven Expo EAS release: OTA update or native store build',
|
|
85
|
-
//
|
|
260
|
+
// Every command takes the same flags, before or after the command name.
|
|
86
261
|
globals: releaseFlags,
|
|
87
262
|
context: (flags) => flags,
|
|
263
|
+
// The composite action names the command in EXPO_RELEASE_COMMAND and passes no argv.
|
|
264
|
+
defaultCommand: (env) => env.EXPO_RELEASE_COMMAND?.trim() || undefined,
|
|
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
|
|
275
|
+
}
|
|
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()
|
|
282
|
+
},
|
|
88
283
|
commands: {
|
|
89
284
|
release: defineCommand({
|
|
90
285
|
description: 'Run decide, then operate on its decision',
|
|
91
286
|
run: async (_input, ctx: Context) => {
|
|
92
|
-
const
|
|
93
|
-
const decision = needsDecide(options.operation)
|
|
94
|
-
? await decide(
|
|
287
|
+
const prepared = await prepare('release', ctx)
|
|
288
|
+
const decision = needsDecide(prepared.options.operation)
|
|
289
|
+
? await decide(prepared, ctx)
|
|
95
290
|
: null
|
|
96
|
-
await operate(options, decision, ctx)
|
|
291
|
+
await operate(prepared.options, decision, ctx)
|
|
97
292
|
}
|
|
98
293
|
}),
|
|
99
294
|
decide: defineCommand({
|
|
100
295
|
description:
|
|
101
|
-
'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',
|
|
102
297
|
run: async (_input, ctx: Context) => {
|
|
103
|
-
await decide(
|
|
298
|
+
await decide(await prepare('decide', ctx), ctx)
|
|
104
299
|
}
|
|
105
300
|
}),
|
|
106
301
|
operate: defineCommand({
|
|
107
302
|
description: 'Run --operation for --deploy-type',
|
|
108
303
|
run: async (_input, ctx: Context) => {
|
|
109
|
-
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
|
+
}
|
|
110
326
|
}
|
|
111
327
|
})
|
|
112
328
|
}
|
|
113
329
|
})
|
|
114
330
|
|
|
115
|
-
|
|
116
|
-
const env = Object.fromEntries(
|
|
117
|
-
Object.entries(process.env).flatMap(([name, value]) =>
|
|
118
|
-
value?.trim() ? [[name, value.trim()]] : []
|
|
119
|
-
)
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
let argv = process.argv.slice(2)
|
|
123
|
-
if (!argv.some((arg) => COMMANDS.includes(arg as CommandName))) {
|
|
124
|
-
// The composite action names the command in EXPO_RELEASE_COMMAND and passes no argv.
|
|
125
|
-
argv = env.EXPO_RELEASE_COMMAND
|
|
126
|
-
? [env.EXPO_RELEASE_COMMAND, ...argv]
|
|
127
|
-
: argv.length === 0
|
|
128
|
-
? ['--help']
|
|
129
|
-
: argv
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const code = await cli.run(argv, { env })
|
|
133
|
-
// Bad arguments exit 1, as before hanzio/cli, which uses 2.
|
|
134
|
-
process.exitCode = code === 2 ? 1 : code
|
|
331
|
+
process.exitCode = await cli.run()
|