@kungfu-tech/buildchain 4.1.3-alpha.0 → 4.1.3-alpha.2
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 +16 -0
- package/architecture/agent-change-map.md +245 -4
- package/architecture/ci-lane-change-budget.json +289 -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/internal-capabilities.json +340 -4
- package/architecture/maintainability-debt.json +3 -1
- package/architecture/maintainability-policy.json +8 -8
- package/architecture/minimal-consumer-migration.json +7558 -0
- package/architecture/release-topology.json +248 -11
- package/architecture/universal-workflow-bootstrap.json +10 -0
- package/architecture/universal-workflow-capability-policy.json +6 -6
- package/architecture/workflow-taxonomy.json +55 -0
- package/contracts/release-discussion-v1.schema.json +3 -0
- package/dist/readers/business-attempt.cjs +805 -0
- package/dist/readers/release-discussion.cjs +4 -0
- package/dist/site/buildchain-contract.json +7 -5
- package/dist/site/buildchain-site.json +25 -15
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/kfd-claims.json +311 -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 +9 -9
- package/dist/site/page-registry.json +20 -10
- package/dist/site/public-surface-audit.json +201 -7
- package/dist/site/publication-authority-registry.json +121 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +5 -5
- package/dist/site/workflow-registry.json +416 -7
- package/docs/node-api-reference.md +12 -12
- package/docs/release-discussions.md +44 -5
- package/docs/runtime-entry.md +45 -5
- package/docs/workflow-catalog.md +5 -0
- package/package.json +4 -3
- 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 +99 -0
- package/packages/core/consumer/contract/products.js +101 -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/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 +5 -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 +23 -7
- package/packages/core/providers/github/discussions/transport.js +16 -5
- package/packages/core/providers/github/pipeline-checkout.js +49 -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-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-version.js +201 -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 +100 -0
- package/packages/core/publication/pipeline/apply.js +182 -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 +85 -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 +156 -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 +116 -0
- package/packages/core/publication/pipeline/pack.js +151 -0
- package/packages/core/publication/pipeline/package-policy.js +22 -0
- package/packages/core/publication/pipeline/plan.js +114 -0
- package/packages/core/publication/pipeline/prepare.js +151 -0
- package/packages/core/publication/pipeline/qualification.js +170 -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/version.js +81 -0
- package/packages/core/publication/publication-reproducibility.js +2 -2
- package/packages/core/release/discussion/binary.js +11 -0
- package/packages/core/release/discussion/checkpoints.js +45 -4
- package/packages/core/release/discussion/envelope.js +1 -0
- package/packages/core/release/discussion/evidence.js +51 -0
- package/packages/core/release/discussion/presentation.js +92 -0
- package/packages/core/release/discussion/publication.js +3 -1
- package/packages/core/release/discussion/qualification.js +1 -0
- package/packages/core/release/discussion/reader.js +9 -0
- package/packages/core/release/discussion/session.js +2 -0
- package/packages/core/release/discussion/store.js +28 -31
- package/packages/core/release/discussion/threads.js +83 -0
- 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/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 +165 -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 +62 -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 +95 -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 +36 -0
- package/scripts/check-release-topology.mjs +6 -1
- package/scripts/generate-minimal-consumer-contract.mjs +155 -0
- package/scripts/verify-golden-path.mjs +5 -1
- 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,79 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
5
|
+
import {
|
|
6
|
+
publicationFile,
|
|
7
|
+
publicationPath,
|
|
8
|
+
writeImmutablePublicationFile,
|
|
9
|
+
} from "./files.js";
|
|
10
|
+
|
|
11
|
+
export async function retainPipelineProducts(archive, qualified, bundles) {
|
|
12
|
+
const retained = [];
|
|
13
|
+
for (const artifact of qualified.artifacts) {
|
|
14
|
+
const bundle = bundles.find(
|
|
15
|
+
({ manifest }) => manifest.root === artifact.manifestRoot,
|
|
16
|
+
);
|
|
17
|
+
if (!bundle)
|
|
18
|
+
throw new Error("Qualified product has no exact source manifest");
|
|
19
|
+
const file = publicationFile(
|
|
20
|
+
publicationPath(bundle.directory, artifact.file),
|
|
21
|
+
);
|
|
22
|
+
if (file.digest !== artifact.digest || file.size !== artifact.size)
|
|
23
|
+
throw new Error("Qualified product changed before durable retention");
|
|
24
|
+
const handle = await archive.put(file.bytes, {
|
|
25
|
+
name: `${artifact.digest.slice(7)}-${path.basename(artifact.file)}`,
|
|
26
|
+
mediaType: "application/octet-stream",
|
|
27
|
+
});
|
|
28
|
+
await verifiedBytes(archive, handle, artifact);
|
|
29
|
+
retained.push({ id: artifact.id, handle });
|
|
30
|
+
}
|
|
31
|
+
const body = {
|
|
32
|
+
schema: "buildchain.pipeline-sealed-products/v1",
|
|
33
|
+
qualificationRoot: qualified.root,
|
|
34
|
+
products: retained,
|
|
35
|
+
};
|
|
36
|
+
return { ...body, root: recordDigest(body) };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function verifiedBytes(archive, handle, artifact) {
|
|
40
|
+
if (handle.digest !== artifact.digest || handle.size !== artifact.size)
|
|
41
|
+
throw new Error("Retained product handle does not bind qualified bytes");
|
|
42
|
+
const bytes = Buffer.from(await archive.read(handle));
|
|
43
|
+
if (
|
|
44
|
+
bytes.length !== artifact.size ||
|
|
45
|
+
`sha256:${createHash("sha256").update(bytes).digest("hex")}` !==
|
|
46
|
+
artifact.digest
|
|
47
|
+
)
|
|
48
|
+
throw new Error("Retained product readback differs from qualified bytes");
|
|
49
|
+
return bytes;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function restorePipelineProducts(
|
|
53
|
+
archive,
|
|
54
|
+
qualified,
|
|
55
|
+
sealed,
|
|
56
|
+
directory,
|
|
57
|
+
) {
|
|
58
|
+
const { root, ...body } = sealed;
|
|
59
|
+
if (
|
|
60
|
+
root !== recordDigest(body) ||
|
|
61
|
+
sealed.qualificationRoot !== qualified.root ||
|
|
62
|
+
recordDigest(sealed.products.map(({ id }) => id)) !==
|
|
63
|
+
recordDigest(qualified.artifacts.map(({ id }) => id))
|
|
64
|
+
)
|
|
65
|
+
throw new Error(
|
|
66
|
+
"Retained products must cover the exact qualification inventory",
|
|
67
|
+
);
|
|
68
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
69
|
+
for (const artifact of qualified.artifacts) {
|
|
70
|
+
const { handle } = sealed.products.find(({ id }) => id === artifact.id);
|
|
71
|
+
const bytes = await verifiedBytes(archive, handle, artifact);
|
|
72
|
+
const target = path.resolve(directory, artifact.file);
|
|
73
|
+
if (!target.startsWith(`${path.resolve(directory)}${path.sep}`))
|
|
74
|
+
throw new Error("Retained product path escapes its directory");
|
|
75
|
+
writeImmutablePublicationFile(target, bytes);
|
|
76
|
+
publicationPath(directory, artifact.file);
|
|
77
|
+
}
|
|
78
|
+
return directory;
|
|
79
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { publicationContext, uniquePublicationMaterial } from "./context.js";
|
|
3
|
+
import { distributePipelineProducts } from "./distribution.js";
|
|
4
|
+
import { nextPipelineDevelopment } from "./next-development.js";
|
|
5
|
+
|
|
6
|
+
export async function settlePipelinePublication(
|
|
7
|
+
context,
|
|
8
|
+
host,
|
|
9
|
+
directory,
|
|
10
|
+
environment,
|
|
11
|
+
) {
|
|
12
|
+
const { session, journal } = await publicationContext(context, host);
|
|
13
|
+
if (session.observed.phases.publish?.payload.state !== "success")
|
|
14
|
+
throw new Error(
|
|
15
|
+
"Publication must retain success before distribution and next-development",
|
|
16
|
+
);
|
|
17
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
18
|
+
const retained = await uniquePublicationMaterial(
|
|
19
|
+
journal,
|
|
20
|
+
"publication/qualified/",
|
|
21
|
+
);
|
|
22
|
+
if (session.observed.phases.distribution?.payload.state !== "success") {
|
|
23
|
+
const receipt = await distributePipelineProducts(
|
|
24
|
+
context,
|
|
25
|
+
host,
|
|
26
|
+
journal,
|
|
27
|
+
retained,
|
|
28
|
+
environment,
|
|
29
|
+
directory,
|
|
30
|
+
);
|
|
31
|
+
await journal.record("publication/distribution-complete", receipt, {
|
|
32
|
+
phase: "distribution",
|
|
33
|
+
state: "success",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const next = await nextPipelineDevelopment(context, host, journal);
|
|
37
|
+
await journal.record("publication/next-development-result", next, {
|
|
38
|
+
phase: "next-development",
|
|
39
|
+
state: next.state,
|
|
40
|
+
});
|
|
41
|
+
await host.project(session);
|
|
42
|
+
return next;
|
|
43
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import {
|
|
6
|
+
canonicalJson,
|
|
7
|
+
recordDigest,
|
|
8
|
+
} from "../../release/discussion/envelope.js";
|
|
9
|
+
import { verifyPipelineQualification } from "./documents.js";
|
|
10
|
+
import { writeImmutablePublicationFile } from "./files.js";
|
|
11
|
+
|
|
12
|
+
export const PIPELINE_PRODUCT_PREDICATE =
|
|
13
|
+
"https://buildchain.libkungfu.dev/attestations/pipeline-products/v1";
|
|
14
|
+
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
15
|
+
|
|
16
|
+
export function preparePipelineSigning({
|
|
17
|
+
plan,
|
|
18
|
+
materialization,
|
|
19
|
+
qualified,
|
|
20
|
+
directory,
|
|
21
|
+
evaluatedAt,
|
|
22
|
+
}) {
|
|
23
|
+
verifyPipelineQualification({
|
|
24
|
+
plan,
|
|
25
|
+
materialization,
|
|
26
|
+
qualified,
|
|
27
|
+
evaluatedAt,
|
|
28
|
+
});
|
|
29
|
+
const predicate = {
|
|
30
|
+
schema: "buildchain.pipeline-product-attestation/v1",
|
|
31
|
+
planRoot: plan.root,
|
|
32
|
+
materializationRoot: materialization.root,
|
|
33
|
+
qualificationRoot: qualified.root,
|
|
34
|
+
source: qualified.source,
|
|
35
|
+
protectedSource: plan.source,
|
|
36
|
+
publisher: plan.publisher,
|
|
37
|
+
runtime: plan.runtime,
|
|
38
|
+
provider: {
|
|
39
|
+
runId: qualified.build.runId,
|
|
40
|
+
runAttempt: qualified.build.runAttempt,
|
|
41
|
+
source: qualified.build.providerSource,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
const subjectBytes = Buffer.from(`${canonicalJson(qualified)}\n`);
|
|
45
|
+
const subjectPath = writeImmutablePublicationFile(
|
|
46
|
+
path.join(directory, "qualified-products.json"),
|
|
47
|
+
subjectBytes,
|
|
48
|
+
);
|
|
49
|
+
const predicatePath = writeImmutablePublicationFile(
|
|
50
|
+
path.join(directory, "predicate.json"),
|
|
51
|
+
`${canonicalJson(predicate)}\n`,
|
|
52
|
+
);
|
|
53
|
+
return {
|
|
54
|
+
subjectPath,
|
|
55
|
+
predicatePath,
|
|
56
|
+
predicate,
|
|
57
|
+
subjectDigest: digest(subjectBytes),
|
|
58
|
+
predicateType: PIPELINE_PRODUCT_PREDICATE,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function verifyPipelineSigning({
|
|
63
|
+
plan,
|
|
64
|
+
materialization,
|
|
65
|
+
qualified,
|
|
66
|
+
bundlePath,
|
|
67
|
+
directory,
|
|
68
|
+
token,
|
|
69
|
+
execute = execFileSync,
|
|
70
|
+
evaluatedAt,
|
|
71
|
+
}) {
|
|
72
|
+
const prepared = preparePipelineSigning({
|
|
73
|
+
plan,
|
|
74
|
+
materialization,
|
|
75
|
+
qualified,
|
|
76
|
+
directory,
|
|
77
|
+
evaluatedAt,
|
|
78
|
+
});
|
|
79
|
+
const output = execute(
|
|
80
|
+
"gh",
|
|
81
|
+
[
|
|
82
|
+
"attestation",
|
|
83
|
+
"verify",
|
|
84
|
+
prepared.subjectPath,
|
|
85
|
+
"--repo",
|
|
86
|
+
qualified.source.repository,
|
|
87
|
+
"--signer-workflow",
|
|
88
|
+
`${plan.publisher.repository}/${plan.publisher.workflow}`,
|
|
89
|
+
"--signer-digest",
|
|
90
|
+
plan.publisher.workflowSha,
|
|
91
|
+
"--source-digest",
|
|
92
|
+
qualified.build.providerSource,
|
|
93
|
+
"--predicate-type",
|
|
94
|
+
PIPELINE_PRODUCT_PREDICATE,
|
|
95
|
+
"--bundle",
|
|
96
|
+
bundlePath,
|
|
97
|
+
"--deny-self-hosted-runners",
|
|
98
|
+
"--format",
|
|
99
|
+
"json",
|
|
100
|
+
],
|
|
101
|
+
{
|
|
102
|
+
encoding: "utf8",
|
|
103
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
104
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
105
|
+
env: { ...process.env, GH_TOKEN: token },
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
const results = JSON.parse(output);
|
|
109
|
+
const statements = Array.isArray(results)
|
|
110
|
+
? results
|
|
111
|
+
.map((result) => result.verificationResult?.statement)
|
|
112
|
+
.filter(Boolean)
|
|
113
|
+
: [];
|
|
114
|
+
if (
|
|
115
|
+
statements.length !== 1 ||
|
|
116
|
+
statements[0].predicateType !== prepared.predicateType ||
|
|
117
|
+
recordDigest(statements[0].predicate) !==
|
|
118
|
+
recordDigest(prepared.predicate) ||
|
|
119
|
+
statements[0].subject?.length !== 1 ||
|
|
120
|
+
statements[0].subject[0].digest?.sha256 !== prepared.subjectDigest
|
|
121
|
+
)
|
|
122
|
+
throw new Error(
|
|
123
|
+
"Cryptographically verified attestation does not bind the exact publication predicate and subject",
|
|
124
|
+
);
|
|
125
|
+
if (digest(fs.readFileSync(prepared.subjectPath)) !== prepared.subjectDigest)
|
|
126
|
+
throw new Error("Signed publication subject changed during verification");
|
|
127
|
+
const bundleBytes = fs.readFileSync(bundlePath);
|
|
128
|
+
return {
|
|
129
|
+
schema: "buildchain.pipeline-product-signing/v1",
|
|
130
|
+
verified: true,
|
|
131
|
+
planRoot: plan.root,
|
|
132
|
+
materializationRoot: materialization.root,
|
|
133
|
+
qualificationRoot: qualified.root,
|
|
134
|
+
subjectDigest: `sha256:${prepared.subjectDigest}`,
|
|
135
|
+
predicateRoot: recordDigest(prepared.predicate),
|
|
136
|
+
bundleDigest: `sha256:${digest(bundleBytes)}`,
|
|
137
|
+
signer: plan.publisher,
|
|
138
|
+
providerSource: qualified.build.providerSource,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { planPipelinePublication } from "./plan.js";
|
|
2
|
+
|
|
3
|
+
export async function createPipelinePublicationPlan(
|
|
4
|
+
session,
|
|
5
|
+
publisherSha,
|
|
6
|
+
host,
|
|
7
|
+
version,
|
|
8
|
+
) {
|
|
9
|
+
const observed = session.observed;
|
|
10
|
+
const attempt = observed.attempt;
|
|
11
|
+
const integration = await host.integration.observe({
|
|
12
|
+
...observed.history.at(-1),
|
|
13
|
+
intent: session.intent,
|
|
14
|
+
});
|
|
15
|
+
const source = await host.source.source(
|
|
16
|
+
integration.mergeCommit,
|
|
17
|
+
observed.history.at(-1).generation.source.configPath,
|
|
18
|
+
);
|
|
19
|
+
const route = source.plan.channels.find(
|
|
20
|
+
(entry) =>
|
|
21
|
+
entry.to === session.intent.source.targetBranch &&
|
|
22
|
+
entry.operation !== "develop",
|
|
23
|
+
);
|
|
24
|
+
if (!route)
|
|
25
|
+
throw new Error(
|
|
26
|
+
"Protected source does not declare this publication channel",
|
|
27
|
+
);
|
|
28
|
+
const input = await version.inspect(source.identity, source.plan.version);
|
|
29
|
+
const runtimeCommit = await host.request(
|
|
30
|
+
`/repos/${host.runtime.repository}/git/commits/${host.runtime.sha}`,
|
|
31
|
+
);
|
|
32
|
+
if (
|
|
33
|
+
runtimeCommit.sha !== host.runtime.sha ||
|
|
34
|
+
!/^[0-9a-f]{40}$/u.test(runtimeCommit.tree?.sha || "")
|
|
35
|
+
)
|
|
36
|
+
throw new Error("Publication runtime has no exact source tree");
|
|
37
|
+
const definition = {
|
|
38
|
+
repository: "kungfu-systems/buildchain",
|
|
39
|
+
workflow: ".github/workflows/.release-pipeline-products.yml",
|
|
40
|
+
workflowSha: publisherSha,
|
|
41
|
+
job: "apply",
|
|
42
|
+
};
|
|
43
|
+
const parameters = {
|
|
44
|
+
attempt,
|
|
45
|
+
generation: observed.generation,
|
|
46
|
+
source: source.identity,
|
|
47
|
+
intentSource: observed.history.at(-1).generation.source,
|
|
48
|
+
runtime: {
|
|
49
|
+
repository: host.runtime.repository,
|
|
50
|
+
commit: runtimeCommit.sha,
|
|
51
|
+
tree: runtimeCommit.tree.sha,
|
|
52
|
+
},
|
|
53
|
+
publisher: definition,
|
|
54
|
+
contract: source.plan,
|
|
55
|
+
route,
|
|
56
|
+
version: input.version,
|
|
57
|
+
sourceTimestamp: input.sourceTimestamp,
|
|
58
|
+
};
|
|
59
|
+
let plan = planPipelinePublication(parameters);
|
|
60
|
+
const tag = await host.request(
|
|
61
|
+
`/repos/${host.repository}/git/ref/tags/${encodeURIComponent(plan.tag)}`,
|
|
62
|
+
{ allow404: true },
|
|
63
|
+
);
|
|
64
|
+
const channel = `v${plan.version.split(".")[0]}${plan.channel === "alpha" ? "-alpha" : ""}`;
|
|
65
|
+
const previous = await host.request(
|
|
66
|
+
`/repos/${host.repository}/git/ref/tags/${channel}`,
|
|
67
|
+
{ allow404: true },
|
|
68
|
+
);
|
|
69
|
+
if (
|
|
70
|
+
previous &&
|
|
71
|
+
(previous.object?.type !== "commit" ||
|
|
72
|
+
!/^[0-9a-f]{40}$/u.test(previous.object.sha))
|
|
73
|
+
)
|
|
74
|
+
throw new Error(
|
|
75
|
+
"Publication channel requires an exact lightweight commit ref",
|
|
76
|
+
);
|
|
77
|
+
plan = planPipelinePublication({
|
|
78
|
+
...parameters,
|
|
79
|
+
expectedTagSha: tag?.object?.sha || null,
|
|
80
|
+
previousChannelCommit: previous?.object?.sha || null,
|
|
81
|
+
});
|
|
82
|
+
return plan;
|
|
83
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { parse, stringify } from "smol-toml";
|
|
2
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
3
|
+
|
|
4
|
+
function document(file, bytes) {
|
|
5
|
+
const text = Buffer.isBuffer(bytes) ? bytes.toString("utf8") : bytes;
|
|
6
|
+
return file.format === "json" ? JSON.parse(text) : parse(text);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function field(value, key) {
|
|
10
|
+
const names = key.split(".");
|
|
11
|
+
let parent = value;
|
|
12
|
+
for (const name of names.slice(0, -1)) {
|
|
13
|
+
if (
|
|
14
|
+
["__proto__", "prototype", "constructor"].includes(name) ||
|
|
15
|
+
!Object.hasOwn(parent, name) ||
|
|
16
|
+
!parent[name] ||
|
|
17
|
+
typeof parent[name] !== "object"
|
|
18
|
+
)
|
|
19
|
+
throw new Error(
|
|
20
|
+
"Version field does not resolve to an owned document key",
|
|
21
|
+
);
|
|
22
|
+
parent = parent[name];
|
|
23
|
+
}
|
|
24
|
+
const name = names.at(-1);
|
|
25
|
+
if (
|
|
26
|
+
["__proto__", "prototype", "constructor"].includes(name) ||
|
|
27
|
+
!Object.hasOwn(parent, name) ||
|
|
28
|
+
typeof parent[name] !== "string"
|
|
29
|
+
)
|
|
30
|
+
throw new Error("Version field must be an existing string");
|
|
31
|
+
return { parent, name };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function readPipelineVersion(policy, files) {
|
|
35
|
+
const versions = policy.files.map((file) => {
|
|
36
|
+
if (!Object.hasOwn(files, file.path))
|
|
37
|
+
throw new Error(`Missing declared version file: ${file.path}`);
|
|
38
|
+
const { parent, name } = field(document(file, files[file.path]), file.key);
|
|
39
|
+
return parent[name];
|
|
40
|
+
});
|
|
41
|
+
if (!versions.length || new Set(versions).size !== 1)
|
|
42
|
+
throw new Error(
|
|
43
|
+
"All declared version fields must identify the same candidate version",
|
|
44
|
+
);
|
|
45
|
+
return versions[0];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function materializePipelineVersion(policy, files, version) {
|
|
49
|
+
const before = readPipelineVersion(policy, files);
|
|
50
|
+
if (policy.strategy === "anchored" && before !== version)
|
|
51
|
+
throw new Error("Buildchain cannot rewrite anchored version authority");
|
|
52
|
+
const documents = new Map();
|
|
53
|
+
for (const file of policy.files) {
|
|
54
|
+
if (!documents.has(file.path))
|
|
55
|
+
documents.set(file.path, {
|
|
56
|
+
file,
|
|
57
|
+
value: document(file, files[file.path]),
|
|
58
|
+
});
|
|
59
|
+
const entry = documents.get(file.path);
|
|
60
|
+
if (entry.file.format !== file.format)
|
|
61
|
+
throw new Error("One version document cannot have conflicting formats");
|
|
62
|
+
const { parent, name } = field(entry.value, file.key);
|
|
63
|
+
parent[name] = version;
|
|
64
|
+
}
|
|
65
|
+
const changes = [...documents].flatMap(([filePath, { file, value }]) => {
|
|
66
|
+
if (before === version) return [];
|
|
67
|
+
const content =
|
|
68
|
+
file.format === "json"
|
|
69
|
+
? `${JSON.stringify(value, null, 2)}\n`
|
|
70
|
+
: stringify(value);
|
|
71
|
+
return [
|
|
72
|
+
{
|
|
73
|
+
path: filePath,
|
|
74
|
+
before: recordDigest(String(files[filePath])),
|
|
75
|
+
content,
|
|
76
|
+
after: recordDigest(content),
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
});
|
|
80
|
+
return { before, version, changes };
|
|
81
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readNpmPackResult } from "./npm/pack-result.js";
|
|
1
2
|
import crypto from "node:crypto";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import os from "node:os";
|
|
@@ -564,8 +565,7 @@ function packPublication({ buildRoot, packageDir, source, toolchain }) {
|
|
|
564
565
|
label: "npm pack reproducibility candidate",
|
|
565
566
|
},
|
|
566
567
|
);
|
|
567
|
-
const
|
|
568
|
-
const result = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
568
|
+
const result = readNpmPackResult(output);
|
|
569
569
|
if (!result?.filename || !result?.integrity || !result?.shasum) {
|
|
570
570
|
throw new Error(
|
|
571
571
|
"npm pack reproducibility candidate did not report filename, integrity, and shasum",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { releaseCheckpoints } from "./checkpoints.js";
|
|
1
2
|
import { discussionTransport } from "../../providers/github/discussions/transport.js";
|
|
2
3
|
import { releaseDiscussionStore } from "./store.js";
|
|
3
4
|
import { createProgress, decodeRecord } from "./envelope.js";
|
|
@@ -46,6 +47,12 @@ export async function observeBinaryDistribution(
|
|
|
46
47
|
const attemptRecord = state.records.find(
|
|
47
48
|
(record) => record.attempt === state.attempt,
|
|
48
49
|
);
|
|
50
|
+
Object.assign(session, {
|
|
51
|
+
attempt: state.attempt,
|
|
52
|
+
predecessor: attemptRecord.predecessor,
|
|
53
|
+
runtime,
|
|
54
|
+
writer,
|
|
55
|
+
});
|
|
49
56
|
const sequence =
|
|
50
57
|
Math.max(
|
|
51
58
|
-1,
|
|
@@ -82,6 +89,10 @@ export async function observeBinaryDistribution(
|
|
|
82
89
|
await record("failure", 1, {
|
|
83
90
|
code: String(error.code || "execution-failed"),
|
|
84
91
|
});
|
|
92
|
+
await releaseCheckpoints({ session, store, octokit }).diagnostics(
|
|
93
|
+
"binary-distribution",
|
|
94
|
+
error.code,
|
|
95
|
+
);
|
|
85
96
|
} catch (recordError) {
|
|
86
97
|
error.discussionRecordingError = recordError.message;
|
|
87
98
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
retainAttachment,
|
|
3
|
+
diagnosticReport,
|
|
4
|
+
diagnosticLog,
|
|
5
|
+
} from "./evidence.js";
|
|
1
6
|
import fs from "node:fs";
|
|
2
7
|
import path from "node:path";
|
|
3
8
|
import { createProgress, recordDigest } from "./envelope.js";
|
|
@@ -10,7 +15,11 @@ export function releaseCheckpoints({ session, store, octokit }) {
|
|
|
10
15
|
repository: session.intent.repository,
|
|
11
16
|
intentId: session.intent.id,
|
|
12
17
|
});
|
|
13
|
-
async function checkpoint(
|
|
18
|
+
async function checkpoint(
|
|
19
|
+
node,
|
|
20
|
+
value,
|
|
21
|
+
{ attachments = [], label = value.schema || "Recovery checkpoint" } = {},
|
|
22
|
+
) {
|
|
14
23
|
const observed = await store.read(session);
|
|
15
24
|
const records = observed.records.filter(
|
|
16
25
|
(record) =>
|
|
@@ -21,20 +30,29 @@ export function releaseCheckpoints({ session, store, octokit }) {
|
|
|
21
30
|
const root = recordDigest(value);
|
|
22
31
|
const duplicate = records.find((record) => record.payload.root === root);
|
|
23
32
|
if (duplicate) return duplicate;
|
|
24
|
-
const
|
|
33
|
+
const sequence =
|
|
34
|
+
Math.max(-1, ...records.map((record) => record.sequence)) + 1;
|
|
35
|
+
const handle = await retainAttachment(materials, {
|
|
36
|
+
name: `${node}-checkpoint-${sequence}.json`,
|
|
37
|
+
mediaType: "application/json",
|
|
38
|
+
bytes: Buffer.from(JSON.stringify(value, null, 2) + "\n"),
|
|
39
|
+
});
|
|
25
40
|
const record = createProgress({
|
|
26
41
|
intent: session.intent,
|
|
27
42
|
runtime: session.runtime,
|
|
28
43
|
attempt: session.attempt,
|
|
44
|
+
writer: session.writer || session.attempt,
|
|
29
45
|
predecessor: session.predecessor,
|
|
30
46
|
kind: "checkpoint",
|
|
31
47
|
node,
|
|
32
48
|
status: "running",
|
|
33
|
-
sequence
|
|
49
|
+
sequence,
|
|
34
50
|
payload: {
|
|
35
51
|
schema: "buildchain.release-checkpoint/v1",
|
|
36
52
|
root,
|
|
37
53
|
material: handle,
|
|
54
|
+
label,
|
|
55
|
+
attachments: [handle, ...attachments],
|
|
38
56
|
},
|
|
39
57
|
});
|
|
40
58
|
await store.append(session, record);
|
|
@@ -69,7 +87,30 @@ export function releaseCheckpoints({ session, store, octokit }) {
|
|
|
69
87
|
throw new Error("Unsupported retained publication reader manifest");
|
|
70
88
|
return materials.read(manifest.reader);
|
|
71
89
|
}
|
|
72
|
-
|
|
90
|
+
async function diagnostics(node, code) {
|
|
91
|
+
const report = diagnosticReport(
|
|
92
|
+
session,
|
|
93
|
+
await store.read(session),
|
|
94
|
+
node,
|
|
95
|
+
code,
|
|
96
|
+
);
|
|
97
|
+
const log = await retainAttachment(materials, {
|
|
98
|
+
name: `${node}-diagnostics.log`,
|
|
99
|
+
mediaType: "text/plain",
|
|
100
|
+
bytes: diagnosticLog(report),
|
|
101
|
+
});
|
|
102
|
+
return checkpoint(node, report, {
|
|
103
|
+
attachments: [log],
|
|
104
|
+
label: `Execution diagnostics: ${report.code}`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
checkpoint,
|
|
109
|
+
readCheckpoint,
|
|
110
|
+
materials,
|
|
111
|
+
publicationReader,
|
|
112
|
+
diagnostics,
|
|
113
|
+
};
|
|
73
114
|
}
|
|
74
115
|
|
|
75
116
|
function assertNoSymlink(file, base) {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Evidence is declared transaction data, never an implicit upload of workspace
|
|
2
|
+
// files, environment variables, raw exceptions or authenticated HTTP responses.
|
|
3
|
+
export async function retainAttachment(materials, { name, mediaType, bytes }) {
|
|
4
|
+
if (
|
|
5
|
+
!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,159}$/u.test(name) ||
|
|
6
|
+
!["application/json", "text/plain", "application/octet-stream"].includes(
|
|
7
|
+
mediaType,
|
|
8
|
+
)
|
|
9
|
+
)
|
|
10
|
+
throw new Error("Invalid transaction attachment descriptor");
|
|
11
|
+
const handle = await materials.put(bytes, { name, mediaType });
|
|
12
|
+
if (!handle.downloadUrl)
|
|
13
|
+
throw new Error("Retained attachment is missing its provider download URL");
|
|
14
|
+
return { name, mediaType, ...handle };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function diagnosticReport(session, state, node, code) {
|
|
18
|
+
return {
|
|
19
|
+
schema: "buildchain.release-diagnostics/v1",
|
|
20
|
+
intent: session.intent.id,
|
|
21
|
+
attempt: session.attempt,
|
|
22
|
+
node,
|
|
23
|
+
code: String(code || "execution-failed")
|
|
24
|
+
.replace(/[^a-zA-Z0-9._-]/gu, "-")
|
|
25
|
+
.slice(0, 160),
|
|
26
|
+
events: state.records
|
|
27
|
+
.filter((record) => record.attempt === session.attempt)
|
|
28
|
+
.map(({ id, node, kind, status, sequence, writer }) => ({
|
|
29
|
+
id,
|
|
30
|
+
node,
|
|
31
|
+
kind,
|
|
32
|
+
status,
|
|
33
|
+
sequence,
|
|
34
|
+
writer,
|
|
35
|
+
})),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function diagnosticLog(report) {
|
|
40
|
+
return Buffer.from(
|
|
41
|
+
[
|
|
42
|
+
`Release intent ${report.intent}`,
|
|
43
|
+
`Attempt ${report.attempt}: ${report.node} ${report.code}`,
|
|
44
|
+
...report.events.map(
|
|
45
|
+
(event) =>
|
|
46
|
+
`${event.node} ${event.kind} ${event.sequence} ${event.status} writer=${event.writer} record=${event.id}`,
|
|
47
|
+
),
|
|
48
|
+
"",
|
|
49
|
+
].join("\n"),
|
|
50
|
+
);
|
|
51
|
+
}
|