@deftai/directive-core 0.99.0 → 0.101.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 +241 -30
- package/dist/authz/decompose-apply.d.ts +81 -0
- package/dist/authz/decompose-apply.js +377 -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/check/cached-orchestrator.d.ts +26 -5
- package/dist/check/cached-orchestrator.js +228 -6
- package/dist/check/gate-lists.d.ts +10 -4
- package/dist/check/gate-lists.js +19 -5
- package/dist/check/index.d.ts +3 -2
- package/dist/check/index.js +2 -1
- package/dist/check/named-cause.d.ts +45 -0
- package/dist/check/named-cause.js +121 -0
- package/dist/check/orchestrator.js +6 -1
- package/dist/doctor/checks.d.ts +44 -0
- package/dist/doctor/checks.js +428 -2
- package/dist/doctor/help.d.ts +5 -0
- package/dist/doctor/help.js +31 -0
- package/dist/doctor/index.d.ts +2 -0
- package/dist/doctor/index.js +2 -0
- package/dist/doctor/main.d.ts +6 -7
- package/dist/doctor/main.js +130 -79
- package/dist/doctor/session-coda.d.ts +82 -0
- package/dist/doctor/session-coda.js +151 -0
- package/dist/doctor/types.d.ts +16 -0
- 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 +3 -0
- package/dist/index.js +3 -0
- package/dist/init-deposit/agent-hooks.d.ts +20 -0
- package/dist/init-deposit/agent-hooks.js +51 -31
- package/dist/init-deposit/gitignore.js +2 -0
- package/dist/intake/issue-ingest.js +36 -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/ac-pass-banking.d.ts +58 -0
- package/dist/policy/ac-pass-banking.js +150 -0
- package/dist/policy/ceremony-dial.d.ts +96 -11
- package/dist/policy/ceremony-dial.js +250 -23
- package/dist/policy/index.d.ts +2 -0
- package/dist/policy/index.js +16 -1
- 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/preflight-cache/evaluate.d.ts +3 -0
- package/dist/preflight-cache/evaluate.js +3 -0
- package/dist/product-first-done-gate/acceptance.d.ts +25 -0
- package/dist/product-first-done-gate/acceptance.js +247 -0
- package/dist/product-first-done-gate/check-mode.d.ts +45 -0
- package/dist/product-first-done-gate/check-mode.js +148 -0
- package/dist/product-first-done-gate/evaluate.d.ts +50 -0
- package/dist/product-first-done-gate/evaluate.js +255 -0
- package/dist/product-first-done-gate/index.d.ts +11 -0
- package/dist/product-first-done-gate/index.js +11 -0
- package/dist/product-first-done-gate/types.d.ts +64 -0
- package/dist/product-first-done-gate/types.js +61 -0
- package/dist/render/framework-commands.js +4 -0
- package/dist/run-summary/emit.d.ts +56 -0
- package/dist/run-summary/emit.js +171 -0
- package/dist/run-summary/index.d.ts +4 -0
- package/dist/run-summary/index.js +4 -0
- package/dist/run-summary/path.d.ts +25 -0
- package/dist/run-summary/path.js +78 -0
- package/dist/run-summary/types.d.ts +76 -0
- package/dist/run-summary/types.js +21 -0
- package/dist/scope/acceptance-evidence.d.ts +110 -0
- package/dist/scope/acceptance-evidence.js +422 -0
- package/dist/scope/coverage-map.d.ts +98 -0
- package/dist/scope/coverage-map.js +558 -0
- package/dist/scope/decompose.d.ts +5 -0
- package/dist/scope/decompose.js +134 -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/ac-pass-banking.d.ts +221 -0
- package/dist/session/ac-pass-banking.js +761 -0
- package/dist/session/active-cli.d.ts +79 -0
- package/dist/session/active-cli.js +382 -0
- package/dist/session/deposit-sha.d.ts +49 -0
- package/dist/session/deposit-sha.js +121 -0
- package/dist/session/effort-budget.d.ts +136 -0
- package/dist/session/effort-budget.js +352 -0
- package/dist/session/index.d.ts +7 -0
- package/dist/session/index.js +7 -0
- package/dist/session/orientation-compression.d.ts +127 -0
- package/dist/session/orientation-compression.js +425 -0
- package/dist/session/orientation-state.d.ts +29 -0
- package/dist/session/orientation-state.js +85 -0
- package/dist/session/session-start.d.ts +31 -0
- package/dist/session/session-start.js +228 -2
- package/dist/session/toolchain-preflight.d.ts +64 -0
- package/dist/session/toolchain-preflight.js +151 -0
- 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/triage/bootstrap/gitignore.d.ts +1 -1
- package/dist/triage/bootstrap/gitignore.js +8 -2
- package/dist/triage/bootstrap/index.js +15 -3
- package/dist/triage/classify/index.d.ts +1 -0
- package/dist/triage/classify/index.js +2 -0
- package/dist/triage/classify/label-mirror.js +16 -0
- package/dist/triage/classify/mirror-discovery-tip.d.ts +95 -0
- package/dist/triage/classify/mirror-discovery-tip.js +234 -0
- package/dist/triage/reconcile/reconcile.js +28 -6
- package/dist/triage/summary/index.js +12 -4
- package/dist/triage/welcome/default-mode.js +16 -0
- package/dist/ts-check-lane/run-lane.js +21 -4
- package/dist/vbrief-validate/conformance.js +7 -0
- package/dist/vitest-runner/coverage-debt-teardown.js +16 -4
- package/dist/xbrief-migrate/migrate-project.d.ts +4 -0
- package/dist/xbrief-migrate/migrate-project.js +92 -3
- package/package.json +15 -3
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Head-bound human merge approval (#3235).
|
|
3
|
+
*
|
|
4
|
+
* Phase 5 → 6 `plan:approved` events authorize one immutable PR head.
|
|
5
|
+
* When PR HEAD changes after approval (and GitHub auto-merge may still be
|
|
6
|
+
* enabled), Directive fails closed: treats the approval as stale, attempts to
|
|
7
|
+
* disable auto-merge, and emits recovery text requiring a fresh approval bound
|
|
8
|
+
* to the current head.
|
|
9
|
+
*
|
|
10
|
+
* Prefer this path from finish-loop / pr-wait-mergeable / require-human-merge
|
|
11
|
+
* consumers. Does not change consumer repository rulesets.
|
|
12
|
+
*/
|
|
13
|
+
import { type BehavioralEventRecord } from "../lifecycle/events.js";
|
|
14
|
+
/** Event name recorded at Phase 5 → 6 approval. */
|
|
15
|
+
export declare const PLAN_APPROVED_EVENT = "plan:approved";
|
|
16
|
+
export type MergeApprovalHeadStatus =
|
|
17
|
+
/** approved_head_sha matches current_head_sha. */
|
|
18
|
+
"ok"
|
|
19
|
+
/** Approval bound to a different head than current. */
|
|
20
|
+
| "stale"
|
|
21
|
+
/** Approval exists but was not bound to a head SHA. */
|
|
22
|
+
| "missing_binding"
|
|
23
|
+
/** No plan:approved record for this PR. */
|
|
24
|
+
| "no_approval"
|
|
25
|
+
/** Gate intentionally skipped (e.g. no PR / no current head). */
|
|
26
|
+
| "skipped";
|
|
27
|
+
export interface PlanApprovedRecord {
|
|
28
|
+
readonly head_sha: string | null;
|
|
29
|
+
readonly pr_number: number | null;
|
|
30
|
+
readonly approver: string | null;
|
|
31
|
+
readonly plan_ref: string | null;
|
|
32
|
+
readonly detected_at: string | null;
|
|
33
|
+
readonly payload: Readonly<Record<string, unknown>>;
|
|
34
|
+
}
|
|
35
|
+
export interface MergeApprovalHeadResult {
|
|
36
|
+
readonly status: MergeApprovalHeadStatus;
|
|
37
|
+
/** False when merge must not proceed under head-bound contract. */
|
|
38
|
+
readonly allowed: boolean;
|
|
39
|
+
readonly approved_head_sha: string | null;
|
|
40
|
+
readonly current_head_sha: string | null;
|
|
41
|
+
readonly pr_number: number;
|
|
42
|
+
readonly require_human_merge: boolean;
|
|
43
|
+
readonly auto_merge_disabled: boolean | null;
|
|
44
|
+
readonly message: string;
|
|
45
|
+
readonly recovery: string | null;
|
|
46
|
+
}
|
|
47
|
+
export type DisableAutoMergeFn = (prNumber: number, repo: string | null) => {
|
|
48
|
+
readonly ok: boolean;
|
|
49
|
+
readonly stderr: string;
|
|
50
|
+
};
|
|
51
|
+
export type FetchPrHeadShaFn = (prNumber: number, repo: string | null) => string | null;
|
|
52
|
+
export type ReadEventsFn = (logPath?: string | null) => BehavioralEventRecord[];
|
|
53
|
+
/** Full Git object id length — approvals must bind to exact heads (#3235 / Greptile P1). */
|
|
54
|
+
export declare const FULL_HEAD_SHA_LEN = 40;
|
|
55
|
+
/**
|
|
56
|
+
* Exact full-SHA equality only (#3235).
|
|
57
|
+
* Prefix matching is forbidden: abbreviated approval SHAs must not authorize a later head.
|
|
58
|
+
*/
|
|
59
|
+
export declare function headShaMatches(a: string | null, b: string | null): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Latest plan:approved for a PR (by log order; last write wins).
|
|
62
|
+
* When `repo` is set, prefer records whose repository/plan_ref matches (P2 #3235).
|
|
63
|
+
*/
|
|
64
|
+
export declare function findLatestPlanApprovalForPr(prNumber: number, options?: {
|
|
65
|
+
readonly logPath?: string | null;
|
|
66
|
+
readonly readEventsFn?: ReadEventsFn;
|
|
67
|
+
readonly records?: readonly BehavioralEventRecord[];
|
|
68
|
+
/** owner/repo — scopes lookup when multiple remotes share a PR number. */
|
|
69
|
+
readonly repo?: string | null;
|
|
70
|
+
}): PlanApprovedRecord | null;
|
|
71
|
+
/** Recovery instructions when approval is stale or unbound. */
|
|
72
|
+
export declare function buildMergeApprovalRecovery(input: {
|
|
73
|
+
readonly prNumber: number;
|
|
74
|
+
readonly approvedHeadSha: string | null;
|
|
75
|
+
readonly currentHeadSha: string | null;
|
|
76
|
+
readonly status: MergeApprovalHeadStatus;
|
|
77
|
+
readonly autoMergeDisabled: boolean | null;
|
|
78
|
+
}): string;
|
|
79
|
+
export interface EvaluateMergeApprovalHeadInput {
|
|
80
|
+
readonly prNumber: number;
|
|
81
|
+
readonly currentHeadSha: string | null;
|
|
82
|
+
readonly projectRoot?: string;
|
|
83
|
+
readonly logPath?: string | null;
|
|
84
|
+
readonly requireHumanMerge?: boolean;
|
|
85
|
+
/** owner/repo for scoped approval lookup. */
|
|
86
|
+
readonly repo?: string | null;
|
|
87
|
+
/**
|
|
88
|
+
* When true (default), approval without head_sha fails closed under
|
|
89
|
+
* requireHumanMerge. When false, missing binding is advisory only.
|
|
90
|
+
*/
|
|
91
|
+
readonly enforceStrictBinding?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* When true, run the gate whenever a plan:approved exists for the PR
|
|
94
|
+
* (default). When false, only under requireHumanMerge.
|
|
95
|
+
*/
|
|
96
|
+
readonly enforceWhenApprovalPresent?: boolean;
|
|
97
|
+
readonly readEventsFn?: ReadEventsFn;
|
|
98
|
+
readonly records?: readonly BehavioralEventRecord[];
|
|
99
|
+
readonly approval?: PlanApprovedRecord | null;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Pure evaluation: compare recorded approval head to current PR HEAD.
|
|
103
|
+
*/
|
|
104
|
+
export declare function evaluateMergeApprovalHead(input: EvaluateMergeApprovalHeadInput): MergeApprovalHeadResult;
|
|
105
|
+
/** Live `gh pr merge <N> --disable-auto` (best-effort). */
|
|
106
|
+
export declare function disablePullRequestAutoMerge(prNumber: number, repo: string | null, options?: {
|
|
107
|
+
readonly timeoutSec?: number;
|
|
108
|
+
}): {
|
|
109
|
+
readonly ok: boolean;
|
|
110
|
+
readonly stderr: string;
|
|
111
|
+
};
|
|
112
|
+
/** REST HEAD read via `gh api repos/.../pulls/N --jq .head.sha`. */
|
|
113
|
+
export declare function fetchPrHeadShaRest(prNumber: number, repo: string | null, options?: {
|
|
114
|
+
readonly timeoutSec?: number;
|
|
115
|
+
}): string | null;
|
|
116
|
+
export interface EnforceMergeApprovalHeadInput extends EvaluateMergeApprovalHeadInput {
|
|
117
|
+
readonly repo?: string | null;
|
|
118
|
+
/** When true (default on deny), attempt gh --disable-auto. */
|
|
119
|
+
readonly disableAutoMergeOnDeny?: boolean;
|
|
120
|
+
readonly disableAutoMergeFn?: DisableAutoMergeFn;
|
|
121
|
+
readonly fetchHeadShaFn?: FetchPrHeadShaFn;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Evaluate + on deny attempt to disable GitHub auto-merge and attach recovery.
|
|
125
|
+
*/
|
|
126
|
+
export declare function enforceMergeApprovalHead(input: EnforceMergeApprovalHeadInput): MergeApprovalHeadResult;
|
|
127
|
+
/** Default event log path relative to project root. */
|
|
128
|
+
export declare function defaultMergeApprovalEventLog(projectRoot: string): string;
|
|
129
|
+
//# sourceMappingURL=merge-approval-head.d.ts.map
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Head-bound human merge approval (#3235).
|
|
3
|
+
*
|
|
4
|
+
* Phase 5 → 6 `plan:approved` events authorize one immutable PR head.
|
|
5
|
+
* When PR HEAD changes after approval (and GitHub auto-merge may still be
|
|
6
|
+
* enabled), Directive fails closed: treats the approval as stale, attempts to
|
|
7
|
+
* disable auto-merge, and emits recovery text requiring a fresh approval bound
|
|
8
|
+
* to the current head.
|
|
9
|
+
*
|
|
10
|
+
* Prefer this path from finish-loop / pr-wait-mergeable / require-human-merge
|
|
11
|
+
* consumers. Does not change consumer repository rulesets.
|
|
12
|
+
*/
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { DEFAULT_EVENT_LOG, readEvents } from "../lifecycle/events.js";
|
|
15
|
+
import { captureExec } from "../pr-wait-mergeable/wrappers.js";
|
|
16
|
+
import { resolveBinary } from "../scm/binary.js";
|
|
17
|
+
import { resolveHumanMergePolicy } from "./require-human-merge.js";
|
|
18
|
+
/** Event name recorded at Phase 5 → 6 approval. */
|
|
19
|
+
export const PLAN_APPROVED_EVENT = "plan:approved";
|
|
20
|
+
/** Full Git object id length — approvals must bind to exact heads (#3235 / Greptile P1). */
|
|
21
|
+
export const FULL_HEAD_SHA_LEN = 40;
|
|
22
|
+
function isHexSha(value) {
|
|
23
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
24
|
+
const c = value.charCodeAt(i);
|
|
25
|
+
const isDigit = c >= 48 && c <= 57;
|
|
26
|
+
const isAf = c >= 97 && c <= 102;
|
|
27
|
+
if (!isDigit && !isAf)
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return value.length > 0;
|
|
31
|
+
}
|
|
32
|
+
function normalizeSha(value) {
|
|
33
|
+
if (typeof value !== "string")
|
|
34
|
+
return null;
|
|
35
|
+
const trimmed = value.trim().toLowerCase();
|
|
36
|
+
// Exact full-object-id only — reject short and overlong tokens (#3235 Greptile).
|
|
37
|
+
if (trimmed.length !== FULL_HEAD_SHA_LEN)
|
|
38
|
+
return null;
|
|
39
|
+
if (!isHexSha(trimmed))
|
|
40
|
+
return null;
|
|
41
|
+
return trimmed;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Exact full-SHA equality only (#3235).
|
|
45
|
+
* Prefix matching is forbidden: abbreviated approval SHAs must not authorize a later head.
|
|
46
|
+
*/
|
|
47
|
+
export function headShaMatches(a, b) {
|
|
48
|
+
if (a === null || b === null)
|
|
49
|
+
return false;
|
|
50
|
+
const left = a.trim().toLowerCase();
|
|
51
|
+
const right = b.trim().toLowerCase();
|
|
52
|
+
if (left.length !== FULL_HEAD_SHA_LEN || right.length !== FULL_HEAD_SHA_LEN)
|
|
53
|
+
return false;
|
|
54
|
+
if (!isHexSha(left) || !isHexSha(right))
|
|
55
|
+
return false;
|
|
56
|
+
return left === right;
|
|
57
|
+
}
|
|
58
|
+
/** Extract trailing decimal digits starting at index (no regex — CodeQL safe). */
|
|
59
|
+
function parseLeadingDigits(text, start) {
|
|
60
|
+
let digits = "";
|
|
61
|
+
for (let i = start; i < text.length; i += 1) {
|
|
62
|
+
const c = text.charCodeAt(i);
|
|
63
|
+
if (c < 48 || c > 57)
|
|
64
|
+
break;
|
|
65
|
+
digits += text[i];
|
|
66
|
+
}
|
|
67
|
+
if (digits.length === 0)
|
|
68
|
+
return null;
|
|
69
|
+
const n = Number.parseInt(digits, 10);
|
|
70
|
+
return Number.isInteger(n) && n > 0 ? n : null;
|
|
71
|
+
}
|
|
72
|
+
function prNumberFromPayload(payload) {
|
|
73
|
+
const raw = payload.pr_number;
|
|
74
|
+
if (typeof raw === "number" && Number.isInteger(raw) && raw > 0)
|
|
75
|
+
return raw;
|
|
76
|
+
if (typeof raw === "string") {
|
|
77
|
+
const trimmed = raw.trim();
|
|
78
|
+
let allDigits = trimmed.length > 0;
|
|
79
|
+
for (let i = 0; i < trimmed.length; i += 1) {
|
|
80
|
+
const c = trimmed.charCodeAt(i);
|
|
81
|
+
if (c < 48 || c > 57) {
|
|
82
|
+
allDigits = false;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (allDigits)
|
|
87
|
+
return Number.parseInt(trimmed, 10);
|
|
88
|
+
}
|
|
89
|
+
const planRef = payload.plan_ref;
|
|
90
|
+
if (typeof planRef === "string") {
|
|
91
|
+
const lower = planRef.toLowerCase();
|
|
92
|
+
const marker = "/pull/";
|
|
93
|
+
const idx = lower.indexOf(marker);
|
|
94
|
+
if (idx >= 0) {
|
|
95
|
+
return parseLeadingDigits(planRef, idx + marker.length);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
function repositoryFromPayload(payload) {
|
|
101
|
+
if (typeof payload.repository === "string" && payload.repository.includes("/")) {
|
|
102
|
+
return payload.repository.toLowerCase();
|
|
103
|
+
}
|
|
104
|
+
if (typeof payload.plan_ref === "string") {
|
|
105
|
+
const lower = payload.plan_ref.toLowerCase();
|
|
106
|
+
const marker = "github.com/";
|
|
107
|
+
const idx = lower.indexOf(marker);
|
|
108
|
+
if (idx >= 0) {
|
|
109
|
+
const rest = payload.plan_ref.slice(idx + marker.length);
|
|
110
|
+
const parts = rest.split("/");
|
|
111
|
+
if (parts.length >= 2 && parts[0] && parts[1]) {
|
|
112
|
+
return `${parts[0]}/${parts[1]}`.toLowerCase();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
function toPlanApproved(record) {
|
|
119
|
+
const payload = record.payload;
|
|
120
|
+
return {
|
|
121
|
+
head_sha: normalizeSha(payload.head_sha),
|
|
122
|
+
pr_number: prNumberFromPayload(payload),
|
|
123
|
+
approver: typeof payload.approver === "string" ? payload.approver : null,
|
|
124
|
+
plan_ref: typeof payload.plan_ref === "string" ? payload.plan_ref : null,
|
|
125
|
+
detected_at: typeof record.detected_at === "string" ? record.detected_at : null,
|
|
126
|
+
payload,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Latest plan:approved for a PR (by log order; last write wins).
|
|
131
|
+
* When `repo` is set, prefer records whose repository/plan_ref matches (P2 #3235).
|
|
132
|
+
*/
|
|
133
|
+
export function findLatestPlanApprovalForPr(prNumber, options = {}) {
|
|
134
|
+
const stream = options.records !== undefined
|
|
135
|
+
? options.records
|
|
136
|
+
: (options.readEventsFn ?? readEvents)(options.logPath ?? null);
|
|
137
|
+
const wantRepo = typeof options.repo === "string" && options.repo.includes("/")
|
|
138
|
+
? options.repo.toLowerCase()
|
|
139
|
+
: null;
|
|
140
|
+
let latest = null;
|
|
141
|
+
for (const record of stream) {
|
|
142
|
+
if (record.event !== PLAN_APPROVED_EVENT)
|
|
143
|
+
continue;
|
|
144
|
+
const approved = toPlanApproved(record);
|
|
145
|
+
if (approved.pr_number !== prNumber)
|
|
146
|
+
continue;
|
|
147
|
+
if (wantRepo !== null) {
|
|
148
|
+
// When repo is known, never fall back to an unscoped same-number event
|
|
149
|
+
// (cross-repo shadow / Greptile conf floor on #3235).
|
|
150
|
+
const recRepo = repositoryFromPayload(record.payload);
|
|
151
|
+
if (recRepo !== wantRepo)
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
latest = approved;
|
|
155
|
+
}
|
|
156
|
+
return latest;
|
|
157
|
+
}
|
|
158
|
+
/** Recovery instructions when approval is stale or unbound. */
|
|
159
|
+
export function buildMergeApprovalRecovery(input) {
|
|
160
|
+
const approved = input.approvedHeadSha ?? "<none>";
|
|
161
|
+
const current = input.currentHeadSha ?? "<unknown>";
|
|
162
|
+
const lines = [
|
|
163
|
+
"Recovery (#3235 head-bound merge approval):",
|
|
164
|
+
` • approved_head_sha=${approved}`,
|
|
165
|
+
` • current_head_sha=${current}`,
|
|
166
|
+
];
|
|
167
|
+
if (input.autoMergeDisabled === true) {
|
|
168
|
+
lines.push(" • GitHub auto-merge was disabled for this PR (stale authorization revoked).");
|
|
169
|
+
}
|
|
170
|
+
else if (input.autoMergeDisabled === false) {
|
|
171
|
+
lines.push(" • Could not confirm auto-merge disable — run:", ` gh pr merge ${input.prNumber} --disable-auto`);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
lines.push(" • If auto-merge is still enabled, disable it:", ` gh pr merge ${input.prNumber} --disable-auto`);
|
|
175
|
+
}
|
|
176
|
+
lines.push(" • Re-run Phase 5 → 6 approval for the CURRENT head:", " task lifecycle:event -- emit plan:approved \\", " --plan-ref <pr-url> --approver <login> --approval-phrase yes \\", ` --pr-number ${input.prNumber} --head-sha ${current}`, " • Only after a fresh head-bound approval may auto-merge be re-enabled", " or the human merge proceed for this head.");
|
|
177
|
+
if (input.status === "missing_binding") {
|
|
178
|
+
lines.push(" • Prior approval lacked --head-sha; re-emit with --head-sha bound to current HEAD.");
|
|
179
|
+
}
|
|
180
|
+
return lines.join("\n");
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Pure evaluation: compare recorded approval head to current PR HEAD.
|
|
184
|
+
*/
|
|
185
|
+
export function evaluateMergeApprovalHead(input) {
|
|
186
|
+
const prNumber = input.prNumber;
|
|
187
|
+
const currentHeadSha = normalizeSha(input.currentHeadSha);
|
|
188
|
+
const requireHumanMerge = input.requireHumanMerge !== undefined
|
|
189
|
+
? input.requireHumanMerge
|
|
190
|
+
: input.projectRoot !== undefined
|
|
191
|
+
? resolveHumanMergePolicy(input.projectRoot).requireHumanMerge
|
|
192
|
+
: false;
|
|
193
|
+
const enforceWhenApprovalPresent = input.enforceWhenApprovalPresent !== false;
|
|
194
|
+
const enforceStrictBinding = input.enforceStrictBinding !== false;
|
|
195
|
+
if (prNumber <= 0) {
|
|
196
|
+
return {
|
|
197
|
+
status: "skipped",
|
|
198
|
+
allowed: true,
|
|
199
|
+
approved_head_sha: null,
|
|
200
|
+
current_head_sha: currentHeadSha,
|
|
201
|
+
pr_number: prNumber,
|
|
202
|
+
require_human_merge: requireHumanMerge,
|
|
203
|
+
auto_merge_disabled: null,
|
|
204
|
+
message: "merge-approval-head: skipped (invalid pr number)",
|
|
205
|
+
recovery: null,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const resolvedLogPath = input.logPath !== undefined
|
|
209
|
+
? input.logPath
|
|
210
|
+
: input.projectRoot !== undefined
|
|
211
|
+
? join(input.projectRoot, DEFAULT_EVENT_LOG)
|
|
212
|
+
: null;
|
|
213
|
+
const wantRepo = typeof input.repo === "string" && input.repo.includes("/") ? input.repo : null;
|
|
214
|
+
// Fail closed when approvals exist for this PR number but callers omitted repo
|
|
215
|
+
// (cross-repo same-number collision / Greptile #3235).
|
|
216
|
+
if (input.approval === undefined && wantRepo === null) {
|
|
217
|
+
const unscoped = findLatestPlanApprovalForPr(prNumber, {
|
|
218
|
+
logPath: resolvedLogPath,
|
|
219
|
+
readEventsFn: input.readEventsFn,
|
|
220
|
+
records: input.records,
|
|
221
|
+
repo: null,
|
|
222
|
+
});
|
|
223
|
+
if (unscoped !== null && (requireHumanMerge || enforceWhenApprovalPresent)) {
|
|
224
|
+
const recovery = buildMergeApprovalRecovery({
|
|
225
|
+
prNumber,
|
|
226
|
+
approvedHeadSha: unscoped.head_sha,
|
|
227
|
+
currentHeadSha,
|
|
228
|
+
status: "stale",
|
|
229
|
+
autoMergeDisabled: null,
|
|
230
|
+
});
|
|
231
|
+
return {
|
|
232
|
+
status: "stale",
|
|
233
|
+
allowed: false,
|
|
234
|
+
approved_head_sha: unscoped.head_sha,
|
|
235
|
+
current_head_sha: currentHeadSha,
|
|
236
|
+
pr_number: prNumber,
|
|
237
|
+
require_human_merge: requireHumanMerge,
|
|
238
|
+
auto_merge_disabled: null,
|
|
239
|
+
message: `❌ merge-approval-head (#3235): plan:approved exists for PR #${prNumber} but ` +
|
|
240
|
+
"repository is unknown — pass --repo owner/repo (or GH_REPO) so approval " +
|
|
241
|
+
"lookup cannot apply a foreign same-number event.",
|
|
242
|
+
recovery,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const approval = input.approval !== undefined
|
|
247
|
+
? input.approval
|
|
248
|
+
: findLatestPlanApprovalForPr(prNumber, {
|
|
249
|
+
logPath: resolvedLogPath,
|
|
250
|
+
readEventsFn: input.readEventsFn,
|
|
251
|
+
records: input.records,
|
|
252
|
+
repo: wantRepo,
|
|
253
|
+
});
|
|
254
|
+
if (approval === null) {
|
|
255
|
+
// No Directive-owned approval — gate is a no-op (human-merge policy is separate).
|
|
256
|
+
return {
|
|
257
|
+
status: "no_approval",
|
|
258
|
+
allowed: true,
|
|
259
|
+
approved_head_sha: null,
|
|
260
|
+
current_head_sha: currentHeadSha,
|
|
261
|
+
pr_number: prNumber,
|
|
262
|
+
require_human_merge: requireHumanMerge,
|
|
263
|
+
auto_merge_disabled: null,
|
|
264
|
+
message: `merge-approval-head: no plan:approved for PR #${prNumber}`,
|
|
265
|
+
recovery: null,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (!enforceWhenApprovalPresent && !requireHumanMerge) {
|
|
269
|
+
return {
|
|
270
|
+
status: "skipped",
|
|
271
|
+
allowed: true,
|
|
272
|
+
approved_head_sha: approval.head_sha,
|
|
273
|
+
current_head_sha: currentHeadSha,
|
|
274
|
+
pr_number: prNumber,
|
|
275
|
+
require_human_merge: requireHumanMerge,
|
|
276
|
+
auto_merge_disabled: null,
|
|
277
|
+
message: "merge-approval-head: skipped (no requireHumanMerge / not enforcing)",
|
|
278
|
+
recovery: null,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
if (approval.head_sha === null) {
|
|
282
|
+
const recovery = buildMergeApprovalRecovery({
|
|
283
|
+
prNumber,
|
|
284
|
+
approvedHeadSha: null,
|
|
285
|
+
currentHeadSha,
|
|
286
|
+
status: "missing_binding",
|
|
287
|
+
autoMergeDisabled: null,
|
|
288
|
+
});
|
|
289
|
+
const fail = enforceStrictBinding && (requireHumanMerge || enforceWhenApprovalPresent);
|
|
290
|
+
return {
|
|
291
|
+
status: "missing_binding",
|
|
292
|
+
allowed: !fail,
|
|
293
|
+
approved_head_sha: null,
|
|
294
|
+
current_head_sha: currentHeadSha,
|
|
295
|
+
pr_number: prNumber,
|
|
296
|
+
require_human_merge: requireHumanMerge,
|
|
297
|
+
auto_merge_disabled: null,
|
|
298
|
+
message: `❌ merge-approval-head (#3235): plan:approved for PR #${prNumber} has no head_sha. ` +
|
|
299
|
+
"Approval must bind to an exact PR head under requireHumanMerge / auto-merge.",
|
|
300
|
+
recovery: fail ? recovery : null,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
if (currentHeadSha === null) {
|
|
304
|
+
const recovery = buildMergeApprovalRecovery({
|
|
305
|
+
prNumber,
|
|
306
|
+
approvedHeadSha: approval.head_sha,
|
|
307
|
+
currentHeadSha: null,
|
|
308
|
+
status: "stale",
|
|
309
|
+
autoMergeDisabled: null,
|
|
310
|
+
});
|
|
311
|
+
return {
|
|
312
|
+
status: "stale",
|
|
313
|
+
allowed: false,
|
|
314
|
+
approved_head_sha: approval.head_sha,
|
|
315
|
+
current_head_sha: null,
|
|
316
|
+
pr_number: prNumber,
|
|
317
|
+
require_human_merge: requireHumanMerge,
|
|
318
|
+
auto_merge_disabled: null,
|
|
319
|
+
message: `❌ merge-approval-head (#3235): cannot verify approval for PR #${prNumber} ` +
|
|
320
|
+
`(approved_head_sha=${approval.head_sha}) — current HEAD unknown; fail closed.`,
|
|
321
|
+
recovery,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
if (headShaMatches(approval.head_sha, currentHeadSha)) {
|
|
325
|
+
return {
|
|
326
|
+
status: "ok",
|
|
327
|
+
allowed: true,
|
|
328
|
+
approved_head_sha: approval.head_sha,
|
|
329
|
+
current_head_sha: currentHeadSha,
|
|
330
|
+
pr_number: prNumber,
|
|
331
|
+
require_human_merge: requireHumanMerge,
|
|
332
|
+
auto_merge_disabled: null,
|
|
333
|
+
message: `✓ merge-approval-head: plan:approved for PR #${prNumber} bound to ` +
|
|
334
|
+
`current head ${currentHeadSha.slice(0, 12)}`,
|
|
335
|
+
recovery: null,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
const recovery = buildMergeApprovalRecovery({
|
|
339
|
+
prNumber,
|
|
340
|
+
approvedHeadSha: approval.head_sha,
|
|
341
|
+
currentHeadSha,
|
|
342
|
+
status: "stale",
|
|
343
|
+
autoMergeDisabled: null,
|
|
344
|
+
});
|
|
345
|
+
return {
|
|
346
|
+
status: "stale",
|
|
347
|
+
allowed: false,
|
|
348
|
+
approved_head_sha: approval.head_sha,
|
|
349
|
+
current_head_sha: currentHeadSha,
|
|
350
|
+
pr_number: prNumber,
|
|
351
|
+
require_human_merge: requireHumanMerge,
|
|
352
|
+
auto_merge_disabled: null,
|
|
353
|
+
message: `❌ merge-approval-head (#3235): stale approval for PR #${prNumber}. ` +
|
|
354
|
+
`approved_head_sha=${approval.head_sha} != current_head_sha=${currentHeadSha}. ` +
|
|
355
|
+
"Human merge approval authorizes one immutable head; HEAD changed after approval.",
|
|
356
|
+
recovery,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
/** Live `gh pr merge <N> --disable-auto` (best-effort). */
|
|
360
|
+
export function disablePullRequestAutoMerge(prNumber, repo, options = {}) {
|
|
361
|
+
let binary;
|
|
362
|
+
try {
|
|
363
|
+
binary = resolveBinary();
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return { ok: false, stderr: "gh CLI not found" };
|
|
367
|
+
}
|
|
368
|
+
const args = ["pr", "merge", String(prNumber), "--disable-auto"];
|
|
369
|
+
if (repo !== null && repo.length > 0) {
|
|
370
|
+
args.push("--repo", repo);
|
|
371
|
+
}
|
|
372
|
+
const result = captureExec(binary, args, (options.timeoutSec ?? 60) * 1000);
|
|
373
|
+
// gh returns 0 when disabled, and may return non-zero if auto-merge was not enabled
|
|
374
|
+
// ("auto-merge is not enabled") — treat that as ok (already off).
|
|
375
|
+
const stderr = result.stderr.trim();
|
|
376
|
+
if (result.returncode === 0) {
|
|
377
|
+
return { ok: true, stderr };
|
|
378
|
+
}
|
|
379
|
+
const low = stderr.toLowerCase();
|
|
380
|
+
if (low.includes("not enabled") ||
|
|
381
|
+
low.includes("auto-merge is disabled") ||
|
|
382
|
+
low.includes("is not set to auto-merge")) {
|
|
383
|
+
return { ok: true, stderr };
|
|
384
|
+
}
|
|
385
|
+
return { ok: false, stderr };
|
|
386
|
+
}
|
|
387
|
+
/** REST HEAD read via `gh api repos/.../pulls/N --jq .head.sha`. */
|
|
388
|
+
export function fetchPrHeadShaRest(prNumber, repo, options = {}) {
|
|
389
|
+
if (repo === null || repo.length === 0)
|
|
390
|
+
return null;
|
|
391
|
+
let binary;
|
|
392
|
+
try {
|
|
393
|
+
binary = resolveBinary();
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
const endpoint = `repos/${repo}/pulls/${prNumber}`;
|
|
399
|
+
const result = captureExec(binary, ["api", endpoint, "--jq", ".head.sha"], (options.timeoutSec ?? 60) * 1000);
|
|
400
|
+
if (result.returncode !== 0)
|
|
401
|
+
return null;
|
|
402
|
+
return normalizeSha(result.stdout.trim());
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Evaluate + on deny attempt to disable GitHub auto-merge and attach recovery.
|
|
406
|
+
*/
|
|
407
|
+
export function enforceMergeApprovalHead(input) {
|
|
408
|
+
let currentHeadSha = input.currentHeadSha;
|
|
409
|
+
if ((currentHeadSha === null || currentHeadSha === undefined || currentHeadSha.trim() === "") &&
|
|
410
|
+
input.fetchHeadShaFn !== undefined) {
|
|
411
|
+
currentHeadSha = input.fetchHeadShaFn(input.prNumber, input.repo ?? null);
|
|
412
|
+
}
|
|
413
|
+
const evaluated = evaluateMergeApprovalHead({
|
|
414
|
+
...input,
|
|
415
|
+
currentHeadSha: currentHeadSha ?? null,
|
|
416
|
+
});
|
|
417
|
+
if (evaluated.allowed) {
|
|
418
|
+
return evaluated;
|
|
419
|
+
}
|
|
420
|
+
const shouldDisable = input.disableAutoMergeOnDeny !== false;
|
|
421
|
+
let autoMergeDisabled = null;
|
|
422
|
+
if (shouldDisable) {
|
|
423
|
+
const disableFn = input.disableAutoMergeFn ?? disablePullRequestAutoMerge;
|
|
424
|
+
const disabled = disableFn(input.prNumber, input.repo ?? null);
|
|
425
|
+
autoMergeDisabled = disabled.ok;
|
|
426
|
+
}
|
|
427
|
+
const recovery = buildMergeApprovalRecovery({
|
|
428
|
+
prNumber: input.prNumber,
|
|
429
|
+
approvedHeadSha: evaluated.approved_head_sha,
|
|
430
|
+
currentHeadSha: evaluated.current_head_sha,
|
|
431
|
+
status: evaluated.status,
|
|
432
|
+
autoMergeDisabled,
|
|
433
|
+
});
|
|
434
|
+
return {
|
|
435
|
+
...evaluated,
|
|
436
|
+
auto_merge_disabled: autoMergeDisabled,
|
|
437
|
+
message: evaluated.message,
|
|
438
|
+
recovery,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
/** Strip trailing path separators without regex (CodeQL-safe). */
|
|
442
|
+
function trimTrailingSeparators(path) {
|
|
443
|
+
let end = path.length;
|
|
444
|
+
while (end > 0) {
|
|
445
|
+
const ch = path.charCodeAt(end - 1);
|
|
446
|
+
if (ch === 47 || ch === 92) {
|
|
447
|
+
// / or \
|
|
448
|
+
end -= 1;
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
return path.slice(0, end);
|
|
454
|
+
}
|
|
455
|
+
/** Default event log path relative to project root. */
|
|
456
|
+
export function defaultMergeApprovalEventLog(projectRoot) {
|
|
457
|
+
const root = trimTrailingSeparators(projectRoot);
|
|
458
|
+
const log = DEFAULT_EVENT_LOG.split("\\").join("/");
|
|
459
|
+
return `${root}/${log}`;
|
|
460
|
+
}
|
|
461
|
+
//# sourceMappingURL=merge-approval-head.js.map
|
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
* (3) branch-protection / setup docs require ≥1 human reviewer
|
|
9
9
|
*
|
|
10
10
|
* Override: `task policy:allow-bot-merge -- --confirm` + `DEFT_ALLOW_BOT_MERGE=1`.
|
|
11
|
+
*
|
|
12
|
+
* Head-bound Phase 5→6 approval (#3235): when a `plan:approved` event exists,
|
|
13
|
+
* merge paths also enforce `approved_head_sha == current_head_sha` via
|
|
14
|
+
* `merge-approval-head.ts` (stale head → fail closed + disable auto-merge).
|
|
15
|
+
* That gate is complementary to this policy surface and does not replace it.
|
|
11
16
|
*/
|
|
12
17
|
export declare const FIELD_REQUIRE_HUMAN_MERGE = "plan.policy.requireHumanMerge";
|
|
13
18
|
export declare const FIELD_REQUIRE_HUMAN_MERGE_CLI_ALIAS = "requireHumanMerge";
|
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
* (3) branch-protection / setup docs require ≥1 human reviewer
|
|
9
9
|
*
|
|
10
10
|
* Override: `task policy:allow-bot-merge -- --confirm` + `DEFT_ALLOW_BOT_MERGE=1`.
|
|
11
|
+
*
|
|
12
|
+
* Head-bound Phase 5→6 approval (#3235): when a `plan:approved` event exists,
|
|
13
|
+
* merge paths also enforce `approved_head_sha == current_head_sha` via
|
|
14
|
+
* `merge-approval-head.ts` (stale head → fail closed + disable auto-merge).
|
|
15
|
+
* That gate is complementary to this policy surface and does not replace it.
|
|
11
16
|
*/
|
|
12
17
|
import { existsSync, readFileSync } from "node:fs";
|
|
13
18
|
import { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type CheckRunRecord, type RequiredStatusContext } from "./gh.js";
|
|
2
2
|
/**
|
|
3
3
|
* Bot-reviewer check-runs are not CI workflow jobs. Presence of only these
|
|
4
4
|
* (or empty check-runs) is the `ci_never_scheduled` weather signal (#3167).
|
|
@@ -15,7 +15,9 @@ export declare function suiteFamilyOf(name: string): "typescript" | "go" | null;
|
|
|
15
15
|
* not authoritative: a green primary must not clear a cancelled aggregator.
|
|
16
16
|
*/
|
|
17
17
|
export declare function isAuthoritativeSuiteAggregator(name: string): boolean;
|
|
18
|
-
export type CiReadyState = "ready" | "blocked" | "ci_failures" | "not_ready_yet" | "runner_capacity_stall" | "ci_never_scheduled" | "ci_cancelled_no_failover"
|
|
18
|
+
export type CiReadyState = "ready" | "blocked" | "ci_failures" | "not_ready_yet" | "runner_capacity_stall" | "ci_never_scheduled" | "ci_cancelled_no_failover"
|
|
19
|
+
/** Ruleset/BP required context with no check-run on HEAD (#3234). */
|
|
20
|
+
| "ci_absent_required" | "skipped";
|
|
19
21
|
export interface CiGateOptions {
|
|
20
22
|
readonly skipCi?: boolean;
|
|
21
23
|
readonly ignoreCheckNames?: readonly string[];
|
|
@@ -23,6 +25,13 @@ export interface CiGateOptions {
|
|
|
23
25
|
readonly capacityStallBudgetMs?: number;
|
|
24
26
|
/** Injectable clock for capacity-stall tests. */
|
|
25
27
|
readonly nowMs?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Required status-check contexts from rulesets / branch protection (#3234).
|
|
30
|
+
* Strings or `{ name, appId? }` specs. When set, a context with no matching
|
|
31
|
+
* exact-HEAD check-run (name + optional app id) is fail-closed (path-filtered
|
|
32
|
+
* workflows never schedule). Empty/omitted preserves inventory-only.
|
|
33
|
+
*/
|
|
34
|
+
readonly requiredContexts?: readonly (string | RequiredStatusContext)[];
|
|
26
35
|
}
|
|
27
36
|
export interface CiCheckConclusion {
|
|
28
37
|
readonly name: string;
|
|
@@ -41,6 +50,13 @@ export interface CiGateSummary {
|
|
|
41
50
|
readonly capacity_stalled_required: readonly string[];
|
|
42
51
|
/** Required checks cancelled without a green failover sibling (#3167). */
|
|
43
52
|
readonly cancelled_required: readonly string[];
|
|
53
|
+
/**
|
|
54
|
+
* Ruleset/BP required contexts with no matching check-run on HEAD (#3234).
|
|
55
|
+
* Distinct from pending (never scheduled vs still running).
|
|
56
|
+
*/
|
|
57
|
+
readonly absent_required: readonly string[];
|
|
58
|
+
/** Full required-context list used for the comparison (may be empty). */
|
|
59
|
+
readonly required_contexts: readonly string[];
|
|
44
60
|
readonly conclusions: readonly CiCheckConclusion[];
|
|
45
61
|
}
|
|
46
62
|
export interface CiGateResult {
|
|
@@ -49,13 +65,14 @@ export interface CiGateResult {
|
|
|
49
65
|
}
|
|
50
66
|
export declare function buildCiSummaryLine(summary: CiGateSummary): string;
|
|
51
67
|
/**
|
|
52
|
-
* Classify required CI weather for a HEAD check-run list (#2169 / #2672 / #3167).
|
|
68
|
+
* Classify required CI weather for a HEAD check-run list (#2169 / #2672 / #3167 / #3234).
|
|
53
69
|
*
|
|
54
70
|
* Distinct ready_state codes:
|
|
55
71
|
* - `ci_never_scheduled` — no CI workflow check-runs (bots alone / empty)
|
|
56
72
|
* - `runner_capacity_stall` — queued past budget, no runner claimed
|
|
57
73
|
* - `ci_failures` — completed failure / timed_out with product evidence
|
|
58
74
|
* - `ci_cancelled_no_failover` — only cancelled failures; no green failover
|
|
75
|
+
* - `ci_absent_required` — ruleset/BP required context missing on HEAD (#3234)
|
|
59
76
|
* - `not_ready_yet` / `ready` / `blocked` / `skipped` — legacy + residual
|
|
60
77
|
*/
|
|
61
78
|
export declare function evaluateCiGate(checkRuns: readonly CheckRunRecord[], options?: CiGateOptions): CiGateResult;
|