@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,805 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
|
|
29
|
+
// packages/core/workflow/attempt/reader-entry.js
|
|
30
|
+
var reader_entry_exports = {};
|
|
31
|
+
__export(reader_entry_exports, {
|
|
32
|
+
readBusinessAttempt: () => readBusinessAttempt
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(reader_entry_exports);
|
|
35
|
+
var import_node_fs = __toESM(require("fs"), 1);
|
|
36
|
+
|
|
37
|
+
// packages/core/release/discussion/envelope.js
|
|
38
|
+
var import_node_crypto = require("crypto");
|
|
39
|
+
var ENVELOPE_SCHEMA = "buildchain.discussion-record/v1";
|
|
40
|
+
var INTENT_SCHEMA = "buildchain.release-discussion/v1";
|
|
41
|
+
var PAYLOAD_SCHEMA = "buildchain.release-progress/v1";
|
|
42
|
+
var MARKER = "<!-- buildchain-transaction\n";
|
|
43
|
+
var MAX_RECORD_BYTES = 48e3;
|
|
44
|
+
function canonicalJson(value) {
|
|
45
|
+
if (typeof value === "number" && !Number.isFinite(value))
|
|
46
|
+
throw new Error("Transaction numbers must be finite");
|
|
47
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
48
|
+
if (value && typeof value === "object")
|
|
49
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
|
|
50
|
+
const encoded = JSON.stringify(value);
|
|
51
|
+
if (encoded === void 0)
|
|
52
|
+
throw new Error("Transaction records must contain JSON values");
|
|
53
|
+
return encoded;
|
|
54
|
+
}
|
|
55
|
+
function recordDigest(value) {
|
|
56
|
+
return `sha256:${(0, import_node_crypto.createHash)("sha256").update(canonicalJson(value)).digest("hex")}`;
|
|
57
|
+
}
|
|
58
|
+
function requireString(value, label) {
|
|
59
|
+
if (typeof value !== "string" || !value.trim())
|
|
60
|
+
throw new Error(`${label} is required`);
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
function validateRuntime(runtime) {
|
|
64
|
+
if (!/^[\w.-]+\/[\w.-]+$/u.test(runtime?.repository || "") || !/^[a-f0-9]{40}$/u.test(runtime?.sha || "") || !/^sha256:[a-f0-9]{64}$/u.test(runtime?.readerDigest || ""))
|
|
65
|
+
throw new Error(
|
|
66
|
+
"Record requires selected runtime repository, exact revision and reader digest"
|
|
67
|
+
);
|
|
68
|
+
return runtime;
|
|
69
|
+
}
|
|
70
|
+
function encodeRecord(record, summary = "Buildchain release transaction record") {
|
|
71
|
+
const body = `${summary}
|
|
72
|
+
|
|
73
|
+
${MARKER}${canonicalJson(record)}
|
|
74
|
+
-->`;
|
|
75
|
+
if (Buffer.byteLength(body) > MAX_RECORD_BYTES)
|
|
76
|
+
throw new Error(
|
|
77
|
+
"Transaction record exceeds the bounded body size; retain material as artifacts"
|
|
78
|
+
);
|
|
79
|
+
return body;
|
|
80
|
+
}
|
|
81
|
+
function decodeRecord(body) {
|
|
82
|
+
if (typeof body !== "string" || !body.includes(MARKER)) return void 0;
|
|
83
|
+
if (Buffer.byteLength(body) > MAX_RECORD_BYTES)
|
|
84
|
+
throw new Error("Oversized transaction record");
|
|
85
|
+
const start = body.indexOf(MARKER) + MARKER.length;
|
|
86
|
+
const end = body.indexOf("\n-->", start);
|
|
87
|
+
if (end < 0 || body.indexOf(MARKER, start) >= 0)
|
|
88
|
+
throw new Error("Malformed transaction envelope");
|
|
89
|
+
const record = JSON.parse(body.slice(start, end));
|
|
90
|
+
if (![ENVELOPE_SCHEMA, INTENT_SCHEMA].includes(record.schema))
|
|
91
|
+
throw new Error("Unsupported transaction envelope schema");
|
|
92
|
+
return record;
|
|
93
|
+
}
|
|
94
|
+
function createIntent({
|
|
95
|
+
repository,
|
|
96
|
+
key,
|
|
97
|
+
expectedNodes,
|
|
98
|
+
runtime,
|
|
99
|
+
source
|
|
100
|
+
}) {
|
|
101
|
+
requireString(repository, "Consumer repository");
|
|
102
|
+
requireString(key, "Release intent key");
|
|
103
|
+
if (key.length > 240 || /[\r\n<>]/u.test(key))
|
|
104
|
+
throw new Error("Invalid release intent key");
|
|
105
|
+
if (!Array.isArray(expectedNodes) || !expectedNodes.length || new Set(expectedNodes).size !== expectedNodes.length || expectedNodes.some((node) => !/^[a-z][a-z0-9-]*$/u.test(node)))
|
|
106
|
+
throw new Error("Release intent requires unique expected semantic nodes");
|
|
107
|
+
validateRuntime(runtime);
|
|
108
|
+
const identity = { repository, key };
|
|
109
|
+
return {
|
|
110
|
+
schema: INTENT_SCHEMA,
|
|
111
|
+
id: recordDigest(identity),
|
|
112
|
+
...identity,
|
|
113
|
+
expectedNodes,
|
|
114
|
+
organization: "attempt-threads/v1",
|
|
115
|
+
runtime,
|
|
116
|
+
source
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function createProgress({
|
|
120
|
+
intent,
|
|
121
|
+
attempt,
|
|
122
|
+
predecessor = "",
|
|
123
|
+
runtime,
|
|
124
|
+
node,
|
|
125
|
+
status,
|
|
126
|
+
payload = {},
|
|
127
|
+
sequence = 0,
|
|
128
|
+
kind = "progress",
|
|
129
|
+
writer = attempt
|
|
130
|
+
}) {
|
|
131
|
+
validateRuntime(runtime);
|
|
132
|
+
requireString(writer, "Record writer");
|
|
133
|
+
requireString(attempt, "Workflow attempt");
|
|
134
|
+
if (node !== "attempt" && !intent.expectedNodes.includes(node))
|
|
135
|
+
throw new Error(`Undeclared release node: ${node}`);
|
|
136
|
+
if (!["running", "success", "failure", "cancelled"].includes(status))
|
|
137
|
+
throw new Error("Invalid release node status");
|
|
138
|
+
if (!Number.isSafeInteger(sequence) || sequence < 0)
|
|
139
|
+
throw new Error("Invalid record sequence");
|
|
140
|
+
if (predecessor === attempt) throw new Error("Attempt cannot recover itself");
|
|
141
|
+
if (!["progress", "checkpoint"].includes(kind))
|
|
142
|
+
throw new Error("Invalid transaction record kind");
|
|
143
|
+
const event = {
|
|
144
|
+
kind,
|
|
145
|
+
writer,
|
|
146
|
+
schema: ENVELOPE_SCHEMA,
|
|
147
|
+
intent: intent.id,
|
|
148
|
+
attempt,
|
|
149
|
+
predecessor,
|
|
150
|
+
runtime,
|
|
151
|
+
payloadSchema: PAYLOAD_SCHEMA,
|
|
152
|
+
node,
|
|
153
|
+
status,
|
|
154
|
+
sequence,
|
|
155
|
+
payload
|
|
156
|
+
};
|
|
157
|
+
return { ...event, id: recordDigest(event) };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// packages/core/release/discussion/reader.js
|
|
161
|
+
function readReleaseDiscussion({ body, records }) {
|
|
162
|
+
const intent = decodeRecord(body);
|
|
163
|
+
if (intent?.schema !== INTENT_SCHEMA)
|
|
164
|
+
throw new Error("Missing release Discussion intent");
|
|
165
|
+
if (intent.id !== recordDigest({ repository: intent.repository, key: intent.key }))
|
|
166
|
+
throw new Error("Release intent identity mismatch");
|
|
167
|
+
if (!Array.isArray(intent.expectedNodes) || !intent.expectedNodes.length || new Set(intent.expectedNodes).size !== intent.expectedNodes.length)
|
|
168
|
+
throw new Error("Missing or duplicate expected nodes");
|
|
169
|
+
if (intent.organization && intent.organization !== "attempt-threads/v1")
|
|
170
|
+
throw new Error(
|
|
171
|
+
"Discussion organization requires its historical runtime reader"
|
|
172
|
+
);
|
|
173
|
+
const attempts = groupAttempts(validateRecords(intent, records));
|
|
174
|
+
if (!attempts.size)
|
|
175
|
+
return {
|
|
176
|
+
intent,
|
|
177
|
+
status: "pending",
|
|
178
|
+
attempts: [],
|
|
179
|
+
missingNodes: intent.expectedNodes
|
|
180
|
+
};
|
|
181
|
+
const { head, lineage } = resolveLineage(attempts);
|
|
182
|
+
const nodes = currentNodes(attempts.get(head).events);
|
|
183
|
+
const missingNodes = intent.expectedNodes.filter((node) => !nodes[node]);
|
|
184
|
+
const complete = intent.expectedNodes.every(
|
|
185
|
+
(node) => nodes[node]?.status === "success"
|
|
186
|
+
);
|
|
187
|
+
const failed = Object.values(nodes).some(
|
|
188
|
+
(event) => ["failure", "cancelled"].includes(event.status)
|
|
189
|
+
);
|
|
190
|
+
return {
|
|
191
|
+
intent,
|
|
192
|
+
attempt: head,
|
|
193
|
+
attempts: lineage.reverse(),
|
|
194
|
+
nodes,
|
|
195
|
+
missingNodes,
|
|
196
|
+
status: complete ? "complete" : failed ? "failed" : "running",
|
|
197
|
+
handoff: {
|
|
198
|
+
schema: "buildchain.release-handoff/v1",
|
|
199
|
+
intent: intent.id,
|
|
200
|
+
predecessor: head,
|
|
201
|
+
completedNodes: intent.expectedNodes.filter(
|
|
202
|
+
(node) => nodes[node]?.status === "success"
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function validateRecords(intent, records) {
|
|
208
|
+
const events = /* @__PURE__ */ new Map();
|
|
209
|
+
for (const raw of records) {
|
|
210
|
+
const event = typeof raw === "string" ? decodeRecord(raw) : raw;
|
|
211
|
+
if (!event) continue;
|
|
212
|
+
validateRuntime(event.runtime);
|
|
213
|
+
if (!["progress", "checkpoint"].includes(event.kind) || !event.writer || !event.attempt || typeof event.predecessor !== "string")
|
|
214
|
+
throw new Error("Invalid record ownership");
|
|
215
|
+
const { id, ...content } = event;
|
|
216
|
+
if (event.schema !== ENVELOPE_SCHEMA || event.payloadSchema !== PAYLOAD_SCHEMA)
|
|
217
|
+
throw new Error("Record requires its historical runtime reader");
|
|
218
|
+
if (event.intent !== intent.id || id !== recordDigest(content))
|
|
219
|
+
throw new Error("Release record identity mismatch");
|
|
220
|
+
if (event.node !== "attempt" && !intent.expectedNodes.includes(event.node))
|
|
221
|
+
throw new Error("Undeclared release node");
|
|
222
|
+
if (!["running", "success", "failure", "cancelled"].includes(event.status) || !Number.isSafeInteger(event.sequence) || event.sequence < 0)
|
|
223
|
+
throw new Error("Invalid release progress");
|
|
224
|
+
events.set(id, event);
|
|
225
|
+
}
|
|
226
|
+
return events;
|
|
227
|
+
}
|
|
228
|
+
function groupAttempts(events) {
|
|
229
|
+
const attempts = /* @__PURE__ */ new Map();
|
|
230
|
+
for (const event of events.values()) {
|
|
231
|
+
const attempt = attempts.get(event.attempt) || {
|
|
232
|
+
predecessor: event.predecessor,
|
|
233
|
+
events: []
|
|
234
|
+
};
|
|
235
|
+
if (attempt.predecessor !== event.predecessor)
|
|
236
|
+
throw new Error("Conflicting workflow attempt identity");
|
|
237
|
+
attempt.events.push(event);
|
|
238
|
+
attempts.set(event.attempt, attempt);
|
|
239
|
+
}
|
|
240
|
+
return attempts;
|
|
241
|
+
}
|
|
242
|
+
function resolveLineage(attempts) {
|
|
243
|
+
const predecessors = new Set(
|
|
244
|
+
[...attempts.values()].map((attempt) => attempt.predecessor).filter(Boolean)
|
|
245
|
+
);
|
|
246
|
+
for (const predecessor of predecessors)
|
|
247
|
+
if (!attempts.has(predecessor))
|
|
248
|
+
throw new Error("Recovery predecessor is missing");
|
|
249
|
+
const heads = [...attempts.keys()].filter((id) => !predecessors.has(id));
|
|
250
|
+
if (heads.length !== 1)
|
|
251
|
+
throw new Error("Release attempt succession is ambiguous or cyclic");
|
|
252
|
+
const lineage = [];
|
|
253
|
+
let current = heads[0];
|
|
254
|
+
while (current) {
|
|
255
|
+
if (lineage.includes(current)) throw new Error("Cyclic recovery chain");
|
|
256
|
+
lineage.push(current);
|
|
257
|
+
current = attempts.get(current).predecessor;
|
|
258
|
+
}
|
|
259
|
+
if (lineage.length !== attempts.size)
|
|
260
|
+
throw new Error("Disconnected recovery chain");
|
|
261
|
+
return { head: heads[0], lineage };
|
|
262
|
+
}
|
|
263
|
+
function currentNodes(events) {
|
|
264
|
+
const nodes = {}, sequences = /* @__PURE__ */ new Map();
|
|
265
|
+
for (const event of events) {
|
|
266
|
+
if (event.kind === "checkpoint") continue;
|
|
267
|
+
const key = `${event.node}:${event.sequence}`;
|
|
268
|
+
if (sequences.has(key) && sequences.get(key) !== event.id)
|
|
269
|
+
throw new Error("Conflicting node progress at the same sequence");
|
|
270
|
+
sequences.set(key, event.id);
|
|
271
|
+
const old = nodes[event.node];
|
|
272
|
+
if (!old || old.sequence < event.sequence) nodes[event.node] = event;
|
|
273
|
+
}
|
|
274
|
+
return nodes;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// packages/core/consumer/contract/shape.js
|
|
278
|
+
function object(value, required, optional = [], location = "config") {
|
|
279
|
+
if (!value || Object.getPrototypeOf(value) !== Object.prototype)
|
|
280
|
+
throw new Error(`${location}: expected a table`);
|
|
281
|
+
for (const key of Object.keys(value))
|
|
282
|
+
if (![...required, ...optional].includes(key))
|
|
283
|
+
throw new Error(`${location}.${key}: unknown field`);
|
|
284
|
+
for (const key of required)
|
|
285
|
+
if (!Object.hasOwn(value, key))
|
|
286
|
+
throw new Error(`${location}.${key}: required field`);
|
|
287
|
+
return value;
|
|
288
|
+
}
|
|
289
|
+
function text(value, location, pattern = /\S/u) {
|
|
290
|
+
if (typeof value !== "string" || !pattern.test(value) || value.includes("\0"))
|
|
291
|
+
throw new Error(`${location}: invalid string`);
|
|
292
|
+
return value;
|
|
293
|
+
}
|
|
294
|
+
function choice(value, choices, location) {
|
|
295
|
+
if (!choices.includes(value))
|
|
296
|
+
throw new Error(`${location}: expected ${choices.join(" or ")}`);
|
|
297
|
+
return value;
|
|
298
|
+
}
|
|
299
|
+
function list(value, location, validate) {
|
|
300
|
+
if (!Array.isArray(value) || !value.length)
|
|
301
|
+
throw new Error(`${location}: expected a nonempty list`);
|
|
302
|
+
return value.map((item, index) => validate(item, `${location}[${index}]`));
|
|
303
|
+
}
|
|
304
|
+
function unique(values, location) {
|
|
305
|
+
if (new Set(values).size !== values.length)
|
|
306
|
+
throw new Error(`${location}: duplicate identity`);
|
|
307
|
+
}
|
|
308
|
+
function relativePath(value, location) {
|
|
309
|
+
text(value, location, /^[A-Za-z0-9_.][A-Za-z0-9_./*-]*$/u);
|
|
310
|
+
if (value.split("/").some((part) => !part || part === ".." || part === "."))
|
|
311
|
+
throw new Error(`${location}: expected a repository-relative path`);
|
|
312
|
+
return value;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// packages/core/consumer/contract/plan.js
|
|
316
|
+
var CONSUMER_CONTRACT = "buildchain.consumer-contract/v2";
|
|
317
|
+
|
|
318
|
+
// packages/core/workflow/attempt/identity.js
|
|
319
|
+
var PIPELINE_PHASES = [
|
|
320
|
+
"admission",
|
|
321
|
+
"build",
|
|
322
|
+
"review",
|
|
323
|
+
"warrant",
|
|
324
|
+
"merge",
|
|
325
|
+
"publish",
|
|
326
|
+
"distribution",
|
|
327
|
+
"next-development"
|
|
328
|
+
];
|
|
329
|
+
var SOURCE_GENERATION = "buildchain.source-generation/v1";
|
|
330
|
+
var BUSINESS_ATTEMPT = "buildchain.business-attempt/v1";
|
|
331
|
+
var ROOT = /^sha256:[0-9a-f]{64}$/u;
|
|
332
|
+
function pipelineIntent({
|
|
333
|
+
repository,
|
|
334
|
+
repositoryId,
|
|
335
|
+
pullRequest,
|
|
336
|
+
targetBranch,
|
|
337
|
+
phases,
|
|
338
|
+
runtime
|
|
339
|
+
}) {
|
|
340
|
+
text(repository, "repository", /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u);
|
|
341
|
+
text(repositoryId, "repositoryId");
|
|
342
|
+
if (!Number.isSafeInteger(pullRequest) || pullRequest < 1)
|
|
343
|
+
throw new Error("Invalid PR intent number");
|
|
344
|
+
text(
|
|
345
|
+
targetBranch,
|
|
346
|
+
"targetBranch",
|
|
347
|
+
/^(?:dev|alpha|release|publish-gate)\/[A-Za-z0-9/._-]+$/u
|
|
348
|
+
);
|
|
349
|
+
list(
|
|
350
|
+
phases,
|
|
351
|
+
"phases",
|
|
352
|
+
(phase, field) => choice(phase, PIPELINE_PHASES, field)
|
|
353
|
+
);
|
|
354
|
+
unique(phases, "phases");
|
|
355
|
+
if (phases.some(
|
|
356
|
+
(phase, i) => i && PIPELINE_PHASES.indexOf(phase) <= PIPELINE_PHASES.indexOf(phases[i - 1])
|
|
357
|
+
))
|
|
358
|
+
throw new Error("Intent phases must follow pipeline order");
|
|
359
|
+
if (phases[0] !== "admission")
|
|
360
|
+
throw new Error("Intent must begin with admission");
|
|
361
|
+
return createIntent({
|
|
362
|
+
repository,
|
|
363
|
+
key: `pr-${pullRequest}:${targetBranch}`,
|
|
364
|
+
expectedNodes: phases,
|
|
365
|
+
runtime,
|
|
366
|
+
source: { kind: "pipeline", repositoryId, pullRequest, targetBranch }
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
function sourceGeneration(intent, source, baseCommit) {
|
|
370
|
+
object(
|
|
371
|
+
source,
|
|
372
|
+
[
|
|
373
|
+
"schema",
|
|
374
|
+
"repository",
|
|
375
|
+
"commit",
|
|
376
|
+
"tree",
|
|
377
|
+
"configPath",
|
|
378
|
+
"configBlob",
|
|
379
|
+
"configDigest",
|
|
380
|
+
"contract"
|
|
381
|
+
],
|
|
382
|
+
[],
|
|
383
|
+
"source"
|
|
384
|
+
);
|
|
385
|
+
if (source.schema !== "buildchain.consumer-source/v1" || source.contract !== CONSUMER_CONTRACT || source.repository !== intent.repository)
|
|
386
|
+
throw new Error("Generation requires the admitted consumer source");
|
|
387
|
+
for (const key of ["commit", "tree", "configBlob"])
|
|
388
|
+
text(source[key], `source.${key}`, /^[0-9a-f]{40}$/u);
|
|
389
|
+
relativePath(source.configPath, "source.configPath");
|
|
390
|
+
text(source.configDigest, "source.configDigest", ROOT);
|
|
391
|
+
text(baseCommit, "baseCommit", /^[0-9a-f]{40}$/u);
|
|
392
|
+
const value = {
|
|
393
|
+
schema: SOURCE_GENERATION,
|
|
394
|
+
intent: intent.id,
|
|
395
|
+
source: structuredClone(source),
|
|
396
|
+
baseCommit
|
|
397
|
+
};
|
|
398
|
+
return { ...value, id: recordDigest(value) };
|
|
399
|
+
}
|
|
400
|
+
function validateGeneration(value, intent) {
|
|
401
|
+
object(
|
|
402
|
+
value,
|
|
403
|
+
["schema", "intent", "source", "baseCommit", "id"],
|
|
404
|
+
[],
|
|
405
|
+
"generation"
|
|
406
|
+
);
|
|
407
|
+
const expected = sourceGeneration(intent, value.source, value.baseCommit);
|
|
408
|
+
if (recordDigest(value) !== recordDigest(expected))
|
|
409
|
+
throw new Error("Generation identity mismatch");
|
|
410
|
+
return value;
|
|
411
|
+
}
|
|
412
|
+
function businessAttempt({
|
|
413
|
+
intent,
|
|
414
|
+
generation,
|
|
415
|
+
predecessor = "",
|
|
416
|
+
requestKey
|
|
417
|
+
}) {
|
|
418
|
+
validateGeneration(generation, intent);
|
|
419
|
+
text(requestKey, "requestKey");
|
|
420
|
+
if (requestKey.length > 240)
|
|
421
|
+
throw new Error("Attempt request key exceeds its bound");
|
|
422
|
+
if (predecessor) text(predecessor, "predecessor", /^attempt-[0-9a-f]{64}$/u);
|
|
423
|
+
const value = {
|
|
424
|
+
schema: BUSINESS_ATTEMPT,
|
|
425
|
+
intent: intent.id,
|
|
426
|
+
generation: generation.id,
|
|
427
|
+
predecessor,
|
|
428
|
+
requestKey
|
|
429
|
+
};
|
|
430
|
+
return { ...value, id: `attempt-${recordDigest(value).slice(7)}` };
|
|
431
|
+
}
|
|
432
|
+
function validateAttempt(value, intent, generation) {
|
|
433
|
+
object(
|
|
434
|
+
value,
|
|
435
|
+
["schema", "intent", "generation", "predecessor", "requestKey", "id"],
|
|
436
|
+
[],
|
|
437
|
+
"attempt"
|
|
438
|
+
);
|
|
439
|
+
const expected = businessAttempt({
|
|
440
|
+
intent,
|
|
441
|
+
generation,
|
|
442
|
+
predecessor: value.predecessor,
|
|
443
|
+
requestKey: value.requestKey
|
|
444
|
+
});
|
|
445
|
+
if (recordDigest(value) !== recordDigest(expected))
|
|
446
|
+
throw new Error("Business attempt identity mismatch");
|
|
447
|
+
return value;
|
|
448
|
+
}
|
|
449
|
+
function providerWriter(value, repository) {
|
|
450
|
+
object(value, ["repository", "runId", "runAttempt", "jobId"], [], "writer");
|
|
451
|
+
if (value.repository !== repository)
|
|
452
|
+
throw new Error("Writer belongs to a different repository");
|
|
453
|
+
for (const key of ["runId", "runAttempt", "jobId"])
|
|
454
|
+
text(value[key], `writer.${key}`, /^[1-9][0-9]*$/u);
|
|
455
|
+
return `${value.runId}:${value.runAttempt}:${value.jobId}`;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// packages/core/workflow/attempt/materials.js
|
|
459
|
+
var import_node_crypto2 = require("crypto");
|
|
460
|
+
var MATERIAL_KINDS = [
|
|
461
|
+
"artifact",
|
|
462
|
+
"checkpoint",
|
|
463
|
+
"receipt",
|
|
464
|
+
"passport",
|
|
465
|
+
"provider-readback"
|
|
466
|
+
];
|
|
467
|
+
function materialReference(value, attempt, repository = "") {
|
|
468
|
+
object(
|
|
469
|
+
value,
|
|
470
|
+
["id", "kind", "digest", "bytes", "url", "generation", "producerAttempt"],
|
|
471
|
+
[],
|
|
472
|
+
"material"
|
|
473
|
+
);
|
|
474
|
+
text(value.id, "material.id", /^[a-z][a-z0-9._/-]*$/u);
|
|
475
|
+
choice(value.kind, MATERIAL_KINDS, "material.kind");
|
|
476
|
+
text(value.digest, "material.digest", /^sha256:[0-9a-f]{64}$/u);
|
|
477
|
+
if (!Number.isSafeInteger(value.bytes) || value.bytes < 1)
|
|
478
|
+
throw new Error("Invalid material byte size");
|
|
479
|
+
const url = new URL(value.url);
|
|
480
|
+
if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash || !["github.com", "api.github.com"].includes(url.hostname))
|
|
481
|
+
throw new Error(
|
|
482
|
+
"Material must use a permanent GitHub URL without credentials or signed queries"
|
|
483
|
+
);
|
|
484
|
+
if (repository && !(url.hostname === "github.com" ? url.pathname.startsWith(`/${repository}/releases/download/`) || url.pathname.startsWith(`/${repository}/actions/runs/`) : url.pathname.startsWith(`/repos/${repository}/actions/artifacts/`) || url.pathname.startsWith(`/repos/${repository}/releases/assets/`)))
|
|
485
|
+
throw new Error("Material belongs to another consumer repository");
|
|
486
|
+
if (value.generation !== attempt.generation || value.producerAttempt !== attempt.id)
|
|
487
|
+
throw new Error(
|
|
488
|
+
"Material is not qualified for this exact attempt and generation"
|
|
489
|
+
);
|
|
490
|
+
return { ...value };
|
|
491
|
+
}
|
|
492
|
+
function indexMaterials(index, references, attempt) {
|
|
493
|
+
for (const reference of references) {
|
|
494
|
+
materialReference(reference, attempt);
|
|
495
|
+
const old = index[reference.id];
|
|
496
|
+
if (old && recordDigest(old) !== recordDigest(reference))
|
|
497
|
+
throw new Error(`Immutable material identity conflict: ${reference.id}`);
|
|
498
|
+
index[reference.id] = { ...reference };
|
|
499
|
+
}
|
|
500
|
+
return index;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// packages/core/workflow/attempt/records.js
|
|
504
|
+
var ATTEMPT_EVENT = "buildchain.business-attempt-event/v1";
|
|
505
|
+
var STATES = [
|
|
506
|
+
"running",
|
|
507
|
+
"waiting",
|
|
508
|
+
"success",
|
|
509
|
+
"failure",
|
|
510
|
+
"cancelled",
|
|
511
|
+
"superseded"
|
|
512
|
+
];
|
|
513
|
+
var ENVELOPE_STATUS = {
|
|
514
|
+
running: "running",
|
|
515
|
+
waiting: "running",
|
|
516
|
+
success: "success",
|
|
517
|
+
failure: "failure",
|
|
518
|
+
cancelled: "cancelled",
|
|
519
|
+
superseded: "cancelled"
|
|
520
|
+
};
|
|
521
|
+
function attemptRecord({
|
|
522
|
+
intent,
|
|
523
|
+
attempt,
|
|
524
|
+
generation,
|
|
525
|
+
writer,
|
|
526
|
+
runtime,
|
|
527
|
+
previous = null,
|
|
528
|
+
eventKey,
|
|
529
|
+
phase,
|
|
530
|
+
state,
|
|
531
|
+
reason = "",
|
|
532
|
+
materials = []
|
|
533
|
+
}) {
|
|
534
|
+
validateAttempt(attempt, intent, generation);
|
|
535
|
+
const writerId = providerWriter(writer, intent.repository);
|
|
536
|
+
choice(state, STATES, "state");
|
|
537
|
+
text(eventKey, "eventKey");
|
|
538
|
+
if (eventKey.length > 240 || typeof reason !== "string" || reason.length > 2e3)
|
|
539
|
+
throw new Error("Attempt event text exceeds its bound");
|
|
540
|
+
if (!previous && (phase !== "attempt" || state !== "running"))
|
|
541
|
+
throw new Error("Open an attempt root before phase progress");
|
|
542
|
+
if (previous && (previous.attempt !== attempt.id || previous.payload.generation.id !== generation.id))
|
|
543
|
+
throw new Error(
|
|
544
|
+
"Record predecessor belongs to another attempt or generation"
|
|
545
|
+
);
|
|
546
|
+
if (!Array.isArray(materials) || materials.length > 100)
|
|
547
|
+
throw new Error("Attempt event material bound exceeded");
|
|
548
|
+
unique(
|
|
549
|
+
materials.map((item) => item.id),
|
|
550
|
+
"materials"
|
|
551
|
+
);
|
|
552
|
+
materials.forEach(
|
|
553
|
+
(item) => materialReference(item, attempt, intent.repository)
|
|
554
|
+
);
|
|
555
|
+
return createProgress({
|
|
556
|
+
intent,
|
|
557
|
+
attempt: attempt.id,
|
|
558
|
+
predecessor: attempt.predecessor,
|
|
559
|
+
writer: writerId,
|
|
560
|
+
runtime,
|
|
561
|
+
node: phase,
|
|
562
|
+
status: ENVELOPE_STATUS[state],
|
|
563
|
+
sequence: previous ? previous.sequence + 1 : 0,
|
|
564
|
+
payload: {
|
|
565
|
+
schema: ATTEMPT_EVENT,
|
|
566
|
+
identity: attempt,
|
|
567
|
+
generation,
|
|
568
|
+
writer: { ...writer },
|
|
569
|
+
previous: previous?.id || "",
|
|
570
|
+
eventKey,
|
|
571
|
+
state,
|
|
572
|
+
reason,
|
|
573
|
+
materials: structuredClone(materials)
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
function validateAttemptRecord(record, intent) {
|
|
578
|
+
const value = record.payload;
|
|
579
|
+
object(
|
|
580
|
+
value,
|
|
581
|
+
[
|
|
582
|
+
"schema",
|
|
583
|
+
"identity",
|
|
584
|
+
"generation",
|
|
585
|
+
"writer",
|
|
586
|
+
"previous",
|
|
587
|
+
"eventKey",
|
|
588
|
+
"state",
|
|
589
|
+
"reason",
|
|
590
|
+
"materials"
|
|
591
|
+
],
|
|
592
|
+
[],
|
|
593
|
+
"attemptEvent"
|
|
594
|
+
);
|
|
595
|
+
if (value.schema !== ATTEMPT_EVENT)
|
|
596
|
+
throw new Error("Attempt event requires its historical runtime reader");
|
|
597
|
+
validateAttempt(value.identity, intent, value.generation);
|
|
598
|
+
const { id, ...content } = record;
|
|
599
|
+
if (id !== recordDigest(content) || record.attempt !== value.identity.id || record.intent !== intent.id || record.predecessor !== value.identity.predecessor)
|
|
600
|
+
throw new Error("Attempt record identity mismatch");
|
|
601
|
+
if (providerWriter(value.writer, intent.repository) !== record.writer)
|
|
602
|
+
throw new Error("Attempt provider writer mismatch");
|
|
603
|
+
choice(value.state, STATES, "attemptEvent.state");
|
|
604
|
+
if (record.status !== ENVELOPE_STATUS[value.state])
|
|
605
|
+
throw new Error("Attempt record status mismatch");
|
|
606
|
+
if (value.previous)
|
|
607
|
+
text(value.previous, "previous", /^sha256:[0-9a-f]{64}$/u);
|
|
608
|
+
const expected = attemptRecord({
|
|
609
|
+
intent,
|
|
610
|
+
attempt: value.identity,
|
|
611
|
+
generation: value.generation,
|
|
612
|
+
writer: value.writer,
|
|
613
|
+
runtime: record.runtime,
|
|
614
|
+
previous: value.previous ? {
|
|
615
|
+
id: value.previous,
|
|
616
|
+
attempt: record.attempt,
|
|
617
|
+
payload: value,
|
|
618
|
+
sequence: record.sequence - 1
|
|
619
|
+
} : null,
|
|
620
|
+
eventKey: value.eventKey,
|
|
621
|
+
phase: record.node,
|
|
622
|
+
state: value.state,
|
|
623
|
+
reason: value.reason,
|
|
624
|
+
materials: value.materials
|
|
625
|
+
});
|
|
626
|
+
if (recordDigest(expected) !== recordDigest(record))
|
|
627
|
+
throw new Error("Invalid attempt record envelope");
|
|
628
|
+
encodeRecord(record);
|
|
629
|
+
return record;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// packages/core/workflow/attempt/reader.js
|
|
633
|
+
var MAX_ATTEMPT_RECORDS = 1e4;
|
|
634
|
+
var MAX_HISTORY_BYTES = 32 * 1024 * 1024;
|
|
635
|
+
function validateIntent(intent) {
|
|
636
|
+
const source = intent.source;
|
|
637
|
+
if (source?.kind !== "pipeline") throw new Error("Not a pipeline intent");
|
|
638
|
+
const expected = pipelineIntent({
|
|
639
|
+
...source,
|
|
640
|
+
repository: intent.repository,
|
|
641
|
+
phases: intent.expectedNodes,
|
|
642
|
+
runtime: intent.runtime
|
|
643
|
+
});
|
|
644
|
+
if (recordDigest(expected) !== recordDigest(intent))
|
|
645
|
+
throw new Error("Pipeline intent identity mismatch");
|
|
646
|
+
}
|
|
647
|
+
function foldAttempt(events, intent) {
|
|
648
|
+
events.sort((a, b) => a.sequence - b.sequence);
|
|
649
|
+
const roots = events.filter((record) => record.node === "attempt");
|
|
650
|
+
if (roots.length !== 1 || roots[0] !== events[0])
|
|
651
|
+
throw new Error("Attempt requires exactly one initial root");
|
|
652
|
+
const phases = /* @__PURE__ */ Object.create(null), materials = /* @__PURE__ */ Object.create(null), keys = /* @__PURE__ */ new Set(), runs = /* @__PURE__ */ new Map();
|
|
653
|
+
let previous = null;
|
|
654
|
+
for (const record of events) {
|
|
655
|
+
const value = record.payload;
|
|
656
|
+
if (value.identity.id !== roots[0].payload.identity.id || value.generation.id !== roots[0].payload.generation.id)
|
|
657
|
+
throw new Error("Attempt changed its immutable generation");
|
|
658
|
+
if (record.sequence !== (previous ? previous.sequence + 1 : 0) || value.previous !== (previous?.id || ""))
|
|
659
|
+
throw new Error(
|
|
660
|
+
"Attempt record chain has a gap, fork or missing predecessor"
|
|
661
|
+
);
|
|
662
|
+
if (keys.has(value.eventKey))
|
|
663
|
+
throw new Error("Conflicting duplicate event key");
|
|
664
|
+
keys.add(value.eventKey);
|
|
665
|
+
if (record.node !== "attempt") {
|
|
666
|
+
const before = intent.expectedNodes.slice(
|
|
667
|
+
0,
|
|
668
|
+
intent.expectedNodes.indexOf(record.node)
|
|
669
|
+
);
|
|
670
|
+
if (before.some((phase) => phases[phase]?.payload.state !== "success"))
|
|
671
|
+
throw new Error("Phase predecessor has not succeeded");
|
|
672
|
+
const old = phases[record.node];
|
|
673
|
+
if (old && ["success", "failure", "cancelled", "superseded"].includes(
|
|
674
|
+
old.payload.state
|
|
675
|
+
))
|
|
676
|
+
throw new Error(
|
|
677
|
+
"Terminal phase history is immutable; recover in a successor attempt"
|
|
678
|
+
);
|
|
679
|
+
phases[record.node] = record;
|
|
680
|
+
}
|
|
681
|
+
indexMaterials(materials, value.materials, value.identity);
|
|
682
|
+
runs.set(record.writer, value.writer);
|
|
683
|
+
previous = record;
|
|
684
|
+
}
|
|
685
|
+
return {
|
|
686
|
+
identity: roots[0].payload.identity,
|
|
687
|
+
generation: roots[0].payload.generation,
|
|
688
|
+
head: previous.id,
|
|
689
|
+
events,
|
|
690
|
+
phases,
|
|
691
|
+
materials,
|
|
692
|
+
runs: [...runs.values()]
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
function readBusinessAttempt({ intent, records }) {
|
|
696
|
+
validateIntent(intent);
|
|
697
|
+
if (!Array.isArray(records) || records.length > MAX_ATTEMPT_RECORDS)
|
|
698
|
+
throw new Error("Attempt record count exceeds its bound");
|
|
699
|
+
if (Buffer.byteLength(canonicalJson(records)) > MAX_HISTORY_BYTES)
|
|
700
|
+
throw new Error("Attempt history byte bound exceeded");
|
|
701
|
+
const unique2 = /* @__PURE__ */ new Map(), grouped = /* @__PURE__ */ new Map();
|
|
702
|
+
for (const record of records) {
|
|
703
|
+
validateAttemptRecord(record, intent);
|
|
704
|
+
unique2.set(record.id, record);
|
|
705
|
+
}
|
|
706
|
+
for (const record of unique2.values()) {
|
|
707
|
+
const events = grouped.get(record.attempt) || [];
|
|
708
|
+
events.push(record);
|
|
709
|
+
grouped.set(record.attempt, events);
|
|
710
|
+
}
|
|
711
|
+
const lineage = readReleaseDiscussion({
|
|
712
|
+
body: encodeRecord(intent),
|
|
713
|
+
records: [...unique2.values()]
|
|
714
|
+
});
|
|
715
|
+
const history = lineage.attempts.map(
|
|
716
|
+
(id) => foldAttempt(grouped.get(id), intent)
|
|
717
|
+
);
|
|
718
|
+
const current = history.at(-1);
|
|
719
|
+
if (!current)
|
|
720
|
+
return {
|
|
721
|
+
intent,
|
|
722
|
+
status: "pending",
|
|
723
|
+
reason: "not-admitted",
|
|
724
|
+
attempt: "",
|
|
725
|
+
missing: intent.expectedNodes,
|
|
726
|
+
history,
|
|
727
|
+
recovery: null
|
|
728
|
+
};
|
|
729
|
+
const missing = intent.expectedNodes.filter(
|
|
730
|
+
(phase) => current.phases[phase]?.payload.state !== "success"
|
|
731
|
+
);
|
|
732
|
+
const states = Object.values(current.phases).map((record) => record.payload);
|
|
733
|
+
const stopped = states.find(
|
|
734
|
+
(value) => ["failure", "cancelled", "superseded"].includes(value.state)
|
|
735
|
+
);
|
|
736
|
+
const waiting = states.find((value) => value.state === "waiting");
|
|
737
|
+
const status = stopped?.state || (missing.length ? waiting ? "waiting" : "running" : "complete");
|
|
738
|
+
return {
|
|
739
|
+
intent,
|
|
740
|
+
attempt: current.identity.id,
|
|
741
|
+
generation: current.generation.id,
|
|
742
|
+
status,
|
|
743
|
+
reason: stopped?.reason || waiting?.reason || status,
|
|
744
|
+
head: current.head,
|
|
745
|
+
missing,
|
|
746
|
+
phases: current.phases,
|
|
747
|
+
materials: current.materials,
|
|
748
|
+
runs: current.runs,
|
|
749
|
+
history,
|
|
750
|
+
recovery: status === "complete" ? null : { attempt: current.identity.id }
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// packages/core/workflow/attempt/reader-entry.js
|
|
755
|
+
if (typeof require !== "undefined" && require.main === module) {
|
|
756
|
+
const state = readBusinessAttempt(JSON.parse(import_node_fs.default.readFileSync(0, "utf8")));
|
|
757
|
+
const { status, reason, attempt, generation, missing, recovery } = state;
|
|
758
|
+
process.stdout.write(
|
|
759
|
+
`${JSON.stringify({ status, reason, attempt, generation, missing, recovery })}
|
|
760
|
+
`
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
764
|
+
0 && (module.exports = {
|
|
765
|
+
readBusinessAttempt
|
|
766
|
+
});
|
|
767
|
+
/*! Bundled license information:
|
|
768
|
+
|
|
769
|
+
smol-toml/dist/date.js:
|
|
770
|
+
smol-toml/dist/error.js:
|
|
771
|
+
smol-toml/dist/primitive.js:
|
|
772
|
+
smol-toml/dist/util.js:
|
|
773
|
+
smol-toml/dist/extract.js:
|
|
774
|
+
smol-toml/dist/struct.js:
|
|
775
|
+
smol-toml/dist/parse.js:
|
|
776
|
+
smol-toml/dist/stringify.js:
|
|
777
|
+
smol-toml/dist/index.js:
|
|
778
|
+
(*!
|
|
779
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
780
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
781
|
+
*
|
|
782
|
+
* Redistribution and use in source and binary forms, with or without
|
|
783
|
+
* modification, are permitted provided that the following conditions are met:
|
|
784
|
+
*
|
|
785
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
786
|
+
* list of conditions and the following disclaimer.
|
|
787
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
788
|
+
* this list of conditions and the following disclaimer in the
|
|
789
|
+
* documentation and/or other materials provided with the distribution.
|
|
790
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
791
|
+
* may be used to endorse or promote products derived from this software without
|
|
792
|
+
* specific prior written permission.
|
|
793
|
+
*
|
|
794
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
795
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
796
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
797
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
798
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
799
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
800
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
801
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
802
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
803
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
804
|
+
*)
|
|
805
|
+
*/
|