@kungfu-tech/buildchain 4.0.2-alpha.2 → 4.0.2-alpha.4
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 +3 -0
- package/architecture/internal-capabilities.json +2 -0
- package/architecture/maintainability-debt.json +17 -15
- package/architecture/maintainability-policy.json +6 -6
- package/architecture/v3-core-mechanism-inventory.json +1 -0
- package/architecture/v4-delivery-warrant-shadow-fixtures.json +5 -5
- package/architecture/v4-release-invocation-fixtures.json +20 -0
- package/architecture/v4-release-topology.json +85 -80
- package/architecture/v4-runtime-semantic-closure.json +4 -1
- package/contracts/v4-release-invocation-v1.schema.json +50 -2
- package/dist/site/buildchain-contract.json +8 -8
- package/dist/site/buildchain-site.json +7 -7
- package/dist/site/kfd-claims.json +20 -3
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +1 -1
- package/dist/site/node-api-registry.json +88 -13
- package/dist/site/page-registry.json +2 -2
- package/dist/site/public-surface-audit.json +20 -3
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +5 -5
- package/dist/site/workflow-registry.json +22 -5
- package/docs/node-api-reference.md +13 -10
- package/package.json +1 -1
- package/packages/core/dev-delivery-execution-transfer.js +6 -7
- package/packages/core/dev-delivery-warrant-legacy-recovery.js +4 -2
- package/packages/core/dev-delivery-warrant-native-compatibility.js +33 -0
- package/packages/core/dev-delivery-warrant-state.js +42 -26
- package/packages/core/dev-delivery-warrant.js +8 -0
- package/packages/core/dev-delivery-writer-protocol-transition.js +72 -0
- package/packages/core/v4-canonical-contracts.js +2 -0
- package/packages/core/v4-protected-publication-source.js +93 -0
- package/packages/core/v4-publication-qualification.js +1 -0
- package/packages/core/v4-release-invocation.js +72 -3
- package/scripts/check-v4-release-topology.mjs +169 -23
- package/scripts/dev-delivery-warrant.mjs +13 -1
- package/scripts/generate-channel-promotion-workflow.mjs +2 -1
- package/scripts/release-candidate-resolver.mjs +17 -17
- package/scripts/resume-from-candidate-run.mjs +15 -16
- package/scripts/v4-release-candidate-adapter.mjs +41 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { v4ContentRoot } from "./v4-canonical-contracts.js";
|
|
2
|
+
|
|
3
|
+
const SHA_PATTERN = /^[0-9a-f]{40}$/u;
|
|
4
|
+
|
|
5
|
+
function exactSha(value, label) {
|
|
6
|
+
const normalized = String(value || "").toLowerCase();
|
|
7
|
+
if (!SHA_PATTERN.test(normalized)) {
|
|
8
|
+
throw new Error(`${label} must be an exact Git SHA`);
|
|
9
|
+
}
|
|
10
|
+
return normalized;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function exactParents(value, label) {
|
|
14
|
+
if (!Array.isArray(value)) {
|
|
15
|
+
throw new Error(`${label} must be an ordered Git parent list`);
|
|
16
|
+
}
|
|
17
|
+
return value.map((entry, index) => exactSha(entry, `${label}[${index}]`));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function bindV4ProtectedPublicationSource({
|
|
21
|
+
repository,
|
|
22
|
+
protectedCommit,
|
|
23
|
+
candidateCommit,
|
|
24
|
+
pullRequest,
|
|
25
|
+
} = {}) {
|
|
26
|
+
if (!/^[^/\s]+\/[^/\s]+$/u.test(String(repository || ""))) {
|
|
27
|
+
throw new Error("protected publication source requires owner/repository");
|
|
28
|
+
}
|
|
29
|
+
const protectedSource = {
|
|
30
|
+
sha: exactSha(protectedCommit?.sha, "protected source SHA"),
|
|
31
|
+
tree: exactSha(protectedCommit?.tree, "protected source tree"),
|
|
32
|
+
parents: exactParents(protectedCommit?.parents, "protected source parents"),
|
|
33
|
+
};
|
|
34
|
+
const candidateSource = {
|
|
35
|
+
sha: exactSha(candidateCommit?.sha, "candidate source SHA"),
|
|
36
|
+
tree: exactSha(candidateCommit?.tree, "candidate source tree"),
|
|
37
|
+
parents: exactParents(candidateCommit?.parents, "candidate source parents"),
|
|
38
|
+
};
|
|
39
|
+
if (protectedSource.tree !== candidateSource.tree) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
"protected publication source tree does not match the qualified candidate tree",
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let mode = "exact-commit";
|
|
46
|
+
let pullRequestNumber = null;
|
|
47
|
+
if (protectedSource.sha !== candidateSource.sha) {
|
|
48
|
+
mode = "merge-equivalent";
|
|
49
|
+
const sameParents =
|
|
50
|
+
protectedSource.parents.length === candidateSource.parents.length &&
|
|
51
|
+
protectedSource.parents.every(
|
|
52
|
+
(parent, index) => parent === candidateSource.parents[index],
|
|
53
|
+
);
|
|
54
|
+
if (!sameParents) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
"protected publication source is not parent-equivalent to the qualified merge candidate",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
pullRequestNumber = Number(pullRequest?.number || 0);
|
|
60
|
+
const pullRequestHead = exactSha(
|
|
61
|
+
pullRequest?.headSha,
|
|
62
|
+
"publication pull request head SHA",
|
|
63
|
+
);
|
|
64
|
+
const pullRequestMerge = exactSha(
|
|
65
|
+
pullRequest?.mergeSha,
|
|
66
|
+
"publication pull request merge SHA",
|
|
67
|
+
);
|
|
68
|
+
if (
|
|
69
|
+
!Number.isSafeInteger(pullRequestNumber) ||
|
|
70
|
+
pullRequestNumber <= 0 ||
|
|
71
|
+
pullRequest?.merged !== true ||
|
|
72
|
+
pullRequestMerge !== protectedSource.sha ||
|
|
73
|
+
!candidateSource.parents.includes(pullRequestHead)
|
|
74
|
+
) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
"protected publication source does not match the merged pull request and qualified merge candidate",
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const binding = {
|
|
82
|
+
schema: "kungfu.buildchain.v4-protected-publication-source/v1",
|
|
83
|
+
repository,
|
|
84
|
+
mode,
|
|
85
|
+
protectedSource,
|
|
86
|
+
candidateSource,
|
|
87
|
+
pullRequestNumber,
|
|
88
|
+
};
|
|
89
|
+
return {
|
|
90
|
+
...binding,
|
|
91
|
+
bindingRoot: v4ContentRoot("candidate-identity", binding),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -207,6 +207,7 @@ export function validateV4PublicationQualificationReceipt(
|
|
|
207
207
|
export function assertV4DeclarativePromotionInputs(inputs) {
|
|
208
208
|
const forbidden = Object.entries(inputs || {}).filter(
|
|
209
209
|
([name, value]) =>
|
|
210
|
+
name !== "dry-run" &&
|
|
210
211
|
/(command|cmd|script|shell|run)$/iu.test(name) &&
|
|
211
212
|
String(value || "").trim() !== "",
|
|
212
213
|
);
|
|
@@ -13,6 +13,8 @@ export const V4_RELEASE_TRANSACTION_CONTRACT =
|
|
|
13
13
|
"kungfu-buildchain-v4-release-transaction/v1";
|
|
14
14
|
export const V4_RELEASE_RECEIPT_CONTRACT =
|
|
15
15
|
"kungfu-buildchain-v4-release-receipt/v1";
|
|
16
|
+
export const V4_RELEASE_PROVIDER_CONTRACT =
|
|
17
|
+
"kungfu-buildchain-release-tail-provider/v1";
|
|
16
18
|
|
|
17
19
|
const SHA_PATTERN = /^[0-9a-f]{40}$/u;
|
|
18
20
|
const TAG_PATTERN = /^v[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/u;
|
|
@@ -127,10 +129,59 @@ function validateAuthority(value) {
|
|
|
127
129
|
validateV4Root(value[name], `$/authority/${name}`);
|
|
128
130
|
}
|
|
129
131
|
|
|
132
|
+
function validateProvider(value, candidate) {
|
|
133
|
+
exactKeys(value, ["adapter", "contract", "repository"], "$/provider");
|
|
134
|
+
if (
|
|
135
|
+
value.adapter !== "built-in-provider-plane" ||
|
|
136
|
+
value.contract !== V4_RELEASE_PROVIDER_CONTRACT ||
|
|
137
|
+
value.repository !== candidate.repository
|
|
138
|
+
)
|
|
139
|
+
fault(
|
|
140
|
+
"invalid-release-provider",
|
|
141
|
+
"$/provider",
|
|
142
|
+
"provider identity must bind the built-in Provider Plane and candidate repository",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function validateParent(value) {
|
|
147
|
+
exactKeys(
|
|
148
|
+
value,
|
|
149
|
+
["invocationRoot", "transactionRoot", "receiptRoot"],
|
|
150
|
+
"$/parent",
|
|
151
|
+
);
|
|
152
|
+
const roots = [
|
|
153
|
+
value.invocationRoot,
|
|
154
|
+
value.transactionRoot,
|
|
155
|
+
value.receiptRoot,
|
|
156
|
+
];
|
|
157
|
+
if (roots.every((root) => root === null)) return;
|
|
158
|
+
if (roots.some((root) => root === null))
|
|
159
|
+
fault(
|
|
160
|
+
"invalid-release-parent-lineage",
|
|
161
|
+
"$/parent",
|
|
162
|
+
"parent lineage roots must be either all null or all present",
|
|
163
|
+
);
|
|
164
|
+
roots.forEach((root, index) =>
|
|
165
|
+
validateV4Root(
|
|
166
|
+
root,
|
|
167
|
+
`$/parent/${["invocationRoot", "transactionRoot", "receiptRoot"][index]}`,
|
|
168
|
+
),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
130
172
|
export function createV4ReleaseInvocation(value) {
|
|
131
173
|
exactKeys(
|
|
132
174
|
value,
|
|
133
|
-
[
|
|
175
|
+
[
|
|
176
|
+
"schema",
|
|
177
|
+
"publisher",
|
|
178
|
+
"runtime",
|
|
179
|
+
"candidate",
|
|
180
|
+
"target",
|
|
181
|
+
"authority",
|
|
182
|
+
"provider",
|
|
183
|
+
"parent",
|
|
184
|
+
],
|
|
134
185
|
"$",
|
|
135
186
|
);
|
|
136
187
|
if (value.schema !== V4_RELEASE_INVOCATION_CONTRACT)
|
|
@@ -144,6 +195,8 @@ export function createV4ReleaseInvocation(value) {
|
|
|
144
195
|
validateCandidate(value.candidate);
|
|
145
196
|
validateTarget(value.target);
|
|
146
197
|
validateAuthority(value.authority);
|
|
198
|
+
validateProvider(value.provider, value.candidate);
|
|
199
|
+
validateParent(value.parent);
|
|
147
200
|
v4CanonicalBytes(value);
|
|
148
201
|
const roots = {
|
|
149
202
|
publisherRoot: v4ContentRoot(
|
|
@@ -160,6 +213,8 @@ export function createV4ReleaseInvocation(value) {
|
|
|
160
213
|
"release-invocation-authority",
|
|
161
214
|
value.authority,
|
|
162
215
|
),
|
|
216
|
+
providerRoot: v4ContentRoot("release-invocation-provider", value.provider),
|
|
217
|
+
parentRoot: v4ContentRoot("release-invocation-parent", value.parent),
|
|
163
218
|
};
|
|
164
219
|
const invocationRoot = v4ContentRoot("release-invocation", {
|
|
165
220
|
schema: V4_RELEASE_INVOCATION_CONTRACT,
|
|
@@ -261,16 +316,30 @@ export function planV4ReleaseRoute({
|
|
|
261
316
|
export function createV4ReleaseTransaction(value) {
|
|
262
317
|
exactKeys(
|
|
263
318
|
value,
|
|
264
|
-
[
|
|
319
|
+
[
|
|
320
|
+
"invocationRoot",
|
|
321
|
+
"publisherRoot",
|
|
322
|
+
"runtimeRoot",
|
|
323
|
+
"providerRoot",
|
|
324
|
+
"parentRoot",
|
|
325
|
+
],
|
|
265
326
|
"$transaction",
|
|
266
327
|
);
|
|
267
|
-
for (const name of [
|
|
328
|
+
for (const name of [
|
|
329
|
+
"invocationRoot",
|
|
330
|
+
"publisherRoot",
|
|
331
|
+
"runtimeRoot",
|
|
332
|
+
"providerRoot",
|
|
333
|
+
"parentRoot",
|
|
334
|
+
])
|
|
268
335
|
validateV4Root(value[name], `$transaction/${name}`);
|
|
269
336
|
const transaction = {
|
|
270
337
|
schema: V4_RELEASE_TRANSACTION_CONTRACT,
|
|
271
338
|
invocationRoot: value.invocationRoot,
|
|
272
339
|
publisherRoot: value.publisherRoot,
|
|
273
340
|
runtimeRoot: value.runtimeRoot,
|
|
341
|
+
providerRoot: value.providerRoot,
|
|
342
|
+
parentRoot: value.parentRoot,
|
|
274
343
|
phases: ["QUALIFY", "APPLY", "SETTLE"],
|
|
275
344
|
writer: "canonical-v4-apply",
|
|
276
345
|
};
|
|
@@ -12,13 +12,68 @@ import {
|
|
|
12
12
|
|
|
13
13
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
14
14
|
const ledgerPath = path.join(root, "architecture/v4-release-topology.json");
|
|
15
|
-
const releaseMarker =
|
|
16
|
-
/(?:release-candidate-promote\.yml|promote-buildchain-ref|v4-release-candidate-promote|release-tail-runtime)/u;
|
|
17
15
|
|
|
18
16
|
function read(relative) {
|
|
19
17
|
return fs.readFileSync(path.join(root, relative), "utf8");
|
|
20
18
|
}
|
|
21
19
|
|
|
20
|
+
function productionFiles(relative, extensions) {
|
|
21
|
+
const absolute = path.join(root, relative);
|
|
22
|
+
if (!fs.existsSync(absolute)) return [];
|
|
23
|
+
const entries = fs.readdirSync(absolute, { withFileTypes: true });
|
|
24
|
+
return entries.flatMap((entry) => {
|
|
25
|
+
const child = path.posix.join(relative, entry.name);
|
|
26
|
+
if (
|
|
27
|
+
entry.isDirectory() &&
|
|
28
|
+
!["dist", "node_modules", ".git"].includes(entry.name)
|
|
29
|
+
)
|
|
30
|
+
return productionFiles(child, extensions);
|
|
31
|
+
if (
|
|
32
|
+
entry.isFile() &&
|
|
33
|
+
extensions.some((extension) => entry.name.endsWith(extension))
|
|
34
|
+
)
|
|
35
|
+
return [child];
|
|
36
|
+
return [];
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function matchingProductionFiles(roots, extensions, pattern) {
|
|
41
|
+
return roots
|
|
42
|
+
.flatMap((relative) => productionFiles(relative, extensions))
|
|
43
|
+
.filter((relative) => relative !== "scripts/check-v4-release-topology.mjs")
|
|
44
|
+
.filter((relative) => pattern.test(read(relative)))
|
|
45
|
+
.sort();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function discoverV4ReleaseAuthorityClosure() {
|
|
49
|
+
const providerPlane = matchingProductionFiles(
|
|
50
|
+
["actions", "packages/core"],
|
|
51
|
+
[".js", ".mjs", ".cjs"],
|
|
52
|
+
/(?:release-tail-provider-(?:adapters|plane)|create(?:GitHubReleaseAssets|SignedStaticChannel|SiteReleaseActivation|ReleasedEvidence)Adapter)/u,
|
|
53
|
+
);
|
|
54
|
+
const runtimeSelectors = matchingProductionFiles(
|
|
55
|
+
[".github/workflows", "scripts", "packages/core"],
|
|
56
|
+
[".yml", ".yaml", ".js", ".mjs", ".cjs"],
|
|
57
|
+
/(?:promotion-runtime-sha|resume-buildchain-runtime-sha|BUILDCHAIN_(?:CURRENT_RUNTIME_SHA|RUNTIME_REF|RESUME_CANDIDATE_RUN_ID)|authorizeV4RuntimeSelection|scanV4RuntimeSelectorPersistence|v4-release-candidate-adapter)/u,
|
|
58
|
+
);
|
|
59
|
+
const terminalProjections = matchingProductionFiles(
|
|
60
|
+
[".github/workflows", "actions", "packages/core", "scripts"],
|
|
61
|
+
[".yml", ".yaml", ".js", ".mjs", ".cjs"],
|
|
62
|
+
/(?:createV4ReleaseReceipt|release-receipt\.json|V4_RELEASE_RECEIPT_CONTRACT)/u,
|
|
63
|
+
);
|
|
64
|
+
return {
|
|
65
|
+
providerAdapters: [
|
|
66
|
+
...new Set([
|
|
67
|
+
...providerPlane,
|
|
68
|
+
"packages/core/release-tail-provider-plane.js",
|
|
69
|
+
]),
|
|
70
|
+
].sort(),
|
|
71
|
+
runtimeSelectors,
|
|
72
|
+
runtimeEngines: ["actions/v4-release-candidate-promote/index.js"],
|
|
73
|
+
terminalProjections,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
22
77
|
function jobBlock(source, jobId) {
|
|
23
78
|
const escaped = jobId.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
24
79
|
return (
|
|
@@ -104,25 +159,27 @@ export function discoverV4ReleaseTopology(
|
|
|
104
159
|
"carriers",
|
|
105
160
|
"mutationSignals",
|
|
106
161
|
],
|
|
107
|
-
workflows: workflows
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
.
|
|
119
|
-
.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
162
|
+
workflows: workflows
|
|
163
|
+
.filter((workflow) => semanticPaths.has(workflow.path))
|
|
164
|
+
.map((workflow) => ({
|
|
165
|
+
path: workflow.path,
|
|
166
|
+
triggers: workflow.triggers,
|
|
167
|
+
jobs: workflow.jobs.map((job) =>
|
|
168
|
+
[
|
|
169
|
+
job.id,
|
|
170
|
+
job.kind,
|
|
171
|
+
job.uses || "-",
|
|
172
|
+
job.permissions.contents || "-",
|
|
173
|
+
job.permissions.idToken || "-",
|
|
174
|
+
Object.entries(job.carriers)
|
|
175
|
+
.filter(([, present]) => present)
|
|
176
|
+
.map(([name]) => name)
|
|
177
|
+
.join(",") || "-",
|
|
178
|
+
job.mutationSignals.join(",") || "-",
|
|
179
|
+
].join("|"),
|
|
180
|
+
),
|
|
181
|
+
reusableEdges: workflow.reusableEdges,
|
|
182
|
+
})),
|
|
126
183
|
metrics: {
|
|
127
184
|
workflowCount: workflows.length,
|
|
128
185
|
jobCount: jobs.length,
|
|
@@ -174,7 +231,26 @@ export function findUnknownV4ReleaseTopology(
|
|
|
174
231
|
const declared = new Set(workflowPaths);
|
|
175
232
|
return allWorkflowPaths
|
|
176
233
|
.filter((relative) => !declared.has(relative))
|
|
177
|
-
.filter((relative) =>
|
|
234
|
+
.filter((relative) => {
|
|
235
|
+
const source = readWorkflow(relative);
|
|
236
|
+
const usesReleaseAuthority = parseYamlUses(source).some(({ value }) =>
|
|
237
|
+
/(?:release-candidate-promote|promote-buildchain-ref|v4-release-candidate-promote|release-tail)/u.test(
|
|
238
|
+
value,
|
|
239
|
+
),
|
|
240
|
+
);
|
|
241
|
+
const hasReleaseLanguage =
|
|
242
|
+
/(?:^|[-_ ])(?:release|publish|promotion|distribution|tag)(?:$|[-_ :])/imu.test(
|
|
243
|
+
source,
|
|
244
|
+
);
|
|
245
|
+
const hasMutationAuthority =
|
|
246
|
+
/(?:contents|id-token):\s*write/u.test(source) ||
|
|
247
|
+
/(?:git push|npm publish|gh release (?:create|upload)|createRef|updateRef)/u.test(
|
|
248
|
+
source,
|
|
249
|
+
);
|
|
250
|
+
return (
|
|
251
|
+
usesReleaseAuthority || (hasReleaseLanguage && hasMutationAuthority)
|
|
252
|
+
);
|
|
253
|
+
})
|
|
178
254
|
.sort();
|
|
179
255
|
}
|
|
180
256
|
|
|
@@ -187,10 +263,76 @@ function assertClosedWorld(workflowPaths) {
|
|
|
187
263
|
);
|
|
188
264
|
}
|
|
189
265
|
|
|
266
|
+
function assertAuthorityClosure(ledger) {
|
|
267
|
+
const closure = ledger.authorityClosure;
|
|
268
|
+
assert.ok(
|
|
269
|
+
closure && typeof closure === "object",
|
|
270
|
+
"authority closure missing",
|
|
271
|
+
);
|
|
272
|
+
for (const className of [
|
|
273
|
+
"providerAdapters",
|
|
274
|
+
"runtimeSelectors",
|
|
275
|
+
"runtimeEngines",
|
|
276
|
+
"terminalProjections",
|
|
277
|
+
]) {
|
|
278
|
+
assert.ok(
|
|
279
|
+
Array.isArray(closure[className]) && closure[className].length > 0,
|
|
280
|
+
`authority closure class ${className} is empty`,
|
|
281
|
+
);
|
|
282
|
+
for (const relative of closure[className])
|
|
283
|
+
assert.ok(
|
|
284
|
+
fs.statSync(path.join(root, relative)).isFile(),
|
|
285
|
+
`authority closure path is not a file: ${relative}`,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
const discovered = discoverV4ReleaseAuthorityClosure();
|
|
289
|
+
for (const className of Object.keys(discovered))
|
|
290
|
+
assert.deepEqual(
|
|
291
|
+
closure[className],
|
|
292
|
+
discovered[className],
|
|
293
|
+
`authority closure class drifted: ${className}`,
|
|
294
|
+
);
|
|
295
|
+
assert.deepEqual(closure.runtimeEngines, [
|
|
296
|
+
"actions/v4-release-candidate-promote/index.js",
|
|
297
|
+
]);
|
|
298
|
+
assert.equal(
|
|
299
|
+
closure.freshEntry,
|
|
300
|
+
".github/workflows/release-candidate-promote.yml",
|
|
301
|
+
);
|
|
302
|
+
assert.equal(
|
|
303
|
+
closure.recoveryEntry,
|
|
304
|
+
".github/workflows/buildchain-ref-promotion-recovery.yml",
|
|
305
|
+
);
|
|
306
|
+
const engineSurface = [
|
|
307
|
+
".github/workflows/.release-candidate-promote.yml",
|
|
308
|
+
...closure.runtimeEngines,
|
|
309
|
+
]
|
|
310
|
+
.map(read)
|
|
311
|
+
.join("\n");
|
|
312
|
+
for (const pattern of closure.forbiddenLegacyEnginePatterns)
|
|
313
|
+
assert.doesNotMatch(
|
|
314
|
+
engineSurface,
|
|
315
|
+
new RegExp(pattern, "u"),
|
|
316
|
+
`legacy release engine remains reachable: ${pattern}`,
|
|
317
|
+
);
|
|
318
|
+
const canonicalWorkflow = read(
|
|
319
|
+
".github/workflows/.release-candidate-promote.yml",
|
|
320
|
+
);
|
|
321
|
+
assert.match(
|
|
322
|
+
canonicalWorkflow,
|
|
323
|
+
/scripts\/v4-release-candidate-adapter\.mjs/u,
|
|
324
|
+
);
|
|
325
|
+
assert.match(
|
|
326
|
+
canonicalWorkflow,
|
|
327
|
+
/release-invocation\.json[\s\S]*release-transaction\.json[\s\S]*release-receipt\.json/u,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
190
331
|
export function checkV4ReleaseTopology() {
|
|
191
332
|
const ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf8"));
|
|
192
333
|
assert.equal(ledger.contract, "kungfu-buildchain-v4-release-topology/v1");
|
|
193
334
|
assertClosedWorld(ledger.closedWorld.workflowPaths);
|
|
335
|
+
assertAuthorityClosure(ledger);
|
|
194
336
|
const actual = discoverV4ReleaseTopology(
|
|
195
337
|
ledger.closedWorld.workflowPaths,
|
|
196
338
|
ledger.semanticScope.workflowPaths,
|
|
@@ -219,7 +361,11 @@ export function checkV4ReleaseTopology() {
|
|
|
219
361
|
return actual;
|
|
220
362
|
}
|
|
221
363
|
|
|
222
|
-
if (process.argv.includes("--print")) {
|
|
364
|
+
if (process.argv.includes("--print-closure")) {
|
|
365
|
+
process.stdout.write(
|
|
366
|
+
`${JSON.stringify(discoverV4ReleaseAuthorityClosure(), null, 2)}\n`,
|
|
367
|
+
);
|
|
368
|
+
} else if (process.argv.includes("--print")) {
|
|
223
369
|
const ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf8"));
|
|
224
370
|
process.stdout.write(
|
|
225
371
|
`${JSON.stringify(
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
closeDevDeliveryWarrant,
|
|
7
7
|
createDevDeliveryQueue,
|
|
8
8
|
createNativeCommandContract,
|
|
9
|
+
fenceDevDeliveryWriterProtocol,
|
|
9
10
|
heartbeatDevDeliveryWarrant,
|
|
10
11
|
observeDevDeliveryQueue,
|
|
11
12
|
qualifyDevDeliveryWarrant,
|
|
@@ -91,6 +92,13 @@ function requireTerminalEvidenceCas(options) {
|
|
|
91
92
|
"terminal evidence reconciliation execute requires expected-old CAS",
|
|
92
93
|
);
|
|
93
94
|
}
|
|
95
|
+
if (
|
|
96
|
+
options.command === "fence-writer-protocol" &&
|
|
97
|
+
options.execute &&
|
|
98
|
+
!options.expectedOldStateRoot
|
|
99
|
+
) {
|
|
100
|
+
throw new Error("writer protocol fence execute requires expected-old CAS");
|
|
101
|
+
}
|
|
94
102
|
}
|
|
95
103
|
|
|
96
104
|
function normalizeRepository(value) {
|
|
@@ -175,6 +183,9 @@ function jsonObject(value, label) {
|
|
|
175
183
|
}
|
|
176
184
|
|
|
177
185
|
function transitionFor(command, queue, options) {
|
|
186
|
+
if (command === "fence-writer-protocol") {
|
|
187
|
+
return fenceDevDeliveryWriterProtocol(queue, { now: options.now });
|
|
188
|
+
}
|
|
178
189
|
if (command === "submit") {
|
|
179
190
|
const nativeCommandContract = options.environmentRoot
|
|
180
191
|
? createNativeCommandContract(options.nativeCommand)
|
|
@@ -517,7 +528,7 @@ export async function runDevDeliveryCommand(optionsInput = {}, clientInput) {
|
|
|
517
528
|
}
|
|
518
529
|
|
|
519
530
|
function usage() {
|
|
520
|
-
return "Usage:\n buildchain dev warrant <submit|select|heartbeat|qualify|recover|recover-legacy-terminal|close|settle|reconcile-terminal-evidence|cancel-queued|observe> --repository owner/repo --branch dev/vN/vN.M [--execute] [--output FILE] [--json]\n\nLegacy terminal recovery:\n recover-legacy-terminal --expected-old sha256:... --legacy-terminal-recovery FILE [--execute]\n\nRead candidate:\n observe --read-mode v4 --read-qualification FILE --read-qualification-root sha256:... --read-typescript-revision SHA --read-rust-revision SHA --read-validator-version TOKEN [--read-evidence-output FILE]\n";
|
|
531
|
+
return "Usage:\n buildchain dev warrant <fence-writer-protocol|submit|select|heartbeat|qualify|recover|recover-legacy-terminal|close|settle|reconcile-terminal-evidence|cancel-queued|observe> --repository owner/repo --branch dev/vN/vN.M [--execute] [--output FILE] [--json]\n\nWriter protocol fence:\n fence-writer-protocol --expected-old sha256:... [--execute]\n\nLegacy terminal recovery:\n recover-legacy-terminal --expected-old sha256:... --legacy-terminal-recovery FILE [--execute]\n\nRead candidate:\n observe --read-mode v4 --read-qualification FILE --read-qualification-root sha256:... --read-typescript-revision SHA --read-rust-revision SHA --read-validator-version TOKEN [--read-evidence-output FILE]\n";
|
|
521
532
|
}
|
|
522
533
|
|
|
523
534
|
async function main() {
|
|
@@ -529,6 +540,7 @@ async function main() {
|
|
|
529
540
|
const options = devDeliveryCliOptions(args);
|
|
530
541
|
if (
|
|
531
542
|
![
|
|
543
|
+
"fence-writer-protocol",
|
|
532
544
|
"submit",
|
|
533
545
|
"select",
|
|
534
546
|
"heartbeat",
|
|
@@ -214,8 +214,9 @@ function consumerAdmissionJob() {
|
|
|
214
214
|
id: policy
|
|
215
215
|
env:
|
|
216
216
|
BUILDCHAIN_CONSUMER_ROOT: .buildchain/consumer
|
|
217
|
+
BUILDCHAIN_INVOCATION_SOURCE_PATH: \${{ inputs.publication-publisher-workflow-path == '.github/workflows/buildchain-ref-promotion-recovery.yml' && '.github/workflows/release-candidate-promote.yml' || inputs.publication-publisher-workflow-path }}
|
|
217
218
|
BUILDCHAIN_EXPECTED_INVOCATION_CHANNEL: \${{ needs.resolve-promotion.outputs.channel }}
|
|
218
|
-
BUILDCHAIN_INVOKED_WORKFLOW: .github/workflows/release-candidate-promote.yml
|
|
219
|
+
BUILDCHAIN_INVOKED_WORKFLOW: \${{ inputs.publication-publisher-workflow-path == '.github/workflows/buildchain-ref-promotion-recovery.yml' && '.github/workflows/.release-candidate-promote.yml' || '.github/workflows/release-candidate-promote.yml' }}
|
|
219
220
|
BUILDCHAIN_WORKFLOW_SHA: \${{ needs.resolve-promotion.outputs.router-sha }}
|
|
220
221
|
BUILDCHAIN_RUNTIME_SHA: \${{ needs.resolve-promotion.outputs.router-sha }}
|
|
221
222
|
BUILDCHAIN_STABLE_CONTRACT_LOCK_PATH: \${{ inputs.buildchain-stable-contract-lock-path }}
|
|
@@ -110,13 +110,13 @@ function digestFileSync(filePath, algorithm, encoding) {
|
|
|
110
110
|
return hash.digest(encoding);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
export function selectMergedChannelPullRequest({ pullRequests = [], targetRef, repository }) {
|
|
113
|
+
export function selectMergedChannelPullRequest({ pullRequests = [], targetRef, targetSha = "", repository }) {
|
|
114
114
|
const normalizedTarget = normalizeBranch(targetRef);
|
|
115
115
|
const candidates = pullRequests.filter((pr) => {
|
|
116
|
-
const
|
|
117
|
-
const merged = Boolean(pr.merged_at || pr.mergedAt || pr.
|
|
118
|
-
const
|
|
119
|
-
return merged &&
|
|
116
|
+
const baseRepo = pr.base?.repo?.full_name || pr.baseRepository?.nameWithOwner;
|
|
117
|
+
const merged = Boolean(pr.merged_at || pr.mergedAt || pr.merged === true);
|
|
118
|
+
const rooted = !repository || (baseRepo || pr.head?.repo?.full_name) === repository;
|
|
119
|
+
return merged && rooted && (!targetSha || (pr.merge_commit_sha || pr.mergeCommit?.oid) === targetSha) && normalizeBranch(pr.base?.ref || pr.baseRefName || "") === normalizedTarget;
|
|
120
120
|
});
|
|
121
121
|
candidates.sort((left, right) => {
|
|
122
122
|
const leftTime = Date.parse(left.merged_at || left.updated_at || left.closed_at || "");
|
|
@@ -513,11 +513,11 @@ export async function resolveReleaseCandidateArtifacts({
|
|
|
513
513
|
});
|
|
514
514
|
const channelPullRequest = selectMergedChannelPullRequest({
|
|
515
515
|
pullRequests: Array.isArray(pulls) ? pulls : [],
|
|
516
|
-
targetRef: normalizedTarget,
|
|
516
|
+
targetRef: normalizedTarget, targetSha: sha,
|
|
517
517
|
repository: repoInfo.fullName,
|
|
518
518
|
});
|
|
519
519
|
if (!channelPullRequest) {
|
|
520
|
-
throw new Error(`no
|
|
520
|
+
throw new Error(`no exact merged channel PR rooted in ${repoInfo.fullName} found for ${sha} into ${normalizedTarget}`);
|
|
521
521
|
}
|
|
522
522
|
let pullRequest = channelPullRequest;
|
|
523
523
|
if (majorGateTarget) {
|
|
@@ -725,16 +725,6 @@ export async function resolveReleaseCandidateArtifacts({
|
|
|
725
725
|
const noun = publishArtifactKind === "npm" ? "npm package tarballs" : "platform manifests";
|
|
726
726
|
throw new Error(`expected at least ${minimumPayloadCount} downloaded ${noun}, found ${downloadedRequiredArtifactCount}`);
|
|
727
727
|
}
|
|
728
|
-
const manifests = platformManifestPaths.map((manifestPath) => JSON.parse(fs.readFileSync(manifestPath, "utf8")));
|
|
729
|
-
const generatedRequiredArtifacts = generatePublishRequiredArtifacts({
|
|
730
|
-
manifests,
|
|
731
|
-
version: passport.target?.version || "",
|
|
732
|
-
kind: publishArtifactKind,
|
|
733
|
-
tarballPaths: npmTarballPaths,
|
|
734
|
-
mainPackage: publishPackageMain,
|
|
735
|
-
});
|
|
736
|
-
const requiredArtifactsPath = path.join(resolvedOutput, "publish-required-artifacts.json");
|
|
737
|
-
fs.writeFileSync(requiredArtifactsPath, `${JSON.stringify(generatedRequiredArtifacts, null, 2)}\n`);
|
|
738
728
|
const sealedBundle = publishArtifactKind === "npm"
|
|
739
729
|
? createResolvedPublicationSealedBundle({
|
|
740
730
|
bundleRoot: payloadDir,
|
|
@@ -750,6 +740,16 @@ export async function resolveReleaseCandidateArtifacts({
|
|
|
750
740
|
releaseAssetPaths,
|
|
751
741
|
})
|
|
752
742
|
: undefined;
|
|
743
|
+
const manifests = platformManifestPaths.map((manifestPath) => JSON.parse(fs.readFileSync(manifestPath, "utf8")));
|
|
744
|
+
const generatedRequiredArtifacts = generatePublishRequiredArtifacts({
|
|
745
|
+
manifests,
|
|
746
|
+
version: [sealedBundle?.manifest?.npm?.version, passport.target?.version, ""].find(Boolean),
|
|
747
|
+
kind: publishArtifactKind,
|
|
748
|
+
tarballPaths: npmTarballPaths,
|
|
749
|
+
mainPackage: publishPackageMain,
|
|
750
|
+
});
|
|
751
|
+
const requiredArtifactsPath = path.join(resolvedOutput, "publish-required-artifacts.json");
|
|
752
|
+
fs.writeFileSync(requiredArtifactsPath, `${JSON.stringify(generatedRequiredArtifacts, null, 2)}\n`);
|
|
753
753
|
const sealedBundleManifestPath = sealedBundle
|
|
754
754
|
? path.join(resolvedOutput, "sealed-bundle.json")
|
|
755
755
|
: "";
|
|
@@ -785,14 +785,13 @@ async function recoverCandidateEvidence({
|
|
|
785
785
|
for (const artifact of [selected.passport, selected.summary]) initialDownloads.push(await downloadArtifact({ artifact, repoInfo, apiUrl, token, archiveDir, bundleRoot, fetchImpl }));
|
|
786
786
|
const passport = readOnlyJson(initialDownloads[0].files.filter((file) => path.basename(file.path) === "release-candidate-passport.json"), "release-candidate-passport.json");
|
|
787
787
|
const buildSummary = readOnlyJson(initialDownloads[1].files.filter((file) => path.basename(file.path) === "build-summary.json"), "build-summary.json");
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
);
|
|
792
|
-
const stageCapsuleSidecar =
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
: undefined;
|
|
788
|
+
const [stageCapsuleFile, publicationQualificationFile] = [
|
|
789
|
+
"release-candidate-stage-capsules.json",
|
|
790
|
+
"release-candidate-publication-qualification.json",
|
|
791
|
+
].map((name) => initialDownloads[0].files.find((file) => path.basename(file.path) === name));
|
|
792
|
+
const stageCapsuleSidecar = stageCapsuleFile
|
|
793
|
+
? readOnlyJson([stageCapsuleFile], "release-candidate-stage-capsules.json")
|
|
794
|
+
: undefined;
|
|
796
795
|
const { names: requiredNames, publicationNames } = candidateArtifactNames({ passport, selected, artifacts, artifactPatterns });
|
|
797
796
|
const chosen = artifacts.filter((artifact) => requiredNames.has(artifact.name));
|
|
798
797
|
if (chosen.length !== requiredNames.size) {
|
|
@@ -804,7 +803,7 @@ async function recoverCandidateEvidence({
|
|
|
804
803
|
for (const artifact of chosen.filter((entry) => ![selected.passport.id, selected.summary.id].includes(entry.id))) downloads.push(await downloadArtifact({ artifact, repoInfo, apiUrl, token, archiveDir, bundleRoot, fetchImpl }));
|
|
805
804
|
return {
|
|
806
805
|
run, workflow, selected, resolvedOutput, bundleRoot, initialDownloads,
|
|
807
|
-
passport, buildSummary, stageCapsuleSidecar, chosen, downloads, publicationNames,
|
|
806
|
+
passport, buildSummary, stageCapsuleSidecar, stageCapsuleFile, publicationQualificationFile, chosen, downloads, publicationNames,
|
|
808
807
|
};
|
|
809
808
|
}
|
|
810
809
|
|
|
@@ -878,7 +877,7 @@ export async function resumeFromCandidateRun({
|
|
|
878
877
|
try {
|
|
879
878
|
const {
|
|
880
879
|
run, workflow, selected, resolvedOutput, bundleRoot, initialDownloads,
|
|
881
|
-
passport, buildSummary, stageCapsuleSidecar, chosen, downloads, publicationNames,
|
|
880
|
+
passport, buildSummary, stageCapsuleSidecar, stageCapsuleFile, publicationQualificationFile, chosen, downloads, publicationNames,
|
|
882
881
|
} = await recoverCandidateEvidence({
|
|
883
882
|
repoInfo, runId, artifactName, artifactPatterns, requiredArtifactCount,
|
|
884
883
|
outputDir, apiUrl, token, fetchImpl, archiveDir,
|
|
@@ -1023,9 +1022,9 @@ export async function resumeFromCandidateRun({
|
|
|
1023
1022
|
sealedBundleRoot: publication.manifest ? outputPath(bundleRoot) : "",
|
|
1024
1023
|
sealedBundleManifest: publication.manifest ? outputPath(sealedManifestPath) : "",
|
|
1025
1024
|
recoveryReceipt: outputPath(recoveryReceiptPath),
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1025
|
+
stageCapsules: stageCapsuleFile ? outputPath(stageCapsuleFile.absolutePath) : "",
|
|
1026
|
+
publicationQualification: publicationQualificationFile ? outputPath(publicationQualificationFile.absolutePath) : "",
|
|
1027
|
+
runtimeResumeEvidence: runtimeResumeEvidencePath ? outputPath(runtimeResumeEvidencePath) : "",
|
|
1029
1028
|
},
|
|
1030
1029
|
};
|
|
1031
1030
|
} finally {
|
|
@@ -1077,10 +1076,10 @@ export async function resumeFromCandidateRunCli() {
|
|
|
1077
1076
|
"release-candidate-run-url": result.run.url,
|
|
1078
1077
|
"release-candidate-recovery-receipt-path": result.paths.recoveryReceipt,
|
|
1079
1078
|
"release-candidate-recovery-root": result.receipt.root,
|
|
1079
|
+
"release-candidate-stage-capsules-path": result.paths.stageCapsules,
|
|
1080
|
+
"release-candidate-publication-qualification-path": result.paths.publicationQualification,
|
|
1080
1081
|
"v4-runtime-resume-evidence-path": result.paths.runtimeResumeEvidence,
|
|
1081
|
-
"v4-runtime-resume-finalize-command": result.paths.runtimeResumeEvidence
|
|
1082
|
-
? "node .buildchain/runtime/promotion-shell/scripts/resume-from-candidate-run.mjs finalize"
|
|
1083
|
-
: "",
|
|
1082
|
+
"v4-runtime-resume-finalize-command": result.paths.runtimeResumeEvidence ? "node .buildchain/runtime/promotion-shell/scripts/resume-from-candidate-run.mjs finalize" : "",
|
|
1084
1083
|
"release-candidate-root": result.candidateRoot,
|
|
1085
1084
|
"release-candidate-artifact-root": result.artifactRoot,
|
|
1086
1085
|
"publish-sealed-bundle-root": result.paths.sealedBundleRoot,
|