@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.
- package/dist/index.js +5319 -0
- package/dist/index.js.map +1 -0
- package/package.json +12 -9
- package/CHANGELOG.md +0 -58
- package/oxlint.config.ts +0 -6
- package/src/app-layer.ts +0 -29
- package/src/application/build-workflow.ts +0 -222
- package/src/application/command-exit.ts +0 -13
- package/src/application/login.ts +0 -87
- package/src/application/update-promote.ts +0 -88
- package/src/application/update-publish.ts +0 -402
- package/src/application/update-rollback.ts +0 -275
- package/src/commands/analytics/adoption.ts +0 -40
- package/src/commands/analytics/channels.ts +0 -35
- package/src/commands/analytics/helpers.ts +0 -3
- package/src/commands/analytics/index.ts +0 -13
- package/src/commands/analytics/platforms.ts +0 -39
- package/src/commands/analytics/updates.ts +0 -35
- package/src/commands/audit-logs/helpers.ts +0 -3
- package/src/commands/audit-logs/index.ts +0 -8
- package/src/commands/audit-logs/list.ts +0 -66
- package/src/commands/branches.ts +0 -70
- package/src/commands/build/android.ts +0 -129
- package/src/commands/build/index.ts +0 -63
- package/src/commands/build/ios.ts +0 -199
- package/src/commands/build/reserve-and-upload.test.ts +0 -263
- package/src/commands/build/reserve-and-upload.ts +0 -160
- package/src/commands/build/run-step.ts +0 -131
- package/src/commands/builds/compatibility-matrix.ts +0 -48
- package/src/commands/builds/delete.ts +0 -15
- package/src/commands/builds/get.ts +0 -34
- package/src/commands/builds/helpers.ts +0 -3
- package/src/commands/builds/index.ts +0 -20
- package/src/commands/builds/install-link.ts +0 -20
- package/src/commands/builds/list.ts +0 -38
- package/src/commands/channels/create.ts +0 -37
- package/src/commands/channels/delete.ts +0 -15
- package/src/commands/channels/helpers.ts +0 -18
- package/src/commands/channels/index.ts +0 -24
- package/src/commands/channels/list.ts +0 -38
- package/src/commands/channels/pause.ts +0 -15
- package/src/commands/channels/resume.ts +0 -15
- package/src/commands/channels/rollout/complete.ts +0 -17
- package/src/commands/channels/rollout/create.ts +0 -36
- package/src/commands/channels/rollout/index.ts +0 -11
- package/src/commands/channels/rollout/revert.ts +0 -17
- package/src/commands/channels/rollout/update.ts +0 -23
- package/src/commands/channels/update.ts +0 -32
- package/src/commands/credentials/delete.ts +0 -24
- package/src/commands/credentials/index.ts +0 -10
- package/src/commands/credentials/list.ts +0 -33
- package/src/commands/credentials/upload.ts +0 -91
- package/src/commands/env/delete.ts +0 -35
- package/src/commands/env/export.ts +0 -27
- package/src/commands/env/get.ts +0 -25
- package/src/commands/env/helpers.ts +0 -13
- package/src/commands/env/import.ts +0 -31
- package/src/commands/env/index.ts +0 -24
- package/src/commands/env/list.ts +0 -44
- package/src/commands/env/pull.ts +0 -27
- package/src/commands/env/set.ts +0 -42
- package/src/commands/fingerprint/compare.ts +0 -25
- package/src/commands/fingerprint/generate.ts +0 -18
- package/src/commands/fingerprint/index.ts +0 -9
- package/src/commands/init.ts +0 -35
- package/src/commands/login.ts +0 -13
- package/src/commands/logout.ts +0 -12
- package/src/commands/projects.ts +0 -84
- package/src/commands/status.ts +0 -48
- package/src/commands/update/delete.ts +0 -15
- package/src/commands/update/helpers.ts +0 -22
- package/src/commands/update/index.ts +0 -22
- package/src/commands/update/list.ts +0 -60
- package/src/commands/update/promote.ts +0 -30
- package/src/commands/update/publish.ts +0 -94
- package/src/commands/update/rollback.ts +0 -42
- package/src/commands/update/rollout/complete.ts +0 -17
- package/src/commands/update/rollout/index.ts +0 -10
- package/src/commands/update/rollout/revert.ts +0 -17
- package/src/commands/update/rollout/set.ts +0 -23
- package/src/index.ts +0 -53
- package/src/lib/android-keystore.test.ts +0 -114
- package/src/lib/android-keystore.ts +0 -76
- package/src/lib/android-signing-gradle.test.ts +0 -95
- package/src/lib/android-signing-gradle.ts +0 -52
- package/src/lib/app-json.ts +0 -81
- package/src/lib/apple-auth.test.ts +0 -402
- package/src/lib/apple-auth.ts +0 -132
- package/src/lib/artifact-finder.test.ts +0 -195
- package/src/lib/artifact-finder.ts +0 -122
- package/src/lib/browser-login.test.ts +0 -88
- package/src/lib/browser-login.ts +0 -193
- package/src/lib/build-profile.test.ts +0 -290
- package/src/lib/build-profile.ts +0 -234
- package/src/lib/cli-schemas.ts +0 -39
- package/src/lib/command-errors.ts +0 -60
- package/src/lib/credentials-downloader.ts +0 -181
- package/src/lib/credentials-manager.ts +0 -354
- package/src/lib/env-exporter.test.ts +0 -96
- package/src/lib/env-exporter.ts +0 -28
- package/src/lib/exit-codes.ts +0 -82
- package/src/lib/expo-config.ts +0 -130
- package/src/lib/expo-export.test.ts +0 -94
- package/src/lib/expo-export.ts +0 -281
- package/src/lib/fingerprint.ts +0 -67
- package/src/lib/format-error.ts +0 -22
- package/src/lib/git-context.ts +0 -56
- package/src/lib/gradle-config.ts +0 -126
- package/src/lib/ios-export-options.test.ts +0 -98
- package/src/lib/ios-export-options.ts +0 -62
- package/src/lib/ios-keychain.ts +0 -181
- package/src/lib/ios-provisioning.test.ts +0 -115
- package/src/lib/ios-provisioning.ts +0 -179
- package/src/lib/output.ts +0 -32
- package/src/lib/pkcs12.ts +0 -73
- package/src/lib/plist.ts +0 -39
- package/src/lib/post-build-validation.ts +0 -146
- package/src/lib/presigned-upload.test.ts +0 -140
- package/src/lib/presigned-upload.ts +0 -35
- package/src/lib/record.ts +0 -5
- package/src/lib/resolve-named-resource.ts +0 -24
- package/src/lib/runtime-version.test.ts +0 -119
- package/src/lib/runtime-version.ts +0 -62
- package/src/lib/sha256.test.ts +0 -108
- package/src/lib/sha256.ts +0 -80
- package/src/lib/signed-payloads.test.ts +0 -181
- package/src/lib/signed-payloads.ts +0 -164
- package/src/lib/string-utils.ts +0 -4
- package/src/lib/temp-dir.ts +0 -14
- package/src/lib/test-utils.ts +0 -13
- package/src/lib/update-platforms.test.ts +0 -45
- package/src/lib/update-platforms.ts +0 -19
- package/src/lib/xcpretty-formatter.ts +0 -21
- package/src/services/api-client.ts +0 -42
- package/src/services/apple-session-store.ts +0 -100
- package/src/services/auth-store.ts +0 -85
- package/src/services/cli-runtime.ts +0 -46
- package/src/services/config-store.ts +0 -108
- package/src/services/presigned-upload.ts +0 -84
- package/src/services/update-asset-uploader.ts +0 -72
- package/src/types/keychain.d.ts +0 -22
- package/tests/e2e/build.test.ts +0 -270
- package/tests/e2e/commands.test.ts +0 -694
- package/tests/e2e/ota-lifecycle.test.ts +0 -275
- package/tests/e2e/publish.test.ts +0 -150
- package/tests/helpers/cli-e2e.ts +0 -426
- package/tests/helpers/pty-driver.ts +0 -142
- package/tests/interactive/harness/provider-prompt.ts +0 -54
- package/tests/interactive/login.test.ts +0 -47
- package/tests/interactive/provider-select.test.ts +0 -59
- package/tsconfig.json +0 -7
- 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
|
-
});
|