@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
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
import { object } from "../../consumer/contract/shape.js";
|
|
3
|
+
import { pipelinePlatforms } from "./platforms.js";
|
|
4
|
+
import { resumePipelineSession } from "./session.js";
|
|
5
|
+
import { retainPipelineBuildResult } from "./build-result.js";
|
|
6
|
+
|
|
7
|
+
export async function beginPipelineBuild(session, admission, host) {
|
|
8
|
+
const observed = await session.journal.read();
|
|
9
|
+
const attempt = observed.attempt;
|
|
10
|
+
const previous = observed.phases.build;
|
|
11
|
+
if (
|
|
12
|
+
previous &&
|
|
13
|
+
(previous.payload.writer.runId !== String(host.runId) ||
|
|
14
|
+
previous.payload.writer.runAttempt !== String(host.runAttempt))
|
|
15
|
+
) {
|
|
16
|
+
const { run } = await host.runs.read(
|
|
17
|
+
Number(previous.payload.writer.runId),
|
|
18
|
+
Number(previous.payload.writer.runAttempt),
|
|
19
|
+
);
|
|
20
|
+
if (run.status !== "completed") return null;
|
|
21
|
+
const material = await host
|
|
22
|
+
.materialStore(session)
|
|
23
|
+
.retain(
|
|
24
|
+
`build/incomplete-${run.id}-${run.run_attempt}`,
|
|
25
|
+
run,
|
|
26
|
+
"provider-readback",
|
|
27
|
+
);
|
|
28
|
+
await session.progress.progress({
|
|
29
|
+
attempt,
|
|
30
|
+
phase: "build",
|
|
31
|
+
state: "failure",
|
|
32
|
+
eventKey: `build-incomplete:${run.id}:${run.run_attempt}`,
|
|
33
|
+
reason: "build-recording-incomplete",
|
|
34
|
+
materials: [material],
|
|
35
|
+
expectedHead: observed.head,
|
|
36
|
+
});
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
if (!observed.phases.admission) {
|
|
40
|
+
const receipt = {
|
|
41
|
+
schema: "buildchain.pipeline-source-observation/v1",
|
|
42
|
+
repositoryId: admission.repositoryId,
|
|
43
|
+
source: admission.live.source,
|
|
44
|
+
baseCommit: admission.live.baseCommit,
|
|
45
|
+
protectedSource: admission.protectedSource,
|
|
46
|
+
route: admission.route,
|
|
47
|
+
runtime: host.runtime,
|
|
48
|
+
};
|
|
49
|
+
const reference = await host
|
|
50
|
+
.materialStore(session)
|
|
51
|
+
.retain("source/admission", receipt, "provider-readback");
|
|
52
|
+
await session.progress.progress({
|
|
53
|
+
attempt,
|
|
54
|
+
phase: "admission",
|
|
55
|
+
state: "success",
|
|
56
|
+
eventKey: `admission:${observed.generation}`,
|
|
57
|
+
materials: [reference],
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
const scheduling = await session.journal.read();
|
|
61
|
+
const scheduled = scheduling.phases.build?.payload.writer;
|
|
62
|
+
if (
|
|
63
|
+
scheduled &&
|
|
64
|
+
(scheduled.runId !== String(host.runId) ||
|
|
65
|
+
scheduled.runAttempt !== String(host.runAttempt))
|
|
66
|
+
)
|
|
67
|
+
return null;
|
|
68
|
+
await session.progress.progress({
|
|
69
|
+
attempt,
|
|
70
|
+
phase: "build",
|
|
71
|
+
state: "running",
|
|
72
|
+
eventKey: `build-start:${host.runId}:${host.runAttempt}`,
|
|
73
|
+
expectedHead: scheduling.head,
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
schema: "buildchain.pipeline-build-context/v1",
|
|
77
|
+
attempt,
|
|
78
|
+
generation: observed.generation,
|
|
79
|
+
source: admission.live.source,
|
|
80
|
+
runId: host.runId,
|
|
81
|
+
runAttempt: host.runAttempt,
|
|
82
|
+
platforms: pipelinePlatforms(admission.plan),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function validatePipelineBuildContext(context) {
|
|
87
|
+
object(context, [
|
|
88
|
+
"schema",
|
|
89
|
+
"attempt",
|
|
90
|
+
"generation",
|
|
91
|
+
"source",
|
|
92
|
+
"runId",
|
|
93
|
+
"runAttempt",
|
|
94
|
+
"platforms",
|
|
95
|
+
]);
|
|
96
|
+
if (
|
|
97
|
+
context.schema !== "buildchain.pipeline-build-context/v1" ||
|
|
98
|
+
!/^attempt-[0-9a-f]{64}$/u.test(context.attempt) ||
|
|
99
|
+
!/^sha256:[0-9a-f]{64}$/u.test(context.generation) ||
|
|
100
|
+
![context.runId, context.runAttempt].every(
|
|
101
|
+
(value) => Number.isSafeInteger(value) && value > 0,
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
throw new Error("Invalid internal pipeline build context");
|
|
105
|
+
return context;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function recordPipelineBuild(context, host) {
|
|
109
|
+
validatePipelineBuildContext(context);
|
|
110
|
+
if (context.runId !== host.runId || context.runAttempt !== host.runAttempt)
|
|
111
|
+
throw new Error("Build callback belongs to another provider execution");
|
|
112
|
+
const session = await resumePipelineSession(
|
|
113
|
+
{ ...host, attempt: context.attempt },
|
|
114
|
+
host,
|
|
115
|
+
);
|
|
116
|
+
const current = session.observed.history.at(-1);
|
|
117
|
+
if (
|
|
118
|
+
context.generation !== current.generation.id ||
|
|
119
|
+
recordDigest(context.source) !== recordDigest(current.generation.source)
|
|
120
|
+
)
|
|
121
|
+
throw new Error("Build callback changed its admitted source generation");
|
|
122
|
+
const admitted = await host.source.source(
|
|
123
|
+
context.source.commit,
|
|
124
|
+
context.source.configPath,
|
|
125
|
+
);
|
|
126
|
+
if (
|
|
127
|
+
recordDigest(context.platforms) !==
|
|
128
|
+
recordDigest(pipelinePlatforms(admitted.plan))
|
|
129
|
+
)
|
|
130
|
+
throw new Error(
|
|
131
|
+
"Build callback omitted or changed a declared product platform",
|
|
132
|
+
);
|
|
133
|
+
const readback = await host.runs.build(
|
|
134
|
+
context.runId,
|
|
135
|
+
context.runAttempt,
|
|
136
|
+
context.source,
|
|
137
|
+
context.platforms.map((platform) => platform.platform),
|
|
138
|
+
);
|
|
139
|
+
return retainPipelineBuildResult(context, readback, session, host);
|
|
140
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
import {
|
|
3
|
+
PIPELINE_BUILD_QUALIFICATION,
|
|
4
|
+
verifyPipelineBuildQualification,
|
|
5
|
+
} from "./build-qualification.js";
|
|
6
|
+
import { pipelinePlatforms } from "./platforms.js";
|
|
7
|
+
|
|
8
|
+
// The archive reference is already validated against the current attempt. A
|
|
9
|
+
// build result is still re-read from the provider before it can admit delivery.
|
|
10
|
+
export async function pipelineBuildEvidence(session, host) {
|
|
11
|
+
const observed = await session.journal.read();
|
|
12
|
+
const current = observed.history.at(-1);
|
|
13
|
+
const phase = current.phases.build;
|
|
14
|
+
if (phase?.payload.state !== "success") return null;
|
|
15
|
+
const references = phase.payload.materials.filter(
|
|
16
|
+
(material) =>
|
|
17
|
+
material.id.startsWith("build/provider-") ||
|
|
18
|
+
material.id.startsWith("build/qualified-"),
|
|
19
|
+
);
|
|
20
|
+
if (references.length !== 1)
|
|
21
|
+
throw new Error("Pipeline build requires one retained provider receipt");
|
|
22
|
+
const retained = await host.materialStore(session).read(references[0]);
|
|
23
|
+
let { run } = await host.runs.read(retained.runId, retained.runAttempt);
|
|
24
|
+
if (run.status !== "completed" && run.id !== host.runId)
|
|
25
|
+
run = await host.runs.completed(retained.runId, retained.runAttempt);
|
|
26
|
+
if (retained.schema === PIPELINE_BUILD_QUALIFICATION) {
|
|
27
|
+
const source = await host.source.source(
|
|
28
|
+
current.generation.source.commit,
|
|
29
|
+
current.generation.source.configPath,
|
|
30
|
+
);
|
|
31
|
+
if (
|
|
32
|
+
recordDigest(source.identity) !== recordDigest(current.generation.source)
|
|
33
|
+
)
|
|
34
|
+
throw new Error("Recovered build source changed during qualification");
|
|
35
|
+
await verifyPipelineBuildQualification(
|
|
36
|
+
retained,
|
|
37
|
+
source.identity,
|
|
38
|
+
pipelinePlatforms(source.plan).map(({ platform }) => platform),
|
|
39
|
+
host.runs,
|
|
40
|
+
);
|
|
41
|
+
} else {
|
|
42
|
+
const verified = await host.runs.build(
|
|
43
|
+
retained.runId,
|
|
44
|
+
retained.runAttempt,
|
|
45
|
+
current.generation.source,
|
|
46
|
+
retained.jobs.map(
|
|
47
|
+
(job) => job.name.match(/Build product \(([^)]+)\)$/u)?.[1],
|
|
48
|
+
),
|
|
49
|
+
);
|
|
50
|
+
if (recordDigest(verified) !== recordDigest(retained))
|
|
51
|
+
throw new Error("Retained product build provider result changed");
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
run,
|
|
55
|
+
readback: retained,
|
|
56
|
+
sourceHead: current.generation.source.commit,
|
|
57
|
+
outcome: retained.outcome,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function publishPipelineBuildCheck(
|
|
62
|
+
context,
|
|
63
|
+
readback,
|
|
64
|
+
request,
|
|
65
|
+
repository,
|
|
66
|
+
) {
|
|
67
|
+
if (
|
|
68
|
+
context.source.repository !== repository ||
|
|
69
|
+
recordDigest(context.source) !== recordDigest(readback.source) ||
|
|
70
|
+
context.runId !== readback.runId ||
|
|
71
|
+
context.runAttempt !== readback.runAttempt
|
|
72
|
+
)
|
|
73
|
+
throw new Error(
|
|
74
|
+
"Product check source differs from independently verified build",
|
|
75
|
+
);
|
|
76
|
+
const externalId = `buildchain:${context.attempt}:${context.runId}:${context.runAttempt}`;
|
|
77
|
+
return request(`/repos/${repository}/check-runs`, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
body: {
|
|
80
|
+
name: "check",
|
|
81
|
+
head_sha: context.source.commit,
|
|
82
|
+
external_id: externalId,
|
|
83
|
+
status: "completed",
|
|
84
|
+
conclusion: readback.outcome === "success" ? "success" : "failure",
|
|
85
|
+
output: {
|
|
86
|
+
title: "Buildchain product verification",
|
|
87
|
+
summary: `Exact product jobs: ${readback.root}\nAttempt: ${context.attempt}`,
|
|
88
|
+
},
|
|
89
|
+
details_url: `https://github.com/${repository}/actions/runs/${context.runId}/attempts/${context.runAttempt}`,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import {
|
|
2
|
+
recordDigest,
|
|
3
|
+
validateRuntime,
|
|
4
|
+
} from "../../release/discussion/envelope.js";
|
|
5
|
+
import { object } from "../../consumer/contract/shape.js";
|
|
6
|
+
|
|
7
|
+
export const PIPELINE_BUILD_QUALIFICATION =
|
|
8
|
+
"buildchain.pipeline-build-qualification/v1";
|
|
9
|
+
|
|
10
|
+
export function buildReadbackPlatforms(readback) {
|
|
11
|
+
const { root, ...body } = readback;
|
|
12
|
+
if (
|
|
13
|
+
body.schema !== "buildchain.pipeline-build-readback/v1" ||
|
|
14
|
+
root !== recordDigest(body) ||
|
|
15
|
+
!Array.isArray(body.jobs) ||
|
|
16
|
+
!body.jobs.length
|
|
17
|
+
)
|
|
18
|
+
throw new Error(
|
|
19
|
+
"Build qualification requires immutable provider job readback",
|
|
20
|
+
);
|
|
21
|
+
const platforms = body.jobs.map(
|
|
22
|
+
(job) => job.name?.match(/Build product \(([^)]+)\)$/u)?.[1],
|
|
23
|
+
);
|
|
24
|
+
if (
|
|
25
|
+
platforms.some((platform) => !platform) ||
|
|
26
|
+
new Set(platforms).size !== platforms.length
|
|
27
|
+
)
|
|
28
|
+
throw new Error(
|
|
29
|
+
"Provider build has ambiguous or unsupported platform jobs",
|
|
30
|
+
);
|
|
31
|
+
return platforms;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function validateBuildInventory(platforms, segments, runId, runAttempt) {
|
|
35
|
+
if (
|
|
36
|
+
!Array.isArray(platforms) ||
|
|
37
|
+
!platforms.length ||
|
|
38
|
+
platforms.length > 5 ||
|
|
39
|
+
new Set(platforms).size !== platforms.length ||
|
|
40
|
+
!Array.isArray(segments) ||
|
|
41
|
+
!segments.length ||
|
|
42
|
+
segments.length > 5 ||
|
|
43
|
+
![runId, runAttempt].every(
|
|
44
|
+
(value) => Number.isSafeInteger(value) && value > 0,
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
throw new Error(
|
|
48
|
+
"Build qualification requires bounded exact platform and execution coordinates",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function qualifyPipelineBuild({
|
|
53
|
+
source,
|
|
54
|
+
platforms,
|
|
55
|
+
segments,
|
|
56
|
+
runId,
|
|
57
|
+
runAttempt,
|
|
58
|
+
}) {
|
|
59
|
+
validateBuildInventory(platforms, segments, runId, runAttempt);
|
|
60
|
+
const selected = [];
|
|
61
|
+
let successful = true;
|
|
62
|
+
for (const segment of segments) {
|
|
63
|
+
object(segment, ["readback", "platforms", "runtime"]);
|
|
64
|
+
validateRuntime(segment.runtime);
|
|
65
|
+
if (
|
|
66
|
+
segment.runtime.repository !== "kungfu-systems/buildchain" ||
|
|
67
|
+
!Array.isArray(segment.platforms) ||
|
|
68
|
+
!segment.platforms.length ||
|
|
69
|
+
recordDigest(segment.readback.source) !== recordDigest(source)
|
|
70
|
+
)
|
|
71
|
+
throw new Error(
|
|
72
|
+
"Build segment changed its exact source or runtime repository",
|
|
73
|
+
);
|
|
74
|
+
const available = buildReadbackPlatforms(segment.readback);
|
|
75
|
+
for (const platform of segment.platforms) {
|
|
76
|
+
const index = available.indexOf(platform),
|
|
77
|
+
job = segment.readback.jobs[index];
|
|
78
|
+
if (
|
|
79
|
+
index < 0 ||
|
|
80
|
+
!platforms.includes(platform) ||
|
|
81
|
+
selected.includes(platform) ||
|
|
82
|
+
job.status !== "completed" ||
|
|
83
|
+
!job.conclusion ||
|
|
84
|
+
job.run_id !== segment.readback.runId ||
|
|
85
|
+
job.run_attempt !== segment.readback.runAttempt ||
|
|
86
|
+
!Number.isSafeInteger(job.id) ||
|
|
87
|
+
job.id < 1
|
|
88
|
+
)
|
|
89
|
+
throw new Error(
|
|
90
|
+
"Build segments omitted, duplicated or changed a real platform execution",
|
|
91
|
+
);
|
|
92
|
+
selected.push(platform);
|
|
93
|
+
successful &&= job.conclusion === "success";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (selected.length !== platforms.length)
|
|
97
|
+
throw new Error("Build qualification is missing declared platforms");
|
|
98
|
+
const body = {
|
|
99
|
+
schema: PIPELINE_BUILD_QUALIFICATION,
|
|
100
|
+
source,
|
|
101
|
+
platforms: [...platforms].sort(),
|
|
102
|
+
runId,
|
|
103
|
+
runAttempt,
|
|
104
|
+
segments: structuredClone(segments),
|
|
105
|
+
outcome: successful ? "success" : "failure",
|
|
106
|
+
};
|
|
107
|
+
return { ...body, root: recordDigest(body) };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function verifyPipelineBuildQualification(
|
|
111
|
+
value,
|
|
112
|
+
source,
|
|
113
|
+
platforms,
|
|
114
|
+
runs,
|
|
115
|
+
) {
|
|
116
|
+
object(value, [
|
|
117
|
+
"schema",
|
|
118
|
+
"source",
|
|
119
|
+
"platforms",
|
|
120
|
+
"runId",
|
|
121
|
+
"runAttempt",
|
|
122
|
+
"segments",
|
|
123
|
+
"outcome",
|
|
124
|
+
"root",
|
|
125
|
+
]);
|
|
126
|
+
const expected = qualifyPipelineBuild({ ...value, source, platforms });
|
|
127
|
+
if (recordDigest(value) !== recordDigest(expected))
|
|
128
|
+
throw new Error(
|
|
129
|
+
"Retained build qualification changed its source or exact platform inventory",
|
|
130
|
+
);
|
|
131
|
+
for (const segment of value.segments) {
|
|
132
|
+
const old = segment.readback;
|
|
133
|
+
const fresh = await runs.build(
|
|
134
|
+
old.runId,
|
|
135
|
+
old.runAttempt,
|
|
136
|
+
source,
|
|
137
|
+
buildReadbackPlatforms(old),
|
|
138
|
+
);
|
|
139
|
+
if (recordDigest(fresh) !== recordDigest(old))
|
|
140
|
+
throw new Error(
|
|
141
|
+
"A recovered platform result changed during provider requalification",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { publishPipelineBuildCheck } from "./build-evidence.js";
|
|
2
|
+
import { PIPELINE_BUILD_QUALIFICATION } from "./build-qualification.js";
|
|
3
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
4
|
+
import { replayRecoveryIntegration } from "./recovery-integration.js";
|
|
5
|
+
|
|
6
|
+
async function resultReference(context, readback, session, host) {
|
|
7
|
+
const observed = await session.journal.read();
|
|
8
|
+
if (observed.attempt !== context.attempt)
|
|
9
|
+
throw new Error("Late build result belongs to a superseded attempt");
|
|
10
|
+
const prior = observed.history
|
|
11
|
+
.at(-1)
|
|
12
|
+
.events.find(
|
|
13
|
+
(event) =>
|
|
14
|
+
event.payload.eventKey ===
|
|
15
|
+
`build-result:${context.runId}:${context.runAttempt}`,
|
|
16
|
+
);
|
|
17
|
+
const store = host.materialStore(session);
|
|
18
|
+
if (prior) {
|
|
19
|
+
const references = prior.payload.materials;
|
|
20
|
+
if (
|
|
21
|
+
references.length !== 1 ||
|
|
22
|
+
recordDigest(await store.read(references[0])) !== recordDigest(readback)
|
|
23
|
+
)
|
|
24
|
+
throw new Error("Repeated build callback changed its immutable result");
|
|
25
|
+
return references[0];
|
|
26
|
+
}
|
|
27
|
+
return store.retain(
|
|
28
|
+
`build/${readback.schema === PIPELINE_BUILD_QUALIFICATION ? "qualified" : "provider"}-${context.runId}-${context.runAttempt}`,
|
|
29
|
+
readback,
|
|
30
|
+
"provider-readback",
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function retainPipelineBuildResult(
|
|
35
|
+
context,
|
|
36
|
+
readback,
|
|
37
|
+
session,
|
|
38
|
+
host,
|
|
39
|
+
) {
|
|
40
|
+
const reference = await resultReference(context, readback, session, host);
|
|
41
|
+
await session.progress.progress({
|
|
42
|
+
attempt: context.attempt,
|
|
43
|
+
phase: "build",
|
|
44
|
+
state: readback.outcome === "success" ? "success" : "failure",
|
|
45
|
+
eventKey: `build-result:${context.runId}:${context.runAttempt}`,
|
|
46
|
+
reason: readback.outcome === "success" ? "" : "product-build-failed",
|
|
47
|
+
materials: [reference],
|
|
48
|
+
});
|
|
49
|
+
await host.project(session);
|
|
50
|
+
await publishPipelineBuildCheck(
|
|
51
|
+
context,
|
|
52
|
+
readback,
|
|
53
|
+
host.request,
|
|
54
|
+
host.repository,
|
|
55
|
+
);
|
|
56
|
+
if (
|
|
57
|
+
readback.schema === PIPELINE_BUILD_QUALIFICATION &&
|
|
58
|
+
readback.outcome === "success"
|
|
59
|
+
)
|
|
60
|
+
await replayRecoveryIntegration(session, host);
|
|
61
|
+
try {
|
|
62
|
+
await host.wake(context.attempt);
|
|
63
|
+
return { outcome: readback.outcome, wakePending: false };
|
|
64
|
+
} catch {
|
|
65
|
+
// The source build remains qualified if only notification failed. Its
|
|
66
|
+
// durable result is reobserved on the next PR event or exact attempt wake.
|
|
67
|
+
return { outcome: readback.outcome, wakePending: true };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { bindConsumerSource } from "../../consumer/contract/identity.js";
|
|
6
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
7
|
+
import { consumerCommandSession } from "../../runtime/consumer-shell.js";
|
|
8
|
+
import { createNativeChildEnvironment } from "../../dev-delivery/native/execution.js";
|
|
9
|
+
|
|
10
|
+
function inspectTrackedBytes(cwd, tree) {
|
|
11
|
+
const root = fs.realpathSync(cwd);
|
|
12
|
+
const entries = execFileSync(
|
|
13
|
+
"git",
|
|
14
|
+
["-C", root, "ls-tree", "-r", "-z", tree],
|
|
15
|
+
{
|
|
16
|
+
encoding: "utf8",
|
|
17
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
18
|
+
},
|
|
19
|
+
)
|
|
20
|
+
.split("\0")
|
|
21
|
+
.filter(Boolean);
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
const match =
|
|
24
|
+
/^(100644|100755|120000) blob ([0-9a-f]{40})\t([\s\S]+)$/u.exec(entry);
|
|
25
|
+
if (!match) throw new Error("Build source requires resolved tracked files");
|
|
26
|
+
const [, mode, blob, file] = match;
|
|
27
|
+
const absolute = path.join(root, file);
|
|
28
|
+
if (fs.realpathSync(path.dirname(absolute)) !== path.dirname(absolute))
|
|
29
|
+
throw new Error(`Build source parent became a symbolic link: ${file}`);
|
|
30
|
+
const stat = fs.lstatSync(absolute);
|
|
31
|
+
const link = mode === "120000";
|
|
32
|
+
if (link ? !stat.isSymbolicLink() : !stat.isFile())
|
|
33
|
+
throw new Error(`Build tracked source kind changed: ${file}`);
|
|
34
|
+
if (
|
|
35
|
+
!link &&
|
|
36
|
+
process.platform !== "win32" &&
|
|
37
|
+
Boolean(stat.mode & 0o111) !== (mode === "100755")
|
|
38
|
+
)
|
|
39
|
+
throw new Error(`Build tracked source executable mode changed: ${file}`);
|
|
40
|
+
const bytes = link
|
|
41
|
+
? fs.readlinkSync(absolute, { encoding: "buffer" })
|
|
42
|
+
: fs.readFileSync(absolute);
|
|
43
|
+
const actual = createHash("sha1")
|
|
44
|
+
.update(`blob ${bytes.length}\0`)
|
|
45
|
+
.update(bytes)
|
|
46
|
+
.digest("hex");
|
|
47
|
+
if (actual !== blob)
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Build tracked source bytes differ from the admitted tree: ${file}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function inspectPipelineSource(cwd, expected) {
|
|
55
|
+
const git = (...args) =>
|
|
56
|
+
execFileSync("git", ["-C", cwd, ...args], {
|
|
57
|
+
encoding: "utf8",
|
|
58
|
+
maxBuffer: 1024 * 1024,
|
|
59
|
+
}).trim();
|
|
60
|
+
if (
|
|
61
|
+
git("rev-parse", "HEAD") !== expected.commit ||
|
|
62
|
+
git("rev-parse", "HEAD^{tree}") !== expected.tree
|
|
63
|
+
)
|
|
64
|
+
throw new Error("Build checkout does not match admitted source");
|
|
65
|
+
const configFile = fs.realpathSync(path.join(cwd, expected.configPath));
|
|
66
|
+
if (!configFile.startsWith(`${fs.realpathSync(cwd)}${path.sep}`))
|
|
67
|
+
throw new Error("Consumer TOML escapes the source checkout");
|
|
68
|
+
const { identity, plan } = bindConsumerSource(
|
|
69
|
+
{
|
|
70
|
+
repository: expected.repository,
|
|
71
|
+
commit: expected.commit,
|
|
72
|
+
tree: expected.tree,
|
|
73
|
+
configPath: expected.configPath,
|
|
74
|
+
configBlob: git("rev-parse", `HEAD:${expected.configPath}`),
|
|
75
|
+
},
|
|
76
|
+
fs.readFileSync(configFile),
|
|
77
|
+
);
|
|
78
|
+
if (recordDigest(identity) !== recordDigest(expected))
|
|
79
|
+
throw new Error("Build TOML changed after source admission");
|
|
80
|
+
if (git("status", "--porcelain", "--untracked-files=no"))
|
|
81
|
+
throw new Error(
|
|
82
|
+
"Build source has tracked modifications at its verification boundary",
|
|
83
|
+
);
|
|
84
|
+
// Index flags and local Git filters cannot qualify different product bytes.
|
|
85
|
+
inspectTrackedBytes(cwd, expected.tree);
|
|
86
|
+
return plan;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function productDirectory(cwd, product) {
|
|
90
|
+
const directory = fs.realpathSync(
|
|
91
|
+
path.resolve(cwd, product.directory || "."),
|
|
92
|
+
);
|
|
93
|
+
const root = fs.realpathSync(cwd);
|
|
94
|
+
if (directory !== root && !directory.startsWith(`${root}${path.sep}`))
|
|
95
|
+
throw new Error("Product directory escapes its source checkout");
|
|
96
|
+
return directory;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// This node executes only product-owned build/test commands, on a read-only
|
|
100
|
+
// credential job. Its output is an observation; independent seal/qualification
|
|
101
|
+
// jobs must bind provider completion and exact artifacts before granting reuse.
|
|
102
|
+
export async function buildPipelineSource(
|
|
103
|
+
{ cwd, source, platform, environment = process.env },
|
|
104
|
+
{ inspect = inspectPipelineSource, session = consumerCommandSession } = {},
|
|
105
|
+
) {
|
|
106
|
+
const plan = inspect(cwd, source);
|
|
107
|
+
const result = await buildPipelineProducts(
|
|
108
|
+
{ cwd, plan, platform, environment },
|
|
109
|
+
session,
|
|
110
|
+
);
|
|
111
|
+
inspect(cwd, source);
|
|
112
|
+
return {
|
|
113
|
+
schema: "buildchain.pipeline-build-observation/v1",
|
|
114
|
+
source,
|
|
115
|
+
...result,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function buildPipelineProducts(
|
|
120
|
+
{ cwd, plan, platform, environment = process.env },
|
|
121
|
+
session = consumerCommandSession,
|
|
122
|
+
) {
|
|
123
|
+
const products = plan.products.filter((product) =>
|
|
124
|
+
product.platforms.includes(platform),
|
|
125
|
+
);
|
|
126
|
+
if (!products.length)
|
|
127
|
+
throw new Error("No products declared for the selected platform");
|
|
128
|
+
const results = [];
|
|
129
|
+
for (const product of products) {
|
|
130
|
+
const commands = session(createNativeChildEnvironment(environment));
|
|
131
|
+
const directory = productDirectory(cwd, product);
|
|
132
|
+
for (const phase of ["install", "build", "verify"]) {
|
|
133
|
+
for (const script of product[phase] || [])
|
|
134
|
+
await commands.run({ script, cwd: directory, strict: true });
|
|
135
|
+
}
|
|
136
|
+
results.push({ product: product.id, outcome: "success" });
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
platform,
|
|
140
|
+
products: results,
|
|
141
|
+
};
|
|
142
|
+
}
|