expo-release 2.0.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/LICENSE +21 -0
- package/README.md +375 -0
- package/action.yml +162 -0
- package/dist/args.d.ts +38 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +599 -0
- package/dist/decide.d.ts +35 -0
- package/dist/github-output.d.ts +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +525 -0
- package/dist/operate.d.ts +31 -0
- package/dist/run.d.ts +13 -0
- package/package.json +63 -0
- package/src/args.ts +318 -0
- package/src/cli.ts +97 -0
- package/src/decide.ts +293 -0
- package/src/github-output.ts +12 -0
- package/src/index.ts +19 -0
- package/src/operate.ts +236 -0
- package/src/run.ts +90 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export {
|
|
2
|
+
DEFAULT_IMPACT_PATHS,
|
|
3
|
+
type CommandName,
|
|
4
|
+
type DeployType,
|
|
5
|
+
type Operation,
|
|
6
|
+
type ReleaseOptions,
|
|
7
|
+
type ReleasePlatform
|
|
8
|
+
} from './args'
|
|
9
|
+
export {
|
|
10
|
+
type DecisionInput,
|
|
11
|
+
type EASBuild,
|
|
12
|
+
type ReleaseDecision,
|
|
13
|
+
decideRelease
|
|
14
|
+
} from './decide'
|
|
15
|
+
export {
|
|
16
|
+
type OperationInput,
|
|
17
|
+
type OperationResult,
|
|
18
|
+
operateRelease
|
|
19
|
+
} from './operate'
|
package/src/operate.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type { DeployType, Operation, ReleasePlatform } from './args'
|
|
2
|
+
import { appendGitHubOutput } from './github-output'
|
|
3
|
+
import {
|
|
4
|
+
type CommandRunner,
|
|
5
|
+
dryRunCommand,
|
|
6
|
+
easCommand,
|
|
7
|
+
generateFingerprint,
|
|
8
|
+
runInherited
|
|
9
|
+
} from './run'
|
|
10
|
+
|
|
11
|
+
export type { Operation }
|
|
12
|
+
|
|
13
|
+
export type OperationInput = {
|
|
14
|
+
profile: string
|
|
15
|
+
deployType: DeployType
|
|
16
|
+
operation: Operation
|
|
17
|
+
channel: string
|
|
18
|
+
environment?: string
|
|
19
|
+
platform: ReleasePlatform
|
|
20
|
+
autoSubmit: boolean
|
|
21
|
+
fingerprint?: string
|
|
22
|
+
group?: string
|
|
23
|
+
runtimeVersion?: string
|
|
24
|
+
message?: string
|
|
25
|
+
preUpdate?: string
|
|
26
|
+
postUpdate?: string
|
|
27
|
+
updateExtraArgs?: string
|
|
28
|
+
dryRun?: boolean
|
|
29
|
+
readPackageJson?: () => Promise<unknown>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type OperationResult = 'skipped' | 'ota' | 'native-build' | 'recovery'
|
|
33
|
+
|
|
34
|
+
function extraUpdateArgs(value: string | undefined): string[] {
|
|
35
|
+
return value?.split(/\s+/).filter(Boolean) ?? []
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function updateMessage(input: {
|
|
39
|
+
profile: string
|
|
40
|
+
fingerprint?: string
|
|
41
|
+
message?: string
|
|
42
|
+
}): string {
|
|
43
|
+
if (input.message) return input.message
|
|
44
|
+
const sha = process.env.GITHUB_SHA?.slice(0, 7) ?? 'manual'
|
|
45
|
+
const fingerprint = input.fingerprint?.slice(0, 8)
|
|
46
|
+
if (fingerprint) {
|
|
47
|
+
return `${input.profile} ${sha} (fp ${fingerprint})`
|
|
48
|
+
}
|
|
49
|
+
return `${input.profile} ${sha}`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function parseExpoSdkMajor(version: string | undefined): number | null {
|
|
53
|
+
if (!version) return null
|
|
54
|
+
const match = version.match(/(\d+)/)
|
|
55
|
+
if (!match?.[1]) return null
|
|
56
|
+
return Number(match[1])
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function defaultReadPackageJson(): Promise<unknown> {
|
|
60
|
+
const file = Bun.file('package.json')
|
|
61
|
+
if (!(await file.exists())) return null
|
|
62
|
+
return file.json()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function requireEnvironmentIfNeeded(
|
|
66
|
+
input: Pick<OperationInput, 'environment' | 'readPackageJson'>
|
|
67
|
+
) {
|
|
68
|
+
if (input.environment) return
|
|
69
|
+
const pkg = await (input.readPackageJson ?? defaultReadPackageJson)()
|
|
70
|
+
if (!pkg || typeof pkg !== 'object' || pkg === null) return
|
|
71
|
+
const record = pkg as {
|
|
72
|
+
dependencies?: { expo?: string }
|
|
73
|
+
devDependencies?: { expo?: string }
|
|
74
|
+
}
|
|
75
|
+
const version = record.dependencies?.expo ?? record.devDependencies?.expo
|
|
76
|
+
const major = parseExpoSdkMajor(version)
|
|
77
|
+
if (major !== null && major >= 55) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
'EAS Update requires --environment for Expo SDK 55+. Pass --environment <name>.'
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function runHook(
|
|
85
|
+
command: string | undefined,
|
|
86
|
+
commandRunner: CommandRunner
|
|
87
|
+
) {
|
|
88
|
+
if (!command) return
|
|
89
|
+
await commandRunner(['bash', '-c', command])
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function mutatingRunner(
|
|
93
|
+
commandRunner: CommandRunner,
|
|
94
|
+
dryRun: boolean
|
|
95
|
+
): CommandRunner {
|
|
96
|
+
return async (command) => {
|
|
97
|
+
if (dryRun) {
|
|
98
|
+
dryRunCommand(command)
|
|
99
|
+
return ''
|
|
100
|
+
}
|
|
101
|
+
return commandRunner(command)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function publishUpdate(
|
|
106
|
+
input: OperationInput,
|
|
107
|
+
commandRunner: CommandRunner
|
|
108
|
+
) {
|
|
109
|
+
await requireEnvironmentIfNeeded(input)
|
|
110
|
+
await runHook(input.preUpdate, commandRunner)
|
|
111
|
+
const command = easCommand(
|
|
112
|
+
'update',
|
|
113
|
+
'--channel',
|
|
114
|
+
input.channel,
|
|
115
|
+
'--platform',
|
|
116
|
+
input.platform,
|
|
117
|
+
'--message',
|
|
118
|
+
updateMessage(input),
|
|
119
|
+
'--non-interactive'
|
|
120
|
+
)
|
|
121
|
+
if (input.environment) {
|
|
122
|
+
command.push('--environment', input.environment)
|
|
123
|
+
}
|
|
124
|
+
command.push(...extraUpdateArgs(input.updateExtraArgs))
|
|
125
|
+
await commandRunner(command)
|
|
126
|
+
await runHook(input.postUpdate, commandRunner)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function queueNativeBuild(
|
|
130
|
+
input: OperationInput,
|
|
131
|
+
commandRunner: CommandRunner
|
|
132
|
+
) {
|
|
133
|
+
const command = easCommand(
|
|
134
|
+
'build',
|
|
135
|
+
'--platform',
|
|
136
|
+
input.platform,
|
|
137
|
+
'--profile',
|
|
138
|
+
input.profile,
|
|
139
|
+
'--non-interactive',
|
|
140
|
+
'--no-wait'
|
|
141
|
+
)
|
|
142
|
+
if (input.autoSubmit) {
|
|
143
|
+
command.push('--auto-submit')
|
|
144
|
+
}
|
|
145
|
+
await commandRunner(command)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function resolveRuntimeVersion(
|
|
149
|
+
input: OperationInput,
|
|
150
|
+
commandRunner: CommandRunner
|
|
151
|
+
): Promise<string> {
|
|
152
|
+
if (input.runtimeVersion) return input.runtimeVersion
|
|
153
|
+
if (input.fingerprint) return input.fingerprint
|
|
154
|
+
return generateFingerprint(input, commandRunner)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function operateRelease(
|
|
158
|
+
input: OperationInput,
|
|
159
|
+
commandRunner: CommandRunner = runInherited
|
|
160
|
+
): Promise<OperationResult> {
|
|
161
|
+
const execute = mutatingRunner(commandRunner, input.dryRun === true)
|
|
162
|
+
|
|
163
|
+
switch (input.operation) {
|
|
164
|
+
case 'republish': {
|
|
165
|
+
if (!input.group) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
'Usage: --operation republish requires --group <update-group-id>'
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
await requireEnvironmentIfNeeded(input)
|
|
171
|
+
await execute(
|
|
172
|
+
easCommand(
|
|
173
|
+
'update:republish',
|
|
174
|
+
'--group',
|
|
175
|
+
input.group,
|
|
176
|
+
'--platform',
|
|
177
|
+
input.platform,
|
|
178
|
+
'--message',
|
|
179
|
+
updateMessage(input),
|
|
180
|
+
'--non-interactive'
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
return 'recovery'
|
|
184
|
+
}
|
|
185
|
+
case 'rollback-embedded': {
|
|
186
|
+
await requireEnvironmentIfNeeded(input)
|
|
187
|
+
const runtimeVersion = await resolveRuntimeVersion(input, commandRunner)
|
|
188
|
+
await execute(
|
|
189
|
+
easCommand(
|
|
190
|
+
'update:roll-back-to-embedded',
|
|
191
|
+
'--channel',
|
|
192
|
+
input.channel,
|
|
193
|
+
'--platform',
|
|
194
|
+
input.platform,
|
|
195
|
+
'--runtime-version',
|
|
196
|
+
runtimeVersion,
|
|
197
|
+
'--message',
|
|
198
|
+
updateMessage(input),
|
|
199
|
+
'--non-interactive'
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
return 'recovery'
|
|
203
|
+
}
|
|
204
|
+
case 'force-native':
|
|
205
|
+
await queueNativeBuild(input, execute)
|
|
206
|
+
return 'native-build'
|
|
207
|
+
case 'retry-ota':
|
|
208
|
+
await publishUpdate(input, execute)
|
|
209
|
+
return 'ota'
|
|
210
|
+
case 'deploy':
|
|
211
|
+
break
|
|
212
|
+
default: {
|
|
213
|
+
const _exhaustive: never = input.operation
|
|
214
|
+
throw new Error(`Unsupported operation: ${_exhaustive}`)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
switch (input.deployType) {
|
|
219
|
+
case 'none':
|
|
220
|
+
return 'skipped'
|
|
221
|
+
case 'native':
|
|
222
|
+
await queueNativeBuild(input, execute)
|
|
223
|
+
return 'native-build'
|
|
224
|
+
case 'ota':
|
|
225
|
+
await publishUpdate(input, execute)
|
|
226
|
+
return 'ota'
|
|
227
|
+
default: {
|
|
228
|
+
const _exhaustive: never = input.deployType
|
|
229
|
+
throw new Error(`Unsupported deploy type: ${_exhaustive}`)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function writeOperateGitHubOutput(result: OperationResult) {
|
|
235
|
+
await appendGitHubOutput({ result })
|
|
236
|
+
}
|
package/src/run.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export type CommandRunner = (command: string[]) => Promise<string>
|
|
2
|
+
|
|
3
|
+
export function easCommand(...args: string[]): string[] {
|
|
4
|
+
const bin = process.env.EAS_BIN?.trim()
|
|
5
|
+
return [bin && bin.length > 0 ? bin : 'eas', ...args]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function runCaptured(command: string[]): Promise<string> {
|
|
9
|
+
const child = Bun.spawn(command, {
|
|
10
|
+
stdout: 'pipe',
|
|
11
|
+
stderr: 'pipe'
|
|
12
|
+
})
|
|
13
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
14
|
+
new Response(child.stdout).text(),
|
|
15
|
+
new Response(child.stderr).text(),
|
|
16
|
+
child.exited
|
|
17
|
+
])
|
|
18
|
+
if (exitCode !== 0) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`Command failed (${command.join(' ')}):\n${stderr || stdout}`
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
return stdout
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function runInherited(command: string[]): Promise<string> {
|
|
27
|
+
const child = Bun.spawn(command, { stdout: 'inherit', stderr: 'inherit' })
|
|
28
|
+
const exitCode = await child.exited
|
|
29
|
+
if (exitCode !== 0) {
|
|
30
|
+
throw new Error(`Command failed (${command.join(' ')})`)
|
|
31
|
+
}
|
|
32
|
+
return ''
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function parseJson(output: string): unknown {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(output) as unknown
|
|
38
|
+
} catch {
|
|
39
|
+
throw new Error(`Expected JSON output, received:\n${output}`)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getRecord(
|
|
44
|
+
value: unknown,
|
|
45
|
+
context: string
|
|
46
|
+
): Record<string, unknown> {
|
|
47
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
48
|
+
throw new Error(`${context} must be an object`)
|
|
49
|
+
}
|
|
50
|
+
return value as Record<string, unknown>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function parseFingerprintHash(output: string): string {
|
|
54
|
+
const record = getRecord(parseJson(output), 'EAS fingerprint')
|
|
55
|
+
if (typeof record.hash !== 'string' || record.hash.length === 0) {
|
|
56
|
+
throw new Error('EAS fingerprint output did not contain a full hash')
|
|
57
|
+
}
|
|
58
|
+
return record.hash
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function quoteCommand(command: string[]): string {
|
|
62
|
+
return command
|
|
63
|
+
.map((part) =>
|
|
64
|
+
/[\s"]/.test(part) ? `"${part.replaceAll('"', '\\"')}"` : part
|
|
65
|
+
)
|
|
66
|
+
.join(' ')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function dryRunCommand(command: string[]): void {
|
|
70
|
+
process.stderr.write(`[dry-run] ${quoteCommand(command)}\n`)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function generateFingerprint(
|
|
74
|
+
input: { platform: string; profile: string },
|
|
75
|
+
commandRunner: CommandRunner
|
|
76
|
+
): Promise<string> {
|
|
77
|
+
return parseFingerprintHash(
|
|
78
|
+
await commandRunner(
|
|
79
|
+
easCommand(
|
|
80
|
+
'fingerprint:generate',
|
|
81
|
+
'--platform',
|
|
82
|
+
input.platform,
|
|
83
|
+
'--build-profile',
|
|
84
|
+
input.profile,
|
|
85
|
+
'--json',
|
|
86
|
+
'--non-interactive'
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
}
|