@kungfu-tech/buildchain 4.1.3-alpha.1 → 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 +266 -10
- 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 +362 -7
- 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 +328 -74
- package/architecture/stage-capsule-qualification.json +3 -1
- package/architecture/universal-workflow-bootstrap.json +15 -1
- package/architecture/universal-workflow-capability-policy.json +4 -4
- package/architecture/universal-workflow-fault-campaign.json +3 -1
- package/architecture/workflow-taxonomy.json +77 -328
- package/dist/readers/business-attempt.cjs +809 -0
- package/dist/site/buildchain-contract.json +16 -68
- package/dist/site/buildchain-site.json +40 -25
- 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 +54 -54
- package/dist/site/page-registry.json +33 -18
- 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 +102 -102
- 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 +19 -6
- 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/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,21 @@
|
|
|
1
|
+
// Standard labels verified against GitHub's runner reference on 2026-09-13:
|
|
2
|
+
// https://docs.github.com/en/actions/reference/runners/github-hosted-runners
|
|
3
|
+
const RUNNERS = Object.freeze({
|
|
4
|
+
"linux-x64": "ubuntu-24.04",
|
|
5
|
+
"linux-arm64": "ubuntu-24.04-arm",
|
|
6
|
+
"macos-arm64": "macos-15",
|
|
7
|
+
"macos-x64": "macos-15-intel",
|
|
8
|
+
"windows-x64": "windows-2025",
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export function pipelinePlatforms(plan) {
|
|
12
|
+
const platforms = [
|
|
13
|
+
...new Set(plan.products.flatMap((product) => product.platforms)),
|
|
14
|
+
].sort();
|
|
15
|
+
if (
|
|
16
|
+
!platforms.length ||
|
|
17
|
+
platforms.some((platform) => !Object.hasOwn(RUNNERS, platform))
|
|
18
|
+
)
|
|
19
|
+
throw new Error("Pipeline product platform has no admitted hosted runner");
|
|
20
|
+
return platforms.map((platform) => ({ platform, runner: RUNNERS[platform] }));
|
|
21
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { businessAttempt, sourceGeneration } from "../attempt/identity.js";
|
|
2
|
+
import { attemptRecord } from "../attempt/records.js";
|
|
3
|
+
import { canonicalJson } from "../../release/discussion/envelope.js";
|
|
4
|
+
import { STOP_REQUESTED } from "./fence.js";
|
|
5
|
+
|
|
6
|
+
export function pipelineProgress(journal, { intent, writer, runtime }) {
|
|
7
|
+
async function open(source, baseCommit, { successor = false } = {}) {
|
|
8
|
+
const observed = await journal.read();
|
|
9
|
+
const generation = sourceGeneration(intent, source, baseCommit);
|
|
10
|
+
if (observed.attempt && observed.generation === generation.id && !successor)
|
|
11
|
+
return observed;
|
|
12
|
+
if (
|
|
13
|
+
observed.attempt &&
|
|
14
|
+
!["cancelled", "superseded", "failure", "complete"].includes(
|
|
15
|
+
observed.status,
|
|
16
|
+
)
|
|
17
|
+
)
|
|
18
|
+
throw new Error(
|
|
19
|
+
"Prior attempt must reach terminal state before its successor opens",
|
|
20
|
+
);
|
|
21
|
+
const attempt = businessAttempt({
|
|
22
|
+
intent,
|
|
23
|
+
generation,
|
|
24
|
+
predecessor: observed.attempt || "",
|
|
25
|
+
requestKey: `source:${generation.id}`,
|
|
26
|
+
});
|
|
27
|
+
const root = attemptRecord({
|
|
28
|
+
intent,
|
|
29
|
+
attempt,
|
|
30
|
+
generation,
|
|
31
|
+
writer,
|
|
32
|
+
runtime,
|
|
33
|
+
eventKey: `open:${attempt.id}`,
|
|
34
|
+
phase: "attempt",
|
|
35
|
+
state: "running",
|
|
36
|
+
});
|
|
37
|
+
return journal.append(root, observed.head || "");
|
|
38
|
+
}
|
|
39
|
+
async function progress({
|
|
40
|
+
attempt,
|
|
41
|
+
phase,
|
|
42
|
+
state,
|
|
43
|
+
eventKey,
|
|
44
|
+
reason = "",
|
|
45
|
+
materials = [],
|
|
46
|
+
expectedHead,
|
|
47
|
+
}) {
|
|
48
|
+
const observed = await journal.read();
|
|
49
|
+
if (observed.attempt !== attempt)
|
|
50
|
+
throw new Error("Late pipeline result belongs to a superseded attempt");
|
|
51
|
+
if (expectedHead !== undefined && observed.head !== expectedHead)
|
|
52
|
+
throw new Error(
|
|
53
|
+
"Pipeline scheduling head changed; reobserve the active execution",
|
|
54
|
+
);
|
|
55
|
+
const current = observed.history.at(-1);
|
|
56
|
+
if (
|
|
57
|
+
state === "success" &&
|
|
58
|
+
observed.phases[phase]?.payload.reason.startsWith(STOP_REQUESTED)
|
|
59
|
+
)
|
|
60
|
+
throw new Error("A stopped pipeline phase cannot report success");
|
|
61
|
+
const duplicate = current.events.find(
|
|
62
|
+
(event) => event.payload.eventKey === eventKey,
|
|
63
|
+
);
|
|
64
|
+
if (duplicate) {
|
|
65
|
+
if (
|
|
66
|
+
duplicate.node !== phase ||
|
|
67
|
+
duplicate.payload.state !== state ||
|
|
68
|
+
duplicate.payload.reason !== reason ||
|
|
69
|
+
canonicalJson(duplicate.payload.materials) !== canonicalJson(materials)
|
|
70
|
+
)
|
|
71
|
+
throw new Error("Duplicate pipeline event changed its meaning");
|
|
72
|
+
return observed;
|
|
73
|
+
}
|
|
74
|
+
const record = attemptRecord({
|
|
75
|
+
intent,
|
|
76
|
+
attempt: current.identity,
|
|
77
|
+
generation: current.generation,
|
|
78
|
+
writer,
|
|
79
|
+
runtime,
|
|
80
|
+
previous: current.events.at(-1),
|
|
81
|
+
eventKey,
|
|
82
|
+
phase,
|
|
83
|
+
state,
|
|
84
|
+
reason,
|
|
85
|
+
materials,
|
|
86
|
+
});
|
|
87
|
+
return journal.append(record, observed.head);
|
|
88
|
+
}
|
|
89
|
+
async function requestStop(reason) {
|
|
90
|
+
const observed = await journal.read();
|
|
91
|
+
if (
|
|
92
|
+
!observed.attempt ||
|
|
93
|
+
["complete", "cancelled", "failure", "superseded"].includes(
|
|
94
|
+
observed.status,
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
return observed;
|
|
98
|
+
const phase = observed.missing[0];
|
|
99
|
+
return progress({
|
|
100
|
+
attempt: observed.attempt,
|
|
101
|
+
phase,
|
|
102
|
+
state: "waiting",
|
|
103
|
+
eventKey: `stop:${observed.attempt}:${reason}`,
|
|
104
|
+
reason: `${STOP_REQUESTED}${reason}`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return { open, progress, requestStop };
|
|
108
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { releaseDiscussionStore } from "../../release/discussion/store.js";
|
|
2
|
+
import { discussionTransport } from "../../providers/github/discussions/transport.js";
|
|
3
|
+
|
|
4
|
+
// Copy committed journal records into their Discussion for human navigation.
|
|
5
|
+
// A lagging, unavailable or duplicated projection never supplies write authority.
|
|
6
|
+
export function pipelineProjection(
|
|
7
|
+
graphql,
|
|
8
|
+
transport = discussionTransport(graphql),
|
|
9
|
+
) {
|
|
10
|
+
const store = releaseDiscussionStore(transport);
|
|
11
|
+
return async (session) => {
|
|
12
|
+
const observed = await session.journal.read();
|
|
13
|
+
const target = await store.initialize({ intent: session.intent });
|
|
14
|
+
const prior = await store.read(target);
|
|
15
|
+
const known = new Set(prior.records.map((record) => record.id));
|
|
16
|
+
for (const record of observed.snapshot.records)
|
|
17
|
+
if (!known.has(record.id)) await store.append(target, record);
|
|
18
|
+
return { url: target.discussion.url, journalHead: observed.head };
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
|
|
3
|
+
const TERMINAL = new Set([
|
|
4
|
+
"merged",
|
|
5
|
+
"terminal-failure",
|
|
6
|
+
"dequeued",
|
|
7
|
+
"cancelled",
|
|
8
|
+
]);
|
|
9
|
+
const wait = (reason) => ({ operation: "wait", reason });
|
|
10
|
+
|
|
11
|
+
export function terminalPipelineCandidate(candidate) {
|
|
12
|
+
return Boolean(candidate && TERMINAL.has(candidate.status));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function pipelineCandidateRoot(current) {
|
|
16
|
+
if (
|
|
17
|
+
!current.identity?.id ||
|
|
18
|
+
current.identity.generation !== current.generation.id
|
|
19
|
+
)
|
|
20
|
+
throw new Error(
|
|
21
|
+
"Pipeline candidate requires its exact business attempt identity",
|
|
22
|
+
);
|
|
23
|
+
return recordDigest({
|
|
24
|
+
schema: "buildchain.pipeline-candidate-source/v1",
|
|
25
|
+
intent: current.intent,
|
|
26
|
+
attempt: current.identity,
|
|
27
|
+
generation: current.generation,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function pipelineCandidate(queue, current) {
|
|
32
|
+
const source = current.generation.source;
|
|
33
|
+
const number = current.intent.source.pullRequest;
|
|
34
|
+
const matches = queue.candidates.filter(
|
|
35
|
+
(candidate) =>
|
|
36
|
+
candidate.pullRequestNumber === number &&
|
|
37
|
+
candidate.sourceHead === source.commit &&
|
|
38
|
+
candidate.sourceRoot === pipelineCandidateRoot(current),
|
|
39
|
+
);
|
|
40
|
+
const active = matches.find(
|
|
41
|
+
(candidate) => candidate.candidateId === queue.activeWarrant?.candidateId,
|
|
42
|
+
);
|
|
43
|
+
return active || matches.at(-1) || null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function cancellation({
|
|
47
|
+
candidate,
|
|
48
|
+
queue,
|
|
49
|
+
worker,
|
|
50
|
+
reason,
|
|
51
|
+
observedSourceHead,
|
|
52
|
+
}) {
|
|
53
|
+
if (candidate && ["cancelled", "dequeued"].includes(candidate.status))
|
|
54
|
+
return {
|
|
55
|
+
operation: "record-cancellation",
|
|
56
|
+
reason,
|
|
57
|
+
candidateId: candidate.candidateId,
|
|
58
|
+
};
|
|
59
|
+
if (!candidate || TERMINAL.has(candidate.status))
|
|
60
|
+
return { operation: "supersede", reason };
|
|
61
|
+
if (candidate.candidateId !== queue.activeWarrant?.candidateId) {
|
|
62
|
+
if (candidate.status !== "queued") return wait("unfenced-candidate-state");
|
|
63
|
+
return {
|
|
64
|
+
operation: "cancel-queued",
|
|
65
|
+
reason,
|
|
66
|
+
candidateId: candidate.candidateId,
|
|
67
|
+
expectedSourceHead: candidate.sourceHead,
|
|
68
|
+
observedSourceHead,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// Expiration is not evidence that the old native worker stopped. Never
|
|
72
|
+
// release or transfer an active Warrant until the provider proves terminality.
|
|
73
|
+
if (!worker || worker.status !== "completed")
|
|
74
|
+
return {
|
|
75
|
+
operation: "stop-worker",
|
|
76
|
+
reason: "active-worker-not-terminal",
|
|
77
|
+
candidateId: candidate.candidateId,
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
operation: "settle-cancelled",
|
|
81
|
+
reason,
|
|
82
|
+
candidateId: candidate.candidateId,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function sameSource(current, live) {
|
|
87
|
+
if (!live.source) return false;
|
|
88
|
+
return recordDigest(current.generation.source) === recordDigest(live.source);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function mergedDelivery(candidate, live, evidence) {
|
|
92
|
+
if (!candidate) return wait("merged-candidate-authority-required");
|
|
93
|
+
if (
|
|
94
|
+
!evidence.integration ||
|
|
95
|
+
evidence.integration.sourceHead !== live.source.commit
|
|
96
|
+
)
|
|
97
|
+
return wait("exact-protected-integration-proof-required");
|
|
98
|
+
if (candidate.status !== "merged")
|
|
99
|
+
return { operation: "settle-merged", reason: "protected-merge-observed" };
|
|
100
|
+
if (
|
|
101
|
+
!evidence.settlement ||
|
|
102
|
+
evidence.settlement.candidateId !== candidate.candidateId
|
|
103
|
+
)
|
|
104
|
+
return wait("terminal-settlement-readback-required");
|
|
105
|
+
return { operation: "wake-publication", reason: "delivery-terminal" };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// This pure planner selects the next domain transaction. Its booleans are
|
|
109
|
+
// authenticated provider readbacks at the adapter boundary, never workflow inputs.
|
|
110
|
+
// A selected operation is not a receipt or permission to skip that domain's gates.
|
|
111
|
+
export function reconcilePipeline({ current, live, queue, worker, evidence }) {
|
|
112
|
+
if (
|
|
113
|
+
queue.repository !== current.intent.repository ||
|
|
114
|
+
queue.protectedBase !== current.intent.source.targetBranch ||
|
|
115
|
+
live.pullRequest !== current.intent.source.pullRequest
|
|
116
|
+
)
|
|
117
|
+
throw new Error("Pipeline observation belongs to another intent");
|
|
118
|
+
const candidate = pipelineCandidate(queue, current);
|
|
119
|
+
const cancellationInput = {
|
|
120
|
+
candidate,
|
|
121
|
+
queue,
|
|
122
|
+
worker,
|
|
123
|
+
observedSourceHead: live.observedHead || live.source?.commit,
|
|
124
|
+
};
|
|
125
|
+
if (live.state === "closed" && !live.merged)
|
|
126
|
+
return cancellation({
|
|
127
|
+
...cancellationInput,
|
|
128
|
+
reason: "pull-request-closed",
|
|
129
|
+
});
|
|
130
|
+
if (
|
|
131
|
+
live.targetBranch &&
|
|
132
|
+
live.targetBranch !== current.intent.source.targetBranch
|
|
133
|
+
)
|
|
134
|
+
return cancellation({
|
|
135
|
+
...cancellationInput,
|
|
136
|
+
reason: "target-branch-changed",
|
|
137
|
+
});
|
|
138
|
+
if (live.routeEnabled === false)
|
|
139
|
+
return cancellation({
|
|
140
|
+
...cancellationInput,
|
|
141
|
+
reason: "channel-policy-changed",
|
|
142
|
+
});
|
|
143
|
+
if (!sameSource(current, live))
|
|
144
|
+
return cancellation({
|
|
145
|
+
...cancellationInput,
|
|
146
|
+
reason: "source-generation-changed",
|
|
147
|
+
});
|
|
148
|
+
if (live.merged) return mergedDelivery(candidate, live, evidence);
|
|
149
|
+
if (current.generation.baseCommit !== live.baseCommit)
|
|
150
|
+
return cancellation({
|
|
151
|
+
...cancellationInput,
|
|
152
|
+
reason: "protected-base-advanced",
|
|
153
|
+
});
|
|
154
|
+
if (live.dequeued === true)
|
|
155
|
+
return cancellation({
|
|
156
|
+
...cancellationInput,
|
|
157
|
+
reason: "pull-request-dequeued",
|
|
158
|
+
});
|
|
159
|
+
return openDelivery({ candidate, live, queue, worker, evidence });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function openDelivery({ candidate, live, queue, worker, evidence }) {
|
|
163
|
+
if (live.state !== "open") return wait("unknown-provider-state");
|
|
164
|
+
if (live.terminalOnly) return wait("terminal-event-cannot-start-execution");
|
|
165
|
+
if (candidate && TERMINAL.has(candidate.status))
|
|
166
|
+
return wait("successor-attempt-required");
|
|
167
|
+
if (!evidence.build)
|
|
168
|
+
return { operation: "build", reason: "source-needs-qualification" };
|
|
169
|
+
if (evidence.build.sourceHead !== live.source.commit)
|
|
170
|
+
throw new Error("Build evidence belongs to another source");
|
|
171
|
+
if (evidence.build.outcome !== "success") {
|
|
172
|
+
if (candidate && candidate.candidateId === queue.activeWarrant?.candidateId)
|
|
173
|
+
return worker?.status === "completed"
|
|
174
|
+
? { operation: "settle-failure", reason: "native-execution-failed" }
|
|
175
|
+
: { operation: "stop-worker", reason: "failed-worker-not-terminal" };
|
|
176
|
+
return wait("build-failed");
|
|
177
|
+
}
|
|
178
|
+
if (!live.ready || live.draft) return wait("ready-review-required");
|
|
179
|
+
if (!evidence.review) return wait("independent-protected-review-required");
|
|
180
|
+
if (!evidence.checks) return wait("exact-required-checks-pending");
|
|
181
|
+
if (!candidate) return { operation: "reserve", reason: "source-admitted" };
|
|
182
|
+
if (candidate.candidateId !== queue.activeWarrant?.candidateId)
|
|
183
|
+
return queue.activeWarrant
|
|
184
|
+
? wait("another-candidate-active")
|
|
185
|
+
: { operation: "select", reason: "candidate-queued" };
|
|
186
|
+
if (queue.activeWarrant.phase === "provisional")
|
|
187
|
+
return {
|
|
188
|
+
operation: "qualify-native",
|
|
189
|
+
reason: "independent-native-proof-required",
|
|
190
|
+
};
|
|
191
|
+
if (!evidence.qualification)
|
|
192
|
+
return wait("qualified-warrant-readback-required");
|
|
193
|
+
return { operation: "land", reason: "protected-queue-admission" };
|
|
194
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { pipelineHost } from "./host.js";
|
|
2
|
+
import { recordPipelineBuild } from "./build-control.js";
|
|
3
|
+
import { publishRecorderStatus } from "./recorder-status.js";
|
|
4
|
+
import {
|
|
5
|
+
RECOVERY_BUILD_CONTEXT,
|
|
6
|
+
recordRecoveryBuild,
|
|
7
|
+
} from "./recovery-build-control.js";
|
|
8
|
+
|
|
9
|
+
export async function recordPipelineBuildAction(core, env) {
|
|
10
|
+
const host = await pipelineHost(core, env, "Record product build");
|
|
11
|
+
host.publishBuildStatus = (context, readback) =>
|
|
12
|
+
publishRecorderStatus(context, readback, host);
|
|
13
|
+
const context = JSON.parse(core.getInput("context", { required: true }));
|
|
14
|
+
if (context.schema === "buildchain.pipeline-group-build-context/v1")
|
|
15
|
+
return host.groupRecord(context);
|
|
16
|
+
const result =
|
|
17
|
+
context.schema === RECOVERY_BUILD_CONTEXT
|
|
18
|
+
? await recordRecoveryBuild(context, host)
|
|
19
|
+
: await recordPipelineBuild(context, host);
|
|
20
|
+
if (result.outcome !== "success")
|
|
21
|
+
throw new Error("Product build did not succeed");
|
|
22
|
+
core.info(
|
|
23
|
+
result.wakePending
|
|
24
|
+
? "Build retained; next event will retry delivery wake"
|
|
25
|
+
: "Build retained and attempt woken",
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { parse } from "yaml";
|
|
3
|
+
import { publishPipelineBuildStatus } from "./guard-build.js";
|
|
4
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
5
|
+
|
|
6
|
+
// A repaired runtime may execute through an older immutable published entry.
|
|
7
|
+
// Read the real recorder job's permission, rather than assuming the runtime's
|
|
8
|
+
// current workflow is the one GitHub scheduled. API failures are never ignored.
|
|
9
|
+
export async function publishRecorderStatus(context, readback, host) {
|
|
10
|
+
const { root, ...body } = readback;
|
|
11
|
+
if (
|
|
12
|
+
context.runId !== host.runId ||
|
|
13
|
+
context.runAttempt !== host.runAttempt ||
|
|
14
|
+
readback.runId !== host.runId ||
|
|
15
|
+
readback.runAttempt !== host.runAttempt ||
|
|
16
|
+
recordDigest(context.source) !== recordDigest(readback.source) ||
|
|
17
|
+
root !== recordDigest(body)
|
|
18
|
+
)
|
|
19
|
+
throw new Error("Recorder status changed its current source or execution");
|
|
20
|
+
const { run } = await host.runs.read(host.runId, host.runAttempt);
|
|
21
|
+
const path = ".github/workflows/.ops-pipeline-execute.yml";
|
|
22
|
+
const entries = (run.referenced_workflows || []).filter((entry) =>
|
|
23
|
+
entry.path?.startsWith(`kungfu-systems/buildchain/${path}@`),
|
|
24
|
+
);
|
|
25
|
+
if (entries.length !== 1 || !/^[0-9a-f]{40}$/u.test(entries[0].sha || ""))
|
|
26
|
+
throw new Error("Recorder has no exact published execution workflow");
|
|
27
|
+
const file = await host.request(
|
|
28
|
+
`/repos/kungfu-systems/buildchain/contents/${path}?ref=${entries[0].sha}`,
|
|
29
|
+
);
|
|
30
|
+
if (
|
|
31
|
+
file.type !== "file" ||
|
|
32
|
+
file.encoding !== "base64" ||
|
|
33
|
+
!Number.isSafeInteger(file.size) ||
|
|
34
|
+
file.size < 1 ||
|
|
35
|
+
file.size > 64000 ||
|
|
36
|
+
typeof file.content !== "string" ||
|
|
37
|
+
file.content.length > 128000
|
|
38
|
+
)
|
|
39
|
+
throw new Error(
|
|
40
|
+
"Recorder execution workflow is unavailable or exceeds its bound",
|
|
41
|
+
);
|
|
42
|
+
const bytes = Buffer.from(file.content, "base64");
|
|
43
|
+
const blob = createHash("sha1")
|
|
44
|
+
.update(`blob ${bytes.length}\0`)
|
|
45
|
+
.update(bytes)
|
|
46
|
+
.digest("hex");
|
|
47
|
+
if (bytes.length !== file.size || blob !== file.sha)
|
|
48
|
+
throw new Error("Recorder execution workflow bytes changed");
|
|
49
|
+
const job = parse(bytes.toString("utf8")).jobs?.["record-build"];
|
|
50
|
+
if (job?.name !== "Record product build")
|
|
51
|
+
throw new Error("Recorder execution workflow changed its job identity");
|
|
52
|
+
if (job.permissions?.statuses !== "write") return { published: false };
|
|
53
|
+
if (readback.outcome !== "success") return { published: false };
|
|
54
|
+
await publishPipelineBuildStatus(
|
|
55
|
+
{
|
|
56
|
+
"expected-head-sha": context.source.commit,
|
|
57
|
+
"source-workflow-run-id": String(readback.runId),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
sourceHead: context.source.commit,
|
|
61
|
+
outcome: readback.outcome,
|
|
62
|
+
run: { id: readback.runId, run_attempt: readback.runAttempt },
|
|
63
|
+
readback,
|
|
64
|
+
},
|
|
65
|
+
host.request,
|
|
66
|
+
host.repository,
|
|
67
|
+
);
|
|
68
|
+
return { published: true };
|
|
69
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { recoveryInputs } from "../../consumer/contract/entries.js";
|
|
2
|
+
import { pipelineHost } from "./host.js";
|
|
3
|
+
import { controlPipelineRecovery } from "./recovery-controller.js";
|
|
4
|
+
import { pipelineActionOutputs } from "./action-output.js";
|
|
5
|
+
import { resumePipelineSession } from "./session.js";
|
|
6
|
+
|
|
7
|
+
export async function recoverPipelineAction(core, env) {
|
|
8
|
+
const { attempt } = recoveryInputs({
|
|
9
|
+
attempt: core.getInput("attempt", { required: true }),
|
|
10
|
+
});
|
|
11
|
+
const host = await pipelineHost(core, env, "Buildchain recovery controller");
|
|
12
|
+
try {
|
|
13
|
+
const result = await controlPipelineRecovery(
|
|
14
|
+
attempt,
|
|
15
|
+
core.getInput("definition-sha", { required: true }),
|
|
16
|
+
host,
|
|
17
|
+
);
|
|
18
|
+
pipelineActionOutputs(core, result);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
try {
|
|
21
|
+
const session = await resumePipelineSession({ ...host, attempt }, host);
|
|
22
|
+
await host.project(session);
|
|
23
|
+
} catch {
|
|
24
|
+
core.warning(
|
|
25
|
+
"Recovery status projection is unavailable; the canonical attempt remains authoritative.",
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
import { readPipelineCaller } from "../../providers/github/pipeline-run-entry.js";
|
|
3
|
+
import { RECOVERY_ENTRY } from "../../consumer/contract/entries.js";
|
|
4
|
+
|
|
5
|
+
export async function recoveryExecution(host, definitionSha) {
|
|
6
|
+
const { run } = await host.runs.read(host.runId, host.runAttempt);
|
|
7
|
+
const definitions = (run.referenced_workflows || []).filter((item) =>
|
|
8
|
+
item.path?.startsWith(`kungfu-systems/buildchain/${RECOVERY_ENTRY}@`),
|
|
9
|
+
);
|
|
10
|
+
if (
|
|
11
|
+
run.event !== "workflow_dispatch" ||
|
|
12
|
+
run.status !== "in_progress" ||
|
|
13
|
+
run.repository?.full_name !== host.repository ||
|
|
14
|
+
run.head_repository?.full_name !== host.repository ||
|
|
15
|
+
run.path?.split("@")[0] !== ".github/workflows/buildchain-recover.yml" ||
|
|
16
|
+
definitions.length !== 1 ||
|
|
17
|
+
definitions[0].sha !== definitionSha ||
|
|
18
|
+
!/^[0-9a-f]{40}$/u.test(definitionSha || "")
|
|
19
|
+
)
|
|
20
|
+
throw new Error(
|
|
21
|
+
"Recovery must execute the exact canonical entry from the same consumer repository",
|
|
22
|
+
);
|
|
23
|
+
const channel = definitions[0].path
|
|
24
|
+
.split("@")
|
|
25
|
+
.at(-1)
|
|
26
|
+
.replace(/^refs\/tags\//u, "");
|
|
27
|
+
if (!["v4", "v4-alpha"].includes(channel))
|
|
28
|
+
throw new Error("Recovery entry must be a published floating channel");
|
|
29
|
+
return {
|
|
30
|
+
run,
|
|
31
|
+
channel,
|
|
32
|
+
entry: {
|
|
33
|
+
repository: "kungfu-systems/buildchain",
|
|
34
|
+
workflow: RECOVERY_ENTRY,
|
|
35
|
+
sha: definitionSha,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function admitRecoverySource(session, execution, host) {
|
|
41
|
+
const current = session.observed.history.at(-1);
|
|
42
|
+
const original = current.generation.source;
|
|
43
|
+
const source = await host.source.source(original.commit, original.configPath);
|
|
44
|
+
if (
|
|
45
|
+
recordDigest(source.identity) !== recordDigest(original) ||
|
|
46
|
+
host.selection.source?.sha !== original.commit ||
|
|
47
|
+
host.selection.source?.repository !== host.repository
|
|
48
|
+
)
|
|
49
|
+
throw new Error(
|
|
50
|
+
"Recovery source, config or selected runtime source changed",
|
|
51
|
+
);
|
|
52
|
+
const caller = await readPipelineCaller(
|
|
53
|
+
execution.run,
|
|
54
|
+
original.configPath,
|
|
55
|
+
host.request,
|
|
56
|
+
host.repository,
|
|
57
|
+
);
|
|
58
|
+
const admission = await host.source.observeIntent(
|
|
59
|
+
session.intent,
|
|
60
|
+
current.generation,
|
|
61
|
+
{ "config-path": original.configPath },
|
|
62
|
+
);
|
|
63
|
+
if (
|
|
64
|
+
!admission.live.source ||
|
|
65
|
+
recordDigest(admission.live.source) !== recordDigest(original) ||
|
|
66
|
+
admission.live.targetBranch !== session.intent.source.targetBranch ||
|
|
67
|
+
(!admission.live.merged &&
|
|
68
|
+
(admission.live.state !== "open" ||
|
|
69
|
+
admission.live.routeEnabled === false ||
|
|
70
|
+
admission.live.baseCommit !== current.generation.baseCommit))
|
|
71
|
+
)
|
|
72
|
+
throw new Error(
|
|
73
|
+
"Selected source was closed, superseded or rebased; use its current admitted attempt",
|
|
74
|
+
);
|
|
75
|
+
return {
|
|
76
|
+
source,
|
|
77
|
+
admission,
|
|
78
|
+
caller,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function recoveryPredecessorRuns(session, host) {
|
|
83
|
+
const current = session.observed.history.at(-1);
|
|
84
|
+
const keys = [
|
|
85
|
+
...new Map(
|
|
86
|
+
current.runs.map((writer) => [
|
|
87
|
+
`${writer.runId}:${writer.runAttempt}`,
|
|
88
|
+
writer,
|
|
89
|
+
]),
|
|
90
|
+
).values(),
|
|
91
|
+
];
|
|
92
|
+
if (keys.length > 100)
|
|
93
|
+
throw new Error("Recovery predecessor run inventory exceeds its bound");
|
|
94
|
+
const observations = [];
|
|
95
|
+
for (const writer of keys) {
|
|
96
|
+
if (
|
|
97
|
+
Number(writer.runId) === host.runId &&
|
|
98
|
+
Number(writer.runAttempt) === host.runAttempt
|
|
99
|
+
)
|
|
100
|
+
throw new Error("Recovery cannot replace its own live execution");
|
|
101
|
+
const { run } = await host.runs.read(
|
|
102
|
+
Number(writer.runId),
|
|
103
|
+
Number(writer.runAttempt),
|
|
104
|
+
);
|
|
105
|
+
if (
|
|
106
|
+
run.repository?.full_name !== host.repository ||
|
|
107
|
+
run.head_repository?.full_name !== host.repository
|
|
108
|
+
)
|
|
109
|
+
throw new Error(
|
|
110
|
+
"Recovery predecessor execution crossed a repository or fork boundary",
|
|
111
|
+
);
|
|
112
|
+
observations.push(run);
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
schema: "buildchain.pipeline-recovery-executions/v1",
|
|
116
|
+
predecessor: current.identity.id,
|
|
117
|
+
runs: observations,
|
|
118
|
+
terminal: observations.every(
|
|
119
|
+
(run) => run.status === "completed" && run.conclusion,
|
|
120
|
+
),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { object } from "../../consumer/contract/shape.js";
|
|
2
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
3
|
+
import { resumePipelineSession } from "./session.js";
|
|
4
|
+
import { recoveryBuildEvidence } from "./recovery-build-evidence.js";
|
|
5
|
+
import { qualifyPipelineBuild } from "./build-qualification.js";
|
|
6
|
+
import { retainPipelineBuildResult } from "./build-result.js";
|
|
7
|
+
|
|
8
|
+
export const RECOVERY_BUILD_CONTEXT =
|
|
9
|
+
"buildchain.pipeline-recovery-build-context/v1";
|
|
10
|
+
|
|
11
|
+
export async function beginRecoveryBuild(session, host) {
|
|
12
|
+
session.observed = await session.journal.read();
|
|
13
|
+
const { plan, build } = await recoveryBuildEvidence(session, host);
|
|
14
|
+
const observed = session.observed;
|
|
15
|
+
const previous = observed.phases.build;
|
|
16
|
+
if (
|
|
17
|
+
previous &&
|
|
18
|
+
(previous.payload.writer.runId !== String(host.runId) ||
|
|
19
|
+
previous.payload.writer.runAttempt !== String(host.runAttempt))
|
|
20
|
+
)
|
|
21
|
+
return null;
|
|
22
|
+
await session.progress.progress({
|
|
23
|
+
attempt: observed.attempt,
|
|
24
|
+
phase: "build",
|
|
25
|
+
state: "running",
|
|
26
|
+
eventKey: `build-start:${host.runId}:${host.runAttempt}`,
|
|
27
|
+
expectedHead: observed.head,
|
|
28
|
+
reason: build.reason,
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
schema: RECOVERY_BUILD_CONTEXT,
|
|
32
|
+
attempt: observed.attempt,
|
|
33
|
+
generation: observed.generation,
|
|
34
|
+
source: build.source,
|
|
35
|
+
runId: host.runId,
|
|
36
|
+
runAttempt: host.runAttempt,
|
|
37
|
+
platforms: build.scheduled,
|
|
38
|
+
recoveryPlanRoot: plan.root,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function recordRecoveryBuild(context, host) {
|
|
43
|
+
object(context, [
|
|
44
|
+
"schema",
|
|
45
|
+
"attempt",
|
|
46
|
+
"generation",
|
|
47
|
+
"source",
|
|
48
|
+
"runId",
|
|
49
|
+
"runAttempt",
|
|
50
|
+
"platforms",
|
|
51
|
+
"recoveryPlanRoot",
|
|
52
|
+
]);
|
|
53
|
+
if (
|
|
54
|
+
context.schema !== RECOVERY_BUILD_CONTEXT ||
|
|
55
|
+
context.runId !== host.runId ||
|
|
56
|
+
context.runAttempt !== host.runAttempt
|
|
57
|
+
)
|
|
58
|
+
throw new Error("Recovery build callback belongs to another execution");
|
|
59
|
+
const session = await resumePipelineSession(
|
|
60
|
+
{ ...host, attempt: context.attempt },
|
|
61
|
+
host,
|
|
62
|
+
);
|
|
63
|
+
const { plan, build } = await recoveryBuildEvidence(session, host);
|
|
64
|
+
const writer = session.observed.phases.build?.payload.writer;
|
|
65
|
+
if (
|
|
66
|
+
context.generation !== session.observed.generation ||
|
|
67
|
+
context.recoveryPlanRoot !== plan.root ||
|
|
68
|
+
recordDigest(context.source) !== recordDigest(build.source) ||
|
|
69
|
+
recordDigest(context.platforms) !== recordDigest(build.scheduled) ||
|
|
70
|
+
writer?.runId !== String(host.runId) ||
|
|
71
|
+
writer?.runAttempt !== String(host.runAttempt)
|
|
72
|
+
)
|
|
73
|
+
throw new Error(
|
|
74
|
+
"Recovery build callback changed its reserved work or admission",
|
|
75
|
+
);
|
|
76
|
+
const segments = structuredClone(build.segments);
|
|
77
|
+
if (build.scheduled.length) {
|
|
78
|
+
const platforms = build.scheduled.map((item) => item.platform);
|
|
79
|
+
const readback = await host.runs.build(
|
|
80
|
+
host.runId,
|
|
81
|
+
host.runAttempt,
|
|
82
|
+
build.source,
|
|
83
|
+
platforms,
|
|
84
|
+
);
|
|
85
|
+
segments.push({ readback, platforms, runtime: host.runtime });
|
|
86
|
+
}
|
|
87
|
+
const result = qualifyPipelineBuild({
|
|
88
|
+
source: build.source,
|
|
89
|
+
platforms: build.platforms.map((item) => item.platform),
|
|
90
|
+
segments,
|
|
91
|
+
runId: host.runId,
|
|
92
|
+
runAttempt: host.runAttempt,
|
|
93
|
+
});
|
|
94
|
+
return retainPipelineBuildResult(context, result, session, host);
|
|
95
|
+
}
|