@kungfu-tech/buildchain 4.1.3-alpha.1 → 4.1.3-alpha.3
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 +24 -0
- package/architecture/agent-change-map.md +256 -2
- package/architecture/ci-lane-change-budget.json +374 -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/decisions/0009-pipeline-version-preparation.md +186 -0
- package/architecture/internal-capabilities.json +361 -2
- package/architecture/maintainability-debt.json +6 -4
- package/architecture/maintainability-policy.json +8 -8
- package/architecture/minimal-consumer-migration.json +7567 -0
- package/architecture/product-upstreams.json +20 -0
- package/architecture/release-topology.json +282 -11
- package/architecture/universal-workflow-bootstrap.json +12 -0
- package/architecture/universal-workflow-capability-policy.json +1 -1
- package/architecture/workflow-taxonomy.json +66 -0
- package/dist/readers/business-attempt.cjs +809 -0
- package/dist/site/buildchain-contract.json +7 -5
- package/dist/site/buildchain-site.json +18 -13
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/kfd-claims.json +385 -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 +21 -21
- package/dist/site/page-registry.json +13 -8
- package/dist/site/public-surface-audit.json +258 -7
- package/dist/site/publication-authority-registry.json +141 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +5 -5
- package/dist/site/workflow-registry.json +522 -7
- package/docs/node-api-reference.md +39 -39
- package/docs/runtime-entry.md +45 -5
- package/docs/workflow-catalog.md +6 -0
- package/package.json +4 -3
- 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 +146 -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/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 +6 -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 +7 -2
- package/packages/core/providers/github/pipeline-checkout.js +49 -0
- package/packages/core/providers/github/pipeline-entry-runs.js +203 -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-release-evidence.js +111 -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-stable-source.js +354 -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/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 +116 -0
- package/packages/core/publication/pipeline/apply.js +188 -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 +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 +124 -0
- package/packages/core/publication/pipeline/documents.js +192 -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 +117 -0
- package/packages/core/publication/pipeline/pack.js +157 -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 +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/stable-entry.js +215 -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/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 +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 +75 -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 +142 -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 +48 -0
- package/scripts/check-release-topology.mjs +7 -1
- package/scripts/generate-minimal-consumer-contract.mjs +155 -0
- package/scripts/generate-site-bundle.mjs +1 -1
- package/scripts/verify-golden-path.mjs +5 -1
- 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,72 @@
|
|
|
1
|
+
import { resumePipelineSession } from "../../workflow/pipeline/session.js";
|
|
2
|
+
import { pipelinePublicationJournal } from "./journal.js";
|
|
3
|
+
import { retainedPipelineStableWait } from "./stable-wait.js";
|
|
4
|
+
|
|
5
|
+
export async function claimPipelinePublicationWorker(
|
|
6
|
+
attempt,
|
|
7
|
+
publisherSha,
|
|
8
|
+
host,
|
|
9
|
+
) {
|
|
10
|
+
const { run: execution } = await host.runs.read(host.runId, host.runAttempt);
|
|
11
|
+
const definitions = (execution.referenced_workflows || []).filter((entry) =>
|
|
12
|
+
entry.path?.startsWith(
|
|
13
|
+
"kungfu-systems/buildchain/.github/workflows/.release-pipeline-products.yml@",
|
|
14
|
+
),
|
|
15
|
+
);
|
|
16
|
+
if (
|
|
17
|
+
!/^[0-9a-f]{40}$/u.test(publisherSha || "") ||
|
|
18
|
+
definitions.length !== 1 ||
|
|
19
|
+
definitions[0].sha !== publisherSha
|
|
20
|
+
)
|
|
21
|
+
throw new Error(
|
|
22
|
+
"Publisher definition is not the exact hosted reusable workflow",
|
|
23
|
+
);
|
|
24
|
+
const session = await resumePipelineSession({ ...host, attempt }, host);
|
|
25
|
+
const observed = session.observed;
|
|
26
|
+
if (observed.phases.merge?.payload.state !== "success")
|
|
27
|
+
throw new Error(
|
|
28
|
+
"Product publication requires completed protected integration",
|
|
29
|
+
);
|
|
30
|
+
if (observed.status === "complete") return null;
|
|
31
|
+
const phase = observed.missing[0];
|
|
32
|
+
if (!["publish", "distribution", "next-development"].includes(phase))
|
|
33
|
+
throw new Error("Attempt is not at product publication");
|
|
34
|
+
const prior = observed.history
|
|
35
|
+
.at(-1)
|
|
36
|
+
.events.filter((event) =>
|
|
37
|
+
event.payload.materials.some((material) =>
|
|
38
|
+
material.id.startsWith("publication/worker/"),
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
.at(-1)?.payload.writer;
|
|
42
|
+
if (
|
|
43
|
+
prior &&
|
|
44
|
+
(prior.runId !== String(host.runId) ||
|
|
45
|
+
prior.runAttempt !== String(host.runAttempt))
|
|
46
|
+
) {
|
|
47
|
+
const { run } = await host.runs.read(
|
|
48
|
+
Number(prior.runId),
|
|
49
|
+
Number(prior.runAttempt),
|
|
50
|
+
);
|
|
51
|
+
// A time-only wait releases publication ownership before its outer run ends.
|
|
52
|
+
// The next worker still claims the exact native head under the publication lock.
|
|
53
|
+
if (
|
|
54
|
+
run.status !== "completed" &&
|
|
55
|
+
!(await retainedPipelineStableWait(observed, host))
|
|
56
|
+
)
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const journal = pipelinePublicationJournal(session, host);
|
|
60
|
+
const claim = {
|
|
61
|
+
schema: "buildchain.pipeline-publication-worker/v1",
|
|
62
|
+
attempt,
|
|
63
|
+
runId: host.runId,
|
|
64
|
+
runAttempt: host.runAttempt,
|
|
65
|
+
};
|
|
66
|
+
await journal.record("publication/worker", claim, {
|
|
67
|
+
phase,
|
|
68
|
+
expectedHead: observed.head,
|
|
69
|
+
});
|
|
70
|
+
await journal.fence();
|
|
71
|
+
return { session, journal, phase };
|
|
72
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readNpmPackResult } from "./npm/pack-result.js";
|
|
1
2
|
import crypto from "node:crypto";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import os from "node:os";
|
|
@@ -564,8 +565,7 @@ function packPublication({ buildRoot, packageDir, source, toolchain }) {
|
|
|
564
565
|
label: "npm pack reproducibility candidate",
|
|
565
566
|
},
|
|
566
567
|
);
|
|
567
|
-
const
|
|
568
|
-
const result = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
568
|
+
const result = readNpmPackResult(output);
|
|
569
569
|
if (!result?.filename || !result?.integrity || !result?.shasum) {
|
|
570
570
|
throw new Error(
|
|
571
571
|
"npm pack reproducibility candidate did not report filename, integrity, and shasum",
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { parseNpmView } from "../../publication/npm/registry.js";
|
|
2
|
+
import { readNpmPackResult } from "../../publication/npm/pack-result.js";
|
|
1
3
|
import { createOciPublicationAdapter } from "./oci-provider.js";
|
|
2
4
|
import crypto from "node:crypto";
|
|
3
5
|
import fs from "node:fs";
|
|
@@ -306,8 +308,7 @@ function createPackedPackage(context) {
|
|
|
306
308
|
{ cwd: context.cwd, encoding: "utf8" },
|
|
307
309
|
"npm pack",
|
|
308
310
|
);
|
|
309
|
-
const
|
|
310
|
-
const pack = Array.isArray(payload) ? payload[0] : payload;
|
|
311
|
+
const pack = readNpmPackResult(String(result.stdout || "[]"));
|
|
311
312
|
if (
|
|
312
313
|
pack?.name !== context.intent.packageName ||
|
|
313
314
|
pack?.version !== context.intent.version ||
|
|
@@ -363,7 +364,7 @@ async function npmReadback(context, packedPackage, effect) {
|
|
|
363
364
|
"npm-readback-failed",
|
|
364
365
|
);
|
|
365
366
|
}
|
|
366
|
-
const integrity =
|
|
367
|
+
const { integrity } = parseNpmView(result.stdout);
|
|
367
368
|
if (integrity !== expected.integrity) return conflict("npm-integrity-conflict");
|
|
368
369
|
return observed(effect, {
|
|
369
370
|
kind: "npm-package",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseNpmView } from "../../../publication/npm/registry.js";
|
|
1
2
|
import { npmPackageSpec } from "./publish-contract.js";
|
|
2
3
|
import { execNpmSync } from "./npm-distribution.js";
|
|
3
4
|
import fs from "node:fs";
|
|
@@ -14,7 +15,7 @@ export function readExistingNpmIntegrity({ cwd, artifact }) {
|
|
|
14
15
|
if (!output) {
|
|
15
16
|
throw new Error("empty dist.integrity");
|
|
16
17
|
}
|
|
17
|
-
return
|
|
18
|
+
return parseNpmView(output).integrity;
|
|
18
19
|
} catch (error) {
|
|
19
20
|
const message = error.stderr?.toString?.().trim() || error.message;
|
|
20
21
|
throw new Error(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readNpmPackResult } from "../../../publication/npm/pack-result.js";
|
|
1
2
|
import { npmPublishTransaction } from "../../../publication/npm/transaction.js";
|
|
2
3
|
import { npmPublicationEnvironment } from "../../../publication/npm/environment.js";
|
|
3
4
|
import {
|
|
@@ -15,10 +16,21 @@ import {
|
|
|
15
16
|
updateVersionStateContents,
|
|
16
17
|
} from "../../version-state.js";
|
|
17
18
|
import { publishTransactionEnvironment } from "./transaction-context.js";
|
|
18
|
-
export function runPublishCommand({
|
|
19
|
+
export function runPublishCommand({
|
|
20
|
+
cwd,
|
|
21
|
+
command,
|
|
22
|
+
provider,
|
|
23
|
+
loadedConfig,
|
|
24
|
+
env,
|
|
25
|
+
}) {
|
|
19
26
|
if (provider) {
|
|
20
|
-
if (provider.kind !== "npm" || !provider.directory || command)
|
|
21
|
-
|
|
27
|
+
if (provider.kind !== "npm" || !provider.directory || command)
|
|
28
|
+
throw new Error("Invalid or ambiguous declarative publication provider");
|
|
29
|
+
npmPublishTransaction({
|
|
30
|
+
cwd: path.resolve(cwd, provider.directory),
|
|
31
|
+
publication: npmPublicationEnvironment(env),
|
|
32
|
+
env: { ...process.env, ...env },
|
|
33
|
+
});
|
|
22
34
|
return "provider:npm";
|
|
23
35
|
}
|
|
24
36
|
const lifecyclePublish = getLifecycleStage(loadedConfig, "publish");
|
|
@@ -64,7 +76,7 @@ export function rematerializedNpmPackEnvironment({
|
|
|
64
76
|
path.join(os.tmpdir(), "buildchain-rematerialized-npm-"),
|
|
65
77
|
);
|
|
66
78
|
try {
|
|
67
|
-
const
|
|
79
|
+
const result = readNpmPackResult(
|
|
68
80
|
execNpmSync(
|
|
69
81
|
[
|
|
70
82
|
"pack",
|
|
@@ -82,7 +94,6 @@ export function rematerializedNpmPackEnvironment({
|
|
|
82
94
|
},
|
|
83
95
|
),
|
|
84
96
|
);
|
|
85
|
-
const result = Array.isArray(packed) ? packed[0] : packed;
|
|
86
97
|
if (!result?.filename)
|
|
87
98
|
throw new Error("rematerialized npm pack did not return a filename");
|
|
88
99
|
if (result.name !== pkg.name || result.version !== version) {
|
|
@@ -184,8 +184,19 @@ export function evaluateStableReleaseGate({
|
|
|
184
184
|
"version-bound impact declares a summary and at least one surface",
|
|
185
185
|
{ summary: string(impact?.summary), surfaceIds: surfaceImpacts.map((entry) => string(entry?.id)).filter(Boolean) },
|
|
186
186
|
));
|
|
187
|
+
checks.push(check(
|
|
188
|
+
string(impact?.release?.version) === candidateTag.replace(/^v/, ""),
|
|
189
|
+
"stable.impact_version",
|
|
190
|
+
"product impact belongs to the exact candidate version",
|
|
191
|
+
{ expectedVersion: candidateTag.replace(/^v/, ""), observedVersion: string(impact?.release?.version) },
|
|
192
|
+
));
|
|
187
193
|
|
|
188
194
|
const canaryById = new Map(canaries.map((canary) => [string(canary?.id), canary]));
|
|
195
|
+
checks.push(check(
|
|
196
|
+
canaryById.size === canaries.length,
|
|
197
|
+
"canary.evidence_inventory",
|
|
198
|
+
"each canary has one unambiguous observation",
|
|
199
|
+
));
|
|
189
200
|
const canaryCompletionTimes = [];
|
|
190
201
|
for (const required of policy.requiredCanaries) {
|
|
191
202
|
const evidence = canaryById.get(required.id);
|
|
Binary file
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
export const DOMAIN_WASM_ABI_VERSION = 1;
|
|
3
3
|
export const DOMAIN_WASM_RUSTC = "rustc 1.96.0 (ac68faa20 2026-05-25)";
|
|
4
4
|
export const DOMAIN_WASM_SOURCE_SHA256 =
|
|
5
|
-
"
|
|
5
|
+
"9275a02bbc3daac66cac3021384bcaaf43bd52ed9af56b4f1ac67f1cbc69fb71";
|
|
6
6
|
export const DOMAIN_WASM_SHA256 =
|
|
7
|
-
"
|
|
7
|
+
"d42ab5ae8d42253d40ab49057e460285a848925cece176c4d68e63e90ca86c72";
|
|
@@ -1,45 +1,38 @@
|
|
|
1
1
|
import { resolveRecoverySource } from "./recovery.js";
|
|
2
2
|
import { getOctokit } from "@actions/github";
|
|
3
3
|
import { runtimeEntryProvider } from "./github.js";
|
|
4
|
+
import { resolveRuntimeActionSource } from "./source.js";
|
|
4
5
|
import {
|
|
5
6
|
preparedRuntimeSelection,
|
|
6
7
|
selectExecutionRuntime,
|
|
7
8
|
} from "./selection.js";
|
|
9
|
+
import { selectAttemptRecoveryRuntime } from "./attempt.js";
|
|
8
10
|
|
|
9
11
|
export async function selectExecutionRuntimeAction(
|
|
10
12
|
core,
|
|
11
13
|
env,
|
|
12
|
-
{
|
|
14
|
+
{
|
|
15
|
+
providerFactory = runtimeEntryProvider,
|
|
16
|
+
githubFactory = getOctokit,
|
|
17
|
+
pipelineLookup,
|
|
18
|
+
} = {},
|
|
13
19
|
) {
|
|
14
20
|
const retained = core.getInput("selection");
|
|
15
21
|
const transport = retained
|
|
16
22
|
? preparedRuntimeSelection(JSON.parse(retained))
|
|
17
23
|
: undefined;
|
|
18
24
|
const github = githubFactory(core.getInput("token", { required: true }));
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
eventName: env.GITHUB_EVENT_NAME,
|
|
31
|
-
});
|
|
32
|
-
await reader.authorize({ origin: "runtime-parameter" });
|
|
33
|
-
source = await resolveRecoverySource(
|
|
34
|
-
{
|
|
35
|
-
repository: source.repository,
|
|
36
|
-
runId: resumeRunId,
|
|
37
|
-
currentRunId: env.GITHUB_RUN_ID,
|
|
38
|
-
workflow: env.GITHUB_WORKFLOW_REF?.slice(source.repository.length + 1),
|
|
39
|
-
},
|
|
40
|
-
reader.readRun,
|
|
41
|
-
);
|
|
42
|
-
}
|
|
25
|
+
const {
|
|
26
|
+
source: resolvedSource,
|
|
27
|
+
continuation,
|
|
28
|
+
resumeRunId,
|
|
29
|
+
pipelineAttempt,
|
|
30
|
+
} = await resolveRuntimeActionSource(core, env, {
|
|
31
|
+
providerFactory,
|
|
32
|
+
github,
|
|
33
|
+
pipelineLookup,
|
|
34
|
+
});
|
|
35
|
+
let source = resolvedSource;
|
|
43
36
|
const provider = providerFactory(github, {
|
|
44
37
|
sourceRepository: source.repository,
|
|
45
38
|
sourceSha: source.sha,
|
|
@@ -91,6 +84,15 @@ export async function selectExecutionRuntimeAction(
|
|
|
91
84
|
selection.source?.sha !== source.sha
|
|
92
85
|
)
|
|
93
86
|
throw new Error("Runtime transport changed the admitted consumer source");
|
|
87
|
+
} else if (continuation?.recovery && (!runtimeRef || pipelineAttempt)) {
|
|
88
|
+
if (runtimeRef)
|
|
89
|
+
throw new Error(
|
|
90
|
+
"An admitted recovery runtime can change only through a new recovery attempt",
|
|
91
|
+
);
|
|
92
|
+
selection = await selectAttemptRecoveryRuntime(
|
|
93
|
+
continuation.recovery,
|
|
94
|
+
provider,
|
|
95
|
+
);
|
|
94
96
|
} else {
|
|
95
97
|
selection = await selectExecutionRuntime(
|
|
96
98
|
{
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { validateRuntime } from "../../release/discussion/envelope.js";
|
|
2
|
+
import { selectExecutionRuntime } from "./selection.js";
|
|
3
|
+
|
|
4
|
+
// This input is read from the canonical current attempt, never from workflow
|
|
5
|
+
// inputs. Continuation retains its admitted repair without reopening an override.
|
|
6
|
+
export async function selectAttemptRecoveryRuntime(recovery, provider) {
|
|
7
|
+
validateRuntime(recovery.runtime);
|
|
8
|
+
if (
|
|
9
|
+
!/^attempt-[0-9a-f]{64}$/u.test(recovery.attempt || "") ||
|
|
10
|
+
!/^sha256:[0-9a-f]{64}$/u.test(recovery.recordRoot || "") ||
|
|
11
|
+
recovery.runtime.repository !== "kungfu-systems/buildchain"
|
|
12
|
+
)
|
|
13
|
+
throw new Error(
|
|
14
|
+
"Attempt runtime continuation requires its canonical recovery admission",
|
|
15
|
+
);
|
|
16
|
+
const selected = await selectExecutionRuntime(
|
|
17
|
+
{ runtimeRef: recovery.runtime.sha },
|
|
18
|
+
{
|
|
19
|
+
...provider,
|
|
20
|
+
authorize: (input) =>
|
|
21
|
+
provider.authorize({ ...input, origin: "attempt-recovery" }),
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
return { ...selected, origin: "attempt-recovery", recovery };
|
|
25
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { consumerContractLock } from "../../consumer/contract/identity.js";
|
|
2
|
+
import { canonicalJson } from "../../release/discussion/envelope.js";
|
|
3
|
+
|
|
1
4
|
const exactSha = /^[a-f0-9]{40}$/iu;
|
|
2
5
|
const repository = "kungfu-systems/buildchain";
|
|
3
6
|
const trainRef =
|
|
@@ -28,6 +31,14 @@ export function runtimeSelector({ runtimeRef = "", lock, workflowSha }) {
|
|
|
28
31
|
return { ref: runtimeRef, origin: "runtime-parameter" };
|
|
29
32
|
}
|
|
30
33
|
if (lock) {
|
|
34
|
+
if (lock.schema === "buildchain.consumer-contract-lock/v2") {
|
|
35
|
+
const validated = consumerContractLock(lock);
|
|
36
|
+
if (canonicalJson(validated) !== canonicalJson(lock))
|
|
37
|
+
throw new Error(
|
|
38
|
+
"Minimal consumer contract lock contains unsupported fields",
|
|
39
|
+
);
|
|
40
|
+
return { ref: validated.runtime.sha, origin: "consumer-contract-lock" };
|
|
41
|
+
}
|
|
31
42
|
if (lock.contract !== "kungfu-buildchain-contract-lock")
|
|
32
43
|
throw new Error("Unsupported Buildchain contract lock");
|
|
33
44
|
const sha = commit(lock.buildchain?.resolvedSha, "Contract lock runtime");
|
|
@@ -92,6 +103,9 @@ export async function selectExecutionRuntime(
|
|
|
92
103
|
...(selected.origin === "contract-lock"
|
|
93
104
|
? { contract: { digest: request.lock.buildchain.contractDigest } }
|
|
94
105
|
: {}),
|
|
106
|
+
...(selected.origin === "consumer-contract-lock"
|
|
107
|
+
? { contract: { consumerConfigDigest: request.lock.configDigest } }
|
|
108
|
+
: {}),
|
|
95
109
|
class:
|
|
96
110
|
selected.origin === "contract-lock" &&
|
|
97
111
|
request.lock.buildchain.ref === "v4-alpha"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { resolveRecoverySource } from "./recovery.js";
|
|
2
|
+
import {
|
|
3
|
+
pipelineRuntimeContinuation,
|
|
4
|
+
pipelineRecoveryContinuation,
|
|
5
|
+
} from "../../workflow/pipeline/runtime-source.js";
|
|
6
|
+
|
|
7
|
+
export async function resolveRuntimeActionSource(
|
|
8
|
+
core,
|
|
9
|
+
env,
|
|
10
|
+
{ providerFactory, github, pipelineLookup },
|
|
11
|
+
) {
|
|
12
|
+
let source = {
|
|
13
|
+
repository: core.getInput("source-repository") || env.GITHUB_REPOSITORY,
|
|
14
|
+
sha: core.getInput("source-sha") || env.GITHUB_SHA,
|
|
15
|
+
ref: env.GITHUB_REF,
|
|
16
|
+
};
|
|
17
|
+
const pipelineAttempt = core.getInput("pipeline-attempt");
|
|
18
|
+
const recoveryAttempt = core.getInput("recovery-attempt");
|
|
19
|
+
const resumeRunId = core.getInput("resume-run-id");
|
|
20
|
+
if (
|
|
21
|
+
[pipelineAttempt, recoveryAttempt, resumeRunId].filter(Boolean).length > 1
|
|
22
|
+
)
|
|
23
|
+
throw new Error("Runtime source selectors are mutually exclusive");
|
|
24
|
+
let continuation;
|
|
25
|
+
if (recoveryAttempt) {
|
|
26
|
+
continuation = await pipelineRecoveryContinuation(
|
|
27
|
+
recoveryAttempt,
|
|
28
|
+
source.repository,
|
|
29
|
+
core.getInput("token", { required: true }),
|
|
30
|
+
pipelineLookup,
|
|
31
|
+
);
|
|
32
|
+
source = continuation.source;
|
|
33
|
+
}
|
|
34
|
+
if (pipelineAttempt) {
|
|
35
|
+
continuation = await pipelineRuntimeContinuation(
|
|
36
|
+
pipelineAttempt,
|
|
37
|
+
source.repository,
|
|
38
|
+
core.getInput("token", { required: true }),
|
|
39
|
+
pipelineLookup,
|
|
40
|
+
);
|
|
41
|
+
source = continuation.source;
|
|
42
|
+
}
|
|
43
|
+
if (resumeRunId) {
|
|
44
|
+
const reader = providerFactory(github, {
|
|
45
|
+
sourceRepository: source.repository,
|
|
46
|
+
sourceSha: source.sha,
|
|
47
|
+
actor: env.GITHUB_ACTOR,
|
|
48
|
+
eventName: env.GITHUB_EVENT_NAME,
|
|
49
|
+
});
|
|
50
|
+
await reader.authorize({ origin: "runtime-parameter" });
|
|
51
|
+
source = await resolveRecoverySource(
|
|
52
|
+
{
|
|
53
|
+
repository: source.repository,
|
|
54
|
+
runId: resumeRunId,
|
|
55
|
+
currentRunId: env.GITHUB_RUN_ID,
|
|
56
|
+
workflow: env.GITHUB_WORKFLOW_REF?.slice(source.repository.length + 1),
|
|
57
|
+
},
|
|
58
|
+
reader.readRun,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return { source, continuation, resumeRunId, pipelineAttempt };
|
|
62
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createIntent,
|
|
3
|
+
recordDigest,
|
|
4
|
+
} from "../../release/discussion/envelope.js";
|
|
5
|
+
import {
|
|
6
|
+
object,
|
|
7
|
+
text,
|
|
8
|
+
list,
|
|
9
|
+
choice,
|
|
10
|
+
unique,
|
|
11
|
+
relativePath,
|
|
12
|
+
} from "../../consumer/contract/shape.js";
|
|
13
|
+
import { CONSUMER_CONTRACT } from "../../consumer/contract/plan.js";
|
|
14
|
+
|
|
15
|
+
export const PIPELINE_PHASES = [
|
|
16
|
+
"admission",
|
|
17
|
+
"build",
|
|
18
|
+
"review",
|
|
19
|
+
"warrant",
|
|
20
|
+
"merge",
|
|
21
|
+
"publish",
|
|
22
|
+
"distribution",
|
|
23
|
+
"next-development",
|
|
24
|
+
];
|
|
25
|
+
export const SOURCE_GENERATION = "buildchain.source-generation/v1";
|
|
26
|
+
export const BUSINESS_ATTEMPT = "buildchain.business-attempt/v1";
|
|
27
|
+
const ROOT = /^sha256:[0-9a-f]{64}$/u;
|
|
28
|
+
|
|
29
|
+
export function pipelineIntent({
|
|
30
|
+
repository,
|
|
31
|
+
repositoryId,
|
|
32
|
+
pullRequest,
|
|
33
|
+
targetBranch,
|
|
34
|
+
phases,
|
|
35
|
+
runtime,
|
|
36
|
+
}) {
|
|
37
|
+
text(repository, "repository", /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u);
|
|
38
|
+
text(repositoryId, "repositoryId");
|
|
39
|
+
if (!Number.isSafeInteger(pullRequest) || pullRequest < 1)
|
|
40
|
+
throw new Error("Invalid PR intent number");
|
|
41
|
+
text(
|
|
42
|
+
targetBranch,
|
|
43
|
+
"targetBranch",
|
|
44
|
+
/^(?:dev|alpha|release|publish-gate)\/[A-Za-z0-9/._-]+$/u,
|
|
45
|
+
);
|
|
46
|
+
list(phases, "phases", (phase, field) =>
|
|
47
|
+
choice(phase, PIPELINE_PHASES, field),
|
|
48
|
+
);
|
|
49
|
+
unique(phases, "phases");
|
|
50
|
+
if (
|
|
51
|
+
phases.some(
|
|
52
|
+
(phase, i) =>
|
|
53
|
+
i &&
|
|
54
|
+
PIPELINE_PHASES.indexOf(phase) <=
|
|
55
|
+
PIPELINE_PHASES.indexOf(phases[i - 1]),
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
throw new Error("Intent phases must follow pipeline order");
|
|
59
|
+
if (phases[0] !== "admission")
|
|
60
|
+
throw new Error("Intent must begin with admission");
|
|
61
|
+
return createIntent({
|
|
62
|
+
repository,
|
|
63
|
+
key: `pr-${pullRequest}:${targetBranch}`,
|
|
64
|
+
expectedNodes: phases,
|
|
65
|
+
runtime,
|
|
66
|
+
source: { kind: "pipeline", repositoryId, pullRequest, targetBranch },
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function validateConsumerSource(source, repository) {
|
|
71
|
+
object(
|
|
72
|
+
source,
|
|
73
|
+
[
|
|
74
|
+
"schema",
|
|
75
|
+
"repository",
|
|
76
|
+
"commit",
|
|
77
|
+
"tree",
|
|
78
|
+
"configPath",
|
|
79
|
+
"configBlob",
|
|
80
|
+
"configDigest",
|
|
81
|
+
"contract",
|
|
82
|
+
],
|
|
83
|
+
[],
|
|
84
|
+
"source",
|
|
85
|
+
);
|
|
86
|
+
if (
|
|
87
|
+
source.schema !== "buildchain.consumer-source/v1" ||
|
|
88
|
+
source.contract !== CONSUMER_CONTRACT ||
|
|
89
|
+
source.repository !== repository
|
|
90
|
+
)
|
|
91
|
+
throw new Error("Generation requires the admitted consumer source");
|
|
92
|
+
for (const key of ["commit", "tree", "configBlob"])
|
|
93
|
+
text(source[key], `source.${key}`, /^[0-9a-f]{40}$/u);
|
|
94
|
+
relativePath(source.configPath, "source.configPath");
|
|
95
|
+
text(source.configDigest, "source.configDigest", ROOT);
|
|
96
|
+
return source;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function sourceGeneration(intent, source, baseCommit) {
|
|
100
|
+
validateConsumerSource(source, intent.repository);
|
|
101
|
+
text(baseCommit, "baseCommit", /^[0-9a-f]{40}$/u);
|
|
102
|
+
const value = {
|
|
103
|
+
schema: SOURCE_GENERATION,
|
|
104
|
+
intent: intent.id,
|
|
105
|
+
source: structuredClone(source),
|
|
106
|
+
baseCommit,
|
|
107
|
+
};
|
|
108
|
+
return { ...value, id: recordDigest(value) };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function validateGeneration(value, intent) {
|
|
112
|
+
object(
|
|
113
|
+
value,
|
|
114
|
+
["schema", "intent", "source", "baseCommit", "id"],
|
|
115
|
+
[],
|
|
116
|
+
"generation",
|
|
117
|
+
);
|
|
118
|
+
const expected = sourceGeneration(intent, value.source, value.baseCommit);
|
|
119
|
+
if (recordDigest(value) !== recordDigest(expected))
|
|
120
|
+
throw new Error("Generation identity mismatch");
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function businessAttempt({
|
|
125
|
+
intent,
|
|
126
|
+
generation,
|
|
127
|
+
predecessor = "",
|
|
128
|
+
requestKey,
|
|
129
|
+
}) {
|
|
130
|
+
validateGeneration(generation, intent);
|
|
131
|
+
text(requestKey, "requestKey");
|
|
132
|
+
if (requestKey.length > 240)
|
|
133
|
+
throw new Error("Attempt request key exceeds its bound");
|
|
134
|
+
if (predecessor) text(predecessor, "predecessor", /^attempt-[0-9a-f]{64}$/u);
|
|
135
|
+
const value = {
|
|
136
|
+
schema: BUSINESS_ATTEMPT,
|
|
137
|
+
intent: intent.id,
|
|
138
|
+
generation: generation.id,
|
|
139
|
+
predecessor,
|
|
140
|
+
requestKey,
|
|
141
|
+
};
|
|
142
|
+
return { ...value, id: `attempt-${recordDigest(value).slice(7)}` };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function validateAttempt(value, intent, generation) {
|
|
146
|
+
object(
|
|
147
|
+
value,
|
|
148
|
+
["schema", "intent", "generation", "predecessor", "requestKey", "id"],
|
|
149
|
+
[],
|
|
150
|
+
"attempt",
|
|
151
|
+
);
|
|
152
|
+
const expected = businessAttempt({
|
|
153
|
+
intent,
|
|
154
|
+
generation,
|
|
155
|
+
predecessor: value.predecessor,
|
|
156
|
+
requestKey: value.requestKey,
|
|
157
|
+
});
|
|
158
|
+
if (recordDigest(value) !== recordDigest(expected))
|
|
159
|
+
throw new Error("Business attempt identity mismatch");
|
|
160
|
+
return value;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function providerWriter(value, repository) {
|
|
164
|
+
object(value, ["repository", "runId", "runAttempt", "jobId"], [], "writer");
|
|
165
|
+
if (value.repository !== repository)
|
|
166
|
+
throw new Error("Writer belongs to a different repository");
|
|
167
|
+
for (const key of ["runId", "runAttempt", "jobId"])
|
|
168
|
+
text(value[key], `writer.${key}`, /^[1-9][0-9]*$/u);
|
|
169
|
+
return `${value.runId}:${value.runAttempt}:${value.jobId}`;
|
|
170
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalJson,
|
|
3
|
+
recordDigest,
|
|
4
|
+
} from "../../release/discussion/envelope.js";
|
|
5
|
+
import { readBusinessAttempt } from "./reader.js";
|
|
6
|
+
|
|
7
|
+
export const ATTEMPT_JOURNAL = "buildchain.attempt-journal/v1";
|
|
8
|
+
|
|
9
|
+
export function journalSnapshot(intent, records) {
|
|
10
|
+
readBusinessAttempt({ intent, records });
|
|
11
|
+
const body = { schema: ATTEMPT_JOURNAL, intent, records };
|
|
12
|
+
return { ...body, root: recordDigest(body) };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function validateJournal(snapshot, intent) {
|
|
16
|
+
if (
|
|
17
|
+
snapshot?.schema !== ATTEMPT_JOURNAL ||
|
|
18
|
+
canonicalJson(snapshot.intent) !== canonicalJson(intent) ||
|
|
19
|
+
canonicalJson(snapshot) !==
|
|
20
|
+
canonicalJson(journalSnapshot(intent, snapshot.records))
|
|
21
|
+
)
|
|
22
|
+
throw new Error("Attempt journal identity or content root drift");
|
|
23
|
+
return snapshot;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// The provider atomically appends a single-parent Git commit with force=false.
|
|
27
|
+
// Discussion is a projection of these admitted bytes, never a competing writer.
|
|
28
|
+
export function atomicAttemptJournal(provider, intent) {
|
|
29
|
+
readBusinessAttempt({ intent, records: [] });
|
|
30
|
+
async function read() {
|
|
31
|
+
const loaded = await provider.read(intent);
|
|
32
|
+
const snapshot = loaded
|
|
33
|
+
? validateJournal(loaded.snapshot, intent)
|
|
34
|
+
: journalSnapshot(intent, []);
|
|
35
|
+
return {
|
|
36
|
+
commit: loaded?.commit || "",
|
|
37
|
+
snapshot,
|
|
38
|
+
...readBusinessAttempt(snapshot),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async function append(record, expectedHead) {
|
|
42
|
+
const observed = await read();
|
|
43
|
+
if (observed.snapshot.records.some((event) => event.id === record.id)) {
|
|
44
|
+
// Never acknowledge an invalid body merely because its ID was copied.
|
|
45
|
+
const prior = observed.snapshot.records.find(
|
|
46
|
+
(event) => event.id === record.id,
|
|
47
|
+
);
|
|
48
|
+
if (canonicalJson(prior) !== canonicalJson(record))
|
|
49
|
+
throw new Error("Conflicting attempt record bytes");
|
|
50
|
+
return observed;
|
|
51
|
+
}
|
|
52
|
+
if ((observed.head || "") !== expectedHead)
|
|
53
|
+
throw new Error("Stale attempt head; reobserve before writing");
|
|
54
|
+
if (record.node === "attempt") {
|
|
55
|
+
if (record.predecessor !== (observed.attempt || ""))
|
|
56
|
+
throw new Error("Recovery must succeed the current attempt");
|
|
57
|
+
} else if (record.attempt !== observed.attempt)
|
|
58
|
+
throw new Error("A superseded attempt cannot append new results");
|
|
59
|
+
const snapshot = journalSnapshot(intent, [
|
|
60
|
+
...observed.snapshot.records,
|
|
61
|
+
record,
|
|
62
|
+
]);
|
|
63
|
+
let failure;
|
|
64
|
+
try {
|
|
65
|
+
await provider.append({
|
|
66
|
+
intent,
|
|
67
|
+
snapshot,
|
|
68
|
+
expectedCommit: observed.commit,
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
failure = error;
|
|
72
|
+
}
|
|
73
|
+
// A lost response can follow a committed append. Reconcile the immutable
|
|
74
|
+
// record in live history; never retry the old parent with force=true.
|
|
75
|
+
const result = await read();
|
|
76
|
+
if (
|
|
77
|
+
!result.snapshot.records.some(
|
|
78
|
+
(event) => canonicalJson(event) === canonicalJson(record),
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
throw (
|
|
82
|
+
failure || new Error("Attempt append missing from provider readback")
|
|
83
|
+
);
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
return { read, append };
|
|
87
|
+
}
|