@valbuild/server 0.137.0 → 0.138.1
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.
|
@@ -5170,6 +5170,15 @@ class ValOps {
|
|
|
5170
5170
|
return null;
|
|
5171
5171
|
}
|
|
5172
5172
|
|
|
5173
|
+
/**
|
|
5174
|
+
* Does a press of Publish run as a publish job (valbuild/home,
|
|
5175
|
+
* docs/app-mode.md, "Publishing is a queued job")? Never for a project
|
|
5176
|
+
* with no content service.
|
|
5177
|
+
*/
|
|
5178
|
+
publishesAsJobs() {
|
|
5179
|
+
return false;
|
|
5180
|
+
}
|
|
5181
|
+
|
|
5173
5182
|
/**
|
|
5174
5183
|
* The branch the content service keeps this project's commits on, or `null`
|
|
5175
5184
|
* where there is no such service, or it has not said yet.
|
|
@@ -8145,6 +8154,59 @@ async function readCommittedBinaryFiles(ops, descriptors) {
|
|
|
8145
8154
|
};
|
|
8146
8155
|
}
|
|
8147
8156
|
|
|
8157
|
+
/**
|
|
8158
|
+
* A publish job's prepare, split into what the tab builds and what content
|
|
8159
|
+
* archives.
|
|
8160
|
+
*
|
|
8161
|
+
* A job's content is its base plus every change it took (valbuild/home,
|
|
8162
|
+
* docs/app-mode.md, "Publishing is a queued job"). The server has two
|
|
8163
|
+
* `prepare`s to hand: `chainOnly` -- this build's embedded source with every
|
|
8164
|
+
* commit since applied, which is the branch's head -- and `withJob`, the same
|
|
8165
|
+
* with the job's pending changes on top. Anything applied since the job
|
|
8166
|
+
* started came out of the job's own change set, so `withJob` IS the job's
|
|
8167
|
+
* content; were that ever not so, the seal's containment check would restart
|
|
8168
|
+
* the job rather than publish it.
|
|
8169
|
+
*
|
|
8170
|
+
* - The BUILD needs all of `withJob`'s source files: it is a whole site.
|
|
8171
|
+
* - The ARCHIVE is what the commit publishes, so content is sent only what the
|
|
8172
|
+
* job changes: the files whose text differs from the head's, and the modules
|
|
8173
|
+
* and binary files its changes touch. Everything else is the head's, and an
|
|
8174
|
+
* archive that listed it would say this commit changed it.
|
|
8175
|
+
*
|
|
8176
|
+
* Files are compared rather than derived from module paths because a change to
|
|
8177
|
+
* a module can write a file under another path -- a `.val.json` entry of a
|
|
8178
|
+
* record -- which only the prepared output knows about.
|
|
8179
|
+
*/
|
|
8180
|
+
function splitJobPrepare(input) {
|
|
8181
|
+
const job = new Set(input.jobPatchIds);
|
|
8182
|
+
const before = input.chainOnly.patchedSourceFiles;
|
|
8183
|
+
const after = input.withJob.patchedSourceFiles;
|
|
8184
|
+
const patchedSourceFiles = {};
|
|
8185
|
+
for (const [path, text] of Object.entries(after)) {
|
|
8186
|
+
if (!(path in before) || before[path] !== text) {
|
|
8187
|
+
patchedSourceFiles[path] = text;
|
|
8188
|
+
}
|
|
8189
|
+
}
|
|
8190
|
+
const patchedBinaryFilesDescriptors = {};
|
|
8191
|
+
for (const [path, descriptor] of Object.entries(input.withJob.patchedBinaryFilesDescriptors)) {
|
|
8192
|
+
if (job.has(descriptor.patchId)) {
|
|
8193
|
+
patchedBinaryFilesDescriptors[path] = descriptor;
|
|
8194
|
+
}
|
|
8195
|
+
}
|
|
8196
|
+
|
|
8197
|
+
// The modules any of the job's changes were applied to.
|
|
8198
|
+
const touched = new Set(Object.entries(input.withJob.appliedPatches).filter(([, applied]) => applied.some(id => job.has(id))).map(([path]) => path));
|
|
8199
|
+
const modules = Object.fromEntries(Object.entries(input.withJob.moduleVersions).filter(([path]) => touched.has(path)));
|
|
8200
|
+
return {
|
|
8201
|
+
buildSourceFiles: after,
|
|
8202
|
+
archive: {
|
|
8203
|
+
patchedSourceFiles,
|
|
8204
|
+
patchedBinaryFilesDescriptors,
|
|
8205
|
+
modules
|
|
8206
|
+
}
|
|
8207
|
+
};
|
|
8208
|
+
}
|
|
8209
|
+
|
|
8148
8210
|
/**
|
|
8149
8211
|
* Which unpublished changes a failed save has to throw away to make progress.
|
|
8150
8212
|
*
|
|
@@ -8329,7 +8391,13 @@ const GetApplicablePatches = z.z.object({
|
|
|
8329
8391
|
*/
|
|
8330
8392
|
project: z.z.object({
|
|
8331
8393
|
sourceMode: z.z.union([z.z.literal("managed"), z.z.literal("connected")]),
|
|
8332
|
-
branch: z.z.string()
|
|
8394
|
+
branch: z.z.string(),
|
|
8395
|
+
/**
|
|
8396
|
+
* Whether a press of Publish is a publish JOB. Absent from a content
|
|
8397
|
+
* service that predates connected jobs: then managed projects publish
|
|
8398
|
+
* as jobs, and connected ones by commit, as they did.
|
|
8399
|
+
*/
|
|
8400
|
+
publishJobs: z.z.boolean().optional()
|
|
8333
8401
|
}).optional()
|
|
8334
8402
|
});
|
|
8335
8403
|
const FilesResponse = z.z.object({
|
|
@@ -8686,6 +8754,17 @@ class ValOpsHttp extends ValOps {
|
|
|
8686
8754
|
return ((_this$projectExpectat3 = this.projectExpectation) === null || _this$projectExpectat3 === void 0 ? void 0 : _this$projectExpectat3.branch) ?? null;
|
|
8687
8755
|
}
|
|
8688
8756
|
|
|
8757
|
+
/**
|
|
8758
|
+
* Does a press of Publish run as a publish job? Every managed project, and
|
|
8759
|
+
* a connected one content says is hosted on the platform (its CI publishes
|
|
8760
|
+
* through content). `false` before anything has been heard.
|
|
8761
|
+
*/
|
|
8762
|
+
publishesAsJobs() {
|
|
8763
|
+
const expected = this.projectExpectation;
|
|
8764
|
+
if (expected === null) return false;
|
|
8765
|
+
return expected.publishJobs ?? expected.sourceMode === "managed";
|
|
8766
|
+
}
|
|
8767
|
+
|
|
8689
8768
|
/**
|
|
8690
8769
|
* The short-lived publish token, and when it stops being usable.
|
|
8691
8770
|
*
|
|
@@ -8800,22 +8879,66 @@ class ValOpsHttp extends ValOps {
|
|
|
8800
8879
|
* segment gets through.
|
|
8801
8880
|
*/
|
|
8802
8881
|
static publishApiPathAllowed(path) {
|
|
8803
|
-
if (path === "/build-target" || path === "/project-source" || path === "/update-target" || path === "/publish"
|
|
8882
|
+
if (path === "/build-target" || path === "/project-source" || path === "/update-target" || path === "/publish" ||
|
|
8883
|
+
// Where "View run" goes, for a connected build CI reported failed.
|
|
8884
|
+
// Read only: reporting a run is CI's, with its own token.
|
|
8885
|
+
path === "/ci-runs/newest") {
|
|
8886
|
+
return true;
|
|
8887
|
+
}
|
|
8888
|
+
/*
|
|
8889
|
+
* Publishing as a queued job: a press, where it is, Try again, and the
|
|
8890
|
+
* tab's reports about the job it runs. NOT `/publish-jobs/{id}/prepare`:
|
|
8891
|
+
* its body is the job's source files and modules, which this server
|
|
8892
|
+
* computes (`prepareJob`) -- a browser allowed to send it could publish
|
|
8893
|
+
* any text at all as a commit's archive.
|
|
8894
|
+
*/
|
|
8895
|
+
if (/^\/publish-requests(\/[A-Za-z0-9_-]{1,100})?$/.test(path)) {
|
|
8896
|
+
return true;
|
|
8897
|
+
}
|
|
8898
|
+
if (/^\/publish-jobs\/(next|[A-Za-z0-9_-]{1,100}\/(steps|renew|cancel|discard))$/.test(path)) {
|
|
8804
8899
|
return true;
|
|
8805
8900
|
}
|
|
8806
8901
|
return /^\/publish\/[A-Za-z0-9_-]+(\/(artifacts|verify|promote))?$/.test(path);
|
|
8807
8902
|
}
|
|
8808
8903
|
async publishApi(path, init) {
|
|
8809
|
-
const json = "application/json";
|
|
8810
8904
|
if (!ValOpsHttp.publishApiPathAllowed(path)) {
|
|
8811
8905
|
return {
|
|
8812
8906
|
status: 403,
|
|
8813
|
-
contentType: json,
|
|
8907
|
+
contentType: "application/json",
|
|
8814
8908
|
body: JSON.stringify({
|
|
8815
8909
|
message: `'${path}' is not part of the publish API.`
|
|
8816
8910
|
})
|
|
8817
8911
|
};
|
|
8818
8912
|
}
|
|
8913
|
+
return this.callPublishApi(path, init);
|
|
8914
|
+
}
|
|
8915
|
+
|
|
8916
|
+
/**
|
|
8917
|
+
* A publish job's prepare, sent to content: the job's sources, archived
|
|
8918
|
+
* there before the job is built (valbuild/home, docs/app-mode.md, "The
|
|
8919
|
+
* archive, before the seal"). With this server's publish token, as every
|
|
8920
|
+
* publish call is, and never through `publishApi` -- see the allowlist.
|
|
8921
|
+
*/
|
|
8922
|
+
async prepareJob(jobId, body) {
|
|
8923
|
+
if (!/^[A-Za-z0-9_-]{1,100}$/.test(jobId)) {
|
|
8924
|
+
return {
|
|
8925
|
+
status: 400,
|
|
8926
|
+
contentType: "application/json",
|
|
8927
|
+
body: JSON.stringify({
|
|
8928
|
+
message: `'${jobId}' is not a job id.`
|
|
8929
|
+
})
|
|
8930
|
+
};
|
|
8931
|
+
}
|
|
8932
|
+
return this.callPublishApi(`/publish-jobs/${jobId}/prepare`, {
|
|
8933
|
+
method: "POST",
|
|
8934
|
+
body: JSON.stringify({
|
|
8935
|
+
...body,
|
|
8936
|
+
root: this.root
|
|
8937
|
+
})
|
|
8938
|
+
});
|
|
8939
|
+
}
|
|
8940
|
+
async callPublishApi(path, init) {
|
|
8941
|
+
const json = "application/json";
|
|
8819
8942
|
const send = async token => fetch(`${this.contentUrl}/v1${path}`, {
|
|
8820
8943
|
method: init.method,
|
|
8821
8944
|
headers: {
|
|
@@ -12967,6 +13090,9 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
12967
13090
|
...(sourceMode ? {
|
|
12968
13091
|
sourceMode
|
|
12969
13092
|
} : {}),
|
|
13093
|
+
...(sourceMode ? {
|
|
13094
|
+
publishJobs: serverOps.publishesAsJobs()
|
|
13095
|
+
} : {}),
|
|
12970
13096
|
// Not `options.config` verbatim: in proxy mode the branch the
|
|
12971
13097
|
// server resolved is filled in where the file did not name one.
|
|
12972
13098
|
// See `clientConfig`.
|
|
@@ -14132,8 +14258,9 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
14132
14258
|
return res;
|
|
14133
14259
|
}
|
|
14134
14260
|
},
|
|
14135
|
-
"/
|
|
14136
|
-
|
|
14261
|
+
"/publish-job-prepare": {
|
|
14262
|
+
POST: async req => {
|
|
14263
|
+
var _options$config$files2;
|
|
14137
14264
|
const auth = getAuth(req.cookies);
|
|
14138
14265
|
if (auth.error) {
|
|
14139
14266
|
return {
|
|
@@ -14143,14 +14270,93 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
14143
14270
|
}
|
|
14144
14271
|
};
|
|
14145
14272
|
}
|
|
14146
|
-
if (!(serverOps instanceof ValOpsHttp) || !serverOps.embedsSource()) {
|
|
14273
|
+
if (!(serverOps instanceof ValOpsHttp) || !serverOps.publishesAsJobs() || serverOps.sourceMode() === "managed" && !serverOps.embedsSource()) {
|
|
14274
|
+
return {
|
|
14275
|
+
status: 409,
|
|
14276
|
+
json: {
|
|
14277
|
+
message: "This deployment cannot prepare a publish job for this project."
|
|
14278
|
+
}
|
|
14279
|
+
};
|
|
14280
|
+
}
|
|
14281
|
+
const refusal = serverOps.publishRefusal();
|
|
14282
|
+
if (refusal) {
|
|
14147
14283
|
return {
|
|
14148
14284
|
status: 409,
|
|
14149
14285
|
json: {
|
|
14150
|
-
message:
|
|
14286
|
+
message: refusal.message
|
|
14151
14287
|
}
|
|
14152
14288
|
};
|
|
14153
14289
|
}
|
|
14290
|
+
const {
|
|
14291
|
+
jobId,
|
|
14292
|
+
tab,
|
|
14293
|
+
patchIds
|
|
14294
|
+
} = req.body;
|
|
14295
|
+
/*
|
|
14296
|
+
* CONNECTED: the job's sources are what `/save` would commit -- its
|
|
14297
|
+
* own changes, applied as `/save` applies them -- and content pushes
|
|
14298
|
+
* them at the seal. Nothing is built here: CI builds after the push,
|
|
14299
|
+
* so the answer carries the job and no sources.
|
|
14300
|
+
*/
|
|
14301
|
+
if (serverOps.sourceMode() === "connected") {
|
|
14302
|
+
var _options$config$files;
|
|
14303
|
+
const jobPatches = await serverOps.fetchPatches({
|
|
14304
|
+
patchIds,
|
|
14305
|
+
excludePatchOps: false
|
|
14306
|
+
});
|
|
14307
|
+
if (jobPatches.error) {
|
|
14308
|
+
return {
|
|
14309
|
+
status: 500,
|
|
14310
|
+
json: {
|
|
14311
|
+
message: jobPatches.error.message
|
|
14312
|
+
}
|
|
14313
|
+
};
|
|
14314
|
+
}
|
|
14315
|
+
const found = new Set(jobPatches.patches.map(patch => patch.patchId));
|
|
14316
|
+
const gone = patchIds.filter(id => !found.has(id));
|
|
14317
|
+
if (gone.length > 0) {
|
|
14318
|
+
return {
|
|
14319
|
+
status: 409,
|
|
14320
|
+
json: {
|
|
14321
|
+
message: `This publish's changes are no longer all there (${gone.length} discarded).`
|
|
14322
|
+
}
|
|
14323
|
+
};
|
|
14324
|
+
}
|
|
14325
|
+
const prepared = await serverOps.prepare({
|
|
14326
|
+
...serverOps.analyzePatches(jobPatches.patches, jobPatches.commits, commit),
|
|
14327
|
+
...jobPatches
|
|
14328
|
+
});
|
|
14329
|
+
if (prepared.hasErrors) {
|
|
14330
|
+
return {
|
|
14331
|
+
status: 500,
|
|
14332
|
+
json: {
|
|
14333
|
+
message: "This publish's changes could not be rendered into source: " + JSON.stringify(prepared.sourceFilePatchErrors).slice(0, 500)
|
|
14334
|
+
}
|
|
14335
|
+
};
|
|
14336
|
+
}
|
|
14337
|
+
const split = splitJobPrepare({
|
|
14338
|
+
// Every file the job's changes wrote is what the push writes.
|
|
14339
|
+
chainOnly: {
|
|
14340
|
+
patchedSourceFiles: {}
|
|
14341
|
+
},
|
|
14342
|
+
withJob: prepared,
|
|
14343
|
+
jobPatchIds: patchIds
|
|
14344
|
+
});
|
|
14345
|
+
const sent = await serverOps.prepareJob(jobId, {
|
|
14346
|
+
tab,
|
|
14347
|
+
filesDirectory: ((_options$config$files = options.config.files) === null || _options$config$files === void 0 ? void 0 : _options$config$files.directory) || "/public/val",
|
|
14348
|
+
...split.archive,
|
|
14349
|
+
...(commit !== undefined ? {
|
|
14350
|
+
gitCommit: commit
|
|
14351
|
+
} : {})
|
|
14352
|
+
});
|
|
14353
|
+
return jobPrepareAnswer(sent, {
|
|
14354
|
+
sourceFiles: {},
|
|
14355
|
+
binaryFiles: {},
|
|
14356
|
+
binaryFilesUnread: [],
|
|
14357
|
+
branch: serverOps.projectBranch()
|
|
14358
|
+
});
|
|
14359
|
+
}
|
|
14154
14360
|
const chain = await serverOps.fetchPatches({
|
|
14155
14361
|
patchIds: undefined,
|
|
14156
14362
|
excludePatchOps: false
|
|
@@ -14164,37 +14370,63 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
14164
14370
|
};
|
|
14165
14371
|
}
|
|
14166
14372
|
/*
|
|
14167
|
-
* The
|
|
14373
|
+
* The job's content, in two steps from this build's embedded source.
|
|
14168
14374
|
*
|
|
14169
|
-
*
|
|
14170
|
-
*
|
|
14171
|
-
*
|
|
14172
|
-
*
|
|
14173
|
-
*
|
|
14375
|
+
* The commits since this build, as though pending, are the branch's
|
|
14376
|
+
* head. The job's pending changes on top of them are its content: whatever was applied since the job started came
|
|
14377
|
+
* out of its own changes (docs/app-mode.md, "Publishing is a queued
|
|
14378
|
+
* job"), and were that ever not so, the seal would restart the job
|
|
14379
|
+
* rather than publish it. Chain order, both times.
|
|
14174
14380
|
*/
|
|
14381
|
+
const job = new Set(patchIds);
|
|
14175
14382
|
const committed = chain.patches.filter(patch => patch.appliedAt !== null).map(patch => ({
|
|
14176
14383
|
...patch,
|
|
14177
14384
|
appliedAt: null
|
|
14178
14385
|
}));
|
|
14179
|
-
const
|
|
14180
|
-
|
|
14386
|
+
const pending = chain.patches.filter(patch => patch.appliedAt === null && job.has(patch.patchId));
|
|
14387
|
+
const known = new Set(chain.patches.map(patch => patch.patchId));
|
|
14388
|
+
const gone = patchIds.filter(id => !known.has(id));
|
|
14389
|
+
if (gone.length > 0) {
|
|
14390
|
+
return {
|
|
14391
|
+
status: 409,
|
|
14392
|
+
json: {
|
|
14393
|
+
message: `This publish's changes are no longer all there (${gone.length} discarded).`
|
|
14394
|
+
}
|
|
14395
|
+
};
|
|
14396
|
+
}
|
|
14397
|
+
const prepareOf = patches => serverOps.prepare({
|
|
14398
|
+
...serverOps.analyzePatches(patches, chain.commits, commit),
|
|
14181
14399
|
...chain,
|
|
14182
|
-
patches
|
|
14400
|
+
patches
|
|
14183
14401
|
});
|
|
14184
|
-
|
|
14402
|
+
const chainOnly = await prepareOf(committed);
|
|
14403
|
+
const withJob = await prepareOf([...committed, ...pending]);
|
|
14404
|
+
if (chainOnly.hasErrors || withJob.hasErrors) {
|
|
14185
14405
|
return {
|
|
14186
14406
|
status: 500,
|
|
14187
14407
|
json: {
|
|
14188
|
-
message: "
|
|
14408
|
+
message: "This publish's changes could not be rendered into source: " + JSON.stringify((withJob.hasErrors ? withJob : chainOnly).sourceFilePatchErrors).slice(0, 500)
|
|
14189
14409
|
}
|
|
14190
14410
|
};
|
|
14191
14411
|
}
|
|
14192
|
-
|
|
14193
|
-
|
|
14194
|
-
|
|
14195
|
-
|
|
14196
|
-
|
|
14197
|
-
|
|
14412
|
+
const split = splitJobPrepare({
|
|
14413
|
+
chainOnly,
|
|
14414
|
+
withJob,
|
|
14415
|
+
jobPatchIds: patchIds
|
|
14416
|
+
});
|
|
14417
|
+
// Read now, while the job's changes still hold their files.
|
|
14418
|
+
const binaries = await readCommittedBinaryFiles(serverOps, split.archive.patchedBinaryFilesDescriptors);
|
|
14419
|
+
const sent = await serverOps.prepareJob(jobId, {
|
|
14420
|
+
tab,
|
|
14421
|
+
filesDirectory: ((_options$config$files2 = options.config.files) === null || _options$config$files2 === void 0 ? void 0 : _options$config$files2.directory) || "/public/val",
|
|
14422
|
+
...split.archive
|
|
14423
|
+
});
|
|
14424
|
+
return jobPrepareAnswer(sent, {
|
|
14425
|
+
sourceFiles: split.buildSourceFiles,
|
|
14426
|
+
binaryFiles: binaries.files,
|
|
14427
|
+
binaryFilesUnread: binaries.unread,
|
|
14428
|
+
branch: serverOps.projectBranch()
|
|
14429
|
+
});
|
|
14198
14430
|
}
|
|
14199
14431
|
},
|
|
14200
14432
|
"/profiles": {
|
|
@@ -14649,8 +14881,8 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
14649
14881
|
const managedBranch = serverOps.sourceMode() === "managed" ? serverOps.projectBranch() : null;
|
|
14650
14882
|
const committedBinaries = serverOps.sourceMode() === "managed" ? await readCommittedBinaryFiles(serverOps, preparedCommit.patchedBinaryFilesDescriptors) : null;
|
|
14651
14883
|
const commitToGit = () => {
|
|
14652
|
-
var _options$config$
|
|
14653
|
-
return serverOps.commit(preparedCommit, message, auth.id, ((_options$config$
|
|
14884
|
+
var _options$config$files3;
|
|
14885
|
+
return serverOps.commit(preparedCommit, message, auth.id, ((_options$config$files3 = options.config.files) === null || _options$config$files3 === void 0 ? void 0 : _options$config$files3.directory) || "/public/val", undefined,
|
|
14654
14886
|
/*
|
|
14655
14887
|
* Forwarded verbatim, and only the client can decide it: the
|
|
14656
14888
|
* content API closes the group it is named without checking
|
|
@@ -16304,6 +16536,49 @@ function getIsRemoteRequired(schemas) {
|
|
|
16304
16536
|
return false;
|
|
16305
16537
|
}
|
|
16306
16538
|
|
|
16539
|
+
/**
|
|
16540
|
+
* `/publish-job-prepare`'s answer from content's: the job as content has it
|
|
16541
|
+
* now, and what the tab builds from. Content's refusal is passed on -- 409
|
|
16542
|
+
* as 409, anything else as 502, which the tab reads as "content answered,
|
|
16543
|
+
* and has counted the attempt" (see `runStudioJob`).
|
|
16544
|
+
*/
|
|
16545
|
+
function jobPrepareAnswer(sent, build) {
|
|
16546
|
+
if (sent.status !== 200) {
|
|
16547
|
+
let message = `The content service did not prepare the publish (${sent.status}).`;
|
|
16548
|
+
try {
|
|
16549
|
+
const parsed = JSON.parse(sent.body);
|
|
16550
|
+
if (typeof parsed === "object" && parsed !== null && "message" in parsed && typeof parsed.message === "string") {
|
|
16551
|
+
message = parsed.message;
|
|
16552
|
+
}
|
|
16553
|
+
} catch {
|
|
16554
|
+
// not JSON: the status says enough
|
|
16555
|
+
}
|
|
16556
|
+
return {
|
|
16557
|
+
status: sent.status === 409 ? 409 : 502,
|
|
16558
|
+
json: {
|
|
16559
|
+
message
|
|
16560
|
+
}
|
|
16561
|
+
};
|
|
16562
|
+
}
|
|
16563
|
+
try {
|
|
16564
|
+
const prepared = internal.parseJob(JSON.parse(sent.body), "POST /v1/publish-jobs/{id}/prepare");
|
|
16565
|
+
return {
|
|
16566
|
+
status: 200,
|
|
16567
|
+
json: {
|
|
16568
|
+
job: prepared.job,
|
|
16569
|
+
...build
|
|
16570
|
+
}
|
|
16571
|
+
};
|
|
16572
|
+
} catch (e) {
|
|
16573
|
+
return {
|
|
16574
|
+
status: 502,
|
|
16575
|
+
json: {
|
|
16576
|
+
message: e instanceof Error ? e.message : String(e)
|
|
16577
|
+
}
|
|
16578
|
+
};
|
|
16579
|
+
}
|
|
16580
|
+
}
|
|
16581
|
+
|
|
16307
16582
|
async function createValServer(valModules, route, opts, config, callbacks, formatter,
|
|
16308
16583
|
/**
|
|
16309
16584
|
* Called after a save has applied its patches. EXPERIMENTAL — see
|