@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.
Files changed (71) hide show
  1. package/architecture/agent-change-map.md +52 -1
  2. package/architecture/ci-lane-change-budget.json +349 -0
  3. package/architecture/internal-capabilities.json +44 -1
  4. package/architecture/maintainability-debt.json +53 -33
  5. package/architecture/maintainability-policy.json +20 -15
  6. package/architecture/v3-core-mechanism-inventory.json +1 -0
  7. package/architecture/v4-delivery-warrant-shadow-fixtures.json +5 -5
  8. package/architecture/v4-release-invocation-fixtures.json +20 -0
  9. package/architecture/v4-release-topology.json +160 -84
  10. package/architecture/v4-runtime-semantic-closure.json +4 -1
  11. package/architecture/v4-universal-workflow-bootstrap.json +171 -0
  12. package/architecture/v4-universal-workflow-fault-campaign.json +72 -0
  13. package/architecture/v4-universal-workflow-train-admission.json +40 -0
  14. package/contracts/buildchain-v2-residuals-v1.json +0 -54
  15. package/contracts/v4-release-invocation-v1.schema.json +50 -2
  16. package/dist/site/buildchain-contract.json +87 -42
  17. package/dist/site/buildchain-site.json +15 -10
  18. package/dist/site/capability-registry.json +1 -1
  19. package/dist/site/controller-registry.json +46 -10
  20. package/dist/site/kfd-claims.json +174 -196
  21. package/dist/site/kfd-upstream-aggregate.json +1 -1
  22. package/dist/site/manual-registry.json +2 -2
  23. package/dist/site/node-api-registry.json +95 -20
  24. package/dist/site/page-registry.json +9 -4
  25. package/dist/site/public-surface-audit.json +178 -198
  26. package/dist/site/publication-authority-registry.json +67 -57
  27. package/dist/site/publication-registry.json +4 -4
  28. package/dist/site/site-manifest.json +6 -6
  29. package/dist/site/workflow-registry.json +209 -259
  30. package/docs/node-api-reference.md +25 -22
  31. package/docs/reusable-build-surface.md +16 -0
  32. package/package.json +2 -2
  33. package/packages/core/buildchain-publication-authority.js +3 -2
  34. package/packages/core/ci-lane-change-budget.js +14 -1
  35. package/packages/core/dev-delivery-candidate-identity.js +18 -0
  36. package/packages/core/dev-delivery-execution-transfer.js +6 -7
  37. package/packages/core/dev-delivery-warrant-legacy-recovery.js +4 -2
  38. package/packages/core/dev-delivery-warrant-native-compatibility.js +33 -0
  39. package/packages/core/dev-delivery-warrant-state.js +42 -26
  40. package/packages/core/dev-delivery-warrant.js +8 -0
  41. package/packages/core/dev-delivery-writer-protocol-transition.js +72 -0
  42. package/packages/core/release-tail-product-capabilities.js +29 -0
  43. package/packages/core/release-tail-provider-plane.js +3 -3
  44. package/packages/core/v4-canonical-contracts.js +6 -0
  45. package/packages/core/v4-product-publication.js +387 -0
  46. package/packages/core/v4-protected-publication-source.js +93 -0
  47. package/packages/core/v4-publication-qualification.js +1 -0
  48. package/packages/core/v4-release-invocation.js +72 -3
  49. package/packages/core/v4-universal-workflow-bootstrap.js +586 -0
  50. package/scripts/check-inventory.mjs +13 -22
  51. package/scripts/check-maintainability.mjs +16 -113
  52. package/scripts/check-v4-release-topology.mjs +296 -40
  53. package/scripts/check-v4-universal-workflow-bootstrap.mjs +270 -0
  54. package/scripts/dev-delivery-warrant-store.mjs +73 -7
  55. package/scripts/dev-delivery-warrant-transition.mjs +109 -0
  56. package/scripts/dev-delivery-warrant.mjs +84 -100
  57. package/scripts/dev-pr-auto-merge.mjs +15 -15
  58. package/scripts/generate-channel-build-workflow.mjs +5 -1
  59. package/scripts/generate-channel-promotion-workflow.mjs +5 -3
  60. package/scripts/generate-v4-universal-workflow-facades.mjs +415 -0
  61. package/scripts/maintainability-public-surface.mjs +115 -0
  62. package/scripts/release-candidate-resolver.mjs +23 -24
  63. package/scripts/resume-from-candidate-run.mjs +16 -17
  64. package/scripts/universal-facade-maintainability.mjs +82 -0
  65. package/scripts/v4-product-publication-intent.mjs +114 -0
  66. package/scripts/v4-release-candidate-adapter.mjs +41 -0
  67. package/scripts/v4-universal-workflow-backflow.mjs +212 -0
  68. package/scripts/v4-universal-workflow-engine.mjs +617 -0
  69. package/scripts/v4-universal-workflow-self-dogfood.mjs +205 -0
  70. package/templates/universal-buildchain-bootstrap-recovery.yml +282 -0
  71. 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 sidecarFiles = initialDownloads[0].files.filter(
789
- (file) =>
790
- path.basename(file.path) === "release-candidate-stage-capsules.json",
791
- );
792
- const stageCapsuleSidecar =
793
- sidecarFiles.length === 1
794
- ? readOnlyJson(sidecarFiles, "release-candidate-stage-capsules.json")
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
- runtimeResumeEvidence: runtimeResumeEvidencePath
1027
- ? outputPath(runtimeResumeEvidencePath)
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();