@valbuild/cli 0.132.1 → 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 +113 -0
- package/cli/dist/valbuild-cli-cli.cjs.dev.js +1428 -1
- package/cli/dist/valbuild-cli-cli.cjs.prod.js +1428 -1
- package/cli/dist/valbuild-cli-cli.esm.js +1426 -1
- package/package.json +7 -5
- package/src/cli.ts +51 -0
- package/src/debug/context.ts +1 -2
- 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,292 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseArtifacts,
|
|
3
|
+
parseDeclare,
|
|
4
|
+
parseProblems,
|
|
5
|
+
parsePromote,
|
|
6
|
+
parsePublishToken,
|
|
7
|
+
parseStatus,
|
|
8
|
+
parseVerify,
|
|
9
|
+
} from "./protocol";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* What content actually answers, run through the parsers that read it.
|
|
13
|
+
*
|
|
14
|
+
* These fixtures are COPIED FROM `home` - `content/src/handlers/Api.ts` for the
|
|
15
|
+
* shapes, and the handlers beside it for the values they really send - not
|
|
16
|
+
* written from what this side wishes it received. That is the whole point: the
|
|
17
|
+
* fake in `runPublish.test.ts` speaks whatever dialect this CLI expects, so a
|
|
18
|
+
* suite that is green against the fake says nothing about the service the fake
|
|
19
|
+
* stands in for. `home` and `@valbuild/server` have already diverged three
|
|
20
|
+
* times this way; `homeWireContract.test.ts` in `@valbuild/server` is the same
|
|
21
|
+
* test for the routes the Studio uses.
|
|
22
|
+
*
|
|
23
|
+
* When `home`'s publish handlers change, change these fixtures with them, and
|
|
24
|
+
* `contentApi.ts` with both. A failure here is the two repositories drifting,
|
|
25
|
+
* and it is the only thing on this side that can say so.
|
|
26
|
+
*
|
|
27
|
+
* Source: branch `claude/new-project-studio-saves-0g9ksb`, commit `ecf3b8d`.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** `Api["/publish"]["POST"]["res"]`, as `postPublish.ts` builds it. */
|
|
31
|
+
const HOME_DECLARE = {
|
|
32
|
+
publishId: "3f1b5f4e-2f1a-4c1e-9f77-0d3a4b5c6d7e",
|
|
33
|
+
state: "awaiting-artifacts",
|
|
34
|
+
project: {
|
|
35
|
+
publicProjectId: "a1b2c3d",
|
|
36
|
+
siteUrl: "https://site.example",
|
|
37
|
+
},
|
|
38
|
+
uploads: [
|
|
39
|
+
{
|
|
40
|
+
key: "server",
|
|
41
|
+
url: "https://fsn1.your-objectstorage.com/val-publish/0f0e.../publish/aa11?X-Amz-Signature=deadbeef",
|
|
42
|
+
// `mintUploadSlots`: the size is signed into the URL, not declared here.
|
|
43
|
+
method: "PUT",
|
|
44
|
+
headers: { "content-type": "application/octet-stream" },
|
|
45
|
+
expiresAt: "2026-09-19T10:00:00.000Z",
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
have: ["client", "public/index.html"],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The same route when the build is already published.
|
|
53
|
+
*
|
|
54
|
+
* `postPublish.ts` returns early with no slots at all: re-opening would mint
|
|
55
|
+
* permission to overwrite the bytes of a build that is currently serving.
|
|
56
|
+
*/
|
|
57
|
+
const HOME_DECLARE_LIVE = {
|
|
58
|
+
publishId: "3f1b5f4e-2f1a-4c1e-9f77-0d3a4b5c6d7e",
|
|
59
|
+
state: "live",
|
|
60
|
+
project: { publicProjectId: "a1b2c3d", siteUrl: "https://site.example" },
|
|
61
|
+
uploads: [],
|
|
62
|
+
have: ["server", "client"],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** A project with no site yet is reported, not refused. */
|
|
66
|
+
const HOME_DECLARE_NO_SITE = {
|
|
67
|
+
...HOME_DECLARE,
|
|
68
|
+
project: { publicProjectId: "a1b2c3d", siteUrl: null },
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** `Api["/publish/:publishId/artifacts"]["POST"]["res"]`. */
|
|
72
|
+
const HOME_ARTIFACTS = { state: "ready", problems: [] };
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The 409, with `confirmArtifacts`'s own problems in `details`.
|
|
76
|
+
*
|
|
77
|
+
* Both of these say "declare again" in the hint, which is what the round in
|
|
78
|
+
* `runPublish.ts` does.
|
|
79
|
+
*/
|
|
80
|
+
const HOME_ARTIFACTS_CONFLICT = {
|
|
81
|
+
statusCode: 409,
|
|
82
|
+
message: "Some artifacts did not arrive as declared",
|
|
83
|
+
details: [
|
|
84
|
+
{
|
|
85
|
+
code: "ARTIFACT_NOT_UPLOADED",
|
|
86
|
+
message:
|
|
87
|
+
"These artifacts were declared but nothing was uploaded for them.",
|
|
88
|
+
hint: "Upload slots expire. Declare the publish again for fresh ones.",
|
|
89
|
+
keys: ["public/app.css"],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
code: "ARTIFACT_MISMATCH",
|
|
93
|
+
message: "What was uploaded is not what was declared.",
|
|
94
|
+
hint: "The bytes at that key do not match the sha256 or the size given for it.",
|
|
95
|
+
keys: ["server"],
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** `Api["/publish/:publishId/verify"]["POST"]["res"]`, both ways. */
|
|
101
|
+
const HOME_VERIFY_OK = {
|
|
102
|
+
state: "verified",
|
|
103
|
+
ok: true,
|
|
104
|
+
// `CANARY_HAS_NO_PREVIEW = null`. The field is in the contract; there is
|
|
105
|
+
// nowhere to point at yet.
|
|
106
|
+
previewUrl: null,
|
|
107
|
+
problems: [],
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const HOME_VERIFY_FAILED = {
|
|
111
|
+
state: "failed",
|
|
112
|
+
ok: false,
|
|
113
|
+
previewUrl: null,
|
|
114
|
+
problems: [
|
|
115
|
+
{
|
|
116
|
+
code: "PLATFORM_BUILD_FAILED",
|
|
117
|
+
message: "rolldown: Could not resolve './missing'",
|
|
118
|
+
hint: "The canary build failed. Nothing was promoted.",
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** `Api["/publish/:publishId/promote"]["POST"]["res"]`. */
|
|
124
|
+
const HOME_PROMOTE = {
|
|
125
|
+
state: "live",
|
|
126
|
+
url: "https://site.example",
|
|
127
|
+
commit: "1234567890abcdef1234567890abcdef12345678",
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The stale pointer, as `postPublishPromote.ts` really answers it.
|
|
132
|
+
*
|
|
133
|
+
* The API's written contract has a top-level `head` with the sha the branch is
|
|
134
|
+
* at now; the handler does not send one, and puts the code in `details` like
|
|
135
|
+
* every other refusal. The CLI reads it this way and takes `head` if it ever
|
|
136
|
+
* turns up.
|
|
137
|
+
*/
|
|
138
|
+
const HOME_PROMOTE_STALE = {
|
|
139
|
+
statusCode: 409,
|
|
140
|
+
message:
|
|
141
|
+
"This build's commit is no longer the head of its branch, so it was not " +
|
|
142
|
+
"promoted. Rebuild from the current head.",
|
|
143
|
+
details: [{ code: "POINTER_STALE", message: "not-fast-forward" }],
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/** `Api["/publish/:publishId"]["GET"]["res"]`. */
|
|
147
|
+
const HOME_STATUS = {
|
|
148
|
+
publishId: "3f1b5f4e-2f1a-4c1e-9f77-0d3a4b5c6d7e",
|
|
149
|
+
state: "awaiting-artifacts",
|
|
150
|
+
buildHash: "b3a1c2d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90",
|
|
151
|
+
missing: ["public/app.css"],
|
|
152
|
+
problems: [],
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/** A declaration refused before any slot is minted, from `publishPlan.ts`. */
|
|
156
|
+
const HOME_DECLARE_REFUSED = {
|
|
157
|
+
statusCode: 400,
|
|
158
|
+
message: "This publish cannot be declared",
|
|
159
|
+
details: [
|
|
160
|
+
{
|
|
161
|
+
code: "ARTIFACT_MISSING",
|
|
162
|
+
message: "A publish needs at least a server and a client bundle.",
|
|
163
|
+
keys: ["server"],
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
code: "LAYER_REV_MISSING",
|
|
167
|
+
message: "A layer artifact must be declared with the layerRev it is.",
|
|
168
|
+
keys: ["layer"],
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/** `Api["/publish-token"]["POST"]["res"]`, from `postPublishToken.ts`. */
|
|
174
|
+
const HOME_PUBLISH_TOKEN = {
|
|
175
|
+
token:
|
|
176
|
+
"val_pt_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcd",
|
|
177
|
+
expiresAt: "2026-09-19T09:10:00.000Z",
|
|
178
|
+
publicProjectId: "a1b2c3d",
|
|
179
|
+
productionUrl: "https://site.example",
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
describe("the publish API, as content answers it", () => {
|
|
183
|
+
test("a declaration comes back with its slots and what was already held", () => {
|
|
184
|
+
const declared = parseDeclare(HOME_DECLARE);
|
|
185
|
+
|
|
186
|
+
expect(declared.publishId).toBe("3f1b5f4e-2f1a-4c1e-9f77-0d3a4b5c6d7e");
|
|
187
|
+
expect(declared.state).toBe("awaiting-artifacts");
|
|
188
|
+
expect(declared.project.siteUrl).toBe("https://site.example");
|
|
189
|
+
expect(declared.have).toEqual(["client", "public/index.html"]);
|
|
190
|
+
expect(declared.uploads).toHaveLength(1);
|
|
191
|
+
expect(declared.uploads[0].method).toBe("PUT");
|
|
192
|
+
expect(declared.uploads[0].headers["content-type"]).toBe(
|
|
193
|
+
"application/octet-stream",
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("a build that is already live comes back with no slots", () => {
|
|
198
|
+
const declared = parseDeclare(HOME_DECLARE_LIVE);
|
|
199
|
+
|
|
200
|
+
expect(declared.state).toBe("live");
|
|
201
|
+
expect(declared.uploads).toEqual([]);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("a project with no site yet parses, rather than being a failure", () => {
|
|
205
|
+
expect(parseDeclare(HOME_DECLARE_NO_SITE).project.siteUrl).toBeNull();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("confirming answers with a state", () => {
|
|
209
|
+
expect(parseArtifacts(HOME_ARTIFACTS).state).toBe("ready");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("the artifacts that did not arrive come back with the hint that says what to do", () => {
|
|
213
|
+
const problems = parseProblems(HOME_ARTIFACTS_CONFLICT.details);
|
|
214
|
+
|
|
215
|
+
expect(problems.map((problem) => problem.code)).toEqual([
|
|
216
|
+
"ARTIFACT_NOT_UPLOADED",
|
|
217
|
+
"ARTIFACT_MISMATCH",
|
|
218
|
+
]);
|
|
219
|
+
expect(problems[0].hint).toContain("Declare the publish again");
|
|
220
|
+
expect(problems[0].keys).toEqual(["public/app.css"]);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("a verify says whether it rendered, and why not", () => {
|
|
224
|
+
expect(parseVerify(HOME_VERIFY_OK)).toEqual({
|
|
225
|
+
state: "verified",
|
|
226
|
+
ok: true,
|
|
227
|
+
previewUrl: null,
|
|
228
|
+
problems: [],
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const failed = parseVerify(HOME_VERIFY_FAILED);
|
|
232
|
+
expect(failed.ok).toBe(false);
|
|
233
|
+
// The platform's own code, passed through by content unreworded.
|
|
234
|
+
expect(failed.problems[0].code).toBe("PLATFORM_BUILD_FAILED");
|
|
235
|
+
expect(failed.problems[0].hint).toBe(
|
|
236
|
+
"The canary build failed. Nothing was promoted.",
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("a promote says where it went live and at which commit", () => {
|
|
241
|
+
expect(parsePromote(HOME_PROMOTE)).toEqual({
|
|
242
|
+
state: "live",
|
|
243
|
+
url: "https://site.example",
|
|
244
|
+
commit: "1234567890abcdef1234567890abcdef12345678",
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("a stale pointer carries its code in details, where the handler puts it", () => {
|
|
249
|
+
const problems = parseProblems(HOME_PROMOTE_STALE.details);
|
|
250
|
+
|
|
251
|
+
expect(problems).toHaveLength(1);
|
|
252
|
+
expect(problems[0].code).toBe("POINTER_STALE");
|
|
253
|
+
// And the sentence a publisher acts on is content's own `message`, which
|
|
254
|
+
// the envelope carries beside the details rather than inside them.
|
|
255
|
+
expect(HOME_PROMOTE_STALE.message).toContain(
|
|
256
|
+
"Rebuild from the current head",
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("a status answers what is still missing", () => {
|
|
261
|
+
const status = parseStatus(HOME_STATUS);
|
|
262
|
+
|
|
263
|
+
expect(status.state).toBe("awaiting-artifacts");
|
|
264
|
+
expect(status.missing).toEqual(["public/app.css"]);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("a refused declaration carries every problem, not the first", () => {
|
|
268
|
+
const problems = parseProblems(HOME_DECLARE_REFUSED.details);
|
|
269
|
+
|
|
270
|
+
expect(problems.map((problem) => problem.code)).toEqual([
|
|
271
|
+
"ARTIFACT_MISSING",
|
|
272
|
+
"LAYER_REV_MISSING",
|
|
273
|
+
]);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("the exchange answers with a project token and when it dies", () => {
|
|
277
|
+
const exchanged = parsePublishToken(
|
|
278
|
+
HOME_PUBLISH_TOKEN,
|
|
279
|
+
"POST /v1/acme/site/publish-token",
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
expect(exchanged.token.startsWith("val_pt_")).toBe(true);
|
|
283
|
+
expect(exchanged.expiresAt).toBe("2026-09-19T09:10:00.000Z");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("a standing token has no expiry, and that is not a missing field", () => {
|
|
287
|
+
expect(
|
|
288
|
+
parsePublishToken({ ...HOME_PUBLISH_TOKEN, expiresAt: null }, "exchange")
|
|
289
|
+
.expiresAt,
|
|
290
|
+
).toBeNull();
|
|
291
|
+
});
|
|
292
|
+
});
|