@kungfu-tech/buildchain 4.1.3-alpha.1 → 4.1.3-alpha.10
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/AGENTS.md +41 -75
- package/CONTRIBUTING.md +19 -26
- package/architecture/action-taxonomy.json +24 -0
- package/architecture/agent-change-map.md +266 -10
- package/architecture/capability-state-machine-manifest.json +1 -4
- package/architecture/ci-lane-change-budget.json +190 -807
- package/architecture/core-mechanisms.json +3 -6
- package/architecture/decisions/0005-minimal-consumer-contract.md +122 -0
- package/architecture/decisions/0006-business-attempt-journal.md +89 -0
- package/architecture/decisions/0007-hosted-pipeline-controller.md +106 -0
- package/architecture/decisions/0008-pipeline-product-publication.md +120 -0
- package/architecture/decisions/0009-pipeline-version-preparation.md +186 -0
- package/architecture/internal-capabilities.json +362 -7
- package/architecture/maintainability-debt.json +50 -34
- package/architecture/maintainability-policy.json +8 -8
- package/architecture/minimal-consumer-migration.json +5670 -0
- package/architecture/platform-stage-checkpoints.json +31 -7
- package/architecture/product-upstreams.json +20 -0
- package/architecture/release-topology.json +328 -74
- package/architecture/stage-capsule-qualification.json +3 -1
- package/architecture/universal-workflow-bootstrap.json +15 -1
- package/architecture/universal-workflow-capability-policy.json +4 -4
- package/architecture/universal-workflow-fault-campaign.json +3 -1
- package/architecture/workflow-taxonomy.json +77 -328
- package/dist/readers/business-attempt.cjs +809 -0
- package/dist/site/buildchain-contract.json +16 -68
- package/dist/site/buildchain-site.json +40 -25
- package/dist/site/capability-registry.json +4 -4
- package/dist/site/controller-registry.json +1 -47
- package/dist/site/kfd-claims.json +499 -494
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +3 -3
- package/dist/site/node-api-registry.json +54 -54
- package/dist/site/page-registry.json +33 -18
- package/dist/site/public-surface-audit.json +286 -349
- package/dist/site/publication-authority-registry.json +203 -523
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +7 -7
- package/dist/site/workflow-registry.json +590 -940
- package/docs/code-organization.md +9 -6
- package/docs/node-api-reference.md +102 -102
- package/docs/release-governance.md +10 -12
- package/docs/runtime-entry.md +73 -5
- package/docs/runtime-train-validation.md +39 -11
- package/docs/stable-candidate-patrol.md +6 -5
- package/docs/workflow-catalog.md +9 -32
- package/package.json +4 -3
- package/packages/core/adoption/buildchain-kfd-claims.js +7 -7
- package/packages/core/build/standalone/build.js +7 -1
- package/packages/core/consumer/buildchain-config.js +2 -21
- package/packages/core/consumer/contract/entries.js +85 -0
- package/packages/core/consumer/contract/examples.js +96 -0
- package/packages/core/consumer/contract/identity.js +65 -0
- package/packages/core/consumer/contract/inspection.js +45 -0
- package/packages/core/consumer/contract/plan.js +149 -0
- package/packages/core/consumer/contract/products.js +119 -0
- package/packages/core/consumer/contract/reader.js +47 -0
- package/packages/core/consumer/contract/shape.js +46 -0
- package/packages/core/dev-delivery/candidate/admission.js +9 -5
- package/packages/core/dev-delivery/candidate/reservation.js +11 -0
- package/packages/core/dev-delivery/candidate/source-paths.js +12 -4
- package/packages/core/dev-delivery/commands/project-cut-merge-queue-admission.mjs +47 -20
- package/packages/core/dev-delivery/dev-delivery-provider-heartbeat.js +18 -15
- package/packages/core/dev-delivery/native/actions.js +9 -1
- package/packages/core/dev-delivery/native/heartbeat-action.js +16 -0
- package/packages/core/dev-delivery/native/heartbeat.js +6 -4
- package/packages/core/dev-delivery/native/process-entry.js +19 -0
- package/packages/core/dev-delivery/queue/landing-action.js +53 -0
- package/packages/core/dev-delivery/warrant/failure-settlement.js +8 -6
- package/packages/core/dev-delivery/warrant/service.js +7 -0
- package/packages/core/governance/buildchain-publication-authority.js +8 -23
- package/packages/core/governance/github-governance-authority.js +1 -0
- package/packages/core/observability/controller-evidence.js +0 -8
- package/packages/core/paper/operations/bootstrap.js +2 -0
- package/packages/core/paper/paper-npm-bootstrap.js +2 -2
- package/packages/core/providers/github/attempt-index.js +60 -0
- package/packages/core/providers/github/attempt-journal.js +154 -0
- package/packages/core/providers/github/discussions/materials.js +19 -6
- package/packages/core/providers/github/pipeline-checkout.js +49 -0
- package/packages/core/providers/github/pipeline-entry-runs.js +210 -0
- package/packages/core/providers/github/pipeline-events.js +131 -0
- package/packages/core/providers/github/pipeline-integration.js +95 -0
- package/packages/core/providers/github/pipeline-policy.js +240 -0
- package/packages/core/providers/github/pipeline-product-payload.js +50 -0
- package/packages/core/providers/github/pipeline-product-release.js +198 -0
- package/packages/core/providers/github/pipeline-publication-artifacts.js +153 -0
- package/packages/core/providers/github/pipeline-release-evidence.js +30 -0
- package/packages/core/providers/github/pipeline-run-entry.js +86 -0
- package/packages/core/providers/github/pipeline-runs.js +132 -0
- package/packages/core/providers/github/pipeline-source.js +195 -0
- package/packages/core/providers/github/pipeline-stable-baseline.js +86 -0
- package/packages/core/providers/github/pipeline-stable-source.js +372 -0
- package/packages/core/providers/github/pipeline-version-artifacts.js +91 -0
- package/packages/core/providers/github/pipeline-version-readback.js +117 -0
- package/packages/core/providers/github/pipeline-version.js +199 -0
- package/packages/core/providers/github/pipeline-worker.js +108 -0
- package/packages/core/providers/github/release-evidence-assets.js +97 -0
- package/packages/core/publication/candidate/registry-hydration.js +2 -2
- package/packages/core/publication/npm/pack-preview.js +2 -2
- package/packages/core/publication/npm/pack-result.js +31 -0
- package/packages/core/publication/npm/package.js +3 -6
- package/packages/core/publication/npm/pipeline-channel.js +36 -0
- package/packages/core/publication/npm/pipeline-provider.js +137 -0
- package/packages/core/publication/npm/registry.js +12 -9
- package/packages/core/publication/pipeline/actions.js +116 -0
- package/packages/core/publication/pipeline/apply.js +189 -0
- package/packages/core/publication/pipeline/build-segments.js +107 -0
- package/packages/core/publication/pipeline/capsules.js +92 -0
- package/packages/core/publication/pipeline/context.js +39 -0
- package/packages/core/publication/pipeline/development-anchor.js +91 -0
- package/packages/core/publication/pipeline/development-current.js +63 -0
- package/packages/core/publication/pipeline/development-pr.js +112 -0
- package/packages/core/publication/pipeline/development-proof.js +76 -0
- package/packages/core/publication/pipeline/development-transition.js +112 -0
- package/packages/core/publication/pipeline/distribution.js +130 -0
- package/packages/core/publication/pipeline/documents.js +192 -0
- package/packages/core/publication/pipeline/effects.js +192 -0
- package/packages/core/publication/pipeline/files.js +66 -0
- package/packages/core/publication/pipeline/imported-materials.js +32 -0
- package/packages/core/publication/pipeline/journal.js +122 -0
- package/packages/core/publication/pipeline/next-development.js +117 -0
- package/packages/core/publication/pipeline/pack.js +180 -0
- package/packages/core/publication/pipeline/package-policy.js +22 -0
- package/packages/core/publication/pipeline/plan.js +136 -0
- package/packages/core/publication/pipeline/prepare.js +139 -0
- package/packages/core/publication/pipeline/qualification.js +175 -0
- package/packages/core/publication/pipeline/qualify.js +104 -0
- package/packages/core/publication/pipeline/recovery-admission.js +122 -0
- package/packages/core/publication/pipeline/recovery-build-download.js +80 -0
- package/packages/core/publication/pipeline/recovery-build-plan.js +122 -0
- package/packages/core/publication/pipeline/recovery-build-readback.js +68 -0
- package/packages/core/publication/pipeline/recovery-capsules.js +54 -0
- package/packages/core/publication/pipeline/recovery-import.js +80 -0
- package/packages/core/publication/pipeline/recovery-inspection.js +218 -0
- package/packages/core/publication/pipeline/recovery-materials.js +104 -0
- package/packages/core/publication/pipeline/recovery-plan.js +46 -0
- package/packages/core/publication/pipeline/recovery-prepare.js +73 -0
- package/packages/core/publication/pipeline/recovery-qualification.js +128 -0
- package/packages/core/publication/pipeline/recovery-readback.js +56 -0
- package/packages/core/publication/pipeline/recovery-signing.js +58 -0
- package/packages/core/publication/pipeline/sealed-products.js +79 -0
- package/packages/core/publication/pipeline/settle.js +42 -0
- package/packages/core/publication/pipeline/signing.js +140 -0
- package/packages/core/publication/pipeline/source-plan.js +83 -0
- package/packages/core/publication/pipeline/stable-entry.js +219 -0
- package/packages/core/publication/pipeline/stable-products.js +317 -0
- package/packages/core/publication/pipeline/stable-wait.js +170 -0
- package/packages/core/publication/pipeline/stable.js +82 -0
- package/packages/core/publication/pipeline/version-actions.js +93 -0
- package/packages/core/publication/pipeline/version-build.js +130 -0
- package/packages/core/publication/pipeline/version-context.js +143 -0
- package/packages/core/publication/pipeline/version-material.js +36 -0
- package/packages/core/publication/pipeline/version-preparation.js +69 -0
- package/packages/core/publication/pipeline/version-regeneration.js +162 -0
- package/packages/core/publication/pipeline/version-source-guard.js +69 -0
- package/packages/core/publication/pipeline/version.js +81 -0
- package/packages/core/publication/pipeline/worker.js +72 -0
- package/packages/core/publication/publication-reproducibility.js +2 -2
- package/packages/core/release/next-development-transition.js +15 -15
- package/packages/core/release/promote-candidate/product-provider-adapters.js +4 -3
- package/packages/core/release/promote-ref/internal/npm-existing-evidence.js +2 -1
- package/packages/core/release/promote-ref/internal/publish-command.js +16 -5
- package/packages/core/release/stable-release-gate.js +11 -0
- package/packages/core/runtime/buildchain-domain.wasm +0 -0
- package/packages/core/runtime/domain-wasm-artifact.js +2 -2
- package/packages/core/runtime/entry/actions.js +27 -25
- package/packages/core/runtime/entry/attempt.js +25 -0
- package/packages/core/runtime/entry/selection.js +14 -0
- package/packages/core/runtime/entry/source.js +62 -0
- package/packages/core/workflow/attempt/identity.js +170 -0
- package/packages/core/workflow/attempt/journal.js +89 -0
- package/packages/core/workflow/attempt/materials.js +72 -0
- package/packages/core/workflow/attempt/reader-entry.js +12 -0
- package/packages/core/workflow/attempt/reader.js +149 -0
- package/packages/core/workflow/attempt/records.js +153 -0
- package/packages/core/workflow/attempt/store.js +75 -0
- package/packages/core/workflow/commands/pipeline-build.mjs +23 -0
- package/packages/core/workflow/pipeline/action-output.js +11 -0
- package/packages/core/workflow/pipeline/actions.js +52 -0
- package/packages/core/workflow/pipeline/build-control.js +140 -0
- package/packages/core/workflow/pipeline/build-evidence.js +210 -0
- package/packages/core/workflow/pipeline/build-qualification.js +145 -0
- package/packages/core/workflow/pipeline/build-result.js +78 -0
- package/packages/core/workflow/pipeline/build.js +142 -0
- package/packages/core/workflow/pipeline/cancellation.js +169 -0
- package/packages/core/workflow/pipeline/channel-control.js +157 -0
- package/packages/core/workflow/pipeline/channel-source.js +53 -0
- package/packages/core/workflow/pipeline/controller.js +141 -0
- package/packages/core/workflow/pipeline/delivery-control.js +249 -0
- package/packages/core/workflow/pipeline/delivery-observation.js +124 -0
- package/packages/core/workflow/pipeline/delivery-request.js +198 -0
- package/packages/core/workflow/pipeline/events.js +84 -0
- package/packages/core/workflow/pipeline/fence.js +42 -0
- package/packages/core/workflow/pipeline/group-control.js +59 -0
- package/packages/core/workflow/pipeline/guard-build.js +74 -0
- package/packages/core/workflow/pipeline/guard.js +158 -0
- package/packages/core/workflow/pipeline/host.js +144 -0
- package/packages/core/workflow/pipeline/integration-status.js +113 -0
- package/packages/core/workflow/pipeline/materials.js +58 -0
- package/packages/core/workflow/pipeline/notifications.js +36 -0
- package/packages/core/workflow/pipeline/parent-notification.js +27 -0
- package/packages/core/workflow/pipeline/platforms.js +21 -0
- package/packages/core/workflow/pipeline/progress.js +108 -0
- package/packages/core/workflow/pipeline/projection.js +20 -0
- package/packages/core/workflow/pipeline/reconcile.js +194 -0
- package/packages/core/workflow/pipeline/record-action.js +27 -0
- package/packages/core/workflow/pipeline/recorder-status.js +69 -0
- package/packages/core/workflow/pipeline/recovery-action.js +30 -0
- package/packages/core/workflow/pipeline/recovery-admission.js +122 -0
- package/packages/core/workflow/pipeline/recovery-build-control.js +95 -0
- package/packages/core/workflow/pipeline/recovery-build-evidence.js +71 -0
- package/packages/core/workflow/pipeline/recovery-build.js +121 -0
- package/packages/core/workflow/pipeline/recovery-controller.js +91 -0
- package/packages/core/workflow/pipeline/recovery-integration.js +79 -0
- package/packages/core/workflow/pipeline/recovery-merge-proof.js +90 -0
- package/packages/core/workflow/pipeline/recovery-ownership-settlement.js +150 -0
- package/packages/core/workflow/pipeline/recovery-ownership.js +91 -0
- package/packages/core/workflow/pipeline/recovery-plan.js +126 -0
- package/packages/core/workflow/pipeline/recovery-runtime.js +68 -0
- package/packages/core/workflow/pipeline/recovery-session.js +129 -0
- package/packages/core/workflow/pipeline/recovery-transition.js +135 -0
- package/packages/core/workflow/pipeline/recovery-unrecorded-build.js +74 -0
- package/packages/core/workflow/pipeline/runtime-source.js +111 -0
- package/packages/core/workflow/pipeline/selection.js +111 -0
- package/packages/core/workflow/pipeline/session.js +72 -0
- package/packages/core/workflow/pipeline/settlement.js +137 -0
- package/packages/core/workflow/pipeline/wake.js +38 -0
- package/packages/core/workflow/pipeline/web-status.js +108 -0
- package/packages/core/workflow/workflow-taxonomy.mjs +67 -47
- package/scripts/build-release-discussion-reader.mjs +11 -7
- package/scripts/check-inventory.mjs +7 -70
- package/scripts/check-pipeline-publication-topology.mjs +50 -0
- package/scripts/check-public-dogfood-contract.mjs +38 -51
- package/scripts/check-release-topology.mjs +15 -8
- package/scripts/check-self-consumer-contract.mjs +133 -0
- package/scripts/check-universal-workflow-bootstrap.mjs +4 -6
- package/scripts/generate-minimal-consumer-contract.mjs +155 -0
- package/scripts/generate-site-bundle.mjs +1 -1
- package/scripts/inventory/binary.mjs +9 -19
- package/scripts/inventory/promotion.mjs +2 -13
- package/scripts/prepare-product-toolchain.mjs +10 -0
- package/scripts/verify-golden-path.mjs +5 -1
- package/scripts/verify-product-platform.mjs +65 -0
- package/scripts/verify-standalone-product.mjs +42 -0
- package/templates/minimal-consumer/binary/.buildchain/buildchain.toml +45 -0
- package/templates/minimal-consumer/binary/.github/workflows/buildchain-recover.yml +30 -0
- package/templates/minimal-consumer/binary/.github/workflows/buildchain.yml +28 -0
- package/templates/minimal-consumer/binary/package.json +6 -0
- package/templates/minimal-consumer/binary/src/build.mjs +5 -0
- package/templates/minimal-consumer/binary/src/hello.c +2 -0
- package/templates/minimal-consumer/binary/src/verify.mjs +3 -0
- package/templates/minimal-consumer/npm/.buildchain/buildchain.toml +46 -0
- package/templates/minimal-consumer/npm/.github/workflows/buildchain-recover.yml +30 -0
- package/templates/minimal-consumer/npm/.github/workflows/buildchain.yml +28 -0
- package/templates/minimal-consumer/npm/package.json +6 -0
- package/templates/minimal-consumer/npm/src/build.mjs +5 -0
- package/templates/minimal-consumer/npm/src/verify.mjs +3 -0
- package/templates/minimal-consumer/paper/.buildchain/buildchain.toml +45 -0
- package/templates/minimal-consumer/paper/.github/workflows/buildchain-recover.yml +30 -0
- package/templates/minimal-consumer/paper/.github/workflows/buildchain.yml +28 -0
- package/templates/minimal-consumer/paper/package.json +6 -0
- package/templates/minimal-consumer/paper/src/build.mjs +12 -0
- package/templates/minimal-consumer/paper/src/verify.mjs +3 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { choice } from "./shape.js";
|
|
2
|
+
import { consumerWorkflows } from "./entries.js";
|
|
3
|
+
import { CONFIG_PATH } from "./plan.js";
|
|
4
|
+
import { PRODUCT_TYPES } from "./products.js";
|
|
5
|
+
|
|
6
|
+
const ARTIFACTS = {
|
|
7
|
+
npm: { path: "dist/package", kind: "npm-package", provider: "npm" },
|
|
8
|
+
binary: {
|
|
9
|
+
path: "dist/hello.tar.gz",
|
|
10
|
+
kind: "archive",
|
|
11
|
+
provider: "github-release",
|
|
12
|
+
},
|
|
13
|
+
paper: { path: "dist/paper.pdf", kind: "pdf", provider: "github-release" },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function productSource(type) {
|
|
17
|
+
const prefix =
|
|
18
|
+
'import fs from "node:fs";\nfs.mkdirSync("dist", { recursive: true });\n';
|
|
19
|
+
if (type === "npm")
|
|
20
|
+
return `${prefix}fs.mkdirSync("dist/package", { recursive: true });\nfs.writeFileSync("dist/package/index.js", 'export const hello = "hello";\\n');\nfs.writeFileSync("dist/package/package.json", JSON.stringify({ name: "@example/minimal", version: JSON.parse(fs.readFileSync("package.json")).version, type: "module", exports: "./index.js" }));\n`;
|
|
21
|
+
if (type === "binary")
|
|
22
|
+
return `${prefix}import { execFileSync } from "node:child_process";\nexecFileSync("cc", ["src/hello.c", "-o", "dist/hello"], { stdio: "inherit" });\nexecFileSync("tar", ["-czf", "dist/hello.tar.gz", "-C", "dist", "hello"], { stdio: "inherit" });\n`;
|
|
23
|
+
return `${prefix}const stream = "BT /F1 24 Tf 50 700 Td (Minimal Paper) Tj ET";
|
|
24
|
+
const objects = ["<< /Type /Catalog /Pages 2 0 R >>", "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>", "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>", \`<< /Length \${stream.length} >>\\nstream\\n\${stream}\\nendstream\`];
|
|
25
|
+
let pdf = "%PDF-1.4\\n";
|
|
26
|
+
const offsets = [0];
|
|
27
|
+
for (const [index, object] of objects.entries()) { offsets.push(Buffer.byteLength(pdf)); pdf += \`\${index + 1} 0 obj\\n\${object}\\nendobj\\n\`; }
|
|
28
|
+
const xref = Buffer.byteLength(pdf);
|
|
29
|
+
pdf += \`xref\\n0 \${offsets.length}\\n0000000000 65535 f \\n\`;
|
|
30
|
+
for (const offset of offsets.slice(1)) pdf += \`\${String(offset).padStart(10, "0")} 00000 n \\n\`;
|
|
31
|
+
pdf += \`trailer\\n<< /Size \${offsets.length} /Root 1 0 R >>\\nstartxref\\n\${xref}\\n%%EOF\\n\`;
|
|
32
|
+
fs.writeFileSync("dist/paper.pdf", pdf);
|
|
33
|
+
`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function standardConsumerExample(type, channel = "v4") {
|
|
37
|
+
choice(type, PRODUCT_TYPES, "product.type");
|
|
38
|
+
const artifact = ARTIFACTS[type];
|
|
39
|
+
const npmAccess = type === "npm" ? 'access = "public"\n' : "";
|
|
40
|
+
const files = {
|
|
41
|
+
...consumerWorkflows(channel),
|
|
42
|
+
"package.json": `${JSON.stringify({ name: `minimal-${type}`, private: true, version: "1.0.0-alpha.1", type: "module" }, null, 2)}\n`,
|
|
43
|
+
"src/build.mjs": productSource(type),
|
|
44
|
+
"src/verify.mjs": `import assert from "node:assert/strict";\nimport fs from "node:fs";\nassert.ok(fs.statSync(${JSON.stringify(artifact.path)}).${type === "npm" ? "isDirectory()" : "size > 0"});\n`,
|
|
45
|
+
[CONFIG_PATH]: `schema = 2
|
|
46
|
+
|
|
47
|
+
[[products]]
|
|
48
|
+
id = "main"
|
|
49
|
+
type = "${type}"
|
|
50
|
+
platforms = ["linux-x64"]
|
|
51
|
+
build = ["node src/build.mjs"]
|
|
52
|
+
verify = ["node src/verify.mjs"]
|
|
53
|
+
|
|
54
|
+
[[products.artifacts]]
|
|
55
|
+
id = "main"
|
|
56
|
+
path = "${artifact.path}"
|
|
57
|
+
kind = "${artifact.kind}"
|
|
58
|
+
|
|
59
|
+
[[products.targets]]
|
|
60
|
+
provider = "${artifact.provider}"
|
|
61
|
+
artifacts = ["main"]
|
|
62
|
+
${npmAccess}
|
|
63
|
+
[version]
|
|
64
|
+
strategy = "semver"
|
|
65
|
+
|
|
66
|
+
[[version.files]]
|
|
67
|
+
path = "package.json"
|
|
68
|
+
format = "json"
|
|
69
|
+
key = "version"
|
|
70
|
+
|
|
71
|
+
[[channels]]
|
|
72
|
+
from = "feature/*"
|
|
73
|
+
to = "dev/v1/v1.0"
|
|
74
|
+
operation = "develop"
|
|
75
|
+
|
|
76
|
+
[[channels]]
|
|
77
|
+
from = "dev/v1/v1.0"
|
|
78
|
+
to = "alpha/v1/v1.0"
|
|
79
|
+
operation = "alpha"
|
|
80
|
+
|
|
81
|
+
[[channels]]
|
|
82
|
+
from = "alpha/v1/v1.0"
|
|
83
|
+
to = "release/v1/v1.0"
|
|
84
|
+
operation = "stable"
|
|
85
|
+
|
|
86
|
+
[review]
|
|
87
|
+
minimum_approvals = 1
|
|
88
|
+
code_owners = true
|
|
89
|
+
merge_queue = true
|
|
90
|
+
`,
|
|
91
|
+
};
|
|
92
|
+
if (type === "binary")
|
|
93
|
+
files["src/hello.c"] =
|
|
94
|
+
'#include <stdio.h>\nint main(void) { puts("hello"); return 0; }\n';
|
|
95
|
+
return files;
|
|
96
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { object, relativePath, text } from "./shape.js";
|
|
3
|
+
import { compileConsumerPlan, CONSUMER_CONTRACT } from "./plan.js";
|
|
4
|
+
|
|
5
|
+
export function contentDigest(bytes) {
|
|
6
|
+
return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Source identities are prepared from trusted GitHub readback, never caller inputs.
|
|
10
|
+
export function bindConsumerSource(source, configBytes) {
|
|
11
|
+
object(
|
|
12
|
+
source,
|
|
13
|
+
["repository", "commit", "tree", "configPath", "configBlob"],
|
|
14
|
+
[],
|
|
15
|
+
"source",
|
|
16
|
+
);
|
|
17
|
+
text(
|
|
18
|
+
source.repository,
|
|
19
|
+
"source.repository",
|
|
20
|
+
/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u,
|
|
21
|
+
);
|
|
22
|
+
for (const key of ["commit", "tree", "configBlob"])
|
|
23
|
+
text(source[key], `source.${key}`, /^[0-9a-f]{40}$/u);
|
|
24
|
+
relativePath(source.configPath, "source.configPath");
|
|
25
|
+
const bytes = Buffer.from(configBytes);
|
|
26
|
+
const blob = createHash("sha1")
|
|
27
|
+
.update(`blob ${bytes.length}\0`)
|
|
28
|
+
.update(bytes)
|
|
29
|
+
.digest("hex");
|
|
30
|
+
if (blob !== source.configBlob)
|
|
31
|
+
throw new Error("source.configBlob: config bytes do not match Git blob");
|
|
32
|
+
const plan = compileConsumerPlan(bytes.toString("utf8"));
|
|
33
|
+
const identity = {
|
|
34
|
+
schema: "buildchain.consumer-source/v1",
|
|
35
|
+
repository: source.repository,
|
|
36
|
+
commit: source.commit,
|
|
37
|
+
tree: source.tree,
|
|
38
|
+
configPath: source.configPath,
|
|
39
|
+
configBlob: blob,
|
|
40
|
+
configDigest: contentDigest(bytes),
|
|
41
|
+
contract: CONSUMER_CONTRACT,
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
identity,
|
|
45
|
+
identityDigest: contentDigest(JSON.stringify(identity)),
|
|
46
|
+
plan,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function consumerContractLock({ entry, runtime, configDigest }) {
|
|
51
|
+
for (const [name, value] of Object.entries({ entry, runtime })) {
|
|
52
|
+
object(value, ["repository", "sha"], [], name);
|
|
53
|
+
if (value.repository !== "kungfu-systems/buildchain")
|
|
54
|
+
throw new Error(`${name}: untrusted Buildchain repository`);
|
|
55
|
+
text(value.sha, `${name}.sha`, /^[0-9a-f]{40}$/u);
|
|
56
|
+
}
|
|
57
|
+
text(configDigest, "configDigest", /^sha256:[0-9a-f]{64}$/u);
|
|
58
|
+
return {
|
|
59
|
+
schema: "buildchain.consumer-contract-lock/v2",
|
|
60
|
+
contract: CONSUMER_CONTRACT,
|
|
61
|
+
entry: { ...entry },
|
|
62
|
+
runtime: { ...runtime },
|
|
63
|
+
configDigest,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { consumerWorkflows } from "./entries.js";
|
|
2
|
+
import { compileConsumerPlan, CONFIG_PATH } from "./plan.js";
|
|
3
|
+
|
|
4
|
+
const INTERNAL_WIRING = [
|
|
5
|
+
/\b(?:request-json|native-roots-json|used-nonces-json|warrant-result|source-proof|integration-proof)\b/iu,
|
|
6
|
+
/\bbuildchain\s+(?:dev\s+(?:warrant|proof)|release|promote|recover|publish)\b/iu,
|
|
7
|
+
/\b(?:npm\s+publish|gh\s+release\s+(?:create|upload)|gh\s+run\s+download)\b/iu,
|
|
8
|
+
/actions\/download-artifact@/u,
|
|
9
|
+
/\.buildchain\/runtime\/|packages\/core\/(?:publication|release|dev-delivery)\//u,
|
|
10
|
+
/\b(?:fencingToken|candidateRoot|sourceProofRoot|usedNonces|generationRoot|compareAndSwap)\b/u,
|
|
11
|
+
/buildchain[-./][\w-]*(?:warrant|proof|fence|nonce|candidate|authority|transaction)/iu,
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
// The supplied file map must be the complete tracked consumer tree, including
|
|
15
|
+
// package scripts and the source files they invoke. Runtime source is never run.
|
|
16
|
+
export function inspectConsumerContract(
|
|
17
|
+
files,
|
|
18
|
+
{ channel = "v4", configPath = CONFIG_PATH } = {},
|
|
19
|
+
) {
|
|
20
|
+
const issues = [];
|
|
21
|
+
let plan;
|
|
22
|
+
try {
|
|
23
|
+
plan = compileConsumerPlan(files[configPath]);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
issues.push(`${configPath}: ${error.message}`);
|
|
26
|
+
}
|
|
27
|
+
const workflows = consumerWorkflows(channel, configPath);
|
|
28
|
+
for (const [file, expected] of Object.entries(workflows))
|
|
29
|
+
if (files[file] !== expected)
|
|
30
|
+
issues.push(`${file}: must match the generated thin caller`);
|
|
31
|
+
for (const [file, source] of Object.entries(files)) {
|
|
32
|
+
if (
|
|
33
|
+
/^\.github\/workflows\/.*\.ya?ml$/u.test(file) &&
|
|
34
|
+
!Object.hasOwn(workflows, file)
|
|
35
|
+
)
|
|
36
|
+
issues.push(`${file}: extra consumer workflow is forbidden`);
|
|
37
|
+
if (!/\.(?:toml|json|ya?ml|[cm]?js|ts|py|sh|ps1|rb)$/u.test(file)) continue;
|
|
38
|
+
for (const pattern of INTERNAL_WIRING)
|
|
39
|
+
if (pattern.test(String(source)))
|
|
40
|
+
issues.push(
|
|
41
|
+
`${file}: consumer owns internal orchestration (${pattern.source})`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return { ok: issues.length === 0, issues, plan };
|
|
45
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { parse } from "smol-toml";
|
|
2
|
+
import { choice, list, object, relativePath, text, unique } from "./shape.js";
|
|
3
|
+
import { compileProducts } from "./products.js";
|
|
4
|
+
|
|
5
|
+
export const CONSUMER_CONTRACT = "buildchain.consumer-contract/v2";
|
|
6
|
+
export const CONSUMER_PLAN = "buildchain.consumer-plan/v1";
|
|
7
|
+
export const CONFIG_PATH = ".buildchain/buildchain.toml";
|
|
8
|
+
|
|
9
|
+
function compileVersion(value) {
|
|
10
|
+
object(value, ["strategy", "files"], ["derived_files"], "version");
|
|
11
|
+
choice(value.strategy, ["semver", "anchored"], "version.strategy");
|
|
12
|
+
list(value.files, "version.files", (item, field) => {
|
|
13
|
+
object(item, ["path", "format", "key"], [], field);
|
|
14
|
+
relativePath(item.path, `${field}.path`);
|
|
15
|
+
choice(item.format, ["json", "toml"], `${field}.format`);
|
|
16
|
+
text(item.key, `${field}.key`, /^[A-Za-z0-9_.-]+$/u);
|
|
17
|
+
});
|
|
18
|
+
unique(
|
|
19
|
+
value.files.map((item) => `${item.path}:${item.key}`),
|
|
20
|
+
"version.files",
|
|
21
|
+
);
|
|
22
|
+
if (value.derived_files !== undefined)
|
|
23
|
+
list(value.derived_files, "version.derived_files", relativePath);
|
|
24
|
+
return structuredClone(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function compileChannels(values) {
|
|
28
|
+
const channels = list(values, "channels", (value, field) => {
|
|
29
|
+
object(value, ["from", "to", "operation"], [], field);
|
|
30
|
+
text(
|
|
31
|
+
value.from,
|
|
32
|
+
`${field}.from`,
|
|
33
|
+
/^(?:feature|fix|chore|docs|ci|refactor|dev|alpha|release)\/[A-Za-z0-9/*._-]+$/u,
|
|
34
|
+
);
|
|
35
|
+
text(
|
|
36
|
+
value.to,
|
|
37
|
+
`${field}.to`,
|
|
38
|
+
/^(?:dev|alpha|release|publish-gate)\/[A-Za-z0-9/._-]+$/u,
|
|
39
|
+
);
|
|
40
|
+
choice(
|
|
41
|
+
value.operation,
|
|
42
|
+
["develop", "alpha", "stable", "major"],
|
|
43
|
+
`${field}.operation`,
|
|
44
|
+
);
|
|
45
|
+
const source = value.from.split("/")[0],
|
|
46
|
+
target = value.to.split("/")[0];
|
|
47
|
+
const legal = {
|
|
48
|
+
develop:
|
|
49
|
+
["feature", "fix", "chore", "docs", "ci", "refactor"].includes(
|
|
50
|
+
source,
|
|
51
|
+
) && target === "dev",
|
|
52
|
+
alpha: source === "dev" && target === "alpha",
|
|
53
|
+
stable: source === "alpha" && target === "release",
|
|
54
|
+
major: source === "release" && value.to === "publish-gate/major",
|
|
55
|
+
};
|
|
56
|
+
if (
|
|
57
|
+
!legal[value.operation] ||
|
|
58
|
+
value.from.includes("..") ||
|
|
59
|
+
value.to.includes("..")
|
|
60
|
+
)
|
|
61
|
+
throw new Error(`${field}: unlawful channel route`);
|
|
62
|
+
return { ...value };
|
|
63
|
+
});
|
|
64
|
+
unique(
|
|
65
|
+
channels.map((value) => `${value.from}:${value.to}`),
|
|
66
|
+
"channels",
|
|
67
|
+
);
|
|
68
|
+
return channels;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function compileReview(value) {
|
|
72
|
+
object(
|
|
73
|
+
value,
|
|
74
|
+
["minimum_approvals", "code_owners", "merge_queue"],
|
|
75
|
+
[],
|
|
76
|
+
"review",
|
|
77
|
+
);
|
|
78
|
+
if (!Number.isInteger(value.minimum_approvals) || value.minimum_approvals < 1)
|
|
79
|
+
throw new Error(
|
|
80
|
+
"review.minimum_approvals: independent approval is required",
|
|
81
|
+
);
|
|
82
|
+
if (value.code_owners !== true || value.merge_queue !== true)
|
|
83
|
+
throw new Error(
|
|
84
|
+
"review: code owners and protected merge queue are required",
|
|
85
|
+
);
|
|
86
|
+
return { ...value };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function compileStable(value) {
|
|
90
|
+
object(
|
|
91
|
+
value,
|
|
92
|
+
[
|
|
93
|
+
"minimum_interval_seconds",
|
|
94
|
+
"minimum_soak_seconds",
|
|
95
|
+
"product_paths",
|
|
96
|
+
"impact_file",
|
|
97
|
+
"require_published_entry",
|
|
98
|
+
],
|
|
99
|
+
[],
|
|
100
|
+
"stable",
|
|
101
|
+
);
|
|
102
|
+
for (const field of ["minimum_interval_seconds", "minimum_soak_seconds"])
|
|
103
|
+
if (!Number.isSafeInteger(value[field]) || value[field] < 0)
|
|
104
|
+
throw new Error(
|
|
105
|
+
`stable.${field}: expected non-negative safe integer seconds`,
|
|
106
|
+
);
|
|
107
|
+
const paths = list(
|
|
108
|
+
value.product_paths,
|
|
109
|
+
"stable.product_paths",
|
|
110
|
+
(item, field) => {
|
|
111
|
+
text(item, field);
|
|
112
|
+
relativePath(item.endsWith("/") ? item.slice(0, -1) : item, field);
|
|
113
|
+
if (item.includes("*"))
|
|
114
|
+
throw new Error(`${field}: expected a literal product path`);
|
|
115
|
+
return item;
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
unique(paths, "stable.product_paths");
|
|
119
|
+
relativePath(value.impact_file, "stable.impact_file");
|
|
120
|
+
if (!value.impact_file.endsWith(".json") || value.impact_file.includes("*"))
|
|
121
|
+
throw new Error(
|
|
122
|
+
"stable.impact_file: expected one product impact JSON file",
|
|
123
|
+
);
|
|
124
|
+
if (typeof value.require_published_entry !== "boolean")
|
|
125
|
+
throw new Error("stable.require_published_entry: expected a boolean");
|
|
126
|
+
return structuredClone(value);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// This compiler receives bytes. It has no filesystem, process, network or provider port.
|
|
130
|
+
export function compileConsumerPlan(source) {
|
|
131
|
+
const config = parse(source);
|
|
132
|
+
object(
|
|
133
|
+
config,
|
|
134
|
+
["schema", "products", "version", "channels", "review"],
|
|
135
|
+
["stable"],
|
|
136
|
+
);
|
|
137
|
+
choice(config.schema, [2], "schema");
|
|
138
|
+
return {
|
|
139
|
+
schema: CONSUMER_PLAN,
|
|
140
|
+
contract: CONSUMER_CONTRACT,
|
|
141
|
+
products: compileProducts(config.products),
|
|
142
|
+
version: compileVersion(config.version),
|
|
143
|
+
channels: compileChannels(config.channels),
|
|
144
|
+
review: compileReview(config.review),
|
|
145
|
+
...(config.stable === undefined
|
|
146
|
+
? {}
|
|
147
|
+
: { stable: compileStable(config.stable) }),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {
|
|
2
|
+
choice,
|
|
3
|
+
list,
|
|
4
|
+
object,
|
|
5
|
+
relativePath,
|
|
6
|
+
slug,
|
|
7
|
+
text,
|
|
8
|
+
unique,
|
|
9
|
+
} from "./shape.js";
|
|
10
|
+
|
|
11
|
+
export const PRODUCT_TYPES = ["npm", "binary", "paper"];
|
|
12
|
+
export const PLATFORMS = [
|
|
13
|
+
"linux-x64",
|
|
14
|
+
"linux-arm64",
|
|
15
|
+
"macos-arm64",
|
|
16
|
+
"macos-x64",
|
|
17
|
+
"windows-x64",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function artifact(value, location) {
|
|
21
|
+
object(value, ["id", "path", "kind"], ["filename"], location);
|
|
22
|
+
slug(value.id, `${location}.id`);
|
|
23
|
+
if (value.path !== "." || value.kind !== "npm-package")
|
|
24
|
+
relativePath(value.path, `${location}.path`);
|
|
25
|
+
choice(value.kind, ["npm-package", "archive", "pdf"], `${location}.kind`);
|
|
26
|
+
if (value.filename !== undefined) {
|
|
27
|
+
text(
|
|
28
|
+
value.filename,
|
|
29
|
+
`${location}.filename`,
|
|
30
|
+
/^[A-Za-z0-9][A-Za-z0-9._-]{0,254}$/u,
|
|
31
|
+
);
|
|
32
|
+
const extension =
|
|
33
|
+
value.kind === "npm-package"
|
|
34
|
+
? /\.tgz$/u
|
|
35
|
+
: value.kind === "pdf"
|
|
36
|
+
? /\.pdf$/u
|
|
37
|
+
: /\.(?:tar\.gz|tar\.xz|tgz|zip|tar)$/u;
|
|
38
|
+
if (!extension.test(value.filename))
|
|
39
|
+
throw new Error(
|
|
40
|
+
`${location}.filename: artifact extension does not match its kind`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return { ...value };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function target(value, location) {
|
|
47
|
+
object(value, ["provider", "artifacts"], ["access"], location);
|
|
48
|
+
choice(value.provider, ["npm", "github-release"], `${location}.provider`);
|
|
49
|
+
list(value.artifacts, `${location}.artifacts`, slug);
|
|
50
|
+
unique(value.artifacts, `${location}.artifacts`);
|
|
51
|
+
if (value.provider === "npm")
|
|
52
|
+
choice(value.access, ["public", "restricted"], `${location}.access`);
|
|
53
|
+
else if (Object.hasOwn(value, "access"))
|
|
54
|
+
throw new Error(`${location}.access: npm only`);
|
|
55
|
+
return structuredClone(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function compileProducts(values) {
|
|
59
|
+
const products = list(values, "products", (value, location) => {
|
|
60
|
+
object(
|
|
61
|
+
value,
|
|
62
|
+
["id", "type", "platforms", "build", "verify", "artifacts", "targets"],
|
|
63
|
+
["install", "directory"],
|
|
64
|
+
location,
|
|
65
|
+
);
|
|
66
|
+
slug(value.id, `${location}.id`);
|
|
67
|
+
choice(value.type, PRODUCT_TYPES, `${location}.type`);
|
|
68
|
+
list(value.platforms, `${location}.platforms`, (item, field) =>
|
|
69
|
+
choice(item, PLATFORMS, field),
|
|
70
|
+
);
|
|
71
|
+
unique(value.platforms, `${location}.platforms`);
|
|
72
|
+
for (const operation of [
|
|
73
|
+
"build",
|
|
74
|
+
"verify",
|
|
75
|
+
...(value.install !== undefined ? ["install"] : []),
|
|
76
|
+
])
|
|
77
|
+
list(value[operation], `${location}.${operation}`, text);
|
|
78
|
+
if (value.directory !== undefined)
|
|
79
|
+
relativePath(value.directory, `${location}.directory`);
|
|
80
|
+
const artifacts = list(value.artifacts, `${location}.artifacts`, artifact);
|
|
81
|
+
unique(
|
|
82
|
+
artifacts.map((item) => item.id),
|
|
83
|
+
`${location}.artifacts`,
|
|
84
|
+
);
|
|
85
|
+
const targets = list(value.targets, `${location}.targets`, target);
|
|
86
|
+
unique(
|
|
87
|
+
targets.map((item) => item.provider),
|
|
88
|
+
`${location}.targets`,
|
|
89
|
+
);
|
|
90
|
+
const expectedKind = {
|
|
91
|
+
npm: "npm-package",
|
|
92
|
+
binary: "archive",
|
|
93
|
+
paper: "pdf",
|
|
94
|
+
}[value.type];
|
|
95
|
+
if (artifacts.some((item) => item.kind !== expectedKind))
|
|
96
|
+
throw new Error(`${location}: artifact kind does not match product type`);
|
|
97
|
+
const published = targets.flatMap((item) => item.artifacts);
|
|
98
|
+
if (
|
|
99
|
+
artifacts.some((item) => !published.includes(item.id)) ||
|
|
100
|
+
published.some((id) => !artifacts.some((item) => item.id === id))
|
|
101
|
+
)
|
|
102
|
+
throw new Error(
|
|
103
|
+
`${location}: publication targets must cover exactly declared artifacts`,
|
|
104
|
+
);
|
|
105
|
+
if (
|
|
106
|
+
targets.some((item) => item.provider === "npm") !==
|
|
107
|
+
(value.type === "npm")
|
|
108
|
+
)
|
|
109
|
+
throw new Error(
|
|
110
|
+
`${location}: npm products require an npm target; other products cannot publish to npm`,
|
|
111
|
+
);
|
|
112
|
+
return structuredClone({ ...value, artifacts, targets });
|
|
113
|
+
});
|
|
114
|
+
unique(
|
|
115
|
+
products.map((item) => item.id),
|
|
116
|
+
"products",
|
|
117
|
+
);
|
|
118
|
+
return products;
|
|
119
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parse } from "smol-toml";
|
|
4
|
+
import { resolveBuildchainConfigPath } from "../../contracts/buildchain-layout.js";
|
|
5
|
+
import { compileConsumerPlan } from "./plan.js";
|
|
6
|
+
|
|
7
|
+
function productConfiguration(source, config) {
|
|
8
|
+
const plan = compileConsumerPlan(source);
|
|
9
|
+
// Read-only version tooling uses the established normalized field names.
|
|
10
|
+
// Product commands remain in the plan; no publish lifecycle is synthesized.
|
|
11
|
+
return {
|
|
12
|
+
...config,
|
|
13
|
+
version: {
|
|
14
|
+
required: true,
|
|
15
|
+
strategy: plan.version.strategy,
|
|
16
|
+
next: plan.version.strategy === "anchored" ? "manual" : "auto",
|
|
17
|
+
files: plan.version.files.map(({ format, ...file }) => ({
|
|
18
|
+
...file,
|
|
19
|
+
type: format,
|
|
20
|
+
})),
|
|
21
|
+
derivedFiles: plan.version.derived_files || [],
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function readProductConfiguration(cwd, normalizeLegacy) {
|
|
27
|
+
const configPath = resolveBuildchainConfigPath(cwd);
|
|
28
|
+
const filePath = path.join(cwd, configPath);
|
|
29
|
+
if (!fs.existsSync(filePath)) return undefined;
|
|
30
|
+
const source = fs.readFileSync(filePath, "utf8");
|
|
31
|
+
let config;
|
|
32
|
+
try {
|
|
33
|
+
config = parse(source);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw new Error(`${configPath} parse failed: ${error.message}`);
|
|
36
|
+
}
|
|
37
|
+
if (![1, 2].includes(config.schema))
|
|
38
|
+
throw new Error(`${configPath} schema must be 1 or 2`);
|
|
39
|
+
return {
|
|
40
|
+
path: configPath,
|
|
41
|
+
filePath,
|
|
42
|
+
config:
|
|
43
|
+
config.schema === 2
|
|
44
|
+
? productConfiguration(source, config)
|
|
45
|
+
: normalizeLegacy(config),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Closed data validation shared by TOML, event identities and managed locks.
|
|
2
|
+
export function object(value, required, optional = [], location = "config") {
|
|
3
|
+
if (!value || Object.getPrototypeOf(value) !== Object.prototype)
|
|
4
|
+
throw new Error(`${location}: expected a table`);
|
|
5
|
+
for (const key of Object.keys(value))
|
|
6
|
+
if (![...required, ...optional].includes(key))
|
|
7
|
+
throw new Error(`${location}.${key}: unknown field`);
|
|
8
|
+
for (const key of required)
|
|
9
|
+
if (!Object.hasOwn(value, key))
|
|
10
|
+
throw new Error(`${location}.${key}: required field`);
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function text(value, location, pattern = /\S/u) {
|
|
15
|
+
if (typeof value !== "string" || !pattern.test(value) || value.includes("\0"))
|
|
16
|
+
throw new Error(`${location}: invalid string`);
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function choice(value, choices, location) {
|
|
21
|
+
if (!choices.includes(value))
|
|
22
|
+
throw new Error(`${location}: expected ${choices.join(" or ")}`);
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function list(value, location, validate) {
|
|
27
|
+
if (!Array.isArray(value) || !value.length)
|
|
28
|
+
throw new Error(`${location}: expected a nonempty list`);
|
|
29
|
+
return value.map((item, index) => validate(item, `${location}[${index}]`));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function unique(values, location) {
|
|
33
|
+
if (new Set(values).size !== values.length)
|
|
34
|
+
throw new Error(`${location}: duplicate identity`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function relativePath(value, location) {
|
|
38
|
+
text(value, location, /^[A-Za-z0-9_.][A-Za-z0-9_./*-]*$/u);
|
|
39
|
+
if (value.split("/").some((part) => !part || part === ".." || part === "."))
|
|
40
|
+
throw new Error(`${location}: expected a repository-relative path`);
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function slug(value, location) {
|
|
45
|
+
return text(value, location, /^[a-z][a-z0-9-]*$/u);
|
|
46
|
+
}
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
|
-
import {
|
|
5
|
-
sourceCoordinates,
|
|
6
|
-
validateNativeContract,
|
|
7
|
-
} from "./coordinates.js";
|
|
4
|
+
import { sourceCoordinates, validateNativeContract } from "./coordinates.js";
|
|
8
5
|
import {
|
|
9
6
|
verifyProjectCutReplayProof,
|
|
10
7
|
createSourceQualificationProof,
|
|
@@ -16,6 +13,7 @@ import { assertBranchUnlocked } from "../../providers/dev-delivery/protection.js
|
|
|
16
13
|
import { deriveSourcePaths } from "./source-paths.js";
|
|
17
14
|
import { admissionPolicyRequest } from "../admission/request.js";
|
|
18
15
|
import { runAdmissionTransaction } from "../admission/transaction.js";
|
|
16
|
+
import { guardPipelineAdmission } from "../../workflow/pipeline/guard.js";
|
|
19
17
|
export function admitDeliveryRequest(input, defaultBranch) {
|
|
20
18
|
const target = sourceCoordinates({
|
|
21
19
|
branch: input["target-branch"],
|
|
@@ -46,6 +44,7 @@ export async function qualifyDeliverySource(
|
|
|
46
44
|
},
|
|
47
45
|
dependencies = {},
|
|
48
46
|
) {
|
|
47
|
+
const pipeline = await guardPipelineAdmission(request, { repository, token });
|
|
49
48
|
const outputs = {
|
|
50
49
|
"runtime-sha": runtimeSha,
|
|
51
50
|
"qualify-outcome": "skipped",
|
|
@@ -146,7 +145,12 @@ export async function qualifyDeliverySource(
|
|
|
146
145
|
if (!affectedPaths.length)
|
|
147
146
|
affectedPaths = await (dependencies.derivePaths || deriveSourcePaths)(
|
|
148
147
|
source,
|
|
149
|
-
{
|
|
148
|
+
{
|
|
149
|
+
token,
|
|
150
|
+
apiUrl,
|
|
151
|
+
environment,
|
|
152
|
+
qualifiedBase: pipeline?.history.at(-1).generation.baseCommit,
|
|
153
|
+
},
|
|
150
154
|
);
|
|
151
155
|
const receiptRoot = qualification.result.receiptRoot;
|
|
152
156
|
if (!/^sha256:[0-9a-f]{64}$/u.test(receiptRoot || ""))
|
|
@@ -10,6 +10,8 @@ import { dispatchDeliveryHandoff } from "../warrant/handoff.js";
|
|
|
10
10
|
import { resolveCandidateAffectedPaths } from "./evidence.js";
|
|
11
11
|
import { deliverySubmissionRequest } from "./submission.js";
|
|
12
12
|
import { jsonList } from "../warrant/values.js";
|
|
13
|
+
import { guardPipelineAdmission } from "../../workflow/pipeline/guard.js";
|
|
14
|
+
import { wakePipelineCandidate } from "../../workflow/pipeline/wake.js";
|
|
13
15
|
export async function reserveDeliveryCandidate(
|
|
14
16
|
{
|
|
15
17
|
workspace,
|
|
@@ -31,6 +33,7 @@ export async function reserveDeliveryCandidate(
|
|
|
31
33
|
);
|
|
32
34
|
if (!predecessorsOk || proofOutcome !== "success")
|
|
33
35
|
throw new Error("Delivery candidate submission failed");
|
|
36
|
+
await guardPipelineAdmission(input, connection);
|
|
34
37
|
const service =
|
|
35
38
|
dependencies.service ||
|
|
36
39
|
createDeliveryWarrantService({ ...connection, branch });
|
|
@@ -77,6 +80,14 @@ export async function reserveDeliveryCandidate(
|
|
|
77
80
|
warrant.pullRequestNumber !== Number(input["expected-pr-number"]) ||
|
|
78
81
|
warrant.sourceHead !== input["expected-head-sha"]
|
|
79
82
|
) {
|
|
83
|
+
if (input["pipeline-attempt"]) {
|
|
84
|
+
await wakePipelineCandidate(warrant, { ...connection, branch });
|
|
85
|
+
return {
|
|
86
|
+
"warrant-outcome": "success",
|
|
87
|
+
"warrant-handoff-required": "true",
|
|
88
|
+
"warrant-handoff-dispatched": "true",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
80
91
|
outputs = await dispatchDeliveryHandoff(
|
|
81
92
|
{
|
|
82
93
|
warrant,
|
|
@@ -52,7 +52,7 @@ export function pathsAtQualifiedSource(directory, qualifiedBase, source) {
|
|
|
52
52
|
|
|
53
53
|
export async function deriveSourcePaths(
|
|
54
54
|
source,
|
|
55
|
-
{ token, apiUrl, environment = process.env },
|
|
55
|
+
{ token, apiUrl, environment = process.env, qualifiedBase },
|
|
56
56
|
provider = new GitHubTwoPhaseClient({
|
|
57
57
|
repository: source.repository,
|
|
58
58
|
token,
|
|
@@ -65,10 +65,18 @@ export async function deriveSourcePaths(
|
|
|
65
65
|
/^[1-9]\d*$/u.test(String(source.sourceWorkflowRunId || "")),
|
|
66
66
|
"Exact source run coordinates are required",
|
|
67
67
|
);
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
if (qualifiedBase === undefined) {
|
|
69
|
+
const run = await provider.request(
|
|
70
|
+
`/repos/${source.repository}/actions/runs/${source.sourceWorkflowRunId}`,
|
|
71
|
+
);
|
|
72
|
+
qualifiedBase = qualifiedRunBase(run, source);
|
|
73
|
+
}
|
|
74
|
+
// The internal pipeline guard already requalifies the source run and immutable
|
|
75
|
+
// generation. Zero-delta recovery still verifies the full source identity below.
|
|
76
|
+
requireValue(
|
|
77
|
+
/^[0-9a-f]{40}$/u.test(qualifiedBase || ""),
|
|
78
|
+
"Exact qualified source base is required",
|
|
70
79
|
);
|
|
71
|
-
const qualifiedBase = qualifiedRunBase(run, source);
|
|
72
80
|
const directory = fs.mkdtempSync(
|
|
73
81
|
path.join(os.tmpdir(), "buildchain-source-paths-"),
|
|
74
82
|
);
|