@kungfu-tech/buildchain 4.1.3-alpha.1 → 4.1.3-alpha.3
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/action-taxonomy.json +24 -0
- package/architecture/agent-change-map.md +256 -2
- package/architecture/ci-lane-change-budget.json +374 -0
- 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 +99 -0
- package/architecture/decisions/0009-pipeline-version-preparation.md +186 -0
- package/architecture/internal-capabilities.json +361 -2
- package/architecture/maintainability-debt.json +6 -4
- package/architecture/maintainability-policy.json +8 -8
- package/architecture/minimal-consumer-migration.json +7567 -0
- package/architecture/product-upstreams.json +20 -0
- package/architecture/release-topology.json +282 -11
- package/architecture/universal-workflow-bootstrap.json +12 -0
- package/architecture/universal-workflow-capability-policy.json +1 -1
- package/architecture/workflow-taxonomy.json +66 -0
- package/dist/readers/business-attempt.cjs +809 -0
- package/dist/site/buildchain-contract.json +7 -5
- package/dist/site/buildchain-site.json +18 -13
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/kfd-claims.json +385 -10
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +1 -1
- package/dist/site/node-api-registry.json +21 -21
- package/dist/site/page-registry.json +13 -8
- package/dist/site/public-surface-audit.json +258 -7
- package/dist/site/publication-authority-registry.json +141 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +5 -5
- package/dist/site/workflow-registry.json +522 -7
- package/docs/node-api-reference.md +39 -39
- package/docs/runtime-entry.md +45 -5
- package/docs/workflow-catalog.md +6 -0
- package/package.json +4 -3
- 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 +146 -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/native/actions.js +7 -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/queue/landing-action.js +5 -0
- package/packages/core/governance/buildchain-publication-authority.js +6 -0
- 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 +7 -2
- package/packages/core/providers/github/pipeline-checkout.js +49 -0
- package/packages/core/providers/github/pipeline-entry-runs.js +203 -0
- package/packages/core/providers/github/pipeline-events.js +52 -0
- package/packages/core/providers/github/pipeline-integration.js +95 -0
- package/packages/core/providers/github/pipeline-policy.js +213 -0
- package/packages/core/providers/github/pipeline-product-payload.js +50 -0
- package/packages/core/providers/github/pipeline-product-release.js +191 -0
- package/packages/core/providers/github/pipeline-publication-artifacts.js +153 -0
- package/packages/core/providers/github/pipeline-release-evidence.js +111 -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 +178 -0
- package/packages/core/providers/github/pipeline-stable-source.js +354 -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/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 +87 -0
- package/packages/core/publication/npm/pipeline-provider.js +109 -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 +188 -0
- package/packages/core/publication/pipeline/build-segments.js +107 -0
- package/packages/core/publication/pipeline/capsules.js +89 -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 +124 -0
- package/packages/core/publication/pipeline/documents.js +192 -0
- package/packages/core/publication/pipeline/effects.js +162 -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 +110 -0
- package/packages/core/publication/pipeline/next-development.js +117 -0
- package/packages/core/publication/pipeline/pack.js +157 -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 +43 -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 +215 -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/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 +87 -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 +75 -0
- package/packages/core/workflow/pipeline/build-control.js +140 -0
- package/packages/core/workflow/pipeline/build-evidence.js +92 -0
- package/packages/core/workflow/pipeline/build-qualification.js +145 -0
- package/packages/core/workflow/pipeline/build-result.js +69 -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 +127 -0
- package/packages/core/workflow/pipeline/controller.js +133 -0
- package/packages/core/workflow/pipeline/delivery-control.js +204 -0
- package/packages/core/workflow/pipeline/delivery-observation.js +118 -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 +56 -0
- package/packages/core/workflow/pipeline/guard-build.js +37 -0
- package/packages/core/workflow/pipeline/guard.js +156 -0
- package/packages/core/workflow/pipeline/host.js +114 -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/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 +66 -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 +106 -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/scripts/build-release-discussion-reader.mjs +11 -7
- package/scripts/check-pipeline-publication-topology.mjs +48 -0
- package/scripts/check-release-topology.mjs +7 -1
- package/scripts/generate-minimal-consumer-contract.mjs +155 -0
- package/scripts/generate-site-bundle.mjs +1 -1
- package/scripts/verify-golden-path.mjs +5 -1
- 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
|
@@ -11,7 +11,12 @@ import {
|
|
|
11
11
|
readLogPath,
|
|
12
12
|
noopLogger,
|
|
13
13
|
} from "./logging.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
platformTriple,
|
|
16
|
+
copyNodeBinary,
|
|
17
|
+
sourceSha,
|
|
18
|
+
packageVersion,
|
|
19
|
+
} from "./identity.js";
|
|
15
20
|
import { bundleCli, postjectArgs } from "./bundle.js";
|
|
16
21
|
import { run } from "./process.js";
|
|
17
22
|
function prepareStandaloneExecutable({
|
|
@@ -124,6 +129,7 @@ export function buildStandaloneBinary({
|
|
|
124
129
|
nodePath = process.execPath,
|
|
125
130
|
nodeVersion = process.version,
|
|
126
131
|
} = {}) {
|
|
132
|
+
version ||= packageVersion(cwd);
|
|
127
133
|
const resolvedOutputDir = path.resolve(cwd, outputDir);
|
|
128
134
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "buildchain-sea-"));
|
|
129
135
|
try {
|
|
@@ -5,8 +5,8 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
5
5
|
import { parse } from "smol-toml";
|
|
6
6
|
import {
|
|
7
7
|
BUILDCHAIN_CONFIG_PATH,
|
|
8
|
-
resolveBuildchainConfigPath,
|
|
9
8
|
} from "../contracts/buildchain-layout.js";
|
|
9
|
+
import { readProductConfiguration } from "./contract/reader.js";
|
|
10
10
|
import { runShellCommandSync } from "../runtime/spawn-command.js";
|
|
11
11
|
import { normalizeBuildConfiguration } from "../build/build-configuration.js";
|
|
12
12
|
|
|
@@ -181,26 +181,7 @@ function updateTomlStringValuePreservingSource({ source, content, key, value, fi
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
export function loadBuildchainConfig(cwd = process.cwd()) {
|
|
184
|
-
|
|
185
|
-
const filePath = path.join(cwd, configPath);
|
|
186
|
-
if (!fs.existsSync(filePath)) {
|
|
187
|
-
return undefined;
|
|
188
|
-
}
|
|
189
|
-
const source = fs.readFileSync(filePath, "utf8");
|
|
190
|
-
let config;
|
|
191
|
-
try {
|
|
192
|
-
config = parse(source);
|
|
193
|
-
} catch (error) {
|
|
194
|
-
throw new Error(`${configPath} parse failed: ${error.message}`);
|
|
195
|
-
}
|
|
196
|
-
if (config.schema !== 1) {
|
|
197
|
-
throw new Error(`${configPath} schema must be 1`);
|
|
198
|
-
}
|
|
199
|
-
return {
|
|
200
|
-
path: configPath,
|
|
201
|
-
filePath,
|
|
202
|
-
config: normalizeBuildchainConfig(config),
|
|
203
|
-
};
|
|
184
|
+
return readProductConfiguration(cwd, normalizeBuildchainConfig);
|
|
204
185
|
}
|
|
205
186
|
|
|
206
187
|
export function normalizeBuildchainConfig(config) {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { choice, object, relativePath, text } from "./shape.js";
|
|
2
|
+
import { CONFIG_PATH } from "./plan.js";
|
|
3
|
+
|
|
4
|
+
export const PIPELINE_ENTRY = ".github/workflows/public-ops-pipeline.yml";
|
|
5
|
+
export const RECOVERY_ENTRY = ".github/workflows/public-ops-recover.yml";
|
|
6
|
+
export const PIPELINE_EVENTS = {
|
|
7
|
+
pull_request: [
|
|
8
|
+
"opened",
|
|
9
|
+
"reopened",
|
|
10
|
+
"synchronize",
|
|
11
|
+
"ready_for_review",
|
|
12
|
+
"converted_to_draft",
|
|
13
|
+
"closed",
|
|
14
|
+
"labeled",
|
|
15
|
+
"unlabeled",
|
|
16
|
+
"edited",
|
|
17
|
+
"enqueued",
|
|
18
|
+
"dequeued",
|
|
19
|
+
],
|
|
20
|
+
pull_request_review: ["submitted", "dismissed"],
|
|
21
|
+
pull_request_target: ["closed"],
|
|
22
|
+
merge_group: ["checks_requested"],
|
|
23
|
+
push: [],
|
|
24
|
+
repository_dispatch: ["buildchain-attempt-wake"],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function normalInputs(value = {}) {
|
|
28
|
+
object(value, [], ["config-path"], "inputs");
|
|
29
|
+
const configPath = relativePath(
|
|
30
|
+
value["config-path"] ?? CONFIG_PATH,
|
|
31
|
+
"inputs.config-path",
|
|
32
|
+
);
|
|
33
|
+
if (!configPath.endsWith(".toml") || configPath.includes("*"))
|
|
34
|
+
throw new Error("inputs.config-path: expected one TOML path");
|
|
35
|
+
return { configPath };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function recoveryInputs(value) {
|
|
39
|
+
object(value, ["attempt"], ["runtime-ref"], "inputs");
|
|
40
|
+
text(value.attempt, "inputs.attempt", /^attempt-[0-9a-f]{64}$/u);
|
|
41
|
+
const runtimeRef = value["runtime-ref"] ?? "";
|
|
42
|
+
if (runtimeRef !== "")
|
|
43
|
+
text(
|
|
44
|
+
runtimeRef,
|
|
45
|
+
"inputs.runtime-ref",
|
|
46
|
+
/^(?:[0-9a-f]{40}|v4(?:-alpha)?|train\/v4\/v4\.[0-9]+\/[a-z0-9][a-z0-9-]*)$/u,
|
|
47
|
+
);
|
|
48
|
+
return { attempt: value.attempt, runtimeRef };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function admitEvent(name, action = "") {
|
|
52
|
+
choice(name, Object.keys(PIPELINE_EVENTS), "event.name");
|
|
53
|
+
choice(
|
|
54
|
+
action,
|
|
55
|
+
PIPELINE_EVENTS[name].length ? PIPELINE_EVENTS[name] : [""],
|
|
56
|
+
"event.action",
|
|
57
|
+
);
|
|
58
|
+
return { name, action };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function call(entry, channel, extra = "") {
|
|
62
|
+
return `permissions:\n contents: read\njobs:\n buildchain:\n uses: kungfu-systems/buildchain/${entry}@${channel}\n${extra} secrets: inherit\n permissions:\n actions: write\n attestations: write\n checks: write\n contents: write\n discussions: write\n id-token: write\n pull-requests: write\n statuses: write\n`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Product-independent source of the two caller bytes. Public entry publication
|
|
66
|
+
// is qualified separately; rendering this contract is not an execution receipt.
|
|
67
|
+
export function consumerWorkflows(channel = "v4", configPath = CONFIG_PATH) {
|
|
68
|
+
choice(channel, ["v4", "v4-alpha"], "entry.channel");
|
|
69
|
+
normalInputs({ "config-path": configPath });
|
|
70
|
+
const configInput =
|
|
71
|
+
configPath === CONFIG_PATH
|
|
72
|
+
? ""
|
|
73
|
+
: ` with:\n config-path: ${configPath}\n`;
|
|
74
|
+
const events = Object.entries(PIPELINE_EVENTS)
|
|
75
|
+
.map(([name, types]) => {
|
|
76
|
+
return types.length
|
|
77
|
+
? ` ${name}:\n types: [${types.join(", ")}]\n`
|
|
78
|
+
: ` ${name}:\n`;
|
|
79
|
+
})
|
|
80
|
+
.join("");
|
|
81
|
+
return {
|
|
82
|
+
".github/workflows/buildchain.yml": `name: Buildchain pipeline\non:\n${events}${call(PIPELINE_ENTRY, channel, configInput)}`,
|
|
83
|
+
".github/workflows/buildchain-recover.yml": `name: Buildchain recovery\non:\n workflow_dispatch:\n inputs:\n attempt:\n description: Exact Buildchain attempt to recover\n required: true\n type: string\n runtime-ref:\n description: Optional repaired runtime\n required: false\n type: string\n${call(RECOVERY_ENTRY, channel, " with:\n attempt: ${{ inputs.attempt }}\n runtime-ref: ${{ inputs.runtime-ref }}\n")}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -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,146 @@
|
|
|
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|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: source === "feature" && target === "dev",
|
|
49
|
+
alpha: source === "dev" && target === "alpha",
|
|
50
|
+
stable: source === "alpha" && target === "release",
|
|
51
|
+
major: source === "release" && value.to === "publish-gate/major",
|
|
52
|
+
};
|
|
53
|
+
if (
|
|
54
|
+
!legal[value.operation] ||
|
|
55
|
+
value.from.includes("..") ||
|
|
56
|
+
value.to.includes("..")
|
|
57
|
+
)
|
|
58
|
+
throw new Error(`${field}: unlawful channel route`);
|
|
59
|
+
return { ...value };
|
|
60
|
+
});
|
|
61
|
+
unique(
|
|
62
|
+
channels.map((value) => `${value.from}:${value.to}`),
|
|
63
|
+
"channels",
|
|
64
|
+
);
|
|
65
|
+
return channels;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function compileReview(value) {
|
|
69
|
+
object(
|
|
70
|
+
value,
|
|
71
|
+
["minimum_approvals", "code_owners", "merge_queue"],
|
|
72
|
+
[],
|
|
73
|
+
"review",
|
|
74
|
+
);
|
|
75
|
+
if (!Number.isInteger(value.minimum_approvals) || value.minimum_approvals < 1)
|
|
76
|
+
throw new Error(
|
|
77
|
+
"review.minimum_approvals: independent approval is required",
|
|
78
|
+
);
|
|
79
|
+
if (value.code_owners !== true || value.merge_queue !== true)
|
|
80
|
+
throw new Error(
|
|
81
|
+
"review: code owners and protected merge queue are required",
|
|
82
|
+
);
|
|
83
|
+
return { ...value };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function compileStable(value) {
|
|
87
|
+
object(
|
|
88
|
+
value,
|
|
89
|
+
[
|
|
90
|
+
"minimum_interval_seconds",
|
|
91
|
+
"minimum_soak_seconds",
|
|
92
|
+
"product_paths",
|
|
93
|
+
"impact_file",
|
|
94
|
+
"require_published_entry",
|
|
95
|
+
],
|
|
96
|
+
[],
|
|
97
|
+
"stable",
|
|
98
|
+
);
|
|
99
|
+
for (const field of ["minimum_interval_seconds", "minimum_soak_seconds"])
|
|
100
|
+
if (!Number.isSafeInteger(value[field]) || value[field] < 0)
|
|
101
|
+
throw new Error(
|
|
102
|
+
`stable.${field}: expected non-negative safe integer seconds`,
|
|
103
|
+
);
|
|
104
|
+
const paths = list(
|
|
105
|
+
value.product_paths,
|
|
106
|
+
"stable.product_paths",
|
|
107
|
+
(item, field) => {
|
|
108
|
+
text(item, field);
|
|
109
|
+
relativePath(item.endsWith("/") ? item.slice(0, -1) : item, field);
|
|
110
|
+
if (item.includes("*"))
|
|
111
|
+
throw new Error(`${field}: expected a literal product path`);
|
|
112
|
+
return item;
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
unique(paths, "stable.product_paths");
|
|
116
|
+
relativePath(value.impact_file, "stable.impact_file");
|
|
117
|
+
if (!value.impact_file.endsWith(".json") || value.impact_file.includes("*"))
|
|
118
|
+
throw new Error(
|
|
119
|
+
"stable.impact_file: expected one product impact JSON file",
|
|
120
|
+
);
|
|
121
|
+
if (typeof value.require_published_entry !== "boolean")
|
|
122
|
+
throw new Error("stable.require_published_entry: expected a boolean");
|
|
123
|
+
return structuredClone(value);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// This compiler receives bytes. It has no filesystem, process, network or provider port.
|
|
127
|
+
export function compileConsumerPlan(source) {
|
|
128
|
+
const config = parse(source);
|
|
129
|
+
object(
|
|
130
|
+
config,
|
|
131
|
+
["schema", "products", "version", "channels", "review"],
|
|
132
|
+
["stable"],
|
|
133
|
+
);
|
|
134
|
+
choice(config.schema, [2], "schema");
|
|
135
|
+
return {
|
|
136
|
+
schema: CONSUMER_PLAN,
|
|
137
|
+
contract: CONSUMER_CONTRACT,
|
|
138
|
+
products: compileProducts(config.products),
|
|
139
|
+
version: compileVersion(config.version),
|
|
140
|
+
channels: compileChannels(config.channels),
|
|
141
|
+
review: compileReview(config.review),
|
|
142
|
+
...(config.stable === undefined
|
|
143
|
+
? {}
|
|
144
|
+
: { stable: compileStable(config.stable) }),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -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
|
+
}
|