@kungfu-tech/buildchain 2.14.1 → 2.14.2-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/bin/buildchain.mjs +2 -2
- package/dist/site/buildchain-contract.json +7 -7
- package/dist/site/buildchain-site.json +22 -22
- package/dist/site/capability-registry.json +1 -1
- package/dist/site/kfd-claims.json +27 -10
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +6 -6
- package/dist/site/node-api-registry.json +3 -3
- package/dist/site/page-registry.json +12 -12
- package/dist/site/public-surface-audit.json +29 -8
- package/dist/site/publication-authority-registry.json +19 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +10 -10
- package/dist/site/workflow-registry.json +23 -4
- package/docs/cli.md +28 -2
- package/docs/publish-transaction.md +14 -0
- package/docs/release-flow.md +6 -1
- package/docs/release-governance.md +43 -4
- package/docs/versioning.md +1 -0
- package/docs/web-surface-deployments.md +12 -2
- 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/buildchain-patrol.mjs +3 -1
- package/scripts/dev-merge-queue.mjs +287 -8
- package/scripts/dev-pr-auto-merge.mjs +362 -21
|
@@ -158,6 +158,8 @@ export function planReleaseLineBootstrap({
|
|
|
158
158
|
const alphaRef = `alpha/v${parsedMajor}/${line}`;
|
|
159
159
|
const releaseRef = `release/v${parsedMajor}/${line}`;
|
|
160
160
|
const loadedConfig = loadBuildchainConfig(cwd);
|
|
161
|
+
const declaredMergeQueue = loadedConfig?.config?.governance?.dev?.mergeQueue;
|
|
162
|
+
const mergeQueueMode = declaredMergeQueue?.mode || "inherit";
|
|
161
163
|
const versionFiles = discoverVersionStateFiles(cwd, loadedConfig);
|
|
162
164
|
const lifecycleVersionState =
|
|
163
165
|
getLifecycleStage(loadedConfig, "version-state") ||
|
|
@@ -201,22 +203,33 @@ export function planReleaseLineBootstrap({
|
|
|
201
203
|
enforceAdmins: true,
|
|
202
204
|
protectedRefs: [devRef, alphaRef, releaseRef],
|
|
203
205
|
},
|
|
206
|
+
governance: {
|
|
207
|
+
mergeQueue: {
|
|
208
|
+
declared: Boolean(declaredMergeQueue),
|
|
209
|
+
mode: mergeQueueMode,
|
|
210
|
+
source: mergeQueueMode === "inherit" ? "repository-default-branch" : "buildchain-config",
|
|
211
|
+
requiredWorkflows: declaredMergeQueue?.requiredWorkflows || [],
|
|
212
|
+
reconcileBeforeDefaultBranchSwitch: true,
|
|
213
|
+
},
|
|
214
|
+
},
|
|
204
215
|
repositoryActions: [
|
|
205
216
|
{ action: "create-or-verify-source-ref", ref: source },
|
|
206
217
|
{ action: "commit-initial-version-state", ref: branch, version: resolvedInitialVersion },
|
|
207
218
|
{ action: "create-dev-branch", ref: devRef, from: branch },
|
|
208
219
|
{ action: "create-alpha-branch", ref: alphaRef, from: source },
|
|
209
220
|
{ action: "create-release-branch", ref: releaseRef, from: source },
|
|
210
|
-
...(setDefault ? [{ action: "set-default-branch", ref: devRef }] : []),
|
|
211
221
|
{ action: "protect-branch", ref: devRef },
|
|
212
222
|
{ action: "protect-branch", ref: alphaRef },
|
|
213
223
|
{ action: "protect-branch", ref: releaseRef },
|
|
224
|
+
{ action: "reconcile-dev-merge-queue", ref: devRef, mode: mergeQueueMode },
|
|
225
|
+
...(setDefault ? [{ action: "set-default-branch", ref: devRef }] : []),
|
|
214
226
|
...(createAlphaPr ? [{ action: "open-alpha-pr", head: devRef, base: alphaRef }] : []),
|
|
215
227
|
],
|
|
216
228
|
notes: [
|
|
217
229
|
"This bootstrap creates the new minor line before the first alpha promotion.",
|
|
218
230
|
"The source ref stays as the baseline for alpha/release until reviewed channel PRs move them.",
|
|
219
231
|
"The dev branch receives only the initial version-state commit and becomes the active line when setDefault is true.",
|
|
232
|
+
"Merge queue governance is reconciled after branch protection and before the repository default branch moves.",
|
|
220
233
|
],
|
|
221
234
|
};
|
|
222
235
|
}
|
|
@@ -91,6 +91,7 @@ export function normalizePatrolOptions(options = {}) {
|
|
|
91
91
|
sameRepositoryOnly: options.sameRepositoryOnly ?? process.env.BUILDCHAIN_PATROL_SAME_REPOSITORY_ONLY,
|
|
92
92
|
maxActions: intOption(options.maxActions ?? process.env.BUILDCHAIN_PATROL_MAX_ACTIONS, 1),
|
|
93
93
|
mergeMethod: String(options.mergeMethod || process.env.BUILDCHAIN_PATROL_MERGE_METHOD || "merge").trim(),
|
|
94
|
+
landingMode: String(options.landingMode || process.env.BUILDCHAIN_PATROL_LANDING_MODE || "auto").trim(),
|
|
94
95
|
dryRun: boolOption(options.dryRun ?? process.env.BUILDCHAIN_PATROL_DRY_RUN, true),
|
|
95
96
|
outputPath: String(options.outputPath || process.env.BUILDCHAIN_PATROL_OUTPUT_PATH || DEFAULT_OUTPUT_PATH),
|
|
96
97
|
};
|
|
@@ -167,6 +168,7 @@ export async function runBuildchainPatrol(optionsInput = {}, clientInput) {
|
|
|
167
168
|
sameRepositoryOnly: options.sameRepositoryOnly,
|
|
168
169
|
maxMerges: options.maxActions,
|
|
169
170
|
mergeMethod: options.mergeMethod,
|
|
171
|
+
landingMode: options.landingMode,
|
|
170
172
|
dryRun: options.dryRun,
|
|
171
173
|
outputPath: path.join(path.dirname(options.outputPath), "dev-pr-auto-merge.json"),
|
|
172
174
|
},
|
|
@@ -178,7 +180,7 @@ export async function runBuildchainPatrol(optionsInput = {}, clientInput) {
|
|
|
178
180
|
result: mergeResult,
|
|
179
181
|
});
|
|
180
182
|
result.summary.evaluatedCount += mergeResult.evaluated.length;
|
|
181
|
-
result.summary.actionCount += mergeResult.
|
|
183
|
+
result.summary.actionCount += mergeResult.actions.length;
|
|
182
184
|
result.summary.skippedCount += mergeResult.skipped.length;
|
|
183
185
|
}
|
|
184
186
|
|
|
@@ -2,8 +2,10 @@ 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";
|
|
8
|
+
const RULESET_BYPASS_TYPES = new Set(["Integration", "Team", "User"]);
|
|
7
9
|
|
|
8
10
|
function positiveInteger(value, label, fallback) {
|
|
9
11
|
const parsed = Number(value || fallback);
|
|
@@ -19,6 +21,53 @@ function requiredString(value, label) {
|
|
|
19
21
|
return normalized;
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
function normalizeRulesetBypassActors(actors = []) {
|
|
25
|
+
const normalized = [];
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
for (const actor of actors) {
|
|
28
|
+
const actorType = requiredString(actor?.actor_type, "ruleset bypass actor type");
|
|
29
|
+
const actorId = Number(actor?.actor_id);
|
|
30
|
+
if (!RULESET_BYPASS_TYPES.has(actorType)) {
|
|
31
|
+
throw new Error(`ruleset bypass actor type must be Integration, Team, or User, got '${actorType}'`);
|
|
32
|
+
}
|
|
33
|
+
if (!Number.isInteger(actorId) || actorId < 1) {
|
|
34
|
+
throw new Error("ruleset bypass actor id must be a positive integer");
|
|
35
|
+
}
|
|
36
|
+
const key = `${actorType}:${actorId}`;
|
|
37
|
+
if (seen.has(key)) continue;
|
|
38
|
+
seen.add(key);
|
|
39
|
+
normalized.push({ actor_id: actorId, actor_type: actorType, bypass_mode: "always" });
|
|
40
|
+
}
|
|
41
|
+
return normalized;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function resolveRulesetBypassActors({
|
|
45
|
+
api,
|
|
46
|
+
repository,
|
|
47
|
+
apps = [],
|
|
48
|
+
users = [],
|
|
49
|
+
teams = [],
|
|
50
|
+
} = {}) {
|
|
51
|
+
const [owner] = requiredString(repository, "repository").split("/");
|
|
52
|
+
const actors = [];
|
|
53
|
+
for (const slug of apps) {
|
|
54
|
+
const app = await api.request("GET", `apps/${encodeURIComponent(requiredString(slug, "bypass app"))}`);
|
|
55
|
+
actors.push({ actor_id: app.id, actor_type: "Integration" });
|
|
56
|
+
}
|
|
57
|
+
for (const login of users) {
|
|
58
|
+
const user = await api.request("GET", `users/${encodeURIComponent(requiredString(login, "bypass user"))}`);
|
|
59
|
+
actors.push({ actor_id: user.id, actor_type: "User" });
|
|
60
|
+
}
|
|
61
|
+
for (const slug of teams) {
|
|
62
|
+
const team = await api.request(
|
|
63
|
+
"GET",
|
|
64
|
+
`orgs/${encodeURIComponent(owner)}/teams/${encodeURIComponent(requiredString(slug, "bypass team"))}`,
|
|
65
|
+
);
|
|
66
|
+
actors.push({ actor_id: team.id, actor_type: "Team" });
|
|
67
|
+
}
|
|
68
|
+
return normalizeRulesetBypassActors(actors);
|
|
69
|
+
}
|
|
70
|
+
|
|
22
71
|
export function selectMergeQueueMethod(repositorySettings = {}) {
|
|
23
72
|
const candidates = [
|
|
24
73
|
["MERGE", repositorySettings.allow_merge_commit],
|
|
@@ -32,6 +81,19 @@ export function selectMergeQueueMethod(repositorySettings = {}) {
|
|
|
32
81
|
return selected;
|
|
33
82
|
}
|
|
34
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
|
+
|
|
35
97
|
export function validateMergeGroupWorkflows(workflows = []) {
|
|
36
98
|
if (workflows.length === 0) {
|
|
37
99
|
throw new Error("at least one required workflow must be declared with --workflow");
|
|
@@ -64,16 +126,23 @@ export function createDevMergeQueuePlan({
|
|
|
64
126
|
protection,
|
|
65
127
|
repositorySettings,
|
|
66
128
|
rulesets = [],
|
|
129
|
+
bypassActors = [],
|
|
67
130
|
checkResponseTimeoutMinutes = 120,
|
|
68
131
|
maxEntriesToBuild = 1,
|
|
132
|
+
queueParameters = {},
|
|
133
|
+
allowInheritedWorkflowEvidence = false,
|
|
69
134
|
} = {}) {
|
|
70
135
|
const normalizedRepository = requiredString(repository, "repository");
|
|
71
136
|
const normalizedBranch = requiredString(branch, "branch");
|
|
72
137
|
if (!/^dev\/v\d+\/v\d+\.\d+$/.test(normalizedBranch)) {
|
|
73
138
|
throw new Error(`branch must be a Buildchain dev channel, got '${normalizedBranch}'`);
|
|
74
139
|
}
|
|
75
|
-
const workflowChecks =
|
|
76
|
-
|
|
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);
|
|
77
146
|
const requiredChecks = protection?.required_status_checks?.checks || [];
|
|
78
147
|
if (requiredChecks.length === 0) {
|
|
79
148
|
throw new Error(`protected branch ${normalizedBranch} must declare required status checks before merge queue enablement`);
|
|
@@ -84,6 +153,7 @@ export function createDevMergeQueuePlan({
|
|
|
84
153
|
name: rulesetName,
|
|
85
154
|
target: "branch",
|
|
86
155
|
enforcement: "active",
|
|
156
|
+
bypass_actors: normalizeRulesetBypassActors(bypassActors),
|
|
87
157
|
conditions: {
|
|
88
158
|
ref_name: {
|
|
89
159
|
include: [`refs/heads/${normalizedBranch}`],
|
|
@@ -93,13 +163,23 @@ export function createDevMergeQueuePlan({
|
|
|
93
163
|
rules: [{
|
|
94
164
|
type: "merge_queue",
|
|
95
165
|
parameters: {
|
|
96
|
-
check_response_timeout_minutes: positiveInteger(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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),
|
|
100
178
|
merge_method: mergeMethod,
|
|
101
|
-
min_entries_to_merge: 1,
|
|
102
|
-
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,
|
|
103
183
|
},
|
|
104
184
|
}],
|
|
105
185
|
};
|
|
@@ -110,6 +190,7 @@ export function createDevMergeQueuePlan({
|
|
|
110
190
|
branch: normalizedBranch,
|
|
111
191
|
ok: true,
|
|
112
192
|
workflowChecks,
|
|
193
|
+
workflowEvidence: workflowChecks.length > 0 ? "declared-workflows" : "inherited-active-ruleset",
|
|
113
194
|
before: {
|
|
114
195
|
strict: protection.required_status_checks?.strict === true,
|
|
115
196
|
requiredStatusChecks: requiredChecks.map((check) => check.context),
|
|
@@ -144,13 +225,28 @@ export async function reconcileDevMergeQueue({
|
|
|
144
225
|
branch,
|
|
145
226
|
workflows,
|
|
146
227
|
apply = false,
|
|
228
|
+
bypassApps = [],
|
|
229
|
+
bypassUsers = [],
|
|
230
|
+
bypassTeams = [],
|
|
147
231
|
checkResponseTimeoutMinutes = 120,
|
|
148
232
|
maxEntriesToBuild = 1,
|
|
233
|
+
bypassActors,
|
|
234
|
+
queueParameters = {},
|
|
235
|
+
allowInheritedWorkflowEvidence = false,
|
|
149
236
|
} = {}) {
|
|
150
237
|
const encodedBranch = encodeURIComponent(requiredString(branch, "branch"));
|
|
151
238
|
const repositorySettings = await api.request("GET", `repos/${repository}`);
|
|
152
239
|
const protection = await api.request("GET", `repos/${repository}/branches/${encodedBranch}/protection`);
|
|
153
240
|
const rulesets = await api.request("GET", `repos/${repository}/rulesets?includes_parents=false&per_page=100`);
|
|
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);
|
|
154
250
|
const plan = createDevMergeQueuePlan({
|
|
155
251
|
repository,
|
|
156
252
|
branch,
|
|
@@ -158,8 +254,11 @@ export async function reconcileDevMergeQueue({
|
|
|
158
254
|
protection,
|
|
159
255
|
repositorySettings,
|
|
160
256
|
rulesets,
|
|
257
|
+
bypassActors: resolvedBypassActors,
|
|
161
258
|
checkResponseTimeoutMinutes,
|
|
162
259
|
maxEntriesToBuild,
|
|
260
|
+
queueParameters,
|
|
261
|
+
allowInheritedWorkflowEvidence,
|
|
163
262
|
});
|
|
164
263
|
if (!apply) return { ...plan, action: "planned", applied: false };
|
|
165
264
|
const results = [];
|
|
@@ -177,6 +276,172 @@ export async function reconcileDevMergeQueue({
|
|
|
177
276
|
};
|
|
178
277
|
}
|
|
179
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
|
+
|
|
180
445
|
function createGhApi() {
|
|
181
446
|
return {
|
|
182
447
|
request(method, endpoint, body) {
|
|
@@ -236,6 +501,17 @@ async function main(args = process.argv.slice(2)) {
|
|
|
236
501
|
const cwd = path.resolve(readFlag(args, "cwd", process.cwd()));
|
|
237
502
|
const repository = readFlag(args, "repository", process.env.GITHUB_REPOSITORY || "");
|
|
238
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
|
+
}
|
|
239
515
|
const workflows = loadWorkflowSources(cwd, readRepeatedFlag(args, "workflow"));
|
|
240
516
|
const result = await reconcileDevMergeQueue({
|
|
241
517
|
api: createGhApi(),
|
|
@@ -245,6 +521,9 @@ async function main(args = process.argv.slice(2)) {
|
|
|
245
521
|
apply: args.includes("--apply"),
|
|
246
522
|
checkResponseTimeoutMinutes: readFlag(args, "check-response-timeout-minutes", "120"),
|
|
247
523
|
maxEntriesToBuild: readFlag(args, "max-entries-to-build", "1"),
|
|
524
|
+
bypassApps: readRepeatedFlag(args, "bypass-app"),
|
|
525
|
+
bypassUsers: readRepeatedFlag(args, "bypass-user"),
|
|
526
|
+
bypassTeams: readRepeatedFlag(args, "bypass-team"),
|
|
248
527
|
});
|
|
249
528
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
250
529
|
}
|