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/eas-json.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { ReleasePlatform } from './args'
|
|
2
|
+
|
|
3
|
+
/** The fields of an eas.json build profile that expo-release reads. */
|
|
4
|
+
export type BuildProfile = {
|
|
5
|
+
channel?: string
|
|
6
|
+
environment?: string
|
|
7
|
+
env?: Record<string, string>
|
|
8
|
+
node?: string
|
|
9
|
+
bun?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type EasJson = {
|
|
13
|
+
cli?: { version?: string }
|
|
14
|
+
build?: Record<string, Record<string, unknown>>
|
|
15
|
+
submit?: Record<string, unknown>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Reads eas.json from the working directory. eas-cli accepts JSON5 there. */
|
|
19
|
+
export async function readEasJson(path = 'eas.json'): Promise<EasJson | null> {
|
|
20
|
+
const file = Bun.file(path)
|
|
21
|
+
if (!(await file.exists())) return null
|
|
22
|
+
const text = await file.text()
|
|
23
|
+
let value: unknown
|
|
24
|
+
try {
|
|
25
|
+
value = JSON.parse(text)
|
|
26
|
+
} catch (error) {
|
|
27
|
+
// eas-cli also accepts JSON5 (comments, trailing commas). Bun.JSON5 is
|
|
28
|
+
// missing on older Bun, where plain JSON still works.
|
|
29
|
+
const json5 = (Bun as { JSON5?: { parse(text: string): unknown } }).JSON5
|
|
30
|
+
if (!json5) throw error
|
|
31
|
+
value = json5.parse(text)
|
|
32
|
+
}
|
|
33
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
34
|
+
throw new Error(`${path} must hold an object`)
|
|
35
|
+
}
|
|
36
|
+
return value as EasJson
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type RawProfile = Record<string, unknown>
|
|
40
|
+
|
|
41
|
+
function asRecord(value: unknown): RawProfile | undefined {
|
|
42
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
43
|
+
? (value as RawProfile)
|
|
44
|
+
: undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The merge eas-cli applies (@expo/eas-json build/resolver): later keys win,
|
|
48
|
+
// `env` and the platform blocks merge one level deep.
|
|
49
|
+
function merge(base: RawProfile, update: RawProfile): RawProfile {
|
|
50
|
+
const result = { ...base, ...update }
|
|
51
|
+
for (const key of ['env', 'ios', 'android']) {
|
|
52
|
+
const a = asRecord(base[key])
|
|
53
|
+
const b = asRecord(update[key])
|
|
54
|
+
if (a && b) result[key] = key === 'env' ? { ...a, ...b } : merge(a, b)
|
|
55
|
+
}
|
|
56
|
+
return result
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolveRaw(
|
|
60
|
+
easJson: EasJson,
|
|
61
|
+
name: string,
|
|
62
|
+
depth: number
|
|
63
|
+
): RawProfile | null {
|
|
64
|
+
if (depth >= 5) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
'eas.json: too long a chain of "extends"; check the build profiles for a cycle'
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
const profile = asRecord(easJson.build?.[name])
|
|
70
|
+
if (!profile) {
|
|
71
|
+
if (depth === 0) return null
|
|
72
|
+
throw new Error(`eas.json: build profile extends missing "${name}"`)
|
|
73
|
+
}
|
|
74
|
+
const { extends: parent, ...rest } = profile
|
|
75
|
+
return typeof parent === 'string'
|
|
76
|
+
? merge(resolveRaw(easJson, parent, depth + 1) ?? {}, rest)
|
|
77
|
+
: rest
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** `build.<name>` with `extends` and the platform block applied, or null. */
|
|
81
|
+
export function resolveBuildProfile(
|
|
82
|
+
easJson: EasJson,
|
|
83
|
+
name: string,
|
|
84
|
+
platform: ReleasePlatform
|
|
85
|
+
): BuildProfile | null {
|
|
86
|
+
const raw = resolveRaw(easJson, name, 0)
|
|
87
|
+
if (!raw) return null
|
|
88
|
+
const { ios, android, ...base } = raw
|
|
89
|
+
const resolved = merge(
|
|
90
|
+
base,
|
|
91
|
+
asRecord(platform === 'ios' ? ios : android) ?? {}
|
|
92
|
+
)
|
|
93
|
+
const text = (key: string) =>
|
|
94
|
+
typeof resolved[key] === 'string' && resolved[key] !== ''
|
|
95
|
+
? (resolved[key] as string)
|
|
96
|
+
: undefined
|
|
97
|
+
const env = asRecord(resolved.env)
|
|
98
|
+
return {
|
|
99
|
+
channel: text('channel'),
|
|
100
|
+
environment: text('environment'),
|
|
101
|
+
node: text('node'),
|
|
102
|
+
bun: text('bun'),
|
|
103
|
+
...(env
|
|
104
|
+
? {
|
|
105
|
+
env: Object.fromEntries(
|
|
106
|
+
Object.entries(env).map(([key, value]) => [key, String(value)])
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
: {})
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Puts the profile's `env` into this process, so every EAS command and hook
|
|
115
|
+
* sees what the EAS build machine sees. Variables already set win.
|
|
116
|
+
*/
|
|
117
|
+
export function applyProfileEnv(
|
|
118
|
+
profile: BuildProfile | null,
|
|
119
|
+
env: Record<string, string | undefined> = process.env
|
|
120
|
+
): string[] {
|
|
121
|
+
const applied: string[] = []
|
|
122
|
+
for (const [key, value] of Object.entries(profile?.env ?? {})) {
|
|
123
|
+
if (env[key] !== undefined) continue
|
|
124
|
+
env[key] = value
|
|
125
|
+
applied.push(key)
|
|
126
|
+
}
|
|
127
|
+
return applied
|
|
128
|
+
}
|
package/src/index.ts
CHANGED
package/src/operate.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { DeployType, Operation, ReleasePlatform } from './args'
|
|
2
|
+
import { type EASBuild, parseBuildList } from './decide'
|
|
2
3
|
import { appendGitHubOutput } from './github-output'
|
|
3
4
|
import {
|
|
4
5
|
type CommandRunner,
|
|
5
6
|
dryRunCommand,
|
|
6
7
|
easCommand,
|
|
7
8
|
generateFingerprint,
|
|
8
|
-
runInherited
|
|
9
|
+
runInherited,
|
|
10
|
+
stage
|
|
9
11
|
} from './run'
|
|
10
12
|
import {
|
|
11
13
|
assertSourceMaps,
|
|
@@ -32,14 +34,23 @@ export type OperationInput = {
|
|
|
32
34
|
updateExtraArgs?: string
|
|
33
35
|
exportSourceMaps?: boolean
|
|
34
36
|
dryRun?: boolean
|
|
37
|
+
/** Called with the native build `eas build` queued, when it printed one. */
|
|
38
|
+
onQueuedBuild?: (build: EASBuild) => void
|
|
35
39
|
readPackageJson?: () => Promise<unknown>
|
|
36
40
|
hasSourceMaps?: () => Promise<boolean>
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
export type OperationResult = 'skipped' | 'ota' | 'native-build' | 'recovery'
|
|
40
44
|
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
/** Splits like a shell: whitespace separates, single or double quotes group. */
|
|
46
|
+
export function splitArgs(value: string | undefined): string[] {
|
|
47
|
+
const args: string[] = []
|
|
48
|
+
for (const match of (value ?? '').matchAll(
|
|
49
|
+
/"((?:\\.|[^"\\])*)"|'([^']*)'|(\S+)/g
|
|
50
|
+
)) {
|
|
51
|
+
args.push(match[1]?.replace(/\\(.)/g, '$1') ?? match[2] ?? match[3] ?? '')
|
|
52
|
+
}
|
|
53
|
+
return args
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
export function updateMessage(input: {
|
|
@@ -69,31 +80,40 @@ async function defaultReadPackageJson(): Promise<unknown> {
|
|
|
69
80
|
return file.json()
|
|
70
81
|
}
|
|
71
82
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
const pkg = await
|
|
77
|
-
if (!pkg || typeof pkg !== 'object'
|
|
83
|
+
/** The Expo SDK major from package.json, or null when it is not a version. */
|
|
84
|
+
export async function readExpoSdkMajor(
|
|
85
|
+
readPackageJson: () => Promise<unknown> = defaultReadPackageJson
|
|
86
|
+
): Promise<number | null> {
|
|
87
|
+
const pkg = await readPackageJson()
|
|
88
|
+
if (!pkg || typeof pkg !== 'object') return null
|
|
78
89
|
const record = pkg as {
|
|
79
90
|
dependencies?: { expo?: string }
|
|
80
91
|
devDependencies?: { expo?: string }
|
|
81
92
|
}
|
|
82
|
-
|
|
83
|
-
|
|
93
|
+
return parseExpoSdkMajor(
|
|
94
|
+
record.dependencies?.expo ?? record.devDependencies?.expo
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function requireEnvironmentIfNeeded(
|
|
99
|
+
input: Pick<OperationInput, 'environment' | 'readPackageJson'>
|
|
100
|
+
) {
|
|
101
|
+
if (input.environment) return
|
|
102
|
+
const major = await readExpoSdkMajor(input.readPackageJson)
|
|
84
103
|
if (major !== null && major >= 55) {
|
|
85
104
|
throw new Error(
|
|
86
|
-
'EAS Update requires --environment for Expo SDK 55+. Pass --environment <name>.'
|
|
105
|
+
'EAS Update requires --environment for Expo SDK 55+. Pass --environment <name> or set build.<profile>.environment in eas.json.'
|
|
87
106
|
)
|
|
88
107
|
}
|
|
89
108
|
}
|
|
90
109
|
|
|
91
110
|
async function runHook(
|
|
111
|
+
name: 'pre-update' | 'post-update',
|
|
92
112
|
command: string | undefined,
|
|
93
113
|
commandRunner: CommandRunner
|
|
94
114
|
) {
|
|
95
115
|
if (!command) return
|
|
96
|
-
await commandRunner(['bash', '-c', command])
|
|
116
|
+
await stage(name, () => commandRunner(['bash', '-c', command]))
|
|
97
117
|
}
|
|
98
118
|
|
|
99
119
|
function mutatingRunner(
|
|
@@ -115,12 +135,14 @@ async function publishUpdate(
|
|
|
115
135
|
) {
|
|
116
136
|
await requireEnvironmentIfNeeded(input)
|
|
117
137
|
if (input.exportSourceMaps) {
|
|
118
|
-
await
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
138
|
+
await stage('ota export', async () => {
|
|
139
|
+
await commandRunner(sourceMapExportCommand(input.platform))
|
|
140
|
+
if (input.dryRun !== true) {
|
|
141
|
+
await assertSourceMaps(input.hasSourceMaps)
|
|
142
|
+
}
|
|
143
|
+
})
|
|
122
144
|
}
|
|
123
|
-
await runHook(input.preUpdate, commandRunner)
|
|
145
|
+
await runHook('pre-update', input.preUpdate, commandRunner)
|
|
124
146
|
const command = easCommand(
|
|
125
147
|
'update',
|
|
126
148
|
'--channel',
|
|
@@ -136,18 +158,19 @@ async function publishUpdate(
|
|
|
136
158
|
}
|
|
137
159
|
command.push(
|
|
138
160
|
...mergeUpdateArgs(
|
|
139
|
-
|
|
161
|
+
splitArgs(input.updateExtraArgs),
|
|
140
162
|
input.exportSourceMaps === true
|
|
141
163
|
)
|
|
142
164
|
)
|
|
143
|
-
await commandRunner(command)
|
|
144
|
-
await runHook(input.postUpdate, commandRunner)
|
|
165
|
+
await stage('ota update', () => commandRunner(command))
|
|
166
|
+
await runHook('post-update', input.postUpdate, commandRunner)
|
|
145
167
|
}
|
|
146
168
|
|
|
169
|
+
/** Queues the build and reports the build EAS started (none in a dry run). */
|
|
147
170
|
async function queueNativeBuild(
|
|
148
171
|
input: OperationInput,
|
|
149
172
|
commandRunner: CommandRunner
|
|
150
|
-
) {
|
|
173
|
+
): Promise<OperationResult> {
|
|
151
174
|
const command = easCommand(
|
|
152
175
|
'build',
|
|
153
176
|
'--platform',
|
|
@@ -155,12 +178,23 @@ async function queueNativeBuild(
|
|
|
155
178
|
'--profile',
|
|
156
179
|
input.profile,
|
|
157
180
|
'--non-interactive',
|
|
158
|
-
'--no-wait'
|
|
181
|
+
'--no-wait',
|
|
182
|
+
'--json'
|
|
159
183
|
)
|
|
160
184
|
if (input.autoSubmit) {
|
|
161
185
|
command.push('--auto-submit')
|
|
162
186
|
}
|
|
163
|
-
await commandRunner(command)
|
|
187
|
+
const output = await stage('native build', () => commandRunner(command))
|
|
188
|
+
// The build is queued; an unreadable record only loses the outputs.
|
|
189
|
+
try {
|
|
190
|
+
const queued = output.trim() ? parseBuildList(output)[0] : undefined
|
|
191
|
+
if (queued) input.onQueuedBuild?.(queued)
|
|
192
|
+
} catch (error) {
|
|
193
|
+
process.stderr.write(
|
|
194
|
+
`Warning: could not read the queued build from eas build --json: ${error instanceof Error ? error.message : String(error)}\n`
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
return 'native-build'
|
|
164
198
|
}
|
|
165
199
|
|
|
166
200
|
async function resolveRuntimeVersion(
|
|
@@ -169,7 +203,7 @@ async function resolveRuntimeVersion(
|
|
|
169
203
|
): Promise<string> {
|
|
170
204
|
if (input.runtimeVersion) return input.runtimeVersion
|
|
171
205
|
if (input.fingerprint) return input.fingerprint
|
|
172
|
-
return generateFingerprint(input, commandRunner)
|
|
206
|
+
return stage('fingerprint', () => generateFingerprint(input, commandRunner))
|
|
173
207
|
}
|
|
174
208
|
|
|
175
209
|
export async function operateRelease(
|
|
@@ -185,17 +219,20 @@ export async function operateRelease(
|
|
|
185
219
|
'Usage: --operation republish requires --group <update-group-id>'
|
|
186
220
|
)
|
|
187
221
|
}
|
|
222
|
+
const group = input.group
|
|
188
223
|
await requireEnvironmentIfNeeded(input)
|
|
189
|
-
await
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
224
|
+
await stage('recovery', () =>
|
|
225
|
+
execute(
|
|
226
|
+
easCommand(
|
|
227
|
+
'update:republish',
|
|
228
|
+
'--group',
|
|
229
|
+
group,
|
|
230
|
+
'--platform',
|
|
231
|
+
input.platform,
|
|
232
|
+
'--message',
|
|
233
|
+
updateMessage(input),
|
|
234
|
+
'--non-interactive'
|
|
235
|
+
)
|
|
199
236
|
)
|
|
200
237
|
)
|
|
201
238
|
return 'recovery'
|
|
@@ -203,25 +240,26 @@ export async function operateRelease(
|
|
|
203
240
|
case 'rollback-embedded': {
|
|
204
241
|
await requireEnvironmentIfNeeded(input)
|
|
205
242
|
const runtimeVersion = await resolveRuntimeVersion(input, commandRunner)
|
|
206
|
-
await
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
243
|
+
await stage('recovery', () =>
|
|
244
|
+
execute(
|
|
245
|
+
easCommand(
|
|
246
|
+
'update:roll-back-to-embedded',
|
|
247
|
+
'--channel',
|
|
248
|
+
input.channel,
|
|
249
|
+
'--platform',
|
|
250
|
+
input.platform,
|
|
251
|
+
'--runtime-version',
|
|
252
|
+
runtimeVersion,
|
|
253
|
+
'--message',
|
|
254
|
+
updateMessage(input),
|
|
255
|
+
'--non-interactive'
|
|
256
|
+
)
|
|
218
257
|
)
|
|
219
258
|
)
|
|
220
259
|
return 'recovery'
|
|
221
260
|
}
|
|
222
261
|
case 'force-native':
|
|
223
|
-
|
|
224
|
-
return 'native-build'
|
|
262
|
+
return queueNativeBuild(input, execute)
|
|
225
263
|
case 'retry-ota':
|
|
226
264
|
await publishUpdate(input, execute)
|
|
227
265
|
return 'ota'
|
|
@@ -237,8 +275,7 @@ export async function operateRelease(
|
|
|
237
275
|
case 'none':
|
|
238
276
|
return 'skipped'
|
|
239
277
|
case 'native':
|
|
240
|
-
|
|
241
|
-
return 'native-build'
|
|
278
|
+
return queueNativeBuild(input, execute)
|
|
242
279
|
case 'ota':
|
|
243
280
|
await publishUpdate(input, execute)
|
|
244
281
|
return 'ota'
|
|
@@ -249,6 +286,13 @@ export async function operateRelease(
|
|
|
249
286
|
}
|
|
250
287
|
}
|
|
251
288
|
|
|
252
|
-
export async function writeOperateGitHubOutput(
|
|
253
|
-
|
|
289
|
+
export async function writeOperateGitHubOutput(
|
|
290
|
+
result: OperationResult,
|
|
291
|
+
queuedBuild?: EASBuild
|
|
292
|
+
) {
|
|
293
|
+
await appendGitHubOutput({
|
|
294
|
+
result,
|
|
295
|
+
queued_build_id: queuedBuild?.id ?? '',
|
|
296
|
+
queued_build_url: queuedBuild?.buildUrl ?? ''
|
|
297
|
+
})
|
|
254
298
|
}
|