@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,104 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
4
|
+
import { githubPipelinePublicationArtifacts } from "../../providers/github/pipeline-publication-artifacts.js";
|
|
5
|
+
import { publicationContext, uniquePublicationMaterial } from "./context.js";
|
|
6
|
+
import { qualifyPipelineProducts } from "./qualification.js";
|
|
7
|
+
import { pipelineProductCapsules } from "./capsules.js";
|
|
8
|
+
import { retainPipelineProducts } from "./sealed-products.js";
|
|
9
|
+
import { preparePipelineSigning, verifyPipelineSigning } from "./signing.js";
|
|
10
|
+
import { pipelineReleaseDocuments } from "./documents.js";
|
|
11
|
+
import { downloadRecoveryPublicationBuild } from "./recovery-build-download.js";
|
|
12
|
+
import { prepareRecoveredSigning } from "./recovery-signing.js";
|
|
13
|
+
|
|
14
|
+
export async function preparePipelineQualification(context, host, directory) {
|
|
15
|
+
const { journal, archive } = await publicationContext(context, host);
|
|
16
|
+
if (context.recovery?.mode === "prepared")
|
|
17
|
+
return prepareRecoveredSigning(context, host, journal, archive, directory);
|
|
18
|
+
const products = githubPipelinePublicationArtifacts(host);
|
|
19
|
+
const productDirectory = path.join(directory, "products");
|
|
20
|
+
const { build, bundles } = context.recovery?.build
|
|
21
|
+
? await downloadRecoveryPublicationBuild(context, host, productDirectory)
|
|
22
|
+
: await products.download(context, productDirectory);
|
|
23
|
+
const { plan, materialization } = context;
|
|
24
|
+
const qualified = qualifyPipelineProducts({
|
|
25
|
+
plan,
|
|
26
|
+
source: materialization.source,
|
|
27
|
+
bundles,
|
|
28
|
+
build,
|
|
29
|
+
policyRoot: plan.contractRoot,
|
|
30
|
+
});
|
|
31
|
+
const capsules = pipelineProductCapsules({
|
|
32
|
+
plan,
|
|
33
|
+
materialization,
|
|
34
|
+
qualified,
|
|
35
|
+
bundles,
|
|
36
|
+
});
|
|
37
|
+
const sealed = await retainPipelineProducts(archive, qualified, bundles);
|
|
38
|
+
const prepared = {
|
|
39
|
+
schema: "buildchain.pipeline-qualification-preparation/v1",
|
|
40
|
+
contextRoot: recordDigest(context),
|
|
41
|
+
qualified,
|
|
42
|
+
capsules,
|
|
43
|
+
sealed,
|
|
44
|
+
};
|
|
45
|
+
await journal.fence();
|
|
46
|
+
await journal.record(
|
|
47
|
+
`publication/qualification-prepared/${context.runId}-${context.runAttempt}`,
|
|
48
|
+
prepared,
|
|
49
|
+
);
|
|
50
|
+
return preparePipelineSigning({
|
|
51
|
+
plan,
|
|
52
|
+
materialization,
|
|
53
|
+
qualified,
|
|
54
|
+
directory: path.join(directory, "signing"),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function sealPipelineQualification(
|
|
59
|
+
context,
|
|
60
|
+
host,
|
|
61
|
+
directory,
|
|
62
|
+
bundlePath,
|
|
63
|
+
) {
|
|
64
|
+
const { journal, archive } = await publicationContext(context, host);
|
|
65
|
+
const prepared = await uniquePublicationMaterial(
|
|
66
|
+
journal,
|
|
67
|
+
`publication/qualification-prepared/${context.runId}-${context.runAttempt}/`,
|
|
68
|
+
);
|
|
69
|
+
if (prepared.contextRoot !== recordDigest(context))
|
|
70
|
+
throw new Error("Signing preparation changed provider context");
|
|
71
|
+
const { plan, materialization } = context;
|
|
72
|
+
const signing = verifyPipelineSigning({
|
|
73
|
+
plan,
|
|
74
|
+
materialization,
|
|
75
|
+
qualified: prepared.qualified,
|
|
76
|
+
directory: path.join(directory, "verify-signing"),
|
|
77
|
+
bundlePath,
|
|
78
|
+
token: host.token,
|
|
79
|
+
});
|
|
80
|
+
const bundle = await archive.put(fs.readFileSync(bundlePath), {
|
|
81
|
+
name: "attestation.json",
|
|
82
|
+
mediaType: "application/json",
|
|
83
|
+
});
|
|
84
|
+
if (bundle.digest !== signing.bundleDigest)
|
|
85
|
+
throw new Error("Retained attestation differs from verified signing bytes");
|
|
86
|
+
await archive.read(bundle);
|
|
87
|
+
const documents = pipelineReleaseDocuments({
|
|
88
|
+
plan,
|
|
89
|
+
materialization,
|
|
90
|
+
qualified: prepared.qualified,
|
|
91
|
+
capsules: prepared.capsules,
|
|
92
|
+
signing,
|
|
93
|
+
});
|
|
94
|
+
const result = {
|
|
95
|
+
...prepared,
|
|
96
|
+
schema: "buildchain.pipeline-qualified-products/v1",
|
|
97
|
+
signing,
|
|
98
|
+
bundle,
|
|
99
|
+
documents,
|
|
100
|
+
};
|
|
101
|
+
await journal.fence();
|
|
102
|
+
await journal.record("publication/qualified", result);
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
import {
|
|
3
|
+
createReleaseInvocation,
|
|
4
|
+
createReleaseReceipt,
|
|
5
|
+
RELEASE_RECEIPT_CONTRACT,
|
|
6
|
+
} from "../../release/release-invocation.js";
|
|
7
|
+
import {
|
|
8
|
+
createDomainPublicationQualificationReceipt,
|
|
9
|
+
validatePublicationQualificationReceipt,
|
|
10
|
+
} from "../publication-qualification.js";
|
|
11
|
+
import { verifyPipelineQualification } from "./documents.js";
|
|
12
|
+
|
|
13
|
+
export const RECOVERY_PUBLICATION_ADMISSION =
|
|
14
|
+
"buildchain.pipeline-recovery-publication-admission/v1";
|
|
15
|
+
|
|
16
|
+
// Only the publisher's byte/signature/provider requalification calls this pure
|
|
17
|
+
// domain bridge. It creates fresh authority for the new execution and preserves
|
|
18
|
+
// the original invocation, transaction, Passport and qualification unchanged.
|
|
19
|
+
export function createRecoveryPublicationAdmission({
|
|
20
|
+
context,
|
|
21
|
+
retained,
|
|
22
|
+
execution,
|
|
23
|
+
readback,
|
|
24
|
+
now = new Date(),
|
|
25
|
+
}) {
|
|
26
|
+
const { plan, materialization } = context;
|
|
27
|
+
const { qualified, documents } = retained;
|
|
28
|
+
const { root, ...body } = readback;
|
|
29
|
+
if (
|
|
30
|
+
!context.recovery ||
|
|
31
|
+
!/^sha256:[0-9a-f]{64}$/u.test(context.recovery.planRoot || "") ||
|
|
32
|
+
context.recovery.predecessor === context.attempt ||
|
|
33
|
+
execution.attempt !== context.attempt ||
|
|
34
|
+
execution.runId !== context.runId ||
|
|
35
|
+
execution.runAttempt !== context.runAttempt ||
|
|
36
|
+
body.schema !== "buildchain.pipeline-recovery-publication-readback/v1" ||
|
|
37
|
+
root !== recordDigest(body) ||
|
|
38
|
+
body.transactionRoot !== documents.transaction.transactionRoot ||
|
|
39
|
+
body.qualificationRoot !== qualified.root ||
|
|
40
|
+
body.signingRoot !== recordDigest(retained.signing) ||
|
|
41
|
+
body.sealedRoot !== retained.sealed.root
|
|
42
|
+
)
|
|
43
|
+
throw new Error(
|
|
44
|
+
"Recovery publication admission requires exact current execution and independent retained-byte readback",
|
|
45
|
+
);
|
|
46
|
+
verifyPipelineQualification({
|
|
47
|
+
plan,
|
|
48
|
+
materialization,
|
|
49
|
+
qualified,
|
|
50
|
+
evaluatedAt: qualified.qualification.issuedAt,
|
|
51
|
+
});
|
|
52
|
+
const qualification = createDomainPublicationQualificationReceipt({
|
|
53
|
+
...qualified.qualification,
|
|
54
|
+
issuedAt: now.toISOString(),
|
|
55
|
+
expiresAt: new Date(now.getTime() + 60 * 60 * 1000).toISOString(),
|
|
56
|
+
});
|
|
57
|
+
// This observation reports the predecessor transaction at the recovery
|
|
58
|
+
// boundary. It is not a replacement for any original terminal receipt.
|
|
59
|
+
const predecessorReceipt = createReleaseReceipt({
|
|
60
|
+
schema: RELEASE_RECEIPT_CONTRACT,
|
|
61
|
+
transactionRoot: documents.transaction.transactionRoot,
|
|
62
|
+
outcome: "blocked",
|
|
63
|
+
releasePassportRoot: documents.passport.passportRoot,
|
|
64
|
+
providerTransactionRoot: body.effectsRoot,
|
|
65
|
+
providerStateRoot: readback.root,
|
|
66
|
+
providerReceiptRoots: [...new Set(body.receiptRoots)].sort(),
|
|
67
|
+
});
|
|
68
|
+
const invocation = createReleaseInvocation({
|
|
69
|
+
...documents.invocation.invocation,
|
|
70
|
+
publisher: execution.publisher,
|
|
71
|
+
runtime: execution.runtime,
|
|
72
|
+
authority: {
|
|
73
|
+
policyRoot: plan.contractRoot,
|
|
74
|
+
qualificationRoot: qualification.receiptRoot,
|
|
75
|
+
warrantRoot: qualification.receiptRoot,
|
|
76
|
+
},
|
|
77
|
+
parent: {
|
|
78
|
+
invocationRoot: documents.invocation.roots.invocationRoot,
|
|
79
|
+
transactionRoot: documents.transaction.transactionRoot,
|
|
80
|
+
receiptRoot: predecessorReceipt.receiptRoot,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
const value = {
|
|
84
|
+
schema: RECOVERY_PUBLICATION_ADMISSION,
|
|
85
|
+
contextRoot: recordDigest(context),
|
|
86
|
+
recoveryPlanRoot: context.recovery.planRoot,
|
|
87
|
+
execution,
|
|
88
|
+
originalDocumentsRoot: recordDigest(documents),
|
|
89
|
+
readbackRoot: readback.root,
|
|
90
|
+
qualification,
|
|
91
|
+
predecessorReceipt,
|
|
92
|
+
invocation,
|
|
93
|
+
};
|
|
94
|
+
return { ...value, root: recordDigest(value) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function verifyRecoveryPublicationAdmission(
|
|
98
|
+
admission,
|
|
99
|
+
{ context, retained, execution, readback, now = new Date() },
|
|
100
|
+
) {
|
|
101
|
+
const expected = createRecoveryPublicationAdmission({
|
|
102
|
+
context,
|
|
103
|
+
retained,
|
|
104
|
+
execution,
|
|
105
|
+
readback,
|
|
106
|
+
now: new Date(admission.qualification.issuedAt),
|
|
107
|
+
});
|
|
108
|
+
if (recordDigest(expected) !== recordDigest(admission))
|
|
109
|
+
throw new Error(
|
|
110
|
+
"Recovery publication authority changed its exact execution or original transaction",
|
|
111
|
+
);
|
|
112
|
+
validatePublicationQualificationReceipt(admission.qualification, {
|
|
113
|
+
repository: context.plan.source.repository,
|
|
114
|
+
candidateRoot: retained.qualified.qualification.candidateRoot,
|
|
115
|
+
sourceSha: retained.qualified.source.commit,
|
|
116
|
+
sourceRoot: recordDigest(retained.qualified.source),
|
|
117
|
+
artifactRoot: retained.qualified.qualification.artifactRoot,
|
|
118
|
+
policyDigest: context.plan.contractRoot,
|
|
119
|
+
evaluatedAt: now.toISOString(),
|
|
120
|
+
});
|
|
121
|
+
return admission;
|
|
122
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readPipelineCaller } from "../../providers/github/pipeline-run-entry.js";
|
|
3
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
4
|
+
import { qualifyRecoveryRuntime } from "../../workflow/pipeline/recovery-runtime.js";
|
|
5
|
+
import { githubPipelinePublicationArtifacts } from "../../providers/github/pipeline-publication-artifacts.js";
|
|
6
|
+
import { qualifyPublicationBuildSegments } from "./build-segments.js";
|
|
7
|
+
|
|
8
|
+
export async function downloadRecoveryPublicationBuild(
|
|
9
|
+
context,
|
|
10
|
+
host,
|
|
11
|
+
directory,
|
|
12
|
+
) {
|
|
13
|
+
const recovery = context.recovery.build;
|
|
14
|
+
const { root, ...body } = recovery;
|
|
15
|
+
if (
|
|
16
|
+
root !== recordDigest(body) ||
|
|
17
|
+
recordDigest(recovery.source) !==
|
|
18
|
+
recordDigest(context.materialization.source)
|
|
19
|
+
)
|
|
20
|
+
throw new Error(
|
|
21
|
+
"Recovery publication build changed its retained source evidence",
|
|
22
|
+
);
|
|
23
|
+
const provider = githubPipelinePublicationArtifacts(host),
|
|
24
|
+
segments = [],
|
|
25
|
+
bundles = [];
|
|
26
|
+
for (const [index, segment] of recovery.segments.entries()) {
|
|
27
|
+
const comparison = await qualifyRecoveryRuntime(
|
|
28
|
+
segment.runtime,
|
|
29
|
+
host.runtime,
|
|
30
|
+
"publication-build",
|
|
31
|
+
host.request,
|
|
32
|
+
);
|
|
33
|
+
if (!comparison.compatible)
|
|
34
|
+
throw new Error(
|
|
35
|
+
"Recovered publication producer changed after runtime admission",
|
|
36
|
+
);
|
|
37
|
+
const original = {
|
|
38
|
+
plan: segment.plan,
|
|
39
|
+
materialization: segment.materialization,
|
|
40
|
+
runId: segment.build.runId,
|
|
41
|
+
runAttempt: segment.build.runAttempt,
|
|
42
|
+
};
|
|
43
|
+
const downloaded = await provider.download(
|
|
44
|
+
original,
|
|
45
|
+
path.join(directory, `predecessor-${index}`),
|
|
46
|
+
segment.build.platforms,
|
|
47
|
+
);
|
|
48
|
+
if (recordDigest(downloaded.build) !== recordDigest(segment.build))
|
|
49
|
+
throw new Error(
|
|
50
|
+
"Recovered publication artifact evidence changed after admission",
|
|
51
|
+
);
|
|
52
|
+
segments.push(segment);
|
|
53
|
+
bundles.push(...downloaded.bundles);
|
|
54
|
+
}
|
|
55
|
+
if (recovery.scheduled.length) {
|
|
56
|
+
const downloaded = await provider.download(
|
|
57
|
+
context,
|
|
58
|
+
path.join(directory, "current"),
|
|
59
|
+
recovery.scheduled,
|
|
60
|
+
);
|
|
61
|
+
segments.push({
|
|
62
|
+
plan: context.plan,
|
|
63
|
+
materialization: context.materialization,
|
|
64
|
+
runtime: host.runtime,
|
|
65
|
+
build: downloaded.build,
|
|
66
|
+
});
|
|
67
|
+
bundles.push(...downloaded.bundles);
|
|
68
|
+
}
|
|
69
|
+
const { run } = await host.runs.read(context.runId, context.runAttempt);
|
|
70
|
+
await readPipelineCaller(
|
|
71
|
+
run,
|
|
72
|
+
context.materialization.source.configPath,
|
|
73
|
+
host.request,
|
|
74
|
+
host.repository,
|
|
75
|
+
);
|
|
76
|
+
return {
|
|
77
|
+
build: qualifyPublicationBuildSegments(context, segments, run.head_sha),
|
|
78
|
+
bundles,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
import { qualifyRecoveryRuntime } from "../../workflow/pipeline/recovery-runtime.js";
|
|
3
|
+
import { githubPipelinePublicationArtifacts } from "../../providers/github/pipeline-publication-artifacts.js";
|
|
4
|
+
import { verifyPipelinePublicationPlan } from "./plan.js";
|
|
5
|
+
|
|
6
|
+
export function assertRecoveryProductContract(
|
|
7
|
+
original,
|
|
8
|
+
plan,
|
|
9
|
+
source,
|
|
10
|
+
materialization,
|
|
11
|
+
) {
|
|
12
|
+
verifyPipelinePublicationPlan(original);
|
|
13
|
+
if (
|
|
14
|
+
original.version !== plan.version ||
|
|
15
|
+
original.contractRoot !== plan.contractRoot ||
|
|
16
|
+
recordDigest(original.outputs) !== recordDigest(plan.outputs) ||
|
|
17
|
+
recordDigest(original.source) !== recordDigest(plan.source) ||
|
|
18
|
+
recordDigest(source) !== recordDigest(materialization.source)
|
|
19
|
+
)
|
|
20
|
+
throw new Error(
|
|
21
|
+
"Recovered product build changed version, source or declared outputs",
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function completedPlatforms(context, platforms, host) {
|
|
26
|
+
const { run, jobs } = await host.runs.read(context.runId, context.runAttempt);
|
|
27
|
+
if (run.status !== "completed")
|
|
28
|
+
throw new Error("Publication recovery cannot reuse an active producer");
|
|
29
|
+
return platforms.filter((platform) => {
|
|
30
|
+
const name = `Build publication (${platform})`;
|
|
31
|
+
const matches = jobs.filter(
|
|
32
|
+
(job) => job.name === name || job.name.endsWith(` / ${name}`),
|
|
33
|
+
);
|
|
34
|
+
if (matches.length > 1)
|
|
35
|
+
throw new Error("Publication recovery product job is ambiguous");
|
|
36
|
+
return (
|
|
37
|
+
matches[0]?.status === "completed" && matches[0]?.conclusion === "success"
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function planRecoveryPublicationBuild(
|
|
43
|
+
session,
|
|
44
|
+
plan,
|
|
45
|
+
materialization,
|
|
46
|
+
materials,
|
|
47
|
+
host,
|
|
48
|
+
) {
|
|
49
|
+
const declared = [
|
|
50
|
+
...new Set(plan.outputs.map(({ platform }) => platform)),
|
|
51
|
+
].sort();
|
|
52
|
+
const contexts = [
|
|
53
|
+
...new Map(
|
|
54
|
+
materials
|
|
55
|
+
.filter((item) =>
|
|
56
|
+
/^publication\/(?:context|predecessor-context)\//u.test(item.id),
|
|
57
|
+
)
|
|
58
|
+
.map(({ value }) => [recordDigest(value), value]),
|
|
59
|
+
).values(),
|
|
60
|
+
].reverse();
|
|
61
|
+
if (contexts.length > 100)
|
|
62
|
+
throw new Error("Publication producer history exceeds its bound");
|
|
63
|
+
const remaining = new Set(declared),
|
|
64
|
+
segments = [],
|
|
65
|
+
comparisons = [];
|
|
66
|
+
const provider = githubPipelinePublicationArtifacts(host);
|
|
67
|
+
for (const context of contexts) {
|
|
68
|
+
if (!remaining.size) break;
|
|
69
|
+
const owner = session.observed.history.find(
|
|
70
|
+
(item) => item.identity.id === context.attempt,
|
|
71
|
+
);
|
|
72
|
+
if (
|
|
73
|
+
!owner ||
|
|
74
|
+
owner.generation.id !== session.observed.generation ||
|
|
75
|
+
context.schema !== "buildchain.pipeline-publication-context/v1"
|
|
76
|
+
)
|
|
77
|
+
throw new Error(
|
|
78
|
+
"Publication producer has no canonical same-generation attempt",
|
|
79
|
+
);
|
|
80
|
+
assertRecoveryProductContract(
|
|
81
|
+
context.plan,
|
|
82
|
+
plan,
|
|
83
|
+
context.materialization.source,
|
|
84
|
+
materialization,
|
|
85
|
+
);
|
|
86
|
+
const producerRuntime = owner.events[0].runtime;
|
|
87
|
+
if (context.plan.runtime.commit !== producerRuntime.sha)
|
|
88
|
+
throw new Error(
|
|
89
|
+
"Publication product producer differs from its admitted runtime",
|
|
90
|
+
);
|
|
91
|
+
const comparison = await qualifyRecoveryRuntime(
|
|
92
|
+
producerRuntime,
|
|
93
|
+
host.runtime,
|
|
94
|
+
"publication-build",
|
|
95
|
+
host.request,
|
|
96
|
+
);
|
|
97
|
+
comparisons.push(comparison);
|
|
98
|
+
if (!comparison.compatible) continue;
|
|
99
|
+
const platforms = await completedPlatforms(context, [...remaining], host);
|
|
100
|
+
if (!platforms.length) continue;
|
|
101
|
+
const { build } = await provider.buildReadback(context, platforms);
|
|
102
|
+
segments.push({
|
|
103
|
+
plan: context.plan,
|
|
104
|
+
materialization: context.materialization,
|
|
105
|
+
runtime: producerRuntime,
|
|
106
|
+
build,
|
|
107
|
+
});
|
|
108
|
+
platforms.forEach((platform) => remaining.delete(platform));
|
|
109
|
+
}
|
|
110
|
+
const body = {
|
|
111
|
+
schema: "buildchain.pipeline-recovery-publication-build/v1",
|
|
112
|
+
source: materialization.source,
|
|
113
|
+
predecessorPlanRoot: plan.root,
|
|
114
|
+
segments,
|
|
115
|
+
comparisons,
|
|
116
|
+
scheduled: [...remaining].sort(),
|
|
117
|
+
reason: contexts.length
|
|
118
|
+
? "Requalify exact successful producer artifacts; execute only missing or explicitly changed implementations"
|
|
119
|
+
: "No product execution was retained; execute the declared publication platforms",
|
|
120
|
+
};
|
|
121
|
+
return { ...body, root: recordDigest(body) };
|
|
122
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { PUBLICATION_BUILD_AGGREGATE } from "./build-segments.js";
|
|
2
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
3
|
+
import { readPipelineCaller } from "../../providers/github/pipeline-run-entry.js";
|
|
4
|
+
|
|
5
|
+
export async function reobservePublicationBuild(
|
|
6
|
+
build,
|
|
7
|
+
source,
|
|
8
|
+
host,
|
|
9
|
+
depth = 0,
|
|
10
|
+
) {
|
|
11
|
+
const { root, ...body } = build;
|
|
12
|
+
if (
|
|
13
|
+
depth > 100 ||
|
|
14
|
+
root !== recordDigest(body) ||
|
|
15
|
+
recordDigest(build.source) !== recordDigest(source)
|
|
16
|
+
)
|
|
17
|
+
throw new Error(
|
|
18
|
+
"Publication recovery build lineage changed its source or exceeds its bound",
|
|
19
|
+
);
|
|
20
|
+
if (build.schema === PUBLICATION_BUILD_AGGREGATE) {
|
|
21
|
+
if (
|
|
22
|
+
!Array.isArray(build.segments) ||
|
|
23
|
+
!build.segments.length ||
|
|
24
|
+
build.segments.length > 100
|
|
25
|
+
)
|
|
26
|
+
throw new Error("Publication producer segment history exceeds its bound");
|
|
27
|
+
for (const segment of build.segments)
|
|
28
|
+
await reobservePublicationBuild(segment.build, source, host, depth + 1);
|
|
29
|
+
return build;
|
|
30
|
+
}
|
|
31
|
+
if (build.schema === "buildchain.pipeline-publication-requalification/v1")
|
|
32
|
+
return reobservePublicationBuild(
|
|
33
|
+
build.predecessorBuild,
|
|
34
|
+
source,
|
|
35
|
+
host,
|
|
36
|
+
depth + 1,
|
|
37
|
+
);
|
|
38
|
+
if (
|
|
39
|
+
build.schema !== "buildchain.pipeline-publication-build-readback/v1" ||
|
|
40
|
+
!Array.isArray(build.jobs) ||
|
|
41
|
+
!build.jobs.length ||
|
|
42
|
+
build.outcome !== "success"
|
|
43
|
+
)
|
|
44
|
+
throw new Error(
|
|
45
|
+
"Publication recovery requires actual retained successful product jobs",
|
|
46
|
+
);
|
|
47
|
+
const { run, jobs } = await host.runs.read(build.runId, build.runAttempt);
|
|
48
|
+
if (run.head_sha !== build.providerSource)
|
|
49
|
+
throw new Error("Publication producer changed its exact provider source");
|
|
50
|
+
await readPipelineCaller(
|
|
51
|
+
run,
|
|
52
|
+
source.configPath,
|
|
53
|
+
host.request,
|
|
54
|
+
host.repository,
|
|
55
|
+
);
|
|
56
|
+
for (const old of build.jobs)
|
|
57
|
+
if (
|
|
58
|
+
old.status !== "completed" ||
|
|
59
|
+
old.conclusion !== "success" ||
|
|
60
|
+
old.run_id !== build.runId ||
|
|
61
|
+
old.run_attempt !== build.runAttempt ||
|
|
62
|
+
!jobs.some((job) => recordDigest(job) === recordDigest(old))
|
|
63
|
+
)
|
|
64
|
+
throw new Error(
|
|
65
|
+
"Retained publication product job changed during requalification",
|
|
66
|
+
);
|
|
67
|
+
return build;
|
|
68
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
import {
|
|
3
|
+
validateStageCapsule,
|
|
4
|
+
stageCapsuleIdentityRoot,
|
|
5
|
+
stageCapsuleRoot,
|
|
6
|
+
} from "../../build/stage-capsule.js";
|
|
7
|
+
import { verifyRootedPublication } from "./documents.js";
|
|
8
|
+
|
|
9
|
+
export function requalifyPublicationCapsules(previous, qualified, plan) {
|
|
10
|
+
verifyRootedPublication(previous, "buildchain.pipeline-product-capsules/v1");
|
|
11
|
+
if (
|
|
12
|
+
recordDigest(previous.capsules.map((item) => item.artifact)) !==
|
|
13
|
+
recordDigest(qualified.artifacts)
|
|
14
|
+
)
|
|
15
|
+
throw new Error(
|
|
16
|
+
"Recovery capsules changed the exact sealed artifact inventory",
|
|
17
|
+
);
|
|
18
|
+
const capsules = previous.capsules.map((entry) => {
|
|
19
|
+
validateStageCapsule(entry.capsule);
|
|
20
|
+
const identity = {
|
|
21
|
+
...entry.capsule.identity,
|
|
22
|
+
qualificationRoot: qualified.qualification.receiptRoot,
|
|
23
|
+
declaredInputs: [
|
|
24
|
+
{ name: "predecessor-capsule", root: entry.capsule.capsuleRoot },
|
|
25
|
+
{ name: "publication-plan", root: plan.root },
|
|
26
|
+
],
|
|
27
|
+
observationRoots: [
|
|
28
|
+
{ name: "provider-build", root: qualified.build.root },
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
// Retain the producer's runtime and original retention promise, even when
|
|
32
|
+
// expired. The current sealed-byte readback supplies publication authority;
|
|
33
|
+
// this derived capsule does not invent a renewed Actions artifact lease.
|
|
34
|
+
const capsule = {
|
|
35
|
+
...entry.capsule,
|
|
36
|
+
identity,
|
|
37
|
+
identityRoot: stageCapsuleIdentityRoot(identity),
|
|
38
|
+
};
|
|
39
|
+
capsule.capsuleRoot = stageCapsuleRoot(capsule);
|
|
40
|
+
validateStageCapsule(capsule);
|
|
41
|
+
return {
|
|
42
|
+
...entry,
|
|
43
|
+
capsule,
|
|
44
|
+
predecessorCapsuleRoot: entry.capsule.capsuleRoot,
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
const body = {
|
|
48
|
+
schema: "buildchain.pipeline-product-capsules/v1",
|
|
49
|
+
qualificationRoot: qualified.root,
|
|
50
|
+
predecessorRoot: previous.root,
|
|
51
|
+
capsules,
|
|
52
|
+
};
|
|
53
|
+
return { ...body, root: recordDigest(body) };
|
|
54
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
import { recoveryPublicationMaterial } from "./recovery-materials.js";
|
|
3
|
+
import { deriveRecoveryPublicationPlan } from "./recovery-plan.js";
|
|
4
|
+
import {
|
|
5
|
+
PUBLICATION_IMPORT,
|
|
6
|
+
publicationImportedValues,
|
|
7
|
+
} from "./imported-materials.js";
|
|
8
|
+
|
|
9
|
+
function importedId(item, publication) {
|
|
10
|
+
const id = item.id;
|
|
11
|
+
if (id.startsWith("publication/worker/")) return null;
|
|
12
|
+
if (id.startsWith("publication/context/"))
|
|
13
|
+
return "publication/predecessor-context";
|
|
14
|
+
if (
|
|
15
|
+
id.startsWith("publication/qualification-prepared/") ||
|
|
16
|
+
id.startsWith("publication/predecessor-prepared/")
|
|
17
|
+
)
|
|
18
|
+
return publication.mode !== "qualified" &&
|
|
19
|
+
recordDigest(item.value) === publication.preparedRoot
|
|
20
|
+
? "publication/predecessor-prepared"
|
|
21
|
+
: null;
|
|
22
|
+
for (const name of [
|
|
23
|
+
"complete",
|
|
24
|
+
"distribution-complete",
|
|
25
|
+
"next-development-result",
|
|
26
|
+
])
|
|
27
|
+
if (id.startsWith(`publication/${name}/`))
|
|
28
|
+
return `publication/predecessor-${name}`;
|
|
29
|
+
if (publication.mode !== "qualified")
|
|
30
|
+
for (const name of ["plan", "materialization"])
|
|
31
|
+
if (id.startsWith(`publication/${name}/`))
|
|
32
|
+
return `publication/predecessor-${name}`;
|
|
33
|
+
return id.slice(0, id.lastIndexOf("/"));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function importRecoveredPublication(
|
|
37
|
+
publication,
|
|
38
|
+
execution,
|
|
39
|
+
recoveryPlanRoot,
|
|
40
|
+
journal,
|
|
41
|
+
phase,
|
|
42
|
+
) {
|
|
43
|
+
const values = [];
|
|
44
|
+
const add = (id, value) =>
|
|
45
|
+
values.push({ id: `${id}/${recordDigest(value).slice(7)}`, value });
|
|
46
|
+
for (const item of publication.materials) {
|
|
47
|
+
const id = importedId(item, publication);
|
|
48
|
+
if (id) add(id, item.value);
|
|
49
|
+
}
|
|
50
|
+
if (publication.mode !== "qualified") {
|
|
51
|
+
const original = recoveryPublicationMaterial(
|
|
52
|
+
publication.materials,
|
|
53
|
+
"publication/plan/",
|
|
54
|
+
true,
|
|
55
|
+
);
|
|
56
|
+
const materialization = recoveryPublicationMaterial(
|
|
57
|
+
publication.materials,
|
|
58
|
+
"publication/materialization/",
|
|
59
|
+
);
|
|
60
|
+
const derived = deriveRecoveryPublicationPlan(
|
|
61
|
+
original,
|
|
62
|
+
materialization,
|
|
63
|
+
execution,
|
|
64
|
+
recoveryPlanRoot,
|
|
65
|
+
);
|
|
66
|
+
add("publication/plan", derived.plan);
|
|
67
|
+
if (derived.materialization)
|
|
68
|
+
add("publication/materialization", derived.materialization);
|
|
69
|
+
}
|
|
70
|
+
const body = {
|
|
71
|
+
schema: PUBLICATION_IMPORT,
|
|
72
|
+
admissionRoot: recoveryPlanRoot,
|
|
73
|
+
values: [...new Map(values.map((item) => [item.id, item])).values()],
|
|
74
|
+
};
|
|
75
|
+
const bundle = { ...body, root: recordDigest(body) };
|
|
76
|
+
publicationImportedValues(bundle);
|
|
77
|
+
// One journal append publishes the complete import and derived source/plan.
|
|
78
|
+
// An interruption cannot expose a new plan beside an old materialization.
|
|
79
|
+
await journal.record("publication/recovery-import", bundle, { phase });
|
|
80
|
+
}
|