@kungfu-tech/buildchain 4.1.3-alpha.0 → 4.1.3-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +41 -75
- package/CONTRIBUTING.md +19 -26
- package/architecture/action-taxonomy.json +24 -0
- package/architecture/agent-change-map.md +272 -12
- package/architecture/capability-state-machine-manifest.json +1 -4
- package/architecture/ci-lane-change-budget.json +190 -807
- package/architecture/core-mechanisms.json +3 -6
- package/architecture/decisions/0005-minimal-consumer-contract.md +122 -0
- package/architecture/decisions/0006-business-attempt-journal.md +89 -0
- package/architecture/decisions/0007-hosted-pipeline-controller.md +106 -0
- package/architecture/decisions/0008-pipeline-product-publication.md +120 -0
- package/architecture/decisions/0009-pipeline-version-preparation.md +186 -0
- package/architecture/internal-capabilities.json +371 -9
- package/architecture/maintainability-debt.json +50 -34
- package/architecture/maintainability-policy.json +8 -8
- package/architecture/minimal-consumer-migration.json +5670 -0
- package/architecture/platform-stage-checkpoints.json +31 -7
- package/architecture/product-upstreams.json +20 -0
- package/architecture/release-topology.json +331 -74
- package/architecture/stage-capsule-qualification.json +3 -1
- package/architecture/universal-workflow-bootstrap.json +15 -1
- package/architecture/universal-workflow-capability-policy.json +9 -9
- package/architecture/universal-workflow-fault-campaign.json +3 -1
- package/architecture/workflow-taxonomy.json +77 -328
- package/contracts/release-discussion-v1.schema.json +3 -0
- package/dist/readers/business-attempt.cjs +809 -0
- package/dist/readers/release-discussion.cjs +4 -0
- package/dist/site/buildchain-contract.json +16 -68
- package/dist/site/buildchain-site.json +47 -27
- package/dist/site/capability-registry.json +4 -4
- package/dist/site/controller-registry.json +1 -47
- package/dist/site/kfd-claims.json +499 -494
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +3 -3
- package/dist/site/node-api-registry.json +56 -56
- package/dist/site/page-registry.json +40 -20
- package/dist/site/public-surface-audit.json +286 -349
- package/dist/site/publication-authority-registry.json +203 -523
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +7 -7
- package/dist/site/workflow-registry.json +590 -940
- package/docs/code-organization.md +9 -6
- package/docs/node-api-reference.md +103 -103
- package/docs/release-discussions.md +44 -5
- package/docs/release-governance.md +10 -12
- package/docs/runtime-entry.md +73 -5
- package/docs/runtime-train-validation.md +39 -11
- package/docs/stable-candidate-patrol.md +6 -5
- package/docs/workflow-catalog.md +9 -32
- package/package.json +4 -3
- package/packages/core/adoption/buildchain-kfd-claims.js +7 -7
- package/packages/core/build/standalone/build.js +7 -1
- package/packages/core/consumer/buildchain-config.js +2 -21
- package/packages/core/consumer/contract/entries.js +85 -0
- package/packages/core/consumer/contract/examples.js +96 -0
- package/packages/core/consumer/contract/identity.js +65 -0
- package/packages/core/consumer/contract/inspection.js +45 -0
- package/packages/core/consumer/contract/plan.js +149 -0
- package/packages/core/consumer/contract/products.js +119 -0
- package/packages/core/consumer/contract/reader.js +47 -0
- package/packages/core/consumer/contract/shape.js +46 -0
- package/packages/core/dev-delivery/candidate/admission.js +9 -5
- package/packages/core/dev-delivery/candidate/reservation.js +11 -0
- package/packages/core/dev-delivery/candidate/source-paths.js +12 -4
- package/packages/core/dev-delivery/commands/project-cut-merge-queue-admission.mjs +47 -20
- package/packages/core/dev-delivery/dev-delivery-provider-heartbeat.js +18 -15
- package/packages/core/dev-delivery/native/actions.js +9 -1
- package/packages/core/dev-delivery/native/heartbeat-action.js +16 -0
- package/packages/core/dev-delivery/native/heartbeat.js +6 -4
- package/packages/core/dev-delivery/native/process-entry.js +19 -0
- package/packages/core/dev-delivery/queue/landing-action.js +53 -0
- package/packages/core/dev-delivery/warrant/failure-settlement.js +8 -6
- package/packages/core/dev-delivery/warrant/service.js +7 -0
- package/packages/core/governance/buildchain-publication-authority.js +8 -23
- package/packages/core/governance/github-governance-authority.js +1 -0
- package/packages/core/observability/controller-evidence.js +0 -8
- package/packages/core/paper/operations/bootstrap.js +2 -0
- package/packages/core/paper/paper-npm-bootstrap.js +2 -2
- package/packages/core/providers/github/attempt-index.js +60 -0
- package/packages/core/providers/github/attempt-journal.js +154 -0
- package/packages/core/providers/github/discussions/materials.js +35 -11
- 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-entry-runs.js +210 -0
- package/packages/core/providers/github/pipeline-events.js +131 -0
- package/packages/core/providers/github/pipeline-integration.js +95 -0
- package/packages/core/providers/github/pipeline-policy.js +240 -0
- package/packages/core/providers/github/pipeline-product-payload.js +50 -0
- package/packages/core/providers/github/pipeline-product-release.js +198 -0
- package/packages/core/providers/github/pipeline-publication-artifacts.js +153 -0
- package/packages/core/providers/github/pipeline-release-evidence.js +30 -0
- package/packages/core/providers/github/pipeline-run-entry.js +86 -0
- package/packages/core/providers/github/pipeline-runs.js +132 -0
- package/packages/core/providers/github/pipeline-source.js +195 -0
- package/packages/core/providers/github/pipeline-stable-baseline.js +86 -0
- package/packages/core/providers/github/pipeline-stable-source.js +372 -0
- package/packages/core/providers/github/pipeline-version-artifacts.js +91 -0
- package/packages/core/providers/github/pipeline-version-readback.js +117 -0
- package/packages/core/providers/github/pipeline-version.js +199 -0
- package/packages/core/providers/github/pipeline-worker.js +108 -0
- package/packages/core/providers/github/release-evidence-assets.js +97 -0
- package/packages/core/publication/candidate/registry-hydration.js +2 -2
- package/packages/core/publication/npm/pack-preview.js +2 -2
- package/packages/core/publication/npm/pack-result.js +31 -0
- package/packages/core/publication/npm/package.js +3 -6
- package/packages/core/publication/npm/pipeline-channel.js +36 -0
- package/packages/core/publication/npm/pipeline-provider.js +137 -0
- package/packages/core/publication/npm/registry.js +12 -9
- package/packages/core/publication/pipeline/actions.js +116 -0
- package/packages/core/publication/pipeline/apply.js +189 -0
- package/packages/core/publication/pipeline/build-segments.js +107 -0
- package/packages/core/publication/pipeline/capsules.js +92 -0
- package/packages/core/publication/pipeline/context.js +39 -0
- package/packages/core/publication/pipeline/development-anchor.js +91 -0
- package/packages/core/publication/pipeline/development-current.js +63 -0
- package/packages/core/publication/pipeline/development-pr.js +112 -0
- package/packages/core/publication/pipeline/development-proof.js +76 -0
- package/packages/core/publication/pipeline/development-transition.js +112 -0
- package/packages/core/publication/pipeline/distribution.js +130 -0
- package/packages/core/publication/pipeline/documents.js +192 -0
- package/packages/core/publication/pipeline/effects.js +192 -0
- package/packages/core/publication/pipeline/files.js +66 -0
- package/packages/core/publication/pipeline/imported-materials.js +32 -0
- package/packages/core/publication/pipeline/journal.js +122 -0
- package/packages/core/publication/pipeline/next-development.js +117 -0
- package/packages/core/publication/pipeline/pack.js +180 -0
- package/packages/core/publication/pipeline/package-policy.js +22 -0
- package/packages/core/publication/pipeline/plan.js +136 -0
- package/packages/core/publication/pipeline/prepare.js +139 -0
- package/packages/core/publication/pipeline/qualification.js +175 -0
- package/packages/core/publication/pipeline/qualify.js +104 -0
- package/packages/core/publication/pipeline/recovery-admission.js +122 -0
- package/packages/core/publication/pipeline/recovery-build-download.js +80 -0
- package/packages/core/publication/pipeline/recovery-build-plan.js +122 -0
- package/packages/core/publication/pipeline/recovery-build-readback.js +68 -0
- package/packages/core/publication/pipeline/recovery-capsules.js +54 -0
- package/packages/core/publication/pipeline/recovery-import.js +80 -0
- package/packages/core/publication/pipeline/recovery-inspection.js +218 -0
- package/packages/core/publication/pipeline/recovery-materials.js +104 -0
- package/packages/core/publication/pipeline/recovery-plan.js +46 -0
- package/packages/core/publication/pipeline/recovery-prepare.js +73 -0
- package/packages/core/publication/pipeline/recovery-qualification.js +128 -0
- package/packages/core/publication/pipeline/recovery-readback.js +56 -0
- package/packages/core/publication/pipeline/recovery-signing.js +58 -0
- package/packages/core/publication/pipeline/sealed-products.js +79 -0
- package/packages/core/publication/pipeline/settle.js +42 -0
- package/packages/core/publication/pipeline/signing.js +140 -0
- package/packages/core/publication/pipeline/source-plan.js +83 -0
- package/packages/core/publication/pipeline/stable-entry.js +219 -0
- package/packages/core/publication/pipeline/stable-products.js +317 -0
- package/packages/core/publication/pipeline/stable-wait.js +170 -0
- package/packages/core/publication/pipeline/stable.js +82 -0
- package/packages/core/publication/pipeline/version-actions.js +93 -0
- package/packages/core/publication/pipeline/version-build.js +130 -0
- package/packages/core/publication/pipeline/version-context.js +143 -0
- package/packages/core/publication/pipeline/version-material.js +36 -0
- package/packages/core/publication/pipeline/version-preparation.js +69 -0
- package/packages/core/publication/pipeline/version-regeneration.js +162 -0
- package/packages/core/publication/pipeline/version-source-guard.js +69 -0
- package/packages/core/publication/pipeline/version.js +81 -0
- package/packages/core/publication/pipeline/worker.js +72 -0
- package/packages/core/publication/publication-reproducibility.js +2 -2
- package/packages/core/release/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/next-development-transition.js +15 -15
- package/packages/core/release/promote-candidate/product-provider-adapters.js +4 -3
- package/packages/core/release/promote-ref/internal/npm-existing-evidence.js +2 -1
- package/packages/core/release/promote-ref/internal/publish-command.js +16 -5
- package/packages/core/release/stable-release-gate.js +11 -0
- package/packages/core/runtime/buildchain-domain.wasm +0 -0
- package/packages/core/runtime/domain-wasm-artifact.js +2 -2
- package/packages/core/runtime/entry/actions.js +27 -25
- package/packages/core/runtime/entry/attempt.js +25 -0
- package/packages/core/runtime/entry/selection.js +14 -0
- package/packages/core/runtime/entry/source.js +62 -0
- package/packages/core/workflow/attempt/identity.js +170 -0
- package/packages/core/workflow/attempt/journal.js +89 -0
- package/packages/core/workflow/attempt/materials.js +72 -0
- package/packages/core/workflow/attempt/reader-entry.js +12 -0
- package/packages/core/workflow/attempt/reader.js +149 -0
- package/packages/core/workflow/attempt/records.js +153 -0
- package/packages/core/workflow/attempt/store.js +75 -0
- package/packages/core/workflow/commands/pipeline-build.mjs +23 -0
- package/packages/core/workflow/pipeline/action-output.js +11 -0
- package/packages/core/workflow/pipeline/actions.js +52 -0
- package/packages/core/workflow/pipeline/build-control.js +140 -0
- package/packages/core/workflow/pipeline/build-evidence.js +210 -0
- package/packages/core/workflow/pipeline/build-qualification.js +145 -0
- package/packages/core/workflow/pipeline/build-result.js +78 -0
- package/packages/core/workflow/pipeline/build.js +142 -0
- package/packages/core/workflow/pipeline/cancellation.js +169 -0
- package/packages/core/workflow/pipeline/channel-control.js +157 -0
- package/packages/core/workflow/pipeline/channel-source.js +53 -0
- package/packages/core/workflow/pipeline/controller.js +141 -0
- package/packages/core/workflow/pipeline/delivery-control.js +249 -0
- package/packages/core/workflow/pipeline/delivery-observation.js +124 -0
- package/packages/core/workflow/pipeline/delivery-request.js +198 -0
- package/packages/core/workflow/pipeline/events.js +84 -0
- package/packages/core/workflow/pipeline/fence.js +42 -0
- package/packages/core/workflow/pipeline/group-control.js +59 -0
- package/packages/core/workflow/pipeline/guard-build.js +74 -0
- package/packages/core/workflow/pipeline/guard.js +158 -0
- package/packages/core/workflow/pipeline/host.js +144 -0
- package/packages/core/workflow/pipeline/integration-status.js +113 -0
- package/packages/core/workflow/pipeline/materials.js +58 -0
- package/packages/core/workflow/pipeline/notifications.js +36 -0
- package/packages/core/workflow/pipeline/parent-notification.js +27 -0
- package/packages/core/workflow/pipeline/platforms.js +21 -0
- package/packages/core/workflow/pipeline/progress.js +108 -0
- package/packages/core/workflow/pipeline/projection.js +20 -0
- package/packages/core/workflow/pipeline/reconcile.js +194 -0
- package/packages/core/workflow/pipeline/record-action.js +27 -0
- package/packages/core/workflow/pipeline/recorder-status.js +69 -0
- package/packages/core/workflow/pipeline/recovery-action.js +30 -0
- package/packages/core/workflow/pipeline/recovery-admission.js +122 -0
- package/packages/core/workflow/pipeline/recovery-build-control.js +95 -0
- package/packages/core/workflow/pipeline/recovery-build-evidence.js +71 -0
- package/packages/core/workflow/pipeline/recovery-build.js +121 -0
- package/packages/core/workflow/pipeline/recovery-controller.js +91 -0
- package/packages/core/workflow/pipeline/recovery-integration.js +79 -0
- package/packages/core/workflow/pipeline/recovery-merge-proof.js +90 -0
- package/packages/core/workflow/pipeline/recovery-ownership-settlement.js +150 -0
- package/packages/core/workflow/pipeline/recovery-ownership.js +91 -0
- package/packages/core/workflow/pipeline/recovery-plan.js +126 -0
- package/packages/core/workflow/pipeline/recovery-runtime.js +68 -0
- package/packages/core/workflow/pipeline/recovery-session.js +129 -0
- package/packages/core/workflow/pipeline/recovery-transition.js +135 -0
- package/packages/core/workflow/pipeline/recovery-unrecorded-build.js +74 -0
- package/packages/core/workflow/pipeline/runtime-source.js +111 -0
- package/packages/core/workflow/pipeline/selection.js +111 -0
- package/packages/core/workflow/pipeline/session.js +72 -0
- package/packages/core/workflow/pipeline/settlement.js +137 -0
- package/packages/core/workflow/pipeline/wake.js +38 -0
- package/packages/core/workflow/pipeline/web-status.js +108 -0
- package/packages/core/workflow/workflow-taxonomy.mjs +67 -47
- package/scripts/build-release-discussion-reader.mjs +11 -7
- package/scripts/check-inventory.mjs +7 -70
- package/scripts/check-pipeline-publication-topology.mjs +50 -0
- package/scripts/check-public-dogfood-contract.mjs +38 -51
- package/scripts/check-release-topology.mjs +15 -8
- package/scripts/check-self-consumer-contract.mjs +133 -0
- package/scripts/check-universal-workflow-bootstrap.mjs +4 -6
- package/scripts/generate-minimal-consumer-contract.mjs +155 -0
- package/scripts/generate-site-bundle.mjs +1 -1
- package/scripts/inventory/binary.mjs +9 -19
- package/scripts/inventory/promotion.mjs +2 -13
- package/scripts/prepare-product-toolchain.mjs +10 -0
- package/scripts/verify-golden-path.mjs +5 -1
- package/scripts/verify-product-platform.mjs +65 -0
- package/scripts/verify-standalone-product.mjs +42 -0
- package/templates/minimal-consumer/binary/.buildchain/buildchain.toml +45 -0
- package/templates/minimal-consumer/binary/.github/workflows/buildchain-recover.yml +30 -0
- package/templates/minimal-consumer/binary/.github/workflows/buildchain.yml +28 -0
- package/templates/minimal-consumer/binary/package.json +6 -0
- package/templates/minimal-consumer/binary/src/build.mjs +5 -0
- package/templates/minimal-consumer/binary/src/hello.c +2 -0
- package/templates/minimal-consumer/binary/src/verify.mjs +3 -0
- package/templates/minimal-consumer/npm/.buildchain/buildchain.toml +46 -0
- package/templates/minimal-consumer/npm/.github/workflows/buildchain-recover.yml +30 -0
- package/templates/minimal-consumer/npm/.github/workflows/buildchain.yml +28 -0
- package/templates/minimal-consumer/npm/package.json +6 -0
- package/templates/minimal-consumer/npm/src/build.mjs +5 -0
- package/templates/minimal-consumer/npm/src/verify.mjs +3 -0
- package/templates/minimal-consumer/paper/.buildchain/buildchain.toml +45 -0
- package/templates/minimal-consumer/paper/.github/workflows/buildchain-recover.yml +30 -0
- package/templates/minimal-consumer/paper/.github/workflows/buildchain.yml +28 -0
- package/templates/minimal-consumer/paper/package.json +6 -0
- package/templates/minimal-consumer/paper/src/build.mjs +12 -0
- package/templates/minimal-consumer/paper/src/verify.mjs +3 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import artifact from "@actions/artifact";
|
|
4
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
5
|
+
import {
|
|
6
|
+
publicationPath,
|
|
7
|
+
writeImmutablePublicationFile,
|
|
8
|
+
} from "../../publication/pipeline/files.js";
|
|
9
|
+
import { pipelineVersionRegenerationResult } from "../../publication/pipeline/version-regeneration.js";
|
|
10
|
+
import {
|
|
11
|
+
pipelineVersionArtifactName,
|
|
12
|
+
readPipelineVersionBuild,
|
|
13
|
+
} from "./pipeline-version-readback.js";
|
|
14
|
+
|
|
15
|
+
function verifyResult(context, result, platform) {
|
|
16
|
+
const expected = pipelineVersionRegenerationResult(
|
|
17
|
+
context.preparation,
|
|
18
|
+
platform,
|
|
19
|
+
result.files,
|
|
20
|
+
);
|
|
21
|
+
if (recordDigest(result) !== recordDigest(expected))
|
|
22
|
+
throw new Error(
|
|
23
|
+
"Version artifact changed its exact preparation, platform or bytes",
|
|
24
|
+
);
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function githubPipelineVersionArtifacts(host, client = artifact) {
|
|
29
|
+
async function upload(context, result, directory) {
|
|
30
|
+
verifyResult(context, result, result.platform);
|
|
31
|
+
const file = writeImmutablePublicationFile(
|
|
32
|
+
path.join(directory, "version.json"),
|
|
33
|
+
JSON.stringify(result),
|
|
34
|
+
);
|
|
35
|
+
const uploaded = await client.uploadArtifact(
|
|
36
|
+
pipelineVersionArtifactName(context, result.platform),
|
|
37
|
+
[file],
|
|
38
|
+
directory,
|
|
39
|
+
{
|
|
40
|
+
retentionDays: 30,
|
|
41
|
+
compressionLevel: 0,
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
if (!uploaded.id || !uploaded.digest)
|
|
45
|
+
throw new Error("Version upload omitted immutable provider coordinates");
|
|
46
|
+
return uploaded;
|
|
47
|
+
}
|
|
48
|
+
async function download(context, directory) {
|
|
49
|
+
const { build, assets } = await readPipelineVersionBuild(context, host);
|
|
50
|
+
const [repositoryOwner, repositoryName] = host.repository.split("/");
|
|
51
|
+
const results = [];
|
|
52
|
+
for (const [index, asset] of assets.entries()) {
|
|
53
|
+
const target = path.join(directory, String(asset.id));
|
|
54
|
+
fs.mkdirSync(target, { recursive: true });
|
|
55
|
+
const downloaded = await client.downloadArtifact(asset.id, {
|
|
56
|
+
path: target,
|
|
57
|
+
expectedHash: asset.digest,
|
|
58
|
+
findBy: {
|
|
59
|
+
repositoryOwner,
|
|
60
|
+
repositoryName,
|
|
61
|
+
workflowRunId: context.runId,
|
|
62
|
+
token: host.token,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
if (downloaded.digestMismatch)
|
|
66
|
+
throw new Error(
|
|
67
|
+
"Version download digest differs from its provider archive",
|
|
68
|
+
);
|
|
69
|
+
if (
|
|
70
|
+
recordDigest(fs.readdirSync(target)) !== recordDigest(["version.json"])
|
|
71
|
+
)
|
|
72
|
+
throw new Error("Version artifact contains undeclared files");
|
|
73
|
+
const file = publicationPath(target, "version.json");
|
|
74
|
+
// JSON escaping can expand the bounded UTF-8 document inventory sixfold.
|
|
75
|
+
if (fs.statSync(file).size > 384 * 1024 * 1024)
|
|
76
|
+
throw new Error("Version artifact exceeds its encoded material limit");
|
|
77
|
+
const bytes = fs.readFileSync(file);
|
|
78
|
+
if (!Buffer.from(bytes.toString("utf8")).equals(bytes))
|
|
79
|
+
throw new Error("Version artifact is not canonical UTF-8 text");
|
|
80
|
+
results.push(
|
|
81
|
+
verifyResult(
|
|
82
|
+
context,
|
|
83
|
+
JSON.parse(bytes.toString("utf8")),
|
|
84
|
+
context.preparation.platforms[index],
|
|
85
|
+
),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return { build, results };
|
|
89
|
+
}
|
|
90
|
+
return { upload, download };
|
|
91
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
import { readPipelineCaller } from "./pipeline-run-entry.js";
|
|
3
|
+
|
|
4
|
+
export const pipelineVersionArtifactName = (context, platform) =>
|
|
5
|
+
`buildchain-version-${context.preparation.root.slice(7)}-${context.runId}-${context.runAttempt}-${platform}`;
|
|
6
|
+
|
|
7
|
+
export const pipelineVersionJobName = (preparation, platform) =>
|
|
8
|
+
`Prepare version (${preparation.purpose}, ${platform})`;
|
|
9
|
+
|
|
10
|
+
export async function readPipelineVersionBuild(context, host) {
|
|
11
|
+
const { preparation, runId, runAttempt, definitionSha } = context;
|
|
12
|
+
const { repository, request, runs } = host;
|
|
13
|
+
if (preparation.source.repository !== repository)
|
|
14
|
+
throw new Error("Version preparation crossed its repository boundary");
|
|
15
|
+
const { run, jobs } = await runs.read(runId, runAttempt);
|
|
16
|
+
await readPipelineCaller(
|
|
17
|
+
run,
|
|
18
|
+
preparation.source.configPath,
|
|
19
|
+
request,
|
|
20
|
+
repository,
|
|
21
|
+
);
|
|
22
|
+
const definitions = (run.referenced_workflows || []).filter((entry) =>
|
|
23
|
+
entry.path?.startsWith(
|
|
24
|
+
"kungfu-systems/buildchain/.github/workflows/.release-pipeline-products.yml@",
|
|
25
|
+
),
|
|
26
|
+
);
|
|
27
|
+
const versionDefinitions = (run.referenced_workflows || []).filter((entry) =>
|
|
28
|
+
entry.path?.startsWith(
|
|
29
|
+
"kungfu-systems/buildchain/.github/workflows/.release-pipeline-version.yml@",
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
if (
|
|
33
|
+
definitions.length !== 1 ||
|
|
34
|
+
definitions[0].sha !== definitionSha ||
|
|
35
|
+
versionDefinitions.length !== 1 ||
|
|
36
|
+
versionDefinitions[0].sha !== definitionSha ||
|
|
37
|
+
!/^[0-9a-f]{40}$/u.test(definitionSha || "")
|
|
38
|
+
)
|
|
39
|
+
throw new Error("Version preparation changed its exact hosted definition");
|
|
40
|
+
const selected = preparation.platforms.map((platform) => {
|
|
41
|
+
const name = pipelineVersionJobName(preparation, platform);
|
|
42
|
+
const matches = jobs.filter(
|
|
43
|
+
(job) => job.name === name || job.name.endsWith(` / ${name}`),
|
|
44
|
+
);
|
|
45
|
+
if (
|
|
46
|
+
matches.length !== 1 ||
|
|
47
|
+
matches[0].run_id !== runId ||
|
|
48
|
+
matches[0].run_attempt !== runAttempt ||
|
|
49
|
+
matches[0].status !== "completed" ||
|
|
50
|
+
matches[0].conclusion !== "success"
|
|
51
|
+
)
|
|
52
|
+
throw new Error(
|
|
53
|
+
"Version preparation requires every exact successful product job",
|
|
54
|
+
);
|
|
55
|
+
return matches[0];
|
|
56
|
+
});
|
|
57
|
+
if (
|
|
58
|
+
!selected.length ||
|
|
59
|
+
new Set(preparation.platforms).size !== selected.length
|
|
60
|
+
)
|
|
61
|
+
throw new Error(
|
|
62
|
+
"Version preparation platform inventory is empty or duplicated",
|
|
63
|
+
);
|
|
64
|
+
const inventory = await request(
|
|
65
|
+
`/repos/${repository}/actions/runs/${runId}/artifacts?per_page=100`,
|
|
66
|
+
);
|
|
67
|
+
if (
|
|
68
|
+
!Array.isArray(inventory.artifacts) ||
|
|
69
|
+
inventory.total_count !== inventory.artifacts.length ||
|
|
70
|
+
inventory.total_count > 100
|
|
71
|
+
)
|
|
72
|
+
throw new Error("Version preparation artifact inventory is incomplete");
|
|
73
|
+
const assets = preparation.platforms.map((platform) => {
|
|
74
|
+
const matches = inventory.artifacts.filter(
|
|
75
|
+
(asset) => asset.name === pipelineVersionArtifactName(context, platform),
|
|
76
|
+
);
|
|
77
|
+
if (
|
|
78
|
+
matches.length !== 1 ||
|
|
79
|
+
!Number.isSafeInteger(matches[0].id) ||
|
|
80
|
+
matches[0].id < 1 ||
|
|
81
|
+
matches[0].expired ||
|
|
82
|
+
matches[0].workflow_run?.id !== runId ||
|
|
83
|
+
matches[0].workflow_run?.head_sha !== run.head_sha ||
|
|
84
|
+
!/^sha256:[0-9a-f]{64}$/u.test(matches[0].digest || "")
|
|
85
|
+
)
|
|
86
|
+
throw new Error(
|
|
87
|
+
"Version preparation has no unique immutable platform artifact",
|
|
88
|
+
);
|
|
89
|
+
return matches[0];
|
|
90
|
+
});
|
|
91
|
+
const again = await runs.read(runId, runAttempt);
|
|
92
|
+
if (
|
|
93
|
+
again.run.head_sha !== run.head_sha ||
|
|
94
|
+
recordDigest(again.run.referenced_workflows) !==
|
|
95
|
+
recordDigest(run.referenced_workflows) ||
|
|
96
|
+
selected.some(
|
|
97
|
+
(job) =>
|
|
98
|
+
!again.jobs.some((value) => recordDigest(value) === recordDigest(job)),
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
throw new Error(
|
|
102
|
+
"Version preparation producer changed during independent readback",
|
|
103
|
+
);
|
|
104
|
+
const body = {
|
|
105
|
+
schema: "buildchain.pipeline-version-build-readback/v1",
|
|
106
|
+
preparationRoot: preparation.root,
|
|
107
|
+
source: preparation.source,
|
|
108
|
+
runtime: preparation.runtime,
|
|
109
|
+
runId,
|
|
110
|
+
runAttempt,
|
|
111
|
+
definitionSha,
|
|
112
|
+
providerSource: run.head_sha,
|
|
113
|
+
jobs: selected,
|
|
114
|
+
artifacts: assets.map(({ id, digest }) => ({ id, digest })),
|
|
115
|
+
};
|
|
116
|
+
return { build: { ...body, root: recordDigest(body) }, assets };
|
|
117
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
3
|
+
import { readPipelineVersion } from "../../publication/pipeline/version.js";
|
|
4
|
+
import { preparedPipelineVersionMaterial } from "../../publication/pipeline/version-material.js";
|
|
5
|
+
import { verifyPipelinePublicationPlan } from "../../publication/pipeline/plan.js";
|
|
6
|
+
|
|
7
|
+
function blobBytes(value, sha) {
|
|
8
|
+
if (
|
|
9
|
+
value.encoding !== "base64" ||
|
|
10
|
+
value.size > 4 * 1024 * 1024 ||
|
|
11
|
+
typeof value.content !== "string"
|
|
12
|
+
)
|
|
13
|
+
throw new Error("Version material requires a bounded Git blob");
|
|
14
|
+
const bytes = Buffer.from(value.content, "base64");
|
|
15
|
+
const digest = createHash("sha1")
|
|
16
|
+
.update(`blob ${bytes.length}\0`)
|
|
17
|
+
.update(bytes)
|
|
18
|
+
.digest("hex");
|
|
19
|
+
if (bytes.length !== value.size || digest !== sha || value.sha !== sha)
|
|
20
|
+
throw new Error("Version material blob identity drift");
|
|
21
|
+
return bytes.toString("utf8");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function githubPipelineVersion(request, repository) {
|
|
25
|
+
const base = `/repos/${repository}`;
|
|
26
|
+
const post = (url, body) => request(url, { method: "POST", body });
|
|
27
|
+
async function inspect(source, policy) {
|
|
28
|
+
if (source.repository !== repository)
|
|
29
|
+
throw new Error("Version material repository mismatch");
|
|
30
|
+
const commit = await request(`${base}/git/commits/${source.commit}`);
|
|
31
|
+
if (commit.sha !== source.commit || commit.tree?.sha !== source.tree)
|
|
32
|
+
throw new Error("Version material source tree drift");
|
|
33
|
+
const tree = await request(`${base}/git/trees/${source.tree}?recursive=1`);
|
|
34
|
+
if (tree.truncated || tree.sha !== source.tree || !Array.isArray(tree.tree))
|
|
35
|
+
throw new Error("Version material requires a complete source tree");
|
|
36
|
+
const files = {},
|
|
37
|
+
entries = {};
|
|
38
|
+
for (const file of [
|
|
39
|
+
...policy.files,
|
|
40
|
+
...(policy.derived_files || []).map((path) => ({ path })),
|
|
41
|
+
]) {
|
|
42
|
+
if (Object.hasOwn(files, file.path)) continue;
|
|
43
|
+
const found = tree.tree.filter((entry) => entry.path === file.path);
|
|
44
|
+
if (
|
|
45
|
+
found.length !== 1 ||
|
|
46
|
+
found[0].type !== "blob" ||
|
|
47
|
+
!["100644", "100755"].includes(found[0].mode)
|
|
48
|
+
)
|
|
49
|
+
throw new Error("Version material must be a tracked regular file");
|
|
50
|
+
const entry = found[0];
|
|
51
|
+
entries[file.path] = entry;
|
|
52
|
+
files[file.path] = blobBytes(
|
|
53
|
+
await request(`${base}/git/blobs/${entry.sha}`),
|
|
54
|
+
entry.sha,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
commit,
|
|
59
|
+
files,
|
|
60
|
+
entries,
|
|
61
|
+
version: readPipelineVersion(policy, files),
|
|
62
|
+
sourceTimestamp: commit.committer.date,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
async function materialize(plan, regeneration) {
|
|
66
|
+
verifyPipelinePublicationPlan(plan);
|
|
67
|
+
const input = await inspect(plan.source, plan.versionPolicy);
|
|
68
|
+
if (input.version !== plan.candidateVersion)
|
|
69
|
+
throw new Error("Version material changed after planning");
|
|
70
|
+
const material = preparedPipelineVersionMaterial(
|
|
71
|
+
plan,
|
|
72
|
+
input.files,
|
|
73
|
+
regeneration,
|
|
74
|
+
);
|
|
75
|
+
let source = plan.source;
|
|
76
|
+
if (material.changes.length)
|
|
77
|
+
source = await createMaterializedSource(plan, input, material);
|
|
78
|
+
const body = {
|
|
79
|
+
schema: "buildchain.pipeline-version-materialization/v1",
|
|
80
|
+
planRoot: plan.root,
|
|
81
|
+
protectedSource: plan.source,
|
|
82
|
+
source,
|
|
83
|
+
material,
|
|
84
|
+
};
|
|
85
|
+
return { ...body, root: recordDigest(body) };
|
|
86
|
+
}
|
|
87
|
+
async function materializeDevelopment(plan, regeneration) {
|
|
88
|
+
const input = await inspect(plan.source, plan.versionPolicy);
|
|
89
|
+
const material = preparedPipelineVersionMaterial(
|
|
90
|
+
plan,
|
|
91
|
+
input.files,
|
|
92
|
+
regeneration,
|
|
93
|
+
);
|
|
94
|
+
const branch = `feature/buildchain-next/${plan.root.slice(7)}`;
|
|
95
|
+
const source = material.changes.length
|
|
96
|
+
? await createMaterializedSource(plan, input, material, `heads/${branch}`)
|
|
97
|
+
: plan.source;
|
|
98
|
+
const body = {
|
|
99
|
+
schema: "buildchain.pipeline-development-materialization/v1",
|
|
100
|
+
transitionRoot: plan.root,
|
|
101
|
+
protectedSource: plan.source,
|
|
102
|
+
source,
|
|
103
|
+
branch,
|
|
104
|
+
material,
|
|
105
|
+
};
|
|
106
|
+
return { ...body, root: recordDigest(body) };
|
|
107
|
+
}
|
|
108
|
+
async function createMaterializedSource(plan, input, material, targetRef) {
|
|
109
|
+
const changes = material.changes.map(({ path, content }) => ({
|
|
110
|
+
path,
|
|
111
|
+
content,
|
|
112
|
+
mode: input.entries[path].mode,
|
|
113
|
+
type: "blob",
|
|
114
|
+
}));
|
|
115
|
+
const tree = await post(`${base}/git/trees`, {
|
|
116
|
+
base_tree: plan.source.tree,
|
|
117
|
+
tree: changes,
|
|
118
|
+
});
|
|
119
|
+
const identity = {
|
|
120
|
+
name: "Buildchain automation",
|
|
121
|
+
email: "noreply@github.com",
|
|
122
|
+
date: plan.sourceTimestamp,
|
|
123
|
+
};
|
|
124
|
+
const parents = [plan.source.commit];
|
|
125
|
+
// Preserve the prior published version overlay as real Git ancestry. The
|
|
126
|
+
// resulting tree still changes only the current protected version fields;
|
|
127
|
+
// no provider force-update is needed for the next stable floating channel.
|
|
128
|
+
if (
|
|
129
|
+
plan.previousChannelCommit &&
|
|
130
|
+
plan.previousChannelCommit !== plan.source.commit
|
|
131
|
+
) {
|
|
132
|
+
if (!/^[0-9a-f]{40}$/u.test(plan.previousChannelCommit))
|
|
133
|
+
throw new Error(
|
|
134
|
+
"Version overlay parent must be an exact prior channel commit",
|
|
135
|
+
);
|
|
136
|
+
const previous = await request(
|
|
137
|
+
`${base}/git/commits/${plan.previousChannelCommit}`,
|
|
138
|
+
);
|
|
139
|
+
if (previous.sha !== plan.previousChannelCommit)
|
|
140
|
+
throw new Error("Prior version overlay commit changed");
|
|
141
|
+
parents.push(previous.sha);
|
|
142
|
+
}
|
|
143
|
+
const created = await post(`${base}/git/commits`, {
|
|
144
|
+
message: `chore(release): materialize ${plan.version}\n\nBuildchain-Plan: ${plan.root}\nSigned-off-by: Buildchain automation <noreply@github.com>`,
|
|
145
|
+
tree: tree.sha,
|
|
146
|
+
parents,
|
|
147
|
+
author: identity,
|
|
148
|
+
committer: identity,
|
|
149
|
+
});
|
|
150
|
+
const commit = await request(`${base}/git/commits/${created.sha}`);
|
|
151
|
+
if (
|
|
152
|
+
commit.tree?.sha !== tree.sha ||
|
|
153
|
+
recordDigest(commit.parents?.map(({ sha }) => sha)) !==
|
|
154
|
+
recordDigest(parents)
|
|
155
|
+
)
|
|
156
|
+
throw new Error("Materialized version commit readback mismatch");
|
|
157
|
+
const source = { ...plan.source, commit: commit.sha, tree: tree.sha };
|
|
158
|
+
const verified = await inspect(source, plan.versionPolicy);
|
|
159
|
+
if (verified.version !== plan.version)
|
|
160
|
+
throw new Error("Version material did not retain the planned version");
|
|
161
|
+
const expectedFiles = {
|
|
162
|
+
...input.files,
|
|
163
|
+
...Object.fromEntries(
|
|
164
|
+
material.changes.map(({ path, content }) => [path, content]),
|
|
165
|
+
),
|
|
166
|
+
};
|
|
167
|
+
if (recordDigest(verified.files) !== recordDigest(expectedFiles))
|
|
168
|
+
throw new Error(
|
|
169
|
+
"Version material changed document bytes beyond the planned fields",
|
|
170
|
+
);
|
|
171
|
+
const comparison = await request(
|
|
172
|
+
`${base}/compare/${plan.source.commit}...${commit.sha}`,
|
|
173
|
+
);
|
|
174
|
+
const paths = comparison.files?.map((file) => file.filename).sort();
|
|
175
|
+
if (
|
|
176
|
+
comparison.status !== "ahead" ||
|
|
177
|
+
(parents.length === 1 && comparison.total_commits !== 1) ||
|
|
178
|
+
recordDigest(paths) !==
|
|
179
|
+
recordDigest(material.changes.map(({ path }) => path).sort()) ||
|
|
180
|
+
comparison.files.some((file) => file.status !== "modified")
|
|
181
|
+
)
|
|
182
|
+
throw new Error(
|
|
183
|
+
"Version material changed paths outside the declared version fields",
|
|
184
|
+
);
|
|
185
|
+
const ref =
|
|
186
|
+
targetRef || `heads/buildchain/publication-source/${plan.root.slice(7)}`;
|
|
187
|
+
try {
|
|
188
|
+
await post(`${base}/git/refs`, { ref: `refs/${ref}`, sha: commit.sha });
|
|
189
|
+
} catch (error) {
|
|
190
|
+
const observed = await request(`${base}/git/ref/${ref}`);
|
|
191
|
+
if (observed.object?.sha !== commit.sha) throw error;
|
|
192
|
+
}
|
|
193
|
+
const observed = await request(`${base}/git/ref/${ref}`);
|
|
194
|
+
if (observed.object?.sha !== commit.sha)
|
|
195
|
+
throw new Error("Version material ref readback mismatch");
|
|
196
|
+
return source;
|
|
197
|
+
}
|
|
198
|
+
return { inspect, materialize, materializeDevelopment };
|
|
199
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { object } from "../../consumer/contract/shape.js";
|
|
2
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
3
|
+
|
|
4
|
+
export const PIPELINE_WORKER = "buildchain.pipeline-worker/v1";
|
|
5
|
+
|
|
6
|
+
export function validatePipelineWorker(binding, current, warrant) {
|
|
7
|
+
object(binding, [
|
|
8
|
+
"schema",
|
|
9
|
+
"repository",
|
|
10
|
+
"attempt",
|
|
11
|
+
"generation",
|
|
12
|
+
"sourceHead",
|
|
13
|
+
"candidateId",
|
|
14
|
+
"fencingToken",
|
|
15
|
+
"leaseGeneration",
|
|
16
|
+
"runId",
|
|
17
|
+
"runAttempt",
|
|
18
|
+
"nativeJobId",
|
|
19
|
+
"sealJobId",
|
|
20
|
+
]);
|
|
21
|
+
if (
|
|
22
|
+
binding.schema !== PIPELINE_WORKER ||
|
|
23
|
+
binding.repository !== current.intent.repository ||
|
|
24
|
+
binding.attempt !== current.identity.id ||
|
|
25
|
+
binding.generation !== current.generation.id ||
|
|
26
|
+
binding.sourceHead !== current.generation.source.commit ||
|
|
27
|
+
binding.candidateId !== warrant.candidateId ||
|
|
28
|
+
binding.fencingToken !== warrant.fencingToken ||
|
|
29
|
+
binding.leaseGeneration !== warrant.generation
|
|
30
|
+
)
|
|
31
|
+
throw new Error("Native worker does not bind the active pipeline Warrant");
|
|
32
|
+
for (const key of [
|
|
33
|
+
"runId",
|
|
34
|
+
"runAttempt",
|
|
35
|
+
"nativeJobId",
|
|
36
|
+
"sealJobId",
|
|
37
|
+
"leaseGeneration",
|
|
38
|
+
])
|
|
39
|
+
if (!Number.isSafeInteger(binding[key]) || binding[key] < 1)
|
|
40
|
+
throw new Error("Native worker provider identity is invalid");
|
|
41
|
+
if (binding.nativeJobId === binding.sealJobId)
|
|
42
|
+
throw new Error("Native execution and evidence seal must be separate jobs");
|
|
43
|
+
return binding;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function githubPipelineWorker(request) {
|
|
47
|
+
async function jobs(base, binding) {
|
|
48
|
+
const result = [];
|
|
49
|
+
for (let page = 1; page <= 100; page++) {
|
|
50
|
+
const response = await request(
|
|
51
|
+
`${base}/actions/runs/${binding.runId}/attempts/${binding.runAttempt}/jobs?per_page=100&page=${page}`,
|
|
52
|
+
);
|
|
53
|
+
if (!Array.isArray(response.jobs))
|
|
54
|
+
throw new Error("Missing provider jobs readback");
|
|
55
|
+
result.push(...response.jobs);
|
|
56
|
+
if (response.jobs.length < 100) return result;
|
|
57
|
+
}
|
|
58
|
+
throw new Error("Pipeline worker jobs exceed the readback bound");
|
|
59
|
+
}
|
|
60
|
+
async function observe(binding, current, warrant) {
|
|
61
|
+
validatePipelineWorker(binding, current, warrant);
|
|
62
|
+
const base = `/repos/${binding.repository}`;
|
|
63
|
+
const run = await request(`${base}/actions/runs/${binding.runId}`);
|
|
64
|
+
if (
|
|
65
|
+
run.id !== binding.runId ||
|
|
66
|
+
run.run_attempt !== binding.runAttempt ||
|
|
67
|
+
run.repository?.full_name !== binding.repository
|
|
68
|
+
)
|
|
69
|
+
throw new Error(
|
|
70
|
+
"Native worker provider run changed; terminality is unproved",
|
|
71
|
+
);
|
|
72
|
+
const entries = await jobs(base, binding);
|
|
73
|
+
const selected = [binding.nativeJobId, binding.sealJobId].map((id) => {
|
|
74
|
+
const matches = entries.filter((job) => job.id === id);
|
|
75
|
+
if (
|
|
76
|
+
matches.length !== 1 ||
|
|
77
|
+
matches[0].run_id !== binding.runId ||
|
|
78
|
+
matches[0].run_attempt !== binding.runAttempt
|
|
79
|
+
)
|
|
80
|
+
throw new Error("Native worker exact job identity drift");
|
|
81
|
+
return matches[0];
|
|
82
|
+
});
|
|
83
|
+
const again = await request(`${base}/actions/runs/${binding.runId}`);
|
|
84
|
+
if (
|
|
85
|
+
again.id !== run.id ||
|
|
86
|
+
again.run_attempt !== run.run_attempt ||
|
|
87
|
+
again.status !== run.status ||
|
|
88
|
+
again.conclusion !== run.conclusion
|
|
89
|
+
)
|
|
90
|
+
throw new Error("Native worker changed during terminal readback");
|
|
91
|
+
const terminal =
|
|
92
|
+
run.status === "completed" &&
|
|
93
|
+
Boolean(run.conclusion) &&
|
|
94
|
+
selected.every(
|
|
95
|
+
(job) =>
|
|
96
|
+
job.status === "completed" && job.conclusion && job.completed_at,
|
|
97
|
+
);
|
|
98
|
+
const body = {
|
|
99
|
+
schema: "buildchain.pipeline-worker-readback/v1",
|
|
100
|
+
binding,
|
|
101
|
+
status: terminal ? "completed" : "in_progress",
|
|
102
|
+
run,
|
|
103
|
+
jobs: selected,
|
|
104
|
+
};
|
|
105
|
+
return { ...body, root: recordDigest(body) };
|
|
106
|
+
}
|
|
107
|
+
return { observe };
|
|
108
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
3
|
+
|
|
4
|
+
const LIMIT = 8 * 1024 * 1024;
|
|
5
|
+
|
|
6
|
+
async function inventory(request, prefix) {
|
|
7
|
+
const assets = [],
|
|
8
|
+
ids = new Set(),
|
|
9
|
+
names = new Set();
|
|
10
|
+
for (let page = 1; page <= 20; page++) {
|
|
11
|
+
const values = await request(`${prefix}/assets?per_page=100&page=${page}`);
|
|
12
|
+
if (!Array.isArray(values) || values.length > 100)
|
|
13
|
+
throw new Error("Published evidence asset inventory is incomplete");
|
|
14
|
+
for (const value of values) {
|
|
15
|
+
if (
|
|
16
|
+
!Number.isSafeInteger(value.id) ||
|
|
17
|
+
value.id < 1 ||
|
|
18
|
+
typeof value.name !== "string" ||
|
|
19
|
+
!value.name ||
|
|
20
|
+
ids.has(value.id) ||
|
|
21
|
+
names.has(value.name)
|
|
22
|
+
)
|
|
23
|
+
throw new Error("Published evidence asset inventory is ambiguous");
|
|
24
|
+
ids.add(value.id);
|
|
25
|
+
names.add(value.name);
|
|
26
|
+
assets.push(value);
|
|
27
|
+
}
|
|
28
|
+
if (values.length < 100) return assets;
|
|
29
|
+
}
|
|
30
|
+
throw new Error(
|
|
31
|
+
"Published evidence inventory exceeds its complete-read bound",
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function selectedAssets(assets, names) {
|
|
36
|
+
return names.map((name) => {
|
|
37
|
+
const asset = assets.find((value) => value.name === name);
|
|
38
|
+
if (!asset) return { name, missing: true };
|
|
39
|
+
if (
|
|
40
|
+
asset.state !== "uploaded" ||
|
|
41
|
+
!Number.isSafeInteger(asset.size) ||
|
|
42
|
+
asset.size < 1 ||
|
|
43
|
+
asset.size > LIMIT ||
|
|
44
|
+
!/^sha256:[0-9a-f]{64}$/u.test(asset.digest || "")
|
|
45
|
+
)
|
|
46
|
+
throw new Error("Published evidence asset lacks bounded immutable bytes");
|
|
47
|
+
return { id: asset.id, name, size: asset.size, digest: asset.digest };
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Download only fixed evidence names from the exact already-observed release.
|
|
52
|
+
// There is no caller-supplied URL, artifact selector or provider write here.
|
|
53
|
+
export async function readReleaseEvidenceAssets(host, candidate, names) {
|
|
54
|
+
const repository = host.repository;
|
|
55
|
+
if (
|
|
56
|
+
!/^[\w.-]+\/[\w.-]+$/u.test(repository || "") ||
|
|
57
|
+
!Number.isSafeInteger(candidate.id) ||
|
|
58
|
+
candidate.id < 1
|
|
59
|
+
)
|
|
60
|
+
throw new Error("Published evidence requires exact release coordinates");
|
|
61
|
+
const prefix = `/repos/${repository}/releases/${candidate.id}`;
|
|
62
|
+
const selected = selectedAssets(await inventory(host.request, prefix), names);
|
|
63
|
+
const missing = selected
|
|
64
|
+
.filter((value) => value.missing)
|
|
65
|
+
.map(({ name }) => name);
|
|
66
|
+
if (missing.length) return { status: "missing", missing };
|
|
67
|
+
const [owner, repo] = repository.split("/");
|
|
68
|
+
const values = {};
|
|
69
|
+
for (const asset of selected) {
|
|
70
|
+
const result = await host.github.rest.repos.getReleaseAsset({
|
|
71
|
+
owner,
|
|
72
|
+
repo,
|
|
73
|
+
asset_id: asset.id,
|
|
74
|
+
headers: { accept: "application/octet-stream" },
|
|
75
|
+
});
|
|
76
|
+
const bytes = Buffer.from(result.data);
|
|
77
|
+
if (
|
|
78
|
+
bytes.length !== asset.size ||
|
|
79
|
+
bytes.length > LIMIT ||
|
|
80
|
+
`sha256:${createHash("sha256").update(bytes).digest("hex")}` !==
|
|
81
|
+
asset.digest
|
|
82
|
+
)
|
|
83
|
+
throw new Error(
|
|
84
|
+
"Published evidence download differs from provider bytes",
|
|
85
|
+
);
|
|
86
|
+
values[asset.name] = bytes;
|
|
87
|
+
}
|
|
88
|
+
if (
|
|
89
|
+
recordDigest(
|
|
90
|
+
selectedAssets(await inventory(host.request, prefix), names),
|
|
91
|
+
) !== recordDigest(selected)
|
|
92
|
+
)
|
|
93
|
+
throw new Error(
|
|
94
|
+
"Published evidence assets changed during provider readback",
|
|
95
|
+
);
|
|
96
|
+
return { status: "present", values, assets: selected };
|
|
97
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readNpmPackResult } from "../npm/pack-result.js";
|
|
1
2
|
import { execFileSync } from "node:child_process";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
@@ -117,7 +118,7 @@ export function hydratePublishedPublicationRegistry({
|
|
|
117
118
|
for (const version of publishedVersions) {
|
|
118
119
|
const packageDir = path.join(resolvedOutputDir, `.package-${version}`);
|
|
119
120
|
fs.mkdirSync(packageDir, { recursive: true });
|
|
120
|
-
const
|
|
121
|
+
const pack = readNpmPackResult(
|
|
121
122
|
commandRunner(
|
|
122
123
|
"npm",
|
|
123
124
|
[
|
|
@@ -132,7 +133,6 @@ export function hydratePublishedPublicationRegistry({
|
|
|
132
133
|
{ cwd: resolvedCwd },
|
|
133
134
|
),
|
|
134
135
|
);
|
|
135
|
-
const pack = Array.isArray(packed) ? packed[0] : packed;
|
|
136
136
|
if (!pack?.filename || !pack?.integrity) {
|
|
137
137
|
throw new Error(
|
|
138
138
|
`npm pack did not report filename and integrity for ${packageName}@${version}`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { readNpmPackResult } from "./pack-result.js";
|
|
1
2
|
export function summarizePackPreview(stdout) {
|
|
2
|
-
const
|
|
3
|
-
const pack = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
3
|
+
const pack = readNpmPackResult(stdout);
|
|
4
4
|
const files = Array.isArray(pack?.files) ? pack.files : [];
|
|
5
5
|
const bin = files.find((file) => file.path === "bin/buildchain.mjs");
|
|
6
6
|
const fileEntries = files.map((file) => ({
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export function readNpmPackResult(stdout) {
|
|
4
|
+
const parsed = JSON.parse(stdout);
|
|
5
|
+
const entries = Array.isArray(parsed)
|
|
6
|
+
? parsed
|
|
7
|
+
: typeof parsed?.name === "string"
|
|
8
|
+
? [parsed]
|
|
9
|
+
: parsed && typeof parsed === "object"
|
|
10
|
+
? Object.values(parsed)
|
|
11
|
+
: [];
|
|
12
|
+
if (entries.length !== 1)
|
|
13
|
+
throw new Error("npm pack must return exactly one package");
|
|
14
|
+
const pack = entries[0];
|
|
15
|
+
if (
|
|
16
|
+
typeof pack?.name !== "string" ||
|
|
17
|
+
!pack.name ||
|
|
18
|
+
typeof pack?.version !== "string" ||
|
|
19
|
+
!pack.version
|
|
20
|
+
) {
|
|
21
|
+
throw new Error("npm pack did not return package name and version");
|
|
22
|
+
}
|
|
23
|
+
if (
|
|
24
|
+
typeof pack.filename !== "string" ||
|
|
25
|
+
!pack.filename.endsWith(".tgz") ||
|
|
26
|
+
/[/\\]/u.test(pack.filename) ||
|
|
27
|
+
path.basename(pack.filename) !== pack.filename
|
|
28
|
+
)
|
|
29
|
+
throw new Error("npm pack must return a local tarball filename");
|
|
30
|
+
return pack;
|
|
31
|
+
}
|