@kungfu-tech/buildchain 4.1.3-alpha.0 → 4.1.3-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/architecture/action-taxonomy.json +16 -0
- package/architecture/agent-change-map.md +245 -4
- package/architecture/ci-lane-change-budget.json +289 -0
- package/architecture/decisions/0005-minimal-consumer-contract.md +122 -0
- package/architecture/decisions/0006-business-attempt-journal.md +89 -0
- package/architecture/decisions/0007-hosted-pipeline-controller.md +106 -0
- package/architecture/decisions/0008-pipeline-product-publication.md +99 -0
- package/architecture/internal-capabilities.json +340 -4
- package/architecture/maintainability-debt.json +3 -1
- package/architecture/maintainability-policy.json +8 -8
- package/architecture/minimal-consumer-migration.json +7558 -0
- package/architecture/release-topology.json +248 -11
- package/architecture/universal-workflow-bootstrap.json +10 -0
- package/architecture/universal-workflow-capability-policy.json +6 -6
- package/architecture/workflow-taxonomy.json +55 -0
- package/contracts/release-discussion-v1.schema.json +3 -0
- package/dist/readers/business-attempt.cjs +805 -0
- package/dist/readers/release-discussion.cjs +4 -0
- package/dist/site/buildchain-contract.json +7 -5
- package/dist/site/buildchain-site.json +25 -15
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/kfd-claims.json +311 -10
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +1 -1
- package/dist/site/node-api-registry.json +9 -9
- package/dist/site/page-registry.json +20 -10
- package/dist/site/public-surface-audit.json +201 -7
- package/dist/site/publication-authority-registry.json +121 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +5 -5
- package/dist/site/workflow-registry.json +416 -7
- package/docs/node-api-reference.md +12 -12
- package/docs/release-discussions.md +44 -5
- package/docs/runtime-entry.md +45 -5
- package/docs/workflow-catalog.md +5 -0
- package/package.json +4 -3
- package/packages/core/consumer/contract/entries.js +85 -0
- package/packages/core/consumer/contract/examples.js +96 -0
- package/packages/core/consumer/contract/identity.js +65 -0
- package/packages/core/consumer/contract/inspection.js +45 -0
- package/packages/core/consumer/contract/plan.js +99 -0
- package/packages/core/consumer/contract/products.js +101 -0
- package/packages/core/consumer/contract/shape.js +46 -0
- package/packages/core/dev-delivery/candidate/admission.js +9 -5
- package/packages/core/dev-delivery/candidate/reservation.js +11 -0
- package/packages/core/dev-delivery/candidate/source-paths.js +12 -4
- package/packages/core/dev-delivery/native/actions.js +7 -1
- package/packages/core/dev-delivery/native/heartbeat-action.js +16 -0
- package/packages/core/dev-delivery/native/heartbeat.js +6 -4
- package/packages/core/dev-delivery/queue/landing-action.js +5 -0
- package/packages/core/governance/buildchain-publication-authority.js +5 -0
- package/packages/core/paper/operations/bootstrap.js +2 -0
- package/packages/core/paper/paper-npm-bootstrap.js +2 -2
- package/packages/core/providers/github/attempt-index.js +60 -0
- package/packages/core/providers/github/attempt-journal.js +154 -0
- package/packages/core/providers/github/discussions/materials.js +23 -7
- package/packages/core/providers/github/discussions/transport.js +16 -5
- package/packages/core/providers/github/pipeline-checkout.js +49 -0
- package/packages/core/providers/github/pipeline-events.js +52 -0
- package/packages/core/providers/github/pipeline-integration.js +95 -0
- package/packages/core/providers/github/pipeline-policy.js +213 -0
- package/packages/core/providers/github/pipeline-product-payload.js +50 -0
- package/packages/core/providers/github/pipeline-product-release.js +191 -0
- package/packages/core/providers/github/pipeline-publication-artifacts.js +153 -0
- package/packages/core/providers/github/pipeline-run-entry.js +86 -0
- package/packages/core/providers/github/pipeline-runs.js +132 -0
- package/packages/core/providers/github/pipeline-source.js +178 -0
- package/packages/core/providers/github/pipeline-version.js +201 -0
- package/packages/core/providers/github/pipeline-worker.js +108 -0
- package/packages/core/publication/candidate/registry-hydration.js +2 -2
- package/packages/core/publication/npm/pack-preview.js +2 -2
- package/packages/core/publication/npm/pack-result.js +31 -0
- package/packages/core/publication/npm/package.js +3 -6
- package/packages/core/publication/npm/pipeline-channel.js +87 -0
- package/packages/core/publication/npm/pipeline-provider.js +109 -0
- package/packages/core/publication/npm/registry.js +12 -9
- package/packages/core/publication/pipeline/actions.js +100 -0
- package/packages/core/publication/pipeline/apply.js +182 -0
- package/packages/core/publication/pipeline/build-segments.js +107 -0
- package/packages/core/publication/pipeline/capsules.js +89 -0
- package/packages/core/publication/pipeline/context.js +39 -0
- package/packages/core/publication/pipeline/development-anchor.js +91 -0
- package/packages/core/publication/pipeline/development-current.js +63 -0
- package/packages/core/publication/pipeline/development-pr.js +85 -0
- package/packages/core/publication/pipeline/development-proof.js +76 -0
- package/packages/core/publication/pipeline/development-transition.js +112 -0
- package/packages/core/publication/pipeline/distribution.js +124 -0
- package/packages/core/publication/pipeline/documents.js +156 -0
- package/packages/core/publication/pipeline/effects.js +162 -0
- package/packages/core/publication/pipeline/files.js +66 -0
- package/packages/core/publication/pipeline/imported-materials.js +32 -0
- package/packages/core/publication/pipeline/journal.js +110 -0
- package/packages/core/publication/pipeline/next-development.js +116 -0
- package/packages/core/publication/pipeline/pack.js +151 -0
- package/packages/core/publication/pipeline/package-policy.js +22 -0
- package/packages/core/publication/pipeline/plan.js +114 -0
- package/packages/core/publication/pipeline/prepare.js +151 -0
- package/packages/core/publication/pipeline/qualification.js +170 -0
- package/packages/core/publication/pipeline/qualify.js +104 -0
- package/packages/core/publication/pipeline/recovery-admission.js +122 -0
- package/packages/core/publication/pipeline/recovery-build-download.js +80 -0
- package/packages/core/publication/pipeline/recovery-build-plan.js +122 -0
- package/packages/core/publication/pipeline/recovery-build-readback.js +68 -0
- package/packages/core/publication/pipeline/recovery-capsules.js +54 -0
- package/packages/core/publication/pipeline/recovery-import.js +80 -0
- package/packages/core/publication/pipeline/recovery-inspection.js +218 -0
- package/packages/core/publication/pipeline/recovery-materials.js +104 -0
- package/packages/core/publication/pipeline/recovery-plan.js +46 -0
- package/packages/core/publication/pipeline/recovery-prepare.js +73 -0
- package/packages/core/publication/pipeline/recovery-qualification.js +128 -0
- package/packages/core/publication/pipeline/recovery-readback.js +56 -0
- package/packages/core/publication/pipeline/recovery-signing.js +58 -0
- package/packages/core/publication/pipeline/sealed-products.js +79 -0
- package/packages/core/publication/pipeline/settle.js +43 -0
- package/packages/core/publication/pipeline/signing.js +140 -0
- package/packages/core/publication/pipeline/source-plan.js +83 -0
- package/packages/core/publication/pipeline/version.js +81 -0
- package/packages/core/publication/publication-reproducibility.js +2 -2
- package/packages/core/release/discussion/binary.js +11 -0
- package/packages/core/release/discussion/checkpoints.js +45 -4
- package/packages/core/release/discussion/envelope.js +1 -0
- package/packages/core/release/discussion/evidence.js +51 -0
- package/packages/core/release/discussion/presentation.js +92 -0
- package/packages/core/release/discussion/publication.js +3 -1
- package/packages/core/release/discussion/qualification.js +1 -0
- package/packages/core/release/discussion/reader.js +9 -0
- package/packages/core/release/discussion/session.js +2 -0
- package/packages/core/release/discussion/store.js +28 -31
- package/packages/core/release/discussion/threads.js +83 -0
- package/packages/core/release/promote-candidate/product-provider-adapters.js +4 -3
- package/packages/core/release/promote-ref/internal/npm-existing-evidence.js +2 -1
- package/packages/core/release/promote-ref/internal/publish-command.js +16 -5
- package/packages/core/runtime/buildchain-domain.wasm +0 -0
- package/packages/core/runtime/domain-wasm-artifact.js +2 -2
- package/packages/core/runtime/entry/actions.js +27 -25
- package/packages/core/runtime/entry/attempt.js +25 -0
- package/packages/core/runtime/entry/selection.js +14 -0
- package/packages/core/runtime/entry/source.js +62 -0
- package/packages/core/workflow/attempt/identity.js +165 -0
- package/packages/core/workflow/attempt/journal.js +87 -0
- package/packages/core/workflow/attempt/materials.js +72 -0
- package/packages/core/workflow/attempt/reader-entry.js +12 -0
- package/packages/core/workflow/attempt/reader.js +149 -0
- package/packages/core/workflow/attempt/records.js +153 -0
- package/packages/core/workflow/attempt/store.js +75 -0
- package/packages/core/workflow/commands/pipeline-build.mjs +23 -0
- package/packages/core/workflow/pipeline/action-output.js +11 -0
- package/packages/core/workflow/pipeline/actions.js +62 -0
- package/packages/core/workflow/pipeline/build-control.js +140 -0
- package/packages/core/workflow/pipeline/build-evidence.js +92 -0
- package/packages/core/workflow/pipeline/build-qualification.js +145 -0
- package/packages/core/workflow/pipeline/build-result.js +69 -0
- package/packages/core/workflow/pipeline/build.js +95 -0
- package/packages/core/workflow/pipeline/cancellation.js +169 -0
- package/packages/core/workflow/pipeline/channel-control.js +127 -0
- package/packages/core/workflow/pipeline/controller.js +133 -0
- package/packages/core/workflow/pipeline/delivery-control.js +204 -0
- package/packages/core/workflow/pipeline/delivery-observation.js +118 -0
- package/packages/core/workflow/pipeline/delivery-request.js +198 -0
- package/packages/core/workflow/pipeline/events.js +84 -0
- package/packages/core/workflow/pipeline/fence.js +42 -0
- package/packages/core/workflow/pipeline/group-control.js +56 -0
- package/packages/core/workflow/pipeline/guard-build.js +37 -0
- package/packages/core/workflow/pipeline/guard.js +156 -0
- package/packages/core/workflow/pipeline/host.js +114 -0
- package/packages/core/workflow/pipeline/materials.js +58 -0
- package/packages/core/workflow/pipeline/notifications.js +36 -0
- package/packages/core/workflow/pipeline/parent-notification.js +27 -0
- package/packages/core/workflow/pipeline/platforms.js +21 -0
- package/packages/core/workflow/pipeline/progress.js +108 -0
- package/packages/core/workflow/pipeline/projection.js +20 -0
- package/packages/core/workflow/pipeline/reconcile.js +194 -0
- package/packages/core/workflow/pipeline/recovery-action.js +30 -0
- package/packages/core/workflow/pipeline/recovery-admission.js +122 -0
- package/packages/core/workflow/pipeline/recovery-build-control.js +95 -0
- package/packages/core/workflow/pipeline/recovery-build-evidence.js +71 -0
- package/packages/core/workflow/pipeline/recovery-build.js +121 -0
- package/packages/core/workflow/pipeline/recovery-controller.js +91 -0
- package/packages/core/workflow/pipeline/recovery-integration.js +66 -0
- package/packages/core/workflow/pipeline/recovery-merge-proof.js +90 -0
- package/packages/core/workflow/pipeline/recovery-ownership-settlement.js +150 -0
- package/packages/core/workflow/pipeline/recovery-ownership.js +91 -0
- package/packages/core/workflow/pipeline/recovery-plan.js +126 -0
- package/packages/core/workflow/pipeline/recovery-runtime.js +68 -0
- package/packages/core/workflow/pipeline/recovery-session.js +129 -0
- package/packages/core/workflow/pipeline/recovery-transition.js +135 -0
- package/packages/core/workflow/pipeline/recovery-unrecorded-build.js +74 -0
- package/packages/core/workflow/pipeline/runtime-source.js +111 -0
- package/packages/core/workflow/pipeline/selection.js +106 -0
- package/packages/core/workflow/pipeline/session.js +72 -0
- package/packages/core/workflow/pipeline/settlement.js +137 -0
- package/packages/core/workflow/pipeline/wake.js +38 -0
- package/packages/core/workflow/pipeline/web-status.js +108 -0
- package/scripts/build-release-discussion-reader.mjs +11 -7
- package/scripts/check-pipeline-publication-topology.mjs +36 -0
- package/scripts/check-release-topology.mjs +6 -1
- package/scripts/generate-minimal-consumer-contract.mjs +155 -0
- package/scripts/verify-golden-path.mjs +5 -1
- package/templates/minimal-consumer/binary/.buildchain/buildchain.toml +45 -0
- package/templates/minimal-consumer/binary/.github/workflows/buildchain-recover.yml +30 -0
- package/templates/minimal-consumer/binary/.github/workflows/buildchain.yml +28 -0
- package/templates/minimal-consumer/binary/package.json +6 -0
- package/templates/minimal-consumer/binary/src/build.mjs +5 -0
- package/templates/minimal-consumer/binary/src/hello.c +2 -0
- package/templates/minimal-consumer/binary/src/verify.mjs +3 -0
- package/templates/minimal-consumer/npm/.buildchain/buildchain.toml +46 -0
- package/templates/minimal-consumer/npm/.github/workflows/buildchain-recover.yml +30 -0
- package/templates/minimal-consumer/npm/.github/workflows/buildchain.yml +28 -0
- package/templates/minimal-consumer/npm/package.json +6 -0
- package/templates/minimal-consumer/npm/src/build.mjs +5 -0
- package/templates/minimal-consumer/npm/src/verify.mjs +3 -0
- package/templates/minimal-consumer/paper/.buildchain/buildchain.toml +45 -0
- package/templates/minimal-consumer/paper/.github/workflows/buildchain-recover.yml +30 -0
- package/templates/minimal-consumer/paper/.github/workflows/buildchain.yml +28 -0
- package/templates/minimal-consumer/paper/package.json +6 -0
- package/templates/minimal-consumer/paper/src/build.mjs +12 -0
- package/templates/minimal-consumer/paper/src/verify.mjs +3 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { readBusinessAttempt } from "../attempt/reader.js";
|
|
2
|
+
import { atomicAttemptJournal } from "../attempt/journal.js";
|
|
3
|
+
import { businessAttempt } from "../attempt/identity.js";
|
|
4
|
+
import { attemptRecord } from "../attempt/records.js";
|
|
5
|
+
import { pipelineProgress } from "./progress.js";
|
|
6
|
+
import { recoveryRequestKey, verifyRecoveryPlan } from "./recovery-plan.js";
|
|
7
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
8
|
+
|
|
9
|
+
export async function selectRecoveryAttempt(attempt, host, entry) {
|
|
10
|
+
const loaded = await host.index.resolve(attempt);
|
|
11
|
+
const journal = atomicAttemptJournal(host.provider, loaded.snapshot.intent);
|
|
12
|
+
const observed = await journal.read();
|
|
13
|
+
if (observed.intent.repository !== host.repository)
|
|
14
|
+
throw new Error("Recovery cannot cross consumer repositories");
|
|
15
|
+
const selected = observed.history.find(
|
|
16
|
+
(item) => item.identity.id === attempt,
|
|
17
|
+
);
|
|
18
|
+
if (!selected)
|
|
19
|
+
throw new Error("Recovery attempt is absent from canonical history");
|
|
20
|
+
const key = recoveryRequestKey(attempt, host.runtime, entry);
|
|
21
|
+
const current = observed.history.at(-1);
|
|
22
|
+
const duplicate = current.identity.id !== attempt;
|
|
23
|
+
if (
|
|
24
|
+
duplicate &&
|
|
25
|
+
(current.identity.predecessor !== attempt ||
|
|
26
|
+
current.identity.requestKey !== key)
|
|
27
|
+
)
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Historical recovery cannot fork current ownership; select ${current.identity.id}`,
|
|
30
|
+
);
|
|
31
|
+
return {
|
|
32
|
+
intent: observed.intent,
|
|
33
|
+
journal,
|
|
34
|
+
observed,
|
|
35
|
+
selected,
|
|
36
|
+
duplicate,
|
|
37
|
+
progress: pipelineProgress(journal, { intent: observed.intent, ...host }),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function openRecoveryAttempt(session, plan, host, evidence) {
|
|
42
|
+
const observed = await session.journal.read();
|
|
43
|
+
verifyRecoveryPlan(plan, observed);
|
|
44
|
+
if (recordDigest(plan.runtime) !== recordDigest(host.runtime))
|
|
45
|
+
throw new Error("Recovery executor differs from the admitted runtime");
|
|
46
|
+
if (evidence === undefined || recordDigest(evidence) !== plan.evidenceRoot)
|
|
47
|
+
throw new Error(
|
|
48
|
+
"Recovery requires the actual qualified evidence bound by its plan",
|
|
49
|
+
);
|
|
50
|
+
const generation = observed.history.at(-1).generation;
|
|
51
|
+
const identity = businessAttempt({
|
|
52
|
+
intent: session.intent,
|
|
53
|
+
generation,
|
|
54
|
+
predecessor: observed.attempt,
|
|
55
|
+
requestKey: plan.requestKey,
|
|
56
|
+
});
|
|
57
|
+
const pending = {
|
|
58
|
+
...session,
|
|
59
|
+
observed: { ...observed, history: [{ identity, generation }] },
|
|
60
|
+
};
|
|
61
|
+
const reference = await host
|
|
62
|
+
.materialStore(pending)
|
|
63
|
+
.retain("recovery/admission", plan);
|
|
64
|
+
const evidenceReference = await host
|
|
65
|
+
.materialStore(pending)
|
|
66
|
+
.retain("recovery/evidence", evidence);
|
|
67
|
+
const root = attemptRecord({
|
|
68
|
+
intent: session.intent,
|
|
69
|
+
attempt: identity,
|
|
70
|
+
generation,
|
|
71
|
+
writer: host.writer,
|
|
72
|
+
runtime: host.runtime,
|
|
73
|
+
eventKey: `open:${identity.id}`,
|
|
74
|
+
phase: "attempt",
|
|
75
|
+
state: "running",
|
|
76
|
+
reason:
|
|
77
|
+
"Explicit recovery; completed effects remain immutable in predecessor history",
|
|
78
|
+
materials: [reference, evidenceReference],
|
|
79
|
+
});
|
|
80
|
+
const opened = await session.journal.append(root, observed.head);
|
|
81
|
+
await host.index.retain(session.intent, identity.id);
|
|
82
|
+
return {
|
|
83
|
+
...session,
|
|
84
|
+
observed: opened,
|
|
85
|
+
duplicate: false,
|
|
86
|
+
progress: pipelineProgress(session.journal, {
|
|
87
|
+
intent: session.intent,
|
|
88
|
+
...host,
|
|
89
|
+
}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function retainedRecoveryPlan(session, host) {
|
|
94
|
+
const current = session.observed.history.at(-1);
|
|
95
|
+
const references = current.events[0].payload.materials.filter(
|
|
96
|
+
(item) => item.id === "recovery/admission",
|
|
97
|
+
);
|
|
98
|
+
if (references.length !== 1)
|
|
99
|
+
throw new Error("Recovery successor has no unique retained admission plan");
|
|
100
|
+
const plan = await host.materialStore(session).read(references[0]);
|
|
101
|
+
const records = session.observed.snapshot.records.filter((record) =>
|
|
102
|
+
session.observed.history
|
|
103
|
+
.slice(0, -1)
|
|
104
|
+
.some((item) => item.identity.id === record.attempt),
|
|
105
|
+
);
|
|
106
|
+
const predecessor = readBusinessAttempt({ intent: session.intent, records });
|
|
107
|
+
verifyRecoveryPlan(plan, predecessor);
|
|
108
|
+
if (
|
|
109
|
+
current.identity.predecessor !== plan.predecessor ||
|
|
110
|
+
current.identity.requestKey !== plan.requestKey
|
|
111
|
+
)
|
|
112
|
+
throw new Error("Recovery successor does not bind its immutable admission");
|
|
113
|
+
return plan;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function retainedRecoveryEvidence(session, host) {
|
|
117
|
+
const plan = await retainedRecoveryPlan(session, host);
|
|
118
|
+
const references = session.observed.history
|
|
119
|
+
.at(-1)
|
|
120
|
+
.events[0].payload.materials.filter(
|
|
121
|
+
(item) => item.id === "recovery/evidence",
|
|
122
|
+
);
|
|
123
|
+
if (references.length !== 1)
|
|
124
|
+
throw new Error("Recovery successor lacks its unique qualified evidence");
|
|
125
|
+
const evidence = await host.materialStore(session).read(references[0]);
|
|
126
|
+
if (recordDigest(evidence) !== plan.evidenceRoot)
|
|
127
|
+
throw new Error("Recovery evidence differs from its immutable admission");
|
|
128
|
+
return { plan, evidence };
|
|
129
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
import { planPipelineRecovery } from "./recovery-plan.js";
|
|
3
|
+
import { settleRecoveryOwnership } from "./recovery-ownership-settlement.js";
|
|
4
|
+
import { qualifyRecoveryIntegration } from "./recovery-integration.js";
|
|
5
|
+
import { retainedRecoveryEvidence } from "./recovery-session.js";
|
|
6
|
+
|
|
7
|
+
export async function terminateRecoveryPredecessor(
|
|
8
|
+
session,
|
|
9
|
+
executions,
|
|
10
|
+
ownership,
|
|
11
|
+
host,
|
|
12
|
+
) {
|
|
13
|
+
const observed = await session.journal.read();
|
|
14
|
+
if (observed.head !== session.observed.head || !executions.terminal)
|
|
15
|
+
throw new Error(
|
|
16
|
+
"Recovery predecessor advanced after terminal execution readback",
|
|
17
|
+
);
|
|
18
|
+
if (["failure", "cancelled", "superseded"].includes(observed.status))
|
|
19
|
+
return session;
|
|
20
|
+
if (observed.status === "complete")
|
|
21
|
+
throw new Error("Completed attempt has no recovery work");
|
|
22
|
+
const evidence = { executions, ownership };
|
|
23
|
+
const material = await host
|
|
24
|
+
.materialStore(session)
|
|
25
|
+
.retain("recovery/predecessor-terminal", evidence, "provider-readback");
|
|
26
|
+
const after = await session.progress.progress({
|
|
27
|
+
attempt: observed.attempt,
|
|
28
|
+
phase: observed.missing[0],
|
|
29
|
+
state: "cancelled",
|
|
30
|
+
eventKey: `recovery-terminal:${recordDigest(evidence)}`,
|
|
31
|
+
reason:
|
|
32
|
+
"Explicit recovery after provider executions became terminal; native ownership remains fenced until successor admission",
|
|
33
|
+
materials: [material],
|
|
34
|
+
expectedHead: observed.head,
|
|
35
|
+
});
|
|
36
|
+
return { ...session, observed: after };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function recoveryNodePlan(session, evidence, entry, host) {
|
|
40
|
+
const cleanup = Boolean(evidence.ownership?.candidate);
|
|
41
|
+
const nodes = session.intent.expectedNodes.map((phase) => {
|
|
42
|
+
if (phase === "admission")
|
|
43
|
+
return {
|
|
44
|
+
phase,
|
|
45
|
+
operation: cleanup ? "reconcile" : "reuse",
|
|
46
|
+
reason:
|
|
47
|
+
"Exact original source and protected route independently reobserved",
|
|
48
|
+
evidenceRoots: [recordDigest(evidence.admitted)],
|
|
49
|
+
};
|
|
50
|
+
if (phase === "build")
|
|
51
|
+
return {
|
|
52
|
+
phase,
|
|
53
|
+
operation: evidence.build.scheduled.length
|
|
54
|
+
? "execute"
|
|
55
|
+
: cleanup
|
|
56
|
+
? "reconcile"
|
|
57
|
+
: "reuse",
|
|
58
|
+
reason: evidence.build.reason,
|
|
59
|
+
evidenceRoots: [evidence.build.root],
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
phase,
|
|
63
|
+
operation: "reconcile",
|
|
64
|
+
reason:
|
|
65
|
+
"Reobserve protected provider authority and execute remaining work",
|
|
66
|
+
evidenceRoots: [],
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
return planPipelineRecovery({
|
|
70
|
+
observed: session.observed,
|
|
71
|
+
runtime: host.runtime,
|
|
72
|
+
entry,
|
|
73
|
+
nodes,
|
|
74
|
+
evidenceRoot: recordDigest(evidence),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function replayRecoveryAdmission(session, host) {
|
|
79
|
+
session.observed = await session.journal.read();
|
|
80
|
+
const { plan, evidence } = await retainedRecoveryEvidence(session, host);
|
|
81
|
+
const value = {
|
|
82
|
+
schema: "buildchain.pipeline-recovery-source/v1",
|
|
83
|
+
recoveryPlanRoot: plan.root,
|
|
84
|
+
predecessor: plan.predecessor,
|
|
85
|
+
source: evidence.admitted.source.identity,
|
|
86
|
+
admission: evidence.admitted.admission,
|
|
87
|
+
caller: evidence.admitted.caller,
|
|
88
|
+
};
|
|
89
|
+
const prior = session.observed.phases.admission;
|
|
90
|
+
if (prior?.payload.state === "success") {
|
|
91
|
+
if (
|
|
92
|
+
prior.payload.state !== "success" ||
|
|
93
|
+
prior.payload.materials.length !== 1 ||
|
|
94
|
+
recordDigest(
|
|
95
|
+
sourceFields(
|
|
96
|
+
await host.materialStore(session).read(prior.payload.materials[0]),
|
|
97
|
+
),
|
|
98
|
+
) !== recordDigest(value)
|
|
99
|
+
)
|
|
100
|
+
throw new Error(
|
|
101
|
+
"Recovery admission replay differs from its immutable source qualification",
|
|
102
|
+
);
|
|
103
|
+
return session;
|
|
104
|
+
}
|
|
105
|
+
const qualified = evidence.admitted.admission.live.merged
|
|
106
|
+
? await qualifyRecoveryIntegration(
|
|
107
|
+
session,
|
|
108
|
+
evidence.admitted.admission,
|
|
109
|
+
host,
|
|
110
|
+
)
|
|
111
|
+
: null;
|
|
112
|
+
const ownership = await settleRecoveryOwnership(session, qualified, host);
|
|
113
|
+
const reference = await host
|
|
114
|
+
.materialStore(session)
|
|
115
|
+
.retain(
|
|
116
|
+
"source/recovery-admission",
|
|
117
|
+
{ ...value, ownership },
|
|
118
|
+
"provider-readback",
|
|
119
|
+
);
|
|
120
|
+
await session.progress.progress({
|
|
121
|
+
attempt: session.observed.attempt,
|
|
122
|
+
phase: "admission",
|
|
123
|
+
state: "success",
|
|
124
|
+
eventKey: `recovery-source:${plan.root}`,
|
|
125
|
+
materials: [reference],
|
|
126
|
+
expectedHead: session.observed.head,
|
|
127
|
+
});
|
|
128
|
+
session.observed = await session.journal.read();
|
|
129
|
+
return session;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function sourceFields(value) {
|
|
133
|
+
const { ownership, ...source } = value;
|
|
134
|
+
return source;
|
|
135
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
import { retainedRecoveryEvidence } from "./recovery-session.js";
|
|
3
|
+
import { buildReadbackPlatforms } from "./build-qualification.js";
|
|
4
|
+
|
|
5
|
+
// Cancellation can prevent the recorder from running after product jobs finish.
|
|
6
|
+
// Recover those exact provider jobs, plus previously admitted segments, without
|
|
7
|
+
// claiming that an unstarted or failed platform succeeded.
|
|
8
|
+
export async function unrecordedRecoveryBuild(
|
|
9
|
+
session,
|
|
10
|
+
source,
|
|
11
|
+
platforms,
|
|
12
|
+
host,
|
|
13
|
+
) {
|
|
14
|
+
const current = session.observed.history.at(-1);
|
|
15
|
+
const phase = current.phases.build;
|
|
16
|
+
if (!phase) return [];
|
|
17
|
+
const segments = [];
|
|
18
|
+
if (current.identity.requestKey.startsWith("recover:")) {
|
|
19
|
+
const { evidence } = await retainedRecoveryEvidence(session, host);
|
|
20
|
+
for (const segment of evidence.build.segments) {
|
|
21
|
+
const old = segment.readback;
|
|
22
|
+
const fresh = await host.runs.build(
|
|
23
|
+
old.runId,
|
|
24
|
+
old.runAttempt,
|
|
25
|
+
source,
|
|
26
|
+
buildReadbackPlatforms(old),
|
|
27
|
+
);
|
|
28
|
+
if (
|
|
29
|
+
recordDigest(old.source) !== recordDigest(source) ||
|
|
30
|
+
recordDigest(old) !== recordDigest(fresh)
|
|
31
|
+
)
|
|
32
|
+
throw new Error(
|
|
33
|
+
"Interrupted recovery lost its previously qualified build segment",
|
|
34
|
+
);
|
|
35
|
+
segments.push(segment);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const writer = phase.payload.writer;
|
|
39
|
+
const { run, jobs } = await host.runs.read(
|
|
40
|
+
Number(writer.runId),
|
|
41
|
+
Number(writer.runAttempt),
|
|
42
|
+
);
|
|
43
|
+
if (
|
|
44
|
+
run.status !== "completed" ||
|
|
45
|
+
run.repository?.full_name !== host.repository ||
|
|
46
|
+
run.head_repository?.full_name !== host.repository
|
|
47
|
+
)
|
|
48
|
+
throw new Error(
|
|
49
|
+
"Unrecorded build requires terminal same-repository provider execution",
|
|
50
|
+
);
|
|
51
|
+
const completed = platforms.filter((platform) => {
|
|
52
|
+
const name = `Build product (${platform})`;
|
|
53
|
+
const matches = jobs.filter(
|
|
54
|
+
(job) => job.name === name || job.name.endsWith(` / ${name}`),
|
|
55
|
+
);
|
|
56
|
+
if (matches.length > 1)
|
|
57
|
+
throw new Error("Interrupted build platform identity is ambiguous");
|
|
58
|
+
return (
|
|
59
|
+
matches[0]?.status === "completed" && matches[0].conclusion === "success"
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
if (completed.length)
|
|
63
|
+
segments.push({
|
|
64
|
+
readback: await host.runs.build(
|
|
65
|
+
Number(writer.runId),
|
|
66
|
+
Number(writer.runAttempt),
|
|
67
|
+
source,
|
|
68
|
+
completed,
|
|
69
|
+
),
|
|
70
|
+
platforms: completed,
|
|
71
|
+
runtime: phase.runtime,
|
|
72
|
+
});
|
|
73
|
+
return segments;
|
|
74
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { githubAttemptJournal } from "../../providers/github/attempt-journal.js";
|
|
2
|
+
import { githubAttemptIndex } from "../../providers/github/attempt-index.js";
|
|
3
|
+
import { githubJsonClient } from "../../providers/github/json-client.js";
|
|
4
|
+
import { readBusinessAttempt } from "../attempt/reader.js";
|
|
5
|
+
|
|
6
|
+
async function retainedAttempt(attempt, repository, token, lookup) {
|
|
7
|
+
const request = githubJsonClient({
|
|
8
|
+
token,
|
|
9
|
+
userAgent: "buildchain-pipeline-runtime-source",
|
|
10
|
+
});
|
|
11
|
+
const index =
|
|
12
|
+
lookup ||
|
|
13
|
+
githubAttemptIndex(request, githubAttemptJournal(request), repository);
|
|
14
|
+
const loaded = await index.resolve(attempt);
|
|
15
|
+
const observed = readBusinessAttempt(loaded.snapshot);
|
|
16
|
+
if (observed.intent.repository !== repository)
|
|
17
|
+
throw new Error(
|
|
18
|
+
"Foreign pipeline selector cannot select the runtime source",
|
|
19
|
+
);
|
|
20
|
+
return observed;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function sourceOf(observed) {
|
|
24
|
+
const current = observed.history.at(-1);
|
|
25
|
+
return {
|
|
26
|
+
repository: observed.intent.repository,
|
|
27
|
+
sha: current.generation.source.commit,
|
|
28
|
+
ref: `refs/pull/${observed.intent.source.pullRequest}/head`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function pipelineRuntimeSource(
|
|
33
|
+
attempt,
|
|
34
|
+
repository,
|
|
35
|
+
token,
|
|
36
|
+
lookup,
|
|
37
|
+
) {
|
|
38
|
+
return (await pipelineRuntimeContinuation(attempt, repository, token, lookup))
|
|
39
|
+
.source;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function pipelineRuntimeContinuation(
|
|
43
|
+
attempt,
|
|
44
|
+
repository,
|
|
45
|
+
token,
|
|
46
|
+
lookup,
|
|
47
|
+
) {
|
|
48
|
+
const observed = await retainedAttempt(attempt, repository, token, lookup);
|
|
49
|
+
if (observed.attempt !== attempt)
|
|
50
|
+
throw new Error(
|
|
51
|
+
"Historical or foreign pipeline selector cannot select the runtime source",
|
|
52
|
+
);
|
|
53
|
+
const current = observed.history.at(-1);
|
|
54
|
+
return {
|
|
55
|
+
source: sourceOf(observed),
|
|
56
|
+
recovery: current.identity.requestKey.startsWith("recover:")
|
|
57
|
+
? {
|
|
58
|
+
attempt,
|
|
59
|
+
recordRoot: current.events[0].id,
|
|
60
|
+
runtime: current.events[0].runtime,
|
|
61
|
+
}
|
|
62
|
+
: null,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Read-only bootstrap can locate a directly recovered successor after a lost
|
|
67
|
+
// response. The credentialed recovery controller independently admits its exact
|
|
68
|
+
// entry/runtime request before any replay or provider effect.
|
|
69
|
+
export async function pipelineRecoveryRuntimeSource(
|
|
70
|
+
attempt,
|
|
71
|
+
repository,
|
|
72
|
+
token,
|
|
73
|
+
lookup,
|
|
74
|
+
) {
|
|
75
|
+
return (
|
|
76
|
+
await pipelineRecoveryContinuation(attempt, repository, token, lookup)
|
|
77
|
+
).source;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function pipelineRecoveryContinuation(
|
|
81
|
+
attempt,
|
|
82
|
+
repository,
|
|
83
|
+
token,
|
|
84
|
+
lookup,
|
|
85
|
+
) {
|
|
86
|
+
const observed = await retainedAttempt(attempt, repository, token, lookup);
|
|
87
|
+
const current = observed.history.at(-1);
|
|
88
|
+
const selected = observed.history.find(
|
|
89
|
+
(item) => item.identity.id === attempt,
|
|
90
|
+
);
|
|
91
|
+
if (
|
|
92
|
+
!selected ||
|
|
93
|
+
(observed.attempt !== attempt &&
|
|
94
|
+
(current.identity.predecessor !== attempt ||
|
|
95
|
+
!current.identity.requestKey.startsWith("recover:") ||
|
|
96
|
+
current.generation.id !== selected.generation.id))
|
|
97
|
+
)
|
|
98
|
+
throw new Error(
|
|
99
|
+
"Historical recovery selector is not the direct current recovery predecessor",
|
|
100
|
+
);
|
|
101
|
+
return {
|
|
102
|
+
source: sourceOf(observed),
|
|
103
|
+
recovery: current.identity.requestKey.startsWith("recover:")
|
|
104
|
+
? {
|
|
105
|
+
attempt: current.identity.id,
|
|
106
|
+
recordRoot: current.events[0].id,
|
|
107
|
+
runtime: current.events[0].runtime,
|
|
108
|
+
}
|
|
109
|
+
: null,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { readBusinessAttempt } from "../attempt/reader.js";
|
|
2
|
+
import { openPipelineSession, resumePipelineSession } from "./session.js";
|
|
3
|
+
import { sourceGeneration } from "../attempt/identity.js";
|
|
4
|
+
|
|
5
|
+
async function successorSelection(
|
|
6
|
+
session,
|
|
7
|
+
admission,
|
|
8
|
+
event,
|
|
9
|
+
payload,
|
|
10
|
+
inputs,
|
|
11
|
+
host,
|
|
12
|
+
) {
|
|
13
|
+
if (
|
|
14
|
+
!["superseded", "cancelled"].includes(session.observed.status) ||
|
|
15
|
+
event.terminalOnly ||
|
|
16
|
+
admission.live.state !== "open" ||
|
|
17
|
+
admission.live.targetBranch !== session.intent.source.targetBranch
|
|
18
|
+
)
|
|
19
|
+
return { session, admission };
|
|
20
|
+
const next = await host.source.observe(
|
|
21
|
+
session.intent.source.pullRequest,
|
|
22
|
+
inputs,
|
|
23
|
+
);
|
|
24
|
+
if (!next.eligible) return { session, admission };
|
|
25
|
+
const changed =
|
|
26
|
+
sourceGeneration(session.intent, next.live.source, next.live.baseCommit)
|
|
27
|
+
.id !== session.observed.generation;
|
|
28
|
+
if (
|
|
29
|
+
!changed &&
|
|
30
|
+
!["reopened", "enqueued", "ready_for_review", "labeled"].includes(
|
|
31
|
+
payload.action,
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
return { session, admission };
|
|
35
|
+
await host.notifyTerminal?.(session);
|
|
36
|
+
return {
|
|
37
|
+
session: await openPipelineSession(
|
|
38
|
+
{ admission: next, ...host, successor: true },
|
|
39
|
+
host,
|
|
40
|
+
),
|
|
41
|
+
admission: next,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Event branch names are lookup hints only. The immutable intent and live PR
|
|
46
|
+
// readback decide whether an old route still needs cleanup before a new route.
|
|
47
|
+
export async function selectPipelineSession(event, payload, inputs, host) {
|
|
48
|
+
if (event.kind === "attempt") {
|
|
49
|
+
const session = await resumePipelineSession(
|
|
50
|
+
{ ...host, attempt: event.attempt },
|
|
51
|
+
host,
|
|
52
|
+
);
|
|
53
|
+
const admission = await host.source.observeIntent(
|
|
54
|
+
session.intent,
|
|
55
|
+
session.observed.history.at(-1).generation,
|
|
56
|
+
inputs,
|
|
57
|
+
);
|
|
58
|
+
return successorSelection(session, admission, event, payload, inputs, host);
|
|
59
|
+
}
|
|
60
|
+
if (event.kind !== "pull-request")
|
|
61
|
+
throw new Error("Not a PR pipeline selection");
|
|
62
|
+
const pr = await host.request(
|
|
63
|
+
`/repos/${host.repository}/pulls/${event.pullRequest}`,
|
|
64
|
+
);
|
|
65
|
+
const branches = [
|
|
66
|
+
...new Set(
|
|
67
|
+
[payload.changes?.base?.ref?.from, pr.base?.ref].filter(Boolean),
|
|
68
|
+
),
|
|
69
|
+
];
|
|
70
|
+
for (const branch of branches) {
|
|
71
|
+
if (!/^(?:dev|alpha|release|publish-gate)\/[A-Za-z0-9/._-]+$/u.test(branch))
|
|
72
|
+
continue;
|
|
73
|
+
const retained = await host.provider.lookup(
|
|
74
|
+
host.repository,
|
|
75
|
+
event.pullRequest,
|
|
76
|
+
branch,
|
|
77
|
+
);
|
|
78
|
+
if (!retained) continue;
|
|
79
|
+
const observed = readBusinessAttempt(retained.snapshot);
|
|
80
|
+
if (!observed.attempt) continue;
|
|
81
|
+
if (
|
|
82
|
+
branch !== pr.base.ref &&
|
|
83
|
+
["cancelled", "failure", "superseded", "complete"].includes(
|
|
84
|
+
observed.status,
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
continue;
|
|
88
|
+
const session = await resumePipelineSession(
|
|
89
|
+
{ ...host, attempt: observed.attempt },
|
|
90
|
+
host,
|
|
91
|
+
);
|
|
92
|
+
const admission = await host.source.observeIntent(
|
|
93
|
+
session.intent,
|
|
94
|
+
observed.history.at(-1).generation,
|
|
95
|
+
inputs,
|
|
96
|
+
);
|
|
97
|
+
return successorSelection(session, admission, event, payload, inputs, host);
|
|
98
|
+
}
|
|
99
|
+
if (event.terminalOnly || pr.state !== "open") return null;
|
|
100
|
+
const admission = await host.source.observe(event.pullRequest, inputs);
|
|
101
|
+
if (!admission.eligible) return null;
|
|
102
|
+
return {
|
|
103
|
+
session: await openPipelineSession({ admission, ...host }, host),
|
|
104
|
+
admission,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { pipelineIntent } from "../attempt/identity.js";
|
|
2
|
+
import { atomicAttemptJournal } from "../attempt/journal.js";
|
|
3
|
+
import { pipelineProgress } from "./progress.js";
|
|
4
|
+
|
|
5
|
+
export function pipelinePhases(operation) {
|
|
6
|
+
if (operation === "develop")
|
|
7
|
+
return ["admission", "build", "review", "warrant", "merge"];
|
|
8
|
+
if (["alpha", "stable", "major"].includes(operation))
|
|
9
|
+
return [
|
|
10
|
+
"admission",
|
|
11
|
+
"build",
|
|
12
|
+
"review",
|
|
13
|
+
"merge",
|
|
14
|
+
"publish",
|
|
15
|
+
"distribution",
|
|
16
|
+
"next-development",
|
|
17
|
+
];
|
|
18
|
+
throw new Error("Unsupported pipeline channel operation");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function openPipelineSession(
|
|
22
|
+
{ admission, runtime, writer, successor = false },
|
|
23
|
+
{ provider, index },
|
|
24
|
+
) {
|
|
25
|
+
if (!admission.eligible || !admission.route || !admission.live.source)
|
|
26
|
+
throw new Error(
|
|
27
|
+
"A new pipeline attempt requires an admitted source and channel",
|
|
28
|
+
);
|
|
29
|
+
const { live } = admission;
|
|
30
|
+
const retained = await provider.lookup(
|
|
31
|
+
live.source.repository,
|
|
32
|
+
live.pullRequest,
|
|
33
|
+
admission.route.to,
|
|
34
|
+
);
|
|
35
|
+
const intent =
|
|
36
|
+
retained?.snapshot.intent ||
|
|
37
|
+
pipelineIntent({
|
|
38
|
+
repository: live.source.repository,
|
|
39
|
+
repositoryId: admission.repositoryId,
|
|
40
|
+
pullRequest: live.pullRequest,
|
|
41
|
+
targetBranch: admission.route.to,
|
|
42
|
+
phases: pipelinePhases(admission.route.operation),
|
|
43
|
+
runtime,
|
|
44
|
+
});
|
|
45
|
+
const journal = atomicAttemptJournal(provider, intent);
|
|
46
|
+
const progress = pipelineProgress(journal, { intent, writer, runtime });
|
|
47
|
+
const observed = await progress.open(live.source, live.baseCommit, {
|
|
48
|
+
successor,
|
|
49
|
+
});
|
|
50
|
+
await index.retain(intent, observed.attempt);
|
|
51
|
+
return { intent, journal, progress, observed };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function resumePipelineSession(
|
|
55
|
+
{ attempt, runtime, writer },
|
|
56
|
+
{ provider, index },
|
|
57
|
+
) {
|
|
58
|
+
const retained = await index.resolve(attempt);
|
|
59
|
+
const intent = retained.snapshot.intent;
|
|
60
|
+
const journal = atomicAttemptJournal(provider, intent);
|
|
61
|
+
const observed = await journal.read();
|
|
62
|
+
if (observed.attempt !== attempt)
|
|
63
|
+
throw new Error(
|
|
64
|
+
"Selected attempt is historical; it cannot regain current write authority",
|
|
65
|
+
);
|
|
66
|
+
return {
|
|
67
|
+
intent,
|
|
68
|
+
journal,
|
|
69
|
+
observed,
|
|
70
|
+
progress: pipelineProgress(journal, { intent, writer, runtime }),
|
|
71
|
+
};
|
|
72
|
+
}
|