@valbuild/cli 0.133.0 → 0.134.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/CHANGELOG.md +49 -0
- package/cli/dist/valbuild-cli-cli.cjs.dev.js +1424 -0
- package/cli/dist/valbuild-cli-cli.cjs.prod.js +1424 -0
- package/cli/dist/valbuild-cli-cli.esm.js +1422 -0
- package/package.json +7 -5
- package/src/cli.ts +51 -0
- package/src/publish/artifacts.ts +194 -0
- package/src/publish/client.ts +177 -0
- package/src/publish/contentApi.ts +210 -0
- package/src/publish/contentHost.ts +160 -0
- package/src/publish/credentials.test.ts +348 -0
- package/src/publish/credentials.ts +258 -0
- package/src/publish/fakeContent.ts +482 -0
- package/src/publish/protocol.ts +174 -0
- package/src/publish/publishWireContract.test.ts +292 -0
- package/src/publish/runPublish.test.ts +536 -0
- package/src/publish/runPublish.ts +633 -0
- package/src/publish.ts +112 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import zlib from "zlib";
|
|
6
|
+
import { startFakeContentService } from "./fakeContent";
|
|
7
|
+
import { runPublish } from "./runPublish";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `val publish` against a content service that is really there.
|
|
11
|
+
*
|
|
12
|
+
* The fake speaks the publish API over HTTP and re-hashes what lands, so what
|
|
13
|
+
* is under test is the whole command: which calls it makes, in which order,
|
|
14
|
+
* with which credential, and what it does when a slot expires, when an upload
|
|
15
|
+
* is lost, when the canary does not render, and when somebody pushed while it
|
|
16
|
+
* was building.
|
|
17
|
+
*/
|
|
18
|
+
const TOKEN = "val_pt_test";
|
|
19
|
+
|
|
20
|
+
const A_BUILD: Record<string, string> = {
|
|
21
|
+
server: "export default { fetch() {} }\n",
|
|
22
|
+
client: "console.log('client')\n",
|
|
23
|
+
"chunk/client/app-a1b2c3.js": "export const app = 1\n",
|
|
24
|
+
"public/index.html": "<!doctype html><title>hi</title>\n",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function makeArtifacts(files: Record<string, string> = A_BUILD): string {
|
|
28
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "val-publish-run-"));
|
|
29
|
+
for (const [key, contents] of Object.entries(files)) {
|
|
30
|
+
const absolute = path.join(root, ".val", "publish", key);
|
|
31
|
+
fs.mkdirSync(path.dirname(absolute), { recursive: true });
|
|
32
|
+
fs.writeFileSync(absolute, contents);
|
|
33
|
+
}
|
|
34
|
+
return root;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const sha256 = (contents: string) =>
|
|
38
|
+
crypto.createHash("sha256").update(contents).digest("hex");
|
|
39
|
+
|
|
40
|
+
function run(
|
|
41
|
+
root: string,
|
|
42
|
+
url: string,
|
|
43
|
+
overrides: Parameters<typeof runPublish>[0] = {},
|
|
44
|
+
) {
|
|
45
|
+
return runPublish({
|
|
46
|
+
root,
|
|
47
|
+
commit: "1234567890abcdef1234567890abcdef12345678",
|
|
48
|
+
branch: "main",
|
|
49
|
+
env: { VAL_PROJECT_TOKEN: TOKEN, VAL_CONTENT_URL: url },
|
|
50
|
+
sleep: () => Promise.resolve(),
|
|
51
|
+
log: () => undefined,
|
|
52
|
+
...overrides,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const apiCalls = (calls: Array<{ method: string; path: string }>) =>
|
|
57
|
+
calls
|
|
58
|
+
.filter(({ path: p }) => p.startsWith("/v1/publish"))
|
|
59
|
+
.map(({ method, path: p }) => `${method} ${p}`);
|
|
60
|
+
|
|
61
|
+
describe("val publish", () => {
|
|
62
|
+
test("declares, uploads, confirms, verifies and promotes", async () => {
|
|
63
|
+
const fake = await startFakeContentService({ token: TOKEN });
|
|
64
|
+
const root = makeArtifacts();
|
|
65
|
+
try {
|
|
66
|
+
const result = await run(root, fake.url);
|
|
67
|
+
|
|
68
|
+
expect(result).toEqual({
|
|
69
|
+
status: "live",
|
|
70
|
+
publishId: "pub_1",
|
|
71
|
+
url: "https://example.test",
|
|
72
|
+
commit: "1234567890abcdef1234567890abcdef12345678",
|
|
73
|
+
artifacts: 4,
|
|
74
|
+
uploaded: 4,
|
|
75
|
+
uploadedBytes: Object.values(A_BUILD).reduce(
|
|
76
|
+
(sum, contents) => sum + Buffer.byteLength(contents),
|
|
77
|
+
0,
|
|
78
|
+
),
|
|
79
|
+
// `CANARY_HAS_NO_PREVIEW`: null, until the platform has somewhere to
|
|
80
|
+
// point at.
|
|
81
|
+
previewUrl: null,
|
|
82
|
+
});
|
|
83
|
+
expect(apiCalls(fake.calls)).toEqual([
|
|
84
|
+
"POST /v1/publish",
|
|
85
|
+
"POST /v1/publish/pub_1/artifacts",
|
|
86
|
+
"POST /v1/publish/pub_1/verify",
|
|
87
|
+
"POST /v1/publish/pub_1/promote",
|
|
88
|
+
]);
|
|
89
|
+
// The bytes that landed are the bytes on disk, under the key the file
|
|
90
|
+
// was at - content re-hashed them and would have refused otherwise.
|
|
91
|
+
expect(fake.stored.get("public/index.html")?.toString()).toBe(
|
|
92
|
+
A_BUILD["public/index.html"],
|
|
93
|
+
);
|
|
94
|
+
expect(fake.stored.get("chunk/client/app-a1b2c3.js")?.toString()).toBe(
|
|
95
|
+
A_BUILD["chunk/client/app-a1b2c3.js"],
|
|
96
|
+
);
|
|
97
|
+
} finally {
|
|
98
|
+
await fake.close();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("the declaration says what the build is, by key and by hash", async () => {
|
|
103
|
+
const fake = await startFakeContentService({ token: TOKEN });
|
|
104
|
+
const root = makeArtifacts();
|
|
105
|
+
try {
|
|
106
|
+
await run(root, fake.url);
|
|
107
|
+
|
|
108
|
+
const [declaration] = [...fake.declarations.values()];
|
|
109
|
+
expect(declaration).toMatchObject({
|
|
110
|
+
commit: "1234567890abcdef1234567890abcdef12345678",
|
|
111
|
+
branch: "main",
|
|
112
|
+
layerRev: null,
|
|
113
|
+
// Null is a third answer - "this build did not say" - not false.
|
|
114
|
+
linksOwnCss: null,
|
|
115
|
+
artifacts: [
|
|
116
|
+
{
|
|
117
|
+
key: "chunk/client/app-a1b2c3.js",
|
|
118
|
+
sha256: sha256(A_BUILD["chunk/client/app-a1b2c3.js"]),
|
|
119
|
+
bytes: Buffer.byteLength(A_BUILD["chunk/client/app-a1b2c3.js"]),
|
|
120
|
+
},
|
|
121
|
+
{ key: "client", sha256: sha256(A_BUILD["client"]) },
|
|
122
|
+
{
|
|
123
|
+
key: "public/index.html",
|
|
124
|
+
sha256: sha256(A_BUILD["public/index.html"]),
|
|
125
|
+
},
|
|
126
|
+
{ key: "server", sha256: sha256(A_BUILD["server"]) },
|
|
127
|
+
],
|
|
128
|
+
});
|
|
129
|
+
} finally {
|
|
130
|
+
await fake.close();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("every call to content carries the project token, and the store gets none", async () => {
|
|
135
|
+
const fake = await startFakeContentService({ token: TOKEN });
|
|
136
|
+
const root = makeArtifacts();
|
|
137
|
+
try {
|
|
138
|
+
await run(root, fake.url);
|
|
139
|
+
|
|
140
|
+
const toContent = fake.calls.filter(({ path: p }) =>
|
|
141
|
+
p.startsWith("/v1/publish"),
|
|
142
|
+
);
|
|
143
|
+
expect(toContent).not.toHaveLength(0);
|
|
144
|
+
for (const call of toContent) {
|
|
145
|
+
expect(call.authorization).toBe(`Bearer ${TOKEN}`);
|
|
146
|
+
}
|
|
147
|
+
// A presigned URL is the permission. Sending ours would hand a bucket a
|
|
148
|
+
// token that can publish.
|
|
149
|
+
for (const call of fake.calls.filter(({ path: p }) =>
|
|
150
|
+
p.startsWith("/store/"),
|
|
151
|
+
)) {
|
|
152
|
+
expect(call.authorization).toBeNull();
|
|
153
|
+
}
|
|
154
|
+
} finally {
|
|
155
|
+
await fake.close();
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("an artifact content already holds gets no slot", async () => {
|
|
160
|
+
const fake = await startFakeContentService({
|
|
161
|
+
token: TOKEN,
|
|
162
|
+
have: [sha256(A_BUILD["public/index.html"])],
|
|
163
|
+
});
|
|
164
|
+
const root = makeArtifacts();
|
|
165
|
+
try {
|
|
166
|
+
const result = await run(root, fake.url);
|
|
167
|
+
|
|
168
|
+
expect(result.status).toBe("live");
|
|
169
|
+
if (result.status === "live") {
|
|
170
|
+
expect(result.artifacts).toBe(4);
|
|
171
|
+
expect(result.uploaded).toBe(3);
|
|
172
|
+
}
|
|
173
|
+
expect(fake.stored.has("public/index.html")).toBe(false);
|
|
174
|
+
} finally {
|
|
175
|
+
await fake.close();
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("a build content holds entirely still confirms, verifies and promotes", async () => {
|
|
180
|
+
const fake = await startFakeContentService({
|
|
181
|
+
token: TOKEN,
|
|
182
|
+
have: Object.values(A_BUILD).map(sha256),
|
|
183
|
+
});
|
|
184
|
+
const root = makeArtifacts();
|
|
185
|
+
try {
|
|
186
|
+
const result = await run(root, fake.url);
|
|
187
|
+
|
|
188
|
+
expect(result.status).toBe("live");
|
|
189
|
+
if (result.status === "live") {
|
|
190
|
+
expect(result.uploaded).toBe(0);
|
|
191
|
+
}
|
|
192
|
+
// The pointer is what was out of date, so the publish still has to happen.
|
|
193
|
+
expect(apiCalls(fake.calls)).toContain(
|
|
194
|
+
"POST /v1/publish/pub_1/artifacts",
|
|
195
|
+
);
|
|
196
|
+
expect(fake.stored.size).toBe(0);
|
|
197
|
+
} finally {
|
|
198
|
+
await fake.close();
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("re-publishing a build that is live is a success, not a second publish", async () => {
|
|
203
|
+
const fake = await startFakeContentService({ token: TOKEN });
|
|
204
|
+
const root = makeArtifacts();
|
|
205
|
+
try {
|
|
206
|
+
await run(root, fake.url);
|
|
207
|
+
const again = await run(root, fake.url);
|
|
208
|
+
|
|
209
|
+
// Idempotent on the build hash: the same publish, not re-opened - that
|
|
210
|
+
// would mint slots to overwrite bytes that are currently serving.
|
|
211
|
+
expect(again.status).toBe("live");
|
|
212
|
+
if (again.status === "live") {
|
|
213
|
+
expect(again.publishId).toBe("pub_1");
|
|
214
|
+
expect(again.uploaded).toBe(0);
|
|
215
|
+
}
|
|
216
|
+
expect(
|
|
217
|
+
apiCalls(fake.calls).filter((call) => call.endsWith("/promote")),
|
|
218
|
+
).toHaveLength(1);
|
|
219
|
+
} finally {
|
|
220
|
+
await fake.close();
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("an upload that is accepted and lost is declared again and sent again", async () => {
|
|
225
|
+
// The re-hash is the only thing that can notice: the store answered 200
|
|
226
|
+
// and kept nothing, which is what a connection dying mid-body looks like.
|
|
227
|
+
const fake = await startFakeContentService({
|
|
228
|
+
token: TOKEN,
|
|
229
|
+
dropUploads: 1,
|
|
230
|
+
});
|
|
231
|
+
const root = makeArtifacts();
|
|
232
|
+
try {
|
|
233
|
+
const result = await run(root, fake.url);
|
|
234
|
+
|
|
235
|
+
expect(result.status).toBe("live");
|
|
236
|
+
expect(fake.stored.size).toBe(4);
|
|
237
|
+
if (result.status === "live") {
|
|
238
|
+
// Four artifacts, one of them sent twice: counting attempts would
|
|
239
|
+
// report five uploaded out of four.
|
|
240
|
+
expect(result.uploaded).toBe(4);
|
|
241
|
+
}
|
|
242
|
+
expect(apiCalls(fake.calls)).toEqual([
|
|
243
|
+
"POST /v1/publish",
|
|
244
|
+
"POST /v1/publish/pub_1/artifacts",
|
|
245
|
+
"POST /v1/publish",
|
|
246
|
+
"POST /v1/publish/pub_1/artifacts",
|
|
247
|
+
"POST /v1/publish/pub_1/verify",
|
|
248
|
+
"POST /v1/publish/pub_1/promote",
|
|
249
|
+
]);
|
|
250
|
+
} finally {
|
|
251
|
+
await fake.close();
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("an expired slot is declared again rather than failing the publish", async () => {
|
|
256
|
+
const fake = await startFakeContentService({
|
|
257
|
+
token: TOKEN,
|
|
258
|
+
expireSlots: 1,
|
|
259
|
+
});
|
|
260
|
+
const root = makeArtifacts();
|
|
261
|
+
try {
|
|
262
|
+
const result = await run(root, fake.url);
|
|
263
|
+
|
|
264
|
+
expect(result.status).toBe("live");
|
|
265
|
+
expect(fake.stored.size).toBe(4);
|
|
266
|
+
expect(
|
|
267
|
+
apiCalls(fake.calls).filter((call) => call === "POST /v1/publish"),
|
|
268
|
+
).toHaveLength(2);
|
|
269
|
+
} finally {
|
|
270
|
+
await fake.close();
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("a store saying 'slow down' is retried without declaring again", async () => {
|
|
275
|
+
const fake = await startFakeContentService({
|
|
276
|
+
token: TOKEN,
|
|
277
|
+
failUploadsWith5xx: 2,
|
|
278
|
+
});
|
|
279
|
+
const root = makeArtifacts();
|
|
280
|
+
try {
|
|
281
|
+
const result = await run(root, fake.url);
|
|
282
|
+
|
|
283
|
+
expect(result.status).toBe("live");
|
|
284
|
+
expect(fake.stored.size).toBe(4);
|
|
285
|
+
expect(
|
|
286
|
+
apiCalls(fake.calls).filter((call) => call === "POST /v1/publish"),
|
|
287
|
+
).toHaveLength(1);
|
|
288
|
+
} finally {
|
|
289
|
+
await fake.close();
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("a canary that does not render stops the publish, and says why", async () => {
|
|
294
|
+
const fake = await startFakeContentService({
|
|
295
|
+
token: TOKEN,
|
|
296
|
+
verify: "fail",
|
|
297
|
+
});
|
|
298
|
+
const root = makeArtifacts();
|
|
299
|
+
try {
|
|
300
|
+
const result = await run(root, fake.url);
|
|
301
|
+
|
|
302
|
+
expect(result.status).toBe("failed");
|
|
303
|
+
if (result.status === "failed") {
|
|
304
|
+
expect(result.state).toBe("failed");
|
|
305
|
+
// The platform's own code, passed through: rewording it would lose the
|
|
306
|
+
// only sentence that says what to change.
|
|
307
|
+
expect(result.problems).toEqual([
|
|
308
|
+
{
|
|
309
|
+
code: "PLATFORM_RENDER_FAILED",
|
|
310
|
+
message: "/ threw on the server",
|
|
311
|
+
},
|
|
312
|
+
]);
|
|
313
|
+
}
|
|
314
|
+
expect(apiCalls(fake.calls)).not.toContain(
|
|
315
|
+
"POST /v1/publish/pub_1/promote",
|
|
316
|
+
);
|
|
317
|
+
} finally {
|
|
318
|
+
await fake.close();
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("a push while we were building is a rebuild, not a retry", async () => {
|
|
323
|
+
const fake = await startFakeContentService({
|
|
324
|
+
token: TOKEN,
|
|
325
|
+
promote: "stale",
|
|
326
|
+
head: "abcdef1234567890abcdef1234567890abcdef12",
|
|
327
|
+
});
|
|
328
|
+
const root = makeArtifacts();
|
|
329
|
+
try {
|
|
330
|
+
const result = await run(root, fake.url);
|
|
331
|
+
|
|
332
|
+
expect(result.status).toBe("failed");
|
|
333
|
+
if (result.status === "failed") {
|
|
334
|
+
// Content's own sentence, kept: it already says what to do.
|
|
335
|
+
expect(result.message).toContain("Rebuild from the current head");
|
|
336
|
+
expect(result.message).toContain("abcdef1");
|
|
337
|
+
expect(result.problems[0].code).toBe("POINTER_STALE");
|
|
338
|
+
}
|
|
339
|
+
} finally {
|
|
340
|
+
await fake.close();
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("a refused declaration reports every problem, not the first", async () => {
|
|
345
|
+
const fake = await startFakeContentService({ token: TOKEN });
|
|
346
|
+
// No server bundle, and a layer with no rev to name it by.
|
|
347
|
+
const root = makeArtifacts({
|
|
348
|
+
client: "console.log('client')\n",
|
|
349
|
+
layer: "not gzip at all",
|
|
350
|
+
});
|
|
351
|
+
try {
|
|
352
|
+
const result = await run(root, fake.url, { layerRev: "" });
|
|
353
|
+
|
|
354
|
+
expect(result.status).toBe("error");
|
|
355
|
+
if (result.status === "error") {
|
|
356
|
+
// Read before anything is declared: the layer is unreadable here.
|
|
357
|
+
expect(result.message).toContain("gzipped JSON");
|
|
358
|
+
}
|
|
359
|
+
} finally {
|
|
360
|
+
await fake.close();
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("the layer names itself, and the rev comes from inside it", async () => {
|
|
365
|
+
const fake = await startFakeContentService({ token: TOKEN });
|
|
366
|
+
const root = makeArtifacts();
|
|
367
|
+
fs.writeFileSync(
|
|
368
|
+
path.join(root, ".val", "publish", "layer"),
|
|
369
|
+
zlib.gzipSync(
|
|
370
|
+
JSON.stringify({ rev: "layer-rev-7", worker: {}, browser: {} }),
|
|
371
|
+
),
|
|
372
|
+
);
|
|
373
|
+
try {
|
|
374
|
+
const result = await run(root, fake.url);
|
|
375
|
+
|
|
376
|
+
expect(result.status).toBe("live");
|
|
377
|
+
const [declaration] = [...fake.declarations.values()];
|
|
378
|
+
// A layer that is SENT has to say which it is: that is the name the
|
|
379
|
+
// loader stores and later reuses it by.
|
|
380
|
+
expect(declaration).toMatchObject({ layerRev: "layer-rev-7" });
|
|
381
|
+
} finally {
|
|
382
|
+
await fake.close();
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("a layer content already holds is named rather than sent", async () => {
|
|
387
|
+
const fake = await startFakeContentService({ token: TOKEN });
|
|
388
|
+
const root = makeArtifacts();
|
|
389
|
+
try {
|
|
390
|
+
const result = await run(root, fake.url, { layerRev: "layer-rev-9" });
|
|
391
|
+
|
|
392
|
+
expect(result.status).toBe("live");
|
|
393
|
+
const [declaration] = [...fake.declarations.values()];
|
|
394
|
+
expect(declaration).toMatchObject({ layerRev: "layer-rev-9" });
|
|
395
|
+
expect(fake.stored.has("layer")).toBe(false);
|
|
396
|
+
} finally {
|
|
397
|
+
await fake.close();
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("a declaration content refuses is reported with all of its problems", async () => {
|
|
402
|
+
const fake = await startFakeContentService({ token: TOKEN });
|
|
403
|
+
const root = makeArtifacts({ client: "console.log('client')\n" });
|
|
404
|
+
try {
|
|
405
|
+
const result = await run(root, fake.url);
|
|
406
|
+
|
|
407
|
+
expect(result.status).toBe("failed");
|
|
408
|
+
if (result.status === "failed") {
|
|
409
|
+
expect(result.message).toBe("This publish cannot be declared");
|
|
410
|
+
expect(result.problems).toEqual([
|
|
411
|
+
{
|
|
412
|
+
code: "ARTIFACT_MISSING",
|
|
413
|
+
message: "A publish needs at least a server and a client bundle.",
|
|
414
|
+
keys: ["server"],
|
|
415
|
+
},
|
|
416
|
+
]);
|
|
417
|
+
}
|
|
418
|
+
// Nothing was uploaded: a malformed declaration is refused before any
|
|
419
|
+
// slot is minted.
|
|
420
|
+
expect(fake.stored.size).toBe(0);
|
|
421
|
+
} finally {
|
|
422
|
+
await fake.close();
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test("--dry-run verifies and leaves the pointer alone", async () => {
|
|
427
|
+
const fake = await startFakeContentService({ token: TOKEN });
|
|
428
|
+
const root = makeArtifacts();
|
|
429
|
+
try {
|
|
430
|
+
const result = await run(root, fake.url, { dryRun: true });
|
|
431
|
+
|
|
432
|
+
expect(result.status).toBe("verified");
|
|
433
|
+
if (result.status === "verified") {
|
|
434
|
+
expect(result.previewUrl).toBeNull();
|
|
435
|
+
}
|
|
436
|
+
expect(apiCalls(fake.calls)).toEqual([
|
|
437
|
+
"POST /v1/publish",
|
|
438
|
+
"POST /v1/publish/pub_1/artifacts",
|
|
439
|
+
"POST /v1/publish/pub_1/verify",
|
|
440
|
+
]);
|
|
441
|
+
} finally {
|
|
442
|
+
await fake.close();
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test("a token that is not a project token is refused with that sentence", async () => {
|
|
447
|
+
const fake = await startFakeContentService({ token: TOKEN });
|
|
448
|
+
const root = makeArtifacts();
|
|
449
|
+
try {
|
|
450
|
+
const result = await run(root, fake.url, {
|
|
451
|
+
env: { VAL_PROJECT_TOKEN: "val_pt_other", VAL_CONTENT_URL: fake.url },
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
expect(result.status).toBe("error");
|
|
455
|
+
if (result.status === "error") {
|
|
456
|
+
expect(result.message).toContain("may have been revoked");
|
|
457
|
+
}
|
|
458
|
+
} finally {
|
|
459
|
+
await fake.close();
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test("an answer this CLI cannot read names the field, not the symptom", async () => {
|
|
464
|
+
// The service versions separately, so its answers are parsed rather than
|
|
465
|
+
// trusted: a type from a private repo could not have caught this, and a
|
|
466
|
+
// missing field surfacing three functions later is the failure worth
|
|
467
|
+
// spending a schema on.
|
|
468
|
+
const fake = await startFakeContentService({ token: TOKEN });
|
|
469
|
+
const root = makeArtifacts();
|
|
470
|
+
const truncating: typeof fetch = async (input, init) => {
|
|
471
|
+
const res = await fetch(input, init);
|
|
472
|
+
const url = typeof input === "string" ? input : String(input);
|
|
473
|
+
if (url.endsWith("/v1/publish") && init?.method === "POST") {
|
|
474
|
+
return new Response(JSON.stringify({ state: "awaiting-artifacts" }), {
|
|
475
|
+
status: 200,
|
|
476
|
+
headers: { "Content-Type": "application/json" },
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
return res;
|
|
480
|
+
};
|
|
481
|
+
try {
|
|
482
|
+
const result = await run(root, fake.url, { fetchImpl: truncating });
|
|
483
|
+
|
|
484
|
+
expect(result.status).toBe("error");
|
|
485
|
+
if (result.status === "error") {
|
|
486
|
+
expect(result.message).toContain("POST /v1/publish");
|
|
487
|
+
expect(result.message).toContain("publishId");
|
|
488
|
+
}
|
|
489
|
+
} finally {
|
|
490
|
+
await fake.close();
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test("nothing to publish is said before a credential is asked for", async () => {
|
|
495
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "val-publish-empty-"));
|
|
496
|
+
|
|
497
|
+
const result = await runPublish({ root, env: {}, log: () => undefined });
|
|
498
|
+
|
|
499
|
+
expect(result.status).toBe("error");
|
|
500
|
+
if (result.status === "error") {
|
|
501
|
+
expect(result.message).toContain("--artifacts");
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test("an unknown commit is refused rather than guessed", async () => {
|
|
506
|
+
const root = makeArtifacts();
|
|
507
|
+
|
|
508
|
+
const result = await runPublish({
|
|
509
|
+
root,
|
|
510
|
+
branch: "main",
|
|
511
|
+
env: {},
|
|
512
|
+
log: () => undefined,
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
expect(result.status).toBe("error");
|
|
516
|
+
if (result.status === "error") {
|
|
517
|
+
expect(result.message).toContain("which commit this build is of");
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test("a detached HEAD is not a branch", async () => {
|
|
522
|
+
const root = makeArtifacts();
|
|
523
|
+
|
|
524
|
+
const result = await runPublish({
|
|
525
|
+
root,
|
|
526
|
+
commit: "abc1234",
|
|
527
|
+
env: { VAL_GIT_BRANCH: "HEAD" },
|
|
528
|
+
log: () => undefined,
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
expect(result.status).toBe("error");
|
|
532
|
+
if (result.status === "error") {
|
|
533
|
+
expect(result.message).toContain("which branch this build is of");
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
});
|