@kungfu-tech/buildchain 3.0.6 → 3.0.7-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/actions/promote-buildchain-ref/README.md +10 -0
- package/actions/release-tail/README.md +17 -0
- package/bin/buildchain.mjs +11 -0
- package/bin/internal/command-registry.mjs +2 -0
- package/contracts/auditable-demo-scenario-v1.schema.json +1 -1
- package/contracts/engineering-housekeeper-v1.schema.json +143 -0
- package/contracts/fixtures/engineering-housekeeper-v1/cases.json +68 -0
- package/contracts/fixtures/release-tail-capabilities-v1/kungfu-alpha.json +273 -0
- package/contracts/publication-rehearsal-capsule-v1.schema.json +173 -0
- package/contracts/release-tail-capabilities-v1.schema.json +199 -0
- package/contracts/release-tail-provider-bindings-v1.schema.json +56 -0
- package/dist/site/agent-index.json +3 -0
- package/dist/site/artifact-schemas.json +6 -0
- package/dist/site/buildchain-contract.json +42 -27
- package/dist/site/buildchain-site.json +384 -33
- package/dist/site/capability-registry.json +15 -12
- package/dist/site/cli-registry.json +102 -0
- package/dist/site/controller-registry.json +19 -3
- package/dist/site/kfd-claims.json +359 -17
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +53 -7
- package/dist/site/node-api-registry.json +5882 -3605
- package/dist/site/page-registry.json +348 -21
- package/dist/site/public-surface-audit.json +396 -16
- package/dist/site/publication-authority-registry.json +106 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +7 -0
- package/dist/site/schemas/publication-rehearsal-capsule-v1.schema.json +269 -0
- package/dist/site/schemas/release-tail-capabilities-v1.schema.json +353 -0
- package/dist/site/schemas/release-tail-provider-bindings-v1.schema.json +94 -0
- package/dist/site/site-manifest.json +35 -11
- package/dist/site/workflow-registry.json +204 -10
- package/docs/MAP.md +6 -2
- package/docs/auditable-demo.md +2 -2
- package/docs/cli-reference.md +136 -0
- package/docs/dev-delivery-warrant.md +49 -4
- package/docs/engineering-housekeeper.md +138 -0
- package/docs/lifecycle-protocol.md +4 -2
- package/docs/node-api-reference.md +495 -281
- package/docs/publication-rehearsal.md +94 -0
- package/docs/release-governance.md +17 -2
- package/docs/release-tail-contract.md +160 -0
- package/docs/release-tail-provider-plane.md +120 -0
- package/docs/reusable-build-surface.md +11 -0
- package/package.json +11 -3
- package/packages/core/artifact-signing.js +61 -0
- package/packages/core/buildchain-agent-manuals.js +3 -0
- package/packages/core/buildchain-config.js +66 -6
- package/packages/core/buildchain-kfd-claims.js +1 -1
- package/packages/core/buildchain-publication-authority.js +5 -0
- package/packages/core/controller-evidence.js +2 -1
- package/packages/core/dev-delivery-warrant-cancellation.js +1 -0
- package/packages/core/dev-delivery-warrant-shadow.js +502 -0
- package/packages/core/dev-delivery-warrant.js +15 -6
- package/packages/core/diagnostics.js +8 -3
- package/packages/core/engineering-housekeeper-github-client.js +222 -0
- package/packages/core/engineering-housekeeper-github.js +501 -0
- package/packages/core/engineering-housekeeper.js +259 -0
- package/packages/core/index.js +42 -0
- package/packages/core/kfd-gate.js +45 -15
- package/packages/core/paper-agent-entry.js +11 -5
- package/packages/core/paper-repository.js +1 -0
- package/packages/core/paper-scaffold-content.js +21 -0
- package/packages/core/paper.js +28 -2
- package/packages/core/publication-rehearsal-projection.js +173 -0
- package/packages/core/publication-rehearsal-runtime.js +921 -0
- package/packages/core/release-passport.js +130 -20
- package/packages/core/release-tail-compatibility.js +60 -0
- package/packages/core/release-tail-provider-adapters.js +461 -0
- package/packages/core/release-tail-provider-plane.js +1228 -0
- package/scripts/assemble-publication-artifact-admission.mjs +1 -1
- package/scripts/assemble-self-publication-admission.mjs +2 -2
- package/scripts/audit-publication-control-plane.mjs +1 -1
- package/scripts/auditable-demo-bundle-verification.mjs +2 -3
- package/scripts/auditable-demo-platform.mjs +2 -2
- package/scripts/auditable-demo-renditions.mjs +1 -1
- package/scripts/auditable-demo.mjs +2 -2
- package/scripts/build-contract-core.mjs +8 -3
- package/scripts/build-standalone-binary.mjs +14 -3
- package/scripts/buildchain-cli-help.mjs +13 -0
- package/scripts/check-core-mechanism-inventory.mjs +347 -0
- package/scripts/check-inventory.mjs +10 -8
- package/scripts/check-maintainability.mjs +9 -2
- package/scripts/check-release-tail-contract.mjs +435 -0
- package/scripts/dev-delivery-warrant.mjs +31 -4
- package/scripts/dev-pr-auto-merge.mjs +30 -4
- package/scripts/dev-pr-delivery-warrant.mjs +50 -0
- package/scripts/engineering-housekeeper-workflow.mjs +394 -0
- package/scripts/generate-channel-promotion-workflow.mjs +10 -8
- package/scripts/generate-site-bundle.mjs +47 -4
- package/scripts/init-repo.mjs +26 -2
- package/scripts/inspect-artifact-signing-requests.mjs +6 -0
- package/scripts/materialize-self-release-candidate-version.mjs +137 -0
- package/scripts/publication-commit-evidence.mjs +69 -23
- package/scripts/release-candidate-resolver.mjs +16 -10
- package/scripts/release-tail.mjs +159 -0
- package/scripts/resume-from-candidate-run.mjs +123 -9
- package/scripts/seal-artifact-signing-requests.mjs +6 -0
- package/scripts/site-capability-metadata.mjs +13 -0
- package/scripts/v4-architecture.mjs +600 -0
- package/scripts/web-surface-core.mjs +8 -2
- package/scripts/workflow-call-contract.mjs +184 -5
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import {
|
|
5
|
+
getVersionStrategy,
|
|
6
|
+
loadConfiguredAnchorManifest,
|
|
7
|
+
discoverConfiguredDerivedVersionMaterial,
|
|
8
|
+
} from "../packages/core/buildchain-config.js";
|
|
9
|
+
import {
|
|
10
|
+
alignMajorBootstrapReleaseImpact,
|
|
11
|
+
currentConfiguredVersion,
|
|
12
|
+
discoverVersionStateFiles,
|
|
13
|
+
runVersionVerification,
|
|
14
|
+
updateVersionStateContents,
|
|
15
|
+
versionVerificationAllowedPathsForPromotion,
|
|
16
|
+
versionVerificationEnv,
|
|
17
|
+
} from "../actions/promote-buildchain-ref/internal/version-state.js";
|
|
18
|
+
|
|
19
|
+
function required(value, name) {
|
|
20
|
+
const normalized = String(value || "").trim();
|
|
21
|
+
if (!normalized) throw new Error(`${name} is required`);
|
|
22
|
+
return normalized;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseArgs(argv) {
|
|
26
|
+
const options = {};
|
|
27
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
28
|
+
const argument = argv[index];
|
|
29
|
+
if (!argument.startsWith("--")) throw new Error(`unexpected argument: ${argument}`);
|
|
30
|
+
const name = argument.slice(2);
|
|
31
|
+
const value = argv[index + 1];
|
|
32
|
+
if (!value || value.startsWith("--")) throw new Error(`--${name} requires a value`);
|
|
33
|
+
options[name] = value;
|
|
34
|
+
index += 1;
|
|
35
|
+
}
|
|
36
|
+
return options;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function materializeSelfReleaseCandidateVersion({
|
|
40
|
+
cwd = process.cwd(),
|
|
41
|
+
version,
|
|
42
|
+
channel,
|
|
43
|
+
sourceSha,
|
|
44
|
+
generatedAt,
|
|
45
|
+
} = {}) {
|
|
46
|
+
const resolvedVersion = required(version, "version");
|
|
47
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(resolvedVersion)) {
|
|
48
|
+
throw new Error(`version must be an exact semantic version: ${resolvedVersion}`);
|
|
49
|
+
}
|
|
50
|
+
const resolvedChannel = required(channel, "channel");
|
|
51
|
+
if (!new Set(["alpha", "release", "major"]).has(resolvedChannel)) {
|
|
52
|
+
throw new Error(`channel must be alpha, release, or major: ${resolvedChannel}`);
|
|
53
|
+
}
|
|
54
|
+
const resolvedSourceSha = required(sourceSha, "sourceSha");
|
|
55
|
+
if (!/^[0-9a-f]{40}$/i.test(resolvedSourceSha)) {
|
|
56
|
+
throw new Error("sourceSha must be a 40-character Git SHA");
|
|
57
|
+
}
|
|
58
|
+
const resolvedGeneratedAt = required(generatedAt, "generatedAt");
|
|
59
|
+
if (Number.isNaN(Date.parse(resolvedGeneratedAt))) {
|
|
60
|
+
throw new Error(`generatedAt must be an ISO timestamp: ${resolvedGeneratedAt}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const discovered = discoverVersionStateFiles(cwd);
|
|
64
|
+
if (discovered.files.length === 0) {
|
|
65
|
+
throw new Error("self-release candidate requires declared version state");
|
|
66
|
+
}
|
|
67
|
+
const discoveredPaths = discovered.files.map((file) => file.path);
|
|
68
|
+
const derivedPaths = discoverConfiguredDerivedVersionMaterial(
|
|
69
|
+
cwd,
|
|
70
|
+
discovered.config,
|
|
71
|
+
).map((file) => file.path);
|
|
72
|
+
const versionStrategy = getVersionStrategy(discovered.config);
|
|
73
|
+
const anchorManifest = loadConfiguredAnchorManifest(cwd, discovered.config);
|
|
74
|
+
const lifecycleEnv = versionVerificationEnv(versionStrategy, anchorManifest, {
|
|
75
|
+
generatedAt: resolvedGeneratedAt,
|
|
76
|
+
sourceSha: resolvedSourceSha,
|
|
77
|
+
});
|
|
78
|
+
if (resolvedChannel === "major") {
|
|
79
|
+
lifecycleEnv.BUILDCHAIN_MAJOR_VERSION_BOOTSTRAP = "true";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let changedFiles = updateVersionStateContents(discovered.files, resolvedVersion);
|
|
83
|
+
if (resolvedChannel === "major") {
|
|
84
|
+
changedFiles = alignMajorBootstrapReleaseImpact(changedFiles, {
|
|
85
|
+
version: resolvedVersion,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
const materializedFiles = runVersionVerification({
|
|
89
|
+
cwd,
|
|
90
|
+
loadedConfig: discovered.config,
|
|
91
|
+
version: resolvedVersion,
|
|
92
|
+
changedFiles,
|
|
93
|
+
allowedPaths: versionVerificationAllowedPathsForPromotion(
|
|
94
|
+
resolvedChannel,
|
|
95
|
+
discoveredPaths,
|
|
96
|
+
derivedPaths,
|
|
97
|
+
),
|
|
98
|
+
env: lifecycleEnv,
|
|
99
|
+
runLifecycleVerify: false,
|
|
100
|
+
});
|
|
101
|
+
const materialized = discoverVersionStateFiles(cwd);
|
|
102
|
+
const actualVersion = currentConfiguredVersion(materialized.files);
|
|
103
|
+
if (actualVersion !== resolvedVersion) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`materialized self-release candidate version mismatch: expected ${resolvedVersion}, got ${actualVersion || "<empty>"}`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
schemaVersion: 1,
|
|
110
|
+
contract: "kungfu-buildchain-self-release-candidate-version/v1",
|
|
111
|
+
version: resolvedVersion,
|
|
112
|
+
channel: resolvedChannel,
|
|
113
|
+
sourceSha: resolvedSourceSha,
|
|
114
|
+
generatedAt: resolvedGeneratedAt,
|
|
115
|
+
files: materializedFiles.map((file) => file.path),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function materializeSelfReleaseCandidateVersionCli(argv = process.argv.slice(2)) {
|
|
120
|
+
const options = parseArgs(argv);
|
|
121
|
+
const result = materializeSelfReleaseCandidateVersion({
|
|
122
|
+
cwd: path.resolve(options.cwd || process.cwd()),
|
|
123
|
+
version: options.version,
|
|
124
|
+
channel: options.channel,
|
|
125
|
+
sourceSha: options["source-sha"],
|
|
126
|
+
generatedAt: options["generated-at"],
|
|
127
|
+
});
|
|
128
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (
|
|
133
|
+
process.argv[1] &&
|
|
134
|
+
import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href
|
|
135
|
+
) {
|
|
136
|
+
materializeSelfReleaseCandidateVersionCli();
|
|
137
|
+
}
|
|
@@ -7,6 +7,12 @@ import { pathToFileURL } from "node:url";
|
|
|
7
7
|
|
|
8
8
|
const SCHEMA = "kungfu-buildchain-publication-commit-evidence/v1";
|
|
9
9
|
const INSTALLER_BUNDLE_SCHEMA = "kungfu.installer-publication-bundle/v1";
|
|
10
|
+
const RELEASE_REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
11
|
+
const RELEASE_REDIRECT_HOSTS = new Set([
|
|
12
|
+
"github.com",
|
|
13
|
+
"release-assets.githubusercontent.com",
|
|
14
|
+
"objects.githubusercontent.com",
|
|
15
|
+
]);
|
|
10
16
|
|
|
11
17
|
function requiredString(value, label) {
|
|
12
18
|
if (typeof value !== "string" || value.trim() === "") {
|
|
@@ -104,7 +110,7 @@ function validateInstallerBundle(evidence, expected) {
|
|
|
104
110
|
}
|
|
105
111
|
if (
|
|
106
112
|
exactSha(bundle.sourceCommit, "installerBundle.sourceCommit") !==
|
|
107
|
-
expected.
|
|
113
|
+
expected.candidateSourceSha ||
|
|
108
114
|
!["alpha", "stable"].includes(bundle.channel)
|
|
109
115
|
) {
|
|
110
116
|
throw new Error("installer bundle release identity mismatch");
|
|
@@ -228,6 +234,7 @@ function validateInstallerBundle(evidence, expected) {
|
|
|
228
234
|
return {
|
|
229
235
|
schema: bundle.schema,
|
|
230
236
|
bundleRoot,
|
|
237
|
+
sourceCommit: bundle.sourceCommit,
|
|
231
238
|
manifestDigest: bundle.manifestDigest,
|
|
232
239
|
channel: bundle.channel,
|
|
233
240
|
channelPayloadRoot: bundle.channelPayloadRoot,
|
|
@@ -238,10 +245,9 @@ function validateInstallerBundle(evidence, expected) {
|
|
|
238
245
|
assets: bundle.assets,
|
|
239
246
|
};
|
|
240
247
|
}
|
|
241
|
-
|
|
242
248
|
export function validatePublicationCommitEvidence(
|
|
243
249
|
evidence,
|
|
244
|
-
{ version, sourceSha, releaseSha, releaseTag } = {},
|
|
250
|
+
{ version, sourceSha, candidateSourceSha, releaseSha, releaseTag } = {},
|
|
245
251
|
) {
|
|
246
252
|
if (!evidence || typeof evidence !== "object" || Array.isArray(evidence)) {
|
|
247
253
|
throw new Error("publication commit evidence must be an object");
|
|
@@ -264,6 +270,17 @@ export function validatePublicationCommitEvidence(
|
|
|
264
270
|
throw new Error(`publication commit evidence ${field} mismatch`);
|
|
265
271
|
}
|
|
266
272
|
}
|
|
273
|
+
expected.candidateSourceSha = exactSha(
|
|
274
|
+
candidateSourceSha ?? identity.candidateSourceSha ?? expected.sourceSha,
|
|
275
|
+
"expected candidateSourceSha",
|
|
276
|
+
);
|
|
277
|
+
if (
|
|
278
|
+
identity.candidateSourceSha !== undefined &&
|
|
279
|
+
exactSha(identity.candidateSourceSha, "identity.candidateSourceSha") !==
|
|
280
|
+
expected.candidateSourceSha
|
|
281
|
+
) {
|
|
282
|
+
throw new Error("publication commit evidence candidateSourceSha mismatch");
|
|
283
|
+
}
|
|
267
284
|
const publicUrl = publicHttps(evidence.publication?.url, "publication.url");
|
|
268
285
|
const payloadRoot = sha256Root(
|
|
269
286
|
evidence.publication?.payloadRoot,
|
|
@@ -313,15 +330,47 @@ export async function verifyInstallerBundleReadback(
|
|
|
313
330
|
if (typeof fetchImpl !== "function") {
|
|
314
331
|
throw new Error("installer bundle read-back requires fetch");
|
|
315
332
|
}
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
333
|
+
const fetchReleaseReadback = async (url, label) => {
|
|
334
|
+
let current = url;
|
|
335
|
+
for (let hop = 0; hop <= 3; hop += 1) {
|
|
336
|
+
const response = await fetchImpl(current, {
|
|
337
|
+
redirect: "manual",
|
|
338
|
+
cache: "no-store",
|
|
339
|
+
});
|
|
340
|
+
if (response.status === 200) return response;
|
|
341
|
+
if (!RELEASE_REDIRECT_STATUSES.has(response.status)) {
|
|
342
|
+
throw new Error(`${label} failed: HTTP ${response.status}`);
|
|
343
|
+
}
|
|
344
|
+
if (hop === 3) {
|
|
345
|
+
throw new Error(`${label} exceeded the bounded redirect limit`);
|
|
346
|
+
}
|
|
347
|
+
const location = response.headers?.get?.("location");
|
|
348
|
+
if (!location) {
|
|
349
|
+
throw new Error(`${label} redirect omitted Location`);
|
|
350
|
+
}
|
|
351
|
+
const redirect = new URL(location, current);
|
|
352
|
+
if (
|
|
353
|
+
redirect.protocol !== "https:" ||
|
|
354
|
+
redirect.username ||
|
|
355
|
+
redirect.password ||
|
|
356
|
+
!RELEASE_REDIRECT_HOSTS.has(redirect.hostname) ||
|
|
357
|
+
(redirect.hostname === "github.com" &&
|
|
358
|
+
!redirect.pathname.startsWith(
|
|
359
|
+
"/kungfu-systems/kungfu/releases/download/",
|
|
360
|
+
))
|
|
361
|
+
) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`${label} redirected outside trusted GitHub release storage`,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
current = redirect.href;
|
|
367
|
+
}
|
|
368
|
+
throw new Error(`${label} failed without a terminal response`);
|
|
369
|
+
};
|
|
370
|
+
const manifestResponse = await fetchReleaseReadback(
|
|
371
|
+
result.publicUrl,
|
|
372
|
+
"installer bundle manifest read-back",
|
|
373
|
+
);
|
|
325
374
|
const manifestBytes = Buffer.from(await manifestResponse.arrayBuffer());
|
|
326
375
|
if (digest(manifestBytes) !== bundle.manifestDigest) {
|
|
327
376
|
throw new Error("installer bundle manifest digest mismatch");
|
|
@@ -336,7 +385,7 @@ export async function verifyInstallerBundleReadback(
|
|
|
336
385
|
semanticRoot(unsigned) !== bundle.bundleRoot ||
|
|
337
386
|
manifest.package?.name !== "@kungfu-tech/site" ||
|
|
338
387
|
typeof manifest.package?.version !== "string" ||
|
|
339
|
-
manifest.identity?.sourceCommit !== result.identity.
|
|
388
|
+
manifest.identity?.sourceCommit !== result.identity.candidateSourceSha ||
|
|
340
389
|
manifest.identity?.releaseSha !== result.identity.releaseSha ||
|
|
341
390
|
manifest.identity?.releaseTag !== result.identity.releaseTag ||
|
|
342
391
|
manifest.identity?.version !== result.identity.version ||
|
|
@@ -365,15 +414,10 @@ export async function verifyInstallerBundleReadback(
|
|
|
365
414
|
for (const asset of bundle.assets) {
|
|
366
415
|
let observation = byUrl.get(asset.releaseUrl);
|
|
367
416
|
if (!observation) {
|
|
368
|
-
const response = await
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
if (response.status !== 200) {
|
|
373
|
-
throw new Error(
|
|
374
|
-
`installer bundle asset read-back failed: HTTP ${response.status}`,
|
|
375
|
-
);
|
|
376
|
-
}
|
|
417
|
+
const response = await fetchReleaseReadback(
|
|
418
|
+
asset.releaseUrl,
|
|
419
|
+
"installer bundle asset read-back",
|
|
420
|
+
);
|
|
377
421
|
const bytes = Buffer.from(await response.arrayBuffer());
|
|
378
422
|
observation = {
|
|
379
423
|
releaseUrl: asset.releaseUrl,
|
|
@@ -394,7 +438,7 @@ export async function verifyInstallerBundleReadback(
|
|
|
394
438
|
schema: "kungfu-buildchain-installer-publication-bundle-seal/v1",
|
|
395
439
|
bundleRoot: bundle.bundleRoot,
|
|
396
440
|
manifestDigest: bundle.manifestDigest,
|
|
397
|
-
sourceCommit: result.identity.
|
|
441
|
+
sourceCommit: result.identity.candidateSourceSha,
|
|
398
442
|
releaseTag: result.identity.releaseTag,
|
|
399
443
|
releasePassport: bundle.releasePassport,
|
|
400
444
|
observations,
|
|
@@ -409,6 +453,8 @@ async function main(args) {
|
|
|
409
453
|
if (value === "--evidence") options.evidence = args[++index];
|
|
410
454
|
else if (value === "--version") options.version = args[++index];
|
|
411
455
|
else if (value === "--source-sha") options.sourceSha = args[++index];
|
|
456
|
+
else if (value === "--candidate-source-sha")
|
|
457
|
+
options.candidateSourceSha = args[++index];
|
|
412
458
|
else if (value === "--release-sha") options.releaseSha = args[++index];
|
|
413
459
|
else if (value === "--release-tag") options.releaseTag = args[++index];
|
|
414
460
|
else throw new Error(`unknown argument: ${value}`);
|
|
@@ -351,16 +351,22 @@ export function generatePublishRequiredArtifacts({
|
|
|
351
351
|
const files = Array.isArray(manifest.files) ? manifest.files : [];
|
|
352
352
|
return files
|
|
353
353
|
.filter((file) => file?.sha256)
|
|
354
|
-
.map((file) =>
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
?
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
354
|
+
.map((file) => {
|
|
355
|
+
const name = String(file.path || file.name || manifest.artifactName || platform)
|
|
356
|
+
.replaceAll("\\", "/")
|
|
357
|
+
.replace(/^\.\//, "");
|
|
358
|
+
return {
|
|
359
|
+
...(platform ? { group: platform } : {}),
|
|
360
|
+
kind,
|
|
361
|
+
name,
|
|
362
|
+
ref,
|
|
363
|
+
digest: String(file.sha256).startsWith("sha256:")
|
|
364
|
+
? String(file.sha256)
|
|
365
|
+
: `sha256:${file.sha256}`,
|
|
366
|
+
role: "platform",
|
|
367
|
+
platform,
|
|
368
|
+
};
|
|
369
|
+
});
|
|
364
370
|
});
|
|
365
371
|
}
|
|
366
372
|
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
compileReleaseTailDeclaration,
|
|
8
|
+
createReleaseTailTransaction,
|
|
9
|
+
readReleaseTailTransaction,
|
|
10
|
+
validateReleaseTailTransaction,
|
|
11
|
+
writeReleaseTailTransaction,
|
|
12
|
+
} from "../packages/core/release-tail-provider-plane.js";
|
|
13
|
+
import { diagnoseLegacyReleaseTailHooks } from "../packages/core/release-tail-compatibility.js";
|
|
14
|
+
import {
|
|
15
|
+
executePublicationRehearsal,
|
|
16
|
+
publicationRehearsalDiagnostic,
|
|
17
|
+
} from "../packages/core/publication-rehearsal-runtime.js";
|
|
18
|
+
|
|
19
|
+
function flag(args, name, fallback = "") {
|
|
20
|
+
const index = args.indexOf(`--${name}`);
|
|
21
|
+
return index < 0 ? fallback : args[index + 1] || "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readJson(value, label) {
|
|
25
|
+
if (!value) throw new Error(`${label} is required`);
|
|
26
|
+
const filePath = path.resolve(value);
|
|
27
|
+
if (fs.existsSync(filePath))
|
|
28
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
29
|
+
return JSON.parse(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function output(value, filePath = "") {
|
|
33
|
+
if (filePath) {
|
|
34
|
+
const resolved = path.resolve(filePath);
|
|
35
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
36
|
+
fs.writeFileSync(resolved, `${JSON.stringify(value, null, 2)}\n`);
|
|
37
|
+
}
|
|
38
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function runReleaseTailCli(args = process.argv.slice(2)) {
|
|
42
|
+
const [mode = "status", ...options] = args;
|
|
43
|
+
if (mode === "rehearse") {
|
|
44
|
+
const capsulePath = flag(options, "capsule");
|
|
45
|
+
const capsuleRoot = flag(options, "capsule-root");
|
|
46
|
+
const statePath = flag(options, "state");
|
|
47
|
+
const evidencePath = flag(options, "evidence");
|
|
48
|
+
for (const [name, value] of Object.entries({
|
|
49
|
+
"--capsule": capsulePath,
|
|
50
|
+
"--capsule-root": capsuleRoot,
|
|
51
|
+
"--state": statePath,
|
|
52
|
+
"--evidence": evidencePath,
|
|
53
|
+
})) {
|
|
54
|
+
if (!value || !path.isAbsolute(value)) {
|
|
55
|
+
throw new Error(`${name} must be an explicit absolute path`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const capsule = readJson(capsulePath, "--capsule");
|
|
59
|
+
const rehearsalMode = flag(options, "mode", "simulate");
|
|
60
|
+
if (!new Set(["simulate", "replay"]).has(rehearsalMode)) {
|
|
61
|
+
throw new Error("local rehearsal --mode must be simulate or replay");
|
|
62
|
+
}
|
|
63
|
+
const environment = flag(options, "environment-json")
|
|
64
|
+
? readJson(flag(options, "environment-json"), "--environment-json")
|
|
65
|
+
: {};
|
|
66
|
+
let result;
|
|
67
|
+
try {
|
|
68
|
+
result = await executePublicationRehearsal({
|
|
69
|
+
capsule,
|
|
70
|
+
capsuleRoot,
|
|
71
|
+
mode: rehearsalMode,
|
|
72
|
+
environment,
|
|
73
|
+
checkpoint: (transaction) =>
|
|
74
|
+
writeReleaseTailTransaction(statePath, transaction),
|
|
75
|
+
});
|
|
76
|
+
} catch (error) {
|
|
77
|
+
const diagnostic = publicationRehearsalDiagnostic(error, { capsule });
|
|
78
|
+
output(diagnostic, evidencePath);
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
writeReleaseTailTransaction(statePath, result.transaction);
|
|
82
|
+
output(result.evidence, evidencePath);
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
if (mode === "plan") {
|
|
86
|
+
const plan = compileReleaseTailDeclaration(
|
|
87
|
+
readJson(flag(options, "declaration"), "--declaration"),
|
|
88
|
+
);
|
|
89
|
+
output(plan, flag(options, "output"));
|
|
90
|
+
return plan;
|
|
91
|
+
}
|
|
92
|
+
if (mode === "init") {
|
|
93
|
+
const transaction = createReleaseTailTransaction(
|
|
94
|
+
readJson(flag(options, "declaration"), "--declaration"),
|
|
95
|
+
);
|
|
96
|
+
const statePath = flag(
|
|
97
|
+
options,
|
|
98
|
+
"state",
|
|
99
|
+
".buildchain/release-tail/state.json",
|
|
100
|
+
);
|
|
101
|
+
writeReleaseTailTransaction(statePath, transaction);
|
|
102
|
+
output(transaction);
|
|
103
|
+
return transaction;
|
|
104
|
+
}
|
|
105
|
+
if (mode === "status" || mode === "verify") {
|
|
106
|
+
const statePath = flag(
|
|
107
|
+
options,
|
|
108
|
+
"state",
|
|
109
|
+
".buildchain/release-tail/state.json",
|
|
110
|
+
);
|
|
111
|
+
const transaction = readReleaseTailTransaction(statePath);
|
|
112
|
+
const validation = validateReleaseTailTransaction(transaction);
|
|
113
|
+
const nextOperation = transaction.operations.find(
|
|
114
|
+
(entry) => entry.status === "pending",
|
|
115
|
+
);
|
|
116
|
+
const report = {
|
|
117
|
+
schema: "kungfu.buildchain.release-tail.status/v1",
|
|
118
|
+
ok: validation.valid,
|
|
119
|
+
state: transaction.state,
|
|
120
|
+
stateRoot: transaction.stateRoot,
|
|
121
|
+
transactionRoot: transaction.transactionRoot,
|
|
122
|
+
nextOperation: nextOperation
|
|
123
|
+
? {
|
|
124
|
+
operationId: nextOperation.operationId,
|
|
125
|
+
capabilityId: nextOperation.capabilityId,
|
|
126
|
+
adapter: nextOperation.effect.adapter,
|
|
127
|
+
}
|
|
128
|
+
: null,
|
|
129
|
+
receiptRoots: transaction.receipts.map((receipt) => receipt.receiptRoot),
|
|
130
|
+
issues: validation.issues,
|
|
131
|
+
};
|
|
132
|
+
output(report, flag(options, "output"));
|
|
133
|
+
return report;
|
|
134
|
+
}
|
|
135
|
+
if (mode === "compat") {
|
|
136
|
+
const report = diagnoseLegacyReleaseTailHooks(
|
|
137
|
+
readJson(flag(options, "hooks-json"), "--hooks-json"),
|
|
138
|
+
);
|
|
139
|
+
output(report, flag(options, "output"));
|
|
140
|
+
if (!report.compatible) process.exitCode = 1;
|
|
141
|
+
return report;
|
|
142
|
+
}
|
|
143
|
+
throw new Error(
|
|
144
|
+
"usage: buildchain release-tail <plan|init|status|verify|compat|rehearse> [--declaration <json-or-path>] [--capsule <path>] [--capsule-root <absolute-path>] [--mode <simulate|replay>] [--state <path>] [--evidence <path>] [--hooks-json <json-or-path>] [--output <path>]",
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (
|
|
149
|
+
!process.env.BUILDCHAIN_EMBEDDED_ENTRYPOINT &&
|
|
150
|
+
process.argv[1] &&
|
|
151
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
152
|
+
) {
|
|
153
|
+
try {
|
|
154
|
+
await runReleaseTailCli();
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error(`release-tail: ${error.message}`);
|
|
157
|
+
process.exitCode = 1;
|
|
158
|
+
}
|
|
159
|
+
}
|