@ebay-dt/bonfire-node 0.2.0-alpha.10 → 0.2.0-alpha.11
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/README.md +18 -5
- package/dist/deployment-provider.d.ts +1 -1
- package/dist/deployment-provider.d.ts.map +1 -1
- package/dist/deployment-provider.js +1 -1
- package/dist/deployment-provider.js.map +1 -1
- package/dist/duplicate.d.ts.map +1 -1
- package/dist/duplicate.js +103 -47
- package/dist/duplicate.js.map +1 -1
- package/dist/git-repository.d.ts +14 -1
- package/dist/git-repository.d.ts.map +1 -1
- package/dist/git-repository.js +99 -3
- package/dist/git-repository.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/prototype-templates.d.ts +0 -1
- package/dist/internal/prototype-templates.d.ts.map +1 -1
- package/dist/internal/prototype-templates.js +0 -5
- package/dist/internal/prototype-templates.js.map +1 -1
- package/dist/lifecycle/deployment-worktree.d.ts +5 -4
- package/dist/lifecycle/deployment-worktree.d.ts.map +1 -1
- package/dist/lifecycle/deployment-worktree.js +269 -68
- package/dist/lifecycle/deployment-worktree.js.map +1 -1
- package/dist/lifecycle/local-prototype-lifecycle.d.ts +13 -1
- package/dist/lifecycle/local-prototype-lifecycle.d.ts.map +1 -1
- package/dist/lifecycle/local-prototype-lifecycle.js +634 -110
- package/dist/lifecycle/local-prototype-lifecycle.js.map +1 -1
- package/dist/lifecycle/preparation-marker.d.ts +12 -0
- package/dist/lifecycle/preparation-marker.d.ts.map +1 -0
- package/dist/lifecycle/preparation-marker.js +76 -0
- package/dist/lifecycle/preparation-marker.js.map +1 -0
- package/dist/lifecycle/prototype-path-policy.d.ts +18 -0
- package/dist/lifecycle/prototype-path-policy.d.ts.map +1 -0
- package/dist/lifecycle/prototype-path-policy.js +429 -0
- package/dist/lifecycle/prototype-path-policy.js.map +1 -0
- package/dist/lifecycle/repository-summary.d.ts +5 -1
- package/dist/lifecycle/repository-summary.d.ts.map +1 -1
- package/dist/lifecycle/repository-summary.js +6 -0
- package/dist/lifecycle/repository-summary.js.map +1 -1
- package/dist/lifecycle/repository-synchronization.d.ts +13 -9
- package/dist/lifecycle/repository-synchronization.d.ts.map +1 -1
- package/dist/lifecycle/repository-synchronization.js +670 -173
- package/dist/lifecycle/repository-synchronization.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { constants } from "node:fs";
|
|
2
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
2
3
|
import { lstat, open, rename, rm, writeFile } from "node:fs/promises";
|
|
3
4
|
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { promisify } from "node:util";
|
|
4
6
|
import { join, relative, sep } from "node:path";
|
|
5
7
|
import { definePrototypeRef, getPrototypeUrl, LifecycleInputError, lifecycleFailure, lifecycleSuccess, parseCreatePrototypeInput, parseDuplicatePrototypeInput, parseEditPrototypeInput, parseBonfireIdentity, parsePrototypeCommandInput, parsePrototypeMetadata, slugifyPrototypeTitle, PrototypeRefError, parseGitModeSetting, } from "@ebay-dt/bonfire-core";
|
|
6
|
-
import { commitPath,
|
|
8
|
+
import { commitPath, GitRepositoryError, pushBranch, pushCommitToBranch, remoteBranchCommit, } from "../git-repository.js";
|
|
7
9
|
import { duplicatePrototype, PrototypeDuplicateError } from "../duplicate.js";
|
|
8
10
|
import { PrototypeCatalog, } from "../prototype-catalog.js";
|
|
9
11
|
import { PrototypeScaffoldError, scaffoldPrototype } from "../scaffold.js";
|
|
@@ -13,11 +15,19 @@ import { RepositoryBusyError, withRepositoryOperation, } from "./operation-gate.
|
|
|
13
15
|
import { DeploymentProviderError, } from "../deployment-provider.js";
|
|
14
16
|
import { assertIdentityBranchReady, synchronizeIdentityBranch, } from "./repository-synchronization.js";
|
|
15
17
|
import { getRepositorySummary } from "./repository-summary.js";
|
|
16
|
-
import { DeploymentWorktreeError,
|
|
18
|
+
import { DeploymentWorktreeError, publishDeploymentSource, reportDeploymentIssue, } from "./deployment-worktree.js";
|
|
19
|
+
import { commitPrototypePath, deletePrototypePath, discardPrototypePath, } from "./prototype-path-policy.js";
|
|
20
|
+
import { clearPreparationMarker, readPreparationMarker, writePreparationMarker, } from "./preparation-marker.js";
|
|
21
|
+
const execFile = promisify(execFileCallback);
|
|
17
22
|
function repositoryRelativePath(runtime, ref) {
|
|
18
23
|
const absolutePath = join(runtime.prototypeRoot, ref.login, ref.slug);
|
|
19
24
|
return relative(runtime.repositoryRoot, absolutePath).split(sep).join("/");
|
|
20
25
|
}
|
|
26
|
+
function namespaceRelativePath(runtime, login) {
|
|
27
|
+
return relative(runtime.repositoryRoot, join(runtime.prototypeRoot, login))
|
|
28
|
+
.split(sep)
|
|
29
|
+
.join("/");
|
|
30
|
+
}
|
|
21
31
|
function destinationRef(identity, title) {
|
|
22
32
|
const slug = slugifyPrototypeTitle(title);
|
|
23
33
|
if (slug === null)
|
|
@@ -36,21 +46,68 @@ class DeploymentSourcePushError extends Error {
|
|
|
36
46
|
}
|
|
37
47
|
class DeploymentNotAcceptedError extends Error {
|
|
38
48
|
}
|
|
49
|
+
const pendingPublications = new Map();
|
|
50
|
+
function pendingPublicationKey(repositoryRoot, login) {
|
|
51
|
+
return `${repositoryRoot}\0${login}`;
|
|
52
|
+
}
|
|
39
53
|
function metadataPath(runtime, ref) {
|
|
40
54
|
return join(runtime.prototypeRoot, ref.login, ref.slug, "metadata.json");
|
|
41
55
|
}
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
function prototypeDirectoryPaths(runtime, ref) {
|
|
57
|
+
return [
|
|
44
58
|
runtime.prototypeRoot,
|
|
45
59
|
join(runtime.prototypeRoot, ref.login),
|
|
46
60
|
join(runtime.prototypeRoot, ref.login, ref.slug),
|
|
47
61
|
];
|
|
48
|
-
|
|
49
|
-
|
|
62
|
+
}
|
|
63
|
+
async function capturePrototypeDirectory(runtime, ref) {
|
|
64
|
+
const entries = [];
|
|
65
|
+
for (const path of prototypeDirectoryPaths(runtime, ref)) {
|
|
66
|
+
try {
|
|
50
67
|
const stats = await lstat(path);
|
|
51
|
-
|
|
52
|
-
|
|
68
|
+
entries.push({
|
|
69
|
+
path,
|
|
70
|
+
kind: stats.isSymbolicLink()
|
|
71
|
+
? "symlink"
|
|
72
|
+
: stats.isDirectory()
|
|
73
|
+
? "directory"
|
|
74
|
+
: "other",
|
|
75
|
+
dev: stats.dev,
|
|
76
|
+
ino: stats.ino,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (error &&
|
|
81
|
+
typeof error === "object" &&
|
|
82
|
+
"code" in error &&
|
|
83
|
+
error.code === "ENOENT") {
|
|
84
|
+
entries.push({ path, kind: "missing" });
|
|
85
|
+
continue;
|
|
53
86
|
}
|
|
87
|
+
throw new PrototypeMetadataFileError();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return entries;
|
|
91
|
+
}
|
|
92
|
+
function samePrototypeDirectorySnapshot(expected, actual) {
|
|
93
|
+
return expected.every((entry, index) => {
|
|
94
|
+
const current = actual[index];
|
|
95
|
+
return (current !== undefined &&
|
|
96
|
+
current.path === entry.path &&
|
|
97
|
+
current.kind === entry.kind &&
|
|
98
|
+
current.dev === entry.dev &&
|
|
99
|
+
current.ino === entry.ino);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
async function assertPrototypeDirectory(runtime, ref, expected) {
|
|
103
|
+
try {
|
|
104
|
+
const actual = await capturePrototypeDirectory(runtime, ref);
|
|
105
|
+
if (expected !== undefined &&
|
|
106
|
+
!samePrototypeDirectorySnapshot(expected, actual)) {
|
|
107
|
+
throw new PrototypeMetadataFileError();
|
|
108
|
+
}
|
|
109
|
+
if (actual.some((entry) => entry.kind !== "directory")) {
|
|
110
|
+
throw new PrototypeMetadataFileError();
|
|
54
111
|
}
|
|
55
112
|
}
|
|
56
113
|
catch (error) {
|
|
@@ -84,18 +141,6 @@ async function readMetadataDocument(runtime, ref, themeRegistry) {
|
|
|
84
141
|
throw new PrototypeMetadataFileError();
|
|
85
142
|
}
|
|
86
143
|
}
|
|
87
|
-
async function removePrototypeDirectory(runtime, ref) {
|
|
88
|
-
await assertPrototypeDirectory(runtime, ref);
|
|
89
|
-
try {
|
|
90
|
-
await rm(join(runtime.prototypeRoot, ref.login, ref.slug), {
|
|
91
|
-
force: false,
|
|
92
|
-
recursive: true,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
throw new PrototypeMetadataFileError();
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
144
|
async function writeMetadataDocument(runtime, ref, document) {
|
|
100
145
|
const path = metadataPath(runtime, ref);
|
|
101
146
|
const temporaryPath = `${path}.${randomUUID()}.tmp`;
|
|
@@ -193,8 +238,10 @@ export class LocalPrototypeLifecycle {
|
|
|
193
238
|
#deploymentProvider;
|
|
194
239
|
#applicationId;
|
|
195
240
|
#themeRegistry;
|
|
241
|
+
#publishDeploymentSource;
|
|
242
|
+
#installDependencies;
|
|
196
243
|
#restartRequired = false;
|
|
197
|
-
#
|
|
244
|
+
#preparationAttention = new Map();
|
|
198
245
|
constructor(options) {
|
|
199
246
|
const internalOptions = options;
|
|
200
247
|
const runtime = internalOptions.runtime ??
|
|
@@ -216,6 +263,11 @@ export class LocalPrototypeLifecycle {
|
|
|
216
263
|
internalOptions.preparePrototypeFiles ?? (() => undefined);
|
|
217
264
|
this.#deploymentProvider = internalOptions.deploymentProvider;
|
|
218
265
|
this.#applicationId = internalOptions.applicationId;
|
|
266
|
+
this.#publishDeploymentSource =
|
|
267
|
+
internalOptions.publishDeploymentSource === null
|
|
268
|
+
? null
|
|
269
|
+
: (internalOptions.publishDeploymentSource ?? publishDeploymentSource);
|
|
270
|
+
this.#installDependencies = options.installDependencies;
|
|
219
271
|
if (this.#deploymentProvider &&
|
|
220
272
|
(typeof this.#applicationId !== "string" ||
|
|
221
273
|
this.#applicationId.length === 0)) {
|
|
@@ -232,22 +284,27 @@ export class LocalPrototypeLifecycle {
|
|
|
232
284
|
}
|
|
233
285
|
return branch;
|
|
234
286
|
}
|
|
235
|
-
async #
|
|
287
|
+
async #authorizedPrototype(identity, input) {
|
|
236
288
|
const verifiedIdentity = await this.#verifiedIdentity(identity);
|
|
237
289
|
const normalizedInput = parsePrototypeCommandInput(input);
|
|
238
290
|
if (normalizedInput.ref.login !== verifiedIdentity.login) {
|
|
239
291
|
throw new PrototypeOwnershipError();
|
|
240
292
|
}
|
|
241
|
-
const
|
|
242
|
-
if (!summary) {
|
|
243
|
-
throw new PrototypeNotFoundError();
|
|
244
|
-
}
|
|
293
|
+
const directorySnapshot = await capturePrototypeDirectory(this.#runtime, normalizedInput.ref);
|
|
245
294
|
return {
|
|
246
295
|
ref: normalizedInput.ref,
|
|
247
296
|
path: repositoryRelativePath(this.#runtime, normalizedInput.ref),
|
|
248
|
-
|
|
297
|
+
directorySnapshot,
|
|
249
298
|
};
|
|
250
299
|
}
|
|
300
|
+
async #ownedPrototype(identity, input) {
|
|
301
|
+
const authorized = await this.#authorizedPrototype(identity, input);
|
|
302
|
+
const summary = await this.#catalog.getSummary(authorized.ref);
|
|
303
|
+
if (!summary) {
|
|
304
|
+
throw new PrototypeNotFoundError();
|
|
305
|
+
}
|
|
306
|
+
return { ...authorized, summary };
|
|
307
|
+
}
|
|
251
308
|
async #prepareMutationBranch(login) {
|
|
252
309
|
await assertIdentityBranchReady(this.#repository, login);
|
|
253
310
|
return login;
|
|
@@ -282,35 +339,160 @@ export class LocalPrototypeLifecycle {
|
|
|
282
339
|
if (this.#runtime.gitMode === "manual" ||
|
|
283
340
|
this.#runtime.automaticBranchSwitch === "disabled") {
|
|
284
341
|
await this.#currentBranch();
|
|
285
|
-
|
|
342
|
+
const disabled = {
|
|
343
|
+
status: "disabled",
|
|
344
|
+
effects: {
|
|
345
|
+
changed: false,
|
|
346
|
+
pushed: false,
|
|
347
|
+
reconciledPaths: [],
|
|
348
|
+
restartRequired: false,
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
this.#preparationAttention.set(verifiedIdentity.login, disabled);
|
|
352
|
+
return disabled;
|
|
286
353
|
}
|
|
287
354
|
const synchronization = await synchronizeIdentityBranch(this.#repository, {
|
|
288
355
|
identityBranch: verifiedIdentity.login,
|
|
289
356
|
primaryBranch: this.#runtime.primaryBranch,
|
|
290
357
|
remote: this.#runtime.remote,
|
|
291
|
-
|
|
358
|
+
prototypeRoot: relative(this.#runtime.repositoryRoot, this.#runtime.prototypeRoot)
|
|
359
|
+
.split(sep)
|
|
360
|
+
.join("/"),
|
|
292
361
|
});
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
362
|
+
const reconciledPaths = synchronization.status === "synchronized" &&
|
|
363
|
+
synchronization.platformPaths.length === 0
|
|
364
|
+
? synchronization.affectedPaths.filter((path) => this.#runtime.managedRepositoryPaths.includes(path))
|
|
365
|
+
: synchronization.platformPaths;
|
|
366
|
+
const effects = {
|
|
367
|
+
changed: synchronization.changed,
|
|
368
|
+
pushed: synchronization.pushed,
|
|
369
|
+
reconciledPaths,
|
|
370
|
+
restartRequired: synchronization.status === "synchronized" &&
|
|
371
|
+
synchronization.platformPaths.length === 0
|
|
372
|
+
? reconciledPaths.length > 0
|
|
373
|
+
: synchronization.restartRequired,
|
|
374
|
+
...(synchronization.platformPaths.length > 0
|
|
375
|
+
? { platformPaths: synchronization.platformPaths }
|
|
376
|
+
: {}),
|
|
377
|
+
...(synchronization.dependencyPaths.length > 0
|
|
378
|
+
? { dependencyPaths: synchronization.dependencyPaths }
|
|
379
|
+
: {}),
|
|
380
|
+
...(synchronization.preparationRequired
|
|
381
|
+
? { preparationRequired: true }
|
|
382
|
+
: {}),
|
|
383
|
+
};
|
|
384
|
+
if (effects.restartRequired)
|
|
385
|
+
this.#restartRequired = true;
|
|
386
|
+
if (effects.preparationRequired) {
|
|
387
|
+
await writePreparationMarker(this.#repository, this.#runtime.repositoryRoot, effects.dependencyPaths ?? []);
|
|
388
|
+
}
|
|
389
|
+
if (synchronization.status === "deferred") {
|
|
390
|
+
const attention = {
|
|
391
|
+
status: "deferred",
|
|
296
392
|
reason: synchronization.reason,
|
|
393
|
+
effects,
|
|
297
394
|
...(synchronization.affectedPaths.length > 0
|
|
298
395
|
? { affectedPaths: synchronization.affectedPaths }
|
|
299
396
|
: {}),
|
|
300
397
|
};
|
|
398
|
+
this.#preparationAttention.set(verifiedIdentity.login, attention);
|
|
399
|
+
return attention;
|
|
301
400
|
}
|
|
302
|
-
if (synchronization.
|
|
303
|
-
|
|
401
|
+
if (synchronization.status === "unsafe") {
|
|
402
|
+
const attention = {
|
|
403
|
+
status: "unsafe",
|
|
404
|
+
reason: synchronization.reason,
|
|
405
|
+
effects,
|
|
406
|
+
...(synchronization.affectedPaths.length > 0
|
|
407
|
+
? { affectedPaths: synchronization.affectedPaths }
|
|
408
|
+
: {}),
|
|
409
|
+
};
|
|
410
|
+
this.#preparationAttention.set(verifiedIdentity.login, attention);
|
|
411
|
+
return attention;
|
|
304
412
|
}
|
|
305
|
-
|
|
413
|
+
const ready = {
|
|
306
414
|
status: "ready",
|
|
415
|
+
effects,
|
|
307
416
|
...(synchronization.affectedPaths.length > 0
|
|
308
417
|
? { affectedPaths: synchronization.affectedPaths }
|
|
309
418
|
: {}),
|
|
310
419
|
};
|
|
420
|
+
this.#preparationAttention.delete(verifiedIdentity.login);
|
|
421
|
+
return ready;
|
|
311
422
|
});
|
|
312
423
|
});
|
|
313
424
|
}
|
|
425
|
+
async prepareDependencies(dependencyPaths = [], installer) {
|
|
426
|
+
return resultFrom(async () => withRepositoryOperation(this.#runtime.repositoryRealPath, async () => {
|
|
427
|
+
let pending = null;
|
|
428
|
+
try {
|
|
429
|
+
pending = await readPreparationMarker(this.#repository, this.#runtime.repositoryRoot);
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
// A retained/corrupt marker is recovery truth. Restricted modes must
|
|
433
|
+
// preserve it without attempting installation; managed mode must
|
|
434
|
+
// report not-ready rather than guessing what to install.
|
|
435
|
+
if (this.#runtime.gitMode === "manual" ||
|
|
436
|
+
this.#runtime.automaticBranchSwitch === "disabled") {
|
|
437
|
+
return {
|
|
438
|
+
status: "skipped",
|
|
439
|
+
dependencyPaths: [...dependencyPaths],
|
|
440
|
+
pending: true,
|
|
441
|
+
restartRequired: false,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
status: "failed",
|
|
446
|
+
dependencyPaths: [...dependencyPaths],
|
|
447
|
+
pending: true,
|
|
448
|
+
restartRequired: true,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
const paths = [...new Set([...(pending ?? []), ...dependencyPaths])];
|
|
452
|
+
if (this.#runtime.gitMode === "manual" ||
|
|
453
|
+
this.#runtime.automaticBranchSwitch === "disabled") {
|
|
454
|
+
return {
|
|
455
|
+
status: "skipped",
|
|
456
|
+
dependencyPaths: paths,
|
|
457
|
+
pending: pending !== null,
|
|
458
|
+
restartRequired: false,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
// An empty path list is still an obligation when the marker exists.
|
|
462
|
+
// Do not reinterpret a retained marker as "not required".
|
|
463
|
+
if (paths.length === 0 && pending === null) {
|
|
464
|
+
return {
|
|
465
|
+
status: "not-required",
|
|
466
|
+
dependencyPaths: [],
|
|
467
|
+
pending: false,
|
|
468
|
+
restartRequired: false,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
await writePreparationMarker(this.#repository, this.#runtime.repositoryRoot, paths);
|
|
472
|
+
try {
|
|
473
|
+
await (installer ??
|
|
474
|
+
this.#installDependencies ??
|
|
475
|
+
(async (root) => {
|
|
476
|
+
await execFile("pnpm", ["install"], { cwd: root });
|
|
477
|
+
}))(this.#runtime.repositoryRoot);
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
return {
|
|
481
|
+
status: "failed",
|
|
482
|
+
dependencyPaths: paths,
|
|
483
|
+
pending: true,
|
|
484
|
+
restartRequired: true,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
await clearPreparationMarker(this.#repository, this.#runtime.repositoryRoot);
|
|
488
|
+
return {
|
|
489
|
+
status: "prepared",
|
|
490
|
+
dependencyPaths: paths,
|
|
491
|
+
pending: false,
|
|
492
|
+
restartRequired: true,
|
|
493
|
+
};
|
|
494
|
+
}));
|
|
495
|
+
}
|
|
314
496
|
async create(identity, input) {
|
|
315
497
|
return resultFrom(async () => {
|
|
316
498
|
const verifiedIdentity = await this.#verifiedIdentity(identity);
|
|
@@ -350,11 +532,13 @@ export class LocalPrototypeLifecycle {
|
|
|
350
532
|
};
|
|
351
533
|
}
|
|
352
534
|
await commitPath(this.#repository, repositoryRelativePath(this.#runtime, ref), `init: ${ref.slug}`);
|
|
353
|
-
const
|
|
535
|
+
const sourceSha = await this.#headCommit();
|
|
536
|
+
const outcome = await this.#publishOrdinaryNamespace(verifiedIdentity, "create", ref, repositoryRelativePath(this.#runtime, ref), sourceSha);
|
|
354
537
|
return {
|
|
355
538
|
ref,
|
|
356
539
|
url: getPrototypeUrl(ref),
|
|
357
|
-
remoteStatus:
|
|
540
|
+
remoteStatus: "local-only",
|
|
541
|
+
outcome,
|
|
358
542
|
};
|
|
359
543
|
});
|
|
360
544
|
});
|
|
@@ -400,16 +584,34 @@ export class LocalPrototypeLifecycle {
|
|
|
400
584
|
const verifiedIdentity = await this.#verifiedIdentity(identity);
|
|
401
585
|
const normalizedInput = parsePrototypeCommandInput(input);
|
|
402
586
|
return withRepositoryOperation(this.#runtime.repositoryRealPath, async () => {
|
|
403
|
-
const { ref, path } = await this.#
|
|
587
|
+
const { ref, path, directorySnapshot } = await this.#authorizedPrototype(verifiedIdentity, normalizedInput);
|
|
588
|
+
if (directorySnapshot.at(-1)?.kind === "missing") {
|
|
589
|
+
throw new PrototypeNotFoundError();
|
|
590
|
+
}
|
|
404
591
|
const branch = this.#runtime.gitMode === "managed"
|
|
405
592
|
? await this.#prepareMutationBranch(verifiedIdentity.login)
|
|
406
593
|
: undefined;
|
|
407
|
-
await
|
|
594
|
+
await assertPrototypeDirectory(this.#runtime, ref, directorySnapshot);
|
|
595
|
+
await deletePrototypePath(this.#repository, path, branch === undefined ? undefined : `delete: ${ref.slug}`);
|
|
408
596
|
if (branch !== undefined) {
|
|
409
|
-
await
|
|
410
|
-
await
|
|
597
|
+
const sourceSha = await this.#headCommit();
|
|
598
|
+
const outcome = await this.#publishOrdinaryNamespace(verifiedIdentity, "delete", ref, path, sourceSha);
|
|
599
|
+
return {
|
|
600
|
+
deleted: true,
|
|
601
|
+
ref,
|
|
602
|
+
outcome,
|
|
603
|
+
};
|
|
411
604
|
}
|
|
412
|
-
return {
|
|
605
|
+
return {
|
|
606
|
+
deleted: true,
|
|
607
|
+
ref,
|
|
608
|
+
outcome: {
|
|
609
|
+
local: { status: "completed" },
|
|
610
|
+
upload: { status: "not-attempted" },
|
|
611
|
+
preview: { status: "not-attempted" },
|
|
612
|
+
publication: { status: "not-attempted" },
|
|
613
|
+
},
|
|
614
|
+
};
|
|
413
615
|
});
|
|
414
616
|
});
|
|
415
617
|
}
|
|
@@ -428,7 +630,7 @@ export class LocalPrototypeLifecycle {
|
|
|
428
630
|
async getRepositorySummary(identity) {
|
|
429
631
|
return resultFrom(async () => {
|
|
430
632
|
const verifiedIdentity = await this.#verifiedIdentity(identity);
|
|
431
|
-
return getRepositorySummary({
|
|
633
|
+
return withRepositoryOperation(this.#runtime.repositoryRealPath, async () => getRepositorySummary({
|
|
432
634
|
repository: this.#repository,
|
|
433
635
|
catalog: this.#catalog,
|
|
434
636
|
repositoryRoot: this.#runtime.repositoryRoot,
|
|
@@ -438,13 +640,15 @@ export class LocalPrototypeLifecycle {
|
|
|
438
640
|
remote: this.#runtime.remote,
|
|
439
641
|
primaryBranch: this.#runtime.primaryBranch,
|
|
440
642
|
restartRequired: this.#restartRequired,
|
|
441
|
-
|
|
643
|
+
preparationAttention: this.#preparationAttention.get(verifiedIdentity.login),
|
|
644
|
+
preparationPending: (await readPreparationMarker(this.#repository, this.#runtime.repositoryRoot)) !== null,
|
|
645
|
+
}));
|
|
442
646
|
});
|
|
443
647
|
}
|
|
444
648
|
async checkRepositorySummary(identity) {
|
|
445
649
|
return resultFrom(async () => {
|
|
446
650
|
const verifiedIdentity = await this.#verifiedIdentity(identity);
|
|
447
|
-
return getRepositorySummary({
|
|
651
|
+
return withRepositoryOperation(this.#runtime.repositoryRealPath, async () => getRepositorySummary({
|
|
448
652
|
repository: this.#repository,
|
|
449
653
|
catalog: this.#catalog,
|
|
450
654
|
repositoryRoot: this.#runtime.repositoryRoot,
|
|
@@ -454,8 +658,10 @@ export class LocalPrototypeLifecycle {
|
|
|
454
658
|
remote: this.#runtime.remote,
|
|
455
659
|
primaryBranch: this.#runtime.primaryBranch,
|
|
456
660
|
restartRequired: this.#restartRequired,
|
|
661
|
+
preparationAttention: this.#preparationAttention.get(verifiedIdentity.login),
|
|
662
|
+
preparationPending: (await readPreparationMarker(this.#repository, this.#runtime.repositoryRoot)) !== null,
|
|
457
663
|
refresh: true,
|
|
458
|
-
});
|
|
664
|
+
}));
|
|
459
665
|
});
|
|
460
666
|
}
|
|
461
667
|
async #headCommit() {
|
|
@@ -465,25 +671,6 @@ export class LocalPrototypeLifecycle {
|
|
|
465
671
|
}
|
|
466
672
|
return commit.toLowerCase();
|
|
467
673
|
}
|
|
468
|
-
async #prepareDeploymentBranch(login) {
|
|
469
|
-
if (this.#runtime.automaticBranchSwitch === "disabled") {
|
|
470
|
-
await assertIdentityBranchReady(this.#repository, login);
|
|
471
|
-
return "updates-not-synchronized";
|
|
472
|
-
}
|
|
473
|
-
const synchronization = await synchronizeIdentityBranch(this.#repository, {
|
|
474
|
-
identityBranch: login,
|
|
475
|
-
primaryBranch: this.#runtime.primaryBranch,
|
|
476
|
-
remote: this.#runtime.remote,
|
|
477
|
-
managedPaths: this.#runtime.managedRepositoryPaths,
|
|
478
|
-
});
|
|
479
|
-
if (synchronization.status === "unsafe") {
|
|
480
|
-
throw new GitRepositoryError("conflict", "deployment-preparation");
|
|
481
|
-
}
|
|
482
|
-
await assertIdentityBranchReady(this.#repository, login);
|
|
483
|
-
return synchronization.status === "deferred"
|
|
484
|
-
? "updates-not-synchronized"
|
|
485
|
-
: undefined;
|
|
486
|
-
}
|
|
487
674
|
#workspaceRuntime(workspacePath, workspaceRepository) {
|
|
488
675
|
return {
|
|
489
676
|
...this.#runtime,
|
|
@@ -493,11 +680,100 @@ export class LocalPrototypeLifecycle {
|
|
|
493
680
|
gitRepository: workspaceRepository,
|
|
494
681
|
};
|
|
495
682
|
}
|
|
683
|
+
#pendingKey(login) {
|
|
684
|
+
return pendingPublicationKey(this.#runtime.repositoryRoot, login);
|
|
685
|
+
}
|
|
686
|
+
#rememberPending(pending) {
|
|
687
|
+
pendingPublications.set(this.#pendingKey(pending.identity.login), pending);
|
|
688
|
+
}
|
|
689
|
+
#clearPending(login) {
|
|
690
|
+
pendingPublications.delete(this.#pendingKey(login));
|
|
691
|
+
}
|
|
692
|
+
#recoveryValue(pending, outcome) {
|
|
693
|
+
return {
|
|
694
|
+
recoveryId: pending.recoveryId,
|
|
695
|
+
operation: pending.operation,
|
|
696
|
+
kind: pending.kind,
|
|
697
|
+
ref: pending.ref,
|
|
698
|
+
sourceSha: pending.sourceSha,
|
|
699
|
+
...(pending.previewUrl === undefined
|
|
700
|
+
? {}
|
|
701
|
+
: { previewUrl: pending.previewUrl }),
|
|
702
|
+
...(pending.deploymentId === undefined
|
|
703
|
+
? {}
|
|
704
|
+
: { deploymentId: pending.deploymentId }),
|
|
705
|
+
...(pending.readyState === undefined
|
|
706
|
+
? {}
|
|
707
|
+
: { readyState: pending.readyState }),
|
|
708
|
+
...(pending.acceptedAt === undefined
|
|
709
|
+
? {}
|
|
710
|
+
: { acceptedAt: pending.acceptedAt }),
|
|
711
|
+
outcome,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
async #publishCommittedNamespace(identity, sourceSha) {
|
|
715
|
+
if (this.#publishDeploymentSource === null) {
|
|
716
|
+
throw new DeploymentUnavailableError();
|
|
717
|
+
}
|
|
718
|
+
return this.#publishDeploymentSource(this.#repository, {
|
|
719
|
+
remote: this.#runtime.remote,
|
|
720
|
+
primaryBranch: this.#runtime.primaryBranch,
|
|
721
|
+
sourceSha,
|
|
722
|
+
namespacePath: namespaceRelativePath(this.#runtime, identity.login),
|
|
723
|
+
commitIdentity: this.#runtime.commitIdentity,
|
|
724
|
+
}, async () => undefined);
|
|
725
|
+
}
|
|
726
|
+
async #publishOrdinaryNamespace(identity, operation, ref, path, sourceSha) {
|
|
727
|
+
const baseOutcome = {
|
|
728
|
+
local: { status: "completed", checkpointSha: sourceSha },
|
|
729
|
+
upload: { status: "not-attempted", sourceSha },
|
|
730
|
+
preview: { status: "not-attempted" },
|
|
731
|
+
publication: {
|
|
732
|
+
status: this.#publishDeploymentSource === null ? "not-attempted" : "pending",
|
|
733
|
+
},
|
|
734
|
+
};
|
|
735
|
+
if (this.#publishDeploymentSource === null)
|
|
736
|
+
return baseOutcome;
|
|
737
|
+
try {
|
|
738
|
+
const publication = await this.#publishCommittedNamespace(identity, sourceSha);
|
|
739
|
+
this.#clearPending(identity.login);
|
|
740
|
+
return {
|
|
741
|
+
...baseOutcome,
|
|
742
|
+
publication: {
|
|
743
|
+
status: "confirmed",
|
|
744
|
+
revision: publication.publicationSha,
|
|
745
|
+
},
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
catch {
|
|
749
|
+
const pending = {
|
|
750
|
+
recoveryId: randomUUID(),
|
|
751
|
+
operation,
|
|
752
|
+
kind: "ordinary-publication",
|
|
753
|
+
identity,
|
|
754
|
+
ref,
|
|
755
|
+
path,
|
|
756
|
+
sourceSha,
|
|
757
|
+
};
|
|
758
|
+
this.#rememberPending(pending);
|
|
759
|
+
return {
|
|
760
|
+
...baseOutcome,
|
|
761
|
+
publication: {
|
|
762
|
+
status: "failed",
|
|
763
|
+
recoveryId: pending.recoveryId,
|
|
764
|
+
},
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
}
|
|
496
768
|
async #publishDeploymentMetadata(pending) {
|
|
497
|
-
|
|
769
|
+
if (this.#publishDeploymentSource === null) {
|
|
770
|
+
throw new DeploymentUnavailableError();
|
|
771
|
+
}
|
|
772
|
+
return this.#publishDeploymentSource(this.#repository, {
|
|
498
773
|
remote: this.#runtime.remote,
|
|
499
774
|
primaryBranch: this.#runtime.primaryBranch,
|
|
500
775
|
sourceSha: pending.sourceSha,
|
|
776
|
+
namespacePath: namespaceRelativePath(this.#runtime, pending.identity.login),
|
|
501
777
|
commitIdentity: this.#runtime.commitIdentity,
|
|
502
778
|
}, async ({ path, repository }) => {
|
|
503
779
|
const workspaceRuntime = this.#workspaceRuntime(path, repository);
|
|
@@ -505,18 +781,13 @@ export class LocalPrototypeLifecycle {
|
|
|
505
781
|
await diagnoseMetadataStep("metadata-write", () => writeMetadataDocument(workspaceRuntime, pending.ref, {
|
|
506
782
|
...mergedDocument,
|
|
507
783
|
previewUrl: pending.previewUrl,
|
|
508
|
-
updatedAt:
|
|
784
|
+
updatedAt: pending.acceptedAt,
|
|
509
785
|
}));
|
|
510
786
|
await diagnoseMetadataStep("metadata-commit", () => commitPath(repository, pending.path, `deploy: ${pending.ref.slug} (metadata)`));
|
|
511
787
|
});
|
|
512
788
|
}
|
|
513
|
-
async #deploymentValue(pending, publicationWarnings = []) {
|
|
514
|
-
const warnings = [
|
|
515
|
-
...(pending.synchronizationWarning === undefined
|
|
516
|
-
? []
|
|
517
|
-
: [pending.synchronizationWarning]),
|
|
518
|
-
...publicationWarnings,
|
|
519
|
-
];
|
|
789
|
+
async #deploymentValue(pending, publicationWarnings = [], outcome) {
|
|
790
|
+
const warnings = [...publicationWarnings];
|
|
520
791
|
return {
|
|
521
792
|
deployed: true,
|
|
522
793
|
ref: pending.ref,
|
|
@@ -524,7 +795,8 @@ export class LocalPrototypeLifecycle {
|
|
|
524
795
|
deploymentId: pending.deploymentId,
|
|
525
796
|
sourceSha: pending.sourceSha,
|
|
526
797
|
readyState: pending.readyState,
|
|
527
|
-
|
|
798
|
+
...(outcome === undefined ? {} : { outcome }),
|
|
799
|
+
warning: publicationWarnings[0],
|
|
528
800
|
...(warnings.length === 0 ? {} : { warnings }),
|
|
529
801
|
};
|
|
530
802
|
}
|
|
@@ -540,7 +812,7 @@ export class LocalPrototypeLifecycle {
|
|
|
540
812
|
}
|
|
541
813
|
return withRepositoryOperation(this.#runtime.repositoryRealPath, async () => {
|
|
542
814
|
const { ref, path } = await this.#ownedPrototype(verifiedIdentity, normalizedInput);
|
|
543
|
-
|
|
815
|
+
await assertIdentityBranchReady(this.#repository, verifiedIdentity.login);
|
|
544
816
|
await assertPrototypeDirectory(this.#runtime, ref);
|
|
545
817
|
const document = await readMetadataDocument(this.#runtime, ref, this.#themeRegistry);
|
|
546
818
|
const metadata = parsePrototypeMetadata(document, {
|
|
@@ -549,94 +821,344 @@ export class LocalPrototypeLifecycle {
|
|
|
549
821
|
if (metadata.authorLogin !== ref.login) {
|
|
550
822
|
throw new PrototypeNotFoundError();
|
|
551
823
|
}
|
|
552
|
-
await
|
|
824
|
+
await commitPrototypePath(this.#repository, path, `deploy: ${ref.slug}`);
|
|
553
825
|
const sourceSha = await this.#headCommit();
|
|
554
|
-
|
|
826
|
+
let sourcePush;
|
|
827
|
+
try {
|
|
828
|
+
sourcePush = await pushCommitToBranch(this.#repository, this.#runtime.remote, sourceSha, verifiedIdentity.login, true);
|
|
829
|
+
}
|
|
830
|
+
catch (error) {
|
|
831
|
+
if (error instanceof GitRepositoryError) {
|
|
832
|
+
const pending = {
|
|
833
|
+
recoveryId: randomUUID(),
|
|
834
|
+
operation: "deploy",
|
|
835
|
+
kind: "deployment-upload",
|
|
836
|
+
identity: verifiedIdentity,
|
|
837
|
+
ref,
|
|
838
|
+
path,
|
|
839
|
+
sourceSha,
|
|
840
|
+
};
|
|
841
|
+
this.#rememberPending(pending);
|
|
842
|
+
return {
|
|
843
|
+
deployed: false,
|
|
844
|
+
ref,
|
|
845
|
+
sourceSha,
|
|
846
|
+
warning: "upload-unconfirmed",
|
|
847
|
+
outcome: {
|
|
848
|
+
local: { status: "completed", checkpointSha: sourceSha },
|
|
849
|
+
upload: {
|
|
850
|
+
status: "unconfirmed",
|
|
851
|
+
sourceSha,
|
|
852
|
+
},
|
|
853
|
+
preview: { status: "not-attempted" },
|
|
854
|
+
publication: {
|
|
855
|
+
status: "pending",
|
|
856
|
+
recoveryId: pending.recoveryId,
|
|
857
|
+
},
|
|
858
|
+
},
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
throw error;
|
|
862
|
+
}
|
|
555
863
|
if (sourcePush.remoteStatus !== "pushed") {
|
|
556
864
|
throw new DeploymentSourcePushError();
|
|
557
865
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
866
|
+
let deployment;
|
|
867
|
+
try {
|
|
868
|
+
deployment = await this.#deploymentProvider.deploy({
|
|
869
|
+
applicationId: this.#applicationId,
|
|
870
|
+
login: verifiedIdentity.login,
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
catch (error) {
|
|
874
|
+
if (error instanceof DeploymentProviderError &&
|
|
875
|
+
[
|
|
876
|
+
"timeout",
|
|
877
|
+
"transport-failure",
|
|
878
|
+
"invalid-response",
|
|
879
|
+
"invalid-url",
|
|
880
|
+
].includes(error.kind)) {
|
|
881
|
+
const pending = {
|
|
882
|
+
recoveryId: randomUUID(),
|
|
883
|
+
operation: "deploy",
|
|
884
|
+
kind: "deployment-preview-unconfirmed",
|
|
885
|
+
identity: verifiedIdentity,
|
|
886
|
+
ref,
|
|
887
|
+
path,
|
|
888
|
+
sourceSha,
|
|
889
|
+
};
|
|
890
|
+
this.#rememberPending(pending);
|
|
891
|
+
return {
|
|
892
|
+
deployed: false,
|
|
893
|
+
ref,
|
|
894
|
+
sourceSha,
|
|
895
|
+
warning: "preview-unconfirmed",
|
|
896
|
+
outcome: {
|
|
897
|
+
local: { status: "completed", checkpointSha: sourceSha },
|
|
898
|
+
upload: { status: "confirmed", sourceSha },
|
|
899
|
+
preview: { status: "unconfirmed" },
|
|
900
|
+
publication: { status: "not-attempted" },
|
|
901
|
+
},
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
throw error;
|
|
905
|
+
}
|
|
568
906
|
if (deployment.commitSha.toLowerCase() !== sourceSha ||
|
|
569
907
|
!["READY", "QUEUED", "INITIALIZING", "BUILDING"].includes(deployment.readyState)) {
|
|
570
908
|
throw new DeploymentNotAcceptedError();
|
|
571
909
|
}
|
|
572
910
|
const previewUrl = new URL(getPrototypeUrl(ref).slice(1), `${deployment.url.replace(/\/+$/, "")}/`).toString();
|
|
573
911
|
const pending = {
|
|
912
|
+
recoveryId: randomUUID(),
|
|
913
|
+
operation: "deploy",
|
|
914
|
+
kind: "deployment-publication",
|
|
574
915
|
identity: verifiedIdentity,
|
|
575
|
-
input: normalizedInput,
|
|
576
916
|
ref,
|
|
577
917
|
path,
|
|
578
918
|
previewUrl,
|
|
579
919
|
deploymentId: deployment.deploymentId,
|
|
580
920
|
sourceSha,
|
|
921
|
+
acceptedAt: new Date().toISOString(),
|
|
581
922
|
readyState: deployment.readyState,
|
|
582
|
-
|
|
923
|
+
};
|
|
924
|
+
const acceptedOutcome = {
|
|
925
|
+
local: { status: "completed", checkpointSha: sourceSha },
|
|
926
|
+
upload: { status: "confirmed", sourceSha },
|
|
927
|
+
preview: {
|
|
928
|
+
status: "accepted",
|
|
929
|
+
previewUrl,
|
|
930
|
+
deploymentId: deployment.deploymentId,
|
|
931
|
+
},
|
|
932
|
+
publication: { status: "pending" },
|
|
583
933
|
};
|
|
584
934
|
try {
|
|
585
935
|
const publication = await this.#publishDeploymentMetadata(pending);
|
|
586
|
-
this.#
|
|
587
|
-
return this.#deploymentValue(pending, publication.cleanupFailed ? ["publication-cleanup-failed"] : []
|
|
936
|
+
this.#clearPending(verifiedIdentity.login);
|
|
937
|
+
return this.#deploymentValue(pending, publication.cleanupFailed ? ["publication-cleanup-failed"] : [], {
|
|
938
|
+
...acceptedOutcome,
|
|
939
|
+
publication: {
|
|
940
|
+
status: "confirmed",
|
|
941
|
+
revision: publication.publicationSha,
|
|
942
|
+
},
|
|
943
|
+
});
|
|
588
944
|
}
|
|
589
945
|
catch (error) {
|
|
590
946
|
// Keep the accepted deployment retryable for every publication
|
|
591
947
|
// failure, including metadata preparation failures.
|
|
592
|
-
this.#
|
|
948
|
+
this.#rememberPending(pending);
|
|
593
949
|
return this.#deploymentValue(pending, [
|
|
594
950
|
error instanceof DeploymentWorktreeError &&
|
|
595
|
-
error.kind !== "cleanup-failure"
|
|
951
|
+
error.kind !== "cleanup-failure" &&
|
|
952
|
+
error.kind !== "metadata-failure"
|
|
596
953
|
? "primary-merge-failed"
|
|
597
954
|
: "metadata-persistence-failed",
|
|
598
955
|
...(error instanceof DeploymentWorktreeError &&
|
|
599
956
|
error.cleanupFailed
|
|
600
957
|
? ["publication-cleanup-failed"]
|
|
601
958
|
: []),
|
|
602
|
-
]
|
|
959
|
+
], {
|
|
960
|
+
...acceptedOutcome,
|
|
961
|
+
publication: {
|
|
962
|
+
status: "failed",
|
|
963
|
+
recoveryId: pending.recoveryId,
|
|
964
|
+
},
|
|
965
|
+
});
|
|
603
966
|
}
|
|
604
967
|
});
|
|
605
968
|
});
|
|
606
969
|
}
|
|
607
|
-
async
|
|
970
|
+
async getPendingRecovery(identity) {
|
|
971
|
+
return resultFrom(async () => {
|
|
972
|
+
const verifiedIdentity = await this.#verifiedIdentity(identity);
|
|
973
|
+
const pending = pendingPublications.get(this.#pendingKey(verifiedIdentity.login));
|
|
974
|
+
if (pending === undefined)
|
|
975
|
+
return null;
|
|
976
|
+
return this.#recoveryValue(pending, {
|
|
977
|
+
local: { status: "completed", checkpointSha: pending.sourceSha },
|
|
978
|
+
upload: {
|
|
979
|
+
status: pending.kind === "deployment-upload" ? "unconfirmed" : "confirmed",
|
|
980
|
+
sourceSha: pending.sourceSha,
|
|
981
|
+
},
|
|
982
|
+
preview: pending.kind === "deployment-publication"
|
|
983
|
+
? {
|
|
984
|
+
status: "accepted",
|
|
985
|
+
previewUrl: pending.previewUrl,
|
|
986
|
+
deploymentId: pending.deploymentId,
|
|
987
|
+
}
|
|
988
|
+
: pending.kind === "deployment-preview-unconfirmed"
|
|
989
|
+
? { status: "unconfirmed" }
|
|
990
|
+
: { status: "not-attempted" },
|
|
991
|
+
publication: {
|
|
992
|
+
status: pending.kind === "deployment-upload"
|
|
993
|
+
? "pending"
|
|
994
|
+
: pending.kind === "deployment-preview-unconfirmed"
|
|
995
|
+
? "not-attempted"
|
|
996
|
+
: "failed",
|
|
997
|
+
...(pending.kind === "deployment-preview-unconfirmed"
|
|
998
|
+
? {}
|
|
999
|
+
: { recoveryId: pending.recoveryId }),
|
|
1000
|
+
},
|
|
1001
|
+
});
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
async retryPendingPublication(identity, recoveryId) {
|
|
608
1005
|
if (this.#runtime.gitMode === "manual") {
|
|
609
1006
|
return lifecycleFailure("forbidden");
|
|
610
1007
|
}
|
|
611
1008
|
return resultFrom(async () => {
|
|
612
1009
|
const verifiedIdentity = await this.#verifiedIdentity(identity);
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
if (pending === undefined ||
|
|
616
|
-
pending.identity.login !== verifiedIdentity.login ||
|
|
617
|
-
pending.ref.login !== normalizedInput.ref.login ||
|
|
618
|
-
pending.ref.slug !== normalizedInput.ref.slug) {
|
|
1010
|
+
const pending = pendingPublications.get(this.#pendingKey(verifiedIdentity.login));
|
|
1011
|
+
if (pending === undefined || pending.recoveryId !== recoveryId) {
|
|
619
1012
|
throw new DeploymentUnavailableError();
|
|
620
1013
|
}
|
|
621
1014
|
return withRepositoryOperation(this.#runtime.repositoryRealPath, async () => {
|
|
622
1015
|
await assertIdentityBranchReady(this.#repository, verifiedIdentity.login);
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
1016
|
+
if (pending.kind === "deployment-preview-unconfirmed") {
|
|
1017
|
+
throw new DeploymentUnavailableError();
|
|
1018
|
+
}
|
|
1019
|
+
let deploymentPending = pending;
|
|
1020
|
+
if (pending.kind === "ordinary-publication") {
|
|
1021
|
+
deploymentPending = {
|
|
1022
|
+
...pending,
|
|
1023
|
+
sourceSha: await this.#headCommit(),
|
|
1024
|
+
};
|
|
1025
|
+
this.#rememberPending(deploymentPending);
|
|
1026
|
+
}
|
|
1027
|
+
if (pending.kind === "deployment-upload") {
|
|
1028
|
+
const remoteSource = await remoteBranchCommit(this.#repository, this.#runtime.remote, verifiedIdentity.login);
|
|
1029
|
+
if (remoteSource !== pending.sourceSha) {
|
|
1030
|
+
const upload = await pushCommitToBranch(this.#repository, this.#runtime.remote, pending.sourceSha, verifiedIdentity.login, true);
|
|
1031
|
+
if (upload.remoteStatus !== "pushed") {
|
|
1032
|
+
throw new DeploymentSourcePushError();
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
if (!this.#deploymentProvider) {
|
|
1036
|
+
throw new DeploymentUnavailableError();
|
|
1037
|
+
}
|
|
1038
|
+
let deployment;
|
|
1039
|
+
try {
|
|
1040
|
+
deployment = await this.#deploymentProvider.deploy({
|
|
1041
|
+
applicationId: this.#applicationId,
|
|
1042
|
+
login: verifiedIdentity.login,
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
catch (error) {
|
|
1046
|
+
if (error instanceof DeploymentProviderError &&
|
|
1047
|
+
[
|
|
1048
|
+
"timeout",
|
|
1049
|
+
"transport-failure",
|
|
1050
|
+
"invalid-response",
|
|
1051
|
+
"invalid-url",
|
|
1052
|
+
].includes(error.kind)) {
|
|
1053
|
+
const uncertain = {
|
|
1054
|
+
...pending,
|
|
1055
|
+
kind: "deployment-preview-unconfirmed",
|
|
1056
|
+
};
|
|
1057
|
+
this.#rememberPending(uncertain);
|
|
1058
|
+
return this.#recoveryValue(uncertain, {
|
|
1059
|
+
local: {
|
|
1060
|
+
status: "completed",
|
|
1061
|
+
checkpointSha: pending.sourceSha,
|
|
1062
|
+
},
|
|
1063
|
+
upload: {
|
|
1064
|
+
status: "confirmed",
|
|
1065
|
+
sourceSha: pending.sourceSha,
|
|
1066
|
+
},
|
|
1067
|
+
preview: { status: "unconfirmed" },
|
|
1068
|
+
publication: { status: "not-attempted" },
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
throw error;
|
|
1072
|
+
}
|
|
1073
|
+
if (deployment.commitSha.toLowerCase() !== pending.sourceSha ||
|
|
1074
|
+
!["READY", "QUEUED", "INITIALIZING", "BUILDING"].includes(deployment.readyState)) {
|
|
1075
|
+
throw new DeploymentNotAcceptedError();
|
|
1076
|
+
}
|
|
1077
|
+
deploymentPending = {
|
|
1078
|
+
...pending,
|
|
1079
|
+
kind: "deployment-publication",
|
|
1080
|
+
previewUrl: new URL(getPrototypeUrl(pending.ref).slice(1), `${deployment.url.replace(/\/+$/, "")}/`).toString(),
|
|
1081
|
+
deploymentId: deployment.deploymentId,
|
|
1082
|
+
acceptedAt: new Date().toISOString(),
|
|
1083
|
+
readyState: deployment.readyState,
|
|
1084
|
+
};
|
|
1085
|
+
this.#rememberPending(deploymentPending);
|
|
1086
|
+
}
|
|
1087
|
+
const publication = deploymentPending.kind === "deployment-publication"
|
|
1088
|
+
? await this.#publishDeploymentMetadata(deploymentPending)
|
|
1089
|
+
: await this.#publishCommittedNamespace(verifiedIdentity, deploymentPending.sourceSha);
|
|
1090
|
+
this.#clearPending(verifiedIdentity.login);
|
|
1091
|
+
return this.#recoveryValue(deploymentPending, {
|
|
1092
|
+
local: {
|
|
1093
|
+
status: "completed",
|
|
1094
|
+
checkpointSha: deploymentPending.sourceSha,
|
|
1095
|
+
},
|
|
1096
|
+
upload: {
|
|
1097
|
+
status: "confirmed",
|
|
1098
|
+
sourceSha: deploymentPending.sourceSha,
|
|
1099
|
+
},
|
|
1100
|
+
preview: deploymentPending.kind === "deployment-publication"
|
|
1101
|
+
? {
|
|
1102
|
+
status: "accepted",
|
|
1103
|
+
previewUrl: deploymentPending.previewUrl,
|
|
1104
|
+
deploymentId: deploymentPending.deploymentId,
|
|
1105
|
+
}
|
|
1106
|
+
: { status: "not-attempted" },
|
|
1107
|
+
publication: {
|
|
1108
|
+
status: "confirmed",
|
|
1109
|
+
revision: publication.publicationSha,
|
|
1110
|
+
},
|
|
1111
|
+
});
|
|
626
1112
|
});
|
|
627
1113
|
});
|
|
628
1114
|
}
|
|
1115
|
+
/** Compatibility wrapper; recovery-ID retry is the implementation. */
|
|
1116
|
+
async retryPublish(identity, input) {
|
|
1117
|
+
if (this.#runtime.gitMode === "manual") {
|
|
1118
|
+
return lifecycleFailure("forbidden");
|
|
1119
|
+
}
|
|
1120
|
+
let normalizedInput;
|
|
1121
|
+
try {
|
|
1122
|
+
normalizedInput = parsePrototypeCommandInput(input);
|
|
1123
|
+
}
|
|
1124
|
+
catch {
|
|
1125
|
+
return lifecycleFailure("invalid-input");
|
|
1126
|
+
}
|
|
1127
|
+
const pending = pendingPublications.get(this.#pendingKey(identity.login));
|
|
1128
|
+
if (pending === undefined ||
|
|
1129
|
+
pending.kind !== "deployment-publication" ||
|
|
1130
|
+
pending.ref.login !== normalizedInput.ref.login ||
|
|
1131
|
+
pending.ref.slug !== normalizedInput.ref.slug) {
|
|
1132
|
+
return lifecycleFailure("deployment-unavailable");
|
|
1133
|
+
}
|
|
1134
|
+
const result = await this.retryPendingPublication(identity, pending.recoveryId);
|
|
1135
|
+
if (!result.ok)
|
|
1136
|
+
return result;
|
|
1137
|
+
const current = pendingPublications.get(this.#pendingKey(identity.login));
|
|
1138
|
+
const completed = {
|
|
1139
|
+
...pending,
|
|
1140
|
+
...(current ?? {}),
|
|
1141
|
+
sourceSha: result.value.sourceSha,
|
|
1142
|
+
...(result.value.previewUrl === undefined
|
|
1143
|
+
? {}
|
|
1144
|
+
: { previewUrl: result.value.previewUrl }),
|
|
1145
|
+
...(result.value.deploymentId === undefined
|
|
1146
|
+
? {}
|
|
1147
|
+
: { deploymentId: result.value.deploymentId }),
|
|
1148
|
+
};
|
|
1149
|
+
return lifecycleSuccess(await this.#deploymentValue(completed, [], result.value.outcome));
|
|
1150
|
+
}
|
|
629
1151
|
async save(identity, input) {
|
|
630
1152
|
if (this.#runtime.gitMode === "manual") {
|
|
631
1153
|
return lifecycleFailure("forbidden");
|
|
632
1154
|
}
|
|
633
1155
|
return resultFrom(async () => {
|
|
634
1156
|
const verifiedIdentity = await this.#verifiedIdentity(identity);
|
|
635
|
-
const { ref, path } = await this.#ownedPrototype(verifiedIdentity, input);
|
|
636
1157
|
return withRepositoryOperation(this.#runtime.repositoryRealPath, async () => {
|
|
637
1158
|
await this.#prepareMutationBranch(verifiedIdentity.login);
|
|
638
|
-
|
|
639
|
-
await
|
|
1159
|
+
const { ref, path, directorySnapshot } = await this.#authorizedPrototype(verifiedIdentity, input);
|
|
1160
|
+
await assertPrototypeDirectory(this.#runtime, ref, directorySnapshot);
|
|
1161
|
+
await commitPrototypePath(this.#repository, path, `save: ${ref.slug}`);
|
|
640
1162
|
return { saved: true };
|
|
641
1163
|
});
|
|
642
1164
|
});
|
|
@@ -647,9 +1169,11 @@ export class LocalPrototypeLifecycle {
|
|
|
647
1169
|
}
|
|
648
1170
|
return resultFrom(async () => {
|
|
649
1171
|
const verifiedIdentity = await this.#verifiedIdentity(identity);
|
|
650
|
-
const { path } = await this.#ownedPrototype(verifiedIdentity, input);
|
|
651
1172
|
return withRepositoryOperation(this.#runtime.repositoryRealPath, async () => {
|
|
652
|
-
await
|
|
1173
|
+
await this.#prepareMutationBranch(verifiedIdentity.login);
|
|
1174
|
+
const { ref, path, directorySnapshot } = await this.#authorizedPrototype(verifiedIdentity, input);
|
|
1175
|
+
await assertPrototypeDirectory(this.#runtime, ref, directorySnapshot);
|
|
1176
|
+
await discardPrototypePath(this.#repository, path);
|
|
653
1177
|
return { discarded: true };
|
|
654
1178
|
});
|
|
655
1179
|
});
|