@kungfu-tech/buildchain 4.0.1 → 4.0.2-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/architecture/agent-change-map.md +7 -5
- package/architecture/ci-lane-change-budget.json +76 -8
- package/architecture/internal-capabilities.json +4 -1
- package/architecture/maintainability-debt.json +45 -119
- package/architecture/maintainability-policy.json +13 -8
- package/architecture/release-tail-contract-inventory.json +4 -4
- package/architecture/v3-core-mechanism-inventory.json +2 -0
- package/architecture/v3-v4-live-capability-inventory.json +17 -17
- package/architecture/v4-release-invocation-fixtures.json +119 -0
- package/architecture/v4-release-topology.json +238 -0
- package/contracts/buildchain-v2-residuals-v1.json +0 -9
- package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
- package/contracts/v4-release-invocation-v1.schema.json +86 -0
- package/dist/site/buildchain-contract.json +9 -9
- package/dist/site/buildchain-site.json +9 -9
- package/dist/site/kfd-claims.json +9 -7
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +1 -1
- package/dist/site/node-api-registry.json +3 -3
- package/dist/site/page-registry.json +4 -4
- package/dist/site/public-surface-audit.json +23 -10
- package/dist/site/publication-authority-registry.json +2 -3
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +5 -5
- package/dist/site/workflow-registry.json +19 -11
- package/docs/dev-delivery-warrant.md +21 -0
- package/docs/node-api-reference.md +3 -3
- package/package.json +2 -2
- package/packages/core/dev-delivery-candidate-identity.js +45 -17
- package/packages/core/dev-delivery-provider-heartbeat.js +22 -6
- package/packages/core/dev-delivery-warrant-legacy-recovery.js +246 -0
- package/packages/core/dev-delivery-warrant-state.js +12 -28
- package/packages/core/v4-canonical-contracts.js +8 -0
- package/packages/core/v4-floating-consumer-policy.js +4 -1
- package/packages/core/v4-release-invocation.js +356 -0
- package/scripts/audit-publication-control-plane.mjs +0 -1
- package/scripts/check-inventory.mjs +27 -59
- package/scripts/check-maintainability.mjs +13 -5
- package/scripts/check-v3-v4-capability-inventory.mjs +3 -61
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +10 -20
- package/scripts/check-v4-release-topology.mjs +237 -0
- package/scripts/dev-delivery-warrant-options.mjs +15 -4
- package/scripts/dev-delivery-warrant.mjs +29 -7
- package/scripts/generate-channel-promotion-workflow.mjs +12 -54
- package/scripts/v3-v4-capability-catalog.mjs +107 -0
- package/scripts/v4-declarative-promotion-admission.mjs +4 -1
- package/scripts/capture-package-release-propagation.mjs +0 -263
- package/scripts/publication-commit-evidence.mjs +0 -444
- package/scripts/publish-github-artifact-attestation-evidence.mjs +0 -201
- package/scripts/stage-github-artifact-attestation-inputs.mjs +0 -65
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import {
|
|
2
|
+
devDeliveryClone as clone,
|
|
3
|
+
devDeliveryContentRoot,
|
|
4
|
+
devDeliveryExactRoot as exactRoot,
|
|
5
|
+
devDeliveryExactSha as exactSha,
|
|
6
|
+
devDeliveryPositiveInteger as positiveInteger,
|
|
7
|
+
devDeliveryText as text,
|
|
8
|
+
devDeliveryTimestamp as timestamp,
|
|
9
|
+
} from "./dev-delivery-common.js";
|
|
10
|
+
import {
|
|
11
|
+
TERMINAL_STATES,
|
|
12
|
+
normalizeDevDeliveryQueue,
|
|
13
|
+
} from "./dev-delivery-warrant-state.js";
|
|
14
|
+
|
|
15
|
+
export const LEGACY_TERMINAL_RECOVERY_REQUEST_SCHEMA =
|
|
16
|
+
"kungfu.buildchain.legacy-terminal-recovery-request/v1";
|
|
17
|
+
export const LEGACY_HOSTED_TERMINAL_EVIDENCE_SCHEMA =
|
|
18
|
+
"kungfu.buildchain.legacy-hosted-terminal-evidence/v1";
|
|
19
|
+
export const LEGACY_TERMINAL_RECOVERY_RECEIPT_SCHEMA =
|
|
20
|
+
"kungfu.buildchain.legacy-terminal-recovery-receipt/v1";
|
|
21
|
+
|
|
22
|
+
function legacyLiveNativeCandidate(candidate) {
|
|
23
|
+
return (
|
|
24
|
+
!TERMINAL_STATES.has(candidate.status) &&
|
|
25
|
+
candidate.deliveryClass !== "non-native-fast" &&
|
|
26
|
+
(!candidate.environmentRoot || !candidate.nativeCommandContract)
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeProviderJob(input, runAttempt) {
|
|
31
|
+
const job = {
|
|
32
|
+
id: positiveInteger(input?.id, "provider job id"),
|
|
33
|
+
name: text(input?.name),
|
|
34
|
+
runAttempt: positiveInteger(input?.runAttempt, "provider job runAttempt"),
|
|
35
|
+
status: text(input?.status).toLowerCase(),
|
|
36
|
+
conclusion: text(input?.conclusion).toLowerCase(),
|
|
37
|
+
completedAt: timestamp(input?.completedAt, "provider job completedAt"),
|
|
38
|
+
};
|
|
39
|
+
if (job.runAttempt !== runAttempt) {
|
|
40
|
+
throw new Error("legacy provider job run attempt mismatch");
|
|
41
|
+
}
|
|
42
|
+
if (job.status !== "completed" || !job.conclusion) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
"legacy recovery requires every provider job to be terminal",
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return job;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeEvidence(input) {
|
|
51
|
+
const runAttempt = positiveInteger(input?.runAttempt, "runAttempt");
|
|
52
|
+
if (!Array.isArray(input?.jobs) || input.jobs.length === 0) {
|
|
53
|
+
throw new Error("legacy terminal evidence requires provider jobs");
|
|
54
|
+
}
|
|
55
|
+
const jobs = input.jobs
|
|
56
|
+
.map((job) => normalizeProviderJob(job, runAttempt))
|
|
57
|
+
.sort((left, right) => left.id - right.id);
|
|
58
|
+
if (new Set(jobs.map((job) => job.id)).size !== jobs.length) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
"legacy terminal evidence contains duplicate provider jobs",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (!jobs.some((job) => job.conclusion === "failure")) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
"legacy failed run evidence requires a failed provider job",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const runUpdatedAt = timestamp(input?.runUpdatedAt, "runUpdatedAt");
|
|
69
|
+
if (
|
|
70
|
+
jobs.some((job) => Date.parse(job.completedAt) > Date.parse(runUpdatedAt))
|
|
71
|
+
) {
|
|
72
|
+
throw new Error("legacy provider job completion follows the run readback");
|
|
73
|
+
}
|
|
74
|
+
const body = {
|
|
75
|
+
schema: text(input?.schema),
|
|
76
|
+
candidateId: exactRoot(input?.candidateId, "candidateId"),
|
|
77
|
+
pullRequestNumber: positiveInteger(
|
|
78
|
+
input?.pullRequestNumber,
|
|
79
|
+
"pullRequestNumber",
|
|
80
|
+
),
|
|
81
|
+
sourceHead: exactSha(input?.sourceHead, "sourceHead"),
|
|
82
|
+
sourceWorkflowRunId: positiveInteger(
|
|
83
|
+
input?.sourceWorkflowRunId,
|
|
84
|
+
"sourceWorkflowRunId",
|
|
85
|
+
),
|
|
86
|
+
runAttempt,
|
|
87
|
+
runStatus: text(input?.runStatus).toLowerCase(),
|
|
88
|
+
runConclusion: text(input?.runConclusion).toLowerCase(),
|
|
89
|
+
runUpdatedAt,
|
|
90
|
+
totalJobCount: jobs.length,
|
|
91
|
+
nonterminalJobCount: 0,
|
|
92
|
+
workerTerminationProven: true,
|
|
93
|
+
jobs,
|
|
94
|
+
reason: text(input?.reason),
|
|
95
|
+
};
|
|
96
|
+
if (body.schema !== LEGACY_HOSTED_TERMINAL_EVIDENCE_SCHEMA) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`legacy terminal evidence must use ${LEGACY_HOSTED_TERMINAL_EVIDENCE_SCHEMA}`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
if (
|
|
102
|
+
body.runStatus !== "completed" ||
|
|
103
|
+
body.runConclusion !== "failure" ||
|
|
104
|
+
!body.reason
|
|
105
|
+
) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
"legacy terminal evidence requires one completed failed run with zero nonterminal jobs and proven worker termination",
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const evidenceRoot = devDeliveryContentRoot(body);
|
|
111
|
+
if (exactRoot(input?.evidenceRoot, "evidenceRoot") !== evidenceRoot) {
|
|
112
|
+
throw new Error("legacy terminal evidence root mismatch");
|
|
113
|
+
}
|
|
114
|
+
return { ...body, evidenceRoot };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function assertExactCoverage(candidates, evidence) {
|
|
118
|
+
const expected = candidates.map((candidate) => candidate.candidateId).sort();
|
|
119
|
+
const observed = evidence.map((entry) => entry.candidateId).sort();
|
|
120
|
+
if (
|
|
121
|
+
expected.length !== observed.length ||
|
|
122
|
+
expected.some((candidateId, index) => candidateId !== observed[index])
|
|
123
|
+
) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
"legacy terminal recovery must cover every live legacy native candidate exactly once",
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function recoverLegacyTerminalDevDeliveryQueue(
|
|
131
|
+
queueInput,
|
|
132
|
+
requestInput,
|
|
133
|
+
{ now = new Date().toISOString() } = {},
|
|
134
|
+
) {
|
|
135
|
+
const currentTime = timestamp(now, "now");
|
|
136
|
+
const request = requestInput || {};
|
|
137
|
+
if (request.schema !== LEGACY_TERMINAL_RECOVERY_REQUEST_SCHEMA) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`legacy terminal recovery must use ${LEGACY_TERMINAL_RECOVERY_REQUEST_SCHEMA}`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const before = normalizeDevDeliveryQueue(queueInput, {
|
|
143
|
+
allowLegacyV3Readback: true,
|
|
144
|
+
});
|
|
145
|
+
const expectedOldStateRoot = exactRoot(
|
|
146
|
+
request.expectedOldStateRoot,
|
|
147
|
+
"expectedOldStateRoot",
|
|
148
|
+
);
|
|
149
|
+
if (expectedOldStateRoot !== before.stateRoot) {
|
|
150
|
+
throw new Error("legacy terminal recovery expected-old state drift");
|
|
151
|
+
}
|
|
152
|
+
const legacyCandidates = before.candidates.filter(legacyLiveNativeCandidate);
|
|
153
|
+
if (legacyCandidates.length === 0) {
|
|
154
|
+
throw new Error("legacy terminal recovery found no live legacy candidate");
|
|
155
|
+
}
|
|
156
|
+
if (!Array.isArray(request.evidence)) {
|
|
157
|
+
throw new Error("legacy terminal recovery evidence must be an array");
|
|
158
|
+
}
|
|
159
|
+
const evidence = request.evidence.map(normalizeEvidence);
|
|
160
|
+
if (
|
|
161
|
+
new Set(evidence.map((entry) => entry.candidateId)).size !== evidence.length
|
|
162
|
+
) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
"legacy terminal recovery evidence contains duplicate candidates",
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
assertExactCoverage(legacyCandidates, evidence);
|
|
168
|
+
|
|
169
|
+
const queue = clone(before);
|
|
170
|
+
delete queue.stateRoot;
|
|
171
|
+
const transitions = [];
|
|
172
|
+
for (const entry of evidence) {
|
|
173
|
+
const candidate = queue.candidates.find(
|
|
174
|
+
(row) => row.candidateId === entry.candidateId,
|
|
175
|
+
);
|
|
176
|
+
if (
|
|
177
|
+
!candidate ||
|
|
178
|
+
candidate.pullRequestNumber !== entry.pullRequestNumber ||
|
|
179
|
+
candidate.sourceHead !== entry.sourceHead ||
|
|
180
|
+
candidate.sourceWorkflowRunId !== entry.sourceWorkflowRunId
|
|
181
|
+
) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
"legacy terminal evidence does not match candidate identity",
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
const active = queue.activeWarrant?.candidateId === candidate.candidateId;
|
|
187
|
+
if (!active && candidate.status !== "queued") {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`legacy candidate status ${candidate.status} requires the exact active Warrant`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
const priorStatus = candidate.status;
|
|
193
|
+
candidate.status = "terminal-failure";
|
|
194
|
+
candidate.updatedAt = currentTime;
|
|
195
|
+
candidate.terminal = {
|
|
196
|
+
outcome: "terminal-failure",
|
|
197
|
+
reason: entry.reason,
|
|
198
|
+
evidenceRoot: entry.evidenceRoot,
|
|
199
|
+
authority: "legacy-hosted-native-terminal-recovery",
|
|
200
|
+
sourceWorkflowRunId: entry.sourceWorkflowRunId,
|
|
201
|
+
runAttempt: entry.runAttempt,
|
|
202
|
+
workerTerminationProven: true,
|
|
203
|
+
closedAt: currentTime,
|
|
204
|
+
...(active
|
|
205
|
+
? {
|
|
206
|
+
fencingToken: queue.activeWarrant.fencingToken,
|
|
207
|
+
leaseGeneration: queue.activeWarrant.generation,
|
|
208
|
+
}
|
|
209
|
+
: {}),
|
|
210
|
+
};
|
|
211
|
+
transitions.push({
|
|
212
|
+
candidateId: candidate.candidateId,
|
|
213
|
+
pullRequestNumber: candidate.pullRequestNumber,
|
|
214
|
+
sourceHead: candidate.sourceHead,
|
|
215
|
+
priorStatus,
|
|
216
|
+
activeWarrant: active,
|
|
217
|
+
outcome: "terminal-failure",
|
|
218
|
+
evidenceRoot: entry.evidenceRoot,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
queue.activeWarrant = null;
|
|
222
|
+
queue.generation += 1;
|
|
223
|
+
queue.updatedAt = currentTime;
|
|
224
|
+
queue.stateRoot = devDeliveryContentRoot(queue);
|
|
225
|
+
const after = normalizeDevDeliveryQueue(queue);
|
|
226
|
+
const requestBody = {
|
|
227
|
+
schema: LEGACY_TERMINAL_RECOVERY_REQUEST_SCHEMA,
|
|
228
|
+
expectedOldStateRoot,
|
|
229
|
+
evidence,
|
|
230
|
+
};
|
|
231
|
+
const requestRoot = devDeliveryContentRoot(requestBody);
|
|
232
|
+
const receipt = {
|
|
233
|
+
schema: LEGACY_TERMINAL_RECOVERY_RECEIPT_SCHEMA,
|
|
234
|
+
action: "legacy-terminal-recovery",
|
|
235
|
+
expectedOldStateRoot,
|
|
236
|
+
nextStateRoot: after.stateRoot,
|
|
237
|
+
requestRoot,
|
|
238
|
+
transitions,
|
|
239
|
+
nextAction: "Select the next strictly valid queued candidate, if any.",
|
|
240
|
+
};
|
|
241
|
+
return {
|
|
242
|
+
queue: after,
|
|
243
|
+
receipt,
|
|
244
|
+
receiptRoot: devDeliveryContentRoot(receipt),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
import {
|
|
13
13
|
chainedDevDeliveryAttemptInput,
|
|
14
14
|
createDevDeliveryCandidateIdentity,
|
|
15
|
+
EXACT_DEV_DELIVERY_PROOF_FIELDS,
|
|
16
|
+
matchesExactDevDeliveryCandidate,
|
|
15
17
|
validateDevDeliveryCandidateChain,
|
|
16
18
|
} from "./dev-delivery-candidate-identity.js";
|
|
17
19
|
import {
|
|
@@ -380,6 +382,8 @@ export function transitionDevDeliveryQueue(queueInput, mutate, nowInput) {
|
|
|
380
382
|
delete queue.stateRoot;
|
|
381
383
|
const now = timestamp(nowInput, "now");
|
|
382
384
|
const result = mutate(queue, before, now);
|
|
385
|
+
if (result.preserveState)
|
|
386
|
+
return { before, after: before, expectedOldStateRoot, result };
|
|
383
387
|
queue.generation += 1;
|
|
384
388
|
queue.updatedAt = now;
|
|
385
389
|
const after = withQueueRoot(queue);
|
|
@@ -478,34 +482,14 @@ export function submitDevDeliveryCandidate(
|
|
|
478
482
|
`candidate ${attemptedCandidate.candidateId} is terminal and cannot be resubmitted`,
|
|
479
483
|
);
|
|
480
484
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
"dependencyRoot",
|
|
487
|
-
"toolchainRoot",
|
|
488
|
-
"environmentRoot",
|
|
489
|
-
"sourceWorkflowRunId",
|
|
490
|
-
];
|
|
491
|
-
const exactProofMatches =
|
|
492
|
-
exactProofFields.every(
|
|
493
|
-
(field) => existing[field] === attemptedCandidate[field],
|
|
494
|
-
) &&
|
|
495
|
-
JSON.stringify(existing.affectedPaths || []) ===
|
|
496
|
-
JSON.stringify(attemptedCandidate.affectedPaths || []) &&
|
|
497
|
-
JSON.stringify(existing.shardEvidenceRoots || []) ===
|
|
498
|
-
JSON.stringify(attemptedCandidate.shardEvidenceRoots || []) &&
|
|
499
|
-
existing.nativeCommandContract?.commandRoot ===
|
|
500
|
-
attemptedCandidate.nativeCommandContract?.commandRoot &&
|
|
501
|
-
existing.releaseBlockerPriority?.claimRoot ===
|
|
502
|
-
attemptedCandidate.releaseBlockerPriority?.claimRoot;
|
|
503
|
-
if (
|
|
504
|
-
existing.sourceHead === attemptedCandidate.sourceHead &&
|
|
505
|
-
exactProofMatches
|
|
506
|
-
) {
|
|
507
|
-
action = "duplicate-noop";
|
|
485
|
+
if (matchesExactDevDeliveryCandidate(existing, attemptedCandidate)) {
|
|
486
|
+
action =
|
|
487
|
+
before.activeWarrant?.candidateId === existing.candidateId
|
|
488
|
+
? "active-warrant-noop"
|
|
489
|
+
: "duplicate-noop";
|
|
508
490
|
selected = existing;
|
|
491
|
+
if (action === "active-warrant-noop")
|
|
492
|
+
return { candidate: selected, action, preserveState: true };
|
|
509
493
|
} else {
|
|
510
494
|
if (before.activeWarrant?.candidateId === existing.candidateId) {
|
|
511
495
|
throw new Error(
|
|
@@ -515,7 +499,7 @@ export function submitDevDeliveryCandidate(
|
|
|
515
499
|
const headChanged =
|
|
516
500
|
existing.sourceHead !== attemptedCandidate.sourceHead;
|
|
517
501
|
existing.sourceHead = attemptedCandidate.sourceHead;
|
|
518
|
-
for (const field of
|
|
502
|
+
for (const field of EXACT_DEV_DELIVERY_PROOF_FIELDS)
|
|
519
503
|
existing[field] = attemptedCandidate[field];
|
|
520
504
|
if (attemptedCandidate.nativeCommandContract)
|
|
521
505
|
existing.nativeCommandContract =
|
|
@@ -61,6 +61,14 @@ const ROOT_DOMAINS = new Set([
|
|
|
61
61
|
"tail-reseal-artifact-files",
|
|
62
62
|
"tail-reseal-receipt",
|
|
63
63
|
"release-candidate-passport",
|
|
64
|
+
"release-invocation-publisher",
|
|
65
|
+
"release-invocation-runtime",
|
|
66
|
+
"release-invocation-candidate",
|
|
67
|
+
"release-invocation-target",
|
|
68
|
+
"release-invocation-authority",
|
|
69
|
+
"release-invocation",
|
|
70
|
+
"release-transaction",
|
|
71
|
+
"release-receipt",
|
|
64
72
|
]);
|
|
65
73
|
const FAULT_CLASSES = new Set([
|
|
66
74
|
"validation",
|
|
@@ -302,8 +302,11 @@ function classifyBuildchainUses(records, failures, { repository } = {}) {
|
|
|
302
302
|
parsed.selector === ALPHA_RECOVERY_BOOTSTRAP.selector;
|
|
303
303
|
const protectedBootstrap =
|
|
304
304
|
repository === BUILDCHAIN_REPOSITORY && bootstrapShape;
|
|
305
|
+
const unauthorizedBootstrap =
|
|
306
|
+
repository !== BUILDCHAIN_REPOSITORY && bootstrapShape;
|
|
305
307
|
const channel =
|
|
306
|
-
|
|
308
|
+
!unauthorizedBootstrap &&
|
|
309
|
+
(CHANNELS[parsed.selector] || (protectedBootstrap ? "alpha" : ""));
|
|
307
310
|
uses.push({
|
|
308
311
|
...record,
|
|
309
312
|
...parsed,
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import {
|
|
2
|
+
V4ContractFault,
|
|
3
|
+
v4CanonicalBytes,
|
|
4
|
+
v4ContentRoot,
|
|
5
|
+
validateV4Root,
|
|
6
|
+
} from "./v4-canonical-contracts.js";
|
|
7
|
+
|
|
8
|
+
export const V4_RELEASE_INVOCATION_CONTRACT =
|
|
9
|
+
"kungfu-buildchain-v4-release-invocation/v1";
|
|
10
|
+
export const V4_RELEASE_INVOCATION_ADAPTER_CONTRACT =
|
|
11
|
+
"kungfu-buildchain-v4-release-invocation-adapter/v1";
|
|
12
|
+
export const V4_RELEASE_TRANSACTION_CONTRACT =
|
|
13
|
+
"kungfu-buildchain-v4-release-transaction/v1";
|
|
14
|
+
export const V4_RELEASE_RECEIPT_CONTRACT =
|
|
15
|
+
"kungfu-buildchain-v4-release-receipt/v1";
|
|
16
|
+
|
|
17
|
+
const SHA_PATTERN = /^[0-9a-f]{40}$/u;
|
|
18
|
+
const TAG_PATTERN = /^v[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/u;
|
|
19
|
+
const ROUTE_SURFACES = new Set([
|
|
20
|
+
"alpha",
|
|
21
|
+
"stable",
|
|
22
|
+
"public",
|
|
23
|
+
"private",
|
|
24
|
+
"declarative",
|
|
25
|
+
"legacy-compatible",
|
|
26
|
+
]);
|
|
27
|
+
const EXECUTION_MODES = new Set(["fresh", "resume"]);
|
|
28
|
+
const COMPARISON_STATES = new Set(["identical", "ahead", "behind", "diverged"]);
|
|
29
|
+
|
|
30
|
+
function fault(code, path, message) {
|
|
31
|
+
throw new V4ContractFault(code, path, message);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function exactKeys(value, keys, path) {
|
|
35
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
36
|
+
fault("invalid-release-invocation", path, `${path} must be an object`);
|
|
37
|
+
const actual = Object.keys(value).sort();
|
|
38
|
+
const expected = [...keys].sort();
|
|
39
|
+
if (
|
|
40
|
+
actual.length !== expected.length ||
|
|
41
|
+
actual.some((key, index) => key !== expected[index])
|
|
42
|
+
)
|
|
43
|
+
fault(
|
|
44
|
+
"invalid-release-invocation-shape",
|
|
45
|
+
path,
|
|
46
|
+
`${path} keys are not canonical`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sha(value, path, nullable = false) {
|
|
51
|
+
if (nullable && value === null) return value;
|
|
52
|
+
if (typeof value !== "string" || !SHA_PATTERN.test(value))
|
|
53
|
+
fault("invalid-release-sha", path, `${path} must be an exact Git SHA`);
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function text(value, path) {
|
|
58
|
+
if (
|
|
59
|
+
typeof value !== "string" ||
|
|
60
|
+
value.length === 0 ||
|
|
61
|
+
/[^\x20-\x7e]/u.test(value)
|
|
62
|
+
)
|
|
63
|
+
fault("invalid-release-text", path, `${path} must be printable ASCII`);
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function validatePublisher(value) {
|
|
68
|
+
exactKeys(
|
|
69
|
+
value,
|
|
70
|
+
["repository", "workflow", "workflowSha", "job"],
|
|
71
|
+
"$/publisher",
|
|
72
|
+
);
|
|
73
|
+
if (
|
|
74
|
+
value.repository !== "kungfu-systems/buildchain" ||
|
|
75
|
+
value.workflow !== ".github/workflows/.release-candidate-promote.yml" ||
|
|
76
|
+
value.job !== "apply"
|
|
77
|
+
)
|
|
78
|
+
fault(
|
|
79
|
+
"invalid-publisher-identity",
|
|
80
|
+
"$/publisher",
|
|
81
|
+
"publisher identity is not the canonical v4 APPLY job",
|
|
82
|
+
);
|
|
83
|
+
sha(value.workflowSha, "$/publisher/workflowSha");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function validateRuntime(value) {
|
|
87
|
+
exactKeys(value, ["repository", "commit", "tree"], "$/runtime");
|
|
88
|
+
if (value.repository !== "kungfu-systems/buildchain")
|
|
89
|
+
fault(
|
|
90
|
+
"invalid-runtime-identity",
|
|
91
|
+
"$/runtime/repository",
|
|
92
|
+
"runtime repository is not canonical",
|
|
93
|
+
);
|
|
94
|
+
sha(value.commit, "$/runtime/commit");
|
|
95
|
+
sha(value.tree, "$/runtime/tree");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function validateCandidate(value) {
|
|
99
|
+
exactKeys(value, ["repository", "commit", "tree", "version"], "$/candidate");
|
|
100
|
+
text(value.repository, "$/candidate/repository");
|
|
101
|
+
sha(value.commit, "$/candidate/commit");
|
|
102
|
+
sha(value.tree, "$/candidate/tree");
|
|
103
|
+
text(value.version, "$/candidate/version");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function validateTarget(value) {
|
|
107
|
+
exactKeys(value, ["channel", "tag", "expectedOldSha"], "$/target");
|
|
108
|
+
if (
|
|
109
|
+
!["alpha", "stable"].includes(value.channel) ||
|
|
110
|
+
!TAG_PATTERN.test(value.tag)
|
|
111
|
+
)
|
|
112
|
+
fault(
|
|
113
|
+
"invalid-release-target",
|
|
114
|
+
"$/target",
|
|
115
|
+
"release channel and exact tag are not canonical",
|
|
116
|
+
);
|
|
117
|
+
sha(value.expectedOldSha, "$/target/expectedOldSha", true);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function validateAuthority(value) {
|
|
121
|
+
exactKeys(
|
|
122
|
+
value,
|
|
123
|
+
["policyRoot", "qualificationRoot", "warrantRoot"],
|
|
124
|
+
"$/authority",
|
|
125
|
+
);
|
|
126
|
+
for (const name of ["policyRoot", "qualificationRoot", "warrantRoot"])
|
|
127
|
+
validateV4Root(value[name], `$/authority/${name}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function createV4ReleaseInvocation(value) {
|
|
131
|
+
exactKeys(
|
|
132
|
+
value,
|
|
133
|
+
["schema", "publisher", "runtime", "candidate", "target", "authority"],
|
|
134
|
+
"$",
|
|
135
|
+
);
|
|
136
|
+
if (value.schema !== V4_RELEASE_INVOCATION_CONTRACT)
|
|
137
|
+
fault(
|
|
138
|
+
"invalid-release-invocation",
|
|
139
|
+
"$/schema",
|
|
140
|
+
"unsupported invocation schema",
|
|
141
|
+
);
|
|
142
|
+
validatePublisher(value.publisher);
|
|
143
|
+
validateRuntime(value.runtime);
|
|
144
|
+
validateCandidate(value.candidate);
|
|
145
|
+
validateTarget(value.target);
|
|
146
|
+
validateAuthority(value.authority);
|
|
147
|
+
v4CanonicalBytes(value);
|
|
148
|
+
const roots = {
|
|
149
|
+
publisherRoot: v4ContentRoot(
|
|
150
|
+
"release-invocation-publisher",
|
|
151
|
+
value.publisher,
|
|
152
|
+
),
|
|
153
|
+
runtimeRoot: v4ContentRoot("release-invocation-runtime", value.runtime),
|
|
154
|
+
candidateRoot: v4ContentRoot(
|
|
155
|
+
"release-invocation-candidate",
|
|
156
|
+
value.candidate,
|
|
157
|
+
),
|
|
158
|
+
targetRoot: v4ContentRoot("release-invocation-target", value.target),
|
|
159
|
+
authorityRoot: v4ContentRoot(
|
|
160
|
+
"release-invocation-authority",
|
|
161
|
+
value.authority,
|
|
162
|
+
),
|
|
163
|
+
};
|
|
164
|
+
const invocationRoot = v4ContentRoot("release-invocation", {
|
|
165
|
+
schema: V4_RELEASE_INVOCATION_CONTRACT,
|
|
166
|
+
...roots,
|
|
167
|
+
});
|
|
168
|
+
return { invocation: value, roots: { ...roots, invocationRoot } };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function adaptV4ReleaseInvocation(value) {
|
|
172
|
+
exactKeys(value, ["schema", "route", "invocation"], "$adapter");
|
|
173
|
+
if (value.schema !== V4_RELEASE_INVOCATION_ADAPTER_CONTRACT)
|
|
174
|
+
fault(
|
|
175
|
+
"invalid-release-adapter",
|
|
176
|
+
"$adapter/schema",
|
|
177
|
+
"unsupported adapter schema",
|
|
178
|
+
);
|
|
179
|
+
exactKeys(value.route, ["surface", "execution"], "$adapter/route");
|
|
180
|
+
if (!ROUTE_SURFACES.has(value.route.surface))
|
|
181
|
+
fault(
|
|
182
|
+
"invalid-release-adapter",
|
|
183
|
+
"$adapter/route/surface",
|
|
184
|
+
"unsupported route surface",
|
|
185
|
+
);
|
|
186
|
+
if (!EXECUTION_MODES.has(value.route.execution))
|
|
187
|
+
fault(
|
|
188
|
+
"invalid-release-adapter",
|
|
189
|
+
"$adapter/route/execution",
|
|
190
|
+
"unsupported execution mode",
|
|
191
|
+
);
|
|
192
|
+
return createV4ReleaseInvocation(value.invocation);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function planV4ReleaseRoute({
|
|
196
|
+
requestedSha,
|
|
197
|
+
observedSha,
|
|
198
|
+
comparisonStatus,
|
|
199
|
+
requestedChannel = "",
|
|
200
|
+
targetRef,
|
|
201
|
+
dryRun = false,
|
|
202
|
+
resume = false,
|
|
203
|
+
}) {
|
|
204
|
+
sha(requestedSha, "$route/requestedSha");
|
|
205
|
+
sha(observedSha, "$route/observedSha");
|
|
206
|
+
if (!COMPARISON_STATES.has(comparisonStatus))
|
|
207
|
+
fault(
|
|
208
|
+
"invalid-release-route",
|
|
209
|
+
"$route/comparisonStatus",
|
|
210
|
+
"unsupported source comparison state",
|
|
211
|
+
);
|
|
212
|
+
text(targetRef, "$route/targetRef");
|
|
213
|
+
const alphaLane = /^alpha\/v[0-9]+\/v[0-9]+\.[0-9]+$/u.test(targetRef);
|
|
214
|
+
const stableLane =
|
|
215
|
+
/^release\/v[0-9]+\/v[0-9]+\.[0-9]+$/u.test(targetRef) ||
|
|
216
|
+
["publish-gate/major", "major-gate"].includes(targetRef);
|
|
217
|
+
if (!alphaLane && !stableLane)
|
|
218
|
+
fault(
|
|
219
|
+
"invalid-release-route",
|
|
220
|
+
"$route/targetRef",
|
|
221
|
+
"source lane is not a supported v4 release lane",
|
|
222
|
+
);
|
|
223
|
+
const derivedChannel = alphaLane ? "alpha" : "stable";
|
|
224
|
+
const normalizedRequested =
|
|
225
|
+
requestedChannel === "alpha"
|
|
226
|
+
? "alpha"
|
|
227
|
+
: ["release", "stable", "major"].includes(requestedChannel)
|
|
228
|
+
? "stable"
|
|
229
|
+
: requestedChannel === ""
|
|
230
|
+
? derivedChannel
|
|
231
|
+
: "";
|
|
232
|
+
if (!normalizedRequested || normalizedRequested !== derivedChannel)
|
|
233
|
+
fault(
|
|
234
|
+
"invalid-release-route",
|
|
235
|
+
"$route/channel",
|
|
236
|
+
"requested channel does not match the source lane",
|
|
237
|
+
);
|
|
238
|
+
let decision = resume ? "Resume" : "Fresh";
|
|
239
|
+
let reason = resume ? "resume" : "fresh";
|
|
240
|
+
if (comparisonStatus === "ahead" && !resume && !dryRun) {
|
|
241
|
+
decision = "NoOp";
|
|
242
|
+
reason = "source-advanced";
|
|
243
|
+
} else if (
|
|
244
|
+
requestedSha !== observedSha &&
|
|
245
|
+
!(resume && comparisonStatus === "ahead") &&
|
|
246
|
+
!(dryRun && comparisonStatus === "ahead")
|
|
247
|
+
) {
|
|
248
|
+
decision = "Blocked";
|
|
249
|
+
reason = `source-${comparisonStatus}`;
|
|
250
|
+
}
|
|
251
|
+
return Object.freeze({
|
|
252
|
+
decision,
|
|
253
|
+
reason,
|
|
254
|
+
channel: normalizedRequested,
|
|
255
|
+
targetRef,
|
|
256
|
+
requestedSha,
|
|
257
|
+
observedSha,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function createV4ReleaseTransaction(value) {
|
|
262
|
+
exactKeys(
|
|
263
|
+
value,
|
|
264
|
+
["invocationRoot", "publisherRoot", "runtimeRoot"],
|
|
265
|
+
"$transaction",
|
|
266
|
+
);
|
|
267
|
+
for (const name of ["invocationRoot", "publisherRoot", "runtimeRoot"])
|
|
268
|
+
validateV4Root(value[name], `$transaction/${name}`);
|
|
269
|
+
const transaction = {
|
|
270
|
+
schema: V4_RELEASE_TRANSACTION_CONTRACT,
|
|
271
|
+
invocationRoot: value.invocationRoot,
|
|
272
|
+
publisherRoot: value.publisherRoot,
|
|
273
|
+
runtimeRoot: value.runtimeRoot,
|
|
274
|
+
phases: ["QUALIFY", "APPLY", "SETTLE"],
|
|
275
|
+
writer: "canonical-v4-apply",
|
|
276
|
+
};
|
|
277
|
+
return {
|
|
278
|
+
transaction,
|
|
279
|
+
transactionRoot: v4ContentRoot("release-transaction", transaction),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function createV4ReleaseReceipt(value) {
|
|
284
|
+
exactKeys(
|
|
285
|
+
value,
|
|
286
|
+
[
|
|
287
|
+
"schema",
|
|
288
|
+
"transactionRoot",
|
|
289
|
+
"outcome",
|
|
290
|
+
"releasePassportRoot",
|
|
291
|
+
"providerTransactionRoot",
|
|
292
|
+
"providerStateRoot",
|
|
293
|
+
"providerReceiptRoots",
|
|
294
|
+
],
|
|
295
|
+
"$receipt",
|
|
296
|
+
);
|
|
297
|
+
if (value.schema !== V4_RELEASE_RECEIPT_CONTRACT)
|
|
298
|
+
fault(
|
|
299
|
+
"invalid-release-receipt",
|
|
300
|
+
"$receipt/schema",
|
|
301
|
+
"unsupported receipt schema",
|
|
302
|
+
);
|
|
303
|
+
validateV4Root(value.transactionRoot, "$receipt/transactionRoot");
|
|
304
|
+
if (!["complete", "blocked"].includes(value.outcome))
|
|
305
|
+
fault(
|
|
306
|
+
"invalid-release-receipt",
|
|
307
|
+
"$receipt/outcome",
|
|
308
|
+
"unsupported receipt outcome",
|
|
309
|
+
);
|
|
310
|
+
for (const name of [
|
|
311
|
+
"releasePassportRoot",
|
|
312
|
+
"providerTransactionRoot",
|
|
313
|
+
"providerStateRoot",
|
|
314
|
+
]) {
|
|
315
|
+
if (value[name] !== null) validateV4Root(value[name], `$receipt/${name}`);
|
|
316
|
+
}
|
|
317
|
+
if (!Array.isArray(value.providerReceiptRoots))
|
|
318
|
+
fault(
|
|
319
|
+
"invalid-release-receipt",
|
|
320
|
+
"$receipt/providerReceiptRoots",
|
|
321
|
+
"receipt roots must be an array",
|
|
322
|
+
);
|
|
323
|
+
value.providerReceiptRoots.forEach((root, index) =>
|
|
324
|
+
validateV4Root(root, `$receipt/providerReceiptRoots/${index}`),
|
|
325
|
+
);
|
|
326
|
+
const canonicalRoots = [...new Set(value.providerReceiptRoots)].sort();
|
|
327
|
+
if (
|
|
328
|
+
canonicalRoots.length !== value.providerReceiptRoots.length ||
|
|
329
|
+
canonicalRoots.some(
|
|
330
|
+
(root, index) => root !== value.providerReceiptRoots[index],
|
|
331
|
+
)
|
|
332
|
+
)
|
|
333
|
+
fault(
|
|
334
|
+
"invalid-release-receipt",
|
|
335
|
+
"$receipt/providerReceiptRoots",
|
|
336
|
+
"provider receipt roots must be sorted and unique",
|
|
337
|
+
);
|
|
338
|
+
if (
|
|
339
|
+
value.outcome === "complete" &&
|
|
340
|
+
[
|
|
341
|
+
value.releasePassportRoot,
|
|
342
|
+
value.providerTransactionRoot,
|
|
343
|
+
value.providerStateRoot,
|
|
344
|
+
].some((root) => root === null)
|
|
345
|
+
)
|
|
346
|
+
fault(
|
|
347
|
+
"invalid-release-receipt",
|
|
348
|
+
"$receipt/outcome",
|
|
349
|
+
"complete receipt requires every terminal root",
|
|
350
|
+
);
|
|
351
|
+
v4CanonicalBytes(value);
|
|
352
|
+
return {
|
|
353
|
+
receipt: value,
|
|
354
|
+
receiptRoot: v4ContentRoot("release-receipt", value),
|
|
355
|
+
};
|
|
356
|
+
}
|