@kungfu-tech/buildchain 4.0.0 → 4.0.1-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/AGENTS.md +13 -5
- package/actions/promote-buildchain-ref/README.md +11 -6
- package/architecture/internal-capabilities.json +11 -2
- package/architecture/maintainability-policy.json +149 -17
- package/architecture/v4-floating-consumer-policy.json +75 -0
- package/architecture/v4-floating-consumer-policy.md +32 -0
- package/architecture/v4-runtime-ref-resume-authority.json +64 -0
- package/architecture/v4-stage-capsule-qualification.json +2 -2
- package/bin/internal/trust-release-release-handlers.mjs +5 -0
- package/contracts/fixtures/v4-floating-consumer-policy-v1/cases.json +53 -0
- package/contracts/fixtures/v4-runtime-ref-resume-authority-v1/scenario.json +15 -0
- package/contracts/v4-floating-consumer-policy-receipt-v1.schema.json +105 -0
- package/contracts/v4-runtime-ref-resume-authority-v1.schema.json +235 -0
- package/dist/site/buildchain-contract.json +101 -31
- package/dist/site/buildchain-site.json +73 -27
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/controller-registry.json +62 -6
- package/dist/site/kfd-claims.json +68 -13
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +6 -6
- package/dist/site/node-api-registry.json +799 -127
- package/dist/site/page-registry.json +63 -17
- package/dist/site/public-surface-audit.json +49 -17
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +2 -0
- package/dist/site/site-manifest.json +10 -10
- package/dist/site/workflow-registry.json +46 -18
- package/docs/MAP.md +2 -0
- package/docs/dev-delivery-warrant.md +19 -1
- package/docs/lifecycle-protocol.md +41 -0
- package/docs/node-api-reference.md +207 -134
- package/docs/reusable-build-surface.md +41 -0
- package/docs/runtime-train-validation.md +10 -0
- package/docs/v4-runtime-ref-resume-authority.md +69 -0
- package/docs/versioning.md +1 -0
- package/package.json +6 -4
- package/packages/core/artifact-signing.js +61 -0
- package/packages/core/buildchain-config.js +66 -1
- package/packages/core/dev-delivery-proof.js +37 -3
- package/packages/core/dev-delivery-warrant.js +3 -3
- package/packages/core/index.js +2 -0
- package/packages/core/publication-authority.js +5 -5
- package/packages/core/release-candidate.js +79 -19
- package/packages/core/release-passport.js +239 -33
- package/packages/core/v4-floating-consumer-evidence.js +324 -0
- package/packages/core/v4-floating-consumer-policy.js +446 -0
- package/packages/core/v4-floating-consumer-release-passport.js +126 -0
- package/packages/core/v4-runtime-ref-resume-authority.js +625 -0
- package/packages/core/v4-runtime-selector-persistence.js +228 -0
- package/packages/core/workflow-yaml-contract.js +48 -0
- package/scripts/audit-publication-control-plane.mjs +31 -31
- package/scripts/check-inventory.mjs +1 -1
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +198 -0
- package/scripts/check-v4-public-dogfood-contract.mjs +6 -11
- package/scripts/dev-delivery-source-proof-reuse.mjs +631 -0
- package/scripts/dev-pr-auto-merge.mjs +37 -48
- package/scripts/dev-pr-delivery-warrant.mjs +205 -2
- package/scripts/dev-pr-prequeue-guard.mjs +399 -0
- package/scripts/ensure-github-release.mjs +3 -3
- package/scripts/generate-channel-build-workflow.mjs +3 -0
- package/scripts/generate-channel-promotion-workflow.mjs +112 -12
- package/scripts/generate-release-candidate-passport.mjs +41 -33
- package/scripts/init-repo.mjs +5 -1
- package/scripts/inspect-artifact-signing-requests.mjs +6 -0
- package/scripts/npm-publish-transaction.mjs +101 -89
- package/scripts/resume-from-candidate-run.mjs +11 -14
- package/scripts/seal-artifact-signing-requests.mjs +6 -0
- package/scripts/site-capability-metadata.mjs +2 -0
- package/scripts/v4-consumer-policy.mjs +231 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
classifyDevDeliveryDelta,
|
|
5
|
+
verifyProjectCutReplayProof,
|
|
6
|
+
} from "../packages/core/dev-delivery-warrant.js";
|
|
7
|
+
|
|
8
|
+
function mismatch(code, details = {}) {
|
|
9
|
+
const error = new Error(code);
|
|
10
|
+
error.code = code;
|
|
11
|
+
Object.assign(error, details);
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function readOptionalJson(file) {
|
|
16
|
+
if (!file) return null;
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function attributedBaseDelta(data, previousBase) {
|
|
25
|
+
const files = Array.isArray(data?.files) ? data.files : [];
|
|
26
|
+
const graphKnown =
|
|
27
|
+
data?.status === "ahead" &&
|
|
28
|
+
data?.merge_base_commit?.sha === previousBase &&
|
|
29
|
+
files.length < 300;
|
|
30
|
+
const renames = files
|
|
31
|
+
.filter((entry) => entry.status === "renamed")
|
|
32
|
+
.map((entry) => ({
|
|
33
|
+
from: String(entry.previous_filename || ""),
|
|
34
|
+
to: String(entry.filename || ""),
|
|
35
|
+
}));
|
|
36
|
+
const attributionComplete =
|
|
37
|
+
graphKnown &&
|
|
38
|
+
files.every((entry) => String(entry.filename || "")) &&
|
|
39
|
+
renames.every((entry) => entry.from && entry.to);
|
|
40
|
+
return {
|
|
41
|
+
graphKnown,
|
|
42
|
+
attributionComplete,
|
|
43
|
+
changedPaths: attributionComplete
|
|
44
|
+
? [
|
|
45
|
+
...new Set(
|
|
46
|
+
files
|
|
47
|
+
.flatMap((entry) => [
|
|
48
|
+
String(entry.filename || ""),
|
|
49
|
+
String(entry.previous_filename || ""),
|
|
50
|
+
])
|
|
51
|
+
.filter(Boolean),
|
|
52
|
+
),
|
|
53
|
+
].sort()
|
|
54
|
+
: [],
|
|
55
|
+
renames: attributionComplete ? renames : [],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function reuseFailure(decision) {
|
|
60
|
+
if (decision.reason === "dev-delta-overlaps-affected-closure") {
|
|
61
|
+
return "pre-enqueue-base-delta-overlap";
|
|
62
|
+
}
|
|
63
|
+
if (
|
|
64
|
+
[
|
|
65
|
+
"dependency-attribution-unknown",
|
|
66
|
+
"affected-closure-paths-unknown",
|
|
67
|
+
].includes(decision.reason)
|
|
68
|
+
) {
|
|
69
|
+
return "pre-enqueue-base-attribution-unknown";
|
|
70
|
+
}
|
|
71
|
+
return "pre-enqueue-source-proof-drift";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function githubBaseDelta(
|
|
75
|
+
client,
|
|
76
|
+
options,
|
|
77
|
+
previousBaseSha,
|
|
78
|
+
currentBaseSha,
|
|
79
|
+
) {
|
|
80
|
+
if (typeof client.getBaseDelta === "function") {
|
|
81
|
+
return client.getBaseDelta(previousBaseSha, currentBaseSha);
|
|
82
|
+
}
|
|
83
|
+
const { data } = await client.request(
|
|
84
|
+
"GET",
|
|
85
|
+
`/repos/${options.repository.owner}/${options.repository.repo}/compare/${previousBaseSha}...${currentBaseSha}`,
|
|
86
|
+
);
|
|
87
|
+
return data;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function githubCommitTree(client, options, mergeCommitSha) {
|
|
91
|
+
if (typeof client.getCommitTree === "function") {
|
|
92
|
+
return client.getCommitTree(mergeCommitSha);
|
|
93
|
+
}
|
|
94
|
+
const { data } = await client.request(
|
|
95
|
+
"GET",
|
|
96
|
+
`/repos/${options.repository.owner}/${options.repository.repo}/git/commits/${mergeCommitSha}`,
|
|
97
|
+
);
|
|
98
|
+
return String(data?.tree?.sha || "").toLowerCase();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function projectCutQualification(pr, options, client) {
|
|
102
|
+
if (!options.projectCutProofPath) {
|
|
103
|
+
return { ok: false, reason: "project-cut-proof-required" };
|
|
104
|
+
}
|
|
105
|
+
let proof;
|
|
106
|
+
try {
|
|
107
|
+
proof = JSON.parse(fs.readFileSync(options.projectCutProofPath, "utf8"));
|
|
108
|
+
} catch {
|
|
109
|
+
return { ok: false, reason: "project-cut-proof-invalid" };
|
|
110
|
+
}
|
|
111
|
+
const currentBase = await client
|
|
112
|
+
.getBranchSha(options.targetBranch)
|
|
113
|
+
.catch(() => "");
|
|
114
|
+
const verification = verifyProjectCutReplayProof(proof, {
|
|
115
|
+
repository: options.repository.fullName,
|
|
116
|
+
protectedBase: options.targetBranch,
|
|
117
|
+
pullRequestNumber: Number(pr.number),
|
|
118
|
+
sourceHead: String(pr.head?.sha || "").toLowerCase(),
|
|
119
|
+
...(options.sourcePatchRoot
|
|
120
|
+
? { sourcePatchRoot: options.sourcePatchRoot }
|
|
121
|
+
: {}),
|
|
122
|
+
currentBase,
|
|
123
|
+
});
|
|
124
|
+
return verification.ok
|
|
125
|
+
? {
|
|
126
|
+
ok: true,
|
|
127
|
+
reason: verification.reason,
|
|
128
|
+
proofRoot: verification.proofRoot,
|
|
129
|
+
currentBase,
|
|
130
|
+
}
|
|
131
|
+
: {
|
|
132
|
+
ok: false,
|
|
133
|
+
reason: `project-cut-${verification.reason}`,
|
|
134
|
+
currentBase,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function qualifyProjectCut({
|
|
139
|
+
client,
|
|
140
|
+
options,
|
|
141
|
+
pullRequest,
|
|
142
|
+
previousBaseSha,
|
|
143
|
+
currentBaseSha,
|
|
144
|
+
expectedHeadSha,
|
|
145
|
+
observedPullRequest,
|
|
146
|
+
projectCut,
|
|
147
|
+
mergeableAccepted,
|
|
148
|
+
root,
|
|
149
|
+
}) {
|
|
150
|
+
const observedHeadSha = String(
|
|
151
|
+
observedPullRequest.head?.sha || "",
|
|
152
|
+
).toLowerCase();
|
|
153
|
+
if (observedHeadSha !== expectedHeadSha) {
|
|
154
|
+
mismatch("head-sha-drift-after-lease-readback", { observedHeadSha });
|
|
155
|
+
}
|
|
156
|
+
if (observedPullRequest.mergeable === false) {
|
|
157
|
+
mismatch("pre-enqueue-merge-conflict");
|
|
158
|
+
}
|
|
159
|
+
if (
|
|
160
|
+
!mergeableAccepted(observedPullRequest, "queue", projectCut?.ok === true)
|
|
161
|
+
) {
|
|
162
|
+
mismatch("not-mergeable-after-latest-base-replay");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let reuseDecision = null;
|
|
166
|
+
if (currentBaseSha !== previousBaseSha) {
|
|
167
|
+
const observedPullRequestBase = String(
|
|
168
|
+
observedPullRequest.base?.sha || "",
|
|
169
|
+
).toLowerCase();
|
|
170
|
+
if (observedPullRequestBase !== currentBaseSha) {
|
|
171
|
+
mismatch("pre-enqueue-project-cut-base-stale", {
|
|
172
|
+
observedPullRequestBase,
|
|
173
|
+
currentBaseSha,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
const proof = readOptionalJson(options.sourceProofPath);
|
|
177
|
+
if (!proof) mismatch("pre-enqueue-base-attribution-unknown");
|
|
178
|
+
if (
|
|
179
|
+
proof.sourceHead !== expectedHeadSha ||
|
|
180
|
+
proof.proofRoot !== options.verifiedDeliveryWarrant?.sourceProofRoot ||
|
|
181
|
+
(options.sourcePatchRoot &&
|
|
182
|
+
proof.sourcePatchRoot !== options.sourcePatchRoot)
|
|
183
|
+
) {
|
|
184
|
+
mismatch("pre-enqueue-source-proof-drift");
|
|
185
|
+
}
|
|
186
|
+
let delta;
|
|
187
|
+
try {
|
|
188
|
+
delta = attributedBaseDelta(
|
|
189
|
+
await githubBaseDelta(client, options, previousBaseSha, currentBaseSha),
|
|
190
|
+
previousBaseSha,
|
|
191
|
+
);
|
|
192
|
+
} catch {
|
|
193
|
+
mismatch("pre-enqueue-base-attribution-unknown");
|
|
194
|
+
}
|
|
195
|
+
reuseDecision = classifyDevDeliveryDelta({
|
|
196
|
+
proof,
|
|
197
|
+
current: {
|
|
198
|
+
sourceIdentityRoot: proof.sourceIdentityRoot,
|
|
199
|
+
sourcePatchRoot: proof.sourcePatchRoot,
|
|
200
|
+
planRoot: proof.planRoot,
|
|
201
|
+
closureRoot: proof.closureRoot,
|
|
202
|
+
dependencyRoot: proof.dependencyRoot,
|
|
203
|
+
toolchainRoot: proof.toolchainRoot,
|
|
204
|
+
graphKnown: delta.graphKnown,
|
|
205
|
+
changedPaths: delta.changedPaths,
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
if (!reuseDecision.reusable) {
|
|
209
|
+
mismatch(reuseFailure(reuseDecision), { reuseDecision });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const mergeCommitSha = String(
|
|
214
|
+
observedPullRequest.merge_commit_sha || "",
|
|
215
|
+
).toLowerCase();
|
|
216
|
+
const replayTree = /^[0-9a-f]{40}$/u.test(mergeCommitSha)
|
|
217
|
+
? await githubCommitTree(client, options, mergeCommitSha).catch(() => "")
|
|
218
|
+
: "";
|
|
219
|
+
if (
|
|
220
|
+
currentBaseSha !== previousBaseSha &&
|
|
221
|
+
(!/^[0-9a-f]{40}$/u.test(mergeCommitSha) ||
|
|
222
|
+
!/^[0-9a-f]{40}$/u.test(replayTree))
|
|
223
|
+
) {
|
|
224
|
+
mismatch("pre-enqueue-project-cut-composition-missing", {
|
|
225
|
+
mergeCommitSha,
|
|
226
|
+
replayTree,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
const receipt = {
|
|
230
|
+
schema: "kungfu.buildchain.pre-enqueue-project-cut/v1",
|
|
231
|
+
repository: options.repository.fullName,
|
|
232
|
+
protectedBase: options.targetBranch,
|
|
233
|
+
pullRequestNumber: pullRequest.number,
|
|
234
|
+
sourceHead: expectedHeadSha,
|
|
235
|
+
previousBase: previousBaseSha,
|
|
236
|
+
admittedBase: currentBaseSha,
|
|
237
|
+
baseMoved: currentBaseSha !== previousBaseSha,
|
|
238
|
+
sourceHeadMutationRequired: false,
|
|
239
|
+
composition: { mergeCommitSha, replayTree },
|
|
240
|
+
sourceProofReuseDecisionRoot: reuseDecision ? root(reuseDecision) : "",
|
|
241
|
+
projectCutProofRoot: projectCut?.proofRoot || "",
|
|
242
|
+
decision: "qualified",
|
|
243
|
+
};
|
|
244
|
+
return { receipt, receiptRoot: root(receipt) };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function exactQueueEntry(queueState, pullRequest, expectedHeadSha) {
|
|
248
|
+
return queueState.entries.find(
|
|
249
|
+
(candidate) =>
|
|
250
|
+
candidate.pullRequestNumber === pullRequest.number &&
|
|
251
|
+
candidate.pullRequestHeadSha === expectedHeadSha,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function qualifyPreEnqueueReadback({
|
|
256
|
+
client,
|
|
257
|
+
options,
|
|
258
|
+
pullRequest,
|
|
259
|
+
expectedBaseSha,
|
|
260
|
+
expectedHeadSha,
|
|
261
|
+
projectCut,
|
|
262
|
+
mergeableAccepted,
|
|
263
|
+
root,
|
|
264
|
+
verifyCurrentWarrant,
|
|
265
|
+
}) {
|
|
266
|
+
const [
|
|
267
|
+
observedPullRequest,
|
|
268
|
+
observedBaseSha,
|
|
269
|
+
observedQueueState,
|
|
270
|
+
currentWarrant,
|
|
271
|
+
] = await Promise.all([
|
|
272
|
+
client.getPullRequest(pullRequest.number, {
|
|
273
|
+
attempts: options.pollMergeableAttempts,
|
|
274
|
+
delayMs: options.pollMergeableDelayMs,
|
|
275
|
+
}),
|
|
276
|
+
client.getBranchSha(options.targetBranch),
|
|
277
|
+
client.getMergeQueueState(options.targetBranch),
|
|
278
|
+
verifyCurrentWarrant(
|
|
279
|
+
client,
|
|
280
|
+
options,
|
|
281
|
+
pullRequest,
|
|
282
|
+
options.verifiedDeliveryWarrant,
|
|
283
|
+
),
|
|
284
|
+
]);
|
|
285
|
+
const observedHeadSha = String(
|
|
286
|
+
observedPullRequest.head?.sha || "",
|
|
287
|
+
).toLowerCase();
|
|
288
|
+
if (observedHeadSha !== expectedHeadSha) {
|
|
289
|
+
mismatch("head-sha-drift-after-lease-readback", { observedHeadSha });
|
|
290
|
+
}
|
|
291
|
+
if (options.warrantMode !== "required") {
|
|
292
|
+
if (observedBaseSha !== expectedBaseSha) {
|
|
293
|
+
mismatch("base-sha-drift-after-lease-readback", { observedBaseSha });
|
|
294
|
+
}
|
|
295
|
+
if (
|
|
296
|
+
!mergeableAccepted(observedPullRequest, "queue", projectCut?.ok === true)
|
|
297
|
+
) {
|
|
298
|
+
mismatch("not-mergeable-after-lease-readback");
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
observedBaseSha,
|
|
302
|
+
observedQueueState,
|
|
303
|
+
currentWarrant,
|
|
304
|
+
preEnqueueProjectCut: null,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const preEnqueueProjectCut = await qualifyProjectCut({
|
|
309
|
+
client,
|
|
310
|
+
options,
|
|
311
|
+
pullRequest,
|
|
312
|
+
previousBaseSha: expectedBaseSha,
|
|
313
|
+
currentBaseSha: observedBaseSha,
|
|
314
|
+
expectedHeadSha,
|
|
315
|
+
observedPullRequest,
|
|
316
|
+
projectCut,
|
|
317
|
+
mergeableAccepted,
|
|
318
|
+
root,
|
|
319
|
+
});
|
|
320
|
+
const [casPullRequest, casBaseSha, casQueueState, casWarrant] =
|
|
321
|
+
await Promise.all([
|
|
322
|
+
client.getPullRequest(pullRequest.number, {
|
|
323
|
+
attempts: options.pollMergeableAttempts,
|
|
324
|
+
delayMs: options.pollMergeableDelayMs,
|
|
325
|
+
}),
|
|
326
|
+
client.getBranchSha(options.targetBranch),
|
|
327
|
+
client.getMergeQueueState(options.targetBranch),
|
|
328
|
+
verifyCurrentWarrant(
|
|
329
|
+
client,
|
|
330
|
+
options,
|
|
331
|
+
pullRequest,
|
|
332
|
+
options.verifiedDeliveryWarrant,
|
|
333
|
+
),
|
|
334
|
+
]);
|
|
335
|
+
if (casBaseSha !== observedBaseSha) {
|
|
336
|
+
mismatch("base-sha-drift-after-project-cut", {
|
|
337
|
+
observedBaseSha: casBaseSha,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
if (
|
|
341
|
+
String(casPullRequest.head?.sha || "").toLowerCase() !== expectedHeadSha
|
|
342
|
+
) {
|
|
343
|
+
mismatch("head-sha-drift-after-project-cut");
|
|
344
|
+
}
|
|
345
|
+
const casPullRequestBase = String(
|
|
346
|
+
casPullRequest.base?.sha || "",
|
|
347
|
+
).toLowerCase();
|
|
348
|
+
if (casPullRequestBase && casPullRequestBase !== casBaseSha) {
|
|
349
|
+
mismatch("pre-enqueue-project-cut-base-stale", {
|
|
350
|
+
observedPullRequestBase: casPullRequestBase,
|
|
351
|
+
currentBaseSha: casBaseSha,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
if (casPullRequest.mergeable === false) {
|
|
355
|
+
mismatch("pre-enqueue-merge-conflict-after-project-cut");
|
|
356
|
+
}
|
|
357
|
+
if (!mergeableAccepted(casPullRequest, "queue", projectCut?.ok === true)) {
|
|
358
|
+
mismatch("not-mergeable-after-project-cut");
|
|
359
|
+
}
|
|
360
|
+
const casMergeCommitSha = String(
|
|
361
|
+
casPullRequest.merge_commit_sha || "",
|
|
362
|
+
).toLowerCase();
|
|
363
|
+
if (
|
|
364
|
+
casMergeCommitSha !==
|
|
365
|
+
preEnqueueProjectCut.receipt.composition.mergeCommitSha
|
|
366
|
+
) {
|
|
367
|
+
mismatch("pre-enqueue-project-cut-composition-drift", {
|
|
368
|
+
observedMergeCommitSha: casMergeCommitSha,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
if (preEnqueueProjectCut.receipt.composition.replayTree) {
|
|
372
|
+
const casReplayTree = await githubCommitTree(
|
|
373
|
+
client,
|
|
374
|
+
options,
|
|
375
|
+
casMergeCommitSha,
|
|
376
|
+
).catch(() => "");
|
|
377
|
+
if (casReplayTree !== preEnqueueProjectCut.receipt.composition.replayTree) {
|
|
378
|
+
mismatch("pre-enqueue-project-cut-composition-drift", {
|
|
379
|
+
observedReplayTree: casReplayTree,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const exactEntry = exactQueueEntry(
|
|
384
|
+
casQueueState,
|
|
385
|
+
pullRequest,
|
|
386
|
+
expectedHeadSha,
|
|
387
|
+
);
|
|
388
|
+
const predecessor =
|
|
389
|
+
casQueueState.entries.find((candidate) => candidate !== exactEntry) || null;
|
|
390
|
+
if (predecessor) {
|
|
391
|
+
mismatch("queue-predecessor-after-project-cut", { predecessor });
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
observedBaseSha,
|
|
395
|
+
observedQueueState: casQueueState,
|
|
396
|
+
currentWarrant: casWarrant,
|
|
397
|
+
preEnqueueProjectCut,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
|
-
|
|
4
3
|
function classifyPublicationChannel(channel = "") {
|
|
5
4
|
const normalized = String(channel || "").trim().toLowerCase();
|
|
6
5
|
if (!normalized) return undefined;
|
|
@@ -131,6 +130,8 @@ export async function ensureGitHubRelease({
|
|
|
131
130
|
if (tagRef.status === 404) {
|
|
132
131
|
throw new Error(`Git tag ${metadata.tag} does not exist in ${repository}`);
|
|
133
132
|
}
|
|
133
|
+
// Existing exact tags are authoritative; target_commitish is redundant and
|
|
134
|
+
// can require workflow mutation authority for workflow-changing commits.
|
|
134
135
|
const created = await githubRequest({
|
|
135
136
|
apiUrl,
|
|
136
137
|
token,
|
|
@@ -142,11 +143,11 @@ export async function ensureGitHubRelease({
|
|
|
142
143
|
body: notes || `Buildchain release passport assets for ${metadata.tag}.`,
|
|
143
144
|
prerelease: metadata.prerelease,
|
|
144
145
|
make_latest: metadata.makeLatest,
|
|
145
|
-
...(target ? { target_commitish: target } : {}),
|
|
146
146
|
},
|
|
147
147
|
});
|
|
148
148
|
return { action: "created", release: created.data, metadata };
|
|
149
149
|
}
|
|
150
|
+
if (metadata.prerelease === true && existing.data?.tag_name === metadata.tag && existing.data?.name === (title || metadata.tag) && existing.data?.body === (notes || `Buildchain release passport assets for ${metadata.tag}.`) && existing.data?.prerelease === true && existing.data?.draft !== true && (!target || existing.data?.target_commitish === target)) return { action: "existing", release: existing.data, metadata };
|
|
150
151
|
const patched = await githubRequest({
|
|
151
152
|
apiUrl,
|
|
152
153
|
token,
|
|
@@ -156,7 +157,6 @@ export async function ensureGitHubRelease({
|
|
|
156
157
|
name: title || existing.data.name || metadata.tag,
|
|
157
158
|
prerelease: metadata.prerelease,
|
|
158
159
|
make_latest: metadata.makeLatest,
|
|
159
|
-
...(target ? { target_commitish: target } : {}),
|
|
160
160
|
},
|
|
161
161
|
});
|
|
162
162
|
return { action: "updated", release: patched.data, metadata };
|
|
@@ -90,6 +90,9 @@ function forwardedInputs(names) {
|
|
|
90
90
|
if (name === "buildchain-contract-lock-path") {
|
|
91
91
|
return " buildchain-contract-lock-path: ${{ needs.resolve-channel.outputs.contract-lock-path }}";
|
|
92
92
|
}
|
|
93
|
+
if (name === "buildchain-visible-workflow") {
|
|
94
|
+
return " buildchain-visible-workflow: .github/workflows/build.yml";
|
|
95
|
+
}
|
|
93
96
|
return ` ${name}: \${{ inputs.${name} }}`;
|
|
94
97
|
})
|
|
95
98
|
.join("\n");
|
|
@@ -20,9 +20,13 @@ const internalInputs = new Set([
|
|
|
20
20
|
"promotion-publication-channel",
|
|
21
21
|
"promotion-target-ref",
|
|
22
22
|
"promotion-override-used",
|
|
23
|
+
"promotion-runtime-authorization-json",
|
|
24
|
+
"promotion-runtime-authorization-root",
|
|
23
25
|
"publication-authority-workflow-path",
|
|
24
26
|
"buildchain-expected-channel",
|
|
25
27
|
"buildchain-expected-major",
|
|
28
|
+
"buildchain-alpha-contract-lock-path",
|
|
29
|
+
"buildchain-stable-contract-lock-path",
|
|
26
30
|
]);
|
|
27
31
|
|
|
28
32
|
function blockBetween(source, start, end) {
|
|
@@ -120,6 +124,12 @@ function forwardedInputs(inputNames, { includeInternal = true, major = 2, unsupp
|
|
|
120
124
|
if (name === "promotion-override-used") {
|
|
121
125
|
return ` ${name}: \${{ needs.resolve-promotion.outputs.override-used == 'true' }}`;
|
|
122
126
|
}
|
|
127
|
+
if (name === "promotion-runtime-authorization-json") {
|
|
128
|
+
return ` ${name}: \${{ needs.consumer-admission.outputs.runtime-authorization-json }}`;
|
|
129
|
+
}
|
|
130
|
+
if (name === "promotion-runtime-authorization-root") {
|
|
131
|
+
return ` ${name}: \${{ needs.consumer-admission.outputs.runtime-authorization-root }}`;
|
|
132
|
+
}
|
|
123
133
|
if (name === "buildchain-expected-major") {
|
|
124
134
|
return ` buildchain-expected-major: "${major}"`;
|
|
125
135
|
}
|
|
@@ -132,6 +142,15 @@ function forwardedInputs(inputNames, { includeInternal = true, major = 2, unsupp
|
|
|
132
142
|
}).join("\n");
|
|
133
143
|
}
|
|
134
144
|
|
|
145
|
+
function routedInputs(inputNames, route, major) {
|
|
146
|
+
return forwardedInputs(inputNames, {
|
|
147
|
+
includeInternal: route.forwardInternalInputs,
|
|
148
|
+
major,
|
|
149
|
+
unsupportedInputs: route.unsupportedInputs,
|
|
150
|
+
workflowPath: route.workflowPath,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
135
154
|
function validateWorkflowRoute(name, route, expectedLogicalRef) {
|
|
136
155
|
if (!route || typeof route !== "object") throw new Error(`promotion shell routing missing ${name} route`);
|
|
137
156
|
if (route.logicalRef !== expectedLogicalRef) {
|
|
@@ -167,6 +186,94 @@ export function parsePromotionShellRouting(source, { major = 2 } = {}) {
|
|
|
167
186
|
};
|
|
168
187
|
}
|
|
169
188
|
|
|
189
|
+
function consumerAdmissionJob() {
|
|
190
|
+
return ` consumer-admission:
|
|
191
|
+
name: Admit v4 floating consumer policy
|
|
192
|
+
needs: resolve-promotion
|
|
193
|
+
runs-on: ubuntu-24.04
|
|
194
|
+
permissions:
|
|
195
|
+
contents: read
|
|
196
|
+
outputs:
|
|
197
|
+
runtime-authorization-json: \${{ steps.runtime-authority.outputs.runtime-authorization-json }}
|
|
198
|
+
runtime-authorization-root: \${{ steps.runtime-authority.outputs.runtime-authorization-root }}
|
|
199
|
+
steps:
|
|
200
|
+
- name: Checkout exact consumer source
|
|
201
|
+
uses: actions/checkout@v7.0.0
|
|
202
|
+
with:
|
|
203
|
+
ref: \${{ inputs.target-sha || github.sha }}
|
|
204
|
+
path: .buildchain/consumer
|
|
205
|
+
persist-credentials: false
|
|
206
|
+
- name: Checkout exact policy runtime
|
|
207
|
+
uses: actions/checkout@v7.0.0
|
|
208
|
+
with:
|
|
209
|
+
repository: \${{ inputs.buildchain-repository }}
|
|
210
|
+
ref: \${{ needs.resolve-promotion.outputs.router-sha }}
|
|
211
|
+
path: .buildchain/v4-policy-runtime
|
|
212
|
+
persist-credentials: false
|
|
213
|
+
- name: Enforce v4 floating consumer policy
|
|
214
|
+
id: policy
|
|
215
|
+
env:
|
|
216
|
+
BUILDCHAIN_CONSUMER_ROOT: .buildchain/consumer
|
|
217
|
+
BUILDCHAIN_EXPECTED_INVOCATION_CHANNEL: \${{ needs.resolve-promotion.outputs.channel }}
|
|
218
|
+
BUILDCHAIN_INVOKED_WORKFLOW: .github/workflows/release-candidate-promote.yml
|
|
219
|
+
BUILDCHAIN_WORKFLOW_SHA: \${{ needs.resolve-promotion.outputs.router-sha }}
|
|
220
|
+
BUILDCHAIN_RUNTIME_SHA: \${{ needs.resolve-promotion.outputs.router-sha }}
|
|
221
|
+
BUILDCHAIN_STABLE_CONTRACT_LOCK_PATH: \${{ inputs.buildchain-stable-contract-lock-path }}
|
|
222
|
+
BUILDCHAIN_ALPHA_CONTRACT_LOCK_PATH: \${{ inputs.buildchain-alpha-contract-lock-path }}
|
|
223
|
+
BUILDCHAIN_V4_POLICY_RECEIPT_PATH: .buildchain/evidence/v4-consumer-policy-receipt.json
|
|
224
|
+
run: >-
|
|
225
|
+
node .buildchain/v4-policy-runtime/scripts/v4-consumer-policy.mjs scan
|
|
226
|
+
--source-sha "\${{ inputs.target-sha || github.sha }}"
|
|
227
|
+
|
|
228
|
+
- name: Upload rooted consumer admission receipt
|
|
229
|
+
uses: actions/upload-artifact@v7.0.1
|
|
230
|
+
with:
|
|
231
|
+
name: v4-consumer-policy-release-candidate-promote-\${{ github.sha }}
|
|
232
|
+
path: .buildchain/consumer/.buildchain/evidence/v4-consumer-policy-receipt.json
|
|
233
|
+
if-no-files-found: error
|
|
234
|
+
|
|
235
|
+
- name: Authorize transient v4 runtime selection
|
|
236
|
+
id: runtime-authority
|
|
237
|
+
if: \${{ needs.resolve-promotion.outputs.override-used == 'true' }}
|
|
238
|
+
uses: actions/github-script@v8
|
|
239
|
+
env:
|
|
240
|
+
BUILDCHAIN_RUNTIME_AUTHORIZATION_PATH: .buildchain/consumer/.buildchain/evidence/v4-runtime-authorization.json
|
|
241
|
+
with:
|
|
242
|
+
script: |
|
|
243
|
+
const script = require(process.env.GITHUB_WORKSPACE + "/.buildchain/v4-policy-runtime/scripts/authorize-promotion-runtime-override.cjs");
|
|
244
|
+
const result = await script.authorizePromotionRuntimeOverride({
|
|
245
|
+
github,
|
|
246
|
+
context,
|
|
247
|
+
request: {
|
|
248
|
+
consumerRoot: process.env.GITHUB_WORKSPACE + "/.buildchain/consumer",
|
|
249
|
+
runtimeModulePath: process.env.GITHUB_WORKSPACE + "/.buildchain/v4-policy-runtime/packages/core/v4-runtime-ref-resume-authority.js",
|
|
250
|
+
runtimeRepository: "\${{ inputs.buildchain-repository }}",
|
|
251
|
+
consumerPolicyReceiptPath: process.env.GITHUB_WORKSPACE + "/.buildchain/consumer/.buildchain/evidence/v4-consumer-policy-receipt.json",
|
|
252
|
+
consumerPolicyReceiptRoot: "\${{ steps.policy.outputs.v4-consumer-policy-receipt-root }}",
|
|
253
|
+
sourceSha: "\${{ inputs.target-sha || github.sha }}",
|
|
254
|
+
requestedRef: "\${{ needs.resolve-promotion.outputs.runtime-ref }}",
|
|
255
|
+
resolvedRuntimeSha: "\${{ needs.resolve-promotion.outputs.runtime-sha }}",
|
|
256
|
+
reason: "trusted \${{ inputs.resume-candidate-run-id != '' && 'resume' || 'dispatch' }} runtime override \${{ needs.resolve-promotion.outputs.runtime-ref }} for source \${{ inputs.target-sha || github.sha }}",
|
|
257
|
+
mode: "\${{ inputs.resume-candidate-run-id != '' && 'resume' || 'dispatch' }}",
|
|
258
|
+
outputPath: process.env.BUILDCHAIN_RUNTIME_AUTHORIZATION_PATH,
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
core.setOutput("runtime-authorization-json", JSON.stringify({
|
|
262
|
+
receipt: result.receipt,
|
|
263
|
+
receiptRoot: result.receiptRoot,
|
|
264
|
+
}));
|
|
265
|
+
core.setOutput("runtime-authorization-root", result.receiptRoot);
|
|
266
|
+
|
|
267
|
+
- name: Upload transient runtime authorization receipt
|
|
268
|
+
if: \${{ needs.resolve-promotion.outputs.override-used == 'true' }}
|
|
269
|
+
uses: actions/upload-artifact@v7.0.1
|
|
270
|
+
with:
|
|
271
|
+
name: v4-runtime-authorization-release-candidate-promote-\${{ github.run_id }}
|
|
272
|
+
path: .buildchain/consumer/.buildchain/evidence/v4-runtime-authorization.json
|
|
273
|
+
if-no-files-found: error
|
|
274
|
+
`;
|
|
275
|
+
}
|
|
276
|
+
|
|
170
277
|
export function generateChannelPromotionWorkflow(source, { major = 2, shellRouting } = {}) {
|
|
171
278
|
const routes = shellRouting || {
|
|
172
279
|
alpha: {
|
|
@@ -193,16 +300,8 @@ export function generateChannelPromotionWorkflow(source, { major = 2, shellRouti
|
|
|
193
300
|
for (const required of ["buildchain-ref", "buildchain-contract-lock-path", "channel", "target-ref", ...internalInputs]) {
|
|
194
301
|
if (!inputNames.includes(required)) throw new Error(`advanced promotion workflow missing input: ${required}`);
|
|
195
302
|
}
|
|
196
|
-
const alphaForwarded =
|
|
197
|
-
|
|
198
|
-
major,
|
|
199
|
-
unsupportedInputs: alphaRoute.unsupportedInputs, workflowPath: alphaRoute.workflowPath,
|
|
200
|
-
});
|
|
201
|
-
const stableForwarded = forwardedInputs(inputNames, {
|
|
202
|
-
includeInternal: stableRoute.forwardInternalInputs,
|
|
203
|
-
major,
|
|
204
|
-
unsupportedInputs: stableRoute.unsupportedInputs, workflowPath: stableRoute.workflowPath,
|
|
205
|
-
});
|
|
303
|
+
const alphaForwarded = routedInputs(inputNames, alphaRoute, major);
|
|
304
|
+
const stableForwarded = routedInputs(inputNames, stableRoute, major);
|
|
206
305
|
return `# Generated by scripts/generate-channel-promotion-workflow.mjs. Do not edit directly.
|
|
207
306
|
name: Release Candidate Promote
|
|
208
307
|
|
|
@@ -419,9 +518,10 @@ jobs:
|
|
|
419
518
|
[[ "$(git -C .buildchain/shell rev-parse HEAD)" = "\${SHELL_SHA}" ]] || { echo "::error::Promotion shell checkout moved"; exit 1; }
|
|
420
519
|
[[ "$(git -C .buildchain/runtime rev-parse HEAD)" = "\${RUNTIME_SHA}" ]] || { echo "::error::Promotion runtime checkout moved"; exit 1; }
|
|
421
520
|
|
|
521
|
+
${consumerAdmissionJob()}
|
|
422
522
|
alpha:
|
|
423
523
|
name: Promote with alpha workflow shell
|
|
424
|
-
needs: resolve-promotion
|
|
524
|
+
needs: [resolve-promotion, consumer-admission]
|
|
425
525
|
if: \${{ needs.resolve-promotion.outputs.channel == 'alpha' }}
|
|
426
526
|
uses: kungfu-systems/buildchain/${alphaRoute.workflowPath}@${alphaRoute.callRef}
|
|
427
527
|
permissions:
|
|
@@ -439,7 +539,7 @@ ${alphaForwarded}
|
|
|
439
539
|
|
|
440
540
|
stable:
|
|
441
541
|
name: Promote with stable workflow shell
|
|
442
|
-
needs: resolve-promotion
|
|
542
|
+
needs: [resolve-promotion, consumer-admission]
|
|
443
543
|
if: \${{ needs.resolve-promotion.outputs.channel == 'stable' }}
|
|
444
544
|
uses: kungfu-systems/buildchain/${stableRoute.workflowPath}@${stableRoute.callRef}
|
|
445
545
|
permissions:
|