@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,109 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { publishedDigest, runNpm } from "./registry.js";
|
|
4
|
+
import { createNativeChildEnvironment } from "../../dev-delivery/native/execution.js";
|
|
5
|
+
import {
|
|
6
|
+
publicationFile,
|
|
7
|
+
publicationPath,
|
|
8
|
+
writeImmutablePublicationFile,
|
|
9
|
+
} from "../pipeline/files.js";
|
|
10
|
+
|
|
11
|
+
export function pipelineNpmEnvironment(directory, environment = process.env) {
|
|
12
|
+
const cwd = path.resolve(directory);
|
|
13
|
+
const config = path.join(cwd, ".buildchain-publisher.npmrc");
|
|
14
|
+
fs.mkdirSync(cwd, { recursive: true });
|
|
15
|
+
writeImmutablePublicationFile(
|
|
16
|
+
config,
|
|
17
|
+
environment.NODE_AUTH_TOKEN
|
|
18
|
+
? "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}\n"
|
|
19
|
+
: "",
|
|
20
|
+
);
|
|
21
|
+
const env = {
|
|
22
|
+
...createNativeChildEnvironment(environment),
|
|
23
|
+
NPM_CONFIG_USERCONFIG: config,
|
|
24
|
+
NPM_CONFIG_REGISTRY: "https://registry.npmjs.org/",
|
|
25
|
+
NPM_CONFIG_IGNORE_SCRIPTS: "true",
|
|
26
|
+
};
|
|
27
|
+
for (const key of [
|
|
28
|
+
"ACTIONS_ID_TOKEN_REQUEST_URL",
|
|
29
|
+
"ACTIONS_ID_TOKEN_REQUEST_TOKEN",
|
|
30
|
+
"NODE_AUTH_TOKEN",
|
|
31
|
+
"GITHUB_ACTIONS",
|
|
32
|
+
])
|
|
33
|
+
if (environment[key]) env[key] = environment[key];
|
|
34
|
+
return env;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function pipelineNpmProvider({
|
|
38
|
+
directory,
|
|
39
|
+
artifacts,
|
|
40
|
+
environment = process.env,
|
|
41
|
+
run = runNpm,
|
|
42
|
+
lookup = publishedDigest,
|
|
43
|
+
}) {
|
|
44
|
+
const cwd = path.resolve(directory);
|
|
45
|
+
const env = pipelineNpmEnvironment(directory, environment);
|
|
46
|
+
const registry = "https://registry.npmjs.org/";
|
|
47
|
+
const args = { cwd, env, registry };
|
|
48
|
+
function artifactFor(effect) {
|
|
49
|
+
const found = artifacts.filter(
|
|
50
|
+
(artifact) => artifact.id === effect.product,
|
|
51
|
+
);
|
|
52
|
+
if (
|
|
53
|
+
found.length !== 1 ||
|
|
54
|
+
found[0].package?.name !== effect.name ||
|
|
55
|
+
found[0].package?.version !== effect.version ||
|
|
56
|
+
found[0].package?.integrity !== effect.integrity
|
|
57
|
+
)
|
|
58
|
+
throw new Error("npm publication changed qualified package identity");
|
|
59
|
+
const artifact = found[0];
|
|
60
|
+
const file = publicationPath(cwd, artifact.file);
|
|
61
|
+
const bytes = publicationFile(file);
|
|
62
|
+
if (
|
|
63
|
+
bytes.integrity !== effect.integrity ||
|
|
64
|
+
bytes.digest !== artifact.digest
|
|
65
|
+
)
|
|
66
|
+
throw new Error("npm publication bytes changed after qualification");
|
|
67
|
+
return file;
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
async observe(effect) {
|
|
71
|
+
artifactFor(effect);
|
|
72
|
+
if (effect.access === "restricted" && !env.NODE_AUTH_TOKEN)
|
|
73
|
+
throw new Error(
|
|
74
|
+
"Restricted npm readback requires a configured package read credential",
|
|
75
|
+
);
|
|
76
|
+
const integrity = lookup({
|
|
77
|
+
...args,
|
|
78
|
+
name: effect.name,
|
|
79
|
+
version: effect.version,
|
|
80
|
+
});
|
|
81
|
+
return integrity ? { state: "present", integrity } : { state: "absent" };
|
|
82
|
+
},
|
|
83
|
+
matches: (effect, observed) =>
|
|
84
|
+
observed.state === "present" && observed.integrity === effect.integrity,
|
|
85
|
+
async apply(effect) {
|
|
86
|
+
const file = artifactFor(effect);
|
|
87
|
+
const result = run({
|
|
88
|
+
cwd,
|
|
89
|
+
env,
|
|
90
|
+
args: [
|
|
91
|
+
"publish",
|
|
92
|
+
file,
|
|
93
|
+
"--ignore-scripts",
|
|
94
|
+
"--provenance=false",
|
|
95
|
+
"--access",
|
|
96
|
+
effect.access,
|
|
97
|
+
"--tag",
|
|
98
|
+
effect.tag,
|
|
99
|
+
`--registry=${registry}`,
|
|
100
|
+
],
|
|
101
|
+
allowFailure: true,
|
|
102
|
+
});
|
|
103
|
+
if (result.status !== 0)
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Sealed npm publication failed with exit ${result.status}`,
|
|
106
|
+
);
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -19,17 +19,23 @@ export function runNpm({ cwd, args, env, allowFailure = false }) {
|
|
|
19
19
|
return result;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
function parseNpmView(stdout) {
|
|
22
|
+
export function parseNpmView(stdout) {
|
|
23
23
|
const raw = String(stdout || "").trim();
|
|
24
|
-
if (!raw)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
if (!raw) throw new Error("npm view omitted its exact package response");
|
|
25
|
+
const value = JSON.parse(raw);
|
|
26
|
+
if (Array.isArray(value) && value.length !== 1)
|
|
27
|
+
throw new Error("npm view must return exactly one package version");
|
|
28
|
+
const parsed = Array.isArray(value) ? value[0] : value;
|
|
29
|
+
if (typeof parsed === "string" && parsed)
|
|
30
|
+
return { integrity: parsed, shasum: "" };
|
|
28
31
|
const dist = parsed?.dist || parsed;
|
|
29
|
-
|
|
32
|
+
const result = {
|
|
30
33
|
integrity: dist?.integrity || parsed?.["dist.integrity"] || "",
|
|
31
34
|
shasum: dist?.shasum || parsed?.["dist.shasum"] || "",
|
|
32
35
|
};
|
|
36
|
+
if (!result.integrity && !result.shasum)
|
|
37
|
+
throw new Error("npm view omitted the exact package integrity");
|
|
38
|
+
return result;
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
export function publishedDigest({ cwd, name, version, registry, env }) {
|
|
@@ -54,8 +60,5 @@ export function publishedDigest({ cwd, name, version, registry, env }) {
|
|
|
54
60
|
throw new Error(`npm view ${name}@${version} failed\n${output}`.trim());
|
|
55
61
|
}
|
|
56
62
|
const view = parseNpmView(result.stdout);
|
|
57
|
-
if (!view) {
|
|
58
|
-
return undefined;
|
|
59
|
-
}
|
|
60
63
|
return view.integrity || (view.shasum ? `sha1:${view.shasum}` : "");
|
|
61
64
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { pipelineHost } from "../../workflow/pipeline/host.js";
|
|
3
|
+
import { buildPipelineSource } from "../../workflow/pipeline/build.js";
|
|
4
|
+
import { githubPipelinePublicationArtifacts } from "../../providers/github/pipeline-publication-artifacts.js";
|
|
5
|
+
import { preparePipelinePublication } from "./prepare.js";
|
|
6
|
+
import { packPipelineProducts } from "./pack.js";
|
|
7
|
+
import {
|
|
8
|
+
preparePipelineQualification,
|
|
9
|
+
sealPipelineQualification,
|
|
10
|
+
} from "./qualify.js";
|
|
11
|
+
import { applyPipelinePublication } from "./apply.js";
|
|
12
|
+
import { settlePipelinePublication } from "./settle.js";
|
|
13
|
+
import { githubJsonClient } from "../../providers/github/json-client.js";
|
|
14
|
+
|
|
15
|
+
const contextInput = (core) =>
|
|
16
|
+
JSON.parse(core.getInput("context", { required: true }));
|
|
17
|
+
const directory = (env) =>
|
|
18
|
+
path.join(env.GITHUB_WORKSPACE, ".buildchain/publication");
|
|
19
|
+
|
|
20
|
+
export async function preparePipelinePublicationAction(core, env) {
|
|
21
|
+
const host = await pipelineHost(core, env, "Prepare publication");
|
|
22
|
+
const result = await preparePipelinePublication(
|
|
23
|
+
core.getInput("attempt", { required: true }),
|
|
24
|
+
core.getInput("definition-sha", { required: true }),
|
|
25
|
+
host,
|
|
26
|
+
);
|
|
27
|
+
core.setOutput("operation", result.operation);
|
|
28
|
+
core.setOutput("context", JSON.stringify(result.context || {}));
|
|
29
|
+
core.setOutput(
|
|
30
|
+
"matrix",
|
|
31
|
+
JSON.stringify({ include: result.context?.platforms || [] }),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function buildPipelinePublicationAction(core, env) {
|
|
36
|
+
const context = contextInput(core);
|
|
37
|
+
const platform = core.getInput("platform", { required: true });
|
|
38
|
+
const cwd = path.join(env.GITHUB_WORKSPACE, ".buildchain/product");
|
|
39
|
+
await buildPipelineSource({
|
|
40
|
+
cwd,
|
|
41
|
+
source: context.materialization.source,
|
|
42
|
+
platform,
|
|
43
|
+
environment: env,
|
|
44
|
+
});
|
|
45
|
+
const output = path.join(directory(env), "products");
|
|
46
|
+
const manifest = packPipelineProducts({
|
|
47
|
+
cwd,
|
|
48
|
+
output,
|
|
49
|
+
plan: context.plan,
|
|
50
|
+
platform,
|
|
51
|
+
source: context.materialization.source,
|
|
52
|
+
environment: env,
|
|
53
|
+
});
|
|
54
|
+
const artifacts = githubPipelinePublicationArtifacts({});
|
|
55
|
+
await artifacts.upload(context.plan, manifest, output);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function preparePipelineQualificationAction(core, env) {
|
|
59
|
+
const host = await pipelineHost(core, env, "Qualify and sign products");
|
|
60
|
+
const prepared = await preparePipelineQualification(
|
|
61
|
+
contextInput(core),
|
|
62
|
+
host,
|
|
63
|
+
directory(env),
|
|
64
|
+
);
|
|
65
|
+
core.setOutput("subject-path", prepared.subjectPath);
|
|
66
|
+
core.setOutput("predicate-path", prepared.predicatePath);
|
|
67
|
+
core.setOutput("predicate-type", prepared.predicateType);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function sealPipelineQualificationAction(core, env) {
|
|
71
|
+
const host = await pipelineHost(core, env, "Qualify and sign products");
|
|
72
|
+
await sealPipelineQualification(
|
|
73
|
+
contextInput(core),
|
|
74
|
+
host,
|
|
75
|
+
directory(env),
|
|
76
|
+
core.getInput("bundle-path", { required: true }),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function applyPipelinePublicationAction(core, env) {
|
|
81
|
+
const host = await pipelineHost(core, env, "Apply qualified publication");
|
|
82
|
+
await applyPipelinePublication(contextInput(core), host, directory(env), env);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function settlePipelinePublicationAction(core, env) {
|
|
86
|
+
const host = await pipelineHost(core, env, "Settle product follow-ups");
|
|
87
|
+
const prToken = core.getInput("pr-token");
|
|
88
|
+
if (prToken && prToken !== host.token)
|
|
89
|
+
host.pullRequests = githubJsonClient({
|
|
90
|
+
token: prToken,
|
|
91
|
+
userAgent: "buildchain-next-development",
|
|
92
|
+
});
|
|
93
|
+
const result = await settlePipelinePublication(
|
|
94
|
+
contextInput(core),
|
|
95
|
+
host,
|
|
96
|
+
directory(env),
|
|
97
|
+
env,
|
|
98
|
+
);
|
|
99
|
+
core.info(result.reason);
|
|
100
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
5
|
+
import { canonicalJson } from "../../release/discussion/envelope.js";
|
|
6
|
+
import {
|
|
7
|
+
createReleaseReceipt,
|
|
8
|
+
RELEASE_RECEIPT_CONTRACT,
|
|
9
|
+
} from "../../release/release-invocation.js";
|
|
10
|
+
import { githubPipelineProductRelease } from "../../providers/github/pipeline-product-release.js";
|
|
11
|
+
import { pipelineNpmProvider } from "../npm/pipeline-provider.js";
|
|
12
|
+
import { publicationContext, uniquePublicationMaterial } from "./context.js";
|
|
13
|
+
import { restorePipelineProducts } from "./sealed-products.js";
|
|
14
|
+
import { pipelineReleaseDocuments } from "./documents.js";
|
|
15
|
+
import { writeImmutablePublicationFile } from "./files.js";
|
|
16
|
+
import { verifyPipelineSigning } from "./signing.js";
|
|
17
|
+
import { pipelinePublicationEffects, applyPipelineEffects } from "./effects.js";
|
|
18
|
+
import { observeRecoveryPublicationEffects } from "./recovery-readback.js";
|
|
19
|
+
import {
|
|
20
|
+
createRecoveryPublicationAdmission,
|
|
21
|
+
verifyRecoveryPublicationAdmission,
|
|
22
|
+
} from "./recovery-admission.js";
|
|
23
|
+
|
|
24
|
+
export async function applyPipelinePublication(
|
|
25
|
+
context,
|
|
26
|
+
host,
|
|
27
|
+
directory,
|
|
28
|
+
environment,
|
|
29
|
+
{
|
|
30
|
+
verifySigning = verifyPipelineSigning,
|
|
31
|
+
npmProvider = pipelineNpmProvider,
|
|
32
|
+
} = {},
|
|
33
|
+
) {
|
|
34
|
+
const { journal, archive } = await publicationContext(context, host);
|
|
35
|
+
const retained = await uniquePublicationMaterial(
|
|
36
|
+
journal,
|
|
37
|
+
"publication/qualified/",
|
|
38
|
+
);
|
|
39
|
+
const { plan, materialization } = context;
|
|
40
|
+
const { qualified, capsules, sealed } = retained;
|
|
41
|
+
const evaluatedAt = context.recovery?.preserveTransaction
|
|
42
|
+
? qualified.qualification.issuedAt
|
|
43
|
+
: undefined;
|
|
44
|
+
const products = await restorePipelineProducts(
|
|
45
|
+
archive,
|
|
46
|
+
qualified,
|
|
47
|
+
sealed,
|
|
48
|
+
path.join(directory, "sealed-products"),
|
|
49
|
+
);
|
|
50
|
+
const bundlePath = writeImmutablePublicationFile(
|
|
51
|
+
path.join(directory, "attestation.json"),
|
|
52
|
+
await archive.read(retained.bundle),
|
|
53
|
+
);
|
|
54
|
+
const signing = verifySigning({
|
|
55
|
+
plan,
|
|
56
|
+
materialization,
|
|
57
|
+
qualified,
|
|
58
|
+
bundlePath,
|
|
59
|
+
directory: path.join(directory, "signing-readback"),
|
|
60
|
+
token: host.token,
|
|
61
|
+
evaluatedAt,
|
|
62
|
+
});
|
|
63
|
+
const documents = pipelineReleaseDocuments({
|
|
64
|
+
plan,
|
|
65
|
+
materialization,
|
|
66
|
+
qualified,
|
|
67
|
+
capsules,
|
|
68
|
+
signing,
|
|
69
|
+
evaluatedAt,
|
|
70
|
+
});
|
|
71
|
+
if (
|
|
72
|
+
recordDigest(documents) !== recordDigest(retained.documents) ||
|
|
73
|
+
recordDigest(signing) !== recordDigest(retained.signing)
|
|
74
|
+
)
|
|
75
|
+
throw new Error(
|
|
76
|
+
"Retained release documents changed during publisher admission",
|
|
77
|
+
);
|
|
78
|
+
const npm = npmProvider({
|
|
79
|
+
directory: products,
|
|
80
|
+
artifacts: qualified.artifacts,
|
|
81
|
+
environment,
|
|
82
|
+
});
|
|
83
|
+
const evidence = Object.entries({
|
|
84
|
+
qualification: Buffer.from(`${canonicalJson(qualified)}\n`),
|
|
85
|
+
capsules: Buffer.from(`${canonicalJson(capsules)}\n`),
|
|
86
|
+
invocation: Buffer.from(
|
|
87
|
+
`${canonicalJson(documents.invocation.invocation)}\n`,
|
|
88
|
+
),
|
|
89
|
+
attestation: fs.readFileSync(bundlePath),
|
|
90
|
+
}).map(([id, bytes]) => ({
|
|
91
|
+
id,
|
|
92
|
+
name: `buildchain.${id}.json`,
|
|
93
|
+
bytes,
|
|
94
|
+
size: bytes.length,
|
|
95
|
+
digest: `sha256:${createHash("sha256").update(bytes).digest("hex")}`,
|
|
96
|
+
}));
|
|
97
|
+
const github = githubPipelineProductRelease({
|
|
98
|
+
...host,
|
|
99
|
+
plan,
|
|
100
|
+
qualified,
|
|
101
|
+
documents,
|
|
102
|
+
directory: products,
|
|
103
|
+
evidence,
|
|
104
|
+
});
|
|
105
|
+
const providerFor = (effect) =>
|
|
106
|
+
effect.kind === "npm-package" ? npm : github;
|
|
107
|
+
const provider = {
|
|
108
|
+
observe: (effect) => providerFor(effect).observe(effect),
|
|
109
|
+
matches: (effect, value) => providerFor(effect).matches(effect, value),
|
|
110
|
+
apply: (effect) => providerFor(effect).apply(effect),
|
|
111
|
+
};
|
|
112
|
+
const effects = pipelinePublicationEffects({
|
|
113
|
+
plan,
|
|
114
|
+
qualified,
|
|
115
|
+
documents,
|
|
116
|
+
evidence: evidence.map(({ bytes, ...asset }) => asset),
|
|
117
|
+
});
|
|
118
|
+
const receipts = await journal.materials("publication/effect/");
|
|
119
|
+
let fence = journal.fence;
|
|
120
|
+
if (context.recovery?.preserveTransaction) {
|
|
121
|
+
const readback = await observeRecoveryPublicationEffects({
|
|
122
|
+
effects,
|
|
123
|
+
receipts,
|
|
124
|
+
retained,
|
|
125
|
+
provider,
|
|
126
|
+
fence,
|
|
127
|
+
});
|
|
128
|
+
const input = {
|
|
129
|
+
context,
|
|
130
|
+
retained,
|
|
131
|
+
execution: context.recovery.execution,
|
|
132
|
+
readback,
|
|
133
|
+
};
|
|
134
|
+
const admission = createRecoveryPublicationAdmission(input);
|
|
135
|
+
await journal.record("publication/recovery-authorization", {
|
|
136
|
+
admission,
|
|
137
|
+
readback,
|
|
138
|
+
});
|
|
139
|
+
fence = async () => {
|
|
140
|
+
await journal.fence();
|
|
141
|
+
verifyRecoveryPublicationAdmission(admission, input);
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const results = await applyPipelineEffects({
|
|
145
|
+
effects,
|
|
146
|
+
transactionRoot: documents.transaction.transactionRoot,
|
|
147
|
+
receipts,
|
|
148
|
+
provider,
|
|
149
|
+
fence,
|
|
150
|
+
retain: (receipt) => journal.record("publication/effect", receipt),
|
|
151
|
+
});
|
|
152
|
+
const receipt = createReleaseReceipt({
|
|
153
|
+
schema: RELEASE_RECEIPT_CONTRACT,
|
|
154
|
+
transactionRoot: documents.transaction.transactionRoot,
|
|
155
|
+
outcome: "complete",
|
|
156
|
+
releasePassportRoot: documents.passport.passportRoot,
|
|
157
|
+
providerTransactionRoot: recordDigest(effects),
|
|
158
|
+
providerStateRoot: recordDigest(results),
|
|
159
|
+
providerReceiptRoots: [...new Set(results.map(({ root }) => root))].sort(),
|
|
160
|
+
});
|
|
161
|
+
// This is publication success only. Distribution and next-development remain
|
|
162
|
+
// separate mandatory nodes of the same business attempt.
|
|
163
|
+
let complete = {
|
|
164
|
+
schema: "buildchain.pipeline-publication-complete/v1",
|
|
165
|
+
release: receipt,
|
|
166
|
+
completedAt: new Date().toISOString(),
|
|
167
|
+
};
|
|
168
|
+
const originals = await journal.materials(
|
|
169
|
+
"publication/predecessor-complete/",
|
|
170
|
+
);
|
|
171
|
+
if (
|
|
172
|
+
originals.length > 1 ||
|
|
173
|
+
(originals.length &&
|
|
174
|
+
recordDigest(originals[0].release) !== recordDigest(receipt))
|
|
175
|
+
)
|
|
176
|
+
throw new Error(
|
|
177
|
+
"Recovery cannot replace the original completed publication receipt",
|
|
178
|
+
);
|
|
179
|
+
if (originals.length) complete = originals[0];
|
|
180
|
+
await journal.record("publication/complete", complete, { state: "success" });
|
|
181
|
+
return complete;
|
|
182
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
import { assertRecoveryProductContract } from "./recovery-build-plan.js";
|
|
3
|
+
|
|
4
|
+
export const PUBLICATION_BUILD_AGGREGATE =
|
|
5
|
+
"buildchain.pipeline-publication-build-qualification/v1";
|
|
6
|
+
|
|
7
|
+
export function qualifyPublicationBuildSegments(
|
|
8
|
+
context,
|
|
9
|
+
segments,
|
|
10
|
+
providerSource,
|
|
11
|
+
) {
|
|
12
|
+
if (!/^[0-9a-f]{40}$/u.test(providerSource || ""))
|
|
13
|
+
throw new Error(
|
|
14
|
+
"Publication qualification requires the actual current signing execution source",
|
|
15
|
+
);
|
|
16
|
+
if (!segments.length || segments.length > 100)
|
|
17
|
+
throw new Error("Publication build segments exceed their exact bound");
|
|
18
|
+
const platforms = [],
|
|
19
|
+
artifactIds = [];
|
|
20
|
+
for (const segment of segments) {
|
|
21
|
+
const { build, plan, materialization } = segment;
|
|
22
|
+
assertRecoveryProductContract(
|
|
23
|
+
plan,
|
|
24
|
+
context.plan,
|
|
25
|
+
materialization.source,
|
|
26
|
+
context.materialization,
|
|
27
|
+
);
|
|
28
|
+
const { root, ...body } = build;
|
|
29
|
+
if (
|
|
30
|
+
root !== recordDigest(body) ||
|
|
31
|
+
build.schema !== "buildchain.pipeline-publication-build-readback/v1" ||
|
|
32
|
+
build.outcome !== "success" ||
|
|
33
|
+
build.planRoot !== plan.root ||
|
|
34
|
+
recordDigest(build.source) !== recordDigest(materialization.source) ||
|
|
35
|
+
build.platforms.length !== build.jobs.length ||
|
|
36
|
+
build.platforms.length !== build.artifactIds.length
|
|
37
|
+
)
|
|
38
|
+
throw new Error(
|
|
39
|
+
"Publication segment lacks exact independently observed build evidence",
|
|
40
|
+
);
|
|
41
|
+
for (const [index, platform] of build.platforms.entries()) {
|
|
42
|
+
const job = build.jobs[index],
|
|
43
|
+
name = `Build publication (${platform})`;
|
|
44
|
+
if (
|
|
45
|
+
job.run_id !== build.runId ||
|
|
46
|
+
job.run_attempt !== build.runAttempt ||
|
|
47
|
+
job.status !== "completed" ||
|
|
48
|
+
job.conclusion !== "success" ||
|
|
49
|
+
!(job.name === name || job.name.endsWith(` / ${name}`))
|
|
50
|
+
)
|
|
51
|
+
throw new Error("Publication segment producer job or platform changed");
|
|
52
|
+
}
|
|
53
|
+
platforms.push(...build.platforms);
|
|
54
|
+
artifactIds.push(...build.artifactIds);
|
|
55
|
+
}
|
|
56
|
+
const expected = [
|
|
57
|
+
...new Set(context.plan.outputs.map(({ platform }) => platform)),
|
|
58
|
+
].sort();
|
|
59
|
+
if (
|
|
60
|
+
recordDigest([...platforms].sort()) !== recordDigest(expected) ||
|
|
61
|
+
new Set(artifactIds).size !== artifactIds.length
|
|
62
|
+
)
|
|
63
|
+
throw new Error(
|
|
64
|
+
"Publication segments must cover each declared platform and artifact exactly once",
|
|
65
|
+
);
|
|
66
|
+
const body = {
|
|
67
|
+
schema: PUBLICATION_BUILD_AGGREGATE,
|
|
68
|
+
operation: "requalify-original-product-segments",
|
|
69
|
+
outcome: "success",
|
|
70
|
+
planRoot: context.plan.root,
|
|
71
|
+
source: context.materialization.source,
|
|
72
|
+
providerSource,
|
|
73
|
+
runId: context.runId,
|
|
74
|
+
runAttempt: context.runAttempt,
|
|
75
|
+
platforms: expected,
|
|
76
|
+
artifactIds: artifactIds.sort((a, b) => a - b),
|
|
77
|
+
segments,
|
|
78
|
+
};
|
|
79
|
+
return { ...body, root: recordDigest(body) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function publicationArtifactProducer(build, artifactId) {
|
|
83
|
+
if (build.schema !== PUBLICATION_BUILD_AGGREGATE)
|
|
84
|
+
return { build, plan: null };
|
|
85
|
+
const selected = build.segments.filter((segment) =>
|
|
86
|
+
segment.build.artifactIds.includes(artifactId),
|
|
87
|
+
);
|
|
88
|
+
if (selected.length !== 1)
|
|
89
|
+
throw new Error("Publication artifact has no unique original producer");
|
|
90
|
+
return selected[0];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function verifyPublicationBuildAggregate(build, context) {
|
|
94
|
+
if (
|
|
95
|
+
recordDigest(build) !==
|
|
96
|
+
recordDigest(
|
|
97
|
+
qualifyPublicationBuildSegments(
|
|
98
|
+
context,
|
|
99
|
+
build.segments,
|
|
100
|
+
build.providerSource,
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
throw new Error(
|
|
105
|
+
"Publication aggregate changed its exact source or original segments",
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { publicationArtifactProducer } from "./build-segments.js";
|
|
2
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
3
|
+
import {
|
|
4
|
+
STAGE_CAPSULE_CONTRACT,
|
|
5
|
+
STAGE_CAPSULE_IDENTITY_CONTRACT,
|
|
6
|
+
stageCapsuleIdentityRoot,
|
|
7
|
+
stageCapsuleRoot,
|
|
8
|
+
validateStageCapsule,
|
|
9
|
+
} from "../../build/stage-capsule.js";
|
|
10
|
+
import { verifyPipelineQualification } from "./documents.js";
|
|
11
|
+
|
|
12
|
+
export function pipelineProductCapsules({
|
|
13
|
+
plan,
|
|
14
|
+
materialization,
|
|
15
|
+
qualified,
|
|
16
|
+
bundles,
|
|
17
|
+
evaluatedAt,
|
|
18
|
+
}) {
|
|
19
|
+
verifyPipelineQualification({
|
|
20
|
+
plan,
|
|
21
|
+
materialization,
|
|
22
|
+
qualified,
|
|
23
|
+
evaluatedAt,
|
|
24
|
+
});
|
|
25
|
+
const capsules = qualified.artifacts.map((artifact) => {
|
|
26
|
+
const bundle = bundles.find(
|
|
27
|
+
({ providerArtifact }) =>
|
|
28
|
+
providerArtifact.id === artifact.providerArtifactId,
|
|
29
|
+
);
|
|
30
|
+
if (
|
|
31
|
+
!bundle ||
|
|
32
|
+
!bundle.providerArtifact.expires_at ||
|
|
33
|
+
Date.parse(bundle.providerArtifact.expires_at) <=
|
|
34
|
+
Date.parse(qualified.qualification.issuedAt)
|
|
35
|
+
)
|
|
36
|
+
throw new Error(
|
|
37
|
+
"Product Capsule requires a live provider retention promise",
|
|
38
|
+
);
|
|
39
|
+
const producer = publicationArtifactProducer(
|
|
40
|
+
qualified.build,
|
|
41
|
+
artifact.providerArtifactId,
|
|
42
|
+
);
|
|
43
|
+
const identity = {
|
|
44
|
+
schema: STAGE_CAPSULE_IDENTITY_CONTRACT,
|
|
45
|
+
sourceRoot: recordDigest(qualified.source),
|
|
46
|
+
platform: artifact.platform,
|
|
47
|
+
platformRoot: recordDigest({ platform: artifact.platform }),
|
|
48
|
+
stage: "verify",
|
|
49
|
+
toolchainRoots: [],
|
|
50
|
+
runtimeRoot: recordDigest(producer.plan?.runtime || plan.runtime),
|
|
51
|
+
policyRoot: plan.contractRoot,
|
|
52
|
+
declaredInputs: [
|
|
53
|
+
{ name: "publication-plan", root: producer.plan?.root || plan.root },
|
|
54
|
+
],
|
|
55
|
+
transformationRoot: recordDigest({
|
|
56
|
+
source: qualified.source,
|
|
57
|
+
product: artifact.product,
|
|
58
|
+
artifact: artifact.id,
|
|
59
|
+
kind: artifact.kind,
|
|
60
|
+
}),
|
|
61
|
+
outputManifestRoot: artifact.manifestRoot,
|
|
62
|
+
qualificationRoot: qualified.qualification.receiptRoot,
|
|
63
|
+
observationRoots: [
|
|
64
|
+
{ name: "provider-build", root: qualified.build.root },
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
const capsule = {
|
|
68
|
+
schema: STAGE_CAPSULE_CONTRACT,
|
|
69
|
+
writerAuthority: "typescript-v3",
|
|
70
|
+
rustAuthority: "validation-only",
|
|
71
|
+
identity,
|
|
72
|
+
identityRoot: stageCapsuleIdentityRoot(identity),
|
|
73
|
+
retentionPromise: {
|
|
74
|
+
class: "github-artifact",
|
|
75
|
+
retainUntil: bundle.providerArtifact.expires_at,
|
|
76
|
+
},
|
|
77
|
+
capsuleRoot: `sha256:${"0".repeat(64)}`,
|
|
78
|
+
};
|
|
79
|
+
capsule.capsuleRoot = stageCapsuleRoot(capsule);
|
|
80
|
+
validateStageCapsule(capsule);
|
|
81
|
+
return { id: artifact.id, artifact, capsule };
|
|
82
|
+
});
|
|
83
|
+
const body = {
|
|
84
|
+
schema: "buildchain.pipeline-product-capsules/v1",
|
|
85
|
+
qualificationRoot: qualified.root,
|
|
86
|
+
capsules,
|
|
87
|
+
};
|
|
88
|
+
return { ...body, root: recordDigest(body) };
|
|
89
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
import { resumePipelineSession } from "../../workflow/pipeline/session.js";
|
|
3
|
+
import { pipelinePublicationJournal } from "./journal.js";
|
|
4
|
+
|
|
5
|
+
export async function publicationContext(context, host) {
|
|
6
|
+
if (
|
|
7
|
+
context.schema !== "buildchain.pipeline-publication-context/v1" ||
|
|
8
|
+
context.runId !== host.runId ||
|
|
9
|
+
context.runAttempt !== host.runAttempt
|
|
10
|
+
)
|
|
11
|
+
throw new Error(
|
|
12
|
+
"Publication context is not from this exact provider execution",
|
|
13
|
+
);
|
|
14
|
+
const session = await resumePipelineSession(
|
|
15
|
+
{ ...host, attempt: context.attempt },
|
|
16
|
+
host,
|
|
17
|
+
);
|
|
18
|
+
if (session.observed.generation !== context.generation)
|
|
19
|
+
throw new Error(
|
|
20
|
+
"Publication context belongs to a superseded source generation",
|
|
21
|
+
);
|
|
22
|
+
const journal = pipelinePublicationJournal(session, host);
|
|
23
|
+
await journal.fence();
|
|
24
|
+
const contexts = await journal.materials("publication/context/");
|
|
25
|
+
if (!contexts.some((value) => recordDigest(value) === recordDigest(context)))
|
|
26
|
+
throw new Error(
|
|
27
|
+
"Publication context is not retained in the authoritative attempt",
|
|
28
|
+
);
|
|
29
|
+
return { session, journal, archive: host.productArchive(session) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function uniquePublicationMaterial(journal, prefix) {
|
|
33
|
+
const values = await journal.materials(prefix);
|
|
34
|
+
if (values.length !== 1)
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Publication requires one exact retained material: ${prefix}`,
|
|
37
|
+
);
|
|
38
|
+
return values[0];
|
|
39
|
+
}
|