@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,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
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
|
|
3
|
+
const TERMINAL = new Set([
|
|
4
|
+
"merged",
|
|
5
|
+
"terminal-failure",
|
|
6
|
+
"dequeued",
|
|
7
|
+
"cancelled",
|
|
8
|
+
]);
|
|
9
|
+
const wait = (reason) => ({ operation: "wait", reason });
|
|
10
|
+
|
|
11
|
+
export function terminalPipelineCandidate(candidate) {
|
|
12
|
+
return Boolean(candidate && TERMINAL.has(candidate.status));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function pipelineCandidateRoot(current) {
|
|
16
|
+
if (
|
|
17
|
+
!current.identity?.id ||
|
|
18
|
+
current.identity.generation !== current.generation.id
|
|
19
|
+
)
|
|
20
|
+
throw new Error(
|
|
21
|
+
"Pipeline candidate requires its exact business attempt identity",
|
|
22
|
+
);
|
|
23
|
+
return recordDigest({
|
|
24
|
+
schema: "buildchain.pipeline-candidate-source/v1",
|
|
25
|
+
intent: current.intent,
|
|
26
|
+
attempt: current.identity,
|
|
27
|
+
generation: current.generation,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function pipelineCandidate(queue, current) {
|
|
32
|
+
const source = current.generation.source;
|
|
33
|
+
const number = current.intent.source.pullRequest;
|
|
34
|
+
const matches = queue.candidates.filter(
|
|
35
|
+
(candidate) =>
|
|
36
|
+
candidate.pullRequestNumber === number &&
|
|
37
|
+
candidate.sourceHead === source.commit &&
|
|
38
|
+
candidate.sourceRoot === pipelineCandidateRoot(current),
|
|
39
|
+
);
|
|
40
|
+
const active = matches.find(
|
|
41
|
+
(candidate) => candidate.candidateId === queue.activeWarrant?.candidateId,
|
|
42
|
+
);
|
|
43
|
+
return active || matches.at(-1) || null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function cancellation({
|
|
47
|
+
candidate,
|
|
48
|
+
queue,
|
|
49
|
+
worker,
|
|
50
|
+
reason,
|
|
51
|
+
observedSourceHead,
|
|
52
|
+
}) {
|
|
53
|
+
if (candidate && ["cancelled", "dequeued"].includes(candidate.status))
|
|
54
|
+
return {
|
|
55
|
+
operation: "record-cancellation",
|
|
56
|
+
reason,
|
|
57
|
+
candidateId: candidate.candidateId,
|
|
58
|
+
};
|
|
59
|
+
if (!candidate || TERMINAL.has(candidate.status))
|
|
60
|
+
return { operation: "supersede", reason };
|
|
61
|
+
if (candidate.candidateId !== queue.activeWarrant?.candidateId) {
|
|
62
|
+
if (candidate.status !== "queued") return wait("unfenced-candidate-state");
|
|
63
|
+
return {
|
|
64
|
+
operation: "cancel-queued",
|
|
65
|
+
reason,
|
|
66
|
+
candidateId: candidate.candidateId,
|
|
67
|
+
expectedSourceHead: candidate.sourceHead,
|
|
68
|
+
observedSourceHead,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// Expiration is not evidence that the old native worker stopped. Never
|
|
72
|
+
// release or transfer an active Warrant until the provider proves terminality.
|
|
73
|
+
if (!worker || worker.status !== "completed")
|
|
74
|
+
return {
|
|
75
|
+
operation: "stop-worker",
|
|
76
|
+
reason: "active-worker-not-terminal",
|
|
77
|
+
candidateId: candidate.candidateId,
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
operation: "settle-cancelled",
|
|
81
|
+
reason,
|
|
82
|
+
candidateId: candidate.candidateId,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function sameSource(current, live) {
|
|
87
|
+
if (!live.source) return false;
|
|
88
|
+
return recordDigest(current.generation.source) === recordDigest(live.source);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function mergedDelivery(candidate, live, evidence) {
|
|
92
|
+
if (!candidate) return wait("merged-candidate-authority-required");
|
|
93
|
+
if (
|
|
94
|
+
!evidence.integration ||
|
|
95
|
+
evidence.integration.sourceHead !== live.source.commit
|
|
96
|
+
)
|
|
97
|
+
return wait("exact-protected-integration-proof-required");
|
|
98
|
+
if (candidate.status !== "merged")
|
|
99
|
+
return { operation: "settle-merged", reason: "protected-merge-observed" };
|
|
100
|
+
if (
|
|
101
|
+
!evidence.settlement ||
|
|
102
|
+
evidence.settlement.candidateId !== candidate.candidateId
|
|
103
|
+
)
|
|
104
|
+
return wait("terminal-settlement-readback-required");
|
|
105
|
+
return { operation: "wake-publication", reason: "delivery-terminal" };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// This pure planner selects the next domain transaction. Its booleans are
|
|
109
|
+
// authenticated provider readbacks at the adapter boundary, never workflow inputs.
|
|
110
|
+
// A selected operation is not a receipt or permission to skip that domain's gates.
|
|
111
|
+
export function reconcilePipeline({ current, live, queue, worker, evidence }) {
|
|
112
|
+
if (
|
|
113
|
+
queue.repository !== current.intent.repository ||
|
|
114
|
+
queue.protectedBase !== current.intent.source.targetBranch ||
|
|
115
|
+
live.pullRequest !== current.intent.source.pullRequest
|
|
116
|
+
)
|
|
117
|
+
throw new Error("Pipeline observation belongs to another intent");
|
|
118
|
+
const candidate = pipelineCandidate(queue, current);
|
|
119
|
+
const cancellationInput = {
|
|
120
|
+
candidate,
|
|
121
|
+
queue,
|
|
122
|
+
worker,
|
|
123
|
+
observedSourceHead: live.observedHead || live.source?.commit,
|
|
124
|
+
};
|
|
125
|
+
if (live.state === "closed" && !live.merged)
|
|
126
|
+
return cancellation({
|
|
127
|
+
...cancellationInput,
|
|
128
|
+
reason: "pull-request-closed",
|
|
129
|
+
});
|
|
130
|
+
if (
|
|
131
|
+
live.targetBranch &&
|
|
132
|
+
live.targetBranch !== current.intent.source.targetBranch
|
|
133
|
+
)
|
|
134
|
+
return cancellation({
|
|
135
|
+
...cancellationInput,
|
|
136
|
+
reason: "target-branch-changed",
|
|
137
|
+
});
|
|
138
|
+
if (live.routeEnabled === false)
|
|
139
|
+
return cancellation({
|
|
140
|
+
...cancellationInput,
|
|
141
|
+
reason: "channel-policy-changed",
|
|
142
|
+
});
|
|
143
|
+
if (!sameSource(current, live))
|
|
144
|
+
return cancellation({
|
|
145
|
+
...cancellationInput,
|
|
146
|
+
reason: "source-generation-changed",
|
|
147
|
+
});
|
|
148
|
+
if (live.merged) return mergedDelivery(candidate, live, evidence);
|
|
149
|
+
if (current.generation.baseCommit !== live.baseCommit)
|
|
150
|
+
return cancellation({
|
|
151
|
+
...cancellationInput,
|
|
152
|
+
reason: "protected-base-advanced",
|
|
153
|
+
});
|
|
154
|
+
if (live.dequeued === true)
|
|
155
|
+
return cancellation({
|
|
156
|
+
...cancellationInput,
|
|
157
|
+
reason: "pull-request-dequeued",
|
|
158
|
+
});
|
|
159
|
+
return openDelivery({ candidate, live, queue, worker, evidence });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function openDelivery({ candidate, live, queue, worker, evidence }) {
|
|
163
|
+
if (live.state !== "open") return wait("unknown-provider-state");
|
|
164
|
+
if (live.terminalOnly) return wait("terminal-event-cannot-start-execution");
|
|
165
|
+
if (candidate && TERMINAL.has(candidate.status))
|
|
166
|
+
return wait("successor-attempt-required");
|
|
167
|
+
if (!evidence.build)
|
|
168
|
+
return { operation: "build", reason: "source-needs-qualification" };
|
|
169
|
+
if (evidence.build.sourceHead !== live.source.commit)
|
|
170
|
+
throw new Error("Build evidence belongs to another source");
|
|
171
|
+
if (evidence.build.outcome !== "success") {
|
|
172
|
+
if (candidate && candidate.candidateId === queue.activeWarrant?.candidateId)
|
|
173
|
+
return worker?.status === "completed"
|
|
174
|
+
? { operation: "settle-failure", reason: "native-execution-failed" }
|
|
175
|
+
: { operation: "stop-worker", reason: "failed-worker-not-terminal" };
|
|
176
|
+
return wait("build-failed");
|
|
177
|
+
}
|
|
178
|
+
if (!live.ready || live.draft) return wait("ready-review-required");
|
|
179
|
+
if (!evidence.review) return wait("independent-protected-review-required");
|
|
180
|
+
if (!evidence.checks) return wait("exact-required-checks-pending");
|
|
181
|
+
if (!candidate) return { operation: "reserve", reason: "source-admitted" };
|
|
182
|
+
if (candidate.candidateId !== queue.activeWarrant?.candidateId)
|
|
183
|
+
return queue.activeWarrant
|
|
184
|
+
? wait("another-candidate-active")
|
|
185
|
+
: { operation: "select", reason: "candidate-queued" };
|
|
186
|
+
if (queue.activeWarrant.phase === "provisional")
|
|
187
|
+
return {
|
|
188
|
+
operation: "qualify-native",
|
|
189
|
+
reason: "independent-native-proof-required",
|
|
190
|
+
};
|
|
191
|
+
if (!evidence.qualification)
|
|
192
|
+
return wait("qualified-warrant-readback-required");
|
|
193
|
+
return { operation: "land", reason: "protected-queue-admission" };
|
|
194
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { recoveryInputs } from "../../consumer/contract/entries.js";
|
|
2
|
+
import { pipelineHost } from "./host.js";
|
|
3
|
+
import { controlPipelineRecovery } from "./recovery-controller.js";
|
|
4
|
+
import { pipelineActionOutputs } from "./action-output.js";
|
|
5
|
+
import { resumePipelineSession } from "./session.js";
|
|
6
|
+
|
|
7
|
+
export async function recoverPipelineAction(core, env) {
|
|
8
|
+
const { attempt } = recoveryInputs({
|
|
9
|
+
attempt: core.getInput("attempt", { required: true }),
|
|
10
|
+
});
|
|
11
|
+
const host = await pipelineHost(core, env, "Buildchain recovery controller");
|
|
12
|
+
try {
|
|
13
|
+
const result = await controlPipelineRecovery(
|
|
14
|
+
attempt,
|
|
15
|
+
core.getInput("definition-sha", { required: true }),
|
|
16
|
+
host,
|
|
17
|
+
);
|
|
18
|
+
pipelineActionOutputs(core, result);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
try {
|
|
21
|
+
const session = await resumePipelineSession({ ...host, attempt }, host);
|
|
22
|
+
await host.project(session);
|
|
23
|
+
} catch {
|
|
24
|
+
core.warning(
|
|
25
|
+
"Recovery status projection is unavailable; the canonical attempt remains authoritative.",
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|