@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,219 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
import { readBusinessAttempt } from "../../workflow/attempt/reader.js";
|
|
3
|
+
import { verifyRootedPublication } from "./documents.js";
|
|
4
|
+
import { verifyPipelinePublicationPlan } from "./plan.js";
|
|
5
|
+
import {
|
|
6
|
+
pipelineEntryRuns,
|
|
7
|
+
pipelineEntryRun,
|
|
8
|
+
pipelineEntryCheck,
|
|
9
|
+
} from "../../providers/github/pipeline-entry-runs.js";
|
|
10
|
+
|
|
11
|
+
async function nativeBuild(check, run, consumer, host) {
|
|
12
|
+
const loaded = await host.index.resolve(check.attempt);
|
|
13
|
+
const observed = readBusinessAttempt(loaded.snapshot);
|
|
14
|
+
const selected = observed.history.find(
|
|
15
|
+
(item) => item.identity.id === check.attempt,
|
|
16
|
+
);
|
|
17
|
+
if (
|
|
18
|
+
observed.intent.repository !== host.repository ||
|
|
19
|
+
!selected ||
|
|
20
|
+
selected.identity.requestKey.startsWith("recover:") ||
|
|
21
|
+
recordDigest(selected.generation.source) !== recordDigest(consumer.source)
|
|
22
|
+
)
|
|
23
|
+
throw new Error(
|
|
24
|
+
"Published entry check points to another source or a recovery attempt",
|
|
25
|
+
);
|
|
26
|
+
const phase = selected.phases.build;
|
|
27
|
+
if (phase?.payload.state !== "success" || check.outcome !== "success")
|
|
28
|
+
return {
|
|
29
|
+
status: "missing",
|
|
30
|
+
reason: "latest-published-entry-build-failed",
|
|
31
|
+
attempt: check.attempt,
|
|
32
|
+
};
|
|
33
|
+
if (
|
|
34
|
+
phase.payload.writer.runId !== String(run.id) ||
|
|
35
|
+
phase.payload.writer.runAttempt !== String(run.run_attempt) ||
|
|
36
|
+
phase.runtime.repository !== "kungfu-systems/buildchain" ||
|
|
37
|
+
phase.runtime.sha !== consumer.selection.ref
|
|
38
|
+
)
|
|
39
|
+
throw new Error(
|
|
40
|
+
"Published entry native build changed its actual runtime or execution",
|
|
41
|
+
);
|
|
42
|
+
const references = phase.payload.materials.filter((item) =>
|
|
43
|
+
item.id.startsWith("build/provider-"),
|
|
44
|
+
);
|
|
45
|
+
if (references.length !== 1)
|
|
46
|
+
throw new Error(
|
|
47
|
+
"Published entry requires one original normal build receipt",
|
|
48
|
+
);
|
|
49
|
+
const session = {
|
|
50
|
+
intent: loaded.snapshot.intent,
|
|
51
|
+
observed: { ...observed, history: [selected] },
|
|
52
|
+
};
|
|
53
|
+
const readback = await host.materialStore(session).read(references[0]);
|
|
54
|
+
verifyRootedPublication(readback, "buildchain.pipeline-build-readback/v1");
|
|
55
|
+
if (
|
|
56
|
+
readback.runId !== run.id ||
|
|
57
|
+
readback.runAttempt !== run.run_attempt ||
|
|
58
|
+
readback.outcome !== "success" ||
|
|
59
|
+
recordDigest(readback.source) !== recordDigest(consumer.source) ||
|
|
60
|
+
recordDigest(readback.entry) !== recordDigest(consumer.entry) ||
|
|
61
|
+
check.summary !==
|
|
62
|
+
`Exact product jobs: ${readback.root}\nAttempt: ${check.attempt}`
|
|
63
|
+
)
|
|
64
|
+
throw new Error("Published entry check and native build material disagree");
|
|
65
|
+
return {
|
|
66
|
+
readback,
|
|
67
|
+
phase,
|
|
68
|
+
reference: references[0],
|
|
69
|
+
attempt: check.attempt,
|
|
70
|
+
intent: observed.intent.id,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function completion(run, jobs, native, source) {
|
|
75
|
+
const recorder = jobs.filter(
|
|
76
|
+
(job) => String(job.id) === native.phase.payload.writer.jobId,
|
|
77
|
+
);
|
|
78
|
+
if (
|
|
79
|
+
recorder.length !== 1 ||
|
|
80
|
+
recorder[0].status !== "completed" ||
|
|
81
|
+
recorder[0].conclusion !== "success" ||
|
|
82
|
+
recorder[0].run_id !== run.id ||
|
|
83
|
+
recorder[0].run_attempt !== run.run_attempt ||
|
|
84
|
+
!(
|
|
85
|
+
recorder[0].name === "Record product build" ||
|
|
86
|
+
recorder[0].name.endsWith(" / Record product build")
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
throw new Error(
|
|
90
|
+
"Published entry native receipt lacks its exact completed recorder job",
|
|
91
|
+
);
|
|
92
|
+
const selected = [...native.readback.jobs, recorder[0]];
|
|
93
|
+
const start = Date.parse(source.candidate.publishedAt),
|
|
94
|
+
completions = [];
|
|
95
|
+
for (const job of selected) {
|
|
96
|
+
const started = Date.parse(job.started_at),
|
|
97
|
+
ended = Date.parse(job.completed_at);
|
|
98
|
+
if (
|
|
99
|
+
!Number.isFinite(started) ||
|
|
100
|
+
!Number.isFinite(ended) ||
|
|
101
|
+
started < start ||
|
|
102
|
+
ended < started
|
|
103
|
+
)
|
|
104
|
+
throw new Error(
|
|
105
|
+
"Published entry jobs must execute after the exact Alpha publication",
|
|
106
|
+
);
|
|
107
|
+
completions.push(ended);
|
|
108
|
+
}
|
|
109
|
+
return new Date(Math.max(...completions)).toISOString();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function inspectEntryRun(listed, plan, source, host) {
|
|
113
|
+
const { run, jobs } = await host.runs.read(listed.id, listed.run_attempt);
|
|
114
|
+
const consumer = await pipelineEntryRun(run, plan, source, host);
|
|
115
|
+
if (!consumer) return null;
|
|
116
|
+
if (
|
|
117
|
+
!jobs.some(
|
|
118
|
+
(job) =>
|
|
119
|
+
job.conclusion !== "skipped" &&
|
|
120
|
+
/(?:^| \/ )Build product \([^)]+\)$/u.test(job.name),
|
|
121
|
+
) &&
|
|
122
|
+
run.conclusion === "success"
|
|
123
|
+
)
|
|
124
|
+
return null;
|
|
125
|
+
const check = await pipelineEntryCheck(run, host);
|
|
126
|
+
if (!check)
|
|
127
|
+
return {
|
|
128
|
+
status: "missing",
|
|
129
|
+
reason: "latest-published-entry-build-unrecorded",
|
|
130
|
+
runId: run.id,
|
|
131
|
+
};
|
|
132
|
+
const native = await nativeBuild(check, run, consumer, host);
|
|
133
|
+
if (native.status === "missing") return native;
|
|
134
|
+
const platforms = [
|
|
135
|
+
...new Set(plan.outputs.map(({ platform }) => platform)),
|
|
136
|
+
].sort();
|
|
137
|
+
const fresh = await host.runs.build(
|
|
138
|
+
run.id,
|
|
139
|
+
run.run_attempt,
|
|
140
|
+
consumer.source,
|
|
141
|
+
platforms,
|
|
142
|
+
);
|
|
143
|
+
if (recordDigest(fresh) !== recordDigest(native.readback))
|
|
144
|
+
throw new Error(
|
|
145
|
+
"Published entry original jobs changed or omit declared platforms",
|
|
146
|
+
);
|
|
147
|
+
const completedAt = completion(run, jobs, native, source);
|
|
148
|
+
const again = await host.runs.read(run.id, run.run_attempt);
|
|
149
|
+
const repeated = await nativeBuild(
|
|
150
|
+
await pipelineEntryCheck(again.run, host),
|
|
151
|
+
again.run,
|
|
152
|
+
consumer,
|
|
153
|
+
host,
|
|
154
|
+
);
|
|
155
|
+
if (
|
|
156
|
+
recordDigest(repeated) !== recordDigest(native) ||
|
|
157
|
+
recordDigest(again) !== recordDigest({ run, jobs })
|
|
158
|
+
)
|
|
159
|
+
throw new Error(
|
|
160
|
+
"Published entry evidence changed during final provider readback",
|
|
161
|
+
);
|
|
162
|
+
const body = {
|
|
163
|
+
schema: "buildchain.pipeline-stable-entry/v1",
|
|
164
|
+
planRoot: plan.root,
|
|
165
|
+
sourceRoot: source.root,
|
|
166
|
+
consumer,
|
|
167
|
+
native,
|
|
168
|
+
runId: run.id,
|
|
169
|
+
runAttempt: run.run_attempt,
|
|
170
|
+
canary: {
|
|
171
|
+
id: "published-entry",
|
|
172
|
+
source: "public-build",
|
|
173
|
+
status: "success",
|
|
174
|
+
repository: host.repository,
|
|
175
|
+
candidateSha: source.candidate.sha,
|
|
176
|
+
completedAt,
|
|
177
|
+
attestor: "github-actions[bot]",
|
|
178
|
+
runtimeRef: consumer.selection.ref,
|
|
179
|
+
evidenceUrl: `https://github.com/${host.repository}/actions/runs/${run.id}/attempts/${run.run_attempt}`,
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
return { ...body, root: recordDigest(body) };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function readPipelineStableEntry(plan, source, host) {
|
|
186
|
+
verifyPipelinePublicationPlan(plan);
|
|
187
|
+
verifyRootedPublication(source, "buildchain.pipeline-stable-source/v1");
|
|
188
|
+
if (
|
|
189
|
+
plan.channel !== "stable" ||
|
|
190
|
+
source.planRoot !== plan.root ||
|
|
191
|
+
host.repository !== plan.source.repository ||
|
|
192
|
+
source.contractRoot !== plan.contractRoot ||
|
|
193
|
+
source.candidate.sha !== plan.intentSource.commit ||
|
|
194
|
+
!Number.isFinite(Date.parse(source.candidate.publishedAt))
|
|
195
|
+
)
|
|
196
|
+
throw new Error(
|
|
197
|
+
"Published entry collection requires its exact admitted Alpha plan",
|
|
198
|
+
);
|
|
199
|
+
const inventory = await pipelineEntryRuns(host, source.candidate.publishedAt);
|
|
200
|
+
for (const run of inventory) {
|
|
201
|
+
const result = await inspectEntryRun(run, plan, source, host);
|
|
202
|
+
if (result) {
|
|
203
|
+
if (
|
|
204
|
+
result.canary &&
|
|
205
|
+
recordDigest(
|
|
206
|
+
await pipelineEntryRuns(host, source.candidate.publishedAt),
|
|
207
|
+
) !== recordDigest(inventory)
|
|
208
|
+
)
|
|
209
|
+
throw new Error(
|
|
210
|
+
"Published entry run inventory changed during qualification",
|
|
211
|
+
);
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
status: "missing",
|
|
217
|
+
reason: "no-normal-published-entry-build-for-exact-runtime",
|
|
218
|
+
};
|
|
219
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
5
|
+
import { readPipelineReleaseEvidence } from "../../providers/github/pipeline-release-evidence.js";
|
|
6
|
+
import { readPipelineCaller } from "../../providers/github/pipeline-run-entry.js";
|
|
7
|
+
import { verifyPipelinePublicationPlan } from "./plan.js";
|
|
8
|
+
import {
|
|
9
|
+
verifyRootedPublication,
|
|
10
|
+
pipelineReleaseDocuments,
|
|
11
|
+
} from "./documents.js";
|
|
12
|
+
import { verifyPipelineSigning } from "./signing.js";
|
|
13
|
+
import { reobservePublicationBuild } from "./recovery-build-readback.js";
|
|
14
|
+
import { writeImmutablePublicationFile } from "./files.js";
|
|
15
|
+
|
|
16
|
+
function assertPublishedPlan(stablePlan, source, values) {
|
|
17
|
+
const { plan, qualification: qualified, release } = values;
|
|
18
|
+
verifyPipelinePublicationPlan(plan);
|
|
19
|
+
if (
|
|
20
|
+
plan.evidenceVersion !== 1 ||
|
|
21
|
+
plan.channel !== "alpha" ||
|
|
22
|
+
plan.version !== stablePlan.candidateVersion ||
|
|
23
|
+
plan.tag !== source.candidate.tag ||
|
|
24
|
+
plan.contractRoot !== stablePlan.contractRoot ||
|
|
25
|
+
plan.route.operation !== "alpha" ||
|
|
26
|
+
plan.route.to !== stablePlan.route.from ||
|
|
27
|
+
plan.route.from !== stablePlan.developmentBranch ||
|
|
28
|
+
recordDigest(plan.outputs) !== recordDigest(stablePlan.outputs) ||
|
|
29
|
+
recordDigest(qualified.source) !== recordDigest(source.source) ||
|
|
30
|
+
recordDigest(release.source) !== recordDigest(source.source)
|
|
31
|
+
)
|
|
32
|
+
throw new Error(
|
|
33
|
+
"Published product evidence differs from the exact Alpha contract or source",
|
|
34
|
+
);
|
|
35
|
+
if (
|
|
36
|
+
plan.publisher.repository !== "kungfu-systems/buildchain" ||
|
|
37
|
+
plan.publisher.workflow !==
|
|
38
|
+
".github/workflows/.release-pipeline-products.yml" ||
|
|
39
|
+
plan.publisher.job !== "apply" ||
|
|
40
|
+
!/^[0-9a-f]{40}$/u.test(plan.publisher.workflowSha || "") ||
|
|
41
|
+
plan.runtime.repository !== "kungfu-systems/buildchain"
|
|
42
|
+
)
|
|
43
|
+
throw new Error(
|
|
44
|
+
"Published product evidence requires the central publisher",
|
|
45
|
+
);
|
|
46
|
+
const issued = Date.parse(qualified.qualification?.issuedAt);
|
|
47
|
+
if (
|
|
48
|
+
!Number.isFinite(issued) ||
|
|
49
|
+
issued > Date.parse(source.candidate.publishedAt)
|
|
50
|
+
)
|
|
51
|
+
throw new Error(
|
|
52
|
+
"Published product qualification cannot postdate its Alpha release",
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function buildLeaves(build) {
|
|
57
|
+
const pending = [build],
|
|
58
|
+
leaves = [];
|
|
59
|
+
let visited = 0;
|
|
60
|
+
while (pending.length) {
|
|
61
|
+
if (++visited > 100)
|
|
62
|
+
throw new Error("Published product build lineage exceeds its bound");
|
|
63
|
+
const value = pending.pop();
|
|
64
|
+
verifyRootedPublication(value, value.schema);
|
|
65
|
+
if (value.schema === "buildchain.pipeline-publication-build-readback/v1")
|
|
66
|
+
leaves.push(value);
|
|
67
|
+
else if (
|
|
68
|
+
value.schema === "buildchain.pipeline-publication-requalification/v1"
|
|
69
|
+
)
|
|
70
|
+
pending.push(value.predecessorBuild);
|
|
71
|
+
else if (
|
|
72
|
+
value.schema ===
|
|
73
|
+
"buildchain.pipeline-publication-build-qualification/v1" &&
|
|
74
|
+
Array.isArray(value.segments) &&
|
|
75
|
+
value.segments.length &&
|
|
76
|
+
value.segments.length <= 100
|
|
77
|
+
)
|
|
78
|
+
pending.push(...value.segments.map(({ build: segment }) => segment));
|
|
79
|
+
else
|
|
80
|
+
throw new Error(
|
|
81
|
+
"Published product build lineage has an unsupported producer",
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return leaves;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function assertBuildInventory(plan, qualified) {
|
|
88
|
+
const expected = [
|
|
89
|
+
...new Set(plan.outputs.map(({ platform }) => platform)),
|
|
90
|
+
].sort();
|
|
91
|
+
const descriptors = qualified.artifacts
|
|
92
|
+
.map(
|
|
93
|
+
({
|
|
94
|
+
file,
|
|
95
|
+
size,
|
|
96
|
+
digest,
|
|
97
|
+
package: pkg,
|
|
98
|
+
manifestRoot,
|
|
99
|
+
providerArtifactId,
|
|
100
|
+
...value
|
|
101
|
+
}) => value,
|
|
102
|
+
)
|
|
103
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
104
|
+
if (recordDigest(descriptors) !== recordDigest(plan.outputs))
|
|
105
|
+
throw new Error(
|
|
106
|
+
"Published product evidence must cover every declared output exactly",
|
|
107
|
+
);
|
|
108
|
+
const leaves = buildLeaves(qualified.build),
|
|
109
|
+
platforms = [],
|
|
110
|
+
jobs = [];
|
|
111
|
+
for (const build of leaves) {
|
|
112
|
+
if (
|
|
113
|
+
build.outcome !== "success" ||
|
|
114
|
+
!Array.isArray(build.platforms) ||
|
|
115
|
+
!Array.isArray(build.jobs) ||
|
|
116
|
+
build.platforms.length !== build.jobs.length ||
|
|
117
|
+
!build.platforms.length ||
|
|
118
|
+
recordDigest(build.source) !== recordDigest(qualified.source)
|
|
119
|
+
)
|
|
120
|
+
throw new Error(
|
|
121
|
+
"Published product evidence lacks complete successful platform builds",
|
|
122
|
+
);
|
|
123
|
+
for (const [index, platform] of build.platforms.entries()) {
|
|
124
|
+
const job = build.jobs[index],
|
|
125
|
+
name = `Build publication (${platform})`;
|
|
126
|
+
if (
|
|
127
|
+
job.status !== "completed" ||
|
|
128
|
+
job.conclusion !== "success" ||
|
|
129
|
+
job.run_id !== build.runId ||
|
|
130
|
+
job.run_attempt !== build.runAttempt ||
|
|
131
|
+
!(job.name === name || job.name.endsWith(` / ${name}`))
|
|
132
|
+
)
|
|
133
|
+
throw new Error(
|
|
134
|
+
"Published product build job does not match its platform or attempt",
|
|
135
|
+
);
|
|
136
|
+
jobs.push(job);
|
|
137
|
+
}
|
|
138
|
+
platforms.push(...build.platforms);
|
|
139
|
+
}
|
|
140
|
+
if (
|
|
141
|
+
recordDigest(platforms.sort()) !== recordDigest(expected) ||
|
|
142
|
+
new Set(jobs.map(({ id }) => id)).size !== jobs.length
|
|
143
|
+
)
|
|
144
|
+
throw new Error(
|
|
145
|
+
"Published product jobs omit or repeat a declared platform",
|
|
146
|
+
);
|
|
147
|
+
return jobs;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function signingRun(plan, qualified, host) {
|
|
151
|
+
const { run, jobs } = await host.runs.read(
|
|
152
|
+
qualified.build.runId,
|
|
153
|
+
qualified.build.runAttempt,
|
|
154
|
+
);
|
|
155
|
+
if (run.head_sha !== qualified.build.providerSource)
|
|
156
|
+
throw new Error("Published product signing run changed its source");
|
|
157
|
+
await readPipelineCaller(
|
|
158
|
+
run,
|
|
159
|
+
qualified.source.configPath,
|
|
160
|
+
host.request,
|
|
161
|
+
host.repository,
|
|
162
|
+
);
|
|
163
|
+
const definitions = (run.referenced_workflows || []).filter((value) =>
|
|
164
|
+
value.path?.startsWith(
|
|
165
|
+
`${plan.publisher.repository}/${plan.publisher.workflow}@`,
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
const selected = jobs.filter(
|
|
169
|
+
(job) =>
|
|
170
|
+
job.name === "Qualify and sign products" ||
|
|
171
|
+
job.name.endsWith(" / Qualify and sign products"),
|
|
172
|
+
);
|
|
173
|
+
if (
|
|
174
|
+
definitions.length !== 1 ||
|
|
175
|
+
definitions[0].sha !== plan.publisher.workflowSha ||
|
|
176
|
+
selected.length !== 1 ||
|
|
177
|
+
selected[0].run_id !== qualified.build.runId ||
|
|
178
|
+
selected[0].run_attempt !== qualified.build.runAttempt ||
|
|
179
|
+
selected[0].status !== "completed" ||
|
|
180
|
+
selected[0].conclusion !== "success"
|
|
181
|
+
)
|
|
182
|
+
throw new Error(
|
|
183
|
+
"Published product signature lacks its exact successful provider job",
|
|
184
|
+
);
|
|
185
|
+
return selected[0];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function completionTime(jobs, qualified, candidate) {
|
|
189
|
+
const values = jobs.map((job) => Date.parse(job.completed_at));
|
|
190
|
+
const issued = Date.parse(qualified.qualification.issuedAt);
|
|
191
|
+
if (
|
|
192
|
+
values.some((value) => !Number.isFinite(value)) ||
|
|
193
|
+
values.slice(0, -1).some((value) => value > issued) ||
|
|
194
|
+
values.at(-1) < issued ||
|
|
195
|
+
Math.max(...values) > Date.parse(candidate.publishedAt)
|
|
196
|
+
)
|
|
197
|
+
throw new Error(
|
|
198
|
+
"Published product job timing disagrees with qualification or release",
|
|
199
|
+
);
|
|
200
|
+
return new Date(Math.max(...values)).toISOString();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function verifyPublishedDocuments(values, bundle, directory, host, execute) {
|
|
204
|
+
const {
|
|
205
|
+
plan,
|
|
206
|
+
qualification: qualified,
|
|
207
|
+
capsules,
|
|
208
|
+
release,
|
|
209
|
+
invocation,
|
|
210
|
+
} = values;
|
|
211
|
+
const materialization = release.versionMaterialization;
|
|
212
|
+
const signing = verifyPipelineSigning({
|
|
213
|
+
plan,
|
|
214
|
+
materialization,
|
|
215
|
+
qualified,
|
|
216
|
+
bundlePath: writeImmutablePublicationFile(
|
|
217
|
+
path.join(directory, "attestation.json"),
|
|
218
|
+
bundle,
|
|
219
|
+
),
|
|
220
|
+
directory: path.join(directory, "signature"),
|
|
221
|
+
token: host.token,
|
|
222
|
+
// Historical evidence is checked at issue time. It never supplies fresh
|
|
223
|
+
// Stable publisher admission or extends the original receipt's lifetime.
|
|
224
|
+
evaluatedAt: qualified.qualification.issuedAt,
|
|
225
|
+
...(execute ? { execute } : {}),
|
|
226
|
+
});
|
|
227
|
+
const expected = pipelineReleaseDocuments({
|
|
228
|
+
plan,
|
|
229
|
+
materialization,
|
|
230
|
+
qualified,
|
|
231
|
+
capsules,
|
|
232
|
+
signing,
|
|
233
|
+
evaluatedAt: qualified.qualification.issuedAt,
|
|
234
|
+
});
|
|
235
|
+
if (
|
|
236
|
+
recordDigest(expected.passport) !== recordDigest(release) ||
|
|
237
|
+
recordDigest(expected.invocation.invocation) !== recordDigest(invocation)
|
|
238
|
+
)
|
|
239
|
+
throw new Error(
|
|
240
|
+
"Published Passport or invocation differs from verified product evidence",
|
|
241
|
+
);
|
|
242
|
+
return signing;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function readPipelineStableProducts(
|
|
246
|
+
stablePlan,
|
|
247
|
+
source,
|
|
248
|
+
host,
|
|
249
|
+
{ execute } = {},
|
|
250
|
+
) {
|
|
251
|
+
verifyPipelinePublicationPlan(stablePlan);
|
|
252
|
+
verifyRootedPublication(source, "buildchain.pipeline-stable-source/v1");
|
|
253
|
+
if (
|
|
254
|
+
host.repository !== stablePlan.source.repository ||
|
|
255
|
+
source.planRoot !== stablePlan.root ||
|
|
256
|
+
source.contractRoot !== stablePlan.contractRoot ||
|
|
257
|
+
source.source.repository !== host.repository ||
|
|
258
|
+
recordDigest(source.source) !== recordDigest(stablePlan.intentSource) ||
|
|
259
|
+
source.candidate.sha !== source.source.commit ||
|
|
260
|
+
source.candidate.tree !== source.source.tree ||
|
|
261
|
+
source.candidate.tag !== `v${stablePlan.candidateVersion}` ||
|
|
262
|
+
!Number.isFinite(Date.parse(source.candidate.publishedAt)) ||
|
|
263
|
+
stablePlan.channel !== "stable"
|
|
264
|
+
)
|
|
265
|
+
throw new Error(
|
|
266
|
+
"Stable product collection changed its admitted source or repository",
|
|
267
|
+
);
|
|
268
|
+
const evidence = await readPipelineReleaseEvidence(host, source.candidate);
|
|
269
|
+
if (evidence.status === "missing") return evidence;
|
|
270
|
+
assertPublishedPlan(stablePlan, source, evidence.values);
|
|
271
|
+
const { plan, qualification: qualified } = evidence.values;
|
|
272
|
+
const jobs = assertBuildInventory(plan, qualified);
|
|
273
|
+
const directory = fs.mkdtempSync(
|
|
274
|
+
path.join(fs.realpathSync(os.tmpdir()), "buildchain-stable-products-"),
|
|
275
|
+
);
|
|
276
|
+
try {
|
|
277
|
+
const signing = verifyPublishedDocuments(
|
|
278
|
+
evidence.values,
|
|
279
|
+
evidence.bundle,
|
|
280
|
+
directory,
|
|
281
|
+
host,
|
|
282
|
+
execute,
|
|
283
|
+
);
|
|
284
|
+
await reobservePublicationBuild(qualified.build, qualified.source, host);
|
|
285
|
+
const signer = await signingRun(plan, qualified, host);
|
|
286
|
+
const completedAt = completionTime(
|
|
287
|
+
[...jobs, signer],
|
|
288
|
+
qualified,
|
|
289
|
+
source.candidate,
|
|
290
|
+
);
|
|
291
|
+
const body = {
|
|
292
|
+
schema: "buildchain.pipeline-stable-products/v1",
|
|
293
|
+
planRoot: stablePlan.root,
|
|
294
|
+
sourceRoot: source.root,
|
|
295
|
+
alphaPlanRoot: plan.root,
|
|
296
|
+
qualificationRoot: qualified.root,
|
|
297
|
+
signing,
|
|
298
|
+
signer,
|
|
299
|
+
buildRoot: qualified.build.root,
|
|
300
|
+
assets: evidence.assets,
|
|
301
|
+
canary: {
|
|
302
|
+
id: "product-build",
|
|
303
|
+
source: "release-candidate",
|
|
304
|
+
status: "success",
|
|
305
|
+
repository: host.repository,
|
|
306
|
+
candidateSha: source.candidate.sha,
|
|
307
|
+
completedAt,
|
|
308
|
+
attestor: "github-actions[bot]",
|
|
309
|
+
runtimeRef: plan.runtime.commit,
|
|
310
|
+
evidenceUrl: `https://github.com/${host.repository}/actions/runs/${qualified.build.runId}/attempts/${qualified.build.runAttempt}`,
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
return { ...body, root: recordDigest(body) };
|
|
314
|
+
} finally {
|
|
315
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
import { readBusinessAttempt } from "../../workflow/attempt/reader.js";
|
|
3
|
+
import { verifyRootedPublication } from "./documents.js";
|
|
4
|
+
import { verifyPipelinePublicationPlan } from "./plan.js";
|
|
5
|
+
import { assertPipelineStableQualification } from "./stable.js";
|
|
6
|
+
|
|
7
|
+
const TIMERS = new Set(["stable.minimum_interval", "stable.canary_soak"]);
|
|
8
|
+
const MAX_WAIT = 15 * 60 * 1000;
|
|
9
|
+
|
|
10
|
+
export function planPipelineStableWait(
|
|
11
|
+
plan,
|
|
12
|
+
eligibility,
|
|
13
|
+
{ attempt, generation, phase },
|
|
14
|
+
) {
|
|
15
|
+
verifyPipelinePublicationPlan(plan);
|
|
16
|
+
verifyRootedPublication(
|
|
17
|
+
eligibility,
|
|
18
|
+
"buildchain.pipeline-stable-eligibility/v1",
|
|
19
|
+
);
|
|
20
|
+
if (
|
|
21
|
+
plan.channel !== "stable" ||
|
|
22
|
+
!plan.stablePolicy ||
|
|
23
|
+
eligibility.planRoot !== plan.root ||
|
|
24
|
+
generation !== plan.generation ||
|
|
25
|
+
!/^attempt-[0-9a-f]{64}$/u.test(attempt || "") ||
|
|
26
|
+
!["publish", "distribution", "next-development"].includes(phase)
|
|
27
|
+
)
|
|
28
|
+
throw new Error("Stable wait changed its admitted publication coordinates");
|
|
29
|
+
const failed = eligibility.report.checks.filter(
|
|
30
|
+
(check) => check.status !== "pass",
|
|
31
|
+
);
|
|
32
|
+
if (
|
|
33
|
+
eligibility.report.ok ||
|
|
34
|
+
!failed.length ||
|
|
35
|
+
failed.some((check) => !TIMERS.has(check.id))
|
|
36
|
+
)
|
|
37
|
+
return null;
|
|
38
|
+
const times = failed.map((check) => {
|
|
39
|
+
const interval = check.id === "stable.minimum_interval";
|
|
40
|
+
const expected = interval
|
|
41
|
+
? plan.stablePolicy.minimum_interval_seconds
|
|
42
|
+
: plan.stablePolicy.minimum_soak_seconds;
|
|
43
|
+
const start = Date.parse(
|
|
44
|
+
interval
|
|
45
|
+
? check.details.previousPublishedAt
|
|
46
|
+
: check.details.soakStartedAt,
|
|
47
|
+
);
|
|
48
|
+
if (
|
|
49
|
+
check.details.requiredSeconds !== expected ||
|
|
50
|
+
!Number.isSafeInteger(expected) ||
|
|
51
|
+
expected < 0
|
|
52
|
+
)
|
|
53
|
+
throw new Error("Stable wait timer differs from the source-bound policy");
|
|
54
|
+
return start + expected * 1000;
|
|
55
|
+
});
|
|
56
|
+
const ready = Math.max(...times);
|
|
57
|
+
const evaluatedAt = Date.parse(eligibility.evaluatedAt);
|
|
58
|
+
if (
|
|
59
|
+
!Number.isFinite(evaluatedAt) ||
|
|
60
|
+
!Number.isFinite(ready) ||
|
|
61
|
+
ready <= evaluatedAt ||
|
|
62
|
+
ready > 8640000000000000
|
|
63
|
+
)
|
|
64
|
+
throw new Error("Stable wait has no bounded future recheck timestamp");
|
|
65
|
+
const body = {
|
|
66
|
+
schema: "buildchain.pipeline-stable-wait/v1",
|
|
67
|
+
attempt,
|
|
68
|
+
generation,
|
|
69
|
+
phase,
|
|
70
|
+
sourceRoot: recordDigest(plan.intentSource),
|
|
71
|
+
planRoot: plan.root,
|
|
72
|
+
eligibilityRoot: eligibility.root,
|
|
73
|
+
notBefore: new Date(ready).toISOString(),
|
|
74
|
+
};
|
|
75
|
+
return { ...body, root: recordDigest(body) };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function preparePipelineStableQualification(
|
|
79
|
+
plan,
|
|
80
|
+
host,
|
|
81
|
+
journal,
|
|
82
|
+
coordinates,
|
|
83
|
+
) {
|
|
84
|
+
try {
|
|
85
|
+
const eligibility = await assertPipelineStableQualification(plan, host);
|
|
86
|
+
if (eligibility) {
|
|
87
|
+
await journal.fence();
|
|
88
|
+
await journal.record("publication/stable-eligibility", eligibility, {
|
|
89
|
+
phase: coordinates.phase,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (!error.eligibility) throw error;
|
|
95
|
+
const wait = planPipelineStableWait(plan, error.eligibility, coordinates);
|
|
96
|
+
if (!wait) throw error;
|
|
97
|
+
await journal.fence();
|
|
98
|
+
const options = { phase: coordinates.phase, state: "waiting" };
|
|
99
|
+
await journal.record(
|
|
100
|
+
"publication/stable-eligibility",
|
|
101
|
+
error.eligibility,
|
|
102
|
+
options,
|
|
103
|
+
);
|
|
104
|
+
await journal.record("publication/stable-wait", wait, options);
|
|
105
|
+
return wait;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function retainedPipelineStableWait(observed, host) {
|
|
110
|
+
const current = observed.history.at(-1);
|
|
111
|
+
if (observed.intent.repository !== host.repository)
|
|
112
|
+
throw new Error("Stable wait crossed the native repository boundary");
|
|
113
|
+
const phase = observed.missing[0];
|
|
114
|
+
if (
|
|
115
|
+
!["publish", "distribution", "next-development"].includes(phase) ||
|
|
116
|
+
current.phases[phase]?.payload.state !== "waiting"
|
|
117
|
+
)
|
|
118
|
+
return null;
|
|
119
|
+
const references = current.phases[phase].payload.materials.filter((item) =>
|
|
120
|
+
item.id.startsWith("publication/stable-wait/"),
|
|
121
|
+
);
|
|
122
|
+
if (references.length !== 1) return null;
|
|
123
|
+
const session = { intent: observed.intent, observed };
|
|
124
|
+
const retained = await host.materialStore(session).read(references[0]);
|
|
125
|
+
verifyRootedPublication(retained, "buildchain.pipeline-stable-wait/v1");
|
|
126
|
+
if (
|
|
127
|
+
observed.attempt !== retained.attempt ||
|
|
128
|
+
observed.generation !== retained.generation ||
|
|
129
|
+
recordDigest(current.generation.source) !== retained.sourceRoot ||
|
|
130
|
+
phase !== retained.phase ||
|
|
131
|
+
!Number.isFinite(Date.parse(retained.notBefore))
|
|
132
|
+
)
|
|
133
|
+
throw new Error(
|
|
134
|
+
"Retained stable wait changed its native publication coordinates",
|
|
135
|
+
);
|
|
136
|
+
return retained;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function activeWait(wait, host) {
|
|
140
|
+
const loaded = await host.index.resolve(wait.attempt);
|
|
141
|
+
const observed = readBusinessAttempt(loaded.snapshot);
|
|
142
|
+
const retained = await retainedPipelineStableWait(observed, host);
|
|
143
|
+
if (!retained) return false;
|
|
144
|
+
return recordDigest(retained) === recordDigest(wait);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// This node runs outside the publication concurrency group. It only waits and
|
|
148
|
+
// wakes the normal entry; every subsequent publication re-collects qualification.
|
|
149
|
+
export async function wakePipelineStableWait(
|
|
150
|
+
wait,
|
|
151
|
+
host,
|
|
152
|
+
{
|
|
153
|
+
now = () => Date.now(),
|
|
154
|
+
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
155
|
+
} = {},
|
|
156
|
+
) {
|
|
157
|
+
verifyRootedPublication(wait, "buildchain.pipeline-stable-wait/v1");
|
|
158
|
+
const target = Date.parse(wait.notBefore),
|
|
159
|
+
started = now();
|
|
160
|
+
if (!Number.isFinite(target) || !Number.isFinite(started))
|
|
161
|
+
throw new Error("Stable wake requires a finite retained timer");
|
|
162
|
+
const deadline = Math.min(target, started + MAX_WAIT);
|
|
163
|
+
while (now() < deadline) {
|
|
164
|
+
if (!(await activeWait(wait, host))) return { status: "obsolete" };
|
|
165
|
+
await sleep(Math.min(30000, deadline - now()));
|
|
166
|
+
}
|
|
167
|
+
if (!(await activeWait(wait, host))) return { status: "obsolete" };
|
|
168
|
+
await host.wake(wait.attempt);
|
|
169
|
+
return { status: "woken", attempt: wait.attempt, notBefore: wait.notBefore };
|
|
170
|
+
}
|