@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,694 +0,0 @@
1
- import { execFileSync } from "node:child_process";
2
- import { createHash, randomUUID } from "node:crypto";
3
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
- import os from "node:os";
5
- import path from "node:path";
6
- import { setTimeout as sleep } from "node:timers/promises";
7
-
8
- import { setupCliE2E } from "../helpers/cli-e2e";
9
-
10
- const generateSelfSignedP12 = (password: string, subject: string): Buffer => {
11
- const tmp = mkdtempSync(path.join(os.tmpdir(), "cli-e2e-p12-"));
12
- const keyPath = path.join(tmp, "key.pem");
13
- const certPath = path.join(tmp, "cert.pem");
14
- const p12Path = path.join(tmp, "cert.p12");
15
- try {
16
- execFileSync(
17
- "openssl",
18
- [
19
- "req",
20
- "-x509",
21
- "-newkey",
22
- "rsa:2048",
23
- "-sha256",
24
- "-days",
25
- "365",
26
- "-nodes",
27
- "-keyout",
28
- keyPath,
29
- "-out",
30
- certPath,
31
- "-subj",
32
- subject,
33
- ],
34
- { stdio: "pipe" },
35
- );
36
- execFileSync(
37
- "openssl",
38
- [
39
- "pkcs12",
40
- "-export",
41
- "-out",
42
- p12Path,
43
- "-inkey",
44
- keyPath,
45
- "-in",
46
- certPath,
47
- "-passout",
48
- `pass:${password}`,
49
- "-legacy",
50
- ],
51
- { stdio: "pipe" },
52
- );
53
- return readFileSync(p12Path);
54
- } finally {
55
- rmSync(tmp, { recursive: true, force: true });
56
- }
57
- };
58
-
59
- const cli = setupCliE2E(".wrangler/state/e2e-cli");
60
-
61
- const escapeRegExp = (value: string) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
62
- const sqlString = (value: string) => `'${value.replaceAll("'", "''")}'`;
63
-
64
- const getNodeErrorCode = (error: unknown): string | undefined => {
65
- if (!(error instanceof Error)) {
66
- return undefined;
67
- }
68
-
69
- const directCode = (error as NodeJS.ErrnoException).code;
70
- if (typeof directCode === "string") {
71
- return directCode;
72
- }
73
-
74
- const { cause } = error as Error & { readonly cause?: unknown };
75
- if (typeof cause !== "object" || cause === null) {
76
- return undefined;
77
- }
78
-
79
- const nestedCode = (cause as NodeJS.ErrnoException).code;
80
- return typeof nestedCode === "string" ? nestedCode : undefined;
81
- };
82
-
83
- const fetchWithRetry = async (url: string, init: RequestInit): Promise<Response> => {
84
- const maxAttempts = 4;
85
-
86
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
87
- try {
88
- return await fetch(url, init);
89
- } catch (error) {
90
- const code = getNodeErrorCode(error);
91
- if (
92
- !code ||
93
- !["ECONNRESET", "EPIPE", "UND_ERR_SOCKET"].includes(code) ||
94
- attempt === maxAttempts
95
- ) {
96
- throw error;
97
- }
98
-
99
- await sleep(attempt * 100);
100
- }
101
- }
102
-
103
- throw new Error("fetchWithRetry exhausted unexpectedly");
104
- };
105
-
106
- const seedDestinationChannel = (name: string) => {
107
- const branchId = `${name}-branch`;
108
- const channelId = `${name}-channel`;
109
- cli.seedSql(`
110
- INSERT INTO "branches" ("id", "project_id", "name", "created_at")
111
- VALUES (${sqlString(branchId)}, ${sqlString(cli.getProjectId())}, ${sqlString(name)}, '2026-04-14T00:00:00Z');
112
-
113
- INSERT INTO "channels" (
114
- "id", "project_id", "name", "branch_id", "branch_mapping_json", "cache_version", "is_paused", "created_at"
115
- )
116
- VALUES (
117
- ${sqlString(channelId)},
118
- ${sqlString(cli.getProjectId())},
119
- ${sqlString(name)},
120
- ${sqlString(branchId)},
121
- NULL,
122
- 0,
123
- 0,
124
- '2026-04-14T00:00:00Z'
125
- );
126
- `);
127
- };
128
-
129
- interface PromotableUpdate {
130
- readonly id: string;
131
- readonly groupId: string;
132
- readonly assetHash: string;
133
- readonly launchAssetKey: string;
134
- }
135
-
136
- const createPromotableUpdate = async (options?: {
137
- readonly signed?: {
138
- readonly manifestId: string;
139
- readonly createdAt: string;
140
- readonly signature: string;
141
- readonly certificateChain: string;
142
- };
143
- }): Promise<PromotableUpdate> => {
144
- const assetBody = Buffer.from("console.log('cli promote source');\n");
145
- const assetHash = createHash("sha256").update(assetBody).digest("base64url");
146
- const launchAssetKey = "bundles/ios-launch.js";
147
- const registerResponse = await cli.postAuthorized("/api/assets/upload", {
148
- projectId: cli.getProjectId(),
149
- assets: [{ hash: assetHash, contentType: "application/javascript", fileExt: "js" }],
150
- });
151
- expect(registerResponse.status).toBe(201);
152
-
153
- const registerBody = (await registerResponse.json()) as {
154
- uploaded: {
155
- hash: string;
156
- uploadUrl: string;
157
- uploadHeaders: Record<string, string>;
158
- }[];
159
- deduplicated: string[];
160
- };
161
- const upload = registerBody.uploaded.find((asset) => asset.hash === assetHash);
162
- if (upload) {
163
- const uploadResponse = await fetchWithRetry(upload.uploadUrl, {
164
- method: "PUT",
165
- headers: {
166
- "content-length": String(assetBody.byteLength),
167
- ...upload.uploadHeaders,
168
- },
169
- body: assetBody,
170
- });
171
- expect(uploadResponse.status).toBe(200);
172
-
173
- const finalizeResponse = await cli.postAuthorized(
174
- `/api/assets/${assetHash}/finalize`,
175
- undefined,
176
- );
177
- expect(finalizeResponse.status).toBe(200);
178
- } else {
179
- expect(registerBody.deduplicated).toContain(assetHash);
180
- }
181
-
182
- const manifestBody =
183
- options?.signed === undefined
184
- ? undefined
185
- : JSON.stringify({
186
- id: options.signed.manifestId,
187
- createdAt: options.signed.createdAt,
188
- runtimeVersion: "1.0.0",
189
- launchAsset: { key: launchAssetKey, hash: assetHash },
190
- assets: [],
191
- });
192
-
193
- const createUpdateResponse = await cli.postAuthorized("/api/updates", {
194
- branch: "main",
195
- slug: "cli-e2e-app",
196
- runtimeVersion: "1.0.0",
197
- platform: "ios",
198
- message: options?.signed ? "CLI signed promotable update" : "CLI promotable update",
199
- groupId: randomUUID(),
200
- metadata: {},
201
- assets: [{ hash: assetHash, key: launchAssetKey, isLaunch: true }],
202
- ...(manifestBody
203
- ? {
204
- manifestBody,
205
- signature: options?.signed?.signature,
206
- certificateChain: options?.signed?.certificateChain,
207
- }
208
- : {}),
209
- });
210
- expect(createUpdateResponse.status).toBe(201);
211
-
212
- const update = (await createUpdateResponse.json()) as {
213
- id: string;
214
- groupId: string;
215
- };
216
-
217
- return {
218
- id: update.id,
219
- groupId: update.groupId,
220
- assetHash,
221
- launchAssetKey,
222
- };
223
- };
224
-
225
- const cliState = {
226
- rollbackGroupId: "",
227
- rollbackUpdateId: "",
228
- };
229
-
230
- describe("cLI command journey", () => {
231
- it("links the current Expo app to the existing project", () => {
232
- const result = cli.runCli("init");
233
- expect(result.exitCode).toBe(0);
234
- expect(result.stderr).toBe("");
235
- expect(result.stdout).toContain("Linking project: CLI E2E App (cli-e2e-app)");
236
- expect(result.stdout).toContain("Found existing project: CLI E2E App Project");
237
- expect(result.stdout).toContain("Project linked successfully");
238
-
239
- const appJson = cli.readAppJson();
240
- expect(
241
- (
242
- ((appJson["expo"] as Record<string, unknown>)["extra"] as Record<string, unknown>)[
243
- "betterUpdate"
244
- ] as Record<string, unknown>
245
- )["projectId"],
246
- ).toBe(cli.getProjectId());
247
- });
248
-
249
- it("shows project status with credential and build counts", () => {
250
- const result = cli.runCli("status");
251
- expect(result.exitCode).toBe(0);
252
- expect(result.stderr).toBe("");
253
- expect(result.stdout).toContain("Project");
254
- expect(result.stdout).toContain("CLI E2E App Project");
255
- expect(result.stdout).toContain("cli-e2e-app");
256
- expect(result.stdout).toContain("Credentials");
257
- expect(result.stdout).toContain("iOS");
258
- expect(result.stdout).toContain("1");
259
- expect(result.stdout).toContain("Builds");
260
- });
261
-
262
- it("lists credentials for an empty project", () => {
263
- const result = cli.runCli("credentials", "list");
264
- expect(result.exitCode).toBe(0);
265
- expect(result.stderr).toBe("");
266
- expect(result.stdout).toContain("No credentials found.");
267
- });
268
-
269
- it("lists environment variables with masked secret values", () => {
270
- const result = cli.runCli("env", "list", "--environment", "production");
271
- expect(result.exitCode).toBe(0);
272
- expect(result.stderr).toBe("");
273
- expect(result.stdout).toContain("APP_SECRET");
274
- expect(result.stdout).toContain("production");
275
- expect(result.stdout).toContain("secret");
276
- expect(result.stdout).toContain("••••••");
277
- });
278
-
279
- it("imports, exports, pulls, sets, and deletes environment variables", () => {
280
- const envFile = path.join(cli.getProjectDir(), ".env.preview");
281
- writeFileSync(envFile, "EXPO_PUBLIC_API_URL=https://preview.example.com\nFEATURE_FLAG=true\n");
282
-
283
- const importResult = cli.runCli("env", "import", envFile, "--environment", "preview");
284
- expect(importResult.exitCode).toBe(0);
285
- expect(importResult.stderr).toBe("");
286
- expect(importResult.stdout).toContain("Imported: 2 created, 0 updated, 0 skipped");
287
-
288
- const exportResult = cli.runCli("env", "export", "--environment", "preview");
289
- expect(exportResult.exitCode).toBe(0);
290
- expect(exportResult.stderr).toBe("");
291
- expect(exportResult.stdout).toContain("EXPO_PUBLIC_API_URL='https://preview.example.com'");
292
- expect(exportResult.stdout).toContain("FEATURE_FLAG='true'");
293
-
294
- const pullResult = cli.runCli("env", "pull", "--environment", "preview");
295
- expect(pullResult.exitCode).toBe(0);
296
- expect(pullResult.stderr).toBe("");
297
- expect(pullResult.stdout).toContain("export EXPO_PUBLIC_API_URL='https://preview.example.com'");
298
-
299
- const createResult = cli.runCli(
300
- "env",
301
- "set",
302
- "APP_PUBLIC_URL=https://app.example.com",
303
- "--environment",
304
- "production",
305
- );
306
- expect(createResult.exitCode).toBe(0);
307
- expect(createResult.stderr).toBe("");
308
- expect(createResult.stdout).toContain("Created APP_PUBLIC_URL in production");
309
-
310
- const updateResult = cli.runCli(
311
- "env",
312
- "set",
313
- "APP_PUBLIC_URL=https://app-v2.example.com",
314
- "--environment",
315
- "production",
316
- );
317
- expect(updateResult.exitCode).toBe(0);
318
- expect(updateResult.stderr).toBe("");
319
- expect(updateResult.stdout).toContain("Updated APP_PUBLIC_URL in production");
320
-
321
- const listResult = cli.runCli("env", "list", "--environment", "production");
322
- expect(listResult.exitCode).toBe(0);
323
- expect(listResult.stderr).toBe("");
324
- expect(listResult.stdout).toContain("APP_PUBLIC_URL");
325
- expect(listResult.stdout).toContain("https://app-v2.example.com");
326
-
327
- const deleteResult = cli.runCli(
328
- "env",
329
- "delete",
330
- "APP_PUBLIC_URL",
331
- "--environment",
332
- "production",
333
- );
334
- expect(deleteResult.exitCode).toBe(0);
335
- expect(deleteResult.stderr).toBe("");
336
- expect(deleteResult.stdout).toContain("Deleted APP_PUBLIC_URL from production");
337
-
338
- const finalList = cli.runCli("env", "list", "--environment", "production");
339
- expect(finalList.exitCode).toBe(0);
340
- expect(finalList.stdout).not.toContain("APP_PUBLIC_URL");
341
- });
342
-
343
- it("lists builds for the linked project", () => {
344
- const result = cli.runCli("builds", "list");
345
- expect(result.exitCode).toBe(0);
346
- expect(result.stderr).toBe("");
347
- expect(result.stdout).toContain("cli-build-1");
348
- expect(result.stdout).toContain("ad-hoc");
349
- expect(result.stdout).toContain("production");
350
- });
351
-
352
- it("creates a rollback update from the CLI", async () => {
353
- const commitTime = "2026-04-14T00:00:00.000Z";
354
- const rollbackResult = cli.runCli(
355
- "update",
356
- "rollback",
357
- "--branch",
358
- "main",
359
- "--platform",
360
- "ios",
361
- "--commit-time",
362
- commitTime,
363
- );
364
-
365
- expect(rollbackResult.exitCode).toBe(0);
366
- expect(rollbackResult.stderr).toBe("");
367
- expect(rollbackResult.stdout).toContain("Created rollback group");
368
- expect(rollbackResult.stdout).toContain('on branch "main"');
369
- expect(rollbackResult.stdout).toContain(commitTime);
370
-
371
- const listResult = cli.runCli("update", "list", "--branch", "main");
372
- expect(listResult.exitCode).toBe(0);
373
- expect(listResult.stderr).toBe("");
374
- expect(listResult.stdout).toContain("Update ID");
375
- expect(listResult.stdout).toContain("main");
376
- expect(listResult.stdout).toContain("ios");
377
- expect(listResult.stdout).toContain("1.0.0");
378
- expect(listResult.stdout).toContain("yes");
379
- const rollbackMatch = /^([^\s]+)\s+([^\s]+)\s+main\s+ios\s+1\.0\.0\s+100%\s+yes\s+.+$/m.exec(
380
- listResult.stdout,
381
- );
382
- expect(rollbackMatch).toBeDefined();
383
- cliState.rollbackUpdateId = rollbackMatch?.[1] ?? "";
384
- cliState.rollbackGroupId = rollbackMatch?.[2] ?? "";
385
- });
386
-
387
- it("creates a signed rollback update from the CLI using pre-signed files", async () => {
388
- const signedCommitTime = "2026-04-15T00:00:00.000Z";
389
- const directiveBodyPath = path.join(cli.getProjectDir(), "signed-directive.json");
390
- const signaturePath = path.join(cli.getProjectDir(), "signed-directive.sig");
391
- const certificateChainPath = path.join(cli.getProjectDir(), "signed-directive.pem");
392
- const signedMessage = "Signed rollback via CLI";
393
-
394
- writeFileSync(
395
- directiveBodyPath,
396
- JSON.stringify({
397
- type: "rollBackToEmbedded",
398
- parameters: { commitTime: signedCommitTime },
399
- }),
400
- );
401
- writeFileSync(signaturePath, 'sig="signed-cli-test", keyid="main", alg="rsa-v1_5_sha256"\n');
402
- writeFileSync(
403
- certificateChainPath,
404
- "-----BEGIN CERTIFICATE-----\nSIGNED CLI TEST\n-----END CERTIFICATE-----\n",
405
- );
406
-
407
- const rollbackResult = cli.runCli(
408
- "update",
409
- "rollback",
410
- "--branch",
411
- "main",
412
- "--platform",
413
- "ios",
414
- "--message",
415
- signedMessage,
416
- "--directive-body-file",
417
- directiveBodyPath,
418
- "--signature-file",
419
- signaturePath,
420
- "--certificate-chain-file",
421
- certificateChainPath,
422
- );
423
-
424
- expect(rollbackResult.exitCode).toBe(0);
425
- expect(rollbackResult.stderr).toBe("");
426
- expect(rollbackResult.stdout).toContain("Created rollback group");
427
- expect(rollbackResult.stdout).toContain(signedCommitTime);
428
-
429
- const updatesResponse = await cli.getAuthorized(`/api/updates?projectId=${cli.getProjectId()}`);
430
- expect(updatesResponse.status).toBe(200);
431
- const updatesBody = (await updatesResponse.json()) as {
432
- items: {
433
- message: string;
434
- signature: string | null;
435
- certificateChain: string | null;
436
- directiveBody: string | null;
437
- }[];
438
- };
439
-
440
- expect(updatesBody.items).toStrictEqual(
441
- expect.arrayContaining([
442
- expect.objectContaining({
443
- message: signedMessage,
444
- signature: 'sig="signed-cli-test", keyid="main", alg="rsa-v1_5_sha256"',
445
- certificateChain:
446
- "-----BEGIN CERTIFICATE-----\nSIGNED CLI TEST\n-----END CERTIFICATE-----",
447
- directiveBody: JSON.stringify({
448
- type: "rollBackToEmbedded",
449
- parameters: { commitTime: signedCommitTime },
450
- }),
451
- }),
452
- ]),
453
- );
454
- });
455
-
456
- it("uploads and deletes a distribution certificate", async () => {
457
- const credentialFile = path.join(cli.getProjectDir(), "cli-uploaded-cert.p12");
458
- const p12Password = "uploaded-password";
459
- writeFileSync(
460
- credentialFile,
461
- generateSelfSignedP12(p12Password, "/OU=CLIE2ETEAM/CN=Apple Distribution: CLI E2E"),
462
- );
463
-
464
- const uploadResult = cli.runCli(
465
- "credentials",
466
- "upload",
467
- "--platform",
468
- "ios",
469
- "--type",
470
- "distribution-certificate",
471
- "--name",
472
- "CLI Uploaded Certificate",
473
- "--file",
474
- credentialFile,
475
- "--password",
476
- p12Password,
477
- );
478
- expect(uploadResult.exitCode).toBe(0);
479
- expect(uploadResult.stderr).toBe("");
480
- expect(uploadResult.stdout).toContain("Credential uploaded successfully.");
481
- expect(uploadResult.stdout).toContain("CLI Uploaded Certificate");
482
- const uploadedCredentialId = /^ID\s+([^\s]+)$/m.exec(uploadResult.stdout)?.[1];
483
- expect(uploadedCredentialId).toBeDefined();
484
-
485
- const listAfterUpload = cli.runCli("credentials", "list", "--platform", "ios");
486
- expect(listAfterUpload.exitCode).toBe(0);
487
- expect(listAfterUpload.stderr).toBe("");
488
- expect(listAfterUpload.stdout).toContain(uploadedCredentialId!);
489
- expect(listAfterUpload.stdout).toContain("distribution-certificate");
490
- expect(listAfterUpload.stdout).toContain("ios");
491
-
492
- const deleteResult = cli.runCli(
493
- "credentials",
494
- "delete",
495
- uploadedCredentialId!,
496
- "--platform",
497
- "ios",
498
- "--type",
499
- "distribution-certificate",
500
- );
501
- expect(deleteResult.exitCode).toBe(0);
502
- expect(deleteResult.stderr).toBe("");
503
- expect(deleteResult.stdout).toContain(`Credential ${uploadedCredentialId} deleted.`);
504
-
505
- const listAfterDelete = cli.runCli("credentials", "list", "--platform", "ios");
506
- expect(listAfterDelete.exitCode).toBe(0);
507
- expect(listAfterDelete.stderr).toBe("");
508
- expect(listAfterDelete.stdout).not.toContain(uploadedCredentialId!);
509
- });
510
-
511
- it("manages rollout state, promotes an update, and deletes the promoted group", async () => {
512
- expect(cliState.rollbackUpdateId).not.toBe("");
513
- expect(cliState.rollbackGroupId).not.toBe("");
514
-
515
- const setResult = cli.runCli(
516
- "update",
517
- "rollout",
518
- "set",
519
- cliState.rollbackUpdateId,
520
- "--percentage",
521
- "25",
522
- );
523
- expect(setResult.exitCode).toBe(0);
524
- expect(setResult.stderr).toBe("");
525
- expect(setResult.stdout).toContain(`Updated rollout for ${cliState.rollbackUpdateId} to 25%.`);
526
-
527
- const listAfterSet = cli.runCli("update", "list", "--branch", "main");
528
- expect(listAfterSet.exitCode).toBe(0);
529
- expect(listAfterSet.stderr).toBe("");
530
- expect(listAfterSet.stdout).toMatch(
531
- new RegExp(
532
- `^${escapeRegExp(cliState.rollbackUpdateId)}\\s+${escapeRegExp(cliState.rollbackGroupId)}\\s+main\\s+ios\\s+1\\.0\\.0\\s+25%\\s+yes\\s+.+$`,
533
- "m",
534
- ),
535
- );
536
-
537
- const completeResult = cli.runCli("update", "rollout", "complete", cliState.rollbackUpdateId);
538
- expect(completeResult.exitCode).toBe(0);
539
- expect(completeResult.stderr).toBe("");
540
- expect(completeResult.stdout).toContain(
541
- `Completed rollout for ${cliState.rollbackUpdateId}. Current rollout is 100%.`,
542
- );
543
-
544
- const listAfterComplete = cli.runCli("update", "list", "--branch", "main");
545
- expect(listAfterComplete.exitCode).toBe(0);
546
- expect(listAfterComplete.stderr).toBe("");
547
- expect(listAfterComplete.stdout).toMatch(
548
- new RegExp(
549
- `^${escapeRegExp(cliState.rollbackUpdateId)}\\s+${escapeRegExp(cliState.rollbackGroupId)}\\s+main\\s+ios\\s+1\\.0\\.0\\s+100%\\s+yes\\s+.+$`,
550
- "m",
551
- ),
552
- );
553
-
554
- const revertResult = cli.runCli("update", "rollout", "revert", cliState.rollbackUpdateId);
555
- expect(revertResult.exitCode).toBe(0);
556
- expect(revertResult.stderr).toBe("");
557
- expect(revertResult.stdout).toContain(
558
- `Reverted rollout for ${cliState.rollbackUpdateId}. Current rollout is 0%.`,
559
- );
560
-
561
- const listAfterRevert = cli.runCli("update", "list", "--branch", "main");
562
- expect(listAfterRevert.exitCode).toBe(0);
563
- expect(listAfterRevert.stderr).toBe("");
564
- expect(listAfterRevert.stdout).toMatch(
565
- new RegExp(
566
- `^${escapeRegExp(cliState.rollbackUpdateId)}\\s+${escapeRegExp(cliState.rollbackGroupId)}\\s+main\\s+ios\\s+1\\.0\\.0\\s+0%\\s+yes\\s+.+$`,
567
- "m",
568
- ),
569
- );
570
-
571
- const targetName = `preview-${Date.now()}`;
572
- seedDestinationChannel(targetName);
573
-
574
- const promotableUpdate = await createPromotableUpdate();
575
-
576
- const promoteResult = cli.runCli(
577
- "update",
578
- "promote",
579
- promotableUpdate.id,
580
- "--channel",
581
- targetName,
582
- );
583
- expect(promoteResult.exitCode).toBe(0);
584
- expect(promoteResult.stderr).toBe("");
585
- expect(promoteResult.stdout).toContain(
586
- `Promoted update ${promotableUpdate.id} to channel "${targetName}" as update `,
587
- );
588
-
589
- const promotedList = cli.runCli("update", "list", "--branch", targetName);
590
- expect(promotedList.exitCode).toBe(0);
591
- expect(promotedList.stderr).toBe("");
592
- const promotedMatch = new RegExp(
593
- `^([^\\s]+)\\s+([^\\s]+)\\s+${escapeRegExp(targetName)}\\s+ios\\s+1\\.0\\.0\\s+100%\\s+no\\s+.+$`,
594
- "m",
595
- ).exec(promotedList.stdout);
596
- expect(promotedMatch).toBeDefined();
597
-
598
- const promotedGroupId = promotedMatch?.[2] ?? "";
599
- expect(promotedGroupId).not.toBe("");
600
-
601
- const deleteResult = cli.runCli("update", "delete", promotedGroupId);
602
- expect(deleteResult.exitCode).toBe(0);
603
- expect(deleteResult.stderr).toBe("");
604
- expect(deleteResult.stdout).toContain(`Deleted 1 update(s) from group ${promotedGroupId}.`);
605
-
606
- const finalPromotedList = cli.runCli("update", "list", "--branch", targetName);
607
- expect(finalPromotedList.exitCode).toBe(0);
608
- expect(finalPromotedList.stderr).toBe("");
609
- expect(finalPromotedList.stdout).toContain("No updates found.");
610
- });
611
-
612
- it("promotes a signed update from the CLI using replacement signed files", async () => {
613
- const targetName = `signed-preview-${Date.now()}`;
614
- seedDestinationChannel(targetName);
615
-
616
- const promotableUpdate = await createPromotableUpdate({
617
- signed: {
618
- manifestId: "cli-signed-source-manifest",
619
- createdAt: "2026-04-14T10:00:00.000Z",
620
- signature: 'sig="source-signature", keyid="main", alg="rsa-v1_5_sha256"',
621
- certificateChain: "-----BEGIN CERTIFICATE-----\nSOURCE\n-----END CERTIFICATE-----",
622
- },
623
- });
624
-
625
- const manifestBodyPath = path.join(
626
- cli.getProjectDir(),
627
- `signed-promote-manifest-${Date.now()}.json`,
628
- );
629
- const signaturePath = path.join(cli.getProjectDir(), `signed-promote-${Date.now()}.sig`);
630
- const certificateChainPath = path.join(cli.getProjectDir(), `signed-promote-${Date.now()}.pem`);
631
- const replacementManifestBody = JSON.stringify({
632
- id: "cli-signed-promoted-manifest",
633
- createdAt: "2026-04-15T10:00:00.000Z",
634
- runtimeVersion: "1.0.0",
635
- launchAsset: {
636
- key: promotableUpdate.launchAssetKey,
637
- hash: promotableUpdate.assetHash,
638
- },
639
- assets: [],
640
- });
641
-
642
- writeFileSync(manifestBodyPath, replacementManifestBody);
643
- writeFileSync(
644
- signaturePath,
645
- 'sig="replacement-signature", keyid="main", alg="rsa-v1_5_sha256"\n',
646
- );
647
- writeFileSync(
648
- certificateChainPath,
649
- "-----BEGIN CERTIFICATE-----\nREPLACEMENT\n-----END CERTIFICATE-----\n",
650
- );
651
-
652
- const promoteResult = cli.runCli(
653
- "update",
654
- "promote",
655
- promotableUpdate.id,
656
- "--channel",
657
- targetName,
658
- "--manifest-body-file",
659
- manifestBodyPath,
660
- "--signature-file",
661
- signaturePath,
662
- "--certificate-chain-file",
663
- certificateChainPath,
664
- );
665
- expect(promoteResult.exitCode).toBe(0);
666
- expect(promoteResult.stderr).toBe("");
667
-
668
- const promotedUpdateId = /as update ([^\s.]+)\./.exec(promoteResult.stdout)?.[1];
669
- expect(promotedUpdateId).toBeDefined();
670
-
671
- const updatesResponse = await cli.getAuthorized(`/api/updates?projectId=${cli.getProjectId()}`);
672
- expect(updatesResponse.status).toBe(200);
673
- const updatesBody = (await updatesResponse.json()) as {
674
- items: {
675
- id: string;
676
- branchId: string;
677
- signature: string | null;
678
- certificateChain: string | null;
679
- manifestBody: string | null;
680
- }[];
681
- };
682
-
683
- expect(updatesBody.items).toStrictEqual(
684
- expect.arrayContaining([
685
- expect.objectContaining({
686
- id: promotedUpdateId,
687
- signature: 'sig="replacement-signature", keyid="main", alg="rsa-v1_5_sha256"',
688
- certificateChain: "-----BEGIN CERTIFICATE-----\nREPLACEMENT\n-----END CERTIFICATE-----",
689
- manifestBody: replacementManifestBody,
690
- }),
691
- ]),
692
- );
693
- });
694
- });