@kungfu-tech/buildchain 2.14.2-alpha.0 → 2.14.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/bin/buildchain.mjs +2 -2
- package/dist/site/buildchain-contract.json +5 -5
- package/dist/site/buildchain-site.json +16 -16
- package/dist/site/capability-registry.json +1 -1
- package/dist/site/kfd-claims.json +21 -7
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +4 -4
- package/dist/site/node-api-registry.json +3 -3
- package/dist/site/page-registry.json +8 -8
- package/dist/site/public-surface-audit.json +20 -4
- package/dist/site/publication-authority-registry.json +19 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +8 -8
- package/dist/site/workflow-registry.json +14 -0
- package/docs/cli.md +28 -2
- package/docs/release-flow.md +6 -1
- package/docs/release-governance.md +16 -0
- package/docs/versioning.md +1 -0
- package/package.json +1 -1
- package/packages/core/buildchain-config.js +52 -0
- package/packages/core/buildchain-publication-authority.js +1 -0
- package/packages/core/release-line-bootstrap.js +14 -1
- package/scripts/dev-merge-queue.mjs +231 -16
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { loadBuildchainConfig } from "../packages/core/buildchain-config.js";
|
|
5
6
|
|
|
6
7
|
const RULESET_PREFIX = "Buildchain dev merge queue";
|
|
7
8
|
const RULESET_BYPASS_TYPES = new Set(["Integration", "Team", "User"]);
|
|
@@ -80,6 +81,19 @@ export function selectMergeQueueMethod(repositorySettings = {}) {
|
|
|
80
81
|
return selected;
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
function assertMergeQueueMethod(method, repositorySettings = {}) {
|
|
85
|
+
const normalized = requiredString(method, "merge queue method").toUpperCase();
|
|
86
|
+
const allowed = {
|
|
87
|
+
MERGE: repositorySettings.allow_merge_commit === true,
|
|
88
|
+
SQUASH: repositorySettings.allow_squash_merge === true,
|
|
89
|
+
REBASE: repositorySettings.allow_rebase_merge === true,
|
|
90
|
+
};
|
|
91
|
+
if (!(normalized in allowed) || !allowed[normalized]) {
|
|
92
|
+
throw new Error(`inherited merge queue method '${normalized}' is not enabled by the repository`);
|
|
93
|
+
}
|
|
94
|
+
return normalized;
|
|
95
|
+
}
|
|
96
|
+
|
|
83
97
|
export function validateMergeGroupWorkflows(workflows = []) {
|
|
84
98
|
if (workflows.length === 0) {
|
|
85
99
|
throw new Error("at least one required workflow must be declared with --workflow");
|
|
@@ -115,14 +129,20 @@ export function createDevMergeQueuePlan({
|
|
|
115
129
|
bypassActors = [],
|
|
116
130
|
checkResponseTimeoutMinutes = 120,
|
|
117
131
|
maxEntriesToBuild = 1,
|
|
132
|
+
queueParameters = {},
|
|
133
|
+
allowInheritedWorkflowEvidence = false,
|
|
118
134
|
} = {}) {
|
|
119
135
|
const normalizedRepository = requiredString(repository, "repository");
|
|
120
136
|
const normalizedBranch = requiredString(branch, "branch");
|
|
121
137
|
if (!/^dev\/v\d+\/v\d+\.\d+$/.test(normalizedBranch)) {
|
|
122
138
|
throw new Error(`branch must be a Buildchain dev channel, got '${normalizedBranch}'`);
|
|
123
139
|
}
|
|
124
|
-
const workflowChecks =
|
|
125
|
-
|
|
140
|
+
const workflowChecks = workflows.length === 0 && allowInheritedWorkflowEvidence
|
|
141
|
+
? []
|
|
142
|
+
: validateMergeGroupWorkflows(workflows);
|
|
143
|
+
const mergeMethod = queueParameters.merge_method
|
|
144
|
+
? assertMergeQueueMethod(queueParameters.merge_method, repositorySettings)
|
|
145
|
+
: selectMergeQueueMethod(repositorySettings);
|
|
126
146
|
const requiredChecks = protection?.required_status_checks?.checks || [];
|
|
127
147
|
if (requiredChecks.length === 0) {
|
|
128
148
|
throw new Error(`protected branch ${normalizedBranch} must declare required status checks before merge queue enablement`);
|
|
@@ -143,13 +163,23 @@ export function createDevMergeQueuePlan({
|
|
|
143
163
|
rules: [{
|
|
144
164
|
type: "merge_queue",
|
|
145
165
|
parameters: {
|
|
146
|
-
check_response_timeout_minutes: positiveInteger(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
166
|
+
check_response_timeout_minutes: positiveInteger(
|
|
167
|
+
queueParameters.check_response_timeout_minutes || checkResponseTimeoutMinutes,
|
|
168
|
+
"check response timeout",
|
|
169
|
+
120,
|
|
170
|
+
),
|
|
171
|
+
grouping_strategy: queueParameters.grouping_strategy || "ALLGREEN",
|
|
172
|
+
max_entries_to_build: positiveInteger(
|
|
173
|
+
queueParameters.max_entries_to_build || maxEntriesToBuild,
|
|
174
|
+
"max entries to build",
|
|
175
|
+
1,
|
|
176
|
+
),
|
|
177
|
+
max_entries_to_merge: positiveInteger(queueParameters.max_entries_to_merge, "max entries to merge", 1),
|
|
150
178
|
merge_method: mergeMethod,
|
|
151
|
-
min_entries_to_merge: 1,
|
|
152
|
-
min_entries_to_merge_wait_minutes:
|
|
179
|
+
min_entries_to_merge: positiveInteger(queueParameters.min_entries_to_merge, "min entries to merge", 1),
|
|
180
|
+
min_entries_to_merge_wait_minutes: Number.isInteger(queueParameters.min_entries_to_merge_wait_minutes)
|
|
181
|
+
? queueParameters.min_entries_to_merge_wait_minutes
|
|
182
|
+
: 0,
|
|
153
183
|
},
|
|
154
184
|
}],
|
|
155
185
|
};
|
|
@@ -160,6 +190,7 @@ export function createDevMergeQueuePlan({
|
|
|
160
190
|
branch: normalizedBranch,
|
|
161
191
|
ok: true,
|
|
162
192
|
workflowChecks,
|
|
193
|
+
workflowEvidence: workflowChecks.length > 0 ? "declared-workflows" : "inherited-active-ruleset",
|
|
163
194
|
before: {
|
|
164
195
|
strict: protection.required_status_checks?.strict === true,
|
|
165
196
|
requiredStatusChecks: requiredChecks.map((check) => check.context),
|
|
@@ -199,18 +230,23 @@ export async function reconcileDevMergeQueue({
|
|
|
199
230
|
bypassTeams = [],
|
|
200
231
|
checkResponseTimeoutMinutes = 120,
|
|
201
232
|
maxEntriesToBuild = 1,
|
|
233
|
+
bypassActors,
|
|
234
|
+
queueParameters = {},
|
|
235
|
+
allowInheritedWorkflowEvidence = false,
|
|
202
236
|
} = {}) {
|
|
203
237
|
const encodedBranch = encodeURIComponent(requiredString(branch, "branch"));
|
|
204
238
|
const repositorySettings = await api.request("GET", `repos/${repository}`);
|
|
205
239
|
const protection = await api.request("GET", `repos/${repository}/branches/${encodedBranch}/protection`);
|
|
206
240
|
const rulesets = await api.request("GET", `repos/${repository}/rulesets?includes_parents=false&per_page=100`);
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
241
|
+
const resolvedBypassActors = bypassActors === undefined
|
|
242
|
+
? await resolveRulesetBypassActors({
|
|
243
|
+
api,
|
|
244
|
+
repository,
|
|
245
|
+
apps: bypassApps,
|
|
246
|
+
users: bypassUsers,
|
|
247
|
+
teams: bypassTeams,
|
|
248
|
+
})
|
|
249
|
+
: normalizeRulesetBypassActors(bypassActors);
|
|
214
250
|
const plan = createDevMergeQueuePlan({
|
|
215
251
|
repository,
|
|
216
252
|
branch,
|
|
@@ -218,9 +254,11 @@ export async function reconcileDevMergeQueue({
|
|
|
218
254
|
protection,
|
|
219
255
|
repositorySettings,
|
|
220
256
|
rulesets,
|
|
221
|
-
bypassActors,
|
|
257
|
+
bypassActors: resolvedBypassActors,
|
|
222
258
|
checkResponseTimeoutMinutes,
|
|
223
259
|
maxEntriesToBuild,
|
|
260
|
+
queueParameters,
|
|
261
|
+
allowInheritedWorkflowEvidence,
|
|
224
262
|
});
|
|
225
263
|
if (!apply) return { ...plan, action: "planned", applied: false };
|
|
226
264
|
const results = [];
|
|
@@ -238,6 +276,172 @@ export async function reconcileDevMergeQueue({
|
|
|
238
276
|
};
|
|
239
277
|
}
|
|
240
278
|
|
|
279
|
+
async function loadDetailedRulesets(api, repository) {
|
|
280
|
+
const rulesets = await api.request("GET", `repos/${repository}/rulesets?includes_parents=false&per_page=100`);
|
|
281
|
+
const details = [];
|
|
282
|
+
for (const ruleset of Array.isArray(rulesets) ? rulesets : []) {
|
|
283
|
+
if (ruleset?.target !== "branch" || ruleset?.enforcement === "disabled") continue;
|
|
284
|
+
if (Array.isArray(ruleset.rules) && ruleset.conditions) {
|
|
285
|
+
details.push(ruleset);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
details.push(await api.request("GET", `repos/${repository}/rulesets/${ruleset.id}`));
|
|
289
|
+
}
|
|
290
|
+
return details;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function findExactMergeQueueRuleset(rulesets, branch) {
|
|
294
|
+
const include = `refs/heads/${branch}`;
|
|
295
|
+
return rulesets.find((ruleset) =>
|
|
296
|
+
ruleset?.enforcement === "active" &&
|
|
297
|
+
ruleset?.conditions?.ref_name?.include?.length === 1 &&
|
|
298
|
+
ruleset.conditions.ref_name.include[0] === include &&
|
|
299
|
+
ruleset?.rules?.some((rule) => rule?.type === "merge_queue"),
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function mergeQueueRuleParameters(ruleset) {
|
|
304
|
+
return ruleset?.rules?.find((rule) => rule?.type === "merge_queue")?.parameters || {};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export async function resolveConfiguredDevMergeQueuePolicy({
|
|
308
|
+
api,
|
|
309
|
+
repository,
|
|
310
|
+
branch,
|
|
311
|
+
cwd = process.cwd(),
|
|
312
|
+
} = {}) {
|
|
313
|
+
const loadedConfig = loadBuildchainConfig(cwd);
|
|
314
|
+
const declared = loadedConfig?.config?.governance?.dev?.mergeQueue;
|
|
315
|
+
const policy = declared || {
|
|
316
|
+
mode: "inherit",
|
|
317
|
+
requiredWorkflows: [],
|
|
318
|
+
checkResponseTimeoutMinutes: 120,
|
|
319
|
+
maxEntriesToBuild: 1,
|
|
320
|
+
bypassApps: [],
|
|
321
|
+
bypassUsers: [],
|
|
322
|
+
bypassTeams: [],
|
|
323
|
+
};
|
|
324
|
+
if (policy.mode === "disabled") {
|
|
325
|
+
return {
|
|
326
|
+
mode: "disabled",
|
|
327
|
+
declared: Boolean(declared),
|
|
328
|
+
enabled: false,
|
|
329
|
+
reason: "merge queue policy is explicitly disabled",
|
|
330
|
+
sourceBranch: null,
|
|
331
|
+
sourceRulesetId: null,
|
|
332
|
+
policy,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (policy.mode === "enabled") {
|
|
337
|
+
return {
|
|
338
|
+
mode: "enabled",
|
|
339
|
+
declared: true,
|
|
340
|
+
enabled: true,
|
|
341
|
+
reason: "merge queue policy is explicitly enabled",
|
|
342
|
+
sourceBranch: null,
|
|
343
|
+
sourceRulesetId: null,
|
|
344
|
+
policy,
|
|
345
|
+
queueParameters: {},
|
|
346
|
+
inheritedBypassActors: undefined,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const repositorySettings = await api.request("GET", `repos/${repository}`);
|
|
351
|
+
const sourceBranch = repositorySettings.default_branch;
|
|
352
|
+
if (!sourceBranch || sourceBranch === branch) {
|
|
353
|
+
const targetRuleset = findExactMergeQueueRuleset(await loadDetailedRulesets(api, repository), branch);
|
|
354
|
+
if (!targetRuleset) {
|
|
355
|
+
return {
|
|
356
|
+
mode: "inherit",
|
|
357
|
+
declared: Boolean(declared),
|
|
358
|
+
enabled: false,
|
|
359
|
+
reason: "the active dev branch has no exact merge queue ruleset",
|
|
360
|
+
sourceBranch: sourceBranch || null,
|
|
361
|
+
sourceRulesetId: null,
|
|
362
|
+
policy,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
mode: "inherit",
|
|
367
|
+
declared: Boolean(declared),
|
|
368
|
+
enabled: true,
|
|
369
|
+
reason: "the active dev branch merge queue policy is inherited",
|
|
370
|
+
sourceBranch: branch,
|
|
371
|
+
sourceRulesetId: targetRuleset.id,
|
|
372
|
+
policy,
|
|
373
|
+
queueParameters: mergeQueueRuleParameters(targetRuleset),
|
|
374
|
+
inheritedBypassActors: targetRuleset.bypass_actors || [],
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
if (!/^dev\/v\d+\/v\d+\.\d+$/.test(sourceBranch)) {
|
|
378
|
+
return {
|
|
379
|
+
mode: "inherit",
|
|
380
|
+
declared: Boolean(declared),
|
|
381
|
+
enabled: false,
|
|
382
|
+
reason: `repository default branch '${sourceBranch}' is not a Buildchain dev channel`,
|
|
383
|
+
sourceBranch,
|
|
384
|
+
sourceRulesetId: null,
|
|
385
|
+
policy,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
const sourceRuleset = findExactMergeQueueRuleset(await loadDetailedRulesets(api, repository), sourceBranch);
|
|
389
|
+
return {
|
|
390
|
+
mode: "inherit",
|
|
391
|
+
declared: Boolean(declared),
|
|
392
|
+
enabled: Boolean(sourceRuleset),
|
|
393
|
+
reason: sourceRuleset
|
|
394
|
+
? "the active dev branch merge queue policy is inherited"
|
|
395
|
+
: "the active dev branch has no exact merge queue ruleset",
|
|
396
|
+
sourceBranch,
|
|
397
|
+
sourceRulesetId: sourceRuleset?.id || null,
|
|
398
|
+
policy,
|
|
399
|
+
queueParameters: mergeQueueRuleParameters(sourceRuleset),
|
|
400
|
+
inheritedBypassActors: sourceRuleset?.bypass_actors || [],
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export async function reconcileConfiguredDevMergeQueue({
|
|
405
|
+
api,
|
|
406
|
+
repository,
|
|
407
|
+
branch,
|
|
408
|
+
cwd = process.cwd(),
|
|
409
|
+
apply = false,
|
|
410
|
+
} = {}) {
|
|
411
|
+
const resolution = await resolveConfiguredDevMergeQueuePolicy({ api, repository, branch, cwd });
|
|
412
|
+
if (!resolution.enabled) {
|
|
413
|
+
return {
|
|
414
|
+
schemaVersion: 1,
|
|
415
|
+
contract: "kungfu-buildchain-dev-merge-queue-policy",
|
|
416
|
+
repository,
|
|
417
|
+
branch,
|
|
418
|
+
ok: true,
|
|
419
|
+
action: "not-required",
|
|
420
|
+
applied: false,
|
|
421
|
+
policyResolution: resolution,
|
|
422
|
+
operations: [],
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
const workflows = loadWorkflowSources(cwd, resolution.policy.requiredWorkflows);
|
|
426
|
+
const result = await reconcileDevMergeQueue({
|
|
427
|
+
api,
|
|
428
|
+
repository,
|
|
429
|
+
branch,
|
|
430
|
+
workflows,
|
|
431
|
+
apply,
|
|
432
|
+
bypassApps: resolution.policy.bypassApps,
|
|
433
|
+
bypassUsers: resolution.policy.bypassUsers,
|
|
434
|
+
bypassTeams: resolution.policy.bypassTeams,
|
|
435
|
+
bypassActors: resolution.mode === "inherit" ? resolution.inheritedBypassActors : undefined,
|
|
436
|
+
checkResponseTimeoutMinutes: resolution.policy.checkResponseTimeoutMinutes,
|
|
437
|
+
maxEntriesToBuild: resolution.policy.maxEntriesToBuild,
|
|
438
|
+
queueParameters: resolution.mode === "inherit" ? resolution.queueParameters : {},
|
|
439
|
+
allowInheritedWorkflowEvidence:
|
|
440
|
+
resolution.mode === "inherit" && resolution.policy.requiredWorkflows.length === 0,
|
|
441
|
+
});
|
|
442
|
+
return { ...result, policyResolution: resolution };
|
|
443
|
+
}
|
|
444
|
+
|
|
241
445
|
function createGhApi() {
|
|
242
446
|
return {
|
|
243
447
|
request(method, endpoint, body) {
|
|
@@ -297,6 +501,17 @@ async function main(args = process.argv.slice(2)) {
|
|
|
297
501
|
const cwd = path.resolve(readFlag(args, "cwd", process.cwd()));
|
|
298
502
|
const repository = readFlag(args, "repository", process.env.GITHUB_REPOSITORY || "");
|
|
299
503
|
const branch = readFlag(args, "branch", "");
|
|
504
|
+
if (args.includes("--from-config")) {
|
|
505
|
+
const result = await reconcileConfiguredDevMergeQueue({
|
|
506
|
+
api: createGhApi(),
|
|
507
|
+
repository,
|
|
508
|
+
branch,
|
|
509
|
+
cwd,
|
|
510
|
+
apply: args.includes("--apply"),
|
|
511
|
+
});
|
|
512
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
300
515
|
const workflows = loadWorkflowSources(cwd, readRepeatedFlag(args, "workflow"));
|
|
301
516
|
const result = await reconcileDevMergeQueue({
|
|
302
517
|
api: createGhApi(),
|