@git.zone/cli 6.0.1 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/helpers.workflow.d.ts +2 -0
- package/dist_ts/helpers.workflow.js +96 -25
- package/dist_ts/mod_config/index.js +176 -88
- package/dist_ts/mod_release/classes.releasejournal.d.ts +60 -10
- package/dist_ts/mod_release/classes.releasejournal.js +513 -21
- package/dist_ts/mod_release/helpers.releasepublication.d.ts +5 -3
- package/dist_ts/mod_release/helpers.releasepublication.js +386 -13
- package/dist_ts/mod_release/helpers.tsdockerprotocol.d.ts +60 -0
- package/dist_ts/mod_release/helpers.tsdockerprotocol.js +442 -0
- package/dist_ts/mod_release/index.d.ts +1 -1
- package/dist_ts/mod_release/index.js +101 -19
- package/dist_ts/plugins.d.ts +5 -1
- package/dist_ts/plugins.js +5 -2
- package/package.json +2 -1
- package/readme.hints.md +43 -17
- package/readme.md +83 -75
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/helpers.workflow.ts +191 -49
- package/ts/mod_config/index.ts +320 -146
- package/ts/mod_release/classes.releasejournal.ts +939 -70
- package/ts/mod_release/helpers.releasepublication.ts +780 -49
- package/ts/mod_release/helpers.tsdockerprotocol.ts +806 -0
- package/ts/mod_release/index.ts +231 -74
- package/ts/plugins.ts +20 -0
|
@@ -2,8 +2,11 @@ import * as plugins from "./mod.plugins.js";
|
|
|
2
2
|
import {
|
|
3
3
|
ReleaseJournalStore,
|
|
4
4
|
createReleaseAttempt,
|
|
5
|
+
createReleaseDockerPromotions,
|
|
5
6
|
finalizeJournalCompletion,
|
|
6
|
-
|
|
7
|
+
getReleaseDockerPromotionContext,
|
|
8
|
+
type TReleaseJournal,
|
|
9
|
+
type IReleaseJournalV2,
|
|
7
10
|
type IReleaseTargetStatus,
|
|
8
11
|
type TReleaseErrorCode,
|
|
9
12
|
type TReleaseTargetState,
|
|
@@ -17,6 +20,18 @@ import {
|
|
|
17
20
|
type INpmArtifactProbeResult,
|
|
18
21
|
} from "./helpers.npmartifact.js";
|
|
19
22
|
import { releaseGitEnv } from "./helpers.releasebranch.js";
|
|
23
|
+
import {
|
|
24
|
+
TsdockerReleaseCommandError,
|
|
25
|
+
TsdockerReleaseRequestCleanupError,
|
|
26
|
+
assertReleaseCleanupResult,
|
|
27
|
+
assertReleaseProbeResult,
|
|
28
|
+
assertReleasePromotionResult,
|
|
29
|
+
assertReleaseQualificationResult,
|
|
30
|
+
classifyReleasePromotionEvidence,
|
|
31
|
+
type ITsdockerReleaseClient,
|
|
32
|
+
type IReleaseProbeResult,
|
|
33
|
+
type TReleasePromotionEvidence,
|
|
34
|
+
} from "./helpers.tsdockerprotocol.js";
|
|
20
35
|
|
|
21
36
|
const gitProbeTimeoutMs = 60_000;
|
|
22
37
|
const gitPushTimeoutMs = 5 * 60_000;
|
|
@@ -39,9 +54,7 @@ export function buildReleaseGitPushArgs(
|
|
|
39
54
|
refspecs.push(`${optionsArg.mainOid}:refs/heads/main`);
|
|
40
55
|
}
|
|
41
56
|
if (optionsArg.pushTags) {
|
|
42
|
-
refspecs.push(
|
|
43
|
-
`${optionsArg.tagOid}:refs/tags/v${optionsArg.newVersion}`,
|
|
44
|
-
);
|
|
57
|
+
refspecs.push(`${optionsArg.tagOid}:refs/tags/v${optionsArg.newVersion}`);
|
|
45
58
|
}
|
|
46
59
|
const args = ["push", "--no-follow-tags"];
|
|
47
60
|
if (refspecs.length > 1) {
|
|
@@ -58,7 +71,10 @@ export function buildReleaseGitPushArgs(
|
|
|
58
71
|
|
|
59
72
|
type TReleaseTargetSelector =
|
|
60
73
|
| { kind: "git" }
|
|
61
|
-
| { kind: "npm"; registry: string }
|
|
74
|
+
| { kind: "npm"; registry: string }
|
|
75
|
+
| { kind: "docker-qualification" }
|
|
76
|
+
| { kind: "docker-promotion"; promotionId: string }
|
|
77
|
+
| { kind: "docker-cleanup" };
|
|
62
78
|
|
|
63
79
|
type TGitProbeStatus = "pending" | "exact" | "conflict" | "inconclusive";
|
|
64
80
|
|
|
@@ -70,37 +86,66 @@ export interface IReleasePublicationOptions {
|
|
|
70
86
|
smartshell: plugins.smartshell.Smartshell;
|
|
71
87
|
cwd: string;
|
|
72
88
|
store: ReleaseJournalStore;
|
|
73
|
-
journal:
|
|
89
|
+
journal: TReleaseJournal;
|
|
74
90
|
pushUrl?: string;
|
|
75
91
|
recoveryAttemptId?: string;
|
|
76
92
|
npmProbeOptions?: INpmArtifactProbeOptions;
|
|
93
|
+
tsdocker?: ITsdockerReleaseClient;
|
|
77
94
|
ensurePublicationState: (expectedRemoteMainOidArg?: string) => Promise<void>;
|
|
78
95
|
}
|
|
79
96
|
|
|
80
97
|
const getTargetStatus = (
|
|
81
|
-
journalArg:
|
|
98
|
+
journalArg: TReleaseJournal,
|
|
82
99
|
selectorArg: TReleaseTargetSelector,
|
|
83
100
|
): IReleaseTargetStatus => {
|
|
84
101
|
if (selectorArg.kind === "git") {
|
|
85
102
|
return journalArg.git;
|
|
86
103
|
}
|
|
104
|
+
if (selectorArg.kind === "docker-qualification") {
|
|
105
|
+
if (journalArg.schemaVersion !== 2) {
|
|
106
|
+
throw new Error("Release journal does not contain Docker qualification.");
|
|
107
|
+
}
|
|
108
|
+
return journalArg.docker.qualification;
|
|
109
|
+
}
|
|
110
|
+
if (selectorArg.kind === "docker-cleanup") {
|
|
111
|
+
if (journalArg.schemaVersion !== 2) {
|
|
112
|
+
throw new Error("Release journal does not contain Docker cleanup.");
|
|
113
|
+
}
|
|
114
|
+
return journalArg.docker.cleanup;
|
|
115
|
+
}
|
|
116
|
+
if (selectorArg.kind === "docker-promotion") {
|
|
117
|
+
if (journalArg.schemaVersion !== 2) {
|
|
118
|
+
throw new Error("Release journal does not contain Docker promotions.");
|
|
119
|
+
}
|
|
120
|
+
const promotion = journalArg.docker.promotions.find(
|
|
121
|
+
(promotionArg) => promotionArg.promotionId === selectorArg.promotionId,
|
|
122
|
+
);
|
|
123
|
+
if (!promotion) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Release journal does not contain Docker promotion ${selectorArg.promotionId}.`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return promotion;
|
|
129
|
+
}
|
|
87
130
|
const registry = journalArg.npm.registries.find(
|
|
88
131
|
(registryArg) => registryArg.registry === selectorArg.registry,
|
|
89
132
|
);
|
|
90
133
|
if (!registry) {
|
|
91
|
-
throw new Error(
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Release journal does not contain npm registry ${selectorArg.registry}.`,
|
|
136
|
+
);
|
|
92
137
|
}
|
|
93
138
|
return registry;
|
|
94
139
|
};
|
|
95
140
|
|
|
96
141
|
const updateTarget = async (
|
|
97
142
|
storeArg: ReleaseJournalStore,
|
|
98
|
-
journalArg:
|
|
143
|
+
journalArg: TReleaseJournal,
|
|
99
144
|
selectorArg: TReleaseTargetSelector,
|
|
100
145
|
expectedStateArg: TReleaseTargetState | TReleaseTargetState[],
|
|
101
146
|
expectedAttemptIdArg: string | null,
|
|
102
147
|
updateArg: (statusArg: IReleaseTargetStatus) => void,
|
|
103
|
-
): Promise<
|
|
148
|
+
): Promise<TReleaseJournal> => {
|
|
104
149
|
const expectedStates = Array.isArray(expectedStateArg)
|
|
105
150
|
? expectedStateArg
|
|
106
151
|
: [expectedStateArg];
|
|
@@ -113,7 +158,9 @@ const updateTarget = async (
|
|
|
113
158
|
throw new Error("Release target state changed concurrently.");
|
|
114
159
|
}
|
|
115
160
|
if ((status.attempt?.id || null) !== expectedAttemptIdArg) {
|
|
116
|
-
throw new Error(
|
|
161
|
+
throw new Error(
|
|
162
|
+
"Release target attempt ownership changed concurrently.",
|
|
163
|
+
);
|
|
117
164
|
}
|
|
118
165
|
updateArg(status);
|
|
119
166
|
return finalizeJournalCompletion(currentArg);
|
|
@@ -123,9 +170,9 @@ const updateTarget = async (
|
|
|
123
170
|
|
|
124
171
|
const claimTarget = async (
|
|
125
172
|
storeArg: ReleaseJournalStore,
|
|
126
|
-
journalArg:
|
|
173
|
+
journalArg: TReleaseJournal,
|
|
127
174
|
selectorArg: TReleaseTargetSelector,
|
|
128
|
-
): Promise<
|
|
175
|
+
): Promise<TReleaseJournal> =>
|
|
129
176
|
updateTarget(
|
|
130
177
|
storeArg,
|
|
131
178
|
journalArg,
|
|
@@ -142,11 +189,11 @@ const claimTarget = async (
|
|
|
142
189
|
|
|
143
190
|
const finishPublishingTarget = async (
|
|
144
191
|
storeArg: ReleaseJournalStore,
|
|
145
|
-
journalArg:
|
|
192
|
+
journalArg: TReleaseJournal,
|
|
146
193
|
selectorArg: TReleaseTargetSelector,
|
|
147
194
|
stateArg: "verified" | "failed" | "conflict",
|
|
148
195
|
errorArg: TReleaseErrorCode | null,
|
|
149
|
-
): Promise<
|
|
196
|
+
): Promise<TReleaseJournal> => {
|
|
150
197
|
const attemptId = getTargetStatus(journalArg, selectorArg).attempt?.id;
|
|
151
198
|
if (!attemptId) {
|
|
152
199
|
throw new Error("Release target has no active publication attempt.");
|
|
@@ -167,12 +214,12 @@ const finishPublishingTarget = async (
|
|
|
167
214
|
|
|
168
215
|
const markUnclaimedTarget = async (
|
|
169
216
|
storeArg: ReleaseJournalStore,
|
|
170
|
-
journalArg:
|
|
217
|
+
journalArg: TReleaseJournal,
|
|
171
218
|
selectorArg: TReleaseTargetSelector,
|
|
172
219
|
expectedStatesArg: TReleaseTargetState[],
|
|
173
220
|
stateArg: "verified" | "failed" | "conflict",
|
|
174
221
|
errorArg: TReleaseErrorCode | null,
|
|
175
|
-
): Promise<
|
|
222
|
+
): Promise<TReleaseJournal> =>
|
|
176
223
|
updateTarget(
|
|
177
224
|
storeArg,
|
|
178
225
|
journalArg,
|
|
@@ -187,10 +234,10 @@ const markUnclaimedTarget = async (
|
|
|
187
234
|
|
|
188
235
|
const recoverPublishingTarget = async (
|
|
189
236
|
storeArg: ReleaseJournalStore,
|
|
190
|
-
journalArg:
|
|
237
|
+
journalArg: TReleaseJournal,
|
|
191
238
|
selectorArg: TReleaseTargetSelector,
|
|
192
239
|
recoveryAttemptIdArg: string | undefined,
|
|
193
|
-
): Promise<
|
|
240
|
+
): Promise<TReleaseJournal> => {
|
|
194
241
|
const status = getTargetStatus(journalArg, selectorArg);
|
|
195
242
|
const attemptId = status.attempt?.id;
|
|
196
243
|
if (!attemptId || recoveryAttemptIdArg !== attemptId) {
|
|
@@ -212,18 +259,12 @@ const inspectRemoteRelease = async (
|
|
|
212
259
|
smartshellArg: plugins.smartshell.Smartshell,
|
|
213
260
|
cwdArg: string,
|
|
214
261
|
destinationArg: string,
|
|
215
|
-
journalArg:
|
|
262
|
+
journalArg: TReleaseJournal,
|
|
216
263
|
): Promise<IGitProbeResult> => {
|
|
217
264
|
const tagRef = `refs/tags/${journalArg.release.tag}`;
|
|
218
265
|
const result = await smartshellArg.execSpawn(
|
|
219
266
|
"git",
|
|
220
|
-
[
|
|
221
|
-
"ls-remote",
|
|
222
|
-
destinationArg,
|
|
223
|
-
"refs/heads/main",
|
|
224
|
-
tagRef,
|
|
225
|
-
`${tagRef}^{}`,
|
|
226
|
-
],
|
|
267
|
+
["ls-remote", destinationArg, "refs/heads/main", tagRef, `${tagRef}^{}`],
|
|
227
268
|
{
|
|
228
269
|
cwd: cwdArg,
|
|
229
270
|
env: releaseGitEnv,
|
|
@@ -262,8 +303,8 @@ const inspectRemoteRelease = async (
|
|
|
262
303
|
|
|
263
304
|
const executeGitTarget = async (
|
|
264
305
|
optionsArg: IReleasePublicationOptions,
|
|
265
|
-
journalArg:
|
|
266
|
-
): Promise<
|
|
306
|
+
journalArg: TReleaseJournal,
|
|
307
|
+
): Promise<TReleaseJournal> => {
|
|
267
308
|
const selector: TReleaseTargetSelector = { kind: "git" };
|
|
268
309
|
if (journalArg.git.state === "skipped") {
|
|
269
310
|
return journalArg;
|
|
@@ -335,7 +376,9 @@ const executeGitTarget = async (
|
|
|
335
376
|
}
|
|
336
377
|
|
|
337
378
|
if (journal.git.state === "conflict") {
|
|
338
|
-
throw new Error(
|
|
379
|
+
throw new Error(
|
|
380
|
+
"Git release target is in conflict and cannot resume automatically.",
|
|
381
|
+
);
|
|
339
382
|
}
|
|
340
383
|
probe = await inspectRemoteRelease(
|
|
341
384
|
optionsArg.smartshell,
|
|
@@ -430,15 +473,13 @@ const executeGitTarget = async (
|
|
|
430
473
|
journal,
|
|
431
474
|
selector,
|
|
432
475
|
"failed",
|
|
433
|
-
pushResult.exitCode === 0
|
|
434
|
-
? "verification-inconclusive"
|
|
435
|
-
: "command-failed",
|
|
476
|
+
pushResult.exitCode === 0 ? "verification-inconclusive" : "command-failed",
|
|
436
477
|
);
|
|
437
478
|
throw new Error("Git publication did not produce verifiable remote refs.");
|
|
438
479
|
};
|
|
439
480
|
|
|
440
481
|
const probeNpm = async (
|
|
441
|
-
journalArg:
|
|
482
|
+
journalArg: TReleaseJournal,
|
|
442
483
|
registryArg: string,
|
|
443
484
|
optionsArg: IReleasePublicationOptions,
|
|
444
485
|
waitArg = false,
|
|
@@ -463,11 +504,11 @@ const probeNpm = async (
|
|
|
463
504
|
|
|
464
505
|
const markNpmProbeFailure = async (
|
|
465
506
|
optionsArg: IReleasePublicationOptions,
|
|
466
|
-
journalArg:
|
|
507
|
+
journalArg: TReleaseJournal,
|
|
467
508
|
selectorArg: TReleaseTargetSelector,
|
|
468
509
|
expectedStatesArg: TReleaseTargetState[],
|
|
469
510
|
probeArg: INpmArtifactProbeResult,
|
|
470
|
-
): Promise<
|
|
511
|
+
): Promise<TReleaseJournal> => {
|
|
471
512
|
const conflict = probeArg.status === "conflict";
|
|
472
513
|
return markUnclaimedTarget(
|
|
473
514
|
optionsArg.store,
|
|
@@ -481,10 +522,13 @@ const markNpmProbeFailure = async (
|
|
|
481
522
|
|
|
482
523
|
const executeNpmRegistry = async (
|
|
483
524
|
optionsArg: IReleasePublicationOptions,
|
|
484
|
-
journalArg:
|
|
525
|
+
journalArg: TReleaseJournal,
|
|
485
526
|
registryArg: string,
|
|
486
|
-
): Promise<
|
|
487
|
-
const selector: TReleaseTargetSelector = {
|
|
527
|
+
): Promise<TReleaseJournal> => {
|
|
528
|
+
const selector: TReleaseTargetSelector = {
|
|
529
|
+
kind: "npm",
|
|
530
|
+
registry: registryArg,
|
|
531
|
+
};
|
|
488
532
|
let journal = journalArg;
|
|
489
533
|
let status = getTargetStatus(journal, selector);
|
|
490
534
|
let probe = await probeNpm(journal, registryArg, optionsArg);
|
|
@@ -502,7 +546,9 @@ const executeNpmRegistry = async (
|
|
|
502
546
|
"conflict",
|
|
503
547
|
"artifact-conflict",
|
|
504
548
|
);
|
|
505
|
-
throw new Error(
|
|
549
|
+
throw new Error(
|
|
550
|
+
`Previously verified npm registry drifted: ${registryArg}`,
|
|
551
|
+
);
|
|
506
552
|
}
|
|
507
553
|
throw new Error(
|
|
508
554
|
`Previously verified npm registry cannot currently be verified: ${registryArg}`,
|
|
@@ -526,10 +572,14 @@ const executeNpmRegistry = async (
|
|
|
526
572
|
"conflict",
|
|
527
573
|
"artifact-conflict",
|
|
528
574
|
);
|
|
529
|
-
throw new Error(
|
|
575
|
+
throw new Error(
|
|
576
|
+
`npm registry contains conflicting bytes: ${registryArg}`,
|
|
577
|
+
);
|
|
530
578
|
}
|
|
531
579
|
if (probe.status === "inconclusive") {
|
|
532
|
-
throw new Error(
|
|
580
|
+
throw new Error(
|
|
581
|
+
`npm registry publication is inconclusive: ${registryArg}`,
|
|
582
|
+
);
|
|
533
583
|
}
|
|
534
584
|
journal = await recoverPublishingTarget(
|
|
535
585
|
optionsArg.store,
|
|
@@ -576,7 +626,9 @@ const executeNpmRegistry = async (
|
|
|
576
626
|
throw new Error(`npm registry cannot be verified safely: ${registryArg}`);
|
|
577
627
|
}
|
|
578
628
|
|
|
579
|
-
const artifactPath = optionsArg.store.getArtifactPath(
|
|
629
|
+
const artifactPath = optionsArg.store.getArtifactPath(
|
|
630
|
+
journal.release.version,
|
|
631
|
+
);
|
|
580
632
|
if (!journal.artifact) {
|
|
581
633
|
throw new Error("npm release journal has no artifact.");
|
|
582
634
|
}
|
|
@@ -620,10 +672,680 @@ const executeNpmRegistry = async (
|
|
|
620
672
|
throw new Error(`npm publication could not be verified: ${registryArg}`);
|
|
621
673
|
};
|
|
622
674
|
|
|
675
|
+
const requireTsdocker = (
|
|
676
|
+
optionsArg: IReleasePublicationOptions,
|
|
677
|
+
): ITsdockerReleaseClient => {
|
|
678
|
+
if (!optionsArg.tsdocker) {
|
|
679
|
+
throw new Error(
|
|
680
|
+
"Docker release journal requires the verified project-local tSDocker protocol client.",
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
return optionsArg.tsdocker;
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
const getDockerRequestDirectory = (
|
|
687
|
+
optionsArg: IReleasePublicationOptions,
|
|
688
|
+
journalArg: IReleaseJournalV2,
|
|
689
|
+
): string => optionsArg.store.getReleaseDirectory(journalArg.release.version);
|
|
690
|
+
|
|
691
|
+
const assertDockerRecoveryAttempt = (
|
|
692
|
+
statusArg: IReleaseTargetStatus,
|
|
693
|
+
recoveryAttemptIdArg: string | undefined,
|
|
694
|
+
): string => {
|
|
695
|
+
const attemptId = statusArg.attempt?.id;
|
|
696
|
+
if (!attemptId || recoveryAttemptIdArg !== attemptId) {
|
|
697
|
+
throw new Error(
|
|
698
|
+
`Release target has unresolved publication attempt ${attemptId || "unknown"}. ` +
|
|
699
|
+
`After proving its publisher stopped, retry with --recover-attempt=${attemptId || "<attempt-id>"}.`,
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
return attemptId;
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
const reclaimDockerRequestOwnership = async (
|
|
706
|
+
optionsArg: IReleasePublicationOptions,
|
|
707
|
+
journalArg: IReleaseJournalV2,
|
|
708
|
+
statusArg: IReleaseTargetStatus,
|
|
709
|
+
): Promise<string> => {
|
|
710
|
+
const attemptId = assertDockerRecoveryAttempt(
|
|
711
|
+
statusArg,
|
|
712
|
+
optionsArg.recoveryAttemptId,
|
|
713
|
+
);
|
|
714
|
+
await requireTsdocker(optionsArg).reclaimRequestFiles(
|
|
715
|
+
getDockerRequestDirectory(optionsArg, journalArg),
|
|
716
|
+
[attemptId],
|
|
717
|
+
);
|
|
718
|
+
return attemptId;
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
const recoverRequestCleanupOutcome = async <T>(
|
|
722
|
+
optionsArg: IReleasePublicationOptions,
|
|
723
|
+
journalArg: IReleaseJournalV2,
|
|
724
|
+
ownerIdArg: string,
|
|
725
|
+
errorArg: unknown,
|
|
726
|
+
assertResultArg: (valueArg: unknown) => T,
|
|
727
|
+
): Promise<T> => {
|
|
728
|
+
if (!(errorArg instanceof TsdockerReleaseRequestCleanupError)) {
|
|
729
|
+
throw errorArg;
|
|
730
|
+
}
|
|
731
|
+
try {
|
|
732
|
+
await requireTsdocker(optionsArg).reclaimRequestFiles(
|
|
733
|
+
getDockerRequestDirectory(optionsArg, journalArg),
|
|
734
|
+
[ownerIdArg],
|
|
735
|
+
);
|
|
736
|
+
} catch (reclamationError) {
|
|
737
|
+
errorArg.reclamationError = reclamationError;
|
|
738
|
+
throw errorArg;
|
|
739
|
+
}
|
|
740
|
+
if (errorArg.result !== undefined) {
|
|
741
|
+
return assertResultArg(errorArg.result);
|
|
742
|
+
}
|
|
743
|
+
if (errorArg.commandError !== undefined) {
|
|
744
|
+
throw errorArg.commandError;
|
|
745
|
+
}
|
|
746
|
+
throw errorArg;
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
const finishDockerQualification = async (
|
|
750
|
+
optionsArg: IReleasePublicationOptions,
|
|
751
|
+
journalArg: IReleaseJournalV2,
|
|
752
|
+
resultArg: IReleaseJournalV2["docker"]["qualification"]["result"],
|
|
753
|
+
stateArg: "verified" | "conflict",
|
|
754
|
+
errorArg: TReleaseErrorCode | null,
|
|
755
|
+
): Promise<IReleaseJournalV2> => {
|
|
756
|
+
const attemptId = journalArg.docker.qualification.attempt?.id;
|
|
757
|
+
if (!attemptId) {
|
|
758
|
+
throw new Error("Docker qualification has no active publication attempt.");
|
|
759
|
+
}
|
|
760
|
+
return (await optionsArg.store.transact(
|
|
761
|
+
journalArg.release.version,
|
|
762
|
+
journalArg.revision,
|
|
763
|
+
(currentArg) => {
|
|
764
|
+
if (currentArg.schemaVersion !== 2) {
|
|
765
|
+
throw new Error("Docker qualification requires journal schema 2.");
|
|
766
|
+
}
|
|
767
|
+
const qualification = currentArg.docker.qualification;
|
|
768
|
+
if (
|
|
769
|
+
qualification.state !== "publishing" ||
|
|
770
|
+
qualification.attempt?.id !== attemptId
|
|
771
|
+
) {
|
|
772
|
+
throw new Error("Docker qualification ownership changed concurrently.");
|
|
773
|
+
}
|
|
774
|
+
qualification.state = stateArg;
|
|
775
|
+
qualification.attempt = null;
|
|
776
|
+
qualification.error = errorArg;
|
|
777
|
+
qualification.result = resultArg;
|
|
778
|
+
if (resultArg) {
|
|
779
|
+
currentArg.docker.promotions = createReleaseDockerPromotions(resultArg);
|
|
780
|
+
}
|
|
781
|
+
return finalizeJournalCompletion(currentArg);
|
|
782
|
+
},
|
|
783
|
+
)) as IReleaseJournalV2;
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
const finishDockerCleanup = async (
|
|
787
|
+
optionsArg: IReleasePublicationOptions,
|
|
788
|
+
journalArg: IReleaseJournalV2,
|
|
789
|
+
resultArg: IReleaseJournalV2["docker"]["cleanup"]["result"],
|
|
790
|
+
): Promise<IReleaseJournalV2> => {
|
|
791
|
+
const attemptId = journalArg.docker.cleanup.attempt?.id;
|
|
792
|
+
if (!attemptId) {
|
|
793
|
+
throw new Error("Docker cleanup has no active publication attempt.");
|
|
794
|
+
}
|
|
795
|
+
return (await optionsArg.store.transact(
|
|
796
|
+
journalArg.release.version,
|
|
797
|
+
journalArg.revision,
|
|
798
|
+
(currentArg) => {
|
|
799
|
+
if (currentArg.schemaVersion !== 2) {
|
|
800
|
+
throw new Error("Docker cleanup requires journal schema 2.");
|
|
801
|
+
}
|
|
802
|
+
const cleanup = currentArg.docker.cleanup;
|
|
803
|
+
if (cleanup.state !== "publishing" || cleanup.attempt?.id !== attemptId) {
|
|
804
|
+
throw new Error("Docker cleanup ownership changed concurrently.");
|
|
805
|
+
}
|
|
806
|
+
cleanup.state = "verified";
|
|
807
|
+
cleanup.attempt = null;
|
|
808
|
+
cleanup.error = null;
|
|
809
|
+
cleanup.result = resultArg;
|
|
810
|
+
return finalizeJournalCompletion(currentArg);
|
|
811
|
+
},
|
|
812
|
+
)) as IReleaseJournalV2;
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
const runClaimedDockerCleanup = async (
|
|
816
|
+
optionsArg: IReleasePublicationOptions,
|
|
817
|
+
journalArg: IReleaseJournalV2,
|
|
818
|
+
): Promise<IReleaseJournalV2> => {
|
|
819
|
+
const client = requireTsdocker(optionsArg);
|
|
820
|
+
const ownerId = journalArg.docker.cleanup.attempt?.id;
|
|
821
|
+
if (!ownerId) {
|
|
822
|
+
throw new Error("Docker cleanup has no active publication attempt.");
|
|
823
|
+
}
|
|
824
|
+
let result: IReleaseJournalV2["docker"]["cleanup"]["result"];
|
|
825
|
+
try {
|
|
826
|
+
result = await client.cleanup(
|
|
827
|
+
journalArg.docker.request,
|
|
828
|
+
getDockerRequestDirectory(optionsArg, journalArg),
|
|
829
|
+
ownerId,
|
|
830
|
+
);
|
|
831
|
+
} catch (error) {
|
|
832
|
+
result = await recoverRequestCleanupOutcome(
|
|
833
|
+
optionsArg,
|
|
834
|
+
journalArg,
|
|
835
|
+
ownerId,
|
|
836
|
+
error,
|
|
837
|
+
(valueArg) =>
|
|
838
|
+
assertReleaseCleanupResult(valueArg, journalArg.docker.request),
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
return finishDockerCleanup(optionsArg, journalArg, result);
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
const executeDockerCleanup = async (
|
|
845
|
+
optionsArg: IReleasePublicationOptions,
|
|
846
|
+
journalArg: IReleaseJournalV2,
|
|
847
|
+
): Promise<IReleaseJournalV2> => {
|
|
848
|
+
let journal = journalArg;
|
|
849
|
+
if (journal.docker.cleanup.state === "verified") return journal;
|
|
850
|
+
const qualificationConflict =
|
|
851
|
+
journal.docker.qualification.state === "conflict" &&
|
|
852
|
+
journal.docker.qualification.error === "destination-conflict" &&
|
|
853
|
+
journal.docker.promotions.length === 0;
|
|
854
|
+
const promotionsVerified =
|
|
855
|
+
journal.docker.promotions.length > 0 &&
|
|
856
|
+
journal.docker.promotions.every(
|
|
857
|
+
(promotionArg) => promotionArg.state === "verified",
|
|
858
|
+
);
|
|
859
|
+
if (!qualificationConflict && !promotionsVerified) {
|
|
860
|
+
throw new Error(
|
|
861
|
+
"Docker candidate cleanup requires verified promotions or a terminal qualification conflict.",
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
if (journal.docker.cleanup.state === "publishing") {
|
|
865
|
+
await reclaimDockerRequestOwnership(
|
|
866
|
+
optionsArg,
|
|
867
|
+
journal,
|
|
868
|
+
journal.docker.cleanup,
|
|
869
|
+
);
|
|
870
|
+
journal = (await finishPublishingTarget(
|
|
871
|
+
optionsArg.store,
|
|
872
|
+
journal,
|
|
873
|
+
{ kind: "docker-cleanup" },
|
|
874
|
+
"failed",
|
|
875
|
+
"attempt-recovery-required",
|
|
876
|
+
)) as IReleaseJournalV2;
|
|
877
|
+
}
|
|
878
|
+
if (journal.docker.cleanup.state === "conflict") {
|
|
879
|
+
throw new Error("Docker cleanup is in conflict.");
|
|
880
|
+
}
|
|
881
|
+
if (!qualificationConflict) {
|
|
882
|
+
await optionsArg.ensurePublicationState(journal.release.mainOid);
|
|
883
|
+
}
|
|
884
|
+
journal = (await claimTarget(optionsArg.store, journal, {
|
|
885
|
+
kind: "docker-cleanup",
|
|
886
|
+
})) as IReleaseJournalV2;
|
|
887
|
+
return runClaimedDockerCleanup(optionsArg, journal);
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
const cleanupIncompleteQualificationPreparation = async (
|
|
891
|
+
optionsArg: IReleasePublicationOptions,
|
|
892
|
+
journalArg: IReleaseJournalV2,
|
|
893
|
+
): Promise<void> => {
|
|
894
|
+
const ownerId = journalArg.docker.qualification.attempt?.id;
|
|
895
|
+
if (!ownerId) {
|
|
896
|
+
throw new Error("Docker qualification has no active publication attempt.");
|
|
897
|
+
}
|
|
898
|
+
const client = requireTsdocker(optionsArg);
|
|
899
|
+
try {
|
|
900
|
+
await client.cleanup(
|
|
901
|
+
journalArg.docker.request,
|
|
902
|
+
getDockerRequestDirectory(optionsArg, journalArg),
|
|
903
|
+
ownerId,
|
|
904
|
+
);
|
|
905
|
+
} catch (error) {
|
|
906
|
+
await recoverRequestCleanupOutcome(
|
|
907
|
+
optionsArg,
|
|
908
|
+
journalArg,
|
|
909
|
+
ownerId,
|
|
910
|
+
error,
|
|
911
|
+
(valueArg) =>
|
|
912
|
+
assertReleaseCleanupResult(valueArg, journalArg.docker.request),
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
const runClaimedDockerQualification = async (
|
|
918
|
+
optionsArg: IReleasePublicationOptions,
|
|
919
|
+
journalArg: IReleaseJournalV2,
|
|
920
|
+
incompletePreparationCleanupAvailableArg: boolean,
|
|
921
|
+
): Promise<IReleaseJournalV2> => {
|
|
922
|
+
const client = requireTsdocker(optionsArg);
|
|
923
|
+
const ownerId = journalArg.docker.qualification.attempt?.id;
|
|
924
|
+
if (!ownerId) {
|
|
925
|
+
throw new Error("Docker qualification has no active publication attempt.");
|
|
926
|
+
}
|
|
927
|
+
let result: IReleaseJournalV2["docker"]["qualification"]["result"];
|
|
928
|
+
try {
|
|
929
|
+
result = await client.qualify(
|
|
930
|
+
journalArg.docker.request,
|
|
931
|
+
getDockerRequestDirectory(optionsArg, journalArg),
|
|
932
|
+
ownerId,
|
|
933
|
+
);
|
|
934
|
+
} catch (error) {
|
|
935
|
+
try {
|
|
936
|
+
result = await recoverRequestCleanupOutcome(
|
|
937
|
+
optionsArg,
|
|
938
|
+
journalArg,
|
|
939
|
+
ownerId,
|
|
940
|
+
error,
|
|
941
|
+
(valueArg) =>
|
|
942
|
+
assertReleaseQualificationResult(valueArg, journalArg.docker.request),
|
|
943
|
+
);
|
|
944
|
+
} catch (commandError) {
|
|
945
|
+
if (
|
|
946
|
+
commandError instanceof TsdockerReleaseCommandError &&
|
|
947
|
+
commandError.code === "DESTINATION_DIGEST_CONFLICT"
|
|
948
|
+
) {
|
|
949
|
+
return finishDockerQualification(
|
|
950
|
+
optionsArg,
|
|
951
|
+
journalArg,
|
|
952
|
+
null,
|
|
953
|
+
"conflict",
|
|
954
|
+
"destination-conflict",
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
if (
|
|
958
|
+
incompletePreparationCleanupAvailableArg &&
|
|
959
|
+
commandError instanceof TsdockerReleaseCommandError &&
|
|
960
|
+
commandError.code === "RECOVERY_REQUIRED"
|
|
961
|
+
) {
|
|
962
|
+
await cleanupIncompleteQualificationPreparation(optionsArg, journalArg);
|
|
963
|
+
let journal = (await finishPublishingTarget(
|
|
964
|
+
optionsArg.store,
|
|
965
|
+
journalArg,
|
|
966
|
+
{ kind: "docker-qualification" },
|
|
967
|
+
"failed",
|
|
968
|
+
"attempt-recovery-required",
|
|
969
|
+
)) as IReleaseJournalV2;
|
|
970
|
+
journal = (await claimTarget(optionsArg.store, journal, {
|
|
971
|
+
kind: "docker-qualification",
|
|
972
|
+
})) as IReleaseJournalV2;
|
|
973
|
+
return runClaimedDockerQualification(optionsArg, journal, false);
|
|
974
|
+
}
|
|
975
|
+
throw commandError;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return finishDockerQualification(
|
|
979
|
+
optionsArg,
|
|
980
|
+
journalArg,
|
|
981
|
+
result,
|
|
982
|
+
"verified",
|
|
983
|
+
null,
|
|
984
|
+
);
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
const executeDockerQualification = async (
|
|
988
|
+
optionsArg: IReleasePublicationOptions,
|
|
989
|
+
journalArg: IReleaseJournalV2,
|
|
990
|
+
): Promise<IReleaseJournalV2> => {
|
|
991
|
+
let journal = journalArg;
|
|
992
|
+
if (journal.docker.qualification.state === "verified") {
|
|
993
|
+
return journal;
|
|
994
|
+
}
|
|
995
|
+
if (journal.docker.qualification.state === "conflict") {
|
|
996
|
+
await executeDockerCleanup(optionsArg, journal);
|
|
997
|
+
throw new Error(
|
|
998
|
+
"Docker qualification destination conflict is terminal; the candidate was cleaned and Git/npm publication was not attempted.",
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
if (journal.docker.qualification.state === "publishing") {
|
|
1002
|
+
await reclaimDockerRequestOwnership(
|
|
1003
|
+
optionsArg,
|
|
1004
|
+
journal,
|
|
1005
|
+
journal.docker.qualification,
|
|
1006
|
+
);
|
|
1007
|
+
journal = (await finishPublishingTarget(
|
|
1008
|
+
optionsArg.store,
|
|
1009
|
+
journal,
|
|
1010
|
+
{ kind: "docker-qualification" },
|
|
1011
|
+
"failed",
|
|
1012
|
+
"attempt-recovery-required",
|
|
1013
|
+
)) as IReleaseJournalV2;
|
|
1014
|
+
}
|
|
1015
|
+
await optionsArg.ensurePublicationState(
|
|
1016
|
+
journal.git.expectedRemoteMainOid || undefined,
|
|
1017
|
+
);
|
|
1018
|
+
journal = (await claimTarget(optionsArg.store, journal, {
|
|
1019
|
+
kind: "docker-qualification",
|
|
1020
|
+
})) as IReleaseJournalV2;
|
|
1021
|
+
journal = await runClaimedDockerQualification(optionsArg, journal, true);
|
|
1022
|
+
if (journal.docker.qualification.state === "conflict") {
|
|
1023
|
+
await executeDockerCleanup(optionsArg, journal);
|
|
1024
|
+
throw new Error(
|
|
1025
|
+
"Docker qualification destination conflict is terminal; the candidate was cleaned and Git/npm publication was not attempted.",
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
return journal;
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
const recordDockerPromotion = async (
|
|
1032
|
+
optionsArg: IReleasePublicationOptions,
|
|
1033
|
+
journalArg: IReleaseJournalV2,
|
|
1034
|
+
promotionIdArg: string,
|
|
1035
|
+
expectedStatesArg: TReleaseTargetState[],
|
|
1036
|
+
expectedAttemptIdArg: string | null,
|
|
1037
|
+
stateArg: "verified" | "failed" | "conflict",
|
|
1038
|
+
errorArg: TReleaseErrorCode | null,
|
|
1039
|
+
evidenceArg?: TReleasePromotionEvidence,
|
|
1040
|
+
): Promise<IReleaseJournalV2> =>
|
|
1041
|
+
(await optionsArg.store.transact(
|
|
1042
|
+
journalArg.release.version,
|
|
1043
|
+
journalArg.revision,
|
|
1044
|
+
(currentArg) => {
|
|
1045
|
+
if (currentArg.schemaVersion !== 2) {
|
|
1046
|
+
throw new Error("Docker promotion requires journal schema 2.");
|
|
1047
|
+
}
|
|
1048
|
+
const promotion = currentArg.docker.promotions.find(
|
|
1049
|
+
(promotionArg) => promotionArg.promotionId === promotionIdArg,
|
|
1050
|
+
);
|
|
1051
|
+
if (
|
|
1052
|
+
!promotion ||
|
|
1053
|
+
!expectedStatesArg.includes(promotion.state) ||
|
|
1054
|
+
(promotion.attempt?.id || null) !== expectedAttemptIdArg
|
|
1055
|
+
) {
|
|
1056
|
+
throw new Error("Docker promotion ownership changed concurrently.");
|
|
1057
|
+
}
|
|
1058
|
+
promotion.state = stateArg;
|
|
1059
|
+
promotion.attempt = null;
|
|
1060
|
+
promotion.error = errorArg;
|
|
1061
|
+
if (evidenceArg) promotion.evidence.push(evidenceArg);
|
|
1062
|
+
return finalizeJournalCompletion(currentArg);
|
|
1063
|
+
},
|
|
1064
|
+
)) as IReleaseJournalV2;
|
|
1065
|
+
|
|
1066
|
+
const probeDockerPromotion = async (
|
|
1067
|
+
optionsArg: IReleasePublicationOptions,
|
|
1068
|
+
journalArg: IReleaseJournalV2,
|
|
1069
|
+
promotionIdArg: string,
|
|
1070
|
+
ownerIdArg?: string,
|
|
1071
|
+
): Promise<IReleaseProbeResult> => {
|
|
1072
|
+
const context = getReleaseDockerPromotionContext(journalArg, promotionIdArg);
|
|
1073
|
+
try {
|
|
1074
|
+
return await requireTsdocker(optionsArg).probe(
|
|
1075
|
+
journalArg.docker.request,
|
|
1076
|
+
context,
|
|
1077
|
+
ownerIdArg
|
|
1078
|
+
? getDockerRequestDirectory(optionsArg, journalArg)
|
|
1079
|
+
: undefined,
|
|
1080
|
+
ownerIdArg,
|
|
1081
|
+
);
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
if (!ownerIdArg) throw error;
|
|
1084
|
+
return recoverRequestCleanupOutcome(
|
|
1085
|
+
optionsArg,
|
|
1086
|
+
journalArg,
|
|
1087
|
+
ownerIdArg,
|
|
1088
|
+
error,
|
|
1089
|
+
(valueArg) =>
|
|
1090
|
+
assertReleaseProbeResult(valueArg, journalArg.docker.request, context),
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
const executeDockerPromotion = async (
|
|
1096
|
+
optionsArg: IReleasePublicationOptions,
|
|
1097
|
+
journalArg: IReleaseJournalV2,
|
|
1098
|
+
promotionIdArg: string,
|
|
1099
|
+
): Promise<IReleaseJournalV2> => {
|
|
1100
|
+
let journal = journalArg;
|
|
1101
|
+
let promotion = journal.docker.promotions.find(
|
|
1102
|
+
(promotionArg) => promotionArg.promotionId === promotionIdArg,
|
|
1103
|
+
);
|
|
1104
|
+
if (!promotion) {
|
|
1105
|
+
throw new Error(`Docker promotion ${promotionIdArg} is not journaled.`);
|
|
1106
|
+
}
|
|
1107
|
+
if (promotion.state === "conflict") {
|
|
1108
|
+
throw new Error(`Docker promotion ${promotionIdArg} is in conflict.`);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
if (promotion.state === "verified") {
|
|
1112
|
+
const probe = await probeDockerPromotion(
|
|
1113
|
+
optionsArg,
|
|
1114
|
+
journal,
|
|
1115
|
+
promotionIdArg,
|
|
1116
|
+
);
|
|
1117
|
+
const classification = classifyReleasePromotionEvidence(probe);
|
|
1118
|
+
if (classification === "exact") return journal;
|
|
1119
|
+
if (classification === "inconclusive") {
|
|
1120
|
+
throw new Error(
|
|
1121
|
+
`Previously verified Docker promotion cannot currently be verified: ${promotionIdArg}`,
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
await recordDockerPromotion(
|
|
1125
|
+
optionsArg,
|
|
1126
|
+
journal,
|
|
1127
|
+
promotionIdArg,
|
|
1128
|
+
["verified"],
|
|
1129
|
+
null,
|
|
1130
|
+
"conflict",
|
|
1131
|
+
"destination-conflict",
|
|
1132
|
+
probe,
|
|
1133
|
+
);
|
|
1134
|
+
throw new Error(
|
|
1135
|
+
`Previously verified Docker promotion drifted: ${promotionIdArg}`,
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
let recoveredPublishingAttempt = false;
|
|
1140
|
+
if (promotion.state === "publishing") {
|
|
1141
|
+
await reclaimDockerRequestOwnership(optionsArg, journal, promotion);
|
|
1142
|
+
recoveredPublishingAttempt = true;
|
|
1143
|
+
} else {
|
|
1144
|
+
await optionsArg.ensurePublicationState(journal.release.mainOid);
|
|
1145
|
+
journal = (await claimTarget(optionsArg.store, journal, {
|
|
1146
|
+
kind: "docker-promotion",
|
|
1147
|
+
promotionId: promotionIdArg,
|
|
1148
|
+
})) as IReleaseJournalV2;
|
|
1149
|
+
promotion = journal.docker.promotions.find(
|
|
1150
|
+
(promotionArg) => promotionArg.promotionId === promotionIdArg,
|
|
1151
|
+
)!;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
let attemptId = promotion.attempt?.id;
|
|
1155
|
+
if (!attemptId) {
|
|
1156
|
+
throw new Error("Docker promotion has no active publication attempt.");
|
|
1157
|
+
}
|
|
1158
|
+
let probe: IReleaseProbeResult;
|
|
1159
|
+
try {
|
|
1160
|
+
probe = await probeDockerPromotion(
|
|
1161
|
+
optionsArg,
|
|
1162
|
+
journal,
|
|
1163
|
+
promotionIdArg,
|
|
1164
|
+
attemptId,
|
|
1165
|
+
);
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
throw error;
|
|
1168
|
+
}
|
|
1169
|
+
let classification = classifyReleasePromotionEvidence(probe);
|
|
1170
|
+
if (classification === "exact") {
|
|
1171
|
+
return recordDockerPromotion(
|
|
1172
|
+
optionsArg,
|
|
1173
|
+
journal,
|
|
1174
|
+
promotionIdArg,
|
|
1175
|
+
["publishing"],
|
|
1176
|
+
attemptId,
|
|
1177
|
+
"verified",
|
|
1178
|
+
null,
|
|
1179
|
+
probe,
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
if (classification === "conflict") {
|
|
1183
|
+
await recordDockerPromotion(
|
|
1184
|
+
optionsArg,
|
|
1185
|
+
journal,
|
|
1186
|
+
promotionIdArg,
|
|
1187
|
+
["publishing"],
|
|
1188
|
+
attemptId,
|
|
1189
|
+
"conflict",
|
|
1190
|
+
"destination-conflict",
|
|
1191
|
+
probe,
|
|
1192
|
+
);
|
|
1193
|
+
throw new Error(`Docker promotion conflicted: ${promotionIdArg}`);
|
|
1194
|
+
}
|
|
1195
|
+
if (classification === "inconclusive") {
|
|
1196
|
+
if (!recoveredPublishingAttempt) {
|
|
1197
|
+
await recordDockerPromotion(
|
|
1198
|
+
optionsArg,
|
|
1199
|
+
journal,
|
|
1200
|
+
promotionIdArg,
|
|
1201
|
+
["publishing"],
|
|
1202
|
+
attemptId,
|
|
1203
|
+
"failed",
|
|
1204
|
+
"verification-inconclusive",
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
throw new Error(
|
|
1208
|
+
`Docker promotion probe is inconclusive: ${promotionIdArg}`,
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
if (recoveredPublishingAttempt) {
|
|
1212
|
+
journal = (await finishPublishingTarget(
|
|
1213
|
+
optionsArg.store,
|
|
1214
|
+
journal,
|
|
1215
|
+
{ kind: "docker-promotion", promotionId: promotionIdArg },
|
|
1216
|
+
"failed",
|
|
1217
|
+
"attempt-recovery-required",
|
|
1218
|
+
)) as IReleaseJournalV2;
|
|
1219
|
+
promotion = journal.docker.promotions.find(
|
|
1220
|
+
(promotionArg) => promotionArg.promotionId === promotionIdArg,
|
|
1221
|
+
)!;
|
|
1222
|
+
await optionsArg.ensurePublicationState(journal.release.mainOid);
|
|
1223
|
+
journal = (await claimTarget(optionsArg.store, journal, {
|
|
1224
|
+
kind: "docker-promotion",
|
|
1225
|
+
promotionId: promotionIdArg,
|
|
1226
|
+
})) as IReleaseJournalV2;
|
|
1227
|
+
promotion = journal.docker.promotions.find(
|
|
1228
|
+
(promotionArg) => promotionArg.promotionId === promotionIdArg,
|
|
1229
|
+
)!;
|
|
1230
|
+
attemptId = promotion.attempt!.id;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
const context = getReleaseDockerPromotionContext(journal, promotionIdArg);
|
|
1234
|
+
let result: TReleasePromotionEvidence;
|
|
1235
|
+
try {
|
|
1236
|
+
result = await requireTsdocker(optionsArg).promote(
|
|
1237
|
+
journal.docker.request,
|
|
1238
|
+
context,
|
|
1239
|
+
getDockerRequestDirectory(optionsArg, journal),
|
|
1240
|
+
attemptId,
|
|
1241
|
+
);
|
|
1242
|
+
} catch (error) {
|
|
1243
|
+
let commandError = error;
|
|
1244
|
+
if (error instanceof TsdockerReleaseRequestCleanupError) {
|
|
1245
|
+
try {
|
|
1246
|
+
result = await recoverRequestCleanupOutcome(
|
|
1247
|
+
optionsArg,
|
|
1248
|
+
journal,
|
|
1249
|
+
attemptId,
|
|
1250
|
+
error,
|
|
1251
|
+
(valueArg) =>
|
|
1252
|
+
assertReleasePromotionResult(
|
|
1253
|
+
valueArg,
|
|
1254
|
+
journal.docker.request,
|
|
1255
|
+
context,
|
|
1256
|
+
),
|
|
1257
|
+
);
|
|
1258
|
+
commandError = undefined;
|
|
1259
|
+
} catch (recoveredError) {
|
|
1260
|
+
commandError = recoveredError;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
if (
|
|
1264
|
+
commandError instanceof TsdockerReleaseRequestCleanupError &&
|
|
1265
|
+
commandError.reclamationError !== undefined
|
|
1266
|
+
) {
|
|
1267
|
+
throw commandError;
|
|
1268
|
+
}
|
|
1269
|
+
if (commandError === undefined) {
|
|
1270
|
+
classification = classifyReleasePromotionEvidence(result!);
|
|
1271
|
+
} else {
|
|
1272
|
+
try {
|
|
1273
|
+
const reconciliation = await probeDockerPromotion(
|
|
1274
|
+
optionsArg,
|
|
1275
|
+
journal,
|
|
1276
|
+
promotionIdArg,
|
|
1277
|
+
attemptId,
|
|
1278
|
+
);
|
|
1279
|
+
const reconciled = classifyReleasePromotionEvidence(reconciliation);
|
|
1280
|
+
if (reconciled === "exact") {
|
|
1281
|
+
return recordDockerPromotion(
|
|
1282
|
+
optionsArg,
|
|
1283
|
+
journal,
|
|
1284
|
+
promotionIdArg,
|
|
1285
|
+
["publishing"],
|
|
1286
|
+
attemptId,
|
|
1287
|
+
"verified",
|
|
1288
|
+
null,
|
|
1289
|
+
reconciliation,
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
if (reconciled === "conflict") {
|
|
1293
|
+
await recordDockerPromotion(
|
|
1294
|
+
optionsArg,
|
|
1295
|
+
journal,
|
|
1296
|
+
promotionIdArg,
|
|
1297
|
+
["publishing"],
|
|
1298
|
+
attemptId,
|
|
1299
|
+
"conflict",
|
|
1300
|
+
"destination-conflict",
|
|
1301
|
+
reconciliation,
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
} catch {
|
|
1305
|
+
// Preserve publishing ownership when post-command reconciliation is unavailable.
|
|
1306
|
+
}
|
|
1307
|
+
throw commandError;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
classification = classifyReleasePromotionEvidence(result!);
|
|
1312
|
+
if (classification === "exact") {
|
|
1313
|
+
return recordDockerPromotion(
|
|
1314
|
+
optionsArg,
|
|
1315
|
+
journal,
|
|
1316
|
+
promotionIdArg,
|
|
1317
|
+
["publishing"],
|
|
1318
|
+
attemptId,
|
|
1319
|
+
"verified",
|
|
1320
|
+
null,
|
|
1321
|
+
result!,
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1324
|
+
if (classification === "conflict") {
|
|
1325
|
+
await recordDockerPromotion(
|
|
1326
|
+
optionsArg,
|
|
1327
|
+
journal,
|
|
1328
|
+
promotionIdArg,
|
|
1329
|
+
["publishing"],
|
|
1330
|
+
attemptId,
|
|
1331
|
+
"conflict",
|
|
1332
|
+
"destination-conflict",
|
|
1333
|
+
result!,
|
|
1334
|
+
);
|
|
1335
|
+
throw new Error(`Docker promotion conflicted: ${promotionIdArg}`);
|
|
1336
|
+
}
|
|
1337
|
+
throw new Error(
|
|
1338
|
+
`Docker promotion outcome is inconclusive: ${promotionIdArg}`,
|
|
1339
|
+
);
|
|
1340
|
+
};
|
|
1341
|
+
|
|
623
1342
|
export const executeReleasePublication = async (
|
|
624
1343
|
optionsArg: IReleasePublicationOptions,
|
|
625
|
-
): Promise<
|
|
1344
|
+
): Promise<TReleaseJournal> => {
|
|
626
1345
|
let journal = optionsArg.journal;
|
|
1346
|
+
if (journal.schemaVersion === 2) {
|
|
1347
|
+
journal = await executeDockerQualification(optionsArg, journal);
|
|
1348
|
+
}
|
|
627
1349
|
journal = await executeGitTarget(optionsArg, journal);
|
|
628
1350
|
const expectedRemoteMainOid =
|
|
629
1351
|
journal.git.state === "verified"
|
|
@@ -631,11 +1353,20 @@ export const executeReleasePublication = async (
|
|
|
631
1353
|
: journal.git.expectedRemoteMainOid || undefined;
|
|
632
1354
|
for (const registry of journal.npm.registries) {
|
|
633
1355
|
await optionsArg.ensurePublicationState(expectedRemoteMainOid);
|
|
634
|
-
journal = await executeNpmRegistry(
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
1356
|
+
journal = await executeNpmRegistry(optionsArg, journal, registry.registry);
|
|
1357
|
+
}
|
|
1358
|
+
if (
|
|
1359
|
+
journal.schemaVersion === 2 &&
|
|
1360
|
+
journal.docker.cleanup.state !== "verified"
|
|
1361
|
+
) {
|
|
1362
|
+
for (const promotion of journal.docker.promotions) {
|
|
1363
|
+
journal = await executeDockerPromotion(
|
|
1364
|
+
optionsArg,
|
|
1365
|
+
journal,
|
|
1366
|
+
promotion.promotionId,
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
journal = await executeDockerCleanup(optionsArg, journal);
|
|
639
1370
|
}
|
|
640
1371
|
return journal;
|
|
641
1372
|
};
|