@kungfu-tech/buildchain 4.0.2-alpha.2 → 4.0.2-alpha.21
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 +52 -1
- package/architecture/ci-lane-change-budget.json +349 -0
- package/architecture/internal-capabilities.json +44 -1
- package/architecture/maintainability-debt.json +53 -33
- package/architecture/maintainability-policy.json +20 -15
- 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 +160 -84
- package/architecture/v4-runtime-semantic-closure.json +4 -1
- package/architecture/v4-universal-workflow-bootstrap.json +171 -0
- package/architecture/v4-universal-workflow-fault-campaign.json +72 -0
- package/architecture/v4-universal-workflow-train-admission.json +40 -0
- package/contracts/buildchain-v2-residuals-v1.json +0 -54
- package/contracts/v4-release-invocation-v1.schema.json +50 -2
- package/dist/site/buildchain-contract.json +87 -42
- package/dist/site/buildchain-site.json +15 -10
- package/dist/site/capability-registry.json +1 -1
- package/dist/site/controller-registry.json +46 -10
- package/dist/site/kfd-claims.json +174 -196
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +2 -2
- package/dist/site/node-api-registry.json +95 -20
- package/dist/site/page-registry.json +9 -4
- package/dist/site/public-surface-audit.json +178 -198
- package/dist/site/publication-authority-registry.json +67 -57
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +6 -6
- package/dist/site/workflow-registry.json +209 -259
- package/docs/node-api-reference.md +25 -22
- package/docs/reusable-build-surface.md +16 -0
- package/package.json +2 -2
- package/packages/core/buildchain-publication-authority.js +3 -2
- package/packages/core/ci-lane-change-budget.js +14 -1
- package/packages/core/dev-delivery-candidate-identity.js +18 -0
- 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/release-tail-product-capabilities.js +29 -0
- package/packages/core/release-tail-provider-plane.js +3 -3
- package/packages/core/v4-canonical-contracts.js +6 -0
- package/packages/core/v4-product-publication.js +387 -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/packages/core/v4-universal-workflow-bootstrap.js +586 -0
- package/scripts/check-inventory.mjs +13 -22
- package/scripts/check-maintainability.mjs +16 -113
- package/scripts/check-v4-release-topology.mjs +296 -40
- package/scripts/check-v4-universal-workflow-bootstrap.mjs +270 -0
- package/scripts/dev-delivery-warrant-store.mjs +73 -7
- package/scripts/dev-delivery-warrant-transition.mjs +109 -0
- package/scripts/dev-delivery-warrant.mjs +84 -100
- package/scripts/dev-pr-auto-merge.mjs +15 -15
- package/scripts/generate-channel-build-workflow.mjs +5 -1
- package/scripts/generate-channel-promotion-workflow.mjs +5 -3
- package/scripts/generate-v4-universal-workflow-facades.mjs +415 -0
- package/scripts/maintainability-public-surface.mjs +115 -0
- package/scripts/release-candidate-resolver.mjs +23 -24
- package/scripts/resume-from-candidate-run.mjs +16 -17
- package/scripts/universal-facade-maintainability.mjs +82 -0
- package/scripts/v4-product-publication-intent.mjs +114 -0
- package/scripts/v4-release-candidate-adapter.mjs +41 -0
- package/scripts/v4-universal-workflow-backflow.mjs +212 -0
- package/scripts/v4-universal-workflow-engine.mjs +617 -0
- package/scripts/v4-universal-workflow-self-dogfood.mjs +205 -0
- package/templates/universal-buildchain-bootstrap-recovery.yml +282 -0
- package/templates/universal-buildchain-bootstrap.yml +28 -0
|
@@ -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
|
|
|
@@ -822,7 +821,7 @@ export async function resolveRecoveryTransaction({
|
|
|
822
821
|
transactionId,
|
|
823
822
|
publicationVersion,
|
|
824
823
|
}) {
|
|
825
|
-
let transaction = publicationVersion
|
|
824
|
+
let transaction = transactionId && publicationVersion
|
|
826
825
|
? await readExistingTransaction({
|
|
827
826
|
repoInfo,
|
|
828
827
|
apiUrl,
|
|
@@ -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,
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
function loadUniversalFacadeMigration(root) {
|
|
6
|
+
const contract = JSON.parse(
|
|
7
|
+
fs.readFileSync(
|
|
8
|
+
path.join(root, "architecture/v4-universal-workflow-bootstrap.json"),
|
|
9
|
+
"utf8",
|
|
10
|
+
),
|
|
11
|
+
);
|
|
12
|
+
const retired = new Set(contract.retiredWorkflowSurfaces || []);
|
|
13
|
+
const paths = (contract.bootstrapGovernedWorkflows || []).filter(
|
|
14
|
+
(entry) =>
|
|
15
|
+
entry !== contract.bootstrap?.publicWorkflow && !retired.has(entry),
|
|
16
|
+
);
|
|
17
|
+
const sourceRevision = contract.migration?.facadeSourceRevision || "";
|
|
18
|
+
if (!/^[0-9a-f]{40}$/u.test(sourceRevision)) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
"universal facade migration requires an exact facadeSourceRevision",
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return { paths: new Set(paths), sourceRevision };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function verifyUniversalFacadeMigration(root) {
|
|
27
|
+
execFileSync(
|
|
28
|
+
process.execPath,
|
|
29
|
+
["scripts/generate-v4-universal-workflow-facades.mjs", "--check"],
|
|
30
|
+
{ cwd: root, stdio: "pipe" },
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function governUniversalFacadeWorkflowMetrics({
|
|
35
|
+
root,
|
|
36
|
+
current,
|
|
37
|
+
workflowMetricsAtRevision,
|
|
38
|
+
}) {
|
|
39
|
+
const migration = loadUniversalFacadeMigration(root);
|
|
40
|
+
verifyUniversalFacadeMigration(root);
|
|
41
|
+
const frozen = workflowMetricsAtRevision(root, migration.sourceRevision);
|
|
42
|
+
const governed = { ...current, workflows: { ...current.workflows } };
|
|
43
|
+
for (const file of migration.paths) {
|
|
44
|
+
if (!frozen[file])
|
|
45
|
+
throw new Error(`frozen universal facade source is missing ${file}`);
|
|
46
|
+
governed.workflows[file] = frozen[file];
|
|
47
|
+
}
|
|
48
|
+
return { governed, migration };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isGeneratedFacadePublicTransition({
|
|
52
|
+
entry,
|
|
53
|
+
kind,
|
|
54
|
+
migration,
|
|
55
|
+
baseContract,
|
|
56
|
+
currentContract,
|
|
57
|
+
}) {
|
|
58
|
+
if (kind !== "workflow" || !migration?.paths.has(entry.path) || !baseContract)
|
|
59
|
+
return false;
|
|
60
|
+
const { inputs: currentInputs, ...currentRest } = currentContract;
|
|
61
|
+
const { inputs: previousInputs, ...previousRest } = baseContract;
|
|
62
|
+
if (JSON.stringify(currentRest) !== JSON.stringify(previousRest))
|
|
63
|
+
return false;
|
|
64
|
+
const added = currentInputs.filter(
|
|
65
|
+
(input) => !previousInputs.includes(input),
|
|
66
|
+
);
|
|
67
|
+
const removed = previousInputs.filter(
|
|
68
|
+
(input) => !currentInputs.includes(input),
|
|
69
|
+
);
|
|
70
|
+
return (
|
|
71
|
+
removed.length === 0 &&
|
|
72
|
+
added.length === 1 &&
|
|
73
|
+
added[0] === "universal-request-json"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export {
|
|
78
|
+
governUniversalFacadeWorkflowMetrics,
|
|
79
|
+
isGeneratedFacadePublicTransition,
|
|
80
|
+
loadUniversalFacadeMigration,
|
|
81
|
+
verifyUniversalFacadeMigration,
|
|
82
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
import { spawnSyncCommand } from "../packages/core/spawn-command.js";
|
|
8
|
+
import { selectV4ProductPublicationIntent } from "../packages/core/v4-product-publication.js";
|
|
9
|
+
import { v4ContentRoot } from "../packages/core/v4-canonical-contracts.js";
|
|
10
|
+
|
|
11
|
+
function env(name, required = false) {
|
|
12
|
+
const value = String(process.env[name] || "").trim();
|
|
13
|
+
if (required && !value) throw new Error(`${name} is required`);
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readJson(file) {
|
|
18
|
+
return JSON.parse(fs.readFileSync(path.resolve(file), "utf8"));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function packageName() {
|
|
22
|
+
const declared = env("BUILDCHAIN_PUBLISH_PACKAGE_MAIN");
|
|
23
|
+
if (declared) return declared;
|
|
24
|
+
const manifestPath = env("BUILDCHAIN_SEALED_BUNDLE_MANIFEST", true);
|
|
25
|
+
const manifest = readJson(manifestPath);
|
|
26
|
+
const selected = String(manifest?.npm?.name || "").trim();
|
|
27
|
+
if (!selected)
|
|
28
|
+
throw new Error("sealed bundle manifest does not declare npm.name");
|
|
29
|
+
return selected;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function observedVersions(name) {
|
|
33
|
+
const result = spawnSyncCommand(
|
|
34
|
+
"npm",
|
|
35
|
+
[
|
|
36
|
+
"view",
|
|
37
|
+
name,
|
|
38
|
+
"versions",
|
|
39
|
+
"--json",
|
|
40
|
+
"--registry=https://registry.npmjs.org/",
|
|
41
|
+
],
|
|
42
|
+
{ encoding: "utf8" },
|
|
43
|
+
);
|
|
44
|
+
if (result.error) throw result.error;
|
|
45
|
+
if (result.status !== 0) {
|
|
46
|
+
const output = `${result.stdout || ""}\n${result.stderr || ""}`;
|
|
47
|
+
if (/\bE404\b|404 Not Found|is not in this registry/iu.test(output))
|
|
48
|
+
return [];
|
|
49
|
+
throw new Error(`npm version discovery failed: ${output.trim()}`);
|
|
50
|
+
}
|
|
51
|
+
const parsed = JSON.parse(String(result.stdout || "[]"));
|
|
52
|
+
return Array.isArray(parsed) ? parsed : [parsed].filter(Boolean);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function writeOutput(name, value) {
|
|
56
|
+
const output = env("GITHUB_OUTPUT");
|
|
57
|
+
if (!output) return;
|
|
58
|
+
fs.appendFileSync(output, `${name}=${String(value)}\n`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveV4ProductPublicationIntent() {
|
|
62
|
+
const name = packageName();
|
|
63
|
+
const sourceSha = env("BUILDCHAIN_SOURCE_SHA", true);
|
|
64
|
+
const manifest = readJson(env("BUILDCHAIN_SEALED_BUNDLE_MANIFEST", true));
|
|
65
|
+
const requiredArtifacts = readJson(
|
|
66
|
+
env("BUILDCHAIN_REQUIRED_ARTIFACTS_PATH", true),
|
|
67
|
+
);
|
|
68
|
+
const channel = env("BUILDCHAIN_CHANNEL", true);
|
|
69
|
+
const intent = selectV4ProductPublicationIntent({
|
|
70
|
+
channel,
|
|
71
|
+
targetRef: env("BUILDCHAIN_TARGET_REF", true),
|
|
72
|
+
sourceSha,
|
|
73
|
+
sourceTimestamp: env("BUILDCHAIN_SOURCE_TIMESTAMP", true),
|
|
74
|
+
repository: env("BUILDCHAIN_REPOSITORY", true),
|
|
75
|
+
packageName: name,
|
|
76
|
+
distTag:
|
|
77
|
+
env("BUILDCHAIN_PUBLISH_DIST_TAG") ||
|
|
78
|
+
(channel === "alpha" ? "alpha" : "latest"),
|
|
79
|
+
sealedBundleRoot: manifest.root,
|
|
80
|
+
requiredArtifactsRoot: v4ContentRoot(
|
|
81
|
+
"v4-product-required-artifacts",
|
|
82
|
+
requiredArtifacts,
|
|
83
|
+
),
|
|
84
|
+
candidateVersion: env("BUILDCHAIN_CANDIDATE_VERSION", true),
|
|
85
|
+
recoveredVersion: env("BUILDCHAIN_RECOVERED_PUBLICATION_VERSION"),
|
|
86
|
+
observedVersions: observedVersions(name),
|
|
87
|
+
});
|
|
88
|
+
const outputPath = path.resolve(
|
|
89
|
+
env("BUILDCHAIN_PRODUCT_PUBLICATION_INTENT_PATH") ||
|
|
90
|
+
".buildchain/release-candidate/v4-product-publication-intent.json",
|
|
91
|
+
);
|
|
92
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
93
|
+
fs.writeFileSync(outputPath, `${JSON.stringify(intent, null, 2)}\n`);
|
|
94
|
+
writeOutput("version", intent.version);
|
|
95
|
+
writeOutput("exact-tag", intent.exactTag);
|
|
96
|
+
writeOutput("intent-path", outputPath);
|
|
97
|
+
writeOutput("intent-root", intent.intentRoot);
|
|
98
|
+
return { name, intent, outputPath };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (
|
|
102
|
+
process.argv[1] &&
|
|
103
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
104
|
+
) {
|
|
105
|
+
try {
|
|
106
|
+
const result = resolveV4ProductPublicationIntent();
|
|
107
|
+
process.stdout.write(
|
|
108
|
+
`v4 product publication intent: ${result.name}@${result.intent.version} (${result.intent.mode})\n`,
|
|
109
|
+
);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error(`v4-product-publication-intent: ${error.message}`);
|
|
112
|
+
process.exitCode = 1;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
|
|
9
|
+
export function resolveV4ReleaseCandidateAdapter({
|
|
10
|
+
resumeCandidateRunId = "",
|
|
11
|
+
} = {}) {
|
|
12
|
+
const mode = String(resumeCandidateRunId || "").trim() ? "recovery" : "fresh";
|
|
13
|
+
return Object.freeze({
|
|
14
|
+
mode,
|
|
15
|
+
script:
|
|
16
|
+
mode === "recovery"
|
|
17
|
+
? "scripts/resume-from-candidate-run.mjs"
|
|
18
|
+
: "scripts/release-candidate-resolver.mjs",
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function runV4ReleaseCandidateAdapter({
|
|
23
|
+
env = process.env,
|
|
24
|
+
exec = execFileSync,
|
|
25
|
+
} = {}) {
|
|
26
|
+
const route = resolveV4ReleaseCandidateAdapter({
|
|
27
|
+
resumeCandidateRunId: env.BUILDCHAIN_RESUME_CANDIDATE_RUN_ID,
|
|
28
|
+
});
|
|
29
|
+
exec(process.execPath, [path.join(root, route.script)], {
|
|
30
|
+
env,
|
|
31
|
+
stdio: "inherit",
|
|
32
|
+
});
|
|
33
|
+
return route;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (
|
|
37
|
+
process.argv[1] &&
|
|
38
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
39
|
+
) {
|
|
40
|
+
runV4ReleaseCandidateAdapter();
|
|
41
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import {
|
|
6
|
+
validateV4UniversalWorkflowRequest,
|
|
7
|
+
v4UniversalWorkflowRequestRoot,
|
|
8
|
+
} from "../packages/core/v4-universal-workflow-bootstrap.js";
|
|
9
|
+
const SHA = /^[0-9a-f]{40}$/u;
|
|
10
|
+
const ROOT = /^sha256:[0-9a-f]{64}$/u;
|
|
11
|
+
|
|
12
|
+
function fail(message) {
|
|
13
|
+
throw new Error(message);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function canonical(value) {
|
|
17
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
18
|
+
if (value && typeof value === "object")
|
|
19
|
+
return Object.fromEntries(
|
|
20
|
+
Object.keys(value)
|
|
21
|
+
.sort((left, right) => Buffer.from(left).compare(Buffer.from(right)))
|
|
22
|
+
.map((key) => [key, canonical(value[key])]),
|
|
23
|
+
);
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function contentRoot(domain, value) {
|
|
28
|
+
const hash = crypto.createHash("sha256");
|
|
29
|
+
hash.update(domain, "utf8");
|
|
30
|
+
hash.update(Buffer.from([0]));
|
|
31
|
+
hash.update(`${JSON.stringify(canonical(value))}\n`, "utf8");
|
|
32
|
+
return `sha256:${hash.digest("hex")}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function validateTerminalReceipt(value, request) {
|
|
36
|
+
if (
|
|
37
|
+
value?.schema !==
|
|
38
|
+
"kungfu-buildchain-v4-universal-workflow-terminal-receipt/v1" ||
|
|
39
|
+
value.status !== "succeeded" ||
|
|
40
|
+
value.runtime?.repository !== request.candidate.repository ||
|
|
41
|
+
value.runtime?.sha !== request.candidate.expectedSha ||
|
|
42
|
+
value.requestRoot !== v4UniversalWorkflowRequestRoot(request) ||
|
|
43
|
+
!SHA.test(value.runtime?.sha || "") ||
|
|
44
|
+
!ROOT.test(value.receiptRoot || "")
|
|
45
|
+
)
|
|
46
|
+
fail(
|
|
47
|
+
"successful exact-candidate terminal receipt is required for backflow",
|
|
48
|
+
);
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createV4UniversalBackflowPlan({
|
|
53
|
+
request: requestValue,
|
|
54
|
+
receipt,
|
|
55
|
+
}) {
|
|
56
|
+
const request = validateV4UniversalWorkflowRequest(requestValue);
|
|
57
|
+
if (request.mode !== "train") fail("only Train delivery can create backflow");
|
|
58
|
+
if (request.capability.id !== "release-candidate-promote")
|
|
59
|
+
fail("only a release-candidate-promote delivery can create backflow");
|
|
60
|
+
if (request.payload?.inputs?.["dry-run"] !== false)
|
|
61
|
+
fail("only a non-dry-run release delivery can create backflow");
|
|
62
|
+
const terminal = validateTerminalReceipt(receipt, request);
|
|
63
|
+
const marker = `<!-- buildchain-universal-backflow:${request.candidate.expectedSha} -->`;
|
|
64
|
+
const binding = {
|
|
65
|
+
schema: "kungfu-buildchain-v4-universal-workflow-backflow/v1",
|
|
66
|
+
repository: request.candidate.repository,
|
|
67
|
+
pullRequest: request.candidate.reviewPullRequest,
|
|
68
|
+
baseRef: "dev/v4/v4.0",
|
|
69
|
+
trainSha: request.candidate.expectedSha,
|
|
70
|
+
consumerRepository: request.consumer.repository,
|
|
71
|
+
consumerSha: request.consumer.sourceSha,
|
|
72
|
+
requestRoot: terminal.requestRoot,
|
|
73
|
+
terminalReceiptRoot: terminal.receiptRoot,
|
|
74
|
+
};
|
|
75
|
+
const backflowRoot = contentRoot("universal-workflow-backflow", binding);
|
|
76
|
+
return {
|
|
77
|
+
...binding,
|
|
78
|
+
marker,
|
|
79
|
+
backflowRoot,
|
|
80
|
+
body: [
|
|
81
|
+
marker,
|
|
82
|
+
"## Universal Train backflow",
|
|
83
|
+
"",
|
|
84
|
+
`- Train SHA: \`${binding.trainSha}\``,
|
|
85
|
+
`- Consumer: \`${binding.consumerRepository}@${binding.consumerSha}\``,
|
|
86
|
+
`- Request root: \`${binding.requestRoot}\``,
|
|
87
|
+
`- Terminal receipt root: \`${binding.terminalReceiptRoot}\``,
|
|
88
|
+
`- Backflow root: \`${backflowRoot}\``,
|
|
89
|
+
"",
|
|
90
|
+
"This receipt binds a successful consumer delivery to the protected Train-to-dev backflow PR; it does not designate the Train candidate as a Buildchain release.",
|
|
91
|
+
].join("\n"),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function githubHeaders(token) {
|
|
96
|
+
return {
|
|
97
|
+
accept: "application/vnd.github+json",
|
|
98
|
+
authorization: `Bearer ${token}`,
|
|
99
|
+
"content-type": "application/json",
|
|
100
|
+
"user-agent": "buildchain-universal-backflow",
|
|
101
|
+
"x-github-api-version": "2022-11-28",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function githubJson({ path, method = "GET", token, body, fetchImpl }) {
|
|
106
|
+
const api = String(
|
|
107
|
+
process.env.GITHUB_API_URL || "https://api.github.com",
|
|
108
|
+
).replace(/\/+$/u, "");
|
|
109
|
+
const response = await fetchImpl(`${api}${path}`, {
|
|
110
|
+
method,
|
|
111
|
+
headers: githubHeaders(token),
|
|
112
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
113
|
+
});
|
|
114
|
+
const text = await response.text();
|
|
115
|
+
const value = text ? JSON.parse(text) : {};
|
|
116
|
+
if (!response.ok)
|
|
117
|
+
fail(
|
|
118
|
+
`GitHub API ${method} ${path} failed with ${response.status}: ${value.message || text}`,
|
|
119
|
+
);
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function upsertV4UniversalBackflow({
|
|
124
|
+
request,
|
|
125
|
+
receipt,
|
|
126
|
+
token,
|
|
127
|
+
fetchImpl = globalThis.fetch,
|
|
128
|
+
}) {
|
|
129
|
+
const admitted = validateV4UniversalWorkflowRequest(request);
|
|
130
|
+
if (
|
|
131
|
+
admitted.mode !== "train" ||
|
|
132
|
+
admitted.capability.id !== "release-candidate-promote" ||
|
|
133
|
+
admitted.payload?.inputs?.["dry-run"] !== false
|
|
134
|
+
) {
|
|
135
|
+
return {
|
|
136
|
+
schema: "kungfu-buildchain-v4-universal-workflow-backflow-result/v1",
|
|
137
|
+
action: "skipped",
|
|
138
|
+
reason: "not-a-successful-non-dry-run-release-delivery",
|
|
139
|
+
backflowRoot: "",
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (!token) fail("GH_TOKEN is required for protected backflow");
|
|
143
|
+
if (typeof fetchImpl !== "function") fail("fetch is required for backflow");
|
|
144
|
+
const plan = createV4UniversalBackflowPlan({ request: admitted, receipt });
|
|
145
|
+
const encodedRepository = plan.repository;
|
|
146
|
+
const pull = await githubJson({
|
|
147
|
+
path: `/repos/${encodedRepository}/pulls/${plan.pullRequest}`,
|
|
148
|
+
token,
|
|
149
|
+
fetchImpl,
|
|
150
|
+
});
|
|
151
|
+
if (
|
|
152
|
+
pull.state !== "open" ||
|
|
153
|
+
pull.base?.ref !== plan.baseRef ||
|
|
154
|
+
pull.head?.repo?.full_name !== plan.repository ||
|
|
155
|
+
pull.head?.sha !== plan.trainSha
|
|
156
|
+
)
|
|
157
|
+
fail("backflow PR no longer binds the exact admitted Train candidate");
|
|
158
|
+
const comments = await githubJson({
|
|
159
|
+
path: `/repos/${encodedRepository}/issues/${plan.pullRequest}/comments?per_page=100`,
|
|
160
|
+
token,
|
|
161
|
+
fetchImpl,
|
|
162
|
+
});
|
|
163
|
+
const existing = comments.find((comment) =>
|
|
164
|
+
String(comment.body || "").includes(plan.marker),
|
|
165
|
+
);
|
|
166
|
+
const comment = existing
|
|
167
|
+
? await githubJson({
|
|
168
|
+
path: `/repos/${encodedRepository}/issues/comments/${existing.id}`,
|
|
169
|
+
method: "PATCH",
|
|
170
|
+
token,
|
|
171
|
+
body: { body: plan.body },
|
|
172
|
+
fetchImpl,
|
|
173
|
+
})
|
|
174
|
+
: await githubJson({
|
|
175
|
+
path: `/repos/${encodedRepository}/issues/${plan.pullRequest}/comments`,
|
|
176
|
+
method: "POST",
|
|
177
|
+
token,
|
|
178
|
+
body: { body: plan.body },
|
|
179
|
+
fetchImpl,
|
|
180
|
+
});
|
|
181
|
+
return {
|
|
182
|
+
schema: "kungfu-buildchain-v4-universal-workflow-backflow-result/v1",
|
|
183
|
+
action: existing ? "updated" : "created",
|
|
184
|
+
pullRequest: plan.pullRequest,
|
|
185
|
+
trainSha: plan.trainSha,
|
|
186
|
+
terminalReceiptRoot: plan.terminalReceiptRoot,
|
|
187
|
+
backflowRoot: plan.backflowRoot,
|
|
188
|
+
commentUrl: comment.html_url,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function main() {
|
|
193
|
+
const request = JSON.parse(
|
|
194
|
+
process.env.BUILDCHAIN_UNIVERSAL_REQUEST_JSON ||
|
|
195
|
+
fail("request is required"),
|
|
196
|
+
);
|
|
197
|
+
const receipt = JSON.parse(
|
|
198
|
+
process.env.BUILDCHAIN_UNIVERSAL_TERMINAL_RECEIPT_JSON ||
|
|
199
|
+
fail("terminal receipt is required"),
|
|
200
|
+
);
|
|
201
|
+
const result = await upsertV4UniversalBackflow({
|
|
202
|
+
request,
|
|
203
|
+
receipt,
|
|
204
|
+
token: process.env.GH_TOKEN || process.env.GITHUB_TOKEN,
|
|
205
|
+
});
|
|
206
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const isMain =
|
|
210
|
+
process.argv[1] &&
|
|
211
|
+
fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
|
212
|
+
if (isMain) await main();
|