@kungfu-tech/buildchain 4.0.1-alpha.9 → 4.0.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/README.md +9 -6
- package/actions/v4-release-candidate-promote/README.md +3 -0
- package/architecture/agent-change-map.md +1015 -0
- package/architecture/ci-lane-change-budget.json +70 -1
- package/architecture/decisions/0004-v4-declarative-release-tail.md +47 -0
- package/architecture/internal-capabilities.json +156 -11
- package/architecture/maintainability-debt.json +3229 -0
- package/architecture/maintainability-policy.json +480 -129
- package/architecture/v3-core-mechanism-inventory.json +4 -3
- package/architecture/v3-v4-capability-closure.md +59 -0
- package/architecture/v3-v4-live-capability-inventory.json +102404 -0
- package/architecture/v4-cross-platform-adopter-qualification.json +52 -0
- package/architecture/v4-delivery-warrant-shadow-fixtures.json +3 -3
- package/architecture/v4-runtime-semantic-closure.json +710 -0
- package/bin/internal/trust-release-release-handlers.mjs +5 -0
- package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
- package/contracts/v4-publication-qualification-v1.schema.json +51 -0
- package/dist/site/agent-index.json +1 -0
- package/dist/site/artifact-schemas.json +2 -0
- package/dist/site/buildchain-contract.json +123 -32
- package/dist/site/buildchain-site.json +96 -28
- package/dist/site/capability-registry.json +6 -6
- package/dist/site/cli-registry.json +5 -2
- package/dist/site/controller-registry.json +41 -5
- package/dist/site/kfd-claims.json +170 -21
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +6 -6
- package/dist/site/node-api-registry.json +1474 -215
- package/dist/site/page-registry.json +85 -17
- package/dist/site/public-surface-audit.json +85 -20
- package/dist/site/publication-authority-registry.json +28 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +5 -0
- package/dist/site/schemas/v4-publication-qualification-v1.schema.json +51 -0
- package/dist/site/site-manifest.json +10 -10
- package/dist/site/workflow-registry.json +94 -18
- package/docs/MAP.md +1 -1
- package/docs/cli-reference.md +4 -4
- package/docs/dev-delivery-warrant.md +12 -9
- package/docs/node-api-reference.md +291 -206
- package/docs/publish-transaction.md +11 -4
- package/docs/release-candidate.md +7 -0
- package/docs/v4-adopter-delivery.md +23 -4
- package/docs/v4-declarative-release-tail-migration.md +43 -0
- package/package.json +8 -3
- package/packages/core/adopter-delivery-passport.js +36 -0
- package/packages/core/buildchain-contract.js +31 -1
- package/packages/core/buildchain-publication-authority.js +1 -0
- package/packages/core/dev-delivery-warrant-settlement.js +239 -3
- package/packages/core/dev-delivery-warrant-state.js +15 -2
- package/packages/core/dev-delivery-warrant.js +21 -4
- package/packages/core/github-governance-authority.js +4 -4
- package/packages/core/index.js +1 -0
- package/packages/core/paper-scaffold-content.js +401 -0
- package/packages/core/paper.js +12 -378
- package/packages/core/publication-control-plane-audit.js +135 -111
- package/packages/core/publication-rehearsal-projection.js +104 -0
- package/packages/core/publication-rehearsal-runtime.js +190 -0
- package/packages/core/release-candidate-recovery.js +6 -5
- package/packages/core/release-passport.js +82 -184
- package/packages/core/v4-cross-platform-adopter-qualification.js +343 -0
- package/packages/core/v4-floating-consumer-policy.js +3 -2
- package/packages/core/v4-floating-consumer-release-passport.js +134 -15
- package/packages/core/v4-publication-qualification.js +222 -0
- package/scripts/artifact-signing-controller-core.mjs +1 -1
- package/scripts/audit-publication-control-plane.mjs +17 -17
- package/scripts/buildchain-cli-help.mjs +3 -0
- package/scripts/check-internal-architecture.mjs +23 -3
- package/scripts/check-inventory.mjs +23 -5
- package/scripts/check-maintainability.mjs +145 -4
- package/scripts/check-project-cut-merge-queue-admission.mjs +87 -0
- package/scripts/check-v3-v4-capability-inventory.mjs +574 -0
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +10 -3
- package/scripts/check-v4-runtime-semantic-closure.mjs +266 -0
- package/scripts/dev-delivery-warrant-options.mjs +27 -11
- package/scripts/dev-delivery-warrant.mjs +45 -5
- package/scripts/dev-pr-auto-merge.mjs +25 -11
- package/scripts/dev-pr-delivery-warrant.mjs +44 -0
- package/scripts/dev-pr-prequeue-guard.mjs +0 -18
- package/scripts/dispatch-artifact-signing-authority.mjs +1 -1
- package/scripts/generate-agent-change-map.mjs +118 -0
- package/scripts/generate-release-candidate-passport.mjs +26 -1
- package/scripts/generate-site-bundle.mjs +6 -0
- package/scripts/maintainability-domain-metrics.mjs +237 -0
- package/scripts/maintainability-governance.mjs +473 -0
- package/scripts/maintainability-metrics.mjs +28 -6
- package/scripts/project-cut-merge-queue-admission.mjs +394 -0
- package/scripts/promotion-routing-evidence.mjs +35 -6
- package/scripts/release-candidate-resolver.mjs +44 -0
- package/scripts/release-line-policy.mjs +25 -15
- package/scripts/release-tail.mjs +19 -2
- package/scripts/resume-from-candidate-run.mjs +106 -103
- package/scripts/run-lifecycle-core.mjs +341 -376
- package/scripts/site-capability-metadata.mjs +4 -0
- package/scripts/stable-candidate-qualification.mjs +9 -10
- package/scripts/v3-v4-capability-catalog.mjs +439 -0
- package/scripts/v4-cross-platform-adopter-qualification.mjs +228 -0
- package/scripts/v4-declarative-promotion-admission.mjs +42 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
const GIT_OID = /^[0-9a-f]{40}$/u;
|
|
8
|
+
const CONTENT_ROOT = /^sha256:[0-9a-f]{64}$/u;
|
|
9
|
+
const FAMILY_ID = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/u;
|
|
10
|
+
const FAMILY_MARKER =
|
|
11
|
+
/<!-- kungfu-family-queue-lease:v1 ([A-Za-z0-9_-]+) -->/gu;
|
|
12
|
+
const FAMILY_LEASE_SCHEMA = "kungfu.project-cut.family-queue-lease/v1";
|
|
13
|
+
|
|
14
|
+
export class ProjectCutAdmissionError extends Error {
|
|
15
|
+
constructor(reasonCode, message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "ProjectCutAdmissionError";
|
|
18
|
+
this.reasonCode = reasonCode;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function stableValue(value) {
|
|
23
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
24
|
+
if (value && typeof value === "object") {
|
|
25
|
+
return Object.fromEntries(
|
|
26
|
+
Object.entries(value)
|
|
27
|
+
.filter(([, entry]) => entry !== undefined)
|
|
28
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
29
|
+
.map(([key, entry]) => [key, stableValue(entry)]),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function canonicalBytes(value) {
|
|
36
|
+
return Buffer.from(`${JSON.stringify(stableValue(value))}\n`, "utf8");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function contentRoot(value) {
|
|
40
|
+
return `sha256:${crypto.createHash("sha256").update(canonicalBytes(value)).digest("hex")}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function exactRoot(value, label) {
|
|
44
|
+
const normalized = String(value || "")
|
|
45
|
+
.trim()
|
|
46
|
+
.toLowerCase();
|
|
47
|
+
if (!CONTENT_ROOT.test(normalized))
|
|
48
|
+
throw new Error(`${label} must be an exact SHA-256 root`);
|
|
49
|
+
return normalized;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function familyId(value, label) {
|
|
53
|
+
const normalized = String(value || "").trim();
|
|
54
|
+
if (!FAMILY_ID.test(normalized)) throw new Error(`${label} is invalid`);
|
|
55
|
+
return normalized;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function git(
|
|
59
|
+
cwd,
|
|
60
|
+
args,
|
|
61
|
+
{
|
|
62
|
+
env = {},
|
|
63
|
+
input = undefined,
|
|
64
|
+
encoding = "utf8",
|
|
65
|
+
reason = "git-failed",
|
|
66
|
+
} = {},
|
|
67
|
+
) {
|
|
68
|
+
const result = spawnSync("git", args, {
|
|
69
|
+
cwd,
|
|
70
|
+
env: { ...process.env, ...env },
|
|
71
|
+
input,
|
|
72
|
+
encoding,
|
|
73
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
74
|
+
});
|
|
75
|
+
if (result.status !== 0) {
|
|
76
|
+
const detail = String(result.stderr || result.stdout || "").trim();
|
|
77
|
+
throw new ProjectCutAdmissionError(
|
|
78
|
+
reason,
|
|
79
|
+
`git ${args.join(" ")} failed${detail ? `: ${detail}` : ""}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return encoding === null ? result.stdout : String(result.stdout || "").trim();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolveCommit(cwd, revision, label) {
|
|
86
|
+
const oid = git(cwd, ["rev-parse", "--verify", `${revision}^{commit}`], {
|
|
87
|
+
reason: `${label}-unresolved`,
|
|
88
|
+
});
|
|
89
|
+
if (!GIT_OID.test(oid))
|
|
90
|
+
throw new ProjectCutAdmissionError(
|
|
91
|
+
`${label}-unresolved`,
|
|
92
|
+
`${label} is not an exact commit`,
|
|
93
|
+
);
|
|
94
|
+
return oid;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function linearSourceCommits(cwd, fork, head) {
|
|
98
|
+
const commits = git(
|
|
99
|
+
cwd,
|
|
100
|
+
["rev-list", "--reverse", "--topo-order", `${fork}..${head}`],
|
|
101
|
+
{
|
|
102
|
+
reason: "source-history-unreadable",
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
.split(/\s+/u)
|
|
106
|
+
.filter(Boolean);
|
|
107
|
+
if (commits.length === 0)
|
|
108
|
+
throw new ProjectCutAdmissionError(
|
|
109
|
+
"empty-source-range",
|
|
110
|
+
"source range is empty",
|
|
111
|
+
);
|
|
112
|
+
let expectedParent = fork;
|
|
113
|
+
for (const commit of commits) {
|
|
114
|
+
const [observed, ...parents] = git(
|
|
115
|
+
cwd,
|
|
116
|
+
["rev-list", "--parents", "-n", "1", commit],
|
|
117
|
+
{
|
|
118
|
+
reason: "source-history-unreadable",
|
|
119
|
+
},
|
|
120
|
+
).split(/\s+/u);
|
|
121
|
+
if (
|
|
122
|
+
observed !== commit ||
|
|
123
|
+
parents.length !== 1 ||
|
|
124
|
+
parents[0] !== expectedParent
|
|
125
|
+
) {
|
|
126
|
+
throw new ProjectCutAdmissionError(
|
|
127
|
+
"nonlinear-source-range",
|
|
128
|
+
"source range is not one immutable linear patch series",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
expectedParent = commit;
|
|
132
|
+
}
|
|
133
|
+
return commits;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function patchId(cwd, patch) {
|
|
137
|
+
const output = git(cwd, ["patch-id", "--stable"], {
|
|
138
|
+
input: patch,
|
|
139
|
+
encoding: "utf8",
|
|
140
|
+
reason: "patch-id-failed",
|
|
141
|
+
});
|
|
142
|
+
const id = output.split(/\s+/u)[0] || "";
|
|
143
|
+
if (!GIT_OID.test(id))
|
|
144
|
+
throw new ProjectCutAdmissionError(
|
|
145
|
+
"patch-id-failed",
|
|
146
|
+
"patch has no stable identity",
|
|
147
|
+
);
|
|
148
|
+
return id;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function virtualCommitOid(cwd, tree, base, head) {
|
|
152
|
+
const body = [
|
|
153
|
+
`tree ${tree}`,
|
|
154
|
+
`parent ${base}`,
|
|
155
|
+
"author Buildchain Project Cut <project-cut@kungfu.tech> 0 +0000",
|
|
156
|
+
"committer Buildchain Project Cut <project-cut@kungfu.tech> 0 +0000",
|
|
157
|
+
"",
|
|
158
|
+
`Replay ${head} onto ${base}`,
|
|
159
|
+
"",
|
|
160
|
+
].join("\n");
|
|
161
|
+
return git(cwd, ["hash-object", "-t", "commit", "--stdin"], {
|
|
162
|
+
input: body,
|
|
163
|
+
reason: "candidate-commit-hash-failed",
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function replayProjectCut(cwd, base, head) {
|
|
168
|
+
const fork = git(cwd, ["merge-base", base, head], {
|
|
169
|
+
reason: "no-common-fork",
|
|
170
|
+
});
|
|
171
|
+
if (!GIT_OID.test(fork))
|
|
172
|
+
throw new ProjectCutAdmissionError(
|
|
173
|
+
"no-common-fork",
|
|
174
|
+
"base and head have no exact fork",
|
|
175
|
+
);
|
|
176
|
+
const commits = linearSourceCommits(cwd, fork, head);
|
|
177
|
+
const temporary = fs.mkdtempSync(
|
|
178
|
+
path.join(os.tmpdir(), "buildchain-project-cut-"),
|
|
179
|
+
);
|
|
180
|
+
const indexPath = path.join(temporary, "index");
|
|
181
|
+
const indexEnvironment = { GIT_INDEX_FILE: indexPath };
|
|
182
|
+
try {
|
|
183
|
+
git(cwd, ["read-tree", base], {
|
|
184
|
+
env: indexEnvironment,
|
|
185
|
+
reason: "base-tree-unreadable",
|
|
186
|
+
});
|
|
187
|
+
const sourcePatch = git(
|
|
188
|
+
cwd,
|
|
189
|
+
["diff", "--binary", "--full-index", "--no-ext-diff", fork, head],
|
|
190
|
+
{
|
|
191
|
+
encoding: null,
|
|
192
|
+
reason: "source-patch-unreadable",
|
|
193
|
+
},
|
|
194
|
+
);
|
|
195
|
+
const emptySourcePatch = sourcePatch.length === 0;
|
|
196
|
+
if (emptySourcePatch && commits.length !== 1)
|
|
197
|
+
throw new ProjectCutAdmissionError("empty-source-composition", "an empty source composition must be exactly one linear commit");
|
|
198
|
+
if (!emptySourcePatch) git(cwd, ["apply", "--cached", "--3way", "--whitespace=nowarn", "-"], {
|
|
199
|
+
env: indexEnvironment, input: sourcePatch, encoding: "utf8", reason: "project-cut-conflict",
|
|
200
|
+
});
|
|
201
|
+
const candidateTreeOid = git(cwd, ["write-tree"], {
|
|
202
|
+
env: indexEnvironment,
|
|
203
|
+
reason: "candidate-tree-unreadable",
|
|
204
|
+
});
|
|
205
|
+
const replayPatch = git(
|
|
206
|
+
cwd,
|
|
207
|
+
["diff", "--cached", "--binary", "--full-index", "--no-ext-diff", base],
|
|
208
|
+
{
|
|
209
|
+
env: indexEnvironment,
|
|
210
|
+
encoding: null,
|
|
211
|
+
reason: "replay-patch-unreadable",
|
|
212
|
+
},
|
|
213
|
+
);
|
|
214
|
+
const sourceNames = git(
|
|
215
|
+
cwd,
|
|
216
|
+
["diff", "--name-status", "--no-renames", fork, head],
|
|
217
|
+
{
|
|
218
|
+
reason: "source-paths-unreadable",
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
const replayNames = git(
|
|
222
|
+
cwd,
|
|
223
|
+
["diff", "--cached", "--name-status", "--no-renames", base],
|
|
224
|
+
{
|
|
225
|
+
env: indexEnvironment,
|
|
226
|
+
reason: "replay-paths-unreadable",
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
const compositionDrifted = emptySourcePatch ? replayPatch.length !== 0 ||
|
|
230
|
+
sourceNames !== "" || replayNames !== "" : patchId(cwd, sourcePatch) !==
|
|
231
|
+
patchId(cwd, replayPatch) || sourceNames !== replayNames;
|
|
232
|
+
if (compositionDrifted) {
|
|
233
|
+
throw new ProjectCutAdmissionError(
|
|
234
|
+
"composition-drift",
|
|
235
|
+
"latest-base replay changes the immutable source composition",
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
baseCommitOid: base,
|
|
240
|
+
headCommitOid: head,
|
|
241
|
+
forkCommitOid: fork,
|
|
242
|
+
candidateCommitOid: virtualCommitOid(cwd, candidateTreeOid, base, head),
|
|
243
|
+
candidateTreeOid,
|
|
244
|
+
replayedCommitCount: commits.length,
|
|
245
|
+
};
|
|
246
|
+
} finally {
|
|
247
|
+
fs.rmSync(temporary, { recursive: true, force: true });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function encodeMarker(payload) {
|
|
252
|
+
return canonicalBytes(payload)
|
|
253
|
+
.toString("base64")
|
|
254
|
+
.replace(/\+/gu, "-")
|
|
255
|
+
.replace(/\//gu, "_")
|
|
256
|
+
.replace(/=+$/gu, "");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function decodeMarker(token) {
|
|
260
|
+
const padded = `${token}${"=".repeat((4 - (token.length % 4)) % 4)}`
|
|
261
|
+
.replace(/-/gu, "+")
|
|
262
|
+
.replace(/_/gu, "/");
|
|
263
|
+
return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function familyLease(input, replay) {
|
|
267
|
+
const binding = {
|
|
268
|
+
schema: FAMILY_LEASE_SCHEMA,
|
|
269
|
+
state: "active",
|
|
270
|
+
initiativeId: familyId(input.initiativeId, "initiativeId"),
|
|
271
|
+
assignmentId: familyId(input.assignmentId, "assignmentId"),
|
|
272
|
+
deliveryClass: familyId(input.deliveryClass, "deliveryClass"),
|
|
273
|
+
admissionProofRoot: exactRoot(
|
|
274
|
+
input.admissionProofRoot,
|
|
275
|
+
"admissionProofRoot",
|
|
276
|
+
),
|
|
277
|
+
queueAttempt: String(input.queueAttempt || "").trim(),
|
|
278
|
+
pullRequestHead: replay.headCommitOid,
|
|
279
|
+
baseCommitOid: replay.baseCommitOid,
|
|
280
|
+
forkCommitOid: replay.forkCommitOid,
|
|
281
|
+
candidateTreeOid: replay.candidateTreeOid,
|
|
282
|
+
};
|
|
283
|
+
const expectedAttempt = `${replay.headCommitOid}@${replay.baseCommitOid}`;
|
|
284
|
+
if (binding.queueAttempt !== expectedAttempt) {
|
|
285
|
+
throw new ProjectCutAdmissionError(
|
|
286
|
+
"queue-attempt-drift",
|
|
287
|
+
`queueAttempt must equal ${expectedAttempt}`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
const bindingRoot = contentRoot(binding);
|
|
291
|
+
const statusContext = `Queue family lease/${bindingRoot.slice(7, 23)}`;
|
|
292
|
+
const payload = { ...binding, bindingRoot, statusContext };
|
|
293
|
+
const leaseRoot = contentRoot(payload);
|
|
294
|
+
const sealed = { ...payload, leaseRoot };
|
|
295
|
+
return {
|
|
296
|
+
marker: `<!-- kungfu-family-queue-lease:v1 ${encodeMarker(sealed)} -->`,
|
|
297
|
+
leaseRoot,
|
|
298
|
+
statusContext,
|
|
299
|
+
pullRequestHead: replay.headCommitOid,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function parseFamilyQueueLeaseMarker(text) {
|
|
304
|
+
const matches = [...String(text || "").matchAll(FAMILY_MARKER)];
|
|
305
|
+
if (matches.length !== 1) return null;
|
|
306
|
+
try {
|
|
307
|
+
const payload = decodeMarker(matches[0][1]);
|
|
308
|
+
const { leaseRoot, bindingRoot, statusContext, ...binding } = payload;
|
|
309
|
+
if (payload.schema !== FAMILY_LEASE_SCHEMA || payload.state !== "active")
|
|
310
|
+
return null;
|
|
311
|
+
if (contentRoot(binding) !== bindingRoot) return null;
|
|
312
|
+
if (statusContext !== `Queue family lease/${bindingRoot.slice(7, 23)}`)
|
|
313
|
+
return null;
|
|
314
|
+
if (contentRoot({ ...binding, bindingRoot, statusContext }) !== leaseRoot)
|
|
315
|
+
return null;
|
|
316
|
+
if (encodeMarker(payload) !== matches[0][1]) return null;
|
|
317
|
+
exactRoot(leaseRoot, "leaseRoot");
|
|
318
|
+
exactRoot(payload.admissionProofRoot, "admissionProofRoot");
|
|
319
|
+
for (const field of [
|
|
320
|
+
"pullRequestHead",
|
|
321
|
+
"baseCommitOid",
|
|
322
|
+
"forkCommitOid",
|
|
323
|
+
"candidateTreeOid",
|
|
324
|
+
]) {
|
|
325
|
+
if (!GIT_OID.test(String(payload[field] || ""))) return null;
|
|
326
|
+
}
|
|
327
|
+
return payload;
|
|
328
|
+
} catch {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function releaseFamilyQueueLease(markerText, input = {}) {
|
|
334
|
+
const lease = parseFamilyQueueLeaseMarker(markerText);
|
|
335
|
+
if (!lease) throw new Error("family queue lease marker is invalid");
|
|
336
|
+
const expectedHead = String(input.expectedPullRequestHead || "")
|
|
337
|
+
.trim()
|
|
338
|
+
.toLowerCase();
|
|
339
|
+
if (!GIT_OID.test(expectedHead) || expectedHead !== lease.pullRequestHead) {
|
|
340
|
+
throw new Error("family queue lease pull request head drifted");
|
|
341
|
+
}
|
|
342
|
+
const terminalReason = String(input.terminalReason || "").trim();
|
|
343
|
+
if (!["controller-failed", "merged"].includes(terminalReason))
|
|
344
|
+
throw new Error("terminalReason is invalid");
|
|
345
|
+
const evidenceRoot = input.evidenceRoot
|
|
346
|
+
? exactRoot(input.evidenceRoot, "evidenceRoot")
|
|
347
|
+
: null;
|
|
348
|
+
if (terminalReason === "merged" && !evidenceRoot)
|
|
349
|
+
throw new Error("merged family lease requires evidenceRoot");
|
|
350
|
+
const receipt = {
|
|
351
|
+
schema: "project.cut.family-queue-release/v1",
|
|
352
|
+
state: "released",
|
|
353
|
+
leaseRoot: lease.leaseRoot,
|
|
354
|
+
pullRequestHead: lease.pullRequestHead,
|
|
355
|
+
terminalReason,
|
|
356
|
+
evidenceRoot,
|
|
357
|
+
};
|
|
358
|
+
return { ...receipt, releaseRoot: contentRoot(receipt) };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function qualifyProjectCut(input = {}) {
|
|
362
|
+
const cwd = path.resolve(input.cwd || process.cwd());
|
|
363
|
+
const base = resolveCommit(cwd, input.base, "base");
|
|
364
|
+
const head = resolveCommit(cwd, input.head, "head");
|
|
365
|
+
const replay = replayProjectCut(cwd, base, head);
|
|
366
|
+
const familyInputs = [
|
|
367
|
+
input.initiativeId,
|
|
368
|
+
input.assignmentId,
|
|
369
|
+
input.deliveryClass,
|
|
370
|
+
input.queueAttempt,
|
|
371
|
+
input.admissionProofRoot,
|
|
372
|
+
];
|
|
373
|
+
const familyCount = familyInputs.filter((value) =>
|
|
374
|
+
String(value || "").trim(),
|
|
375
|
+
).length;
|
|
376
|
+
if (familyCount !== 0 && familyCount !== familyInputs.length) {
|
|
377
|
+
throw new ProjectCutAdmissionError(
|
|
378
|
+
"partial-family-binding",
|
|
379
|
+
"family admission inputs must be all present or all absent",
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
schema: "project.cut.merge-queue-admission/v1",
|
|
384
|
+
ok: true,
|
|
385
|
+
decision: "qualified",
|
|
386
|
+
retryable: false,
|
|
387
|
+
compositionChanged: false,
|
|
388
|
+
reasonCodes: [],
|
|
389
|
+
...replay,
|
|
390
|
+
...(familyCount === familyInputs.length
|
|
391
|
+
? { familyLease: familyLease(input, replay) }
|
|
392
|
+
: {}),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
2
|
import fs from "node:fs";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { pathToFileURL } from "node:url";
|
|
6
|
-
|
|
7
5
|
function createPromotionRoutingEvidence({
|
|
8
6
|
env = process.env,
|
|
9
7
|
cwd = process.cwd(),
|
|
@@ -55,13 +53,45 @@ function createPromotionRoutingEvidence({
|
|
|
55
53
|
}
|
|
56
54
|
return { routing, routingPath, candidatePath };
|
|
57
55
|
}
|
|
58
|
-
|
|
56
|
+
function bundlePromotionControllerEvidence({
|
|
57
|
+
env = process.env,
|
|
58
|
+
cwd = process.cwd(),
|
|
59
|
+
} = {}) {
|
|
60
|
+
const finalizationNeeded = env.FINALIZATION_NEEDED === "true";
|
|
61
|
+
const files = [
|
|
62
|
+
[env.RELEASE_CANDIDATE_PASSPORT, "release-candidate-passport.json"],
|
|
63
|
+
[env.PUBLISH_EVIDENCE, "publish-evidence.json"],
|
|
64
|
+
...(!finalizationNeeded
|
|
65
|
+
? [[env.RELEASE_PASSPORT, "release-passport.json"]]
|
|
66
|
+
: []),
|
|
67
|
+
...(env.PUBLICATION_COMMIT_ENABLED === "true" && !finalizationNeeded
|
|
68
|
+
? [[env.PUBLICATION_COMMIT_EVIDENCE, "publication-commit-evidence.json"]]
|
|
69
|
+
: []),
|
|
70
|
+
...(!finalizationNeeded ? [
|
|
71
|
+
[env.RELEASE_ACTIVATION_RECEIPTS, "release-activation-receipt-set.json"],
|
|
72
|
+
[env.RELEASED_PRODUCT_EVIDENCE, "released-product-evidence.json"],
|
|
73
|
+
[env.RELEASE_PROPAGATION_CAPTURE, "release-propagation-capture.json"],
|
|
74
|
+
].filter(([source]) => source) : []),
|
|
75
|
+
];
|
|
76
|
+
const outputDir = path.join(cwd, ".buildchain/controller/promotion-evidence");
|
|
77
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
78
|
+
for (const [source, name] of files) {
|
|
79
|
+
if (!source || !fs.existsSync(path.resolve(cwd, source)))
|
|
80
|
+
throw new Error(`promotion controller evidence is missing: ${source || name}`);
|
|
81
|
+
fs.copyFileSync(path.resolve(cwd, source), path.join(outputDir, name));
|
|
82
|
+
}
|
|
83
|
+
return { files: files.map(([, name]) => name), outputDir };
|
|
84
|
+
}
|
|
59
85
|
if (
|
|
60
86
|
process.argv[1] &&
|
|
61
87
|
import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href
|
|
62
88
|
) {
|
|
63
89
|
try {
|
|
64
|
-
|
|
90
|
+
if (process.argv[2] === "bundle-controller-evidence") {
|
|
91
|
+
bundlePromotionControllerEvidence();
|
|
92
|
+
} else {
|
|
93
|
+
createPromotionRoutingEvidence();
|
|
94
|
+
}
|
|
65
95
|
} catch (error) {
|
|
66
96
|
console.error(
|
|
67
97
|
`promotion routing evidence failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -69,5 +99,4 @@ if (
|
|
|
69
99
|
process.exitCode = 1;
|
|
70
100
|
}
|
|
71
101
|
}
|
|
72
|
-
|
|
73
|
-
export { createPromotionRoutingEvidence };
|
|
102
|
+
export { bundlePromotionControllerEvidence, createPromotionRoutingEvidence };
|
|
@@ -9,6 +9,8 @@ import { pipeline } from "node:stream/promises";
|
|
|
9
9
|
import { pathToFileURL } from "node:url";
|
|
10
10
|
import { createResolvedPublicationSealedBundle } from "./publication-candidate-sealer.mjs";
|
|
11
11
|
import { writeGitHubOutputs } from "./build-contract-core.mjs";
|
|
12
|
+
import { v4ContentRoot } from "../packages/core/v4-canonical-contracts.js";
|
|
13
|
+
import { v4PublicationQualificationRoot, validateV4PublicationQualificationReceipt } from "../packages/core/v4-publication-qualification.js";
|
|
12
14
|
|
|
13
15
|
const DEFAULT_WORKFLOW_FILE = "build-surface-fixture.yml";
|
|
14
16
|
|
|
@@ -434,6 +436,42 @@ function findDownloadedFile(root, filename) {
|
|
|
434
436
|
return "";
|
|
435
437
|
}
|
|
436
438
|
|
|
439
|
+
function resolveV4PublicationEvidence(passportDir, passport, outputPath) {
|
|
440
|
+
const stageCapsulesPath = findDownloadedFile(passportDir, "release-candidate-stage-capsules.json");
|
|
441
|
+
const publicationQualificationPath = findDownloadedFile(passportDir, "release-candidate-publication-qualification.json");
|
|
442
|
+
if (!passport.consumerPolicy?.receiptRoot) {
|
|
443
|
+
return {
|
|
444
|
+
paths: {
|
|
445
|
+
stageCapsules: stageCapsulesPath ? outputPath(stageCapsulesPath) : "",
|
|
446
|
+
publicationQualification: publicationQualificationPath ? outputPath(publicationQualificationPath) : "",
|
|
447
|
+
},
|
|
448
|
+
publicationQualificationRoot: "",
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
if (!stageCapsulesPath || !publicationQualificationPath) {
|
|
452
|
+
throw new Error("v4 release candidate is missing Stage Capsules or publication qualification receipt");
|
|
453
|
+
}
|
|
454
|
+
const stageCapsules = JSON.parse(fs.readFileSync(stageCapsulesPath, "utf8"));
|
|
455
|
+
const qualification = JSON.parse(fs.readFileSync(publicationQualificationPath, "utf8"));
|
|
456
|
+
validateV4PublicationQualificationReceipt(qualification, {
|
|
457
|
+
repository: passport.repository,
|
|
458
|
+
candidateRoot: `sha256:${passport.candidateHash}`,
|
|
459
|
+
sourceSha: passport.source.headSha,
|
|
460
|
+
sourceRoot: v4ContentRoot("candidate-identity", passport.source),
|
|
461
|
+
artifactRoot: v4PublicationQualificationRoot(stageCapsules.capsules.map(({ publicationArtifact }) => publicationArtifact)),
|
|
462
|
+
policyDigest: passport.consumerPolicy.receiptRoot,
|
|
463
|
+
});
|
|
464
|
+
if (stageCapsules.publicationQualificationRoot !== qualification.receiptRoot) throw new Error("Stage Capsules do not bind the publication qualification receipt");
|
|
465
|
+
if (stageCapsules.capsules.some(({ capsule }) => capsule.identity.qualificationRoot !== qualification.receiptRoot)) throw new Error("a Stage Capsule does not bind the publication qualification receipt");
|
|
466
|
+
return {
|
|
467
|
+
paths: {
|
|
468
|
+
stageCapsules: outputPath(stageCapsulesPath),
|
|
469
|
+
publicationQualification: outputPath(publicationQualificationPath),
|
|
470
|
+
},
|
|
471
|
+
publicationQualificationRoot: qualification.receiptRoot,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
437
475
|
export async function resolveReleaseCandidateArtifacts({
|
|
438
476
|
repository,
|
|
439
477
|
targetRef,
|
|
@@ -667,6 +705,7 @@ export async function resolveReleaseCandidateArtifacts({
|
|
|
667
705
|
throw new Error("downloaded release-candidate artifacts did not contain release-candidate-passport.json and build-summary.json");
|
|
668
706
|
}
|
|
669
707
|
const passport = JSON.parse(fs.readFileSync(passportPath, "utf8"));
|
|
708
|
+
const v4Publication = resolveV4PublicationEvidence(passportDir, passport, outputPath);
|
|
670
709
|
const platformManifestPaths = findDownloadedFiles(payloadDir, "manifest.json");
|
|
671
710
|
const githubArtifactAttestationPolicyPaths = findDownloadedFiles(
|
|
672
711
|
payloadDir,
|
|
@@ -722,6 +761,7 @@ export async function resolveReleaseCandidateArtifacts({
|
|
|
722
761
|
paths: {
|
|
723
762
|
passport: outputPath(passportPath),
|
|
724
763
|
buildSummary: outputPath(buildSummaryPath),
|
|
764
|
+
...v4Publication.paths,
|
|
725
765
|
payloads: outputPath(payloadDir),
|
|
726
766
|
platformManifests: platformManifestPaths.map(outputPath),
|
|
727
767
|
githubArtifactAttestationPolicies: githubArtifactAttestationPolicyPaths.map(outputPath),
|
|
@@ -738,6 +778,7 @@ export async function resolveReleaseCandidateArtifacts({
|
|
|
738
778
|
githubArtifactAttestationPolicyCount: githubArtifactAttestationPolicyPaths.length,
|
|
739
779
|
npmTarballCount: npmTarballPaths.length,
|
|
740
780
|
publishRequiredArtifacts: generatedRequiredArtifacts,
|
|
781
|
+
publicationQualificationRoot: v4Publication.publicationQualificationRoot,
|
|
741
782
|
};
|
|
742
783
|
}
|
|
743
784
|
|
|
@@ -766,6 +807,9 @@ export async function resolveReleaseCandidateArtifactsCli() {
|
|
|
766
807
|
"promote-only-release-candidate": String(result.enabled === true),
|
|
767
808
|
"release-candidate-passport-path": result.paths?.passport || "",
|
|
768
809
|
"release-candidate-build-summary-path": result.paths?.buildSummary || "",
|
|
810
|
+
"release-candidate-stage-capsules-path": result.paths?.stageCapsules || "",
|
|
811
|
+
"release-candidate-publication-qualification-path": result.paths?.publicationQualification || "",
|
|
812
|
+
"release-candidate-publication-qualification-root": result.publicationQualificationRoot || "",
|
|
769
813
|
"release-candidate-version": result.version || "",
|
|
770
814
|
"release-candidate-source-sha": result.artifacts?.sourceSha || "",
|
|
771
815
|
"release-candidate-artifact": result.artifacts?.passport || "",
|
|
@@ -47,16 +47,27 @@ export function parseVersionStateRef(ref) {
|
|
|
47
47
|
|
|
48
48
|
export function parseReleaseLineRecoveryRef(ref) {
|
|
49
49
|
const normalizedRef = normalizeRef(ref);
|
|
50
|
-
const match = normalizedRef.match(/^fix\/release-line-v(\d+)-v(\d+\.\d+)-[0-9A-Za-z._-]+$/);
|
|
50
|
+
const match = normalizedRef.match(/^fix\/(alpha|release)-line-v(\d+)-v(\d+\.\d+)-[0-9A-Za-z._-]+$/);
|
|
51
51
|
if (!match) return undefined;
|
|
52
52
|
return {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
channel: match[1],
|
|
54
|
+
major: Number(match[2]),
|
|
55
|
+
loose: Number(match[3]),
|
|
56
|
+
normalizedRef: `${match[1]}/v${match[2]}/v${match[3]}`,
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
function getRecoveryBumpKeyword({ target, baseRef, baseChannel, version, looseVersionNumber, headRef }) {
|
|
61
|
+
if (!target) return "";
|
|
62
|
+
if (baseChannel !== target.channel || target.normalizedRef !== normalizeRef(baseRef)) {
|
|
63
|
+
throw new Error(`Versions not match for head/base refs: ${headRef} -> ${baseRef}`);
|
|
64
|
+
}
|
|
65
|
+
if (target.major !== version.major || target.loose !== looseVersionNumber) {
|
|
66
|
+
throw new Error(`The version of head ref ${headRef} does not match current ${version.version}`);
|
|
67
|
+
}
|
|
68
|
+
return baseChannel === "release" ? "patch" : "prerelease";
|
|
69
|
+
}
|
|
70
|
+
|
|
60
71
|
export function parsePublishGateChannelRef(ref) {
|
|
61
72
|
const normalizedRef = normalizeRef(ref);
|
|
62
73
|
const match = normalizedRef.match(/^publish-gate\/(alpha|release)\/(v(\d+)\/v(\d+\.\d+))\/[^/]+$/);
|
|
@@ -131,6 +142,14 @@ export function getBumpKeyword({ cwd = process.cwd(), headRef, baseRef, loose =
|
|
|
131
142
|
const authorityTarget = parseAuthorityRef(baseRef);
|
|
132
143
|
const headChannel = getChannel(headRef);
|
|
133
144
|
const baseChannel = getChannel(baseRef);
|
|
145
|
+
const recoveryKeyword = getRecoveryBumpKeyword({
|
|
146
|
+
target: releaseLineRecoveryTarget,
|
|
147
|
+
baseRef,
|
|
148
|
+
baseChannel,
|
|
149
|
+
version,
|
|
150
|
+
looseVersionNumber,
|
|
151
|
+
headRef,
|
|
152
|
+
});
|
|
134
153
|
const key = `${headChannel}->${baseChannel}`;
|
|
135
154
|
const keywords = {
|
|
136
155
|
"dev->alpha": "prerelease",
|
|
@@ -157,16 +176,7 @@ export function getBumpKeyword({ cwd = process.cwd(), headRef, baseRef, loose =
|
|
|
157
176
|
return "none";
|
|
158
177
|
}
|
|
159
178
|
|
|
160
|
-
if (
|
|
161
|
-
if (baseChannel !== "release" || releaseLineRecoveryTarget.normalizedRef !== normalizeRef(baseRef)) {
|
|
162
|
-
throw new Error(`Versions not match for head/base refs: ${headRef} -> ${baseRef}`);
|
|
163
|
-
}
|
|
164
|
-
const mismatchMsg = `The version of head ref ${headRef} does not match current ${version.version}`;
|
|
165
|
-
if (releaseLineRecoveryTarget.major !== version.major || releaseLineRecoveryTarget.loose !== looseVersionNumber) {
|
|
166
|
-
throw new Error(mismatchMsg);
|
|
167
|
-
}
|
|
168
|
-
return "patch";
|
|
169
|
-
}
|
|
179
|
+
if (recoveryKeyword) return recoveryKeyword;
|
|
170
180
|
|
|
171
181
|
if (publishGateTarget) {
|
|
172
182
|
if (publishGateTarget.channel !== baseChannel || publishGateTarget.normalizedRef !== normalizeRef(baseRef)) {
|
package/scripts/release-tail.mjs
CHANGED
|
@@ -42,7 +42,8 @@ export async function runReleaseTailCli(args = process.argv.slice(2)) {
|
|
|
42
42
|
const [mode = "status", ...options] = args;
|
|
43
43
|
if (mode === "rehearse") {
|
|
44
44
|
const capsulePath = flag(options, "capsule");
|
|
45
|
-
const candidateRoot =
|
|
45
|
+
const candidateRoot =
|
|
46
|
+
flag(options, "candidate-root") || flag(options, "capsule-root");
|
|
46
47
|
const statePath = flag(options, "state");
|
|
47
48
|
const evidencePath = flag(options, "evidence");
|
|
48
49
|
for (const [name, value] of Object.entries({
|
|
@@ -59,6 +60,22 @@ export async function runReleaseTailCli(args = process.argv.slice(2)) {
|
|
|
59
60
|
"local rehearsal --mode must be simulate or replay; authorized provider rehearsal uses the reusable workflow",
|
|
60
61
|
);
|
|
61
62
|
try {
|
|
63
|
+
const environmentInput = flag(options, "environment-json");
|
|
64
|
+
const environment = environmentInput
|
|
65
|
+
? readJson(environmentInput, "--environment-json")
|
|
66
|
+
: {};
|
|
67
|
+
if (
|
|
68
|
+
!environment ||
|
|
69
|
+
typeof environment !== "object" ||
|
|
70
|
+
Array.isArray(environment) ||
|
|
71
|
+
Object.keys(environment).length > 0
|
|
72
|
+
) {
|
|
73
|
+
const error = new Error(
|
|
74
|
+
"--environment-json must be an empty object; v4 binds every semantic input into the source-bound capsule",
|
|
75
|
+
);
|
|
76
|
+
error.code = "ambient-environment-forbidden";
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
62
79
|
const result = await executeV4PublicationRehearsal({
|
|
63
80
|
capsule: readJson(capsulePath, "--capsule"),
|
|
64
81
|
candidateRoot,
|
|
@@ -143,7 +160,7 @@ export async function runReleaseTailCli(args = process.argv.slice(2)) {
|
|
|
143
160
|
return report;
|
|
144
161
|
}
|
|
145
162
|
throw new Error(
|
|
146
|
-
"usage: buildchain release-tail <plan|init|status|verify|compat|rehearse> [--declaration <json-or-path>] [--capsule <absolute-path>] [--candidate-root <absolute-path>] [--mode <simulate|replay>] [--state <absolute-path>] [--evidence <absolute-path>] [--hooks-json <json-or-path>] [--output <path>]",
|
|
163
|
+
"usage: buildchain release-tail <plan|init|status|verify|compat|rehearse> [--declaration <json-or-path>] [--capsule <absolute-path>] [--candidate-root <absolute-path>|--capsule-root <absolute-path>] [--environment-json <empty-json-or-path>] [--mode <simulate|replay>] [--state <absolute-path>] [--evidence <absolute-path>] [--hooks-json <json-or-path>] [--output <path>]",
|
|
147
164
|
);
|
|
148
165
|
}
|
|
149
166
|
|