@valbuild/server 0.146.1 → 0.146.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/dist/declarations/src/ValOps.d.ts +11 -0
- package/dist/declarations/src/ValOpsHttp.d.ts +31 -1
- package/dist/declarations/src/fixHandlers.d.ts +1 -1
- package/dist/valbuild-server.cjs.dev.js +538 -7
- package/dist/valbuild-server.cjs.prod.js +538 -7
- package/dist/valbuild-server.esm.js +539 -8
- package/package.json +4 -4
|
@@ -9711,6 +9711,45 @@ function mergeFromSite(input) {
|
|
|
9711
9711
|
};
|
|
9712
9712
|
}
|
|
9713
9713
|
|
|
9714
|
+
/** A change pending on the site, in chain order. */
|
|
9715
|
+
|
|
9716
|
+
/**
|
|
9717
|
+
* The site's pending changes that conflict with going from `site` to
|
|
9718
|
+
* `merged`, as whole patch sets: each inner list moves together, into a
|
|
9719
|
+
* proposal of its own. Everything else stays pending.
|
|
9720
|
+
*
|
|
9721
|
+
* Patch sets are the Studio's own (`PatchSets`), from the schema the changes
|
|
9722
|
+
* were written against; sets that share a change are one, because a change
|
|
9723
|
+
* cannot be split. A set moves when, taken as the site's side of a three-way
|
|
9724
|
+
* merge with the merged commit as the other, it conflicts (`mergeValues`, so
|
|
9725
|
+
* the same value on both sides moves too); or when it no longer applies to
|
|
9726
|
+
* the merged commit, or leaves it invalid under the merged schema.
|
|
9727
|
+
*/
|
|
9728
|
+
function changesToMove(input) {
|
|
9729
|
+
const {
|
|
9730
|
+
site,
|
|
9731
|
+
merged,
|
|
9732
|
+
pending
|
|
9733
|
+
} = input;
|
|
9734
|
+
const sets = new internal.PatchSets();
|
|
9735
|
+
for (const change of pending) {
|
|
9736
|
+
var _site$modules$change$;
|
|
9737
|
+
sets.insert(change.moduleFilePath, (_site$modules$change$ = site.modules[change.moduleFilePath]) === null || _site$modules$change$ === void 0 ? void 0 : _site$modules$change$.schema, change.patch, change.patchId, "", null);
|
|
9738
|
+
}
|
|
9739
|
+
const groups = internal.joinSharedChanges(sets.serialize().map(set => set.patches.map(p => p.patchId)));
|
|
9740
|
+
const moves = [];
|
|
9741
|
+
for (const group of groups) {
|
|
9742
|
+
const changes = pending.filter(change => group.has(change.patchId));
|
|
9743
|
+
if (conflictsWithMerge(changes, site, merged)) {
|
|
9744
|
+
moves.push(changes.map(change => change.patchId));
|
|
9745
|
+
}
|
|
9746
|
+
}
|
|
9747
|
+
// In chain order, so the moved proposals are opened in the order the
|
|
9748
|
+
// changes were written.
|
|
9749
|
+
const order = ids => pending.findIndex(change => change.patchId === ids[0]);
|
|
9750
|
+
return moves.sort((a, b) => order(a) - order(b));
|
|
9751
|
+
}
|
|
9752
|
+
|
|
9714
9753
|
// --- the pieces --------------------------------------------------------------
|
|
9715
9754
|
|
|
9716
9755
|
/**
|
|
@@ -9743,6 +9782,39 @@ function resolveContent(moduleFilePath, merged, resolutions) {
|
|
|
9743
9782
|
unresolved
|
|
9744
9783
|
};
|
|
9745
9784
|
}
|
|
9785
|
+
function conflictsWithMerge(changes, site, merged) {
|
|
9786
|
+
const modules = [...new Set(changes.map(change => change.moduleFilePath))];
|
|
9787
|
+
for (const moduleFilePath of modules) {
|
|
9788
|
+
var _site$modules$moduleF;
|
|
9789
|
+
const ops = changes.filter(change => change.moduleFilePath === moduleFilePath).flatMap(change => change.patch).filter(op => op.op !== "file");
|
|
9790
|
+
const before = (_site$modules$moduleF = site.modules[moduleFilePath]) === null || _site$modules$moduleF === void 0 ? void 0 : _site$modules$moduleF.source;
|
|
9791
|
+
const after = merged.modules[moduleFilePath];
|
|
9792
|
+
if (before === undefined || after === undefined) return true;
|
|
9793
|
+
const written = apply(before, ops);
|
|
9794
|
+
if (written === undefined) return true;
|
|
9795
|
+
const mergedSource = after.source;
|
|
9796
|
+
const atomic = atomicFor(after.schema);
|
|
9797
|
+
if (mergeValues(before, written, mergedSource, atomic).conflicts.length) {
|
|
9798
|
+
return true;
|
|
9799
|
+
}
|
|
9800
|
+
// Over the merged commit: it applies, and is as valid as it was.
|
|
9801
|
+
const over = apply(mergedSource, ops);
|
|
9802
|
+
if (over === undefined) return true;
|
|
9803
|
+
const known = new Set(errorPaths(moduleFilePath, after, mergedSource));
|
|
9804
|
+
if (errorPaths(moduleFilePath, after, over).some(path => !known.has(path))) {
|
|
9805
|
+
return true;
|
|
9806
|
+
}
|
|
9807
|
+
}
|
|
9808
|
+
return false;
|
|
9809
|
+
}
|
|
9810
|
+
function apply(source, ops) {
|
|
9811
|
+
if (source === undefined) return undefined;
|
|
9812
|
+
const applied = patch.applyPatch(patch.deepClone(source), new patch.JSONOps(), ops);
|
|
9813
|
+
return fp.result.isOk(applied) ? applied.value : undefined;
|
|
9814
|
+
}
|
|
9815
|
+
function errorPaths(moduleFilePath, module, source) {
|
|
9816
|
+
return validateModule(moduleFilePath, module, source).map(error => error.path.join("/"));
|
|
9817
|
+
}
|
|
9746
9818
|
|
|
9747
9819
|
/**
|
|
9748
9820
|
* The schema check of a merge: content in `merged` -- the project
|
|
@@ -10106,6 +10178,105 @@ function sameJson(a, b) {
|
|
|
10106
10178
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
10107
10179
|
}
|
|
10108
10180
|
|
|
10181
|
+
/**
|
|
10182
|
+
* Which of the site's pending changes a merge of a proposal moves into
|
|
10183
|
+
* proposals of their own (valbuild/home `docs/hosted-publishing.md`, step 8.2,
|
|
10184
|
+
* decision 4), from what content hands over (`GET /proposals/:name/move-inputs`)
|
|
10185
|
+
* and this server's schemas.
|
|
10186
|
+
*
|
|
10187
|
+
* The schemas are the ones this server runs: the proposal's own build at its
|
|
10188
|
+
* address, which is the code the merge publishes. They stand in for the site's
|
|
10189
|
+
* too, to group the site's changes into patch sets -- the same unless the
|
|
10190
|
+
* proposal changed that module's schema, in which case a change written
|
|
10191
|
+
* against the old one is checked against the new one, which is what the
|
|
10192
|
+
* merge leaves it under anyway.
|
|
10193
|
+
*
|
|
10194
|
+
* A page this server has no schema for is moved whole: what cannot be checked
|
|
10195
|
+
* is not left on the site to break.
|
|
10196
|
+
*/
|
|
10197
|
+
|
|
10198
|
+
const PatchIdOf = z.z.string().refine(s => !!s);
|
|
10199
|
+
const ModuleFilePathOf = z.z.string().refine(s => s.startsWith("/"));
|
|
10200
|
+
|
|
10201
|
+
/**
|
|
10202
|
+
* Content's `move-inputs` answer, as it is parsed: the one definition, so the
|
|
10203
|
+
* parser and what `movesAtMerge` reads cannot drift apart.
|
|
10204
|
+
*/
|
|
10205
|
+
const MoveInputs = z.z.object({
|
|
10206
|
+
/** The proposal's last save; absent from a content service before it. */
|
|
10207
|
+
save: z.z.string().nullable().optional(),
|
|
10208
|
+
siteHead: z.z.string().nullable(),
|
|
10209
|
+
pending: z.z.array(z.z.object({
|
|
10210
|
+
patchId: PatchIdOf,
|
|
10211
|
+
moduleFilePath: ModuleFilePathOf,
|
|
10212
|
+
patch: internal.Patch,
|
|
10213
|
+
author: z.z.string().nullable(),
|
|
10214
|
+
authorName: z.z.string().nullable()
|
|
10215
|
+
})),
|
|
10216
|
+
modules: z.z.record(z.z.string(), z.z.object({
|
|
10217
|
+
site: internal.JSONValue.nullable(),
|
|
10218
|
+
merged: internal.JSONValue.nullable()
|
|
10219
|
+
}))
|
|
10220
|
+
});
|
|
10221
|
+
|
|
10222
|
+
/** What the Publish press carries (`MergeBody.moves` in content), and who moves. */
|
|
10223
|
+
|
|
10224
|
+
function movesAtMerge(inputs, schemas) {
|
|
10225
|
+
const site = {
|
|
10226
|
+
files: {},
|
|
10227
|
+
modules: {}
|
|
10228
|
+
};
|
|
10229
|
+
const merged = {
|
|
10230
|
+
files: {},
|
|
10231
|
+
modules: {}
|
|
10232
|
+
};
|
|
10233
|
+
// The pages of the pending changes, which are the pages content answered.
|
|
10234
|
+
for (const moduleFilePath of new Set(inputs.pending.map(change => change.moduleFilePath))) {
|
|
10235
|
+
const page = inputs.modules[moduleFilePath];
|
|
10236
|
+
if (page === undefined) continue;
|
|
10237
|
+
const {
|
|
10238
|
+
site: before,
|
|
10239
|
+
merged: after
|
|
10240
|
+
} = page;
|
|
10241
|
+
const schema = schemas[moduleFilePath];
|
|
10242
|
+
if (schema === undefined) continue;
|
|
10243
|
+
const serialized = schema["executeSerialize"]();
|
|
10244
|
+
const validate = source => validateWith(schema, moduleFilePath, source);
|
|
10245
|
+
if (before !== null) {
|
|
10246
|
+
site.modules[moduleFilePath] = {
|
|
10247
|
+
source: before,
|
|
10248
|
+
schema: serialized,
|
|
10249
|
+
entries: null,
|
|
10250
|
+
validate
|
|
10251
|
+
};
|
|
10252
|
+
}
|
|
10253
|
+
if (after !== null) {
|
|
10254
|
+
merged.modules[moduleFilePath] = {
|
|
10255
|
+
source: after,
|
|
10256
|
+
schema: serialized,
|
|
10257
|
+
entries: null,
|
|
10258
|
+
validate
|
|
10259
|
+
};
|
|
10260
|
+
}
|
|
10261
|
+
}
|
|
10262
|
+
const groups = changesToMove({
|
|
10263
|
+
site,
|
|
10264
|
+
merged,
|
|
10265
|
+
pending: inputs.pending.map(change => ({
|
|
10266
|
+
patchId: change.patchId,
|
|
10267
|
+
moduleFilePath: change.moduleFilePath,
|
|
10268
|
+
patch: change.patch
|
|
10269
|
+
}))
|
|
10270
|
+
});
|
|
10271
|
+
const nameOf = new Map(inputs.pending.map(change => [change.patchId, change.authorName ?? "Someone"]));
|
|
10272
|
+
return {
|
|
10273
|
+
siteHead: inputs.siteHead,
|
|
10274
|
+
considered: inputs.pending.map(change => change.patchId),
|
|
10275
|
+
groups,
|
|
10276
|
+
authors: groups.map(group => [...new Set(group.map(id => nameOf.get(id) ?? "Someone"))])
|
|
10277
|
+
};
|
|
10278
|
+
}
|
|
10279
|
+
|
|
10109
10280
|
const textEncoder$2 = new TextEncoder();
|
|
10110
10281
|
|
|
10111
10282
|
/** The body of the content API's 403 for a write the project's plan refuses. */
|
|
@@ -10151,6 +10322,8 @@ const GetApplicablePatches = z.z.object({
|
|
|
10151
10322
|
* Optional for the same reason.
|
|
10152
10323
|
*/
|
|
10153
10324
|
headVersion: z.z.number().optional(),
|
|
10325
|
+
/** The branch's last commit. See `OrderedPatches.headCommitSha`. */
|
|
10326
|
+
headCommitSha: z.z.string().nullable().optional(),
|
|
10154
10327
|
/** Of `patches`, the ones a running publish holds. See `OrderedPatches.publishingPatches`. */
|
|
10155
10328
|
publishingPatches: z.z.array(internal.PatchId).optional(),
|
|
10156
10329
|
/** Of `patches`, the ones a failed publish holds back. See `OrderedPatches.heldBackPatches`. */
|
|
@@ -10871,7 +11044,7 @@ class ValOpsHttp extends ValOps {
|
|
|
10871
11044
|
return /^\/publish-jobs\/[A-Za-z0-9_-]{1,100}\/(merge-prepare|steps|renew|cancel)$/.test(path);
|
|
10872
11045
|
}
|
|
10873
11046
|
async proposalsApi(path, init, profileId) {
|
|
10874
|
-
var _exec2, _exec3, _this$proposal;
|
|
11047
|
+
var _exec2, _exec3, _exec4, _exec5, _this$proposal;
|
|
10875
11048
|
const json = "application/json";
|
|
10876
11049
|
const updating = (_exec2 = /^\/([0-9a-f]{20})\/update-content$/.exec(path)) === null || _exec2 === void 0 ? void 0 : _exec2[1];
|
|
10877
11050
|
if (updating !== undefined && init.method === "POST") {
|
|
@@ -10881,6 +11054,20 @@ class ValOpsHttp extends ValOps {
|
|
|
10881
11054
|
if (withCode !== undefined && init.method === "POST") {
|
|
10882
11055
|
return this.updateCodeFromSite(withCode, init.body, profileId);
|
|
10883
11056
|
}
|
|
11057
|
+
/*
|
|
11058
|
+
* Publish, and the checks before it, at the proposal's own address: Val
|
|
11059
|
+
* says which of the site's pending changes the merge moves, from this
|
|
11060
|
+
* build's schemas (step 8.2). Anywhere else they go to content as they
|
|
11061
|
+
* are, and the site's pending changes block as they always did.
|
|
11062
|
+
*/
|
|
11063
|
+
const checking = (_exec4 = /^\/([0-9a-f]{20})\/merge-checks$/.exec(path)) === null || _exec4 === void 0 ? void 0 : _exec4[1];
|
|
11064
|
+
if (checking !== undefined && init.method === "GET" && (await this.answersMovesFor(checking, profileId))) {
|
|
11065
|
+
return this.mergeChecksWithMoves(checking, profileId);
|
|
11066
|
+
}
|
|
11067
|
+
const pressing = (_exec5 = /^\/([0-9a-f]{20})\/merge$/.exec(path)) === null || _exec5 === void 0 ? void 0 : _exec5[1];
|
|
11068
|
+
if (pressing !== undefined && init.method === "POST" && (await this.answersMovesFor(pressing, profileId))) {
|
|
11069
|
+
return this.pressMergeWithMoves(pressing, init.body, profileId);
|
|
11070
|
+
}
|
|
10884
11071
|
if (!ValOpsHttp.proposalsApiAllowed(init.method, path)) {
|
|
10885
11072
|
return {
|
|
10886
11073
|
status: 403,
|
|
@@ -11068,6 +11255,143 @@ class ValOpsHttp extends ValOps {
|
|
|
11068
11255
|
return updatedAnswer(text);
|
|
11069
11256
|
}
|
|
11070
11257
|
|
|
11258
|
+
/**
|
|
11259
|
+
* Whether this server says what a merge of `name` moves: at the proposal's
|
|
11260
|
+
* own address, whose schemas are the ones it publishes; and on the site, for
|
|
11261
|
+
* Publish selected's proposal, which changes no code -- so the site's
|
|
11262
|
+
* schemas are its own -- and has no address to be published from.
|
|
11263
|
+
*/
|
|
11264
|
+
async answersMovesFor(name, profileId) {
|
|
11265
|
+
if (this.proposal !== null) return this.proposal.name === name;
|
|
11266
|
+
const read = await this.proposalContent(name, profileId)("");
|
|
11267
|
+
if (!read.ok) return false;
|
|
11268
|
+
const kind = ProposalKindAnswer.safeParse(await read.json().catch(() => null));
|
|
11269
|
+
return kind.success && kind.data.proposal.kind === "publish";
|
|
11270
|
+
}
|
|
11271
|
+
|
|
11272
|
+
/** Content's proposals API for one proposal, as the person asking. */
|
|
11273
|
+
proposalContent(name, profileId) {
|
|
11274
|
+
return (path, init = {}) => fetch(`${this.contentUrl}/v1/${this.project}/proposals/${name}${path}`, {
|
|
11275
|
+
...init,
|
|
11276
|
+
headers: {
|
|
11277
|
+
...this.authHeaders,
|
|
11278
|
+
...(profileId !== null ? {
|
|
11279
|
+
"x-val-profile-id": profileId
|
|
11280
|
+
} : {}),
|
|
11281
|
+
...(init.body === undefined ? {} : {
|
|
11282
|
+
"Content-Type": "application/json"
|
|
11283
|
+
})
|
|
11284
|
+
}
|
|
11285
|
+
});
|
|
11286
|
+
}
|
|
11287
|
+
|
|
11288
|
+
/**
|
|
11289
|
+
* Which of the site's pending changes a merge of this proposal moves
|
|
11290
|
+
* (`movesAtMerge`), or `null` when content could not say what is pending:
|
|
11291
|
+
* then nothing is claimed, and the site's pending changes block the merge.
|
|
11292
|
+
*/
|
|
11293
|
+
async mergeMovesOf(name, profileId) {
|
|
11294
|
+
const read = await this.proposalContent(name, profileId)("/move-inputs");
|
|
11295
|
+
if (!read.ok) return null;
|
|
11296
|
+
const inputs = MoveInputs.safeParse(await read.json().catch(() => null));
|
|
11297
|
+
if (!inputs.success) return null;
|
|
11298
|
+
return {
|
|
11299
|
+
...movesAtMerge(inputs.data, await this.getSchemas()),
|
|
11300
|
+
save: inputs.data.save
|
|
11301
|
+
};
|
|
11302
|
+
}
|
|
11303
|
+
|
|
11304
|
+
/**
|
|
11305
|
+
* The merge checks, with `pending-on-site` answered by what the merge
|
|
11306
|
+
* moves rather than by whether anything is pending: the conflicting
|
|
11307
|
+
* changes move to a proposal of their own when it is published, and the
|
|
11308
|
+
* rest stay, so neither blocks it.
|
|
11309
|
+
*/
|
|
11310
|
+
async mergeChecksWithMoves(name, profileId) {
|
|
11311
|
+
const json = "application/json";
|
|
11312
|
+
const read = await this.proposalContent(name, profileId)("/merge-checks");
|
|
11313
|
+
const text = await read.text();
|
|
11314
|
+
const answer = {
|
|
11315
|
+
status: read.status,
|
|
11316
|
+
contentType: json,
|
|
11317
|
+
body: text
|
|
11318
|
+
};
|
|
11319
|
+
if (!read.ok) return answer;
|
|
11320
|
+
const parsed = MergeChecksAnswer.safeParse(parseJsonOrNull(text));
|
|
11321
|
+
if (!parsed.success) return answer;
|
|
11322
|
+
const pending = parsed.data.checks.find(check => check.id === "pending-on-site");
|
|
11323
|
+
if (pending === undefined || pending.ok) return answer;
|
|
11324
|
+
const moves = await this.mergeMovesOf(name, profileId);
|
|
11325
|
+
if (moves === null) return answer;
|
|
11326
|
+
const moving = moves.groups.flat().length;
|
|
11327
|
+
const names = [...new Set(moves.authors.flat())];
|
|
11328
|
+
const checks = parsed.data.checks.map(check => check.id !== "pending-on-site" ? check : {
|
|
11329
|
+
...check,
|
|
11330
|
+
ok: true,
|
|
11331
|
+
blockers: [],
|
|
11332
|
+
message: moving === 0 ? "Unpublished changes on these pages do not conflict with this proposal, and stay unpublished" : `${moving === 1 ? "1 unpublished change" : `${moving} unpublished changes`} by ${namesOf(names)} ${moving === 1 ? "conflicts" : "conflict"} with this proposal, and ${moving === 1 ? "moves" : "move"} to ${moves.groups.length === 1 ? "a new proposal" : "new proposals"} when it is published`,
|
|
11333
|
+
moves: moves.groups.map((group, at) => ({
|
|
11334
|
+
patchIds: group,
|
|
11335
|
+
authors: moves.authors[at] ?? []
|
|
11336
|
+
}))
|
|
11337
|
+
});
|
|
11338
|
+
const othersOk = parsed.data.checks.every(check => check.ok || check.id === "pending-on-site");
|
|
11339
|
+
return {
|
|
11340
|
+
status: 200,
|
|
11341
|
+
contentType: json,
|
|
11342
|
+
body: JSON.stringify({
|
|
11343
|
+
...parsed.data,
|
|
11344
|
+
checks,
|
|
11345
|
+
canMerge: parsed.data.canMerge || othersOk
|
|
11346
|
+
})
|
|
11347
|
+
};
|
|
11348
|
+
}
|
|
11349
|
+
|
|
11350
|
+
/** Publish in a proposal, carrying the moves (`MergeBody.moves` in content). */
|
|
11351
|
+
async pressMergeWithMoves(name, body, profileId) {
|
|
11352
|
+
const json = "application/json";
|
|
11353
|
+
const content = this.proposalContent(name, profileId);
|
|
11354
|
+
const forward = async sent => {
|
|
11355
|
+
const res = await content("/merge", {
|
|
11356
|
+
method: "POST",
|
|
11357
|
+
...(sent === undefined ? {} : {
|
|
11358
|
+
body: sent
|
|
11359
|
+
})
|
|
11360
|
+
});
|
|
11361
|
+
return {
|
|
11362
|
+
status: res.status,
|
|
11363
|
+
contentType: res.headers.get("Content-Type") ?? json,
|
|
11364
|
+
body: await res.text()
|
|
11365
|
+
};
|
|
11366
|
+
};
|
|
11367
|
+
let pressed;
|
|
11368
|
+
try {
|
|
11369
|
+
pressed = body === undefined || body === "" ? {} : JSON.parse(body);
|
|
11370
|
+
} catch {
|
|
11371
|
+
return forward(body);
|
|
11372
|
+
}
|
|
11373
|
+
if (typeof pressed !== "object" || pressed === null) return forward(body);
|
|
11374
|
+
const moves = await this.mergeMovesOf(name, profileId);
|
|
11375
|
+
if (moves === null) return forward(body);
|
|
11376
|
+
return forward(JSON.stringify({
|
|
11377
|
+
/*
|
|
11378
|
+
* The save the moves were worked out against is the one this press
|
|
11379
|
+
* publishes: named, so a change written after the press -- which the
|
|
11380
|
+
* Studio lets an editor make at once -- continues in a new proposal
|
|
11381
|
+
* rather than refusing it as unsaved.
|
|
11382
|
+
*/
|
|
11383
|
+
...(typeof moves.save === "string" ? {
|
|
11384
|
+
save: moves.save
|
|
11385
|
+
} : {}),
|
|
11386
|
+
...pressed,
|
|
11387
|
+
moves: {
|
|
11388
|
+
siteHead: moves.siteHead,
|
|
11389
|
+
considered: moves.considered,
|
|
11390
|
+
groups: moves.groups
|
|
11391
|
+
}
|
|
11392
|
+
}));
|
|
11393
|
+
}
|
|
11394
|
+
|
|
11071
11395
|
/**
|
|
11072
11396
|
* Update from the site, for a proposal with code (valbuild/home
|
|
11073
11397
|
* `docs/hosted-publishing.md`, step 8.2, slice 1b), at
|
|
@@ -11845,6 +12169,9 @@ class ValOpsHttp extends ValOps {
|
|
|
11845
12169
|
...(data.headVersion !== undefined ? {
|
|
11846
12170
|
headVersion: data.headVersion
|
|
11847
12171
|
} : {}),
|
|
12172
|
+
...(data.headCommitSha !== undefined ? {
|
|
12173
|
+
headCommitSha: data.headCommitSha
|
|
12174
|
+
} : {}),
|
|
11848
12175
|
...(data.publishingPatches !== undefined ? {
|
|
11849
12176
|
publishingPatches: data.publishingPatches
|
|
11850
12177
|
} : {}),
|
|
@@ -12289,15 +12616,22 @@ class ValOpsHttp extends ValOps {
|
|
|
12289
12616
|
};
|
|
12290
12617
|
});
|
|
12291
12618
|
}
|
|
12292
|
-
async commit(prepared, message, committer, filesDirectory, newBranch
|
|
12619
|
+
async commit(prepared, message, committer, filesDirectory, newBranch,
|
|
12620
|
+
/**
|
|
12621
|
+
* A save into this proposal rather than this server's own branch: the
|
|
12622
|
+
* site's server saving Publish selected's proposal (`/publish-selected`),
|
|
12623
|
+
* which has no address of its own to save from.
|
|
12624
|
+
*/
|
|
12625
|
+
intoProposal) {
|
|
12293
12626
|
try {
|
|
12294
|
-
var _res$headers$get3;
|
|
12627
|
+
var _this$proposal5, _res$headers$get3;
|
|
12295
12628
|
/*
|
|
12296
12629
|
* A save in a proposal is a commit on the proposal's branch, at its own
|
|
12297
12630
|
* route: the site's `/commit` refuses a proposal's patches, which is
|
|
12298
12631
|
* the line that keeps them off the site until a merge.
|
|
12299
12632
|
*/
|
|
12300
|
-
const
|
|
12633
|
+
const proposalName = intoProposal ?? ((_this$proposal5 = this.proposal) === null || _this$proposal5 === void 0 ? void 0 : _this$proposal5.name);
|
|
12634
|
+
const commitUrl = proposalName !== undefined ? `${this.contentUrl}/v1/${this.project}/proposals/${proposalName}/save` : `${this.contentUrl}/v1/${this.project}/commit`;
|
|
12301
12635
|
const res = await fetch(commitUrl, {
|
|
12302
12636
|
method: "POST",
|
|
12303
12637
|
headers: {
|
|
@@ -12705,6 +13039,27 @@ const UpdateCodeBody = z.z.object({
|
|
|
12705
13039
|
}).optional()
|
|
12706
13040
|
});
|
|
12707
13041
|
|
|
13042
|
+
/** Content's `GET /proposals/:name/move-inputs`. */
|
|
13043
|
+
/** The one thing `answersMovesFor` reads from content's `GET /proposals/:name`. */
|
|
13044
|
+
const ProposalKindAnswer = z.z.object({
|
|
13045
|
+
proposal: z.z.object({
|
|
13046
|
+
kind: z.z.string()
|
|
13047
|
+
})
|
|
13048
|
+
});
|
|
13049
|
+
/** Content's `GET /proposals/:name/merge-checks`, as far as it is changed here. */
|
|
13050
|
+
const MergeChecksAnswer = z.z.object({
|
|
13051
|
+
checks: z.z.array(z.z.object({
|
|
13052
|
+
id: z.z.string(),
|
|
13053
|
+
ok: z.z.boolean()
|
|
13054
|
+
}).passthrough()),
|
|
13055
|
+
canMerge: z.z.boolean()
|
|
13056
|
+
}).passthrough();
|
|
13057
|
+
|
|
13058
|
+
/** "Kari", "Kari and Bo", "Kari, Bo and 2 more". */
|
|
13059
|
+
function namesOf(names) {
|
|
13060
|
+
return names.length <= 2 ? names.join(" and ") : `${names.slice(0, 2).join(", ")} and ${names.length - 2} more`;
|
|
13061
|
+
}
|
|
13062
|
+
|
|
12708
13063
|
/** Content's `GET /proposals/:name/merge-trees`. */
|
|
12709
13064
|
const MergeTrees = z.z.object({
|
|
12710
13065
|
base: z.z.string().nullable(),
|
|
@@ -12748,6 +13103,15 @@ const MergeInputs = z.z.object({
|
|
|
12748
13103
|
}))
|
|
12749
13104
|
});
|
|
12750
13105
|
|
|
13106
|
+
/** `JSON.parse`, or `null` for a body that is not JSON: an answer passed on as it came. */
|
|
13107
|
+
function parseJsonOrNull(text) {
|
|
13108
|
+
try {
|
|
13109
|
+
return JSON.parse(text);
|
|
13110
|
+
} catch {
|
|
13111
|
+
return null;
|
|
13112
|
+
}
|
|
13113
|
+
}
|
|
13114
|
+
|
|
12751
13115
|
/**
|
|
12752
13116
|
* One stored patch. Everything the ordered chain needs and nothing else.
|
|
12753
13117
|
*/
|
|
@@ -16696,6 +17060,142 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
16696
17060
|
});
|
|
16697
17061
|
}
|
|
16698
17062
|
},
|
|
17063
|
+
"/publish-selected": {
|
|
17064
|
+
POST: async req => {
|
|
17065
|
+
const auth = getAuth(req.cookies);
|
|
17066
|
+
if (auth.error) {
|
|
17067
|
+
return {
|
|
17068
|
+
status: 401,
|
|
17069
|
+
json: {
|
|
17070
|
+
message: auth.error
|
|
17071
|
+
}
|
|
17072
|
+
};
|
|
17073
|
+
}
|
|
17074
|
+
const profileId = profileOf(auth);
|
|
17075
|
+
if (profileId === null) {
|
|
17076
|
+
return {
|
|
17077
|
+
status: 401,
|
|
17078
|
+
json: {
|
|
17079
|
+
message: "Sign in to publish"
|
|
17080
|
+
}
|
|
17081
|
+
};
|
|
17082
|
+
}
|
|
17083
|
+
if (serverOps instanceof ValOpsHttp) {
|
|
17084
|
+
await serverOps.learnProjectExpectation();
|
|
17085
|
+
}
|
|
17086
|
+
if (!(serverOps instanceof ValOpsHttp) || serverOps.sourceMode() !== "managed" || !serverOps.publishesAsJobs() || serverOps.currentProposal() !== null) {
|
|
17087
|
+
return {
|
|
17088
|
+
status: 409,
|
|
17089
|
+
json: {
|
|
17090
|
+
message: "Publish selected is for a site whose publishes are proposals: this deployment cannot."
|
|
17091
|
+
}
|
|
17092
|
+
};
|
|
17093
|
+
}
|
|
17094
|
+
const refusal = serverOps.publishRefusal();
|
|
17095
|
+
if (refusal) {
|
|
17096
|
+
return {
|
|
17097
|
+
status: 409,
|
|
17098
|
+
json: {
|
|
17099
|
+
message: refusal.message
|
|
17100
|
+
}
|
|
17101
|
+
};
|
|
17102
|
+
}
|
|
17103
|
+
const {
|
|
17104
|
+
patchIds,
|
|
17105
|
+
displayName,
|
|
17106
|
+
requestId
|
|
17107
|
+
} = req.body;
|
|
17108
|
+
/*
|
|
17109
|
+
* The selection's content over the site's head, exactly as a publish
|
|
17110
|
+
* job of it is prepared: this build's source, the commits since it,
|
|
17111
|
+
* and the selection on top. Only what the selection touched is saved,
|
|
17112
|
+
* so the proposal's save is the selection and nothing else.
|
|
17113
|
+
*/
|
|
17114
|
+
const built = await buildSourcesOf(serverOps, patchIds, commit, {
|
|
17115
|
+
binaries: false
|
|
17116
|
+
});
|
|
17117
|
+
if ("status" in built) return built;
|
|
17118
|
+
if (built.headCommitSha === undefined) {
|
|
17119
|
+
return {
|
|
17120
|
+
status: 409,
|
|
17121
|
+
json: {
|
|
17122
|
+
message: "The content service does not say where the site's head is, which Publish selected needs: it is older than this Studio."
|
|
17123
|
+
}
|
|
17124
|
+
};
|
|
17125
|
+
}
|
|
17126
|
+
const selected = new Set(patchIds);
|
|
17127
|
+
const prepared = {
|
|
17128
|
+
...built.withJob,
|
|
17129
|
+
patchedSourceFiles: built.split.archive.patchedSourceFiles,
|
|
17130
|
+
patchedBinaryFilesDescriptors: built.split.archive.patchedBinaryFilesDescriptors,
|
|
17131
|
+
moduleVersions: built.split.archive.modules,
|
|
17132
|
+
appliedPatches: Object.fromEntries(Object.entries(built.withJob.appliedPatches).map(([path, ids]) => [path, ids.filter(id => selected.has(id))]).filter(([, ids]) => ids.length > 0))
|
|
17133
|
+
};
|
|
17134
|
+
/*
|
|
17135
|
+
* Moved and based in one transaction, on the head the selection was
|
|
17136
|
+
* prepared over: content refuses the create when the site is no
|
|
17137
|
+
* longer there, and nothing has moved.
|
|
17138
|
+
*/
|
|
17139
|
+
const created = await serverOps.proposalsApi("", {
|
|
17140
|
+
method: "POST",
|
|
17141
|
+
body: JSON.stringify({
|
|
17142
|
+
displayName,
|
|
17143
|
+
kind: "publish",
|
|
17144
|
+
patchIds,
|
|
17145
|
+
requestId,
|
|
17146
|
+
base: built.headCommitSha
|
|
17147
|
+
})
|
|
17148
|
+
}, profileId);
|
|
17149
|
+
const createdBody = PublishProposalCreated.safeParse(safeJson(created.body));
|
|
17150
|
+
const createdError = ContentError.safeParse(safeJson(created.body));
|
|
17151
|
+
if (created.status === 409) {
|
|
17152
|
+
/*
|
|
17153
|
+
* Content refuses a base that is not the site's head -- a publish
|
|
17154
|
+
* landed in between, and pressing again prepares over the new one.
|
|
17155
|
+
* Any other refusal is content's to put in words, and again would
|
|
17156
|
+
* be refused the same way.
|
|
17157
|
+
*/
|
|
17158
|
+
const message = createdError.success ? createdError.data.message : "";
|
|
17159
|
+
return /base is not the site's head/.test(message) ? {
|
|
17160
|
+
status: 409,
|
|
17161
|
+
json: {
|
|
17162
|
+
message: "The site was published while this was being prepared. Nothing was published: publish them again.",
|
|
17163
|
+
headMoved: true
|
|
17164
|
+
}
|
|
17165
|
+
} : {
|
|
17166
|
+
status: 409,
|
|
17167
|
+
json: {
|
|
17168
|
+
message: message || "The changes could not be moved into a proposal to publish them."
|
|
17169
|
+
}
|
|
17170
|
+
};
|
|
17171
|
+
}
|
|
17172
|
+
if (created.status !== 200 || !createdBody.success) {
|
|
17173
|
+
return {
|
|
17174
|
+
status: 500,
|
|
17175
|
+
json: {
|
|
17176
|
+
message: `The changes could not be moved into a proposal to publish them (${created.status}): ${created.body.slice(0, 300)}`
|
|
17177
|
+
}
|
|
17178
|
+
};
|
|
17179
|
+
}
|
|
17180
|
+
const proposal = createdBody.data.proposal.name;
|
|
17181
|
+
const saved = await serverOps.commit(prepared, `Publish selected: ${displayName}`, profileId, FILES_DIRECTORY, undefined, proposal);
|
|
17182
|
+
if (saved.error) {
|
|
17183
|
+
return {
|
|
17184
|
+
status: 502,
|
|
17185
|
+
json: {
|
|
17186
|
+
message: `The changes are in a proposal, but could not be saved there: ${saved.error.message}`,
|
|
17187
|
+
proposal
|
|
17188
|
+
}
|
|
17189
|
+
};
|
|
17190
|
+
}
|
|
17191
|
+
return {
|
|
17192
|
+
status: 200,
|
|
17193
|
+
json: {
|
|
17194
|
+
proposal
|
|
17195
|
+
}
|
|
17196
|
+
};
|
|
17197
|
+
}
|
|
17198
|
+
},
|
|
16699
17199
|
"/admin/status": {
|
|
16700
17200
|
GET: async req => {
|
|
16701
17201
|
const credential = await getValBuildCredential(req.cookies);
|
|
@@ -18591,7 +19091,14 @@ function getIsRemoteRequired(schemas) {
|
|
|
18591
19091
|
* `split.archive` is the managed job's archive; a connected job's archive is
|
|
18592
19092
|
* the push's, made from its own changes alone, by the caller.
|
|
18593
19093
|
*/
|
|
18594
|
-
async function buildSourcesOf(serverOps, patchIds, commit
|
|
19094
|
+
async function buildSourcesOf(serverOps, patchIds, commit,
|
|
19095
|
+
/**
|
|
19096
|
+
* Read the job's binary files back for a build. Off for a caller that only
|
|
19097
|
+
* commits what was prepared, which names the files rather than carrying them.
|
|
19098
|
+
*/
|
|
19099
|
+
options = {
|
|
19100
|
+
binaries: true
|
|
19101
|
+
}) {
|
|
18595
19102
|
const chain = await serverOps.fetchPatches({
|
|
18596
19103
|
patchIds: undefined,
|
|
18597
19104
|
excludePatchOps: false
|
|
@@ -18650,9 +19157,14 @@ async function buildSourcesOf(serverOps, patchIds, commit) {
|
|
|
18650
19157
|
jobPatchIds: patchIds
|
|
18651
19158
|
});
|
|
18652
19159
|
// Read now, while the job's changes still hold their files.
|
|
18653
|
-
const binaries = await readCommittedBinaryFiles(serverOps, split.archive.patchedBinaryFilesDescriptors)
|
|
19160
|
+
const binaries = options.binaries ? await readCommittedBinaryFiles(serverOps, split.archive.patchedBinaryFilesDescriptors) : {
|
|
19161
|
+
files: {},
|
|
19162
|
+
unread: []
|
|
19163
|
+
};
|
|
18654
19164
|
return {
|
|
18655
19165
|
split,
|
|
19166
|
+
withJob,
|
|
19167
|
+
headCommitSha: chain.headCommitSha,
|
|
18656
19168
|
sourceFiles: split.buildSourceFiles,
|
|
18657
19169
|
binaryFiles: binaries.files,
|
|
18658
19170
|
binaryFilesUnread: binaries.unread
|
|
@@ -18702,6 +19214,24 @@ function jobPrepareAnswer(sent, build) {
|
|
|
18702
19214
|
}
|
|
18703
19215
|
}
|
|
18704
19216
|
|
|
19217
|
+
/** What `/publish-selected` reads from content's proposal create. */
|
|
19218
|
+
const PublishProposalCreated = z.z.object({
|
|
19219
|
+
proposal: z.z.object({
|
|
19220
|
+
name: z.z.string()
|
|
19221
|
+
})
|
|
19222
|
+
});
|
|
19223
|
+
/** Content's refusal, in its words. */
|
|
19224
|
+
const ContentError = z.z.object({
|
|
19225
|
+
message: z.z.string()
|
|
19226
|
+
});
|
|
19227
|
+
function safeJson(text) {
|
|
19228
|
+
try {
|
|
19229
|
+
return JSON.parse(text);
|
|
19230
|
+
} catch {
|
|
19231
|
+
return null;
|
|
19232
|
+
}
|
|
19233
|
+
}
|
|
19234
|
+
|
|
18705
19235
|
async function createValServer(valModules, route, opts, config, callbacks, formatter,
|
|
18706
19236
|
/**
|
|
18707
19237
|
* Called after a save has applied its patches. EXPERIMENTAL — see
|
|
@@ -23577,7 +24107,8 @@ async function handleExternalUpload() {
|
|
|
23577
24107
|
};
|
|
23578
24108
|
}
|
|
23579
24109
|
|
|
23580
|
-
// Fix handler registry. `keyof:check-keys`,
|
|
24110
|
+
// Fix handler registry. `keyof:check-keys`, the gallery markers
|
|
24111
|
+
// (`image:check-gallery` and the rest), `router:check-route`,
|
|
23581
24112
|
// `locale:check-locale` and `record:fill-keys` are resolved upfront by the
|
|
23582
24113
|
// shared resolveSchemaSourceFixes — they never reach this registry, so they're
|
|
23583
24114
|
// excluded from the key set.
|