@better-update/cli 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +5190 -0
- package/dist/index.mjs.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,275 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
import { setupCliE2E } from "../helpers/cli-e2e";
|
|
5
|
-
|
|
6
|
-
const FIXTURE_DIR = path.resolve(import.meta.dirname, "../../../../fixtures/e2e-app");
|
|
7
|
-
|
|
8
|
-
const otaAppJsonTemplate = {
|
|
9
|
-
expo: {
|
|
10
|
-
name: "OTA Lifecycle App",
|
|
11
|
-
slug: "ota-lifecycle-app",
|
|
12
|
-
owner: "ota-lifecycle",
|
|
13
|
-
version: "1.0.0",
|
|
14
|
-
runtimeVersion: "1.0.0",
|
|
15
|
-
ios: {
|
|
16
|
-
bundleIdentifier: "com.example.otalifecycle",
|
|
17
|
-
buildNumber: "1",
|
|
18
|
-
},
|
|
19
|
-
android: {
|
|
20
|
-
package: "com.example.otalifecycle",
|
|
21
|
-
versionCode: 1,
|
|
22
|
-
},
|
|
23
|
-
extra: {
|
|
24
|
-
betterUpdate: {
|
|
25
|
-
profiles: {
|
|
26
|
-
production: {
|
|
27
|
-
environment: "production",
|
|
28
|
-
ios: { distribution: "ad-hoc" },
|
|
29
|
-
android: { distribution: "direct", format: "apk" },
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const cli = setupCliE2E(".wrangler/state/e2e-cli-ota-lifecycle", {
|
|
38
|
-
projectDir: FIXTURE_DIR,
|
|
39
|
-
appJsonTemplate: otaAppJsonTemplate,
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
// ── Helpers ──────────────────────────────────────────────────────
|
|
43
|
-
|
|
44
|
-
const iosRowPattern = /^ios\s+([0-9a-f-]+)\s+1\.0\.0\s+(\d+)\s+(\d+)\s*$/m;
|
|
45
|
-
|
|
46
|
-
interface MultipartPart {
|
|
47
|
-
readonly headers: Record<string, string>;
|
|
48
|
-
readonly body: string;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const parseMultipart = (contentType: string, rawBody: string): readonly MultipartPart[] => {
|
|
52
|
-
const boundaryMatch = /boundary=([^\s;]+)/.exec(contentType);
|
|
53
|
-
const boundary = boundaryMatch?.[1] ?? "";
|
|
54
|
-
return rawBody
|
|
55
|
-
.split(`--${boundary}`)
|
|
56
|
-
.slice(1, -1)
|
|
57
|
-
.map((part) => {
|
|
58
|
-
const [headerSection = "", ...bodySections] = part.split("\r\n\r\n");
|
|
59
|
-
const headers = Object.fromEntries(
|
|
60
|
-
headerSection
|
|
61
|
-
.split("\r\n")
|
|
62
|
-
.filter(Boolean)
|
|
63
|
-
.map((line) => {
|
|
64
|
-
const idx = line.indexOf(": ");
|
|
65
|
-
return [line.slice(0, idx).toLowerCase(), line.slice(idx + 2)];
|
|
66
|
-
}),
|
|
67
|
-
);
|
|
68
|
-
return { headers, body: bodySections.join("\r\n\r\n").replace(/\r\n$/, "") };
|
|
69
|
-
});
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const manifestHeaders = (overrides?: Record<string, string>) => ({
|
|
73
|
-
"expo-protocol-version": "1",
|
|
74
|
-
"expo-platform": "ios",
|
|
75
|
-
"expo-runtime-version": "1.0.0",
|
|
76
|
-
"expo-channel-name": "main",
|
|
77
|
-
accept: "multipart/mixed",
|
|
78
|
-
...overrides,
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
const fetchManifest = async (projectId: string, overrides?: Record<string, string>) =>
|
|
82
|
-
cli.get(`/manifest/${projectId}`, manifestHeaders(overrides));
|
|
83
|
-
|
|
84
|
-
const extractManifestId = async (response: Response): Promise<string> => {
|
|
85
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
86
|
-
const parts = parseMultipart(contentType, await response.text());
|
|
87
|
-
const manifestPart = parts.find((part) =>
|
|
88
|
-
part.headers["content-disposition"]?.includes('name="manifest"'),
|
|
89
|
-
);
|
|
90
|
-
expect(manifestPart).toBeDefined();
|
|
91
|
-
return JSON.parse(manifestPart!.body).id;
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
/** Reproduces server domain/hash.ts hashToFraction using Node crypto. */
|
|
95
|
-
const hashToFraction = (salt: string, clientId: string): number => {
|
|
96
|
-
const hash = createHash("sha256").update(`${salt}:${clientId}`).digest();
|
|
97
|
-
return hash.readUInt32BE(0) / 4_294_967_296;
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
/** Finds two deterministic client IDs: one in-rollout, one out-rollout. */
|
|
101
|
-
const findRolloutClients = (
|
|
102
|
-
updateId: string,
|
|
103
|
-
percentage: number,
|
|
104
|
-
): { inClient: string; outClient: string } => {
|
|
105
|
-
const threshold = percentage / 100;
|
|
106
|
-
let inClient = "";
|
|
107
|
-
let outClient = "";
|
|
108
|
-
for (let index = 0; index < 200; index++) {
|
|
109
|
-
const clientId = `ota-test-client-${index}`;
|
|
110
|
-
const fraction = hashToFraction(updateId, clientId);
|
|
111
|
-
if (fraction < threshold && !inClient) {
|
|
112
|
-
inClient = clientId;
|
|
113
|
-
}
|
|
114
|
-
if (fraction >= threshold && !outClient) {
|
|
115
|
-
outClient = clientId;
|
|
116
|
-
}
|
|
117
|
-
if (inClient && outClient) {
|
|
118
|
-
break;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (!inClient || !outClient) {
|
|
122
|
-
throw new Error(`Could not find rollout test clients for ${updateId} at ${percentage}%`);
|
|
123
|
-
}
|
|
124
|
-
return { inClient, outClient };
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
// ── Shared state across sequential tests ─────────────────────────
|
|
128
|
-
|
|
129
|
-
const state = {
|
|
130
|
-
v1UpdateId: "",
|
|
131
|
-
v2UpdateId: "",
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
// ── Tests ────────────────────────────────────────────────────────
|
|
135
|
-
|
|
136
|
-
describe("oTA lifecycle: CLI publish → manifest → rollout → rollback", () => {
|
|
137
|
-
// ── Section 1: Setup ────────────────────────────────────────────
|
|
138
|
-
|
|
139
|
-
it("links the fixture app to the seeded project", () => {
|
|
140
|
-
const result = cli.runCli("init");
|
|
141
|
-
expect(result.exitCode).toBe(0);
|
|
142
|
-
expect(result.stdout).toContain("Project linked successfully");
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// ── Section 2: Publish v1 → manifest serves update ─────────────
|
|
146
|
-
|
|
147
|
-
it("publishes v1 iOS update via CLI", () => {
|
|
148
|
-
const result = cli.runCli("update", "publish", "--branch", "main", "--platform", "ios");
|
|
149
|
-
expect(result.exitCode).toBe(0);
|
|
150
|
-
|
|
151
|
-
const iosRow = iosRowPattern.exec(result.stdout);
|
|
152
|
-
expect(iosRow).toBeDefined();
|
|
153
|
-
state.v1UpdateId = iosRow![1]!;
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it("manifest endpoint serves v1 to an Expo app", async () => {
|
|
157
|
-
const response = await fetchManifest(cli.getProjectId());
|
|
158
|
-
expect(response.status).toBe(200);
|
|
159
|
-
await expect(extractManifestId(response)).resolves.toBe(state.v1UpdateId);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it("returns 204 when app already has the latest update", async () => {
|
|
163
|
-
const response = await fetchManifest(cli.getProjectId(), {
|
|
164
|
-
"expo-current-update-id": state.v1UpdateId,
|
|
165
|
-
});
|
|
166
|
-
expect(response.status).toBe(204);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// ── Section 3: Publish v2 → manifest serves latest ─────────────
|
|
170
|
-
|
|
171
|
-
it("publishes v2 iOS update via CLI", () => {
|
|
172
|
-
const result = cli.runCli(
|
|
173
|
-
"update",
|
|
174
|
-
"publish",
|
|
175
|
-
"--branch",
|
|
176
|
-
"main",
|
|
177
|
-
"--platform",
|
|
178
|
-
"ios",
|
|
179
|
-
"--message",
|
|
180
|
-
"v2 update",
|
|
181
|
-
);
|
|
182
|
-
expect(result.exitCode).toBe(0);
|
|
183
|
-
|
|
184
|
-
const iosRow = iosRowPattern.exec(result.stdout);
|
|
185
|
-
expect(iosRow).toBeDefined();
|
|
186
|
-
state.v2UpdateId = iosRow![1]!;
|
|
187
|
-
expect(state.v2UpdateId).not.toBe(state.v1UpdateId);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it("manifest endpoint now serves v2 (latest)", async () => {
|
|
191
|
-
const response = await fetchManifest(cli.getProjectId());
|
|
192
|
-
expect(response.status).toBe(200);
|
|
193
|
-
await expect(extractManifestId(response)).resolves.toBe(state.v2UpdateId);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
// ── Section 4: Per-update rollout → manifest routing ────────────
|
|
197
|
-
|
|
198
|
-
it("sets v2 rollout to 50% via CLI", () => {
|
|
199
|
-
const result = cli.runCli("update", "rollout", "set", state.v2UpdateId, "--percentage", "50");
|
|
200
|
-
expect(result.exitCode).toBe(0);
|
|
201
|
-
expect(result.stdout).toContain(`Updated rollout for ${state.v2UpdateId} to 50%.`);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it("falls back to v1 when no eas-client-id is provided", async () => {
|
|
205
|
-
const response = await fetchManifest(cli.getProjectId());
|
|
206
|
-
expect(response.status).toBe(200);
|
|
207
|
-
await expect(extractManifestId(response)).resolves.toBe(state.v1UpdateId);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it("serves v2 to in-rollout client and v1 to out-rollout client", async () => {
|
|
211
|
-
const { inClient, outClient } = findRolloutClients(state.v2UpdateId, 50);
|
|
212
|
-
|
|
213
|
-
const inResponse = await fetchManifest(cli.getProjectId(), {
|
|
214
|
-
"eas-client-id": inClient,
|
|
215
|
-
});
|
|
216
|
-
expect(inResponse.status).toBe(200);
|
|
217
|
-
await expect(extractManifestId(inResponse)).resolves.toBe(state.v2UpdateId);
|
|
218
|
-
|
|
219
|
-
const outResponse = await fetchManifest(cli.getProjectId(), {
|
|
220
|
-
"eas-client-id": outClient,
|
|
221
|
-
});
|
|
222
|
-
expect(outResponse.status).toBe(200);
|
|
223
|
-
await expect(extractManifestId(outResponse)).resolves.toBe(state.v1UpdateId);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("completing rollout makes v2 available to all clients", async () => {
|
|
227
|
-
const result = cli.runCli("update", "rollout", "complete", state.v2UpdateId);
|
|
228
|
-
expect(result.exitCode).toBe(0);
|
|
229
|
-
expect(result.stdout).toContain("100%");
|
|
230
|
-
|
|
231
|
-
const response = await fetchManifest(cli.getProjectId());
|
|
232
|
-
expect(response.status).toBe(200);
|
|
233
|
-
await expect(extractManifestId(response)).resolves.toBe(state.v2UpdateId);
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
// ── Section 5: Rollback → manifest directive ────────────────────
|
|
237
|
-
|
|
238
|
-
it("creates a rollback directive via CLI", () => {
|
|
239
|
-
const result = cli.runCli(
|
|
240
|
-
"update",
|
|
241
|
-
"rollback",
|
|
242
|
-
"--branch",
|
|
243
|
-
"main",
|
|
244
|
-
"--platform",
|
|
245
|
-
"ios",
|
|
246
|
-
"--commit-time",
|
|
247
|
-
"2026-04-15T00:00:00.000Z",
|
|
248
|
-
);
|
|
249
|
-
expect(result.exitCode).toBe(0);
|
|
250
|
-
expect(result.stdout).toContain("Created rollback group");
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it("manifest returns rollback-to-embedded directive", async () => {
|
|
254
|
-
const response = await fetchManifest(cli.getProjectId());
|
|
255
|
-
expect(response.status).toBe(200);
|
|
256
|
-
|
|
257
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
258
|
-
const parts = parseMultipart(contentType, await response.text());
|
|
259
|
-
|
|
260
|
-
const directivePart = parts.find((part) =>
|
|
261
|
-
part.headers["content-disposition"]?.includes('name="directive"'),
|
|
262
|
-
);
|
|
263
|
-
expect(directivePart).toBeDefined();
|
|
264
|
-
expect(JSON.parse(directivePart!.body)).toStrictEqual({
|
|
265
|
-
type: "rollBackToEmbedded",
|
|
266
|
-
parameters: { commitTime: "2026-04-15T00:00:00.000Z" },
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
// No manifest part — only directive
|
|
270
|
-
const manifestPart = parts.find((part) =>
|
|
271
|
-
part.headers["content-disposition"]?.includes('name="manifest"'),
|
|
272
|
-
);
|
|
273
|
-
expect(manifestPart).toBeUndefined();
|
|
274
|
-
});
|
|
275
|
-
});
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
|
|
3
|
-
import { setupCliE2E } from "../helpers/cli-e2e";
|
|
4
|
-
|
|
5
|
-
const FIXTURE_DIR = path.resolve(import.meta.dirname, "../../../../fixtures/e2e-app");
|
|
6
|
-
|
|
7
|
-
const publishAppJsonTemplate = {
|
|
8
|
-
expo: {
|
|
9
|
-
name: "E2E Publish App",
|
|
10
|
-
slug: "e2e-publish-app",
|
|
11
|
-
owner: "e2e-publish",
|
|
12
|
-
version: "1.0.0",
|
|
13
|
-
runtimeVersion: "1.0.0",
|
|
14
|
-
ios: {
|
|
15
|
-
bundleIdentifier: "com.example.e2epublish",
|
|
16
|
-
buildNumber: "1",
|
|
17
|
-
},
|
|
18
|
-
android: {
|
|
19
|
-
package: "com.example.e2epublish",
|
|
20
|
-
versionCode: 1,
|
|
21
|
-
},
|
|
22
|
-
extra: {
|
|
23
|
-
betterUpdate: {
|
|
24
|
-
profiles: {
|
|
25
|
-
production: {
|
|
26
|
-
environment: "production",
|
|
27
|
-
ios: { distribution: "ad-hoc" },
|
|
28
|
-
android: { distribution: "direct", format: "apk" },
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const cli = setupCliE2E(".wrangler/state/e2e-cli-publish", {
|
|
37
|
-
projectDir: FIXTURE_DIR,
|
|
38
|
-
appJsonTemplate: publishAppJsonTemplate,
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
const publishState = {
|
|
42
|
-
firstGroupId: "",
|
|
43
|
-
firstIosUpdateId: "",
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
// Table rows are padded with trailing whitespace — use \s*$ in all row regexes.
|
|
47
|
-
const iosRowPattern = /^ios\s+([0-9a-f-]+)\s+1\.0\.0\s+(\d+)\s+(\d+)\s*$/m;
|
|
48
|
-
const androidRowPattern = /^android\s+([0-9a-f-]+)\s+1\.0\.0\s+(\d+)\s+(\d+)\s*$/m;
|
|
49
|
-
|
|
50
|
-
describe("cLI publish journey", () => {
|
|
51
|
-
it("links the fixture app to the seeded project", () => {
|
|
52
|
-
const result = cli.runCli("init");
|
|
53
|
-
expect(result.exitCode).toBe(0);
|
|
54
|
-
expect(result.stderr).toBe("");
|
|
55
|
-
expect(result.stdout).toContain("Linking project: E2E Publish App (e2e-publish-app)");
|
|
56
|
-
expect(result.stdout).toContain("Found existing project: E2E Publish App Project");
|
|
57
|
-
expect(result.stdout).toContain("Project linked successfully");
|
|
58
|
-
|
|
59
|
-
const appJson = cli.readAppJson();
|
|
60
|
-
const expo = appJson["expo"] as Record<string, unknown>;
|
|
61
|
-
const extra = expo["extra"] as Record<string, unknown>;
|
|
62
|
-
const betterUpdate = extra["betterUpdate"] as Record<string, unknown>;
|
|
63
|
-
expect(betterUpdate["projectId"]).toBe(cli.getProjectId());
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("publishes an iOS update with fresh assets", () => {
|
|
67
|
-
const result = cli.runCli("update", "publish", "--branch", "main", "--platform", "ios");
|
|
68
|
-
expect(result.exitCode).toBe(0);
|
|
69
|
-
|
|
70
|
-
const groupMatch = /Published update group ([0-9a-f-]+) to branch "main"\./.exec(result.stdout);
|
|
71
|
-
expect(groupMatch).toBeDefined();
|
|
72
|
-
publishState.firstGroupId = groupMatch![1]!;
|
|
73
|
-
|
|
74
|
-
const iosRow = iosRowPattern.exec(result.stdout);
|
|
75
|
-
expect(iosRow).toBeDefined();
|
|
76
|
-
publishState.firstIosUpdateId = iosRow![1]!;
|
|
77
|
-
|
|
78
|
-
const uploaded = Number(iosRow![2]);
|
|
79
|
-
const reused = Number(iosRow![3]);
|
|
80
|
-
expect(uploaded).toBeGreaterThan(0);
|
|
81
|
-
expect(reused).toBe(0);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("re-publishes iOS and produces a distinct update group", () => {
|
|
85
|
-
const result = cli.runCli("update", "publish", "--branch", "main", "--platform", "ios");
|
|
86
|
-
expect(result.exitCode).toBe(0);
|
|
87
|
-
|
|
88
|
-
const groupMatch = /Published update group ([0-9a-f-]+) to branch "main"\./.exec(result.stdout);
|
|
89
|
-
expect(groupMatch).toBeDefined();
|
|
90
|
-
expect(groupMatch![1]).not.toBe(publishState.firstGroupId);
|
|
91
|
-
|
|
92
|
-
const iosRow = iosRowPattern.exec(result.stdout);
|
|
93
|
-
expect(iosRow).toBeDefined();
|
|
94
|
-
// Hermes bytecode is non-deterministic, so bundle hash changes each export.
|
|
95
|
-
// Verify table structure without asserting exact dedup counts.
|
|
96
|
-
expect(Number(iosRow![2]) + Number(iosRow![3])).toBeGreaterThan(0);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("publishes all platforms in a single group", () => {
|
|
100
|
-
const result = cli.runCli("update", "publish", "--branch", "main", "--platform", "all");
|
|
101
|
-
expect(result.exitCode).toBe(0);
|
|
102
|
-
|
|
103
|
-
const groupMatch = /Published update group ([0-9a-f-]+) to branch "main"\./.exec(result.stdout);
|
|
104
|
-
expect(groupMatch).toBeDefined();
|
|
105
|
-
|
|
106
|
-
const iosRow = iosRowPattern.exec(result.stdout);
|
|
107
|
-
const androidRow = androidRowPattern.exec(result.stdout);
|
|
108
|
-
expect(iosRow).toBeDefined();
|
|
109
|
-
expect(androidRow).toBeDefined();
|
|
110
|
-
|
|
111
|
-
// Android first publish should upload at least one asset
|
|
112
|
-
const androidUploaded = Number(androidRow![2]);
|
|
113
|
-
expect(androidUploaded).toBeGreaterThan(0);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("publishes with a custom message visible in the API", async () => {
|
|
117
|
-
const customMessage = "Test release v1";
|
|
118
|
-
const result = cli.runCli(
|
|
119
|
-
"update",
|
|
120
|
-
"publish",
|
|
121
|
-
"--branch",
|
|
122
|
-
"main",
|
|
123
|
-
"--platform",
|
|
124
|
-
"ios",
|
|
125
|
-
"--message",
|
|
126
|
-
customMessage,
|
|
127
|
-
);
|
|
128
|
-
expect(result.exitCode).toBe(0);
|
|
129
|
-
|
|
130
|
-
const updatesResponse = await cli.getAuthorized(`/api/updates?projectId=${cli.getProjectId()}`);
|
|
131
|
-
expect(updatesResponse.status).toBe(200);
|
|
132
|
-
const body = (await updatesResponse.json()) as {
|
|
133
|
-
items: { message: string }[];
|
|
134
|
-
};
|
|
135
|
-
expect(body.items).toStrictEqual(
|
|
136
|
-
expect.arrayContaining([expect.objectContaining({ message: customMessage })]),
|
|
137
|
-
);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("lists all published updates on the branch", () => {
|
|
141
|
-
const result = cli.runCli("update", "list", "--branch", "main");
|
|
142
|
-
expect(result.exitCode).toBe(0);
|
|
143
|
-
expect(result.stderr).toBe("");
|
|
144
|
-
expect(result.stdout).not.toContain("No updates found.");
|
|
145
|
-
expect(result.stdout).toContain("main");
|
|
146
|
-
expect(result.stdout).toContain("ios");
|
|
147
|
-
expect(result.stdout).toContain("1.0.0");
|
|
148
|
-
expect(result.stdout).toContain(publishState.firstIosUpdateId);
|
|
149
|
-
});
|
|
150
|
-
});
|