@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,56 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
import { object } from "../../consumer/contract/shape.js";
|
|
3
|
+
|
|
4
|
+
export async function recordPipelineGroup(context, host) {
|
|
5
|
+
object(context, [
|
|
6
|
+
"schema",
|
|
7
|
+
"source",
|
|
8
|
+
"branch",
|
|
9
|
+
"baseCommit",
|
|
10
|
+
"runId",
|
|
11
|
+
"runAttempt",
|
|
12
|
+
"platforms",
|
|
13
|
+
]);
|
|
14
|
+
if (
|
|
15
|
+
context.schema !== "buildchain.pipeline-group-build-context/v1" ||
|
|
16
|
+
context.runId !== host.runId ||
|
|
17
|
+
context.runAttempt !== host.runAttempt
|
|
18
|
+
)
|
|
19
|
+
throw new Error("Merge group callback execution drift");
|
|
20
|
+
const { run } = await host.runs.read(context.runId, context.runAttempt);
|
|
21
|
+
if (run.event !== "merge_group" || run.head_sha !== context.source.commit)
|
|
22
|
+
throw new Error("Merge group build is not the exact provider execution");
|
|
23
|
+
const queue = await host.queue.getMergeQueueState(context.branch);
|
|
24
|
+
if (
|
|
25
|
+
!queue.enabled ||
|
|
26
|
+
!queue.entries.some(
|
|
27
|
+
(entry) =>
|
|
28
|
+
entry.headSha === context.source.commit &&
|
|
29
|
+
entry.baseSha === context.baseCommit,
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
throw new Error("Merge group was removed or changed before verification");
|
|
33
|
+
const readback = await host.runs.build(
|
|
34
|
+
context.runId,
|
|
35
|
+
context.runAttempt,
|
|
36
|
+
context.source,
|
|
37
|
+
context.platforms.map((platform) => platform.platform),
|
|
38
|
+
);
|
|
39
|
+
await host.request(`/repos/${host.repository}/check-runs`, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
body: {
|
|
42
|
+
name: "check",
|
|
43
|
+
head_sha: context.source.commit,
|
|
44
|
+
external_id: `buildchain:merge-group:${recordDigest(context)}`,
|
|
45
|
+
status: "completed",
|
|
46
|
+
conclusion: readback.outcome,
|
|
47
|
+
output: {
|
|
48
|
+
title: "Buildchain exact merge-group verification",
|
|
49
|
+
summary: `Provider product jobs: ${readback.root}`,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
if (readback.outcome !== "success")
|
|
54
|
+
throw new Error("Merge-group products failed verification");
|
|
55
|
+
return readback;
|
|
56
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { githubPipelineRuns } from "../../providers/github/pipeline-runs.js";
|
|
2
|
+
import { readPipelineCaller } from "../../providers/github/pipeline-run-entry.js";
|
|
3
|
+
import { pipelineBuildEvidence } from "./build-evidence.js";
|
|
4
|
+
import { qualifyPipelineSourceRun } from "./delivery-request.js";
|
|
5
|
+
|
|
6
|
+
export async function guardPipelineBuild(
|
|
7
|
+
input,
|
|
8
|
+
observed,
|
|
9
|
+
{
|
|
10
|
+
material,
|
|
11
|
+
source,
|
|
12
|
+
request,
|
|
13
|
+
repository,
|
|
14
|
+
runs = githubPipelineRuns(request, repository),
|
|
15
|
+
},
|
|
16
|
+
) {
|
|
17
|
+
const session = { observed, journal: { read: async () => observed } };
|
|
18
|
+
const build = await pipelineBuildEvidence(session, {
|
|
19
|
+
runId: 0,
|
|
20
|
+
runs,
|
|
21
|
+
source,
|
|
22
|
+
materialStore: () => material,
|
|
23
|
+
});
|
|
24
|
+
if (!build || build.run.id !== Number(input["source-workflow-run-id"]))
|
|
25
|
+
throw new Error(
|
|
26
|
+
"Delivery requires its independently qualified exact product build",
|
|
27
|
+
);
|
|
28
|
+
const current = { ...observed.history.at(-1), intent: observed.intent };
|
|
29
|
+
qualifyPipelineSourceRun(build.run, current, build.readback);
|
|
30
|
+
await readPipelineCaller(
|
|
31
|
+
build.run,
|
|
32
|
+
current.generation.source.configPath,
|
|
33
|
+
request,
|
|
34
|
+
repository,
|
|
35
|
+
);
|
|
36
|
+
return build;
|
|
37
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { githubJsonClient } from "../../providers/github/json-client.js";
|
|
2
|
+
import { githubAttemptJournal } from "../../providers/github/attempt-journal.js";
|
|
3
|
+
import { githubAttemptIndex } from "../../providers/github/attempt-index.js";
|
|
4
|
+
import { readBusinessAttempt } from "../attempt/reader.js";
|
|
5
|
+
import { assertPipelineExecution } from "./fence.js";
|
|
6
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
7
|
+
import { githubPipelineSource } from "../../providers/github/pipeline-source.js";
|
|
8
|
+
import { githubPipelinePolicy } from "../../providers/github/pipeline-policy.js";
|
|
9
|
+
import { getOctokit } from "@actions/github";
|
|
10
|
+
import { discussionMaterials } from "../../providers/github/discussions/materials.js";
|
|
11
|
+
import { pipelineMaterials } from "./materials.js";
|
|
12
|
+
import { guardPipelineBuild } from "./guard-build.js";
|
|
13
|
+
|
|
14
|
+
async function assertRetainedRequest(input, connection, observed) {
|
|
15
|
+
const current = observed.history.at(-1);
|
|
16
|
+
const event = [...current.events]
|
|
17
|
+
.reverse()
|
|
18
|
+
.find((entry) =>
|
|
19
|
+
entry.payload.materials.some((material) =>
|
|
20
|
+
material.id.startsWith("delivery/execution-"),
|
|
21
|
+
),
|
|
22
|
+
);
|
|
23
|
+
const reference = event?.payload.materials.find((material) =>
|
|
24
|
+
material.id.startsWith("delivery/execution-"),
|
|
25
|
+
);
|
|
26
|
+
if (!reference)
|
|
27
|
+
throw new Error("Pipeline delivery has no admitted execution request");
|
|
28
|
+
const archive = discussionMaterials({
|
|
29
|
+
octokit: getOctokit(connection.token),
|
|
30
|
+
repository: connection.repository,
|
|
31
|
+
intentId: observed.intent.id,
|
|
32
|
+
});
|
|
33
|
+
const material = pipelineMaterials(archive, {
|
|
34
|
+
repository: connection.repository,
|
|
35
|
+
attempt: current.identity,
|
|
36
|
+
});
|
|
37
|
+
const execution = await material.read(reference);
|
|
38
|
+
validatePipelineExecutionRequest(execution, input, observed, {
|
|
39
|
+
runId: Number(process.env.GITHUB_RUN_ID),
|
|
40
|
+
runAttempt: Number(process.env.GITHUB_RUN_ATTEMPT),
|
|
41
|
+
});
|
|
42
|
+
return material;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function validatePipelineExecutionRequest(
|
|
46
|
+
execution,
|
|
47
|
+
input,
|
|
48
|
+
observed,
|
|
49
|
+
coordinates,
|
|
50
|
+
) {
|
|
51
|
+
if (execution.schema !== "buildchain.pipeline-delivery-execution/v1")
|
|
52
|
+
throw new Error("Pipeline delivery request schema is unsupported");
|
|
53
|
+
if (
|
|
54
|
+
execution.attempt !== observed.attempt ||
|
|
55
|
+
execution.generation !== observed.generation ||
|
|
56
|
+
execution.runId !== coordinates.runId ||
|
|
57
|
+
execution.runAttempt !== coordinates.runAttempt
|
|
58
|
+
)
|
|
59
|
+
throw new Error(
|
|
60
|
+
"Pipeline delivery is not the exact admitted provider execution",
|
|
61
|
+
);
|
|
62
|
+
for (const [key, value] of Object.entries(execution.request))
|
|
63
|
+
if (
|
|
64
|
+
!Object.hasOwn(input, key) ||
|
|
65
|
+
recordDigest(input[key]) !== recordDigest(value)
|
|
66
|
+
)
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Pipeline delivery changed its retained request field: ${key}`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function guardPipelineAdmission(input, connection) {
|
|
73
|
+
const observed = await guardPipelineDelivery(input, connection);
|
|
74
|
+
if (!observed) return null;
|
|
75
|
+
const material = await assertRetainedRequest(input, connection, observed);
|
|
76
|
+
const request = githubJsonClient({
|
|
77
|
+
token: connection.token,
|
|
78
|
+
userAgent: "buildchain-pipeline-admission",
|
|
79
|
+
});
|
|
80
|
+
const current = { ...observed.history.at(-1), intent: observed.intent };
|
|
81
|
+
const source = githubPipelineSource(request, connection.repository);
|
|
82
|
+
const admission = await source.observeIntent(
|
|
83
|
+
observed.intent,
|
|
84
|
+
current.generation,
|
|
85
|
+
{ "config-path": current.generation.source.configPath },
|
|
86
|
+
);
|
|
87
|
+
const { live } = admission;
|
|
88
|
+
if (
|
|
89
|
+
admission.cleanupOnly ||
|
|
90
|
+
!live.source ||
|
|
91
|
+
live.state !== "open" ||
|
|
92
|
+
live.draft ||
|
|
93
|
+
!live.ready ||
|
|
94
|
+
live.baseCommit !== current.generation.baseCommit ||
|
|
95
|
+
recordDigest(live.source) !== recordDigest(current.generation.source)
|
|
96
|
+
)
|
|
97
|
+
throw new Error(
|
|
98
|
+
"Pipeline source is no longer admitted for a delivery effect",
|
|
99
|
+
);
|
|
100
|
+
const policy = await githubPipelinePolicy(
|
|
101
|
+
request,
|
|
102
|
+
connection.repository,
|
|
103
|
+
).observe(current, admission.protectedPlan.review);
|
|
104
|
+
if (!policy.review || !policy.checksPassing)
|
|
105
|
+
throw new Error(
|
|
106
|
+
"Pipeline protected review or required checks changed before delivery effect",
|
|
107
|
+
);
|
|
108
|
+
await guardPipelineBuild(input, observed, {
|
|
109
|
+
material,
|
|
110
|
+
source,
|
|
111
|
+
request,
|
|
112
|
+
repository: connection.repository,
|
|
113
|
+
});
|
|
114
|
+
return guardPipelineDelivery(input, connection);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function guardPipelineDelivery(input, connection, lookup) {
|
|
118
|
+
const attempt = input["pipeline-attempt"];
|
|
119
|
+
// Existing advanced delivery callers retain their domain authority. The new
|
|
120
|
+
// normal entry always supplies its internally selected business attempt.
|
|
121
|
+
if (!attempt) return null;
|
|
122
|
+
if (!connection.token)
|
|
123
|
+
throw new Error("Pipeline controller requires its scoped read credential");
|
|
124
|
+
const request = githubJsonClient({
|
|
125
|
+
token: connection.token,
|
|
126
|
+
userAgent: "buildchain-pipeline",
|
|
127
|
+
});
|
|
128
|
+
const index =
|
|
129
|
+
lookup ||
|
|
130
|
+
githubAttemptIndex(
|
|
131
|
+
request,
|
|
132
|
+
githubAttemptJournal(request),
|
|
133
|
+
connection.repository,
|
|
134
|
+
);
|
|
135
|
+
const loaded = await index.resolve(attempt);
|
|
136
|
+
const observed = readBusinessAttempt(loaded.snapshot);
|
|
137
|
+
const current = observed.history.at(-1);
|
|
138
|
+
if (
|
|
139
|
+
!current ||
|
|
140
|
+
observed.intent.repository !== connection.repository ||
|
|
141
|
+
observed.intent.source.targetBranch !== input["target-branch"] ||
|
|
142
|
+
observed.intent.source.pullRequest !==
|
|
143
|
+
Number(input["expected-pr-number"]) ||
|
|
144
|
+
current.generation.source.commit !== input["expected-head-sha"]
|
|
145
|
+
)
|
|
146
|
+
throw new Error(
|
|
147
|
+
"Pipeline delivery request changed its admitted source intent",
|
|
148
|
+
);
|
|
149
|
+
assertPipelineExecution(observed, {
|
|
150
|
+
attempt,
|
|
151
|
+
intent: observed.intent.id,
|
|
152
|
+
generation: current.generation.id,
|
|
153
|
+
sourceRoot: recordDigest(current.generation.source),
|
|
154
|
+
});
|
|
155
|
+
return observed;
|
|
156
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { getOctokit } from "@actions/github";
|
|
5
|
+
import { installationRoot } from "../../runtime/installation-root.js";
|
|
6
|
+
import { preparedRuntimeSelection } from "../../runtime/entry/selection.js";
|
|
7
|
+
import { githubJsonClient } from "../../providers/github/json-client.js";
|
|
8
|
+
import { githubAttemptJournal } from "../../providers/github/attempt-journal.js";
|
|
9
|
+
import { githubAttemptIndex } from "../../providers/github/attempt-index.js";
|
|
10
|
+
import { githubPipelineSource } from "../../providers/github/pipeline-source.js";
|
|
11
|
+
import { githubPipelineRuns } from "../../providers/github/pipeline-runs.js";
|
|
12
|
+
import { githubPipelineWorker } from "../../providers/github/pipeline-worker.js";
|
|
13
|
+
import { githubPipelinePolicy } from "../../providers/github/pipeline-policy.js";
|
|
14
|
+
import { discussionMaterials } from "../../providers/github/discussions/materials.js";
|
|
15
|
+
import { pipelineMaterials } from "./materials.js";
|
|
16
|
+
import { GitHubClient } from "../../dev-delivery/admission/github-client.js";
|
|
17
|
+
import { githubPipelineIntegration } from "../../providers/github/pipeline-integration.js";
|
|
18
|
+
import { recordPipelineGroup } from "./group-control.js";
|
|
19
|
+
import { controlPipelineChannel } from "./channel-control.js";
|
|
20
|
+
import { settlePipeline } from "./settlement.js";
|
|
21
|
+
import { pipelineProjection } from "./projection.js";
|
|
22
|
+
import { resumePipelineNotifications } from "./notifications.js";
|
|
23
|
+
import { projectPipelineStatus } from "./web-status.js";
|
|
24
|
+
import { inspectRecoveryPublication } from "../../publication/pipeline/recovery-inspection.js";
|
|
25
|
+
|
|
26
|
+
export async function pipelineHost(core, env, jobName) {
|
|
27
|
+
const token = core.getInput("token", { required: true });
|
|
28
|
+
const repository = env.GITHUB_REPOSITORY;
|
|
29
|
+
const selection = preparedRuntimeSelection(
|
|
30
|
+
JSON.parse(env.BUILDCHAIN_RUNTIME_SELECTION),
|
|
31
|
+
);
|
|
32
|
+
const runtimeRoot = installationRoot(import.meta.url);
|
|
33
|
+
const reader = fs.readFileSync(
|
|
34
|
+
path.join(runtimeRoot, "dist/readers/business-attempt.cjs"),
|
|
35
|
+
);
|
|
36
|
+
const runtime = {
|
|
37
|
+
repository: selection.repository,
|
|
38
|
+
sha: selection.sha,
|
|
39
|
+
readerDigest: `sha256:${createHash("sha256").update(reader).digest("hex")}`,
|
|
40
|
+
};
|
|
41
|
+
const request = githubJsonClient({ token, userAgent: "buildchain-pipeline" });
|
|
42
|
+
const github = getOctokit(token);
|
|
43
|
+
const provider = githubAttemptJournal(request);
|
|
44
|
+
const index = githubAttemptIndex(request, provider, repository);
|
|
45
|
+
const runs = githubPipelineRuns(request, repository);
|
|
46
|
+
const runId = Number(env.GITHUB_RUN_ID),
|
|
47
|
+
runAttempt = Number(env.GITHUB_RUN_ATTEMPT);
|
|
48
|
+
const writer = await runs.writer(runId, runAttempt, jobName);
|
|
49
|
+
const source = githubPipelineSource(request, repository);
|
|
50
|
+
const [owner, repo] = repository.split("/");
|
|
51
|
+
const host = {
|
|
52
|
+
repository,
|
|
53
|
+
token,
|
|
54
|
+
github,
|
|
55
|
+
selection,
|
|
56
|
+
runtime,
|
|
57
|
+
runtimeRoot,
|
|
58
|
+
request,
|
|
59
|
+
provider,
|
|
60
|
+
index,
|
|
61
|
+
runs,
|
|
62
|
+
runId,
|
|
63
|
+
runAttempt,
|
|
64
|
+
writer,
|
|
65
|
+
source,
|
|
66
|
+
workers: githubPipelineWorker(request),
|
|
67
|
+
policy: githubPipelinePolicy(request, repository),
|
|
68
|
+
queue: new GitHubClient({ repository: { owner, repo }, token }),
|
|
69
|
+
integration: githubPipelineIntegration(request, repository, source, runs),
|
|
70
|
+
groupRecord: (context) => recordPipelineGroup(context, host),
|
|
71
|
+
channel: (session, admission, inputs) =>
|
|
72
|
+
controlPipelineChannel(session, admission, inputs, host),
|
|
73
|
+
settle: (session, fresh, delivery) =>
|
|
74
|
+
settlePipeline(session, fresh, delivery, host),
|
|
75
|
+
project: (session) =>
|
|
76
|
+
projectPipelineStatus(session, {
|
|
77
|
+
project: pipelineProjection(github.graphql),
|
|
78
|
+
core,
|
|
79
|
+
}),
|
|
80
|
+
notifyTerminal: (session) => resumePipelineNotifications(session, host),
|
|
81
|
+
recoverPublication: (session, admitted) =>
|
|
82
|
+
inspectRecoveryPublication(session, admitted, host),
|
|
83
|
+
materialStore: (session) =>
|
|
84
|
+
pipelineMaterials(
|
|
85
|
+
discussionMaterials({
|
|
86
|
+
octokit: github,
|
|
87
|
+
repository,
|
|
88
|
+
intentId: session.intent.id,
|
|
89
|
+
authorityDescription:
|
|
90
|
+
"The canonical Buildchain intent Git journal owns transaction state.",
|
|
91
|
+
}),
|
|
92
|
+
{ repository, attempt: session.observed.history.at(-1).identity },
|
|
93
|
+
),
|
|
94
|
+
productArchive: (session) =>
|
|
95
|
+
discussionMaterials({
|
|
96
|
+
octokit: github,
|
|
97
|
+
repository,
|
|
98
|
+
intentId: session.intent.id,
|
|
99
|
+
authorityDescription:
|
|
100
|
+
"The canonical Buildchain intent Git journal owns transaction state.",
|
|
101
|
+
}),
|
|
102
|
+
wake: async (attempt) => {
|
|
103
|
+
await index.resolve(attempt);
|
|
104
|
+
await request(`/repos/${repository}/dispatches`, {
|
|
105
|
+
method: "POST",
|
|
106
|
+
body: {
|
|
107
|
+
event_type: "buildchain-attempt-wake",
|
|
108
|
+
client_payload: { attempt },
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
return host;
|
|
114
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalJson,
|
|
3
|
+
recordDigest,
|
|
4
|
+
} from "../../release/discussion/envelope.js";
|
|
5
|
+
import {
|
|
6
|
+
materialReference,
|
|
7
|
+
verifyMaterialBytes,
|
|
8
|
+
} from "../attempt/materials.js";
|
|
9
|
+
|
|
10
|
+
// Receipts are retained before the atomic journal references them. An interrupted
|
|
11
|
+
// upload can leave unreferenced material; it cannot advance attempt authority.
|
|
12
|
+
export function pipelineMaterials(archive, { repository, attempt }) {
|
|
13
|
+
async function retain(id, receipt, kind = "receipt") {
|
|
14
|
+
const bytes = Buffer.from(`${canonicalJson(receipt)}\n`);
|
|
15
|
+
const handle = await archive.put(bytes, {
|
|
16
|
+
name: `${id.replaceAll("/", "-")}.json`,
|
|
17
|
+
mediaType: "application/json",
|
|
18
|
+
});
|
|
19
|
+
const reference = materialReference(
|
|
20
|
+
{
|
|
21
|
+
id,
|
|
22
|
+
kind,
|
|
23
|
+
digest: handle.digest,
|
|
24
|
+
bytes: handle.size,
|
|
25
|
+
url: `https://api.github.com/repos/${repository}/releases/assets/${handle.id}`,
|
|
26
|
+
producerAttempt: attempt.id,
|
|
27
|
+
generation: attempt.generation,
|
|
28
|
+
},
|
|
29
|
+
attempt,
|
|
30
|
+
repository,
|
|
31
|
+
);
|
|
32
|
+
verifyMaterialBytes(reference, await archive.read(handle));
|
|
33
|
+
return reference;
|
|
34
|
+
}
|
|
35
|
+
async function read(reference) {
|
|
36
|
+
materialReference(reference, attempt, repository);
|
|
37
|
+
const prefix = `https://api.github.com/repos/${repository}/releases/assets/`;
|
|
38
|
+
if (!reference.url.startsWith(prefix))
|
|
39
|
+
throw new Error(
|
|
40
|
+
"Pipeline receipt is not in its retained material archive",
|
|
41
|
+
);
|
|
42
|
+
const id = reference.url.slice(prefix.length);
|
|
43
|
+
if (!/^[1-9][0-9]*$/u.test(id) || !Number.isSafeInteger(Number(id)))
|
|
44
|
+
throw new Error("Pipeline receipt asset identity is invalid");
|
|
45
|
+
const bytes = await archive.read({
|
|
46
|
+
id: Number(id),
|
|
47
|
+
size: reference.bytes,
|
|
48
|
+
digest: reference.digest,
|
|
49
|
+
});
|
|
50
|
+
verifyMaterialBytes(reference, bytes);
|
|
51
|
+
const receipt = JSON.parse(bytes.toString("utf8"));
|
|
52
|
+
// Normalize the supported JSON value domain, rejecting undefined or NaN
|
|
53
|
+
// from alternate injected archive implementations before returning it.
|
|
54
|
+
recordDigest(receipt);
|
|
55
|
+
return receipt;
|
|
56
|
+
}
|
|
57
|
+
return { retain, read };
|
|
58
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { wakePipelineCandidate } from "./wake.js";
|
|
2
|
+
import { pipelineCandidate } from "./reconcile.js";
|
|
3
|
+
import { notifyPipelineParent } from "./parent-notification.js";
|
|
4
|
+
|
|
5
|
+
export async function resumePipelineNotifications(session, host) {
|
|
6
|
+
await notifyPipelineParent(session, host);
|
|
7
|
+
const observed = await session.journal.read();
|
|
8
|
+
const current = { ...observed.history.at(-1), intent: session.intent };
|
|
9
|
+
const terminal = Object.values(current.phases).filter((phase) =>
|
|
10
|
+
["success", "cancelled", "failure"].includes(phase.payload.state),
|
|
11
|
+
);
|
|
12
|
+
for (const phase of terminal) {
|
|
13
|
+
for (const reference of phase.payload.materials.filter((material) =>
|
|
14
|
+
/^(?:merge\/settlement-|cancellation\/receipt-|failure\/)/u.test(
|
|
15
|
+
material.id,
|
|
16
|
+
),
|
|
17
|
+
)) {
|
|
18
|
+
const receipt = await host.materialStore(session).read(reference);
|
|
19
|
+
const successor =
|
|
20
|
+
receipt.receipt?.successorWake ||
|
|
21
|
+
(Array.isArray(receipt.candidates)
|
|
22
|
+
? pipelineCandidate(receipt, current)?.terminal?.successorWake
|
|
23
|
+
: null);
|
|
24
|
+
if (successor)
|
|
25
|
+
await wakePipelineCandidate(
|
|
26
|
+
successor,
|
|
27
|
+
{
|
|
28
|
+
repository: host.repository,
|
|
29
|
+
token: host.token,
|
|
30
|
+
branch: session.intent.source.targetBranch,
|
|
31
|
+
},
|
|
32
|
+
host,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { readBusinessAttempt } from "../attempt/reader.js";
|
|
2
|
+
|
|
3
|
+
export async function notifyPipelineParent(session, host) {
|
|
4
|
+
const pr = await host.request(
|
|
5
|
+
`/repos/${host.repository}/pulls/${session.intent.source.pullRequest}`,
|
|
6
|
+
);
|
|
7
|
+
if (!/^feature\/buildchain-next\/[0-9a-f]{64}$/u.test(pr.head?.ref || ""))
|
|
8
|
+
return;
|
|
9
|
+
const matches = [
|
|
10
|
+
...String(pr.body || "").matchAll(
|
|
11
|
+
/^Buildchain-Parent-Attempt: (attempt-[0-9a-f]{64})$/gmu,
|
|
12
|
+
),
|
|
13
|
+
];
|
|
14
|
+
if (matches.length !== 1) return;
|
|
15
|
+
const attempt = matches[0][1];
|
|
16
|
+
const parent = await host.index.resolve(attempt);
|
|
17
|
+
const state = readBusinessAttempt(parent.snapshot);
|
|
18
|
+
if (
|
|
19
|
+
state.attempt !== attempt ||
|
|
20
|
+
state.status === "complete" ||
|
|
21
|
+
!state.intent.expectedNodes.includes("next-development")
|
|
22
|
+
)
|
|
23
|
+
return;
|
|
24
|
+
// The body is only a bounded wake selector. Current journal source and exact
|
|
25
|
+
// generated PR readback are re-admitted before any parent mutation.
|
|
26
|
+
await host.wake(attempt);
|
|
27
|
+
}
|
|
@@ -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
|
+
}
|