@multipliers-dev/renovate-workflow 0.3.0 → 0.3.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 +7 -18
- package/package.json +4 -13
- package/scripts/lib/agent-plugins-spec.ts +0 -150
- package/scripts/lib/bootstrap-tarball.ts +0 -230
- package/scripts/lib/derive-stop-causes.ts +0 -121
- package/scripts/lib/renovate-freshness-poll.ts +0 -624
- package/scripts/lib/renovate-guardrails.ts +0 -636
- package/scripts/lib/renovate-investigation-eligibility.ts +0 -65
- package/scripts/lib/renovate-policy-facts.ts +0 -189
- package/scripts/renovate-freshness-poll.ts +0 -210
|
@@ -1,636 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
|
|
3
|
-
import yaml from "yaml";
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
isKnownStopCause,
|
|
7
|
-
validateStopCausesDerivation,
|
|
8
|
-
type StopCauseKey,
|
|
9
|
-
} from "./derive-stop-causes.js";
|
|
10
|
-
|
|
11
|
-
export type MergeAuthorityRuleKey = "allowed" | "allowed_if_no_code_changes" | "denied";
|
|
12
|
-
|
|
13
|
-
export type GuardrailStopGate =
|
|
14
|
-
| "policy_version"
|
|
15
|
-
| "head_sha"
|
|
16
|
-
| "triggered"
|
|
17
|
-
| "merge_authority"
|
|
18
|
-
| "allowed_paths"
|
|
19
|
-
| "unknown_risk_class";
|
|
20
|
-
|
|
21
|
-
export type GuardrailResult =
|
|
22
|
-
{ stop: false } | { stop: true; gate: GuardrailStopGate; reason: string };
|
|
23
|
-
|
|
24
|
-
export type InvestigationApprovedExecutionMode = {
|
|
25
|
-
eligible_risk_classes: string[];
|
|
26
|
-
overridable_classifier_stops: Record<string, StopCauseKey[]>;
|
|
27
|
-
requires_investigation_verdict: string;
|
|
28
|
-
requires_human_approval_modifier: string;
|
|
29
|
-
allowed_paths?: string[];
|
|
30
|
-
execute_time_gates?: string[];
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export type RenovatePolicy = {
|
|
34
|
-
version: string;
|
|
35
|
-
risk_classes: Record<string, string[]>;
|
|
36
|
-
merge_authority_rules: Record<
|
|
37
|
-
MergeAuthorityRuleKey,
|
|
38
|
-
{ risk_classes: string[]; allowed_paths?: string[] }
|
|
39
|
-
>;
|
|
40
|
-
execution_modes?: {
|
|
41
|
-
investigation_approved?: InvestigationApprovedExecutionMode;
|
|
42
|
-
};
|
|
43
|
-
check_assembly: {
|
|
44
|
-
requires_always: string[];
|
|
45
|
-
conditional: Record<string, unknown>;
|
|
46
|
-
};
|
|
47
|
-
checks: Record<string, unknown>;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export type RenovatePacket = {
|
|
51
|
-
policy_version?: string;
|
|
52
|
-
pr?: { head_sha?: string };
|
|
53
|
-
classification?: {
|
|
54
|
-
decision?: string;
|
|
55
|
-
merge_authority?: MergeAuthorityRuleKey;
|
|
56
|
-
risk_class?: string;
|
|
57
|
-
};
|
|
58
|
-
evidence?: { changed_files?: string[] };
|
|
59
|
-
required_checks?: string[];
|
|
60
|
-
triggered_human_required?: string[];
|
|
61
|
-
stop?: boolean;
|
|
62
|
-
stop_reason?: string | null;
|
|
63
|
-
stop_causes?: string[];
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
export type ExecutionOverlay = {
|
|
67
|
-
execution_mode?: string;
|
|
68
|
-
investigation?: {
|
|
69
|
-
report_path?: string;
|
|
70
|
-
verdict?: string;
|
|
71
|
-
investigated_at?: string;
|
|
72
|
-
investigation_head_sha?: string;
|
|
73
|
-
};
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
export type ActiveStopReasonsResult =
|
|
77
|
-
| { failClosed: false; causes: StopCauseKey[] }
|
|
78
|
-
| { failClosed: true; causes: StopCauseKey[]; reason: string };
|
|
79
|
-
|
|
80
|
-
export type TriggeredStopsOptions = {
|
|
81
|
-
executionMode?: "investigation_approved";
|
|
82
|
-
overridableStopReasons?: StopCauseKey[];
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
export type EffectiveExecutionAuthorityResult =
|
|
86
|
-
| {
|
|
87
|
-
effectiveExecutionAuthority: "unchanged";
|
|
88
|
-
mergeAuthority: MergeAuthorityRuleKey;
|
|
89
|
-
}
|
|
90
|
-
| {
|
|
91
|
-
effectiveExecutionAuthority: "investigation_approved_merge";
|
|
92
|
-
originalMergeAuthority: MergeAuthorityRuleKey;
|
|
93
|
-
suppressedStopCauses: StopCauseKey[];
|
|
94
|
-
}
|
|
95
|
-
| {
|
|
96
|
-
effectiveExecutionAuthority: "denied";
|
|
97
|
-
originalMergeAuthority: MergeAuthorityRuleKey;
|
|
98
|
-
reason: string;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
export type EffectiveAuthorityContext = {
|
|
102
|
-
overlay?: ExecutionOverlay;
|
|
103
|
-
humanApprovalModifier?: string;
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
export type PreflightContext = {
|
|
107
|
-
livePolicyVersion: string;
|
|
108
|
-
liveHeadSha: string;
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const REQUIRED_ALWAYS_CHECKS = ["pr_ci_green", "post_merge_main_ci_green"] as const;
|
|
112
|
-
|
|
113
|
-
export function loadRenovatePolicy(path: string): RenovatePolicy {
|
|
114
|
-
const raw = readFileSync(path, "utf8");
|
|
115
|
-
const policy = yaml.parse(raw) as RenovatePolicy;
|
|
116
|
-
|
|
117
|
-
if (typeof policy.version !== "string" || policy.version.trim() === "") {
|
|
118
|
-
throw new Error(`Renovate policy at ${path} must have a non-empty version string`);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return policy;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export function parsePacketSchemaRiskClasses(schemaMarkdown: string): Set<string> {
|
|
125
|
-
const match = schemaMarkdown.match(/^\s*risk_class:\s*(.+)$/m);
|
|
126
|
-
if (!match?.[1]) {
|
|
127
|
-
throw new Error("packet-schema.md must contain a risk_class pipe enum in the example block");
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return new Set(
|
|
131
|
-
match[1]
|
|
132
|
-
.split("|")
|
|
133
|
-
.map((value) => value.trim())
|
|
134
|
-
.filter(Boolean)
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function collectPolicyRiskClasses(policy: RenovatePolicy): Set<string> {
|
|
139
|
-
const classes = new Set<string>();
|
|
140
|
-
|
|
141
|
-
for (const bucket of Object.values(policy.risk_classes ?? {})) {
|
|
142
|
-
for (const riskClass of bucket) {
|
|
143
|
-
classes.add(riskClass);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
for (const rule of Object.values(policy.merge_authority_rules ?? {})) {
|
|
148
|
-
for (const riskClass of rule.risk_classes ?? []) {
|
|
149
|
-
classes.add(riskClass);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return classes;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export function assertRiskClassesMatch(policy: RenovatePolicy, schemaClasses: Set<string>): void {
|
|
157
|
-
const policyClasses = collectPolicyRiskClasses(policy);
|
|
158
|
-
|
|
159
|
-
const missingInPolicy = [...schemaClasses].filter((value) => !policyClasses.has(value));
|
|
160
|
-
const missingInSchema = [...policyClasses].filter((value) => !schemaClasses.has(value));
|
|
161
|
-
|
|
162
|
-
if (missingInPolicy.length > 0 || missingInSchema.length > 0) {
|
|
163
|
-
const parts: string[] = [];
|
|
164
|
-
if (missingInPolicy.length > 0) {
|
|
165
|
-
parts.push(`in schema but not policy: ${missingInPolicy.join(", ")}`);
|
|
166
|
-
}
|
|
167
|
-
if (missingInSchema.length > 0) {
|
|
168
|
-
parts.push(`in policy but not schema: ${missingInSchema.join(", ")}`);
|
|
169
|
-
}
|
|
170
|
-
throw new Error(`risk_class enum mismatch (${parts.join("; ")})`);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export function assertCheckAssembly(policy: RenovatePolicy): void {
|
|
175
|
-
const requiresAlways = policy.check_assembly?.requires_always ?? [];
|
|
176
|
-
for (const check of REQUIRED_ALWAYS_CHECKS) {
|
|
177
|
-
if (!requiresAlways.includes(check)) {
|
|
178
|
-
throw new Error(`check_assembly.requires_always must include ${check}`);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function findMergeAuthorityRuleForRiskClass(
|
|
184
|
-
policy: RenovatePolicy,
|
|
185
|
-
riskClass: string
|
|
186
|
-
): {
|
|
187
|
-
ruleKey: MergeAuthorityRuleKey;
|
|
188
|
-
rule: RenovatePolicy["merge_authority_rules"][MergeAuthorityRuleKey];
|
|
189
|
-
} | null {
|
|
190
|
-
for (const ruleKey of ["allowed", "allowed_if_no_code_changes", "denied"] as const) {
|
|
191
|
-
const rule = policy.merge_authority_rules?.[ruleKey];
|
|
192
|
-
if (rule?.risk_classes?.includes(riskClass)) {
|
|
193
|
-
return { ruleKey, rule };
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return null;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function isAllowedManifestOrLockfilePath(file: string): boolean {
|
|
200
|
-
return fileMatchesAllowedPaths(file, ["**/package.json", "package-lock.json"]);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
export function fileMatchesAllowedPaths(file: string, allowedPaths: string[]): boolean {
|
|
204
|
-
const normalized = file.replace(/\\/g, "/");
|
|
205
|
-
|
|
206
|
-
for (const pattern of allowedPaths) {
|
|
207
|
-
if (pattern === "package-lock.json" && normalized === "package-lock.json") {
|
|
208
|
-
return true;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (pattern === "**/package.json") {
|
|
212
|
-
const segments = normalized.split("/");
|
|
213
|
-
if (segments[segments.length - 1] === "package.json") {
|
|
214
|
-
return true;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return false;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
export function validateInvestigationOverlay(
|
|
223
|
-
packet: RenovatePacket,
|
|
224
|
-
overlay: ExecutionOverlay,
|
|
225
|
-
mode: InvestigationApprovedExecutionMode
|
|
226
|
-
): string | null {
|
|
227
|
-
const investigation = overlay.investigation;
|
|
228
|
-
if (!investigation?.report_path?.trim()) {
|
|
229
|
-
return "missing investigation.report_path";
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (!investigation.investigated_at?.trim()) {
|
|
233
|
-
return "missing investigation.investigated_at";
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const investigationHeadSha = investigation.investigation_head_sha?.trim();
|
|
237
|
-
if (!investigationHeadSha) {
|
|
238
|
-
return "missing investigation.investigation_head_sha";
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const packetHeadSha = packet.pr?.head_sha?.trim();
|
|
242
|
-
if (!packetHeadSha) {
|
|
243
|
-
return "missing packet pr.head_sha for investigation head binding";
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (investigationHeadSha !== packetHeadSha) {
|
|
247
|
-
return "investigation_head_sha does not match packet pr.head_sha";
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const allowedPaths = mode.allowed_paths ?? [];
|
|
251
|
-
if (allowedPaths.length > 0) {
|
|
252
|
-
const changedFiles = packet.evidence?.changed_files ?? [];
|
|
253
|
-
const disallowed = changedFiles.filter((file) => !fileMatchesAllowedPaths(file, allowedPaths));
|
|
254
|
-
if (disallowed.length > 0) {
|
|
255
|
-
return `changed_files outside investigation allowed_paths: ${disallowed.join(", ")}`;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return null;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
export function validateInvestigationLanePreconditions(packet: RenovatePacket): string | null {
|
|
263
|
-
if (packet.classification?.decision !== "human_required") {
|
|
264
|
-
return `decision ${packet.classification?.decision ?? "missing"} is not human_required`;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (packet.classification?.merge_authority !== "denied") {
|
|
268
|
-
return `merge_authority ${packet.classification?.merge_authority ?? "missing"} is not denied`;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (packet.stop !== true) {
|
|
272
|
-
return "packet stop flag is not true";
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return null;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
export function evaluatePreflight(packet: RenovatePacket, ctx: PreflightContext): GuardrailResult {
|
|
279
|
-
const packetVersion = packet.policy_version?.trim();
|
|
280
|
-
if (!packetVersion || packetVersion !== ctx.livePolicyVersion) {
|
|
281
|
-
return {
|
|
282
|
-
stop: true,
|
|
283
|
-
gate: "policy_version",
|
|
284
|
-
reason: `policy_version mismatch (packet: ${packetVersion ?? "missing"}, live: ${ctx.livePolicyVersion})`,
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const packetHeadSha = packet.pr?.head_sha?.trim();
|
|
289
|
-
if (!packetHeadSha || packetHeadSha !== ctx.liveHeadSha) {
|
|
290
|
-
return {
|
|
291
|
-
stop: true,
|
|
292
|
-
gate: "head_sha",
|
|
293
|
-
reason: `head_sha mismatch (packet: ${packetHeadSha ?? "missing"}, live: ${ctx.liveHeadSha})`,
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
return { stop: false };
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
export function resolveOverridableClassifierStops(
|
|
301
|
-
policy: RenovatePolicy,
|
|
302
|
-
riskClass: string
|
|
303
|
-
): StopCauseKey[] {
|
|
304
|
-
const mode = policy.execution_modes?.investigation_approved;
|
|
305
|
-
if (!mode) {
|
|
306
|
-
return [];
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
return mode.overridable_classifier_stops[riskClass] ?? [];
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
export function enumerateActiveStopReasons(packet: RenovatePacket): ActiveStopReasonsResult {
|
|
313
|
-
if (packet.stop !== true) {
|
|
314
|
-
return { failClosed: false, causes: [] };
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const rawCauses = packet.stop_causes;
|
|
318
|
-
if (!rawCauses || rawCauses.length === 0) {
|
|
319
|
-
return {
|
|
320
|
-
failClosed: true,
|
|
321
|
-
causes: [],
|
|
322
|
-
reason: "stop: true with missing or empty stop_causes",
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const causes: StopCauseKey[] = [];
|
|
327
|
-
for (const cause of rawCauses) {
|
|
328
|
-
if (!isKnownStopCause(cause)) {
|
|
329
|
-
return {
|
|
330
|
-
failClosed: true,
|
|
331
|
-
causes,
|
|
332
|
-
reason: `unknown stop_cause key: ${cause}`,
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
causes.push(cause);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
return { failClosed: false, causes };
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function partitionTriggeredStops(
|
|
342
|
-
activeCauses: StopCauseKey[],
|
|
343
|
-
overridableStopReasons: StopCauseKey[]
|
|
344
|
-
): { remaining: StopCauseKey[]; suppressed: StopCauseKey[] } {
|
|
345
|
-
const overridableSet = new Set(overridableStopReasons);
|
|
346
|
-
const remaining: StopCauseKey[] = [];
|
|
347
|
-
const suppressed: StopCauseKey[] = [];
|
|
348
|
-
|
|
349
|
-
for (const cause of activeCauses) {
|
|
350
|
-
if (cause === "decision_defer" || !overridableSet.has(cause)) {
|
|
351
|
-
remaining.push(cause);
|
|
352
|
-
} else {
|
|
353
|
-
suppressed.push(cause);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
return { remaining, suppressed };
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
export function evaluateTriggeredStops(
|
|
361
|
-
packet: RenovatePacket,
|
|
362
|
-
options?: TriggeredStopsOptions
|
|
363
|
-
): GuardrailResult {
|
|
364
|
-
const activeStopReasons = enumerateActiveStopReasons(packet);
|
|
365
|
-
const decision = packet.classification?.decision;
|
|
366
|
-
const triggered = packet.triggered_human_required ?? [];
|
|
367
|
-
|
|
368
|
-
if (options?.executionMode === "investigation_approved") {
|
|
369
|
-
if (activeStopReasons.failClosed) {
|
|
370
|
-
return {
|
|
371
|
-
stop: true,
|
|
372
|
-
gate: "triggered",
|
|
373
|
-
reason: activeStopReasons.reason,
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
if (packet.stop !== true && decision !== "human_required" && decision !== "defer") {
|
|
378
|
-
if (triggered.length > 0) {
|
|
379
|
-
return {
|
|
380
|
-
stop: true,
|
|
381
|
-
gate: "triggered",
|
|
382
|
-
reason: `triggered_human_required without stop_causes: ${triggered.join(", ")}`,
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
return { stop: false };
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (packet.stop !== true) {
|
|
389
|
-
if (decision === "defer") {
|
|
390
|
-
return { stop: true, gate: "triggered", reason: "decision is defer" };
|
|
391
|
-
}
|
|
392
|
-
if (decision === "human_required") {
|
|
393
|
-
return {
|
|
394
|
-
stop: true,
|
|
395
|
-
gate: "triggered",
|
|
396
|
-
reason: "human_required without stop: true and structured stop_causes",
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
if (triggered.length > 0) {
|
|
400
|
-
return {
|
|
401
|
-
stop: true,
|
|
402
|
-
gate: "triggered",
|
|
403
|
-
reason: `triggered_human_required without stop_causes: ${triggered.join(", ")}`,
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
return { stop: false };
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
const overridable = options.overridableStopReasons ?? [];
|
|
410
|
-
const { remaining, suppressed } = partitionTriggeredStops(
|
|
411
|
-
activeStopReasons.causes,
|
|
412
|
-
overridable
|
|
413
|
-
);
|
|
414
|
-
|
|
415
|
-
if (remaining.length > 0) {
|
|
416
|
-
return {
|
|
417
|
-
stop: true,
|
|
418
|
-
gate: "triggered",
|
|
419
|
-
reason: `non-overridable stop_causes: ${remaining.join(", ")}`,
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
if (suppressed.length > 0) {
|
|
424
|
-
return { stop: false };
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
return {
|
|
428
|
-
stop: true,
|
|
429
|
-
gate: "triggered",
|
|
430
|
-
reason: "packet stop flag is true with no overridable stop_causes",
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
if (packet.stop === true) {
|
|
435
|
-
return { stop: true, gate: "triggered", reason: "packet stop flag is true" };
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
if (decision === "human_required" || decision === "defer") {
|
|
439
|
-
return { stop: true, gate: "triggered", reason: `decision is ${decision}` };
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
if (triggered.length > 0) {
|
|
443
|
-
return {
|
|
444
|
-
stop: true,
|
|
445
|
-
gate: "triggered",
|
|
446
|
-
reason: `triggered_human_required: ${triggered.join(", ")}`,
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
return { stop: false };
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
export function evaluateEffectiveExecutionAuthority(
|
|
454
|
-
packet: RenovatePacket,
|
|
455
|
-
policy: RenovatePolicy,
|
|
456
|
-
context: EffectiveAuthorityContext = {}
|
|
457
|
-
): EffectiveExecutionAuthorityResult {
|
|
458
|
-
const originalMergeAuthority = packet.classification?.merge_authority ?? "denied";
|
|
459
|
-
const overlay = context.overlay;
|
|
460
|
-
const modifier = context.humanApprovalModifier;
|
|
461
|
-
|
|
462
|
-
if (!overlay || overlay.execution_mode !== "investigation_approved") {
|
|
463
|
-
return {
|
|
464
|
-
effectiveExecutionAuthority: "unchanged",
|
|
465
|
-
mergeAuthority: originalMergeAuthority,
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
const mode = policy.execution_modes?.investigation_approved;
|
|
470
|
-
if (!mode) {
|
|
471
|
-
return {
|
|
472
|
-
effectiveExecutionAuthority: "denied",
|
|
473
|
-
originalMergeAuthority,
|
|
474
|
-
reason: "policy missing execution_modes.investigation_approved",
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
if (modifier !== mode.requires_human_approval_modifier) {
|
|
479
|
-
return {
|
|
480
|
-
effectiveExecutionAuthority: "denied",
|
|
481
|
-
originalMergeAuthority,
|
|
482
|
-
reason: `human approval modifier ${modifier ?? "missing"} does not match policy requirement`,
|
|
483
|
-
};
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
const riskClass = packet.classification?.risk_class;
|
|
487
|
-
if (!riskClass || !mode.eligible_risk_classes.includes(riskClass)) {
|
|
488
|
-
return {
|
|
489
|
-
effectiveExecutionAuthority: "denied",
|
|
490
|
-
originalMergeAuthority,
|
|
491
|
-
reason: `risk_class ${riskClass ?? "missing"} is not eligible for investigation_approved`,
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
const laneError = validateInvestigationLanePreconditions(packet);
|
|
496
|
-
if (laneError) {
|
|
497
|
-
return {
|
|
498
|
-
effectiveExecutionAuthority: "denied",
|
|
499
|
-
originalMergeAuthority,
|
|
500
|
-
reason: laneError,
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
if (overlay.investigation?.verdict !== mode.requires_investigation_verdict) {
|
|
505
|
-
return {
|
|
506
|
-
effectiveExecutionAuthority: "denied",
|
|
507
|
-
originalMergeAuthority,
|
|
508
|
-
reason: `investigation verdict ${overlay.investigation?.verdict ?? "missing"} does not match policy requirement`,
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
const overlayValidationError = validateInvestigationOverlay(packet, overlay, mode);
|
|
513
|
-
if (overlayValidationError) {
|
|
514
|
-
return {
|
|
515
|
-
effectiveExecutionAuthority: "denied",
|
|
516
|
-
originalMergeAuthority,
|
|
517
|
-
reason: overlayValidationError,
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
const activeStopReasons = enumerateActiveStopReasons(packet);
|
|
522
|
-
if (activeStopReasons.failClosed) {
|
|
523
|
-
return {
|
|
524
|
-
effectiveExecutionAuthority: "denied",
|
|
525
|
-
originalMergeAuthority,
|
|
526
|
-
reason: activeStopReasons.reason,
|
|
527
|
-
};
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
const derivationError = validateStopCausesDerivation(packet);
|
|
531
|
-
if (derivationError) {
|
|
532
|
-
return {
|
|
533
|
-
effectiveExecutionAuthority: "denied",
|
|
534
|
-
originalMergeAuthority,
|
|
535
|
-
reason: derivationError,
|
|
536
|
-
};
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
const overridable = resolveOverridableClassifierStops(policy, riskClass);
|
|
540
|
-
const triggeredResult = evaluateTriggeredStops(packet, {
|
|
541
|
-
executionMode: "investigation_approved",
|
|
542
|
-
overridableStopReasons: overridable,
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
if (triggeredResult.stop) {
|
|
546
|
-
return {
|
|
547
|
-
effectiveExecutionAuthority: "denied",
|
|
548
|
-
originalMergeAuthority,
|
|
549
|
-
reason: triggeredResult.reason,
|
|
550
|
-
};
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
const { suppressed } = partitionTriggeredStops(activeStopReasons.causes, overridable);
|
|
554
|
-
|
|
555
|
-
return {
|
|
556
|
-
effectiveExecutionAuthority: "investigation_approved_merge",
|
|
557
|
-
originalMergeAuthority,
|
|
558
|
-
suppressedStopCauses: suppressed,
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
export function evaluateMergeAuthority(
|
|
563
|
-
packet: RenovatePacket,
|
|
564
|
-
policy: RenovatePolicy
|
|
565
|
-
): GuardrailResult {
|
|
566
|
-
const riskClass = packet.classification?.risk_class;
|
|
567
|
-
const mergeAuthority = packet.classification?.merge_authority;
|
|
568
|
-
const changedFiles = packet.evidence?.changed_files ?? [];
|
|
569
|
-
|
|
570
|
-
if (!riskClass) {
|
|
571
|
-
return { stop: true, gate: "unknown_risk_class", reason: "missing risk_class" };
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
const ruleEntry = findMergeAuthorityRuleForRiskClass(policy, riskClass);
|
|
575
|
-
if (!ruleEntry) {
|
|
576
|
-
return {
|
|
577
|
-
stop: true,
|
|
578
|
-
gate: "unknown_risk_class",
|
|
579
|
-
reason: `unlisted risk_class: ${riskClass}`,
|
|
580
|
-
};
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
const { ruleKey } = ruleEntry;
|
|
584
|
-
|
|
585
|
-
if (mergeAuthority !== ruleKey) {
|
|
586
|
-
return {
|
|
587
|
-
stop: true,
|
|
588
|
-
gate: "merge_authority",
|
|
589
|
-
reason: `merge_authority ${mergeAuthority ?? "missing"} does not match policy rule ${ruleKey} for ${riskClass}`,
|
|
590
|
-
};
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
if (ruleKey === "denied") {
|
|
594
|
-
return {
|
|
595
|
-
stop: true,
|
|
596
|
-
gate: "merge_authority",
|
|
597
|
-
reason: `risk_class ${riskClass} is denied`,
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
if (ruleKey === "allowed_if_no_code_changes") {
|
|
602
|
-
const disallowed = changedFiles.filter((file) => !isAllowedManifestOrLockfilePath(file));
|
|
603
|
-
if (disallowed.length > 0) {
|
|
604
|
-
return {
|
|
605
|
-
stop: true,
|
|
606
|
-
gate: "allowed_paths",
|
|
607
|
-
reason: `changed_files outside allowed paths: ${disallowed.join(", ")}`,
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
return { stop: false };
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
export function assertMergeAuthorityRulesAreAuthorityOnly(policy: RenovatePolicy): void {
|
|
616
|
-
for (const [ruleKey, rule] of Object.entries(policy.merge_authority_rules ?? {})) {
|
|
617
|
-
if ("requires" in rule) {
|
|
618
|
-
throw new Error(
|
|
619
|
-
`merge_authority_rules.${ruleKey} must not include static requires (authority-only)`
|
|
620
|
-
);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
export function assertConditionalChecksExist(policy: RenovatePolicy): void {
|
|
626
|
-
const conditional = policy.check_assembly?.conditional ?? {};
|
|
627
|
-
const checks = policy.checks ?? {};
|
|
628
|
-
|
|
629
|
-
for (const checkKey of Object.keys(conditional)) {
|
|
630
|
-
if (!(checkKey in checks)) {
|
|
631
|
-
throw new Error(
|
|
632
|
-
`check_assembly.conditional.${checkKey} has no matching checks.${checkKey} entry`
|
|
633
|
-
);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { validateStopCausesDerivation, type StopCauseKey } from "./derive-stop-causes.js";
|
|
2
|
-
import {
|
|
3
|
-
enumerateActiveStopReasons,
|
|
4
|
-
resolveOverridableClassifierStops,
|
|
5
|
-
validateInvestigationLanePreconditions,
|
|
6
|
-
type RenovatePacket,
|
|
7
|
-
type RenovatePolicy,
|
|
8
|
-
} from "./renovate-guardrails.js";
|
|
9
|
-
|
|
10
|
-
export type InvestigationEligibilityResult =
|
|
11
|
-
| { eligible: true; riskClass: string; overridableCauses: StopCauseKey[] }
|
|
12
|
-
| { eligible: false; reason: string };
|
|
13
|
-
|
|
14
|
-
export function evaluateInvestigationEligibility(
|
|
15
|
-
packet: RenovatePacket,
|
|
16
|
-
policy: RenovatePolicy
|
|
17
|
-
): InvestigationEligibilityResult {
|
|
18
|
-
const mode = policy.execution_modes?.investigation_approved;
|
|
19
|
-
if (!mode) {
|
|
20
|
-
return { eligible: false, reason: "policy missing execution_modes.investigation_approved" };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const riskClass = packet.classification?.risk_class;
|
|
24
|
-
if (!riskClass) {
|
|
25
|
-
return { eligible: false, reason: "missing classification.risk_class" };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (!mode.eligible_risk_classes.includes(riskClass)) {
|
|
29
|
-
return { eligible: false, reason: `risk_class ${riskClass} is not investigation-eligible` };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const laneError = validateInvestigationLanePreconditions(packet);
|
|
33
|
-
if (laneError) {
|
|
34
|
-
return { eligible: false, reason: laneError };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const activeCauses = enumerateActiveStopReasons(packet);
|
|
38
|
-
if (activeCauses.failClosed) {
|
|
39
|
-
return {
|
|
40
|
-
eligible: false,
|
|
41
|
-
reason: activeCauses.reason ?? "stop_causes missing, empty, or unknown",
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const overridableCauses = resolveOverridableClassifierStops(policy, riskClass);
|
|
46
|
-
const nonOverridable = activeCauses.causes.filter((cause) => !overridableCauses.includes(cause));
|
|
47
|
-
|
|
48
|
-
if (nonOverridable.length > 0) {
|
|
49
|
-
return {
|
|
50
|
-
eligible: false,
|
|
51
|
-
reason: `non-overridable stop_causes remain: ${nonOverridable.join(", ")}`,
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (activeCauses.causes.length === 0) {
|
|
56
|
-
return { eligible: false, reason: "no structured stop_causes to route for investigation" };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const derivationError = validateStopCausesDerivation(packet);
|
|
60
|
-
if (derivationError) {
|
|
61
|
-
return { eligible: false, reason: derivationError };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return { eligible: true, riskClass, overridableCauses };
|
|
65
|
-
}
|