@ebay-dt/bonfire-node 0.2.0-alpha.0 → 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 +2 -1
- package/dist/deployment-provider.d.ts.map +1 -1
- package/dist/deployment-provider.js +2 -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 +16 -1
- package/dist/git-repository.d.ts.map +1 -1
- package/dist/git-repository.js +127 -1
- package/dist/git-repository.js.map +1 -1
- package/dist/github-cli.d.ts.map +1 -1
- package/dist/github-cli.js +3 -1
- package/dist/github-cli.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 +36 -0
- package/dist/lifecycle/deployment-worktree.d.ts.map +1 -0
- package/dist/lifecycle/deployment-worktree.js +503 -0
- package/dist/lifecycle/deployment-worktree.js.map +1 -0
- package/dist/lifecycle/local-prototype-lifecycle.d.ts +15 -1
- package/dist/lifecycle/local-prototype-lifecycle.d.ts.map +1 -1
- package/dist/lifecycle/local-prototype-lifecycle.js +738 -98
- 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 +7 -1
- package/dist/lifecycle/repository-summary.d.ts.map +1 -1
- package/dist/lifecycle/repository-summary.js +20 -4
- package/dist/lifecycle/repository-summary.js.map +1 -1
- package/dist/lifecycle/repository-synchronization.d.ts +13 -8
- package/dist/lifecycle/repository-synchronization.d.ts.map +1 -1
- package/dist/lifecycle/repository-synchronization.js +680 -139
- package/dist/lifecycle/repository-synchronization.js.map +1 -1
- package/dist/lifecycle/runtime-context.d.ts +2 -0
- package/dist/lifecycle/runtime-context.d.ts.map +1 -1
- package/dist/lifecycle/runtime-context.js +5 -1
- package/dist/lifecycle/runtime-context.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,10 +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";
|
|
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);
|
|
16
22
|
function repositoryRelativePath(runtime, ref) {
|
|
17
23
|
const absolutePath = join(runtime.prototypeRoot, ref.login, ref.slug);
|
|
18
24
|
return relative(runtime.repositoryRoot, absolutePath).split(sep).join("/");
|
|
19
25
|
}
|
|
26
|
+
function namespaceRelativePath(runtime, login) {
|
|
27
|
+
return relative(runtime.repositoryRoot, join(runtime.prototypeRoot, login))
|
|
28
|
+
.split(sep)
|
|
29
|
+
.join("/");
|
|
30
|
+
}
|
|
20
31
|
function destinationRef(identity, title) {
|
|
21
32
|
const slug = slugifyPrototypeTitle(title);
|
|
22
33
|
if (slug === null)
|
|
@@ -31,21 +42,72 @@ class PrototypeMetadataFileError extends Error {
|
|
|
31
42
|
}
|
|
32
43
|
class DeploymentUnavailableError extends Error {
|
|
33
44
|
}
|
|
45
|
+
class DeploymentSourcePushError extends Error {
|
|
46
|
+
}
|
|
47
|
+
class DeploymentNotAcceptedError extends Error {
|
|
48
|
+
}
|
|
49
|
+
const pendingPublications = new Map();
|
|
50
|
+
function pendingPublicationKey(repositoryRoot, login) {
|
|
51
|
+
return `${repositoryRoot}\0${login}`;
|
|
52
|
+
}
|
|
34
53
|
function metadataPath(runtime, ref) {
|
|
35
54
|
return join(runtime.prototypeRoot, ref.login, ref.slug, "metadata.json");
|
|
36
55
|
}
|
|
37
|
-
|
|
38
|
-
|
|
56
|
+
function prototypeDirectoryPaths(runtime, ref) {
|
|
57
|
+
return [
|
|
39
58
|
runtime.prototypeRoot,
|
|
40
59
|
join(runtime.prototypeRoot, ref.login),
|
|
41
60
|
join(runtime.prototypeRoot, ref.login, ref.slug),
|
|
42
61
|
];
|
|
43
|
-
|
|
44
|
-
|
|
62
|
+
}
|
|
63
|
+
async function capturePrototypeDirectory(runtime, ref) {
|
|
64
|
+
const entries = [];
|
|
65
|
+
for (const path of prototypeDirectoryPaths(runtime, ref)) {
|
|
66
|
+
try {
|
|
45
67
|
const stats = await lstat(path);
|
|
46
|
-
|
|
47
|
-
|
|
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;
|
|
48
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();
|
|
49
111
|
}
|
|
50
112
|
}
|
|
51
113
|
catch (error) {
|
|
@@ -79,18 +141,6 @@ async function readMetadataDocument(runtime, ref, themeRegistry) {
|
|
|
79
141
|
throw new PrototypeMetadataFileError();
|
|
80
142
|
}
|
|
81
143
|
}
|
|
82
|
-
async function removePrototypeDirectory(runtime, ref) {
|
|
83
|
-
await assertPrototypeDirectory(runtime, ref);
|
|
84
|
-
try {
|
|
85
|
-
await rm(join(runtime.prototypeRoot, ref.login, ref.slug), {
|
|
86
|
-
force: false,
|
|
87
|
-
recursive: true,
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
throw new PrototypeMetadataFileError();
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
144
|
async function writeMetadataDocument(runtime, ref, document) {
|
|
95
145
|
const path = metadataPath(runtime, ref);
|
|
96
146
|
const temporaryPath = `${path}.${randomUUID()}.tmp`;
|
|
@@ -114,26 +164,6 @@ async function writeMetadataDocument(runtime, ref, document) {
|
|
|
114
164
|
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
115
165
|
}
|
|
116
166
|
}
|
|
117
|
-
async function mergeIdentityBranchToPrimary(repository, options) {
|
|
118
|
-
if (options.branch === options.primaryBranch)
|
|
119
|
-
return;
|
|
120
|
-
const hasRemote = (await repository.remotes()).some((remote) => remote.name === options.remote);
|
|
121
|
-
if (!hasRemote)
|
|
122
|
-
return;
|
|
123
|
-
await repository.checkout([options.primaryBranch]);
|
|
124
|
-
try {
|
|
125
|
-
await repository.merge([
|
|
126
|
-
options.branch,
|
|
127
|
-
"--no-ff",
|
|
128
|
-
"-m",
|
|
129
|
-
`merge: ${options.branch}`,
|
|
130
|
-
]);
|
|
131
|
-
await repository.push(options.remote, options.primaryBranch);
|
|
132
|
-
}
|
|
133
|
-
finally {
|
|
134
|
-
await repository.checkout([options.branch]);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
167
|
function mapError(error) {
|
|
138
168
|
if (error instanceof PrototypeNotFoundError)
|
|
139
169
|
return "not-found";
|
|
@@ -175,6 +205,11 @@ function mapError(error) {
|
|
|
175
205
|
if (error instanceof DeploymentUnavailableError) {
|
|
176
206
|
return "deployment-unavailable";
|
|
177
207
|
}
|
|
208
|
+
if (error instanceof DeploymentSourcePushError ||
|
|
209
|
+
error instanceof DeploymentNotAcceptedError ||
|
|
210
|
+
error instanceof DeploymentWorktreeError) {
|
|
211
|
+
return "deployment-failure";
|
|
212
|
+
}
|
|
178
213
|
if (error instanceof DeploymentProviderError) {
|
|
179
214
|
return error.kind === "timeout" ? "timeout" : "deployment-failure";
|
|
180
215
|
}
|
|
@@ -185,6 +220,15 @@ function resultFrom(operation) {
|
|
|
185
220
|
.then(lifecycleSuccess)
|
|
186
221
|
.catch((error) => lifecycleFailure(mapError(error)));
|
|
187
222
|
}
|
|
223
|
+
async function diagnoseMetadataStep(step, operation) {
|
|
224
|
+
try {
|
|
225
|
+
return await operation();
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
reportDeploymentIssue("publication", step, error);
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
188
232
|
export class LocalPrototypeLifecycle {
|
|
189
233
|
#runtime;
|
|
190
234
|
gitMode;
|
|
@@ -194,9 +238,14 @@ export class LocalPrototypeLifecycle {
|
|
|
194
238
|
#deploymentProvider;
|
|
195
239
|
#applicationId;
|
|
196
240
|
#themeRegistry;
|
|
241
|
+
#publishDeploymentSource;
|
|
242
|
+
#installDependencies;
|
|
243
|
+
#restartRequired = false;
|
|
244
|
+
#preparationAttention = new Map();
|
|
197
245
|
constructor(options) {
|
|
198
246
|
const internalOptions = options;
|
|
199
|
-
const runtime = internalOptions.runtime ??
|
|
247
|
+
const runtime = internalOptions.runtime ??
|
|
248
|
+
createPrototypeLifecycleRuntime(internalOptions);
|
|
200
249
|
this.#runtime = {
|
|
201
250
|
...runtime,
|
|
202
251
|
gitMode: parseGitModeSetting(runtime.gitMode),
|
|
@@ -214,6 +263,11 @@ export class LocalPrototypeLifecycle {
|
|
|
214
263
|
internalOptions.preparePrototypeFiles ?? (() => undefined);
|
|
215
264
|
this.#deploymentProvider = internalOptions.deploymentProvider;
|
|
216
265
|
this.#applicationId = internalOptions.applicationId;
|
|
266
|
+
this.#publishDeploymentSource =
|
|
267
|
+
internalOptions.publishDeploymentSource === null
|
|
268
|
+
? null
|
|
269
|
+
: (internalOptions.publishDeploymentSource ?? publishDeploymentSource);
|
|
270
|
+
this.#installDependencies = options.installDependencies;
|
|
217
271
|
if (this.#deploymentProvider &&
|
|
218
272
|
(typeof this.#applicationId !== "string" ||
|
|
219
273
|
this.#applicationId.length === 0)) {
|
|
@@ -230,22 +284,27 @@ export class LocalPrototypeLifecycle {
|
|
|
230
284
|
}
|
|
231
285
|
return branch;
|
|
232
286
|
}
|
|
233
|
-
async #
|
|
287
|
+
async #authorizedPrototype(identity, input) {
|
|
234
288
|
const verifiedIdentity = await this.#verifiedIdentity(identity);
|
|
235
289
|
const normalizedInput = parsePrototypeCommandInput(input);
|
|
236
290
|
if (normalizedInput.ref.login !== verifiedIdentity.login) {
|
|
237
291
|
throw new PrototypeOwnershipError();
|
|
238
292
|
}
|
|
239
|
-
const
|
|
240
|
-
if (!summary) {
|
|
241
|
-
throw new PrototypeNotFoundError();
|
|
242
|
-
}
|
|
293
|
+
const directorySnapshot = await capturePrototypeDirectory(this.#runtime, normalizedInput.ref);
|
|
243
294
|
return {
|
|
244
295
|
ref: normalizedInput.ref,
|
|
245
296
|
path: repositoryRelativePath(this.#runtime, normalizedInput.ref),
|
|
246
|
-
|
|
297
|
+
directorySnapshot,
|
|
247
298
|
};
|
|
248
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
|
+
}
|
|
249
308
|
async #prepareMutationBranch(login) {
|
|
250
309
|
await assertIdentityBranchReady(this.#repository, login);
|
|
251
310
|
return login;
|
|
@@ -280,26 +339,160 @@ export class LocalPrototypeLifecycle {
|
|
|
280
339
|
if (this.#runtime.gitMode === "manual" ||
|
|
281
340
|
this.#runtime.automaticBranchSwitch === "disabled") {
|
|
282
341
|
await this.#currentBranch();
|
|
283
|
-
|
|
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;
|
|
284
353
|
}
|
|
285
354
|
const synchronization = await synchronizeIdentityBranch(this.#repository, {
|
|
286
355
|
identityBranch: verifiedIdentity.login,
|
|
287
356
|
primaryBranch: this.#runtime.primaryBranch,
|
|
288
357
|
remote: this.#runtime.remote,
|
|
358
|
+
prototypeRoot: relative(this.#runtime.repositoryRoot, this.#runtime.prototypeRoot)
|
|
359
|
+
.split(sep)
|
|
360
|
+
.join("/"),
|
|
289
361
|
});
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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",
|
|
392
|
+
reason: synchronization.reason,
|
|
393
|
+
effects,
|
|
394
|
+
...(synchronization.affectedPaths.length > 0
|
|
395
|
+
? { affectedPaths: synchronization.affectedPaths }
|
|
396
|
+
: {}),
|
|
397
|
+
};
|
|
398
|
+
this.#preparationAttention.set(verifiedIdentity.login, attention);
|
|
399
|
+
return attention;
|
|
400
|
+
}
|
|
401
|
+
if (synchronization.status === "unsafe") {
|
|
402
|
+
const attention = {
|
|
403
|
+
status: "unsafe",
|
|
293
404
|
reason: synchronization.reason,
|
|
405
|
+
effects,
|
|
294
406
|
...(synchronization.affectedPaths.length > 0
|
|
295
407
|
? { affectedPaths: synchronization.affectedPaths }
|
|
296
408
|
: {}),
|
|
297
409
|
};
|
|
410
|
+
this.#preparationAttention.set(verifiedIdentity.login, attention);
|
|
411
|
+
return attention;
|
|
298
412
|
}
|
|
299
|
-
|
|
413
|
+
const ready = {
|
|
414
|
+
status: "ready",
|
|
415
|
+
effects,
|
|
416
|
+
...(synchronization.affectedPaths.length > 0
|
|
417
|
+
? { affectedPaths: synchronization.affectedPaths }
|
|
418
|
+
: {}),
|
|
419
|
+
};
|
|
420
|
+
this.#preparationAttention.delete(verifiedIdentity.login);
|
|
421
|
+
return ready;
|
|
300
422
|
});
|
|
301
423
|
});
|
|
302
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
|
+
}
|
|
303
496
|
async create(identity, input) {
|
|
304
497
|
return resultFrom(async () => {
|
|
305
498
|
const verifiedIdentity = await this.#verifiedIdentity(identity);
|
|
@@ -339,11 +532,13 @@ export class LocalPrototypeLifecycle {
|
|
|
339
532
|
};
|
|
340
533
|
}
|
|
341
534
|
await commitPath(this.#repository, repositoryRelativePath(this.#runtime, ref), `init: ${ref.slug}`);
|
|
342
|
-
const
|
|
535
|
+
const sourceSha = await this.#headCommit();
|
|
536
|
+
const outcome = await this.#publishOrdinaryNamespace(verifiedIdentity, "create", ref, repositoryRelativePath(this.#runtime, ref), sourceSha);
|
|
343
537
|
return {
|
|
344
538
|
ref,
|
|
345
539
|
url: getPrototypeUrl(ref),
|
|
346
|
-
remoteStatus:
|
|
540
|
+
remoteStatus: "local-only",
|
|
541
|
+
outcome,
|
|
347
542
|
};
|
|
348
543
|
});
|
|
349
544
|
});
|
|
@@ -389,16 +584,34 @@ export class LocalPrototypeLifecycle {
|
|
|
389
584
|
const verifiedIdentity = await this.#verifiedIdentity(identity);
|
|
390
585
|
const normalizedInput = parsePrototypeCommandInput(input);
|
|
391
586
|
return withRepositoryOperation(this.#runtime.repositoryRealPath, async () => {
|
|
392
|
-
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
|
+
}
|
|
393
591
|
const branch = this.#runtime.gitMode === "managed"
|
|
394
592
|
? await this.#prepareMutationBranch(verifiedIdentity.login)
|
|
395
593
|
: undefined;
|
|
396
|
-
await
|
|
594
|
+
await assertPrototypeDirectory(this.#runtime, ref, directorySnapshot);
|
|
595
|
+
await deletePrototypePath(this.#repository, path, branch === undefined ? undefined : `delete: ${ref.slug}`);
|
|
397
596
|
if (branch !== undefined) {
|
|
398
|
-
await
|
|
399
|
-
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
|
+
};
|
|
400
604
|
}
|
|
401
|
-
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
|
+
};
|
|
402
615
|
});
|
|
403
616
|
});
|
|
404
617
|
}
|
|
@@ -417,7 +630,7 @@ export class LocalPrototypeLifecycle {
|
|
|
417
630
|
async getRepositorySummary(identity) {
|
|
418
631
|
return resultFrom(async () => {
|
|
419
632
|
const verifiedIdentity = await this.#verifiedIdentity(identity);
|
|
420
|
-
return getRepositorySummary({
|
|
633
|
+
return withRepositoryOperation(this.#runtime.repositoryRealPath, async () => getRepositorySummary({
|
|
421
634
|
repository: this.#repository,
|
|
422
635
|
catalog: this.#catalog,
|
|
423
636
|
repositoryRoot: this.#runtime.repositoryRoot,
|
|
@@ -426,13 +639,16 @@ export class LocalPrototypeLifecycle {
|
|
|
426
639
|
mode: this.#runtime.gitMode,
|
|
427
640
|
remote: this.#runtime.remote,
|
|
428
641
|
primaryBranch: this.#runtime.primaryBranch,
|
|
429
|
-
|
|
642
|
+
restartRequired: this.#restartRequired,
|
|
643
|
+
preparationAttention: this.#preparationAttention.get(verifiedIdentity.login),
|
|
644
|
+
preparationPending: (await readPreparationMarker(this.#repository, this.#runtime.repositoryRoot)) !== null,
|
|
645
|
+
}));
|
|
430
646
|
});
|
|
431
647
|
}
|
|
432
648
|
async checkRepositorySummary(identity) {
|
|
433
649
|
return resultFrom(async () => {
|
|
434
650
|
const verifiedIdentity = await this.#verifiedIdentity(identity);
|
|
435
|
-
return getRepositorySummary({
|
|
651
|
+
return withRepositoryOperation(this.#runtime.repositoryRealPath, async () => getRepositorySummary({
|
|
436
652
|
repository: this.#repository,
|
|
437
653
|
catalog: this.#catalog,
|
|
438
654
|
repositoryRoot: this.#runtime.repositoryRoot,
|
|
@@ -441,10 +657,149 @@ export class LocalPrototypeLifecycle {
|
|
|
441
657
|
mode: this.#runtime.gitMode,
|
|
442
658
|
remote: this.#runtime.remote,
|
|
443
659
|
primaryBranch: this.#runtime.primaryBranch,
|
|
660
|
+
restartRequired: this.#restartRequired,
|
|
661
|
+
preparationAttention: this.#preparationAttention.get(verifiedIdentity.login),
|
|
662
|
+
preparationPending: (await readPreparationMarker(this.#repository, this.#runtime.repositoryRoot)) !== null,
|
|
444
663
|
refresh: true,
|
|
445
|
-
});
|
|
664
|
+
}));
|
|
446
665
|
});
|
|
447
666
|
}
|
|
667
|
+
async #headCommit() {
|
|
668
|
+
const commit = (await this.#repository.raw(["rev-parse", "HEAD"])).trim();
|
|
669
|
+
if (!/^[0-9a-f]{40,64}$/iu.test(commit)) {
|
|
670
|
+
throw new GitRepositoryError("malformed-output", "head-commit");
|
|
671
|
+
}
|
|
672
|
+
return commit.toLowerCase();
|
|
673
|
+
}
|
|
674
|
+
#workspaceRuntime(workspacePath, workspaceRepository) {
|
|
675
|
+
return {
|
|
676
|
+
...this.#runtime,
|
|
677
|
+
repositoryRoot: workspacePath,
|
|
678
|
+
repositoryRealPath: workspacePath,
|
|
679
|
+
prototypeRoot: join(workspacePath, relative(this.#runtime.repositoryRoot, this.#runtime.prototypeRoot)),
|
|
680
|
+
gitRepository: workspaceRepository,
|
|
681
|
+
};
|
|
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
|
+
}
|
|
768
|
+
async #publishDeploymentMetadata(pending) {
|
|
769
|
+
if (this.#publishDeploymentSource === null) {
|
|
770
|
+
throw new DeploymentUnavailableError();
|
|
771
|
+
}
|
|
772
|
+
return this.#publishDeploymentSource(this.#repository, {
|
|
773
|
+
remote: this.#runtime.remote,
|
|
774
|
+
primaryBranch: this.#runtime.primaryBranch,
|
|
775
|
+
sourceSha: pending.sourceSha,
|
|
776
|
+
namespacePath: namespaceRelativePath(this.#runtime, pending.identity.login),
|
|
777
|
+
commitIdentity: this.#runtime.commitIdentity,
|
|
778
|
+
}, async ({ path, repository }) => {
|
|
779
|
+
const workspaceRuntime = this.#workspaceRuntime(path, repository);
|
|
780
|
+
const mergedDocument = await diagnoseMetadataStep("metadata-read", () => readMetadataDocument(workspaceRuntime, pending.ref, this.#themeRegistry));
|
|
781
|
+
await diagnoseMetadataStep("metadata-write", () => writeMetadataDocument(workspaceRuntime, pending.ref, {
|
|
782
|
+
...mergedDocument,
|
|
783
|
+
previewUrl: pending.previewUrl,
|
|
784
|
+
updatedAt: pending.acceptedAt,
|
|
785
|
+
}));
|
|
786
|
+
await diagnoseMetadataStep("metadata-commit", () => commitPath(repository, pending.path, `deploy: ${pending.ref.slug} (metadata)`));
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
async #deploymentValue(pending, publicationWarnings = [], outcome) {
|
|
790
|
+
const warnings = [...publicationWarnings];
|
|
791
|
+
return {
|
|
792
|
+
deployed: true,
|
|
793
|
+
ref: pending.ref,
|
|
794
|
+
previewUrl: pending.previewUrl,
|
|
795
|
+
deploymentId: pending.deploymentId,
|
|
796
|
+
sourceSha: pending.sourceSha,
|
|
797
|
+
readyState: pending.readyState,
|
|
798
|
+
...(outcome === undefined ? {} : { outcome }),
|
|
799
|
+
warning: publicationWarnings[0],
|
|
800
|
+
...(warnings.length === 0 ? {} : { warnings }),
|
|
801
|
+
};
|
|
802
|
+
}
|
|
448
803
|
async deploy(identity, input) {
|
|
449
804
|
if (this.#runtime.gitMode === "manual") {
|
|
450
805
|
return lifecycleFailure("forbidden");
|
|
@@ -457,7 +812,7 @@ export class LocalPrototypeLifecycle {
|
|
|
457
812
|
}
|
|
458
813
|
return withRepositoryOperation(this.#runtime.repositoryRealPath, async () => {
|
|
459
814
|
const { ref, path } = await this.#ownedPrototype(verifiedIdentity, normalizedInput);
|
|
460
|
-
|
|
815
|
+
await assertIdentityBranchReady(this.#repository, verifiedIdentity.login);
|
|
461
816
|
await assertPrototypeDirectory(this.#runtime, ref);
|
|
462
817
|
const document = await readMetadataDocument(this.#runtime, ref, this.#themeRegistry);
|
|
463
818
|
const metadata = parsePrototypeMetadata(document, {
|
|
@@ -466,60 +821,344 @@ export class LocalPrototypeLifecycle {
|
|
|
466
821
|
if (metadata.authorLogin !== ref.login) {
|
|
467
822
|
throw new PrototypeNotFoundError();
|
|
468
823
|
}
|
|
469
|
-
await
|
|
470
|
-
await
|
|
471
|
-
|
|
472
|
-
applicationId: this.#applicationId,
|
|
473
|
-
login: verifiedIdentity.login,
|
|
474
|
-
});
|
|
475
|
-
const previewUrl = new URL(getPrototypeUrl(ref).slice(1), `${deployment.url.replace(/\/+$/, "")}/`).toString();
|
|
476
|
-
let warning;
|
|
824
|
+
await commitPrototypePath(this.#repository, path, `deploy: ${ref.slug}`);
|
|
825
|
+
const sourceSha = await this.#headCommit();
|
|
826
|
+
let sourcePush;
|
|
477
827
|
try {
|
|
478
|
-
await
|
|
479
|
-
...document,
|
|
480
|
-
previewUrl,
|
|
481
|
-
updatedAt: new Date().toISOString(),
|
|
482
|
-
});
|
|
483
|
-
await commitPath(this.#repository, path, `deploy: ${ref.slug} (metadata)`);
|
|
484
|
-
await pushBranch(this.#repository, this.#runtime.remote, branch);
|
|
485
|
-
}
|
|
486
|
-
catch {
|
|
487
|
-
warning = "metadata-persistence-failed";
|
|
828
|
+
sourcePush = await pushCommitToBranch(this.#repository, this.#runtime.remote, sourceSha, verifiedIdentity.login, true);
|
|
488
829
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
+
};
|
|
496
860
|
}
|
|
497
|
-
|
|
498
|
-
|
|
861
|
+
throw error;
|
|
862
|
+
}
|
|
863
|
+
if (sourcePush.remoteStatus !== "pushed") {
|
|
864
|
+
throw new DeploymentSourcePushError();
|
|
865
|
+
}
|
|
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
|
+
};
|
|
499
903
|
}
|
|
904
|
+
throw error;
|
|
500
905
|
}
|
|
501
|
-
|
|
502
|
-
|
|
906
|
+
if (deployment.commitSha.toLowerCase() !== sourceSha ||
|
|
907
|
+
!["READY", "QUEUED", "INITIALIZING", "BUILDING"].includes(deployment.readyState)) {
|
|
908
|
+
throw new DeploymentNotAcceptedError();
|
|
909
|
+
}
|
|
910
|
+
const previewUrl = new URL(getPrototypeUrl(ref).slice(1), `${deployment.url.replace(/\/+$/, "")}/`).toString();
|
|
911
|
+
const pending = {
|
|
912
|
+
recoveryId: randomUUID(),
|
|
913
|
+
operation: "deploy",
|
|
914
|
+
kind: "deployment-publication",
|
|
915
|
+
identity: verifiedIdentity,
|
|
503
916
|
ref,
|
|
917
|
+
path,
|
|
504
918
|
previewUrl,
|
|
505
919
|
deploymentId: deployment.deploymentId,
|
|
920
|
+
sourceSha,
|
|
921
|
+
acceptedAt: new Date().toISOString(),
|
|
506
922
|
readyState: deployment.readyState,
|
|
507
|
-
warning,
|
|
508
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" },
|
|
933
|
+
};
|
|
934
|
+
try {
|
|
935
|
+
const publication = await this.#publishDeploymentMetadata(pending);
|
|
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
|
+
});
|
|
944
|
+
}
|
|
945
|
+
catch (error) {
|
|
946
|
+
// Keep the accepted deployment retryable for every publication
|
|
947
|
+
// failure, including metadata preparation failures.
|
|
948
|
+
this.#rememberPending(pending);
|
|
949
|
+
return this.#deploymentValue(pending, [
|
|
950
|
+
error instanceof DeploymentWorktreeError &&
|
|
951
|
+
error.kind !== "cleanup-failure" &&
|
|
952
|
+
error.kind !== "metadata-failure"
|
|
953
|
+
? "primary-merge-failed"
|
|
954
|
+
: "metadata-persistence-failed",
|
|
955
|
+
...(error instanceof DeploymentWorktreeError &&
|
|
956
|
+
error.cleanupFailed
|
|
957
|
+
? ["publication-cleanup-failed"]
|
|
958
|
+
: []),
|
|
959
|
+
], {
|
|
960
|
+
...acceptedOutcome,
|
|
961
|
+
publication: {
|
|
962
|
+
status: "failed",
|
|
963
|
+
recoveryId: pending.recoveryId,
|
|
964
|
+
},
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
});
|
|
969
|
+
}
|
|
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
|
+
},
|
|
509
1001
|
});
|
|
510
1002
|
});
|
|
511
1003
|
}
|
|
1004
|
+
async retryPendingPublication(identity, recoveryId) {
|
|
1005
|
+
if (this.#runtime.gitMode === "manual") {
|
|
1006
|
+
return lifecycleFailure("forbidden");
|
|
1007
|
+
}
|
|
1008
|
+
return resultFrom(async () => {
|
|
1009
|
+
const verifiedIdentity = await this.#verifiedIdentity(identity);
|
|
1010
|
+
const pending = pendingPublications.get(this.#pendingKey(verifiedIdentity.login));
|
|
1011
|
+
if (pending === undefined || pending.recoveryId !== recoveryId) {
|
|
1012
|
+
throw new DeploymentUnavailableError();
|
|
1013
|
+
}
|
|
1014
|
+
return withRepositoryOperation(this.#runtime.repositoryRealPath, async () => {
|
|
1015
|
+
await assertIdentityBranchReady(this.#repository, verifiedIdentity.login);
|
|
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
|
+
});
|
|
1112
|
+
});
|
|
1113
|
+
});
|
|
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
|
+
}
|
|
512
1151
|
async save(identity, input) {
|
|
513
1152
|
if (this.#runtime.gitMode === "manual") {
|
|
514
1153
|
return lifecycleFailure("forbidden");
|
|
515
1154
|
}
|
|
516
1155
|
return resultFrom(async () => {
|
|
517
1156
|
const verifiedIdentity = await this.#verifiedIdentity(identity);
|
|
518
|
-
const { ref, path } = await this.#ownedPrototype(verifiedIdentity, input);
|
|
519
1157
|
return withRepositoryOperation(this.#runtime.repositoryRealPath, async () => {
|
|
520
1158
|
await this.#prepareMutationBranch(verifiedIdentity.login);
|
|
521
|
-
|
|
522
|
-
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}`);
|
|
523
1162
|
return { saved: true };
|
|
524
1163
|
});
|
|
525
1164
|
});
|
|
@@ -530,10 +1169,11 @@ export class LocalPrototypeLifecycle {
|
|
|
530
1169
|
}
|
|
531
1170
|
return resultFrom(async () => {
|
|
532
1171
|
const verifiedIdentity = await this.#verifiedIdentity(identity);
|
|
533
|
-
const { path } = await this.#ownedPrototype(verifiedIdentity, input);
|
|
534
1172
|
return withRepositoryOperation(this.#runtime.repositoryRealPath, async () => {
|
|
535
1173
|
await this.#prepareMutationBranch(verifiedIdentity.login);
|
|
536
|
-
await
|
|
1174
|
+
const { ref, path, directorySnapshot } = await this.#authorizedPrototype(verifiedIdentity, input);
|
|
1175
|
+
await assertPrototypeDirectory(this.#runtime, ref, directorySnapshot);
|
|
1176
|
+
await discardPrototypePath(this.#repository, path);
|
|
537
1177
|
return { discarded: true };
|
|
538
1178
|
});
|
|
539
1179
|
});
|