@kungfu-tech/buildchain 3.0.2 → 3.0.3-alpha.1
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/AGENTS.md +1 -1
- package/CONTRIBUTING.md +1 -1
- package/README.md +27 -1
- package/actions/promote-buildchain-ref/README.md +90 -19
- package/actions/report-buildchain-issue/README.md +20 -2
- package/actions/run-lifecycle/README.md +5 -0
- package/actions/validate-config/README.md +19 -1
- package/bin/buildchain.mjs +56 -2
- package/bin/internal/command-registry.mjs +82 -0
- package/bin/internal/trust-release-cli.mjs +1 -0
- package/contracts/auditable-demo-media-profiles-v1.json +116 -1
- package/contracts/buildchain-v2-residuals-v1.json +146 -0
- package/contracts/evidence/auditable-demo-responsive-web-delivery-v1.json +143 -0
- package/contracts/evidence/auditable-demo-web-delivery-v1.json +7 -3
- package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/complete-transcript.txt +2 -0
- package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/public-projection.json +16 -0
- package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/scene.json +12 -0
- package/dist/site/buildchain-contract.json +188 -46
- package/dist/site/buildchain-site.json +181 -90
- package/dist/site/capability-registry.json +5 -5
- package/dist/site/cli-registry.json +860 -98
- package/dist/site/controller-registry.json +118 -5
- package/dist/site/kfd-claims.json +233 -19
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +20 -20
- package/dist/site/node-api-registry.json +374 -59
- package/dist/site/page-registry.json +156 -65
- package/dist/site/public-surface-audit.json +231 -22
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +3 -0
- package/dist/site/site-manifest.json +24 -24
- package/dist/site/workflow-registry.json +575 -79
- package/docs/MAP.md +22 -4
- package/docs/auditable-demo.md +54 -13
- package/docs/aws-us-elastic-runner-burst-plane.md +381 -0
- package/docs/cli.md +119 -12
- package/docs/consumer-issue-reporting.md +20 -2
- package/docs/dev-alpha-candidate-patrol.md +24 -10
- package/docs/github-governance-authority.md +24 -31
- package/docs/lifecycle-protocol.md +20 -2
- package/docs/migration-inventory.md +24 -5
- package/docs/observed-evidence-patrol.md +28 -12
- package/docs/ownership.md +19 -1
- package/docs/publication-artifacts.md +158 -18
- package/docs/publication-authority.md +4 -4
- package/docs/publish-transaction.md +78 -31
- package/docs/release-candidate.md +24 -0
- package/docs/release-flow.md +50 -32
- package/docs/release-governance.md +39 -32
- package/docs/release-passport.md +4 -0
- package/docs/release-propagation.md +57 -12
- package/docs/reusable-build-surface.md +218 -90
- package/docs/runtime-train-validation.md +20 -2
- package/docs/shifu-gate-profiles.md +7 -1
- package/docs/stable-candidate-patrol.md +6 -6
- package/docs/toolkit-observability.md +22 -0
- package/docs/versioning.md +46 -24
- package/docs/web-surface-deployments.md +82 -1
- package/package.json +7 -5
- package/packages/core/README.md +60 -4
- package/packages/core/artifact-signing.js +1 -0
- package/packages/core/buildchain-contract.js +9 -4
- package/packages/core/cache-evidence.js +1 -0
- package/packages/core/controller-evidence.js +2 -0
- package/packages/core/diagnostics.js +105 -30
- package/packages/core/github-governance-authority.js +25 -17
- package/packages/core/index.js +40 -0
- package/packages/core/paper.js +3226 -0
- package/packages/core/public-surface-audit.js +3 -0
- package/packages/core/publication-artifact.js +1 -6
- package/packages/core/publication-authority.js +1 -0
- package/packages/core/publication-control-plane-audit.js +28 -0
- package/packages/core/publication-reproducibility.js +947 -0
- package/packages/core/publication-sealed-bundle.js +190 -0
- package/packages/core/publish-transaction.js +147 -17
- package/packages/core/release-candidate.js +234 -0
- package/packages/core/release-passport.js +519 -228
- package/packages/core/release-propagation.js +175 -1
- package/packages/core/stable-release-gate.js +4 -1
- package/scripts/artifact-signing-delegation.mjs +268 -0
- package/scripts/audit-github-governance.mjs +32 -13
- package/scripts/audit-publication-control-plane.mjs +17 -8
- package/scripts/auditable-demo.mjs +266 -7
- package/scripts/aws-codebuild-toolchain.mjs +285 -0
- package/scripts/aws-macos-jit-core.mjs +378 -0
- package/scripts/aws-macos-jit.mjs +123 -0
- package/scripts/aws-runner-burst-core.mjs +248 -0
- package/scripts/aws-runner-burst.mjs +79 -0
- package/scripts/aws-windows-jit-core.mjs +374 -0
- package/scripts/aws-windows-jit.mjs +121 -0
- package/scripts/build-contract-core.mjs +69 -6
- package/scripts/build-standalone-binary.mjs +23 -0
- package/scripts/buildchain-contract-lock.mjs +1 -1
- package/scripts/buildchain-patrol.mjs +1 -1
- package/scripts/check-internal-architecture.mjs +69 -2
- package/scripts/check-inventory.mjs +70 -2
- package/scripts/check-javascript-syntax.mjs +31 -0
- package/scripts/check-maintainability.mjs +371 -0
- package/scripts/compiler-cache-evidence.mjs +221 -0
- package/scripts/dev-alpha-candidate-patrol.mjs +288 -43
- package/scripts/dispatch-artifact-signing-authority.mjs +7 -1
- package/scripts/finalize-native-artifact-signing-result.mjs +185 -31
- package/scripts/gate-profile-core.mjs +6 -1
- package/scripts/generate-channel-promotion-workflow.mjs +3 -12
- package/scripts/generate-release-candidate-passport.mjs +4 -0
- package/scripts/generate-site-bundle.mjs +84 -18
- package/scripts/import-artifact-signing-results.mjs +64 -5
- package/scripts/inspect-artifact-signing-requests.mjs +52 -14
- package/scripts/maintainability-metrics.mjs +339 -0
- package/scripts/npm-publish-transaction.mjs +57 -6
- package/scripts/observed-evidence.mjs +151 -33
- package/scripts/paper.mjs +645 -0
- package/scripts/promotion-channel-router.mjs +16 -4
- package/scripts/publication-reproducibility.mjs +62 -0
- package/scripts/reconcile-github-governance.mjs +8 -1
- package/scripts/release-propagation.mjs +39 -0
- package/scripts/resolve-artifact-signing-upload-route.mjs +55 -0
- package/scripts/resolve-build-contract.mjs +6 -0
- package/scripts/route-offline-runners.mjs +236 -0
- package/scripts/run-candidate-body-prefix-renderer.mjs +187 -0
- package/scripts/run-lifecycle-core.mjs +62 -1
- package/scripts/runtime-ref-core.mjs +36 -4
- package/scripts/seal-artifact-signing-requests.mjs +64 -12
- package/scripts/stable-candidate-qualification.mjs +50 -0
- package/scripts/verify-artifact-signing-results.mjs +26 -1
- package/scripts/web-surface-core.mjs +272 -86
- package/scripts/web-surface-production-decision.mjs +19 -3
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
PUBLICATION_ARTIFACT_CANDIDATE_CONTRACT,
|
|
7
|
+
publicationArtifactCandidateDigest,
|
|
8
|
+
} from "./publication-artifact-candidate.js";
|
|
9
|
+
|
|
10
|
+
export const PUBLICATION_SEALED_BUNDLE_CONTRACT = "kungfu-buildchain-publication-sealed-bundle";
|
|
11
|
+
|
|
12
|
+
function requiredString(value, label) {
|
|
13
|
+
const normalized = String(value || "").trim();
|
|
14
|
+
if (!normalized) {
|
|
15
|
+
throw new Error(`${label} must be a non-empty string`);
|
|
16
|
+
}
|
|
17
|
+
return normalized;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function safeRelativePath(value, label) {
|
|
21
|
+
const normalized = requiredString(value, label).replaceAll("\\", "/");
|
|
22
|
+
if (normalized.startsWith("/") || normalized.split("/").some((part) => part === ".." || part === "")) {
|
|
23
|
+
throw new Error(`${label} must be a safe relative path`);
|
|
24
|
+
}
|
|
25
|
+
return normalized;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeSha256(value, label) {
|
|
29
|
+
const normalized = requiredString(value, label)
|
|
30
|
+
.replace(/^sha256:/, "")
|
|
31
|
+
.toLowerCase();
|
|
32
|
+
if (!/^[0-9a-f]{64}$/.test(normalized)) {
|
|
33
|
+
throw new Error(`${label} must be a sha256 digest`);
|
|
34
|
+
}
|
|
35
|
+
return normalized;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sha256File(filePath) {
|
|
39
|
+
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeFile(entry, label) {
|
|
43
|
+
const size = Number(entry?.size ?? entry?.bytes);
|
|
44
|
+
if (!Number.isSafeInteger(size) || size < 0) {
|
|
45
|
+
throw new Error(`${label}.size must be a non-negative safe integer`);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
path: safeRelativePath(entry.path, `${label}.path`),
|
|
49
|
+
size,
|
|
50
|
+
sha256: normalizeSha256(entry.sha256, `${label}.sha256`),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function candidatePayload(candidate) {
|
|
55
|
+
if (candidate?.contract !== PUBLICATION_ARTIFACT_CANDIDATE_CONTRACT || Number(candidate?.schemaVersion) !== 1) {
|
|
56
|
+
throw new Error("publication artifact candidate contract mismatch");
|
|
57
|
+
}
|
|
58
|
+
const { candidateDigest: _candidateDigest, ...payload } = candidate;
|
|
59
|
+
const digest = publicationArtifactCandidateDigest(payload);
|
|
60
|
+
if (normalizeSha256(candidate.candidateDigest, "candidate.candidateDigest") !== digest) {
|
|
61
|
+
throw new Error("publication artifact candidate digest mismatch");
|
|
62
|
+
}
|
|
63
|
+
return { payload, digest };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function selectFile(files, filePath, label) {
|
|
67
|
+
const normalizedPath = safeRelativePath(filePath, label);
|
|
68
|
+
const matches = files.filter((entry) => entry.path === normalizedPath);
|
|
69
|
+
if (matches.length !== 1) {
|
|
70
|
+
throw new Error(`${label} must identify exactly one candidate file`);
|
|
71
|
+
}
|
|
72
|
+
return matches[0];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function createPublicationSealedBundle({
|
|
76
|
+
candidate,
|
|
77
|
+
packageName,
|
|
78
|
+
packageVersion,
|
|
79
|
+
npmTarballPath,
|
|
80
|
+
npmIntegrity,
|
|
81
|
+
releaseAssetPaths = [],
|
|
82
|
+
githubReleaseRequired = true,
|
|
83
|
+
} = {}) {
|
|
84
|
+
const { digest } = candidatePayload(candidate);
|
|
85
|
+
const files = (candidate.files || [])
|
|
86
|
+
.map((entry, index) => normalizeFile(entry, `candidate.files[${index}]`))
|
|
87
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
88
|
+
if (new Set(files.map((entry) => entry.path)).size !== files.length) {
|
|
89
|
+
throw new Error("publication sealed bundle candidate paths must be unique");
|
|
90
|
+
}
|
|
91
|
+
const npmTarball = selectFile(files, npmTarballPath, "npmTarballPath");
|
|
92
|
+
const releaseAssets = [...new Set(releaseAssetPaths)]
|
|
93
|
+
.map((assetPath, index) => selectFile(files, assetPath, `releaseAssetPaths[${index}]`))
|
|
94
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
95
|
+
if (releaseAssets.length === 0) {
|
|
96
|
+
throw new Error("publication sealed bundle requires at least one release asset");
|
|
97
|
+
}
|
|
98
|
+
const normalizedVersion = requiredString(packageVersion, "packageVersion");
|
|
99
|
+
return {
|
|
100
|
+
schemaVersion: 1,
|
|
101
|
+
contract: PUBLICATION_SEALED_BUNDLE_CONTRACT,
|
|
102
|
+
root: `sha256:${digest}`,
|
|
103
|
+
candidate,
|
|
104
|
+
files,
|
|
105
|
+
npm: {
|
|
106
|
+
name: requiredString(packageName, "packageName"),
|
|
107
|
+
version: normalizedVersion,
|
|
108
|
+
path: npmTarball.path,
|
|
109
|
+
size: npmTarball.size,
|
|
110
|
+
sha256: npmTarball.sha256,
|
|
111
|
+
integrity: requiredString(npmIntegrity, "npmIntegrity"),
|
|
112
|
+
},
|
|
113
|
+
releaseAssets,
|
|
114
|
+
completion: {
|
|
115
|
+
githubReleaseRequired: Boolean(githubReleaseRequired),
|
|
116
|
+
},
|
|
117
|
+
durablePath: `sealed-bundle/sha256/${digest}`,
|
|
118
|
+
resumeCommand:
|
|
119
|
+
`buildchain paper resume --version ${JSON.stringify(normalizedVersion)} ` +
|
|
120
|
+
`--state-ref ${JSON.stringify(`refs/heads/buildchain/release-state/${normalizedVersion.replaceAll(".", "-")}`)}`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function verifyPublicationSealedBundle({ bundleRoot, manifest } = {}) {
|
|
125
|
+
if (manifest?.contract !== PUBLICATION_SEALED_BUNDLE_CONTRACT || Number(manifest?.schemaVersion) !== 1) {
|
|
126
|
+
throw new Error("publication sealed bundle contract mismatch");
|
|
127
|
+
}
|
|
128
|
+
const resolvedRoot = path.resolve(requiredString(bundleRoot, "bundleRoot"));
|
|
129
|
+
const { digest } = candidatePayload(manifest.candidate);
|
|
130
|
+
if (normalizeSha256(manifest.root, "manifest.root") !== digest) {
|
|
131
|
+
throw new Error("publication sealed bundle root mismatch");
|
|
132
|
+
}
|
|
133
|
+
const files = (manifest.files || [])
|
|
134
|
+
.map((entry, index) => normalizeFile(entry, `manifest.files[${index}]`))
|
|
135
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
136
|
+
const candidateFiles = (manifest.candidate.files || [])
|
|
137
|
+
.map((entry, index) => normalizeFile(entry, `candidate.files[${index}]`))
|
|
138
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
139
|
+
if (JSON.stringify(files) !== JSON.stringify(candidateFiles)) {
|
|
140
|
+
throw new Error("publication sealed bundle file inventory differs from candidate");
|
|
141
|
+
}
|
|
142
|
+
for (const [index, entry] of files.entries()) {
|
|
143
|
+
const filePath = path.resolve(resolvedRoot, entry.path);
|
|
144
|
+
if (!filePath.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
145
|
+
throw new Error(`manifest.files[${index}].path escapes bundle root`);
|
|
146
|
+
}
|
|
147
|
+
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
148
|
+
throw new Error(`publication sealed bundle file is missing: ${entry.path}`);
|
|
149
|
+
}
|
|
150
|
+
const size = fs.statSync(filePath).size;
|
|
151
|
+
const digestValue = sha256File(filePath);
|
|
152
|
+
if (size !== entry.size || digestValue !== entry.sha256) {
|
|
153
|
+
throw new Error(`publication sealed bundle file mismatch: ${entry.path}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const npmFile = selectFile(files, manifest.npm?.path, "manifest.npm.path");
|
|
157
|
+
if (
|
|
158
|
+
npmFile.size !== Number(manifest.npm?.size) ||
|
|
159
|
+
npmFile.sha256 !== normalizeSha256(manifest.npm?.sha256, "manifest.npm.sha256")
|
|
160
|
+
) {
|
|
161
|
+
throw new Error("publication sealed bundle npm tarball inventory mismatch");
|
|
162
|
+
}
|
|
163
|
+
const releaseAssets = (manifest.releaseAssets || []).map((entry, index) => {
|
|
164
|
+
const selected = selectFile(files, entry.path, `manifest.releaseAssets[${index}].path`);
|
|
165
|
+
const normalized = normalizeFile(entry, `manifest.releaseAssets[${index}]`);
|
|
166
|
+
if (selected.size !== normalized.size || selected.sha256 !== normalized.sha256) {
|
|
167
|
+
throw new Error(`publication sealed bundle release asset mismatch: ${entry.path}`);
|
|
168
|
+
}
|
|
169
|
+
return selected;
|
|
170
|
+
});
|
|
171
|
+
if (releaseAssets.length === 0) {
|
|
172
|
+
throw new Error("publication sealed bundle requires at least one release asset");
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
ok: true,
|
|
176
|
+
root: `sha256:${digest}`,
|
|
177
|
+
bundleRoot: resolvedRoot,
|
|
178
|
+
files,
|
|
179
|
+
npm: {
|
|
180
|
+
...manifest.npm,
|
|
181
|
+
path: npmFile.path,
|
|
182
|
+
absolutePath: path.resolve(resolvedRoot, npmFile.path),
|
|
183
|
+
},
|
|
184
|
+
releaseAssets: releaseAssets.map((entry) => ({
|
|
185
|
+
...entry,
|
|
186
|
+
absolutePath: path.resolve(resolvedRoot, entry.path),
|
|
187
|
+
})),
|
|
188
|
+
manifest,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
|
|
5
5
|
export const RELEASE_TRANSACTION_STATES = Object.freeze([
|
|
6
6
|
"prepared",
|
|
7
|
+
"sealed",
|
|
7
8
|
"publishing",
|
|
8
9
|
"publish_failed",
|
|
9
10
|
"published",
|
|
@@ -18,7 +19,8 @@ const TERMINAL_STATES = new Set(["complete", "abandoned", "failed_permanently"])
|
|
|
18
19
|
const BLOCKED_RECOVERY_STATES = new Set(["abandoned", "failed_permanently"]);
|
|
19
20
|
|
|
20
21
|
const ALLOWED_TRANSITIONS = new Map([
|
|
21
|
-
["prepared", new Set(["publishing", "abandoned", "failed_permanently"])],
|
|
22
|
+
["prepared", new Set(["sealed", "publishing", "abandoned", "failed_permanently"])],
|
|
23
|
+
["sealed", new Set(["publishing", "repair_required", "abandoned", "failed_permanently"])],
|
|
22
24
|
["publishing", new Set(["publish_failed", "published", "repair_required", "abandoned", "failed_permanently"])],
|
|
23
25
|
["publish_failed", new Set(["publishing", "repair_required", "abandoned", "failed_permanently"])],
|
|
24
26
|
["published", new Set(["finalizing", "complete", "repair_required", "abandoned", "failed_permanently"])],
|
|
@@ -209,7 +211,12 @@ export function createReleaseTransaction({
|
|
|
209
211
|
actor = "",
|
|
210
212
|
runId = "",
|
|
211
213
|
} = {}) {
|
|
212
|
-
const id = releaseTransactionId({
|
|
214
|
+
const id = releaseTransactionId({
|
|
215
|
+
repository,
|
|
216
|
+
version,
|
|
217
|
+
sourceSha,
|
|
218
|
+
targetRef,
|
|
219
|
+
});
|
|
213
220
|
const createdAt = nowIso();
|
|
214
221
|
return {
|
|
215
222
|
schema: 1,
|
|
@@ -237,6 +244,10 @@ export function createReleaseTransaction({
|
|
|
237
244
|
failure: "",
|
|
238
245
|
artifacts: [],
|
|
239
246
|
evidence: [],
|
|
247
|
+
sealed_bundle: null,
|
|
248
|
+
publication_state: "prepared",
|
|
249
|
+
milestones: {},
|
|
250
|
+
resume_command: "",
|
|
240
251
|
created_at: createdAt,
|
|
241
252
|
updated_at: createdAt,
|
|
242
253
|
};
|
|
@@ -255,10 +266,11 @@ export function transitionReleaseTransaction(record, nextState, metadata = {}) {
|
|
|
255
266
|
throw new Error(`cannot transition release transaction from ${currentState} to ${nextState}`);
|
|
256
267
|
}
|
|
257
268
|
}
|
|
258
|
-
const nextFailure =
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
269
|
+
const nextFailure =
|
|
270
|
+
nextState === "complete"
|
|
271
|
+
? optionalString(metadata.failure ?? "")
|
|
272
|
+
: optionalString(metadata.failure ?? record.failure);
|
|
273
|
+
const nextRecord = {
|
|
262
274
|
...record,
|
|
263
275
|
previous_state: currentState === nextState ? record.previous_state || "" : currentState,
|
|
264
276
|
state: nextState,
|
|
@@ -268,6 +280,88 @@ export function transitionReleaseTransaction(record, nextState, metadata = {}) {
|
|
|
268
280
|
failure: nextFailure,
|
|
269
281
|
updated_at: nowIso(),
|
|
270
282
|
};
|
|
283
|
+
return {
|
|
284
|
+
...nextRecord,
|
|
285
|
+
publication_state: releaseTransactionPublicationState(nextRecord),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function releaseTransactionPublicationState(record) {
|
|
290
|
+
const state = record?.state || "prepared";
|
|
291
|
+
if (state === "repair_required") {
|
|
292
|
+
return "repair-required";
|
|
293
|
+
}
|
|
294
|
+
if (["abandoned", "failed_permanently"].includes(state)) {
|
|
295
|
+
return state.replaceAll("_", "-");
|
|
296
|
+
}
|
|
297
|
+
const githubReleaseRequired = record?.sealed_bundle?.completion?.githubReleaseRequired === true;
|
|
298
|
+
const githubReleaseComplete = record?.milestones?.github_release?.status === "complete";
|
|
299
|
+
if (state === "complete" && (!githubReleaseRequired || githubReleaseComplete)) {
|
|
300
|
+
return record?.channel === "alpha" ? "alpha-complete" : "release-complete";
|
|
301
|
+
}
|
|
302
|
+
if (
|
|
303
|
+
["published", "finalizing", "complete"].includes(state) ||
|
|
304
|
+
record?.milestones?.package_published?.status === "complete"
|
|
305
|
+
) {
|
|
306
|
+
return "package-published";
|
|
307
|
+
}
|
|
308
|
+
if (state === "sealed" || record?.sealed_bundle?.root) {
|
|
309
|
+
return "sealed";
|
|
310
|
+
}
|
|
311
|
+
if (state === "publishing" || state === "publish_failed") {
|
|
312
|
+
return state.replaceAll("_", "-");
|
|
313
|
+
}
|
|
314
|
+
return "prepared";
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function attachReleaseTransactionSealedBundle(record, sealedBundle, { actor = "", runId = "" } = {}) {
|
|
318
|
+
if (!record || typeof record !== "object") {
|
|
319
|
+
throw new Error("release transaction record must be an object");
|
|
320
|
+
}
|
|
321
|
+
const root = assertNonEmptyString(sealedBundle?.root, "sealedBundle.root");
|
|
322
|
+
if (record.sealed_bundle?.root && record.sealed_bundle.root !== root) {
|
|
323
|
+
throw new Error(`sealed bundle root mismatch: expected ${record.sealed_bundle.root}, got ${root}`);
|
|
324
|
+
}
|
|
325
|
+
if (!["prepared", "sealed"].includes(record.state || "prepared")) {
|
|
326
|
+
if (!record.sealed_bundle?.root) {
|
|
327
|
+
throw new Error(`cannot attach a sealed bundle after transaction entered ${record.state}`);
|
|
328
|
+
}
|
|
329
|
+
return record;
|
|
330
|
+
}
|
|
331
|
+
let next = {
|
|
332
|
+
...record,
|
|
333
|
+
sealed_bundle: sealedBundle,
|
|
334
|
+
resume_command: optionalString(sealedBundle.resumeCommand),
|
|
335
|
+
actor: optionalString(actor || record.actor),
|
|
336
|
+
run_id: optionalString(runId || record.run_id),
|
|
337
|
+
updated_at: nowIso(),
|
|
338
|
+
};
|
|
339
|
+
if ((next.state || "prepared") === "prepared") {
|
|
340
|
+
next = transitionReleaseTransaction(next, "sealed", { actor, runId });
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
...next,
|
|
344
|
+
publication_state: releaseTransactionPublicationState(next),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function recordReleaseTransactionMilestone(record, milestone, details = {}) {
|
|
349
|
+
const normalizedMilestone = assertNonEmptyString(milestone, "milestone").replaceAll("-", "_");
|
|
350
|
+
const next = {
|
|
351
|
+
...record,
|
|
352
|
+
milestones: {
|
|
353
|
+
...(record.milestones || {}),
|
|
354
|
+
[normalizedMilestone]: {
|
|
355
|
+
status: "complete",
|
|
356
|
+
...details,
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
updated_at: nowIso(),
|
|
360
|
+
};
|
|
361
|
+
return {
|
|
362
|
+
...next,
|
|
363
|
+
publication_state: releaseTransactionPublicationState(next),
|
|
364
|
+
};
|
|
271
365
|
}
|
|
272
366
|
|
|
273
367
|
export function readReleaseTransaction(filePath) {
|
|
@@ -284,8 +378,12 @@ export function writeReleaseTransaction(filePath, record) {
|
|
|
284
378
|
return record;
|
|
285
379
|
}
|
|
286
380
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
287
|
-
|
|
288
|
-
|
|
381
|
+
const normalized = {
|
|
382
|
+
...record,
|
|
383
|
+
publication_state: releaseTransactionPublicationState(record),
|
|
384
|
+
};
|
|
385
|
+
fs.writeFileSync(filePath, `${JSON.stringify(normalized, null, 2)}\n`);
|
|
386
|
+
return normalized;
|
|
289
387
|
}
|
|
290
388
|
|
|
291
389
|
function artifactIdentity(artifact) {
|
|
@@ -424,7 +522,9 @@ export function parsePublishArtifactsJson(value, label = "publish artifacts") {
|
|
|
424
522
|
throw new Error(`${label} must be a JSON array`);
|
|
425
523
|
}
|
|
426
524
|
return parsed.map((artifact, index) =>
|
|
427
|
-
normalizePublishArtifact(artifact, `${label}[${index}]`, {
|
|
525
|
+
normalizePublishArtifact(artifact, `${label}[${index}]`, {
|
|
526
|
+
requireDigest: false,
|
|
527
|
+
}),
|
|
428
528
|
);
|
|
429
529
|
}
|
|
430
530
|
|
|
@@ -470,7 +570,9 @@ export function readPublishEvidence(filePath) {
|
|
|
470
570
|
|
|
471
571
|
export function planArtifactPublish({ requiredArtifacts = [], existingArtifacts = [] } = {}) {
|
|
472
572
|
const required = requiredArtifacts.map((artifact, index) =>
|
|
473
|
-
normalizePublishArtifact(artifact, `requiredArtifacts[${index}]`, {
|
|
573
|
+
normalizePublishArtifact(artifact, `requiredArtifacts[${index}]`, {
|
|
574
|
+
requireDigest: false,
|
|
575
|
+
}),
|
|
474
576
|
);
|
|
475
577
|
const existing = existingArtifacts.map((artifact, index) =>
|
|
476
578
|
normalizePublishArtifact(artifact, `existingArtifacts[${index}]`),
|
|
@@ -487,7 +589,11 @@ export function planArtifactPublish({ requiredArtifacts = [], existingArtifacts
|
|
|
487
589
|
const current = candidates.find((candidate) => artifactMatchesRequirement(candidate, artifact));
|
|
488
590
|
if (!current) {
|
|
489
591
|
if (candidates.length > 0) {
|
|
490
|
-
conflicts.push({
|
|
592
|
+
conflicts.push({
|
|
593
|
+
expected: artifact,
|
|
594
|
+
actual: candidates[0],
|
|
595
|
+
fields: ["ref"],
|
|
596
|
+
});
|
|
491
597
|
continue;
|
|
492
598
|
}
|
|
493
599
|
publish.push(artifact);
|
|
@@ -667,7 +773,11 @@ export function planTransactionRecovery({
|
|
|
667
773
|
explicitOverride = false,
|
|
668
774
|
} = {}) {
|
|
669
775
|
if (!transaction) {
|
|
670
|
-
return {
|
|
776
|
+
return {
|
|
777
|
+
action: "prepare",
|
|
778
|
+
blocked: false,
|
|
779
|
+
reason: "no existing transaction",
|
|
780
|
+
};
|
|
671
781
|
}
|
|
672
782
|
const state = transaction.state || "prepared";
|
|
673
783
|
assertKnownState(state);
|
|
@@ -679,19 +789,39 @@ export function planTransactionRecovery({
|
|
|
679
789
|
};
|
|
680
790
|
}
|
|
681
791
|
if (state === "complete") {
|
|
682
|
-
return {
|
|
792
|
+
return {
|
|
793
|
+
action: "inspect",
|
|
794
|
+
blocked: false,
|
|
795
|
+
reason: "transaction is complete",
|
|
796
|
+
};
|
|
683
797
|
}
|
|
684
798
|
if (state === "repair_required" && !explicitOverride) {
|
|
685
|
-
return {
|
|
799
|
+
return {
|
|
800
|
+
action: "blocked",
|
|
801
|
+
blocked: true,
|
|
802
|
+
reason: "transaction requires explicit repair",
|
|
803
|
+
};
|
|
686
804
|
}
|
|
687
805
|
const result = validation || (evidence ? { valid: true } : undefined);
|
|
688
806
|
if (result?.valid) {
|
|
689
|
-
return {
|
|
807
|
+
return {
|
|
808
|
+
action: "finalize",
|
|
809
|
+
blocked: false,
|
|
810
|
+
reason: "publish evidence is valid",
|
|
811
|
+
};
|
|
690
812
|
}
|
|
691
813
|
if (state === "published" || state === "finalizing") {
|
|
692
|
-
return {
|
|
814
|
+
return {
|
|
815
|
+
action: "repair",
|
|
816
|
+
blocked: true,
|
|
817
|
+
reason: "published transaction has invalid or missing evidence",
|
|
818
|
+
};
|
|
693
819
|
}
|
|
694
|
-
return {
|
|
820
|
+
return {
|
|
821
|
+
action: "publish",
|
|
822
|
+
blocked: false,
|
|
823
|
+
reason: "publish evidence is missing or incomplete",
|
|
824
|
+
};
|
|
695
825
|
}
|
|
696
826
|
|
|
697
827
|
export function isReleaseTransactionTerminal(record) {
|