@better-update/cli 0.3.0 → 0.3.2

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.
Files changed (152) hide show
  1. package/dist/index.js +5319 -0
  2. package/dist/index.js.map +1 -0
  3. package/package.json +12 -9
  4. package/CHANGELOG.md +0 -58
  5. package/oxlint.config.ts +0 -6
  6. package/src/app-layer.ts +0 -29
  7. package/src/application/build-workflow.ts +0 -222
  8. package/src/application/command-exit.ts +0 -13
  9. package/src/application/login.ts +0 -87
  10. package/src/application/update-promote.ts +0 -88
  11. package/src/application/update-publish.ts +0 -402
  12. package/src/application/update-rollback.ts +0 -275
  13. package/src/commands/analytics/adoption.ts +0 -40
  14. package/src/commands/analytics/channels.ts +0 -35
  15. package/src/commands/analytics/helpers.ts +0 -3
  16. package/src/commands/analytics/index.ts +0 -13
  17. package/src/commands/analytics/platforms.ts +0 -39
  18. package/src/commands/analytics/updates.ts +0 -35
  19. package/src/commands/audit-logs/helpers.ts +0 -3
  20. package/src/commands/audit-logs/index.ts +0 -8
  21. package/src/commands/audit-logs/list.ts +0 -66
  22. package/src/commands/branches.ts +0 -70
  23. package/src/commands/build/android.ts +0 -129
  24. package/src/commands/build/index.ts +0 -63
  25. package/src/commands/build/ios.ts +0 -199
  26. package/src/commands/build/reserve-and-upload.test.ts +0 -263
  27. package/src/commands/build/reserve-and-upload.ts +0 -160
  28. package/src/commands/build/run-step.ts +0 -131
  29. package/src/commands/builds/compatibility-matrix.ts +0 -48
  30. package/src/commands/builds/delete.ts +0 -15
  31. package/src/commands/builds/get.ts +0 -34
  32. package/src/commands/builds/helpers.ts +0 -3
  33. package/src/commands/builds/index.ts +0 -20
  34. package/src/commands/builds/install-link.ts +0 -20
  35. package/src/commands/builds/list.ts +0 -38
  36. package/src/commands/channels/create.ts +0 -37
  37. package/src/commands/channels/delete.ts +0 -15
  38. package/src/commands/channels/helpers.ts +0 -18
  39. package/src/commands/channels/index.ts +0 -24
  40. package/src/commands/channels/list.ts +0 -38
  41. package/src/commands/channels/pause.ts +0 -15
  42. package/src/commands/channels/resume.ts +0 -15
  43. package/src/commands/channels/rollout/complete.ts +0 -17
  44. package/src/commands/channels/rollout/create.ts +0 -36
  45. package/src/commands/channels/rollout/index.ts +0 -11
  46. package/src/commands/channels/rollout/revert.ts +0 -17
  47. package/src/commands/channels/rollout/update.ts +0 -23
  48. package/src/commands/channels/update.ts +0 -32
  49. package/src/commands/credentials/delete.ts +0 -24
  50. package/src/commands/credentials/index.ts +0 -10
  51. package/src/commands/credentials/list.ts +0 -33
  52. package/src/commands/credentials/upload.ts +0 -91
  53. package/src/commands/env/delete.ts +0 -35
  54. package/src/commands/env/export.ts +0 -27
  55. package/src/commands/env/get.ts +0 -25
  56. package/src/commands/env/helpers.ts +0 -13
  57. package/src/commands/env/import.ts +0 -31
  58. package/src/commands/env/index.ts +0 -24
  59. package/src/commands/env/list.ts +0 -44
  60. package/src/commands/env/pull.ts +0 -27
  61. package/src/commands/env/set.ts +0 -42
  62. package/src/commands/fingerprint/compare.ts +0 -25
  63. package/src/commands/fingerprint/generate.ts +0 -18
  64. package/src/commands/fingerprint/index.ts +0 -9
  65. package/src/commands/init.ts +0 -35
  66. package/src/commands/login.ts +0 -13
  67. package/src/commands/logout.ts +0 -12
  68. package/src/commands/projects.ts +0 -84
  69. package/src/commands/status.ts +0 -48
  70. package/src/commands/update/delete.ts +0 -15
  71. package/src/commands/update/helpers.ts +0 -22
  72. package/src/commands/update/index.ts +0 -22
  73. package/src/commands/update/list.ts +0 -60
  74. package/src/commands/update/promote.ts +0 -30
  75. package/src/commands/update/publish.ts +0 -94
  76. package/src/commands/update/rollback.ts +0 -42
  77. package/src/commands/update/rollout/complete.ts +0 -17
  78. package/src/commands/update/rollout/index.ts +0 -10
  79. package/src/commands/update/rollout/revert.ts +0 -17
  80. package/src/commands/update/rollout/set.ts +0 -23
  81. package/src/index.ts +0 -53
  82. package/src/lib/android-keystore.test.ts +0 -114
  83. package/src/lib/android-keystore.ts +0 -76
  84. package/src/lib/android-signing-gradle.test.ts +0 -95
  85. package/src/lib/android-signing-gradle.ts +0 -52
  86. package/src/lib/app-json.ts +0 -81
  87. package/src/lib/apple-auth.test.ts +0 -402
  88. package/src/lib/apple-auth.ts +0 -132
  89. package/src/lib/artifact-finder.test.ts +0 -195
  90. package/src/lib/artifact-finder.ts +0 -122
  91. package/src/lib/browser-login.test.ts +0 -88
  92. package/src/lib/browser-login.ts +0 -193
  93. package/src/lib/build-profile.test.ts +0 -290
  94. package/src/lib/build-profile.ts +0 -234
  95. package/src/lib/cli-schemas.ts +0 -39
  96. package/src/lib/command-errors.ts +0 -60
  97. package/src/lib/credentials-downloader.ts +0 -181
  98. package/src/lib/credentials-manager.ts +0 -354
  99. package/src/lib/env-exporter.test.ts +0 -96
  100. package/src/lib/env-exporter.ts +0 -28
  101. package/src/lib/exit-codes.ts +0 -82
  102. package/src/lib/expo-config.ts +0 -130
  103. package/src/lib/expo-export.test.ts +0 -94
  104. package/src/lib/expo-export.ts +0 -281
  105. package/src/lib/fingerprint.ts +0 -67
  106. package/src/lib/format-error.ts +0 -22
  107. package/src/lib/git-context.ts +0 -56
  108. package/src/lib/gradle-config.ts +0 -126
  109. package/src/lib/ios-export-options.test.ts +0 -98
  110. package/src/lib/ios-export-options.ts +0 -62
  111. package/src/lib/ios-keychain.ts +0 -181
  112. package/src/lib/ios-provisioning.test.ts +0 -115
  113. package/src/lib/ios-provisioning.ts +0 -179
  114. package/src/lib/output.ts +0 -32
  115. package/src/lib/pkcs12.ts +0 -73
  116. package/src/lib/plist.ts +0 -39
  117. package/src/lib/post-build-validation.ts +0 -146
  118. package/src/lib/presigned-upload.test.ts +0 -140
  119. package/src/lib/presigned-upload.ts +0 -35
  120. package/src/lib/record.ts +0 -5
  121. package/src/lib/resolve-named-resource.ts +0 -24
  122. package/src/lib/runtime-version.test.ts +0 -119
  123. package/src/lib/runtime-version.ts +0 -62
  124. package/src/lib/sha256.test.ts +0 -108
  125. package/src/lib/sha256.ts +0 -80
  126. package/src/lib/signed-payloads.test.ts +0 -181
  127. package/src/lib/signed-payloads.ts +0 -164
  128. package/src/lib/string-utils.ts +0 -4
  129. package/src/lib/temp-dir.ts +0 -14
  130. package/src/lib/test-utils.ts +0 -13
  131. package/src/lib/update-platforms.test.ts +0 -45
  132. package/src/lib/update-platforms.ts +0 -19
  133. package/src/lib/xcpretty-formatter.ts +0 -21
  134. package/src/services/api-client.ts +0 -42
  135. package/src/services/apple-session-store.ts +0 -100
  136. package/src/services/auth-store.ts +0 -85
  137. package/src/services/cli-runtime.ts +0 -46
  138. package/src/services/config-store.ts +0 -108
  139. package/src/services/presigned-upload.ts +0 -84
  140. package/src/services/update-asset-uploader.ts +0 -72
  141. package/src/types/keychain.d.ts +0 -22
  142. package/tests/e2e/build.test.ts +0 -270
  143. package/tests/e2e/commands.test.ts +0 -694
  144. package/tests/e2e/ota-lifecycle.test.ts +0 -275
  145. package/tests/e2e/publish.test.ts +0 -150
  146. package/tests/helpers/cli-e2e.ts +0 -426
  147. package/tests/helpers/pty-driver.ts +0 -142
  148. package/tests/interactive/harness/provider-prompt.ts +0 -54
  149. package/tests/interactive/login.test.ts +0 -47
  150. package/tests/interactive/provider-select.test.ts +0 -59
  151. package/tsconfig.json +0 -7
  152. package/vitest.config.ts +0 -38
@@ -1,402 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import path from "node:path";
3
-
4
- import { fromHex, toBase64Url } from "@better-update/encoding";
5
- import { Effect } from "effect";
6
- import { uniqBy } from "es-toolkit";
7
-
8
- import type { CommandExecutor, FileSystem } from "@effect/platform";
9
-
10
- import { readAppJson, readProjectId, readSlug } from "../lib/app-json";
11
- import { readRuntimeVersionMeta } from "../lib/build-profile";
12
- import { pullEnvVars } from "../lib/env-exporter";
13
- import { UpdatePublishError } from "../lib/exit-codes";
14
- import { readExpoExportAssets, readExpoPublicConfig, runExpoExport } from "../lib/expo-export";
15
- import { formatCause } from "../lib/format-error";
16
- import { readGitContext } from "../lib/git-context";
17
- import { resolveRuntimeVersion } from "../lib/runtime-version";
18
- import { sha256File, sha256Namespaced } from "../lib/sha256";
19
- import { loadSignedPublishPayloads } from "../lib/signed-payloads";
20
- import { acquireBuildTempDir } from "../lib/temp-dir";
21
- import { resolveUpdatePlatforms } from "../lib/update-platforms";
22
- import { apiClient } from "../services/api-client";
23
- import { CliRuntime } from "../services/cli-runtime";
24
- import { UpdateAssetUploader } from "../services/update-asset-uploader";
25
-
26
- import type { Platform } from "../lib/build-profile";
27
- import type {
28
- AuthRequiredError,
29
- BuildProfileError,
30
- BuildFailedError,
31
- ProjectNotLinkedError,
32
- EnvExportError,
33
- RuntimeVersionError,
34
- } from "../lib/exit-codes";
35
- import type { SignedPayload } from "../lib/signed-payloads";
36
- import type { ApiClientService } from "../services/api-client";
37
-
38
- export interface RunUpdatePublishOptions {
39
- readonly branch: string | undefined;
40
- readonly platform: Platform | "all";
41
- readonly message: string | undefined;
42
- readonly auto: boolean;
43
- readonly environment: string;
44
- readonly clear: boolean;
45
- readonly rolloutPercentage: number | undefined;
46
- readonly manifestBodyFile: string | undefined;
47
- readonly signatureFile: string | undefined;
48
- readonly certificateChainFile: string | undefined;
49
- readonly manifestBodyFileIos: string | undefined;
50
- readonly signatureFileIos: string | undefined;
51
- readonly certificateChainFileIos: string | undefined;
52
- readonly manifestBodyFileAndroid: string | undefined;
53
- readonly signatureFileAndroid: string | undefined;
54
- readonly certificateChainFileAndroid: string | undefined;
55
- }
56
-
57
- export interface PublishedPlatformResult {
58
- readonly platform: Platform;
59
- readonly updateId: string;
60
- readonly runtimeVersion: string;
61
- readonly uploadedAssets: number;
62
- readonly deduplicatedAssets: number;
63
- }
64
-
65
- export interface PublishUpdatesResult {
66
- readonly groupId: string;
67
- readonly branch: string;
68
- readonly results: readonly PublishedPlatformResult[];
69
- }
70
-
71
- interface PreparedAsset {
72
- readonly path: string;
73
- readonly key: string;
74
- readonly hash: string;
75
- readonly contentChecksum: string;
76
- readonly byteSize: number;
77
- readonly contentType: string;
78
- readonly fileExt: string;
79
- readonly isLaunch: boolean;
80
- }
81
-
82
- const buildUpdateExtra = (
83
- expoClient: Record<string, unknown>,
84
- projectId: string,
85
- environment: string,
86
- ) => ({
87
- expoClient,
88
- eas: { projectId },
89
- environment,
90
- });
91
-
92
- const dedupeAssetsByHash = (assets: readonly PreparedAsset[]): readonly PreparedAsset[] =>
93
- uniqBy(assets, (asset) => asset.hash);
94
-
95
- const preparePlatformAssets = ({
96
- exportDir,
97
- platform,
98
- }: {
99
- readonly exportDir: string;
100
- readonly platform: Platform;
101
- }): Effect.Effect<
102
- readonly PreparedAsset[],
103
- UpdatePublishError | BuildFailedError,
104
- FileSystem.FileSystem
105
- > =>
106
- Effect.gen(function* () {
107
- const exportedAssets = yield* readExpoExportAssets({ exportDir, platform });
108
- return yield* Effect.forEach(
109
- exportedAssets,
110
- (asset) =>
111
- sha256File(asset.path).pipe(
112
- Effect.map(({ sha256: contentSha256Hex, byteSize }) => ({
113
- ...asset,
114
- hash: sha256Namespaced(asset.contentType, contentSha256Hex),
115
- contentChecksum: toBase64Url(fromHex(contentSha256Hex)),
116
- byteSize,
117
- })),
118
- ),
119
- { concurrency: 4 },
120
- );
121
- });
122
-
123
- const publishPlatform = (params: {
124
- readonly projectRoot: string;
125
- readonly exportDir: string;
126
- readonly projectId: string;
127
- readonly slug: string;
128
- readonly branch: string;
129
- readonly groupId: string;
130
- readonly message: string;
131
- readonly environment: string;
132
- readonly environmentVars: Record<string, string>;
133
- readonly expoClientConfig: Record<string, unknown>;
134
- readonly clear: boolean;
135
- readonly appJson: Record<string, unknown>;
136
- readonly platform: Platform;
137
- readonly signedPayload: SignedPayload | null;
138
- readonly rolloutPercentage: number | undefined;
139
- }): Effect.Effect<
140
- PublishedPlatformResult,
141
- | AuthRequiredError
142
- | UpdatePublishError
143
- | BuildProfileError
144
- | BuildFailedError
145
- | RuntimeVersionError,
146
- | ApiClientService
147
- | CliRuntime
148
- | UpdateAssetUploader
149
- | CommandExecutor.CommandExecutor
150
- | FileSystem.FileSystem
151
- > =>
152
- Effect.gen(function* () {
153
- const api = yield* apiClient;
154
- const assetUploader = yield* UpdateAssetUploader;
155
-
156
- const runtimeVersionMeta = yield* readRuntimeVersionMeta(params.appJson);
157
- const runtimeVersion = yield* resolveRuntimeVersion({
158
- raw: runtimeVersionMeta.rawRuntimeVersion,
159
- appVersion: runtimeVersionMeta.appVersion,
160
- projectRoot: params.projectRoot,
161
- });
162
-
163
- yield* runExpoExport({
164
- projectRoot: params.projectRoot,
165
- exportDir: params.exportDir,
166
- platform: params.platform,
167
- envVars: params.environmentVars,
168
- clear: params.clear,
169
- });
170
-
171
- const preparedAssets = yield* preparePlatformAssets({
172
- exportDir: params.exportDir,
173
- platform: params.platform,
174
- });
175
- const uniqueAssets = dedupeAssetsByHash(preparedAssets);
176
-
177
- const assetRegistration = yield* api.assets
178
- .upload({
179
- payload: {
180
- projectId: params.projectId,
181
- assets: uniqueAssets.map((asset) => ({
182
- hash: asset.hash,
183
- contentType: asset.contentType,
184
- fileExt: asset.fileExt,
185
- contentChecksum: asset.contentChecksum,
186
- })),
187
- },
188
- })
189
- .pipe(
190
- Effect.mapError(
191
- (cause) =>
192
- new UpdatePublishError({
193
- message: `Failed to register ${params.platform} assets: ${formatCause(cause)}`,
194
- }),
195
- ),
196
- );
197
-
198
- const uploadDetailsByHash = new Map(
199
- assetRegistration.uploaded.map((asset) => [asset.hash, asset] as const),
200
- );
201
- yield* Effect.forEach(
202
- uniqueAssets.filter((asset) => uploadDetailsByHash.has(asset.hash)),
203
- (asset) =>
204
- Effect.gen(function* () {
205
- const detail = uploadDetailsByHash.get(asset.hash);
206
- if (!detail) {
207
- return yield* Effect.fail(
208
- new UpdatePublishError({
209
- message: `Missing upload details for asset ${asset.hash}`,
210
- }),
211
- );
212
- }
213
- return yield* assetUploader.uploadAssetBinary({
214
- path: asset.path,
215
- hash: asset.hash,
216
- byteSize: asset.byteSize,
217
- uploadUrl: detail.uploadUrl,
218
- uploadExpiresAt: detail.uploadExpiresAt,
219
- uploadHeaders: detail.uploadHeaders,
220
- });
221
- }),
222
- { concurrency: 4 },
223
- );
224
-
225
- const update = yield* api.updates
226
- .create({
227
- payload: {
228
- branch: params.branch,
229
- slug: params.slug,
230
- runtimeVersion,
231
- platform: params.platform,
232
- message: params.message,
233
- groupId: params.groupId,
234
- metadata: {},
235
- extra: buildUpdateExtra(params.expoClientConfig, params.projectId, params.environment),
236
- assets: preparedAssets.map((asset) => ({
237
- hash: asset.hash,
238
- key: asset.key,
239
- isLaunch: asset.isLaunch,
240
- contentChecksum: asset.contentChecksum,
241
- })),
242
- ...(params.signedPayload
243
- ? {
244
- manifestBody: params.signedPayload.manifestBody,
245
- signature: params.signedPayload.signature,
246
- certificateChain: params.signedPayload.certificateChain,
247
- }
248
- : {}),
249
- ...(params.rolloutPercentage === undefined
250
- ? {}
251
- : { rolloutPercentage: params.rolloutPercentage }),
252
- },
253
- })
254
- .pipe(
255
- Effect.mapError(
256
- (cause) =>
257
- new UpdatePublishError({
258
- message: `Failed to publish ${params.platform} update: ${formatCause(cause)}`,
259
- }),
260
- ),
261
- );
262
-
263
- return {
264
- platform: params.platform,
265
- updateId: update.id,
266
- runtimeVersion,
267
- uploadedAssets: assetRegistration.uploaded.length,
268
- deduplicatedAssets: assetRegistration.deduplicated.length,
269
- } as const satisfies PublishedPlatformResult;
270
- });
271
-
272
- export const runUpdatePublish = (
273
- options: RunUpdatePublishOptions,
274
- ): Effect.Effect<
275
- PublishUpdatesResult,
276
- | AuthRequiredError
277
- | UpdatePublishError
278
- | ProjectNotLinkedError
279
- | BuildProfileError
280
- | RuntimeVersionError
281
- | EnvExportError
282
- | BuildFailedError,
283
- | ApiClientService
284
- | CliRuntime
285
- | UpdateAssetUploader
286
- | CommandExecutor.CommandExecutor
287
- | FileSystem.FileSystem
288
- > =>
289
- Effect.scoped(
290
- // eslint-disable-next-line eslint/max-statements -- update publish orchestration is inherently sequential (read config → resolve runtime version → expo export → register assets → publish per platform); splitting further fragments the pipeline without improving readability
291
- Effect.gen(function* () {
292
- const runtime = yield* CliRuntime;
293
- const projectRoot = yield* runtime.cwd;
294
- const api = yield* apiClient;
295
-
296
- const projectId = yield* readProjectId;
297
- const slug = yield* readSlug;
298
- const appJson = yield* readAppJson;
299
- const platforms = resolveUpdatePlatforms(appJson, options.platform);
300
- if (platforms.length === 0) {
301
- return yield* new UpdatePublishError({
302
- message:
303
- 'No publishable platforms found in app.json. Add an "expo.ios" or "expo.android" section, or pass --platform explicitly.',
304
- });
305
- }
306
-
307
- const environmentVars = yield* pullEnvVars(api, {
308
- projectId,
309
- environment: options.environment,
310
- });
311
- const expoClientConfig = yield* readExpoPublicConfig({
312
- projectRoot,
313
- envVars: environmentVars,
314
- });
315
- const tempDir = yield* acquireBuildTempDir.pipe(
316
- Effect.mapError(
317
- (cause) =>
318
- new UpdatePublishError({
319
- message: `Failed to create a temporary export directory: ${formatCause(cause)}`,
320
- }),
321
- ),
322
- );
323
- let resolvedBranch = options.branch;
324
- let resolvedMessage = options.message;
325
-
326
- if (options.auto) {
327
- const gitContext = yield* readGitContext(projectRoot);
328
- if (!resolvedBranch) {
329
- if (!gitContext.ref) {
330
- return yield* new UpdatePublishError({
331
- message:
332
- "Cannot infer branch from git. Ensure you are in a git repo with a checked-out branch, or provide --branch explicitly.",
333
- });
334
- }
335
- resolvedBranch = gitContext.ref;
336
- }
337
- if (!resolvedMessage && gitContext.commitMessage) {
338
- resolvedMessage = gitContext.commitMessage;
339
- }
340
- }
341
-
342
- if (!resolvedBranch) {
343
- return yield* new UpdatePublishError({
344
- message: "Missing --branch. Provide it explicitly or use --auto to infer from git.",
345
- });
346
- }
347
-
348
- const branch = resolvedBranch;
349
- const groupId = randomUUID();
350
- const message = resolvedMessage ?? "Publish via better-update CLI";
351
- const signedPayloads = yield* loadSignedPublishPayloads({
352
- platforms,
353
- globalFiles: {
354
- manifestBodyFile: options.manifestBodyFile,
355
- signatureFile: options.signatureFile,
356
- certificateChainFile: options.certificateChainFile,
357
- },
358
- platformFiles: {
359
- ios: {
360
- manifestBodyFile: options.manifestBodyFileIos,
361
- signatureFile: options.signatureFileIos,
362
- certificateChainFile: options.certificateChainFileIos,
363
- },
364
- android: {
365
- manifestBodyFile: options.manifestBodyFileAndroid,
366
- signatureFile: options.signatureFileAndroid,
367
- certificateChainFile: options.certificateChainFileAndroid,
368
- },
369
- },
370
- makeError: (errorMessage) => new UpdatePublishError({ message: errorMessage }),
371
- });
372
- const results = yield* Effect.forEach(
373
- platforms,
374
- (platform) =>
375
- publishPlatform({
376
- projectRoot,
377
- exportDir: path.join(tempDir, `export-${platform}`),
378
- projectId,
379
- slug,
380
- branch,
381
- groupId,
382
- message,
383
- environment: options.environment,
384
- environmentVars,
385
- expoClientConfig,
386
- clear: options.clear,
387
- appJson,
388
- platform,
389
- // eslint-disable-next-line eslint-js/no-restricted-syntax -- signedPayload absence means unsigned; null is correct downstream
390
- signedPayload: signedPayloads[platform] ?? null,
391
- rolloutPercentage: options.rolloutPercentage,
392
- }),
393
- { concurrency: 1 },
394
- );
395
-
396
- return {
397
- groupId,
398
- branch,
399
- results,
400
- } as const satisfies PublishUpdatesResult;
401
- }),
402
- );
@@ -1,275 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
-
3
- import { buildRollbackDirectiveBody } from "@better-update/expo-protocol";
4
- import { FileSystem } from "@effect/platform";
5
- import { Effect } from "effect";
6
-
7
- import type { CommandExecutor } from "@effect/platform";
8
-
9
- import { readAppJson, readProjectId, readSlug } from "../lib/app-json";
10
- import { readRuntimeVersionMeta } from "../lib/build-profile";
11
- import { UpdateRollbackError } from "../lib/exit-codes";
12
- import { formatCause } from "../lib/format-error";
13
- import { isRecord } from "../lib/record";
14
- import { resolveRuntimeVersion } from "../lib/runtime-version";
15
- import { resolveUpdatePlatforms } from "../lib/update-platforms";
16
- import { apiClient } from "../services/api-client";
17
- import { CliRuntime } from "../services/cli-runtime";
18
-
19
- import type { Platform } from "../lib/build-profile";
20
- import type {
21
- AuthRequiredError,
22
- BuildProfileError,
23
- ProjectNotLinkedError,
24
- RuntimeVersionError,
25
- } from "../lib/exit-codes";
26
- import type { UpdatePlatformOption } from "../lib/update-platforms";
27
- import type { ApiClientService } from "../services/api-client";
28
-
29
- interface CreateRollbackParams {
30
- readonly branch: string;
31
- readonly projectSlug: string;
32
- readonly runtimeVersion: string;
33
- readonly platform: Platform;
34
- readonly message: string;
35
- readonly groupId: string;
36
- readonly directiveBody: string;
37
- readonly signature: string | undefined;
38
- readonly certificateChain: string | undefined;
39
- }
40
-
41
- export interface RollbackResultItem {
42
- readonly platform: Platform;
43
- readonly updateId: string;
44
- readonly runtimeVersion: string;
45
- }
46
-
47
- export interface RunUpdateRollbackOptions {
48
- readonly branch: string;
49
- readonly platform: UpdatePlatformOption;
50
- readonly message: string | undefined;
51
- readonly commitTime: string | undefined;
52
- readonly directiveBodyFile: string | undefined;
53
- readonly signatureFile: string | undefined;
54
- readonly certificateChainFile: string | undefined;
55
- }
56
-
57
- export interface UpdateRollbackResult {
58
- readonly groupId: string;
59
- readonly branch: string;
60
- readonly commitTime: string;
61
- readonly results: readonly RollbackResultItem[];
62
- }
63
-
64
- interface SignedRollbackPayload {
65
- readonly directiveBody: string;
66
- readonly signature: string;
67
- readonly certificateChain: string;
68
- }
69
-
70
- const resolveCommitTime = (input: string | undefined): Effect.Effect<string, UpdateRollbackError> =>
71
- Effect.gen(function* () {
72
- const commitTime = input ?? new Date().toISOString();
73
- if (Number.isNaN(Date.parse(commitTime))) {
74
- return yield* new UpdateRollbackError({
75
- message: "commitTime must be a valid ISO 8601 timestamp.",
76
- });
77
- }
78
- return commitTime;
79
- });
80
-
81
- const extractDirectiveCommitTime = (
82
- directiveBody: string,
83
- ): Effect.Effect<string, UpdateRollbackError> =>
84
- Effect.gen(function* () {
85
- const directive = yield* Effect.try({
86
- try: (): unknown => JSON.parse(directiveBody),
87
- catch: () =>
88
- new UpdateRollbackError({
89
- message: "directiveBody must be valid JSON.",
90
- }),
91
- });
92
-
93
- if (!isRecord(directive)) {
94
- return yield* new UpdateRollbackError({
95
- message: "directiveBody must decode to a JSON object.",
96
- });
97
- }
98
-
99
- if (directive["type"] !== "rollBackToEmbedded") {
100
- return yield* new UpdateRollbackError({
101
- message: 'directiveBody.type must be "rollBackToEmbedded".',
102
- });
103
- }
104
-
105
- const { parameters } = directive;
106
- if (!isRecord(parameters)) {
107
- return yield* new UpdateRollbackError({
108
- message: "directiveBody.parameters must be an object.",
109
- });
110
- }
111
-
112
- const { commitTime } = parameters;
113
- if (typeof commitTime !== "string" || Number.isNaN(Date.parse(commitTime))) {
114
- return yield* new UpdateRollbackError({
115
- message: "directiveBody.parameters.commitTime must be a valid ISO 8601 timestamp.",
116
- });
117
- }
118
-
119
- return commitTime;
120
- });
121
-
122
- const loadOptionalSignedRollbackPayload = (
123
- options: RunUpdateRollbackOptions,
124
- ): Effect.Effect<SignedRollbackPayload | null, UpdateRollbackError, FileSystem.FileSystem> =>
125
- Effect.gen(function* () {
126
- const fileSystem = yield* FileSystem.FileSystem;
127
- const hasAnySigningInput =
128
- options.directiveBodyFile !== undefined ||
129
- options.signatureFile !== undefined ||
130
- options.certificateChainFile !== undefined;
131
-
132
- if (!hasAnySigningInput) {
133
- return null;
134
- }
135
-
136
- if (!options.directiveBodyFile || !options.signatureFile || !options.certificateChainFile) {
137
- return yield* new UpdateRollbackError({
138
- message:
139
- "Signed rollback requires --directive-body-file, --signature-file, and --certificate-chain-file together.",
140
- });
141
- }
142
-
143
- const [directiveBody, signature, certificateChain] = yield* Effect.all(
144
- [
145
- fileSystem.readFileString(options.directiveBodyFile),
146
- fileSystem.readFileString(options.signatureFile),
147
- fileSystem.readFileString(options.certificateChainFile),
148
- ],
149
- { concurrency: "unbounded" },
150
- ).pipe(
151
- Effect.mapError(
152
- (cause) =>
153
- new UpdateRollbackError({
154
- message: `Failed to read signed rollback inputs: ${formatCause(cause)}`,
155
- }),
156
- ),
157
- );
158
-
159
- return {
160
- directiveBody,
161
- signature: signature.trim(),
162
- certificateChain: certificateChain.trimEnd(),
163
- } satisfies SignedRollbackPayload;
164
- });
165
-
166
- const createRollbackForPlatform = (
167
- params: CreateRollbackParams,
168
- ): Effect.Effect<RollbackResultItem, AuthRequiredError | UpdateRollbackError, ApiClientService> =>
169
- Effect.gen(function* () {
170
- const api = yield* apiClient;
171
- const update = yield* api.updates
172
- .create({
173
- payload: {
174
- branch: params.branch,
175
- slug: params.projectSlug,
176
- runtimeVersion: params.runtimeVersion,
177
- platform: params.platform,
178
- message: params.message,
179
- groupId: params.groupId,
180
- metadata: {},
181
- assets: [],
182
- isRollback: true,
183
- directiveBody: params.directiveBody,
184
- ...(params.signature ? { signature: params.signature } : {}),
185
- ...(params.certificateChain ? { certificateChain: params.certificateChain } : {}),
186
- },
187
- })
188
- .pipe(
189
- Effect.mapError(
190
- (cause) =>
191
- new UpdateRollbackError({
192
- message: `Failed to create ${params.platform} rollback: ${formatCause(cause)}`,
193
- }),
194
- ),
195
- );
196
-
197
- return {
198
- platform: params.platform,
199
- updateId: update.id,
200
- runtimeVersion: params.runtimeVersion,
201
- } as const satisfies RollbackResultItem;
202
- });
203
-
204
- export const runUpdateRollback = (
205
- options: RunUpdateRollbackOptions,
206
- ): Effect.Effect<
207
- UpdateRollbackResult,
208
- | AuthRequiredError
209
- | ProjectNotLinkedError
210
- | BuildProfileError
211
- | RuntimeVersionError
212
- | UpdateRollbackError,
213
- ApiClientService | CliRuntime | CommandExecutor.CommandExecutor | FileSystem.FileSystem
214
- > =>
215
- Effect.gen(function* () {
216
- const runtime = yield* CliRuntime;
217
- const projectRoot = yield* runtime.cwd;
218
- yield* readProjectId;
219
- const projectSlug = yield* readSlug;
220
- const appJson = yield* readAppJson;
221
- const platforms = resolveUpdatePlatforms(appJson, options.platform);
222
- if (platforms.length === 0) {
223
- return yield* new UpdateRollbackError({
224
- message:
225
- 'No publishable platforms found in app.json. Add an "expo.ios" or "expo.android" section, or pass --platform explicitly.',
226
- });
227
- }
228
-
229
- const { appVersion, rawRuntimeVersion } = yield* readRuntimeVersionMeta(appJson);
230
- const runtimeVersion = yield* resolveRuntimeVersion({
231
- raw: rawRuntimeVersion,
232
- appVersion,
233
- projectRoot,
234
- });
235
- const signedPayload = yield* loadOptionalSignedRollbackPayload(options);
236
- const commitTime = signedPayload
237
- ? yield* Effect.gen(function* () {
238
- const directiveCommitTime = yield* extractDirectiveCommitTime(
239
- signedPayload.directiveBody,
240
- );
241
- if (options.commitTime && options.commitTime !== directiveCommitTime) {
242
- return yield* new UpdateRollbackError({
243
- message: "commitTime must match directiveBody.parameters.commitTime in signed mode.",
244
- });
245
- }
246
- return directiveCommitTime;
247
- })
248
- : yield* resolveCommitTime(options.commitTime);
249
- const groupId = randomUUID();
250
- const message = options.message ?? "Rollback to embedded via better-update CLI";
251
-
252
- const results = yield* Effect.forEach(
253
- platforms,
254
- (platform) =>
255
- createRollbackForPlatform({
256
- branch: options.branch,
257
- projectSlug,
258
- runtimeVersion,
259
- platform,
260
- message,
261
- groupId,
262
- directiveBody: signedPayload?.directiveBody ?? buildRollbackDirectiveBody(commitTime),
263
- signature: signedPayload?.signature,
264
- certificateChain: signedPayload?.certificateChain,
265
- }),
266
- { concurrency: 1 },
267
- );
268
-
269
- return {
270
- groupId,
271
- branch: options.branch,
272
- commitTime,
273
- results,
274
- } as const satisfies UpdateRollbackResult;
275
- });