@deftai/directive-core 0.99.0 → 0.100.0
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/dist/authz/actions.d.ts +6 -0
- package/dist/authz/actions.js +3 -0
- package/dist/authz/classify.js +157 -23
- package/dist/authz/decompose-apply.d.ts +67 -0
- package/dist/authz/decompose-apply.js +302 -0
- package/dist/authz/index.d.ts +3 -1
- package/dist/authz/index.js +3 -1
- package/dist/authz/store.d.ts +53 -0
- package/dist/authz/store.js +219 -1
- package/dist/authz/types.d.ts +19 -2
- package/dist/authz/types.js +5 -1
- package/dist/doctor/checks.d.ts +44 -0
- package/dist/doctor/checks.js +428 -2
- package/dist/doctor/main.d.ts +6 -7
- package/dist/doctor/main.js +72 -79
- package/dist/doctor/which.d.ts +20 -1
- package/dist/doctor/which.js +67 -1
- package/dist/finish-loop/pr-finish-loop.d.ts +13 -2
- package/dist/finish-loop/pr-finish-loop.js +116 -5
- package/dist/finish-loop/types.d.ts +1 -1
- package/dist/hooks/dispatcher.d.ts +9 -5
- package/dist/hooks/dispatcher.js +23 -15
- package/dist/hooks/readonly.d.ts +6 -3
- package/dist/hooks/readonly.js +44 -25
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/init-deposit/agent-hooks.d.ts +20 -0
- package/dist/init-deposit/agent-hooks.js +51 -31
- package/dist/intake/issue-ingest.js +29 -0
- package/dist/lifecycle/completed-consistency.d.ts +60 -0
- package/dist/lifecycle/completed-consistency.js +443 -0
- package/dist/lifecycle/completed-tracked-on-delivery.d.ts +47 -0
- package/dist/lifecycle/completed-tracked-on-delivery.js +415 -0
- package/dist/lifecycle/index.d.ts +2 -0
- package/dist/lifecycle/index.js +2 -0
- package/dist/literal-acceptance/capture.d.ts +52 -0
- package/dist/literal-acceptance/capture.js +658 -0
- package/dist/literal-acceptance/evaluate.d.ts +47 -0
- package/dist/literal-acceptance/evaluate.js +193 -0
- package/dist/literal-acceptance/index.d.ts +12 -0
- package/dist/literal-acceptance/index.js +12 -0
- package/dist/literal-acceptance/run.d.ts +38 -0
- package/dist/literal-acceptance/run.js +216 -0
- package/dist/literal-acceptance/safety.d.ts +24 -0
- package/dist/literal-acceptance/safety.js +251 -0
- package/dist/literal-acceptance/types.d.ts +82 -0
- package/dist/literal-acceptance/types.js +26 -0
- package/dist/policy/ceremony-dial.d.ts +67 -11
- package/dist/policy/ceremony-dial.js +174 -23
- package/dist/policy/index.d.ts +1 -0
- package/dist/policy/index.js +1 -0
- package/dist/policy/merge-approval-head.d.ts +129 -0
- package/dist/policy/merge-approval-head.js +461 -0
- package/dist/policy/require-human-merge.d.ts +5 -0
- package/dist/policy/require-human-merge.js +5 -0
- package/dist/pr-merge-readiness/ci-gate.d.ts +20 -3
- package/dist/pr-merge-readiness/ci-gate.js +65 -5
- package/dist/pr-merge-readiness/compute.d.ts +13 -0
- package/dist/pr-merge-readiness/compute.js +99 -14
- package/dist/pr-merge-readiness/gh.d.ts +49 -0
- package/dist/pr-merge-readiness/gh.js +268 -0
- package/dist/pr-merge-readiness/index.d.ts +2 -2
- package/dist/pr-merge-readiness/index.js +2 -2
- package/dist/pr-merge-readiness/test-gh-fixtures.helpers.js +4 -0
- package/dist/pr-wait-mergeable/cascade.d.ts +11 -0
- package/dist/pr-wait-mergeable/cascade.js +115 -1
- package/dist/pr-wait-mergeable/main.d.ts +4 -0
- package/dist/pr-wait-mergeable/main.js +4 -0
- package/dist/pr-wait-mergeable/types.d.ts +3 -1
- package/dist/pr-wait-mergeable/wrappers.d.ts +6 -1
- package/dist/pr-wait-mergeable/wrappers.js +7 -1
- package/dist/preflight/evaluate.d.ts +15 -1
- package/dist/preflight/evaluate.js +33 -2
- package/dist/render/framework-commands.js +4 -0
- package/dist/scope/acceptance-evidence.d.ts +76 -0
- package/dist/scope/acceptance-evidence.js +348 -0
- package/dist/scope/coverage-map.d.ts +98 -0
- package/dist/scope/coverage-map.js +558 -0
- package/dist/scope/decompose.js +125 -11
- package/dist/scope/index.d.ts +3 -0
- package/dist/scope/index.js +3 -0
- package/dist/scope/parent-lineage.d.ts +125 -0
- package/dist/scope/parent-lineage.js +626 -0
- package/dist/scope/transition.d.ts +8 -0
- package/dist/scope/transition.js +61 -2
- package/dist/session/active-cli.d.ts +79 -0
- package/dist/session/active-cli.js +382 -0
- package/dist/session/effort-budget.d.ts +130 -0
- package/dist/session/effort-budget.js +334 -0
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +2 -0
- package/dist/session/session-start.d.ts +6 -0
- package/dist/session/session-start.js +53 -2
- package/dist/session/verify-session-ritual.d.ts +14 -0
- package/dist/session/verify-session-ritual.js +33 -0
- package/dist/story-ready/evaluate.d.ts +11 -0
- package/dist/story-ready/evaluate.js +41 -3
- package/dist/triage/actions/candidates-log.d.ts +1 -2
- package/dist/triage/actions/candidates-log.js +37 -6
- package/dist/xbrief-migrate/migrate-project.d.ts +4 -0
- package/dist/xbrief-migrate/migrate-project.js +92 -3
- package/package.json +7 -3
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { checkRunMatchesRequiredContext, normalizeRequiredContexts, requiredContextLabel, } from "./gh.js";
|
|
1
2
|
import { classifyCapacityStalledRequired, DEFAULT_CAPACITY_STALL_MS, } from "./runner-capacity-stall.js";
|
|
2
3
|
/** Real product/test failures (distinct from cancelled primaries, #3167). */
|
|
3
4
|
const FAILURE_CONCLUSIONS = new Set(["failure", "timed_out"]);
|
|
@@ -68,7 +69,7 @@ function isPending(status, conclusion) {
|
|
|
68
69
|
}
|
|
69
70
|
return status !== "completed" || conclusion === "none";
|
|
70
71
|
}
|
|
71
|
-
function emptySummary(readyState, ignored = []) {
|
|
72
|
+
function emptySummary(readyState, ignored = [], requiredContexts = []) {
|
|
72
73
|
return {
|
|
73
74
|
ready_state: readyState,
|
|
74
75
|
checked_count: 0,
|
|
@@ -77,6 +78,8 @@ function emptySummary(readyState, ignored = []) {
|
|
|
77
78
|
pending_required: [],
|
|
78
79
|
capacity_stalled_required: [],
|
|
79
80
|
cancelled_required: [],
|
|
81
|
+
absent_required: [],
|
|
82
|
+
required_contexts: requiredContexts,
|
|
80
83
|
conclusions: [],
|
|
81
84
|
};
|
|
82
85
|
}
|
|
@@ -88,34 +91,43 @@ export function buildCiSummaryLine(summary) {
|
|
|
88
91
|
const cancelSuffix = summary.cancelled_required.length > 0
|
|
89
92
|
? ` / ${summary.cancelled_required.length} cancelled`
|
|
90
93
|
: "";
|
|
94
|
+
const absentSuffix = summary.absent_required.length > 0
|
|
95
|
+
? ` / ${summary.absent_required.length} absent-required`
|
|
96
|
+
: "";
|
|
91
97
|
const weather = summary.ready_state === "ci_never_scheduled"
|
|
92
98
|
? " (ci_never_scheduled)"
|
|
93
99
|
: summary.ready_state === "ci_cancelled_no_failover"
|
|
94
100
|
? " (ci_cancelled_no_failover)"
|
|
95
101
|
: summary.ready_state === "ci_failures"
|
|
96
102
|
? " (ci_failures)"
|
|
97
|
-
: ""
|
|
103
|
+
: summary.ready_state === "ci_absent_required"
|
|
104
|
+
? " (ci_absent_required)"
|
|
105
|
+
: "";
|
|
98
106
|
return (`CI check-runs: ${passed} passed / ` +
|
|
99
107
|
`${summary.failed_required.length} failed / ${summary.pending_required.length} pending` +
|
|
100
108
|
stallSuffix +
|
|
101
109
|
cancelSuffix +
|
|
110
|
+
absentSuffix +
|
|
102
111
|
weather);
|
|
103
112
|
}
|
|
104
113
|
/**
|
|
105
|
-
* Classify required CI weather for a HEAD check-run list (#2169 / #2672 / #3167).
|
|
114
|
+
* Classify required CI weather for a HEAD check-run list (#2169 / #2672 / #3167 / #3234).
|
|
106
115
|
*
|
|
107
116
|
* Distinct ready_state codes:
|
|
108
117
|
* - `ci_never_scheduled` — no CI workflow check-runs (bots alone / empty)
|
|
109
118
|
* - `runner_capacity_stall` — queued past budget, no runner claimed
|
|
110
119
|
* - `ci_failures` — completed failure / timed_out with product evidence
|
|
111
120
|
* - `ci_cancelled_no_failover` — only cancelled failures; no green failover
|
|
121
|
+
* - `ci_absent_required` — ruleset/BP required context missing on HEAD (#3234)
|
|
112
122
|
* - `not_ready_yet` / `ready` / `blocked` / `skipped` — legacy + residual
|
|
113
123
|
*/
|
|
114
124
|
export function evaluateCiGate(checkRuns, options = {}) {
|
|
125
|
+
const requiredSpecs = normalizeRequiredContexts(options.requiredContexts);
|
|
126
|
+
const requiredContextLabels = requiredSpecs.map(requiredContextLabel);
|
|
115
127
|
if (options.skipCi === true) {
|
|
116
128
|
return {
|
|
117
129
|
failures: [],
|
|
118
|
-
summary: emptySummary("skipped"),
|
|
130
|
+
summary: emptySummary("skipped", [], requiredContextLabels),
|
|
119
131
|
};
|
|
120
132
|
}
|
|
121
133
|
const ignoredSet = new Set(options.ignoreCheckNames ?? []);
|
|
@@ -162,13 +174,25 @@ export function evaluateCiGate(checkRuns, options = {}) {
|
|
|
162
174
|
pendingProbes.push(run);
|
|
163
175
|
}
|
|
164
176
|
}
|
|
177
|
+
// #3234: ruleset/BP required contexts with no matching HEAD check-run
|
|
178
|
+
// (path-filtered). Match name + optional app id; operator-ignored contexts skip.
|
|
179
|
+
const absentRequired = [];
|
|
180
|
+
for (const ctx of requiredSpecs) {
|
|
181
|
+
if (ignoredSet.has(ctx.name) || ignoredSet.has(requiredContextLabel(ctx))) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (!checkRuns.some((run) => checkRunMatchesRequiredContext(run, ctx))) {
|
|
185
|
+
absentRequired.push(requiredContextLabel(ctx));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
165
188
|
// Operator --ci-ignore-check on every non-bot run is intentional, not weather.
|
|
166
189
|
operatorIgnoredOnly =
|
|
167
190
|
workflowCheckCount === 0 &&
|
|
168
191
|
checkRuns.some((r) => ignoredSet.has(r.name) && !isBotReviewCheck(r.name));
|
|
169
192
|
// #3167: no workflow CI check-runs at all (empty list or bots-only) → never scheduled.
|
|
193
|
+
// When ruleset/BP names the absents, prefer ci_absent_required (#3234) over weather.
|
|
170
194
|
// Distinct from "all remaining checks operator-ignored" (legacy ready).
|
|
171
|
-
if (workflowCheckCount === 0 && !operatorIgnoredOnly) {
|
|
195
|
+
if (workflowCheckCount === 0 && !operatorIgnoredOnly && absentRequired.length === 0) {
|
|
172
196
|
return {
|
|
173
197
|
failures: [
|
|
174
198
|
"Required CI workflow check-runs never scheduled (ci_never_scheduled): " +
|
|
@@ -183,6 +207,8 @@ export function evaluateCiGate(checkRuns, options = {}) {
|
|
|
183
207
|
pending_required: [],
|
|
184
208
|
capacity_stalled_required: [],
|
|
185
209
|
cancelled_required: [],
|
|
210
|
+
absent_required: [],
|
|
211
|
+
required_contexts: requiredContextLabels,
|
|
186
212
|
conclusions,
|
|
187
213
|
},
|
|
188
214
|
};
|
|
@@ -201,6 +227,12 @@ export function evaluateCiGate(checkRuns, options = {}) {
|
|
|
201
227
|
failedRequired = [...failedProduct, ...cancelledRequired];
|
|
202
228
|
failures.push(`Required CI check-runs failed (ci_failures): ${failedProduct.join(", ")}. ` +
|
|
203
229
|
"Required checks fail closed by default (#2169 / #3167).");
|
|
230
|
+
if (absentRequired.length > 0) {
|
|
231
|
+
failures.push(`Required status-check contexts absent on HEAD (ci_absent_required): ` +
|
|
232
|
+
`${absentRequired.join(", ")}. ` +
|
|
233
|
+
"Ruleset/branch-protection contexts with no check-run (path-filtered workflows) " +
|
|
234
|
+
"fail closed (#3234).");
|
|
235
|
+
}
|
|
204
236
|
}
|
|
205
237
|
else if (pendingRequired.length > 0) {
|
|
206
238
|
// Pending wins over cancelled siblings (failover may still be arming).
|
|
@@ -217,6 +249,12 @@ export function evaluateCiGate(checkRuns, options = {}) {
|
|
|
217
249
|
failures.push(`Required CI check-runs still running (not-ready-yet): ${pendingRequired.join(", ")}. ` +
|
|
218
250
|
"Wait for required checks to finish before merge.");
|
|
219
251
|
}
|
|
252
|
+
if (absentRequired.length > 0) {
|
|
253
|
+
failures.push(`Required status-check contexts absent on HEAD (ci_absent_required): ` +
|
|
254
|
+
`${absentRequired.join(", ")}. ` +
|
|
255
|
+
"Ruleset/branch-protection contexts with no check-run (path-filtered workflows) " +
|
|
256
|
+
"fail closed (#3234).");
|
|
257
|
+
}
|
|
220
258
|
}
|
|
221
259
|
else if (cancelledRequired.length > 0) {
|
|
222
260
|
// Cancelled suite jobs clear only when a NON-IGNORED AUTHORITATIVE aggregator
|
|
@@ -253,12 +291,32 @@ export function evaluateCiGate(checkRuns, options = {}) {
|
|
|
253
291
|
`${cancelledRequired.join(", ")}. ` +
|
|
254
292
|
"Cancelled required check with no green non-ignored authoritative suite aggregator (#3167 / #3168). " +
|
|
255
293
|
"Do not multi-hour re-push thrash; BLOCKED after thrash caps.");
|
|
294
|
+
if (absentRequired.length > 0) {
|
|
295
|
+
failures.push(`Required status-check contexts absent on HEAD (ci_absent_required): ` +
|
|
296
|
+
`${absentRequired.join(", ")}. ` +
|
|
297
|
+
"Ruleset/branch-protection contexts with no check-run (path-filtered workflows) " +
|
|
298
|
+
"fail closed (#3234).");
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
else if (absentRequired.length > 0) {
|
|
302
|
+
readyState = "ci_absent_required";
|
|
303
|
+
failures.push(`Required status-check contexts absent on HEAD (ci_absent_required): ` +
|
|
304
|
+
`${absentRequired.join(", ")}. ` +
|
|
305
|
+
"Ruleset/branch-protection contexts with no check-run (path-filtered workflows) " +
|
|
306
|
+
"fail closed (#3234).");
|
|
256
307
|
}
|
|
257
308
|
else {
|
|
258
309
|
// Every cancelled suite family has a green non-ignored aggregator; no non-suite cancels.
|
|
259
310
|
readyState = "ready";
|
|
260
311
|
}
|
|
261
312
|
}
|
|
313
|
+
else if (absentRequired.length > 0) {
|
|
314
|
+
readyState = "ci_absent_required";
|
|
315
|
+
failures.push(`Required status-check contexts absent on HEAD (ci_absent_required): ` +
|
|
316
|
+
`${absentRequired.join(", ")}. ` +
|
|
317
|
+
"Ruleset/branch-protection contexts with no check-run (path-filtered workflows) " +
|
|
318
|
+
"fail closed (#3234).");
|
|
319
|
+
}
|
|
262
320
|
else {
|
|
263
321
|
readyState = "ready";
|
|
264
322
|
}
|
|
@@ -272,6 +330,8 @@ export function evaluateCiGate(checkRuns, options = {}) {
|
|
|
272
330
|
pending_required: pendingRequired,
|
|
273
331
|
capacity_stalled_required: capacityStalledRequired,
|
|
274
332
|
cancelled_required: cancelledRequired,
|
|
333
|
+
absent_required: absentRequired,
|
|
334
|
+
required_contexts: requiredContextLabels,
|
|
275
335
|
conclusions,
|
|
276
336
|
},
|
|
277
337
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CiGateOptions } from "./ci-gate.js";
|
|
2
2
|
import { isMergeReady } from "./evaluate.js";
|
|
3
|
+
import { type RequiredStatusContext } from "./gh.js";
|
|
3
4
|
import { type InlineGreptileFindings } from "./greptile-inline.js";
|
|
4
5
|
import { type MergeabilitySignal } from "./mergeability.js";
|
|
5
6
|
import type { SlizardGateOptions } from "./slizard-gate.js";
|
|
@@ -7,6 +8,13 @@ import type { GateResult, RunGhFn } from "./types.js";
|
|
|
7
8
|
declare function buildGateResult(prNumber: number, repo: string | null, headSha: string | null, body: string, via: string, partialData?: Record<string, unknown>, error?: string | null, inline?: InlineGreptileFindings | null, minConfidence?: number): GateResult;
|
|
8
9
|
/** Injectable GitHub-mergeability read seam (#2260) — keeps unit tests hermetic. */
|
|
9
10
|
export type FetchMergeabilityFn = (prNumber: number, repo: string, runGh: RunGhFn) => MergeabilitySignal;
|
|
11
|
+
/** Injectable required-context resolve seam (#3234) — keeps unit tests hermetic. */
|
|
12
|
+
export type FetchRequiredContextsFn = (repo: string, branch: string, runGh: RunGhFn) => {
|
|
13
|
+
contexts: readonly RequiredStatusContext[];
|
|
14
|
+
sources: readonly string[];
|
|
15
|
+
error: string;
|
|
16
|
+
resolutionFailed?: boolean;
|
|
17
|
+
};
|
|
10
18
|
export interface ComputeGateOptions extends CiGateOptions, SlizardGateOptions {
|
|
11
19
|
/** Override the GitHub-mergeability read (defaults to the REST reader). */
|
|
12
20
|
readonly fetchMergeabilityFn?: FetchMergeabilityFn;
|
|
@@ -25,6 +33,11 @@ export interface ComputeGateOptions extends CiGateOptions, SlizardGateOptions {
|
|
|
25
33
|
* Tests and injectors use this; production callers leave it unset.
|
|
26
34
|
*/
|
|
27
35
|
readonly minConfidence?: number;
|
|
36
|
+
/**
|
|
37
|
+
* Override ruleset/BP required-context resolution (#3234). When set, skips
|
|
38
|
+
* REST fetch. Production callers leave unset; tests inject hermetic lists.
|
|
39
|
+
*/
|
|
40
|
+
readonly fetchRequiredContextsFn?: FetchRequiredContextsFn;
|
|
28
41
|
}
|
|
29
42
|
/** Run the primary->fallback1->fallback2 cascade and return a result. */
|
|
30
43
|
export declare function computeGateResult(prNumber: number, repo: string | null, runGh: RunGhFn, options?: ComputeGateOptions): GateResult;
|
|
@@ -2,7 +2,7 @@ import { resolveMinGreptileConfidence } from "../policy/min-greptile-confidence.
|
|
|
2
2
|
import { buildCiSummaryLine, evaluateCiGate } from "./ci-gate.js";
|
|
3
3
|
import { FALLBACK2_NOT_CLEAN_MSG, VIA_ERROR, VIA_FALLBACK1, VIA_FALLBACK2, VIA_PRIMARY, } from "./constants.js";
|
|
4
4
|
import { evaluateGates, isMergeReady } from "./evaluate.js";
|
|
5
|
-
import { fetchCheckRunsRest, fetchGreptileBodyRest, fetchGreptileCommentBody, fetchPrHeadSha, fetchPrHeadShaRest, resolveRepo, } from "./gh.js";
|
|
5
|
+
import { fetchCheckRunsRest, fetchGreptileBodyRest, fetchGreptileCommentBody, fetchPrBaseRef, fetchPrHeadSha, fetchPrHeadShaRest, fetchRequiredStatusContexts, normalizeRequiredContexts, requiredContextLabel, resolveRepo, } from "./gh.js";
|
|
6
6
|
import { fetchUnresolvedGreptileInlineFindings, inlineFindingsToDict, } from "./greptile-inline.js";
|
|
7
7
|
import { fetchMergeability, isGithubMergeableClean, mergeabilityToDict, verdictBlockIsSoftOnly, verdictShaIsStale, } from "./mergeability.js";
|
|
8
8
|
import { emptyVerdict, parseGreptileBody } from "./parse.js";
|
|
@@ -30,8 +30,8 @@ function resolvedMinConfidence(options) {
|
|
|
30
30
|
}
|
|
31
31
|
return resolveMinGreptileConfidence(options.projectRoot ?? process.cwd()).min;
|
|
32
32
|
}
|
|
33
|
-
/** Run the CI + SLizard gates off a single check-runs fetch (#2169 / #2189). */
|
|
34
|
-
function evaluateCiAndSlizard(checkRuns, options) {
|
|
33
|
+
/** Run the CI + SLizard gates off a single check-runs fetch (#2169 / #2189 / #3234). */
|
|
34
|
+
function evaluateCiAndSlizard(checkRuns, options, requiredContexts = [], requiredMeta = {}) {
|
|
35
35
|
const slizardResult = evaluateSlizardGate(checkRuns, options);
|
|
36
36
|
// The SLizard check is scored by the structured gate, so exclude it from the
|
|
37
37
|
// generic CI required set to avoid double-counting the same run.
|
|
@@ -39,11 +39,16 @@ function evaluateCiAndSlizard(checkRuns, options) {
|
|
|
39
39
|
const ciOptions = {
|
|
40
40
|
skipCi: options.skipCi,
|
|
41
41
|
ignoreCheckNames: [...(options.ignoreCheckNames ?? []), ...slizardNames],
|
|
42
|
+
capacityStallBudgetMs: options.capacityStallBudgetMs,
|
|
43
|
+
nowMs: options.nowMs,
|
|
44
|
+
// Prefer explicit inject (tests) over resolved ruleset/BP list (#3234).
|
|
45
|
+
requiredContexts: options.requiredContexts ?? requiredContexts,
|
|
42
46
|
};
|
|
43
47
|
const ciResult = evaluateCiGate(checkRuns, ciOptions);
|
|
44
48
|
const ci = {
|
|
45
49
|
...ciResult.summary,
|
|
46
50
|
summary_line: buildCiSummaryLine(ciResult.summary),
|
|
51
|
+
...requiredMeta,
|
|
47
52
|
};
|
|
48
53
|
// #3180: static status URLs when weather-class (no network fetch).
|
|
49
54
|
attachPlatformStatusUrls(ci, ciResult.summary.ready_state);
|
|
@@ -53,7 +58,52 @@ function evaluateCiAndSlizard(checkRuns, options) {
|
|
|
53
58
|
slizard: { ...slizardResult.summary },
|
|
54
59
|
};
|
|
55
60
|
}
|
|
56
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Resolve ruleset/BP required contexts for the PR base branch (#3234).
|
|
63
|
+
* Soft-degrades to empty only when sources are cleanly absent (404 / none).
|
|
64
|
+
* Base-ref failures and untrusted inventory (parse/API hard errors) fail closed.
|
|
65
|
+
*/
|
|
66
|
+
function resolveRequiredContextsForPr(prNumber, repo, runGh, options) {
|
|
67
|
+
if (options.requiredContexts !== undefined) {
|
|
68
|
+
return {
|
|
69
|
+
contexts: normalizeRequiredContexts(options.requiredContexts),
|
|
70
|
+
meta: {
|
|
71
|
+
required_contexts_source: "injected",
|
|
72
|
+
},
|
|
73
|
+
hardError: null,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const base = fetchPrBaseRef(prNumber, repo, runGh);
|
|
77
|
+
if (base.baseRef === null) {
|
|
78
|
+
return {
|
|
79
|
+
contexts: [],
|
|
80
|
+
meta: {
|
|
81
|
+
required_contexts_source: "unresolved",
|
|
82
|
+
required_contexts_error: base.error || "base.ref unresolved",
|
|
83
|
+
},
|
|
84
|
+
hardError: "Could not resolve PR base.ref for required status-check contexts; " +
|
|
85
|
+
`fail closed (#3234). Root cause: ${base.error || "base.ref unresolved"}`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const fetchFn = options.fetchRequiredContextsFn ?? fetchRequiredStatusContexts;
|
|
89
|
+
const resolved = fetchFn(repo, base.baseRef, runGh);
|
|
90
|
+
const contexts = normalizeRequiredContexts(resolved.contexts);
|
|
91
|
+
const resolutionFailed = resolved.resolutionFailed === true;
|
|
92
|
+
return {
|
|
93
|
+
contexts,
|
|
94
|
+
meta: {
|
|
95
|
+
required_contexts_source: resolved.sources.length > 0 ? resolved.sources.join("+") : "none",
|
|
96
|
+
required_contexts_base_ref: base.baseRef,
|
|
97
|
+
...(resolved.error ? { required_contexts_error: resolved.error } : {}),
|
|
98
|
+
...(resolutionFailed ? { required_contexts_resolution_failed: true } : {}),
|
|
99
|
+
},
|
|
100
|
+
hardError: resolutionFailed
|
|
101
|
+
? "Required status-check context inventory could not be resolved; " +
|
|
102
|
+
`fail closed (#3234). Root cause: ${resolved.error || "unknown inventory error"}`
|
|
103
|
+
: null,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function applyCiGateForHead(prNumber, repo, headSha, runGh, options) {
|
|
57
107
|
if (repo === null) {
|
|
58
108
|
return {
|
|
59
109
|
failures: [
|
|
@@ -63,6 +113,8 @@ function applyCiGateForHead(repo, headSha, runGh, options) {
|
|
|
63
113
|
ci: {
|
|
64
114
|
ready_state: "blocked",
|
|
65
115
|
error: "repo unresolved for check-runs lookup",
|
|
116
|
+
absent_required: [],
|
|
117
|
+
required_contexts: [],
|
|
66
118
|
},
|
|
67
119
|
slizard: {
|
|
68
120
|
ready_state: "skipped",
|
|
@@ -71,10 +123,35 @@ function applyCiGateForHead(repo, headSha, runGh, options) {
|
|
|
71
123
|
},
|
|
72
124
|
};
|
|
73
125
|
}
|
|
126
|
+
const { contexts: requiredContexts, meta: requiredMeta, hardError, } = resolveRequiredContextsForPr(prNumber, repo, runGh, options);
|
|
127
|
+
if (hardError !== null) {
|
|
128
|
+
return {
|
|
129
|
+
failures: [hardError],
|
|
130
|
+
ci: {
|
|
131
|
+
ready_state: "blocked",
|
|
132
|
+
error: hardError,
|
|
133
|
+
checked_count: 0,
|
|
134
|
+
ignored_checks: [...(options.ignoreCheckNames ?? [])],
|
|
135
|
+
failed_required: [],
|
|
136
|
+
pending_required: [],
|
|
137
|
+
capacity_stalled_required: [],
|
|
138
|
+
cancelled_required: [],
|
|
139
|
+
absent_required: [],
|
|
140
|
+
required_contexts: requiredContexts.map(requiredContextLabel),
|
|
141
|
+
conclusions: [],
|
|
142
|
+
...requiredMeta,
|
|
143
|
+
},
|
|
144
|
+
slizard: {
|
|
145
|
+
ready_state: "skipped",
|
|
146
|
+
present: false,
|
|
147
|
+
summary_line: "SLizard review: skipped (required-context inventory unresolved)",
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
74
151
|
// When CI is skipped we do not fetch check-runs, so the SLizard gate cannot
|
|
75
152
|
// evaluate either; run both gates against an empty set to honor the overrides.
|
|
76
153
|
if (options.skipCi === true) {
|
|
77
|
-
const outcome = evaluateCiAndSlizard([], options);
|
|
154
|
+
const outcome = evaluateCiAndSlizard([], options, requiredContexts, requiredMeta);
|
|
78
155
|
outcome.ci.summary_line = "CI check-runs: skipped (--skip-ci)";
|
|
79
156
|
return { failures: outcome.failures, ci: outcome.ci, slizard: outcome.slizard };
|
|
80
157
|
}
|
|
@@ -94,7 +171,10 @@ function applyCiGateForHead(repo, headSha, runGh, options) {
|
|
|
94
171
|
pending_required: [],
|
|
95
172
|
capacity_stalled_required: [],
|
|
96
173
|
cancelled_required: [],
|
|
174
|
+
absent_required: [],
|
|
175
|
+
required_contexts: requiredContexts.map(requiredContextLabel),
|
|
97
176
|
conclusions: [],
|
|
177
|
+
...requiredMeta,
|
|
98
178
|
},
|
|
99
179
|
slizard: {
|
|
100
180
|
ready_state: "skipped",
|
|
@@ -103,7 +183,7 @@ function applyCiGateForHead(repo, headSha, runGh, options) {
|
|
|
103
183
|
},
|
|
104
184
|
};
|
|
105
185
|
}
|
|
106
|
-
const outcome = evaluateCiAndSlizard(check.checkRuns, options);
|
|
186
|
+
const outcome = evaluateCiAndSlizard(check.checkRuns, options, requiredContexts, requiredMeta);
|
|
107
187
|
return { failures: outcome.failures, ci: outcome.ci, slizard: outcome.slizard };
|
|
108
188
|
}
|
|
109
189
|
/**
|
|
@@ -134,7 +214,7 @@ function finalizeVerdictGate(prNumber, repo, headSha, verdict, runGh, options) {
|
|
|
134
214
|
minConfidence,
|
|
135
215
|
});
|
|
136
216
|
if (failures.length === 0) {
|
|
137
|
-
const ci = applyCiGateForHead(resolved.repo, headSha, runGh, options);
|
|
217
|
+
const ci = applyCiGateForHead(prNumber, resolved.repo, headSha, runGh, options);
|
|
138
218
|
failures.push(...ci.failures);
|
|
139
219
|
partialData.ci = ci.ci;
|
|
140
220
|
partialData.slizard = ci.slizard;
|
|
@@ -148,17 +228,23 @@ function finalizeVerdictGate(prNumber, repo, headSha, verdict, runGh, options) {
|
|
|
148
228
|
!verdictBlockIsSoftOnly(verdict, headSha, inline, minConfidence)) {
|
|
149
229
|
return { failures, partialData };
|
|
150
230
|
}
|
|
151
|
-
const ci = applyCiGateForHead(resolved.repo, headSha, runGh, options);
|
|
231
|
+
const ci = applyCiGateForHead(prNumber, resolved.repo, headSha, runGh, options);
|
|
152
232
|
partialData.ci = ci.ci;
|
|
153
233
|
partialData.slizard = ci.slizard;
|
|
154
234
|
const fetchMergeabilityFn = options.fetchMergeabilityFn ?? fetchMergeability;
|
|
155
235
|
const signal = fetchMergeabilityFn(prNumber, resolved.repo, runGh);
|
|
156
236
|
partialData.mergeability = mergeabilityToDict(signal);
|
|
237
|
+
// #3234: never discard CI gate failures during soft-verdict reconciliation.
|
|
238
|
+
// GitHub CLEAN must not erase ci_absent_required / inventory hard-fails.
|
|
239
|
+
if (ci.failures.length > 0) {
|
|
240
|
+
failures.push(...ci.failures);
|
|
241
|
+
return { failures, partialData };
|
|
242
|
+
}
|
|
157
243
|
if (isGithubMergeableClean(signal)) {
|
|
158
|
-
// GitHub itself reports CLEAN + MERGEABLE
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
244
|
+
// GitHub itself reports CLEAN + MERGEABLE and our CI inventory is green:
|
|
245
|
+
// proceed even though the OPTIONAL review verdict is absent/stale for the
|
|
246
|
+
// head SHA -- exactly the manual `gh pr merge --admin` case this fix
|
|
247
|
+
// reproduces automatically (#2260).
|
|
162
248
|
partialData.verdict_override = {
|
|
163
249
|
reason: verdictShaIsStale(verdict, headSha) ? "verdict-stale-head-sha" : "verdict-absent",
|
|
164
250
|
basis: "github mergeable_state=clean + mergeable=true (mergeStateStatus:CLEAN + " +
|
|
@@ -168,8 +254,7 @@ function finalizeVerdictGate(prNumber, repo, headSha, verdict, runGh, options) {
|
|
|
168
254
|
return { failures: [], partialData };
|
|
169
255
|
}
|
|
170
256
|
// Not overridable (GitHub not CLEAN/MERGEABLE). Preserve the soft verdict
|
|
171
|
-
// failures
|
|
172
|
-
failures.push(...ci.failures);
|
|
257
|
+
// failures so the heartbeat's blocked-on is clear.
|
|
173
258
|
return { failures, partialData };
|
|
174
259
|
}
|
|
175
260
|
function computePrimary(prNumber, repo, runGh, options) {
|
|
@@ -9,7 +9,26 @@ export interface CheckRunRecord {
|
|
|
9
9
|
readonly created_at?: string | null;
|
|
10
10
|
/** ISO started_at — null while still queued with no runner (#2672). */
|
|
11
11
|
readonly started_at?: string | null;
|
|
12
|
+
/**
|
|
13
|
+
* GitHub App id that produced the check run (`app.id`), when present.
|
|
14
|
+
* Used to match app-bound required contexts from rulesets / branch protection (#3234).
|
|
15
|
+
*/
|
|
16
|
+
readonly appId?: number | null;
|
|
12
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* A required status-check context name, optionally bound to a GitHub App id
|
|
20
|
+
* (branch-protection `app_id` / ruleset `integration_id`) (#3234).
|
|
21
|
+
*/
|
|
22
|
+
export interface RequiredStatusContext {
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly appId?: number | null;
|
|
25
|
+
}
|
|
26
|
+
/** Display / inventory key for a required context (includes app binding when set). */
|
|
27
|
+
export declare function requiredContextLabel(ctx: RequiredStatusContext): string;
|
|
28
|
+
/** Normalize injected string names or full specs into RequiredStatusContext (#3234). */
|
|
29
|
+
export declare function normalizeRequiredContexts(input: readonly (string | RequiredStatusContext)[] | undefined): RequiredStatusContext[];
|
|
30
|
+
/** True when an observed check-run satisfies a required context (name + optional app id). */
|
|
31
|
+
export declare function checkRunMatchesRequiredContext(run: CheckRunRecord, ctx: RequiredStatusContext): boolean;
|
|
13
32
|
/** UTF-8-safe gh capture via execFile (no shell) — mirrors _safe_subprocess.run_text (#1366). */
|
|
14
33
|
export declare function defaultRunGh(cmd: readonly string[]): RunGhResult;
|
|
15
34
|
export declare function fetchPrHeadSha(prNumber: number, repo: string | null, runGh: RunGhFn): string | null;
|
|
@@ -31,4 +50,34 @@ export declare function fetchCheckRunsRest(sha: string, repo: string, runGh: Run
|
|
|
31
50
|
checkRuns: CheckRunRecord[];
|
|
32
51
|
error: string;
|
|
33
52
|
};
|
|
53
|
+
/** PR base branch ref (rulesets / branch protection apply to the merge target) (#3234). */
|
|
54
|
+
export declare function fetchPrBaseRef(prNumber: number, repo: string, runGh: RunGhFn): {
|
|
55
|
+
baseRef: string | null;
|
|
56
|
+
error: string;
|
|
57
|
+
};
|
|
58
|
+
/** Extract required contexts from `GET .../rules/branches/{branch}` payload (#3234). */
|
|
59
|
+
export declare function contextsFromBranchRules(payload: unknown): RequiredStatusContext[];
|
|
60
|
+
/** Extract required contexts from classic branch-protection payload (#3234). */
|
|
61
|
+
export declare function contextsFromBranchProtection(payload: unknown): RequiredStatusContext[];
|
|
62
|
+
export interface RequiredStatusContextsResult {
|
|
63
|
+
readonly contexts: readonly RequiredStatusContext[];
|
|
64
|
+
/** Which REST surfaces contributed contexts (`rulesets` / `branch_protection`). */
|
|
65
|
+
readonly sources: readonly string[];
|
|
66
|
+
readonly error: string;
|
|
67
|
+
/**
|
|
68
|
+
* True when inventory could not be trusted (parse failure / non-404 REST error
|
|
69
|
+
* with no successful source). Callers MUST fail closed (#3234 Greptile P1).
|
|
70
|
+
*/
|
|
71
|
+
readonly resolutionFailed: boolean;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolve required status-check contexts for a branch from rulesets and/or
|
|
75
|
+
* classic branch protection (REST only) (#3234).
|
|
76
|
+
*
|
|
77
|
+
* Absent or 404 sources are soft-skipped (empty contribution). Parse failures
|
|
78
|
+
* and non-404 REST errors with no successful source set `resolutionFailed`.
|
|
79
|
+
* Callers compare returned contexts against exact-HEAD check runs (name +
|
|
80
|
+
* optional app id) and fail closed on absent required contexts.
|
|
81
|
+
*/
|
|
82
|
+
export declare function fetchRequiredStatusContexts(repo: string, branch: string, runGh: RunGhFn): RequiredStatusContextsResult;
|
|
34
83
|
//# sourceMappingURL=gh.d.ts.map
|