@reneturcios/greenlight 0.3.4 → 0.3.6
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 +29 -1
- package/dist/ci-triage.d.ts +85 -0
- package/dist/ci-triage.js +335 -0
- package/dist/ci-triage.js.map +1 -0
- package/dist/cli.js +72 -0
- package/dist/cli.js.map +1 -1
- package/dist/github.d.ts +2 -0
- package/dist/github.js +7 -0
- package/dist/github.js.map +1 -1
- package/dist/index.js +25 -1
- package/dist/index.js.map +1 -1
- package/dist/review-triage.d.ts +129 -0
- package/dist/review-triage.js +363 -0
- package/dist/review-triage.js.map +1 -0
- package/package.json +2 -2
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -85,8 +85,24 @@ npx @reneturcios/greenlight scan PrefectHQ/fastmcp
|
|
|
85
85
|
# Friction score for a repo
|
|
86
86
|
npx @reneturcios/greenlight friction triggerdotdev/trigger.dev
|
|
87
87
|
|
|
88
|
+
# CI triage for a red PR: is each failing check yours or pre-existing?
|
|
89
|
+
npx @reneturcios/greenlight ci https://github.com/reticlehq/reticle/pull/1620
|
|
90
|
+
|
|
91
|
+
# [pre-existing] install-gate (windows-latest)
|
|
92
|
+
# same check failing on base: 2 of 3 recent base runs, last at 2026-10-14T10:00:00Z
|
|
93
|
+
|
|
94
|
+
# [external-blocker] Vercel - Authorization required
|
|
95
|
+
# check name matches the Vercel deploy/auth pattern; this failure is infrastructure, not the PR
|
|
96
|
+
|
|
88
97
|
# JSON output for scripting
|
|
89
|
-
npx @reneturcios/greenlight
|
|
98
|
+
npx @reneturcios/greenlight ci <pr-url> --json
|
|
99
|
+
|
|
100
|
+
# Review triage for a PR with review feedback: which comments need you?
|
|
101
|
+
npx @reneturcios/greenlight review https://github.com/n8n-io/n8n/pull/41235
|
|
102
|
+
|
|
103
|
+
# [likely-real] interpolation drops `__proto__` (packages/bruno-cli/src/runner/interpolate-vars.js:42)
|
|
104
|
+
# why: anchor matches: the comment's quoted token Object.assign({}, params) is present in the anchored hunk
|
|
105
|
+
# draft reply: Good catch. Fixed in <next commit>: ...
|
|
90
106
|
```
|
|
91
107
|
|
|
92
108
|
Real examples from our vetting:
|
|
@@ -146,9 +162,19 @@ Tools:
|
|
|
146
162
|
* `claim_issue` - verdict-gated claim comment (dry run unless `execute: true`)
|
|
147
163
|
* `prep_issue` - fork and branch check plus a local PREP.md checklist with the repo's learned conventions (dry run unless `execute: true`)
|
|
148
164
|
* `open_pr` - DCO-aware PR opening from a fork branch (dry run unless `execute: true`)
|
|
165
|
+
* `ci_triage` - classify each failing check on a PR as pre-existing, likely-yours, indeterminate (with a stated lean), or external-blocker, by comparing against the base branch's recent check history
|
|
166
|
+
* `review_triage` - triage the review feedback on your PR: each finding classified likely-real / likely-noise / needs-decision with its reason and a reply draft (humans are never called noise; drafts only, nothing posted)
|
|
149
167
|
|
|
150
168
|
Run it locally: `npm run dev` (MCP entry is `src/index.ts`; CLI is `src/cli.ts`).
|
|
151
169
|
|
|
170
|
+
### v0.3.5: CI triage (solve-first)
|
|
171
|
+
|
|
172
|
+
The first solve-side command. On a red PR, `ci` fetches the PR's base and head, collects the failing check runs (with the first failure lines from annotations when it can), and builds the same checks' history from the five most recent base-branch commits. Each failing check gets one line with its class and the evidence: `pre-existing` (same check failing on base, with counts and the last failure time), `likely-yours` (failing on head, passing on base, and the PR touches files that relate to the check), `indeterminate` (no base signal to compare), or `external-blocker` (Vercel/Netlify/deploy-preview auth failures, infrastructure not the PR). Indeterminate checks always state their lean: not touching the check's area leans pre-existing, touching it leans yours, with the reason spelled out. A verdict line closes it out: ALL PRE-EXISTING, HAS LIKELY-YOURS, INDETERMINATE, or NO FAILURES.
|
|
173
|
+
|
|
174
|
+
### v0.3.6: review triage (solve-first)
|
|
175
|
+
|
|
176
|
+
The second solve-side command. On a PR with review feedback, `review` gathers every review thread (bot and human), classifies each finding, and drafts the reply for you. Findings group into three buckets: **likely-real** (the comment's quoted anchor token is in the diff hunk; fix these), **needs-decision** (bot questions and every substantive human comment, which is never called noise; your call), and **likely-noise** (findings on generated files, style nits contradicted by the surrounding code, anchor mismatches, security claims the hunk disproves; probably push back). Each finding prints with its author, file and line, the one-line summary, the deterministic reason for its class, and the draft reply. Summaries, walkthroughs, praise, and findings on dismissed reviews count as noise or resolved, never as findings. Like everything in Greenlight, the drafts are output only: nothing is posted.
|
|
177
|
+
|
|
152
178
|
## Automation
|
|
153
179
|
|
|
154
180
|
Greenlight 0.2.0 automates the contribution pipeline its author ran by hand
|
|
@@ -208,6 +234,8 @@ npm run build
|
|
|
208
234
|
|
|
209
235
|
## Roadmap (Later)
|
|
210
236
|
|
|
237
|
+
* Review-response triage: gather every bot and human review thread on a PR, classify each finding (real, noise, needs a decision) with the code context, draft the reply or fix list (solve-first, next after CI triage)
|
|
238
|
+
* Claim-to-PR prep scaffolding: emit the per-repo work order (house rules, CLA method and link, changeset conventions, fork/branch names) from one `prep` pass
|
|
211
239
|
* Browser extension that scores issues inline on github.com
|
|
212
240
|
* Hosted dashboard of the friction index, refreshed on a schedule
|
|
213
241
|
* Maintainer GitHub Action that auto-labels claim comments
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export type CiCheckClass = "pre-existing" | "likely-yours" | "indeterminate" | "external-blocker";
|
|
2
|
+
export type CiTriageVerdict = "ALL PRE-EXISTING" | "HAS LIKELY-YOURS" | "INDETERMINATE" | "NO FAILURES";
|
|
3
|
+
/** A failing check run on the PR head. */
|
|
4
|
+
export interface HeadCheck {
|
|
5
|
+
name: string;
|
|
6
|
+
conclusion: string;
|
|
7
|
+
detailsUrl?: string;
|
|
8
|
+
/** First meaningful failure lines from check-run annotations, best-effort. */
|
|
9
|
+
failureLines?: string[];
|
|
10
|
+
}
|
|
11
|
+
/** One check-run result from the base branch's recent window. */
|
|
12
|
+
export interface BaseCheckRecord {
|
|
13
|
+
name: string;
|
|
14
|
+
conclusion: string;
|
|
15
|
+
completedAt: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ClassifiedCheck {
|
|
18
|
+
name: string;
|
|
19
|
+
conclusion: string;
|
|
20
|
+
class: CiCheckClass;
|
|
21
|
+
/** Human-readable evidence: base status, times, or the linked file. */
|
|
22
|
+
evidence: string;
|
|
23
|
+
/** On indeterminate only: which way it leans and why is in evidence. */
|
|
24
|
+
lean?: "pre-existing" | "yours";
|
|
25
|
+
failureLines?: string[];
|
|
26
|
+
detailsUrl?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface CiTriageInput {
|
|
29
|
+
prUrl: string;
|
|
30
|
+
prTitle: string;
|
|
31
|
+
baseRef: string;
|
|
32
|
+
baseSha: string;
|
|
33
|
+
headSha: string;
|
|
34
|
+
headChecks: HeadCheck[];
|
|
35
|
+
baseRecords: BaseCheckRecord[];
|
|
36
|
+
changedFiles: string[];
|
|
37
|
+
}
|
|
38
|
+
export interface CiTriageResult {
|
|
39
|
+
pr: string;
|
|
40
|
+
title: string;
|
|
41
|
+
baseRef: string;
|
|
42
|
+
baseSha: string;
|
|
43
|
+
headSha: string;
|
|
44
|
+
checks: ClassifiedCheck[];
|
|
45
|
+
verdict: CiTriageVerdict;
|
|
46
|
+
}
|
|
47
|
+
/** Matrix suffixes ("name (windows-latest)") are family, not identity. */
|
|
48
|
+
export declare function stripMatrix(name: string): string;
|
|
49
|
+
/** Case and whitespace are noise; the matrix suffix is kept so exact
|
|
50
|
+
* variants match first and families match second. */
|
|
51
|
+
export declare function normalizeCheckName(name: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* Files the PR touched that plausibly relate to a check, by path-token or
|
|
54
|
+
* semantic-token match. Explainable by design: the caller quotes the
|
|
55
|
+
* returned paths as the link evidence.
|
|
56
|
+
*/
|
|
57
|
+
export declare function checkTouchesFiles(checkName: string, files: string[]): string[];
|
|
58
|
+
/**
|
|
59
|
+
* Classify one failing head check against base records and changed files.
|
|
60
|
+
* now is injected so tests pin ages.
|
|
61
|
+
*/
|
|
62
|
+
export declare function classifyCheck(head: HeadCheck, baseRecords: BaseCheckRecord[], changedFiles: string[]): ClassifiedCheck;
|
|
63
|
+
/** Roll up per-check classes into the one-line verdict. */
|
|
64
|
+
export declare function triageVerdict(checks: ClassifiedCheck[]): CiTriageVerdict;
|
|
65
|
+
/** The pure engine: classify every failing head check, then roll up. */
|
|
66
|
+
export declare function classifyCiTriage(input: CiTriageInput): CiTriageResult;
|
|
67
|
+
export interface PullDetail {
|
|
68
|
+
title: string;
|
|
69
|
+
baseRef: string;
|
|
70
|
+
baseSha: string;
|
|
71
|
+
headSha: string;
|
|
72
|
+
changedFiles: string[];
|
|
73
|
+
}
|
|
74
|
+
/** Test seam: every network call behind a named function. */
|
|
75
|
+
export interface CiTriageDeps {
|
|
76
|
+
fetchPull?: (owner: string, repo: string, number: number) => Promise<PullDetail>;
|
|
77
|
+
fetchHeadChecks?: (owner: string, repo: string, headSha: string) => Promise<HeadCheck[]>;
|
|
78
|
+
fetchBaseRecords?: (owner: string, repo: string, baseRef: string) => Promise<BaseCheckRecord[]>;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Full triage for a PR URL. Best-effort: if the base window cannot be
|
|
82
|
+
* read, every check is classified without base data (indeterminate with a
|
|
83
|
+
* lean) rather than erroring.
|
|
84
|
+
*/
|
|
85
|
+
export declare function triagePr(prUrl: string, deps?: CiTriageDeps): Promise<CiTriageResult>;
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CI failure triage. Answers the question every contributor asks on a red
|
|
3
|
+
* PR: "is this failure mine or pre-existing?"
|
|
4
|
+
*
|
|
5
|
+
* The classifier is a pure function over injected data (no network), so
|
|
6
|
+
* the rules are unit-testable and pinned to the campaign's real cases:
|
|
7
|
+
*
|
|
8
|
+
* - reticle #1620: windows install-gate failing identically on base
|
|
9
|
+
* (leftover-process flake) -> pre-existing
|
|
10
|
+
* - reticle #1633: fixture-based test failing in an untouched package,
|
|
11
|
+
* same check intermittent on base -> pre-existing
|
|
12
|
+
* - a unit-test check failing only on head in a package the PR touched
|
|
13
|
+
* -> likely-yours
|
|
14
|
+
* - Vercel "Authorization required" -> external-blocker (not the PR)
|
|
15
|
+
*
|
|
16
|
+
* Rule order per failing head check:
|
|
17
|
+
* 1. external-blocker: the check name matches a known deploy/auth
|
|
18
|
+
* pattern (Vercel, Netlify, deploy previews). Evidence quotes the match.
|
|
19
|
+
* 2. pre-existing: the same check name failed in the recent base
|
|
20
|
+
* window (exact variant first, then the matrix family, e.g. a windows
|
|
21
|
+
* install-gate flake with no exact-variant history). Evidence quotes
|
|
22
|
+
* base failure counts and the last failure time.
|
|
23
|
+
* 3. likely-yours: failing on head, passing (or absent but with base
|
|
24
|
+
* history present) on base, AND the PR touches files plausibly
|
|
25
|
+
* related to the check (path token or semantic token match).
|
|
26
|
+
* 4. indeterminate: failing on head with no base signal to compare
|
|
27
|
+
* against. Carries a lean ("pre-existing" when the PR does not touch
|
|
28
|
+
* the check's area, "yours" when it does) and says why.
|
|
29
|
+
*
|
|
30
|
+
* The live fetch layer is best-effort: base history and annotations each
|
|
31
|
+
* degrade to "no evidence" on failure rather than guessing.
|
|
32
|
+
*/
|
|
33
|
+
import { apiGet, parsePullUrl } from "./github.js";
|
|
34
|
+
/** Conclusions that count as "this check failed". "stale" (superseded by a newer push) is not a failure. */
|
|
35
|
+
const FAILING = new Set(["failure", "timed_out", "action_required"]);
|
|
36
|
+
/** Check-name patterns that mean the failure is infrastructure, not the PR. */
|
|
37
|
+
const EXTERNAL_PATTERNS = [
|
|
38
|
+
{ pattern: /vercel/i, label: "Vercel" },
|
|
39
|
+
{ pattern: /netlify/i, label: "Netlify" },
|
|
40
|
+
{ pattern: /authorization required/i, label: "authorization required" },
|
|
41
|
+
{ pattern: /deploy(ment)?\s*(preview|check)?/i, label: "deploy" },
|
|
42
|
+
{ pattern: /preview/i, label: "preview" },
|
|
43
|
+
{ pattern: /cloudflare pages/i, label: "Cloudflare Pages" },
|
|
44
|
+
];
|
|
45
|
+
/** Matrix suffixes ("name (windows-latest)") are family, not identity. */
|
|
46
|
+
export function stripMatrix(name) {
|
|
47
|
+
return name.replace(/\s*\([^)]*\)\s*/g, " ").replace(/\s+/g, " ").trim();
|
|
48
|
+
}
|
|
49
|
+
/** Case and whitespace are noise; the matrix suffix is kept so exact
|
|
50
|
+
* variants match first and families match second. */
|
|
51
|
+
export function normalizeCheckName(name) {
|
|
52
|
+
return name.toLowerCase().replace(/\s+/g, " ").trim();
|
|
53
|
+
}
|
|
54
|
+
/** Path segments that describe CI environments, not code areas. Ignored for file linking. */
|
|
55
|
+
const ENV_TOKENS = new Set([
|
|
56
|
+
"windows", "macos", "ubuntu", "linux", "node", "nodejs", "python", "npm", "pnpm", "yarn",
|
|
57
|
+
"run", "ci", "on", "the", "with", "and", "or", "e2e",
|
|
58
|
+
]);
|
|
59
|
+
/** Check-name tokens that semantically relate to file shapes. */
|
|
60
|
+
const SEMANTIC_TOKENS = [
|
|
61
|
+
{ tokens: new Set(["lint", "eslint", "prettier", "format"]), fileTest: /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|rb)$/i },
|
|
62
|
+
{ tokens: new Set(["test", "tests", "unit", "spec", "jest", "vitest", "mocha", "pytest", "coverage"]), fileTest: /(^|\/)(test|tests|__tests__|spec)\b|\.test\.|\.spec\./i },
|
|
63
|
+
{ tokens: new Set(["typecheck", "tsc", "mypy", "pyright"]), fileTest: /\.(ts|tsx|py)$/i },
|
|
64
|
+
{ tokens: new Set(["build", "compile"]), fileTest: /\.(ts|tsx|js|jsx|py|go|rs)$/i },
|
|
65
|
+
];
|
|
66
|
+
const MAX_LINKED_EVIDENCE = 3;
|
|
67
|
+
/**
|
|
68
|
+
* Files the PR touched that plausibly relate to a check, by path-token or
|
|
69
|
+
* semantic-token match. Explainable by design: the caller quotes the
|
|
70
|
+
* returned paths as the link evidence.
|
|
71
|
+
*/
|
|
72
|
+
export function checkTouchesFiles(checkName, files) {
|
|
73
|
+
const tokens = checkName
|
|
74
|
+
.toLowerCase()
|
|
75
|
+
.split(/[^a-z0-9]+/)
|
|
76
|
+
.filter((t) => t.length > 2 && !ENV_TOKENS.has(t));
|
|
77
|
+
const linked = new Set();
|
|
78
|
+
for (const file of files) {
|
|
79
|
+
const fl = file.toLowerCase();
|
|
80
|
+
const segs = fl.split("/");
|
|
81
|
+
const stem = (segs.pop() ?? "").replace(/\.[a-z0-9]+$/, "");
|
|
82
|
+
for (const t of tokens) {
|
|
83
|
+
if (stem.includes(t)) {
|
|
84
|
+
linked.add(file);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
if (segs.some((s) => s === t || (t.length > 4 && s.includes(t)))) {
|
|
88
|
+
linked.add(file);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
if (SEMANTIC_TOKENS.some((g) => g.tokens.has(t) && g.fileTest.test(fl))) {
|
|
92
|
+
linked.add(file);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return [...linked].slice(0, MAX_LINKED_EVIDENCE);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Classify one failing head check against base records and changed files.
|
|
101
|
+
* now is injected so tests pin ages.
|
|
102
|
+
*/
|
|
103
|
+
export function classifyCheck(head, baseRecords, changedFiles) {
|
|
104
|
+
const name = head.name;
|
|
105
|
+
for (const { pattern, label } of EXTERNAL_PATTERNS) {
|
|
106
|
+
if (pattern.test(name)) {
|
|
107
|
+
return {
|
|
108
|
+
name,
|
|
109
|
+
conclusion: head.conclusion,
|
|
110
|
+
class: "external-blocker",
|
|
111
|
+
evidence: `check name matches the ${label} deploy/auth pattern; this failure is infrastructure, not the PR`,
|
|
112
|
+
detailsUrl: head.detailsUrl,
|
|
113
|
+
failureLines: head.failureLines,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const wanted = normalizeCheckName(name);
|
|
118
|
+
const family = stripMatrix(wanted);
|
|
119
|
+
const baseForCheck = baseRecords.filter((r) => normalizeCheckName(r.name) === wanted);
|
|
120
|
+
const baseFamilyFailures = baseRecords.filter((r) => FAILING.has(r.conclusion) && stripMatrix(normalizeCheckName(r.name)) === family);
|
|
121
|
+
const baseFailures = baseForCheck.filter((r) => FAILING.has(r.conclusion));
|
|
122
|
+
if (baseFailures.length > 0) {
|
|
123
|
+
baseFailures.sort((a, b) => (a.completedAt < b.completedAt ? 1 : -1));
|
|
124
|
+
const total = baseForCheck.length;
|
|
125
|
+
const lastAt = baseFailures[0].completedAt ? `, last at ${baseFailures[0].completedAt}` : "";
|
|
126
|
+
return {
|
|
127
|
+
name,
|
|
128
|
+
conclusion: head.conclusion,
|
|
129
|
+
class: "pre-existing",
|
|
130
|
+
evidence: `same check failing on base: ${baseFailures.length} of ${total} recent base run${total === 1 ? "" : "s"}${lastAt}`,
|
|
131
|
+
detailsUrl: head.detailsUrl,
|
|
132
|
+
failureLines: head.failureLines,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (baseFamilyFailures.length > 0) {
|
|
136
|
+
baseFamilyFailures.sort((a, b) => (a.completedAt < b.completedAt ? 1 : -1));
|
|
137
|
+
const familyRuns = baseRecords.filter((r) => stripMatrix(normalizeCheckName(r.name)) === family).length;
|
|
138
|
+
const lastAt = baseFamilyFailures[0].completedAt ? `, last at ${baseFamilyFailures[0].completedAt}` : "";
|
|
139
|
+
return {
|
|
140
|
+
name,
|
|
141
|
+
conclusion: head.conclusion,
|
|
142
|
+
class: "pre-existing",
|
|
143
|
+
evidence: `check family "${family}" failing on base: ${baseFamilyFailures.length} of ${familyRuns} family runs${lastAt}`,
|
|
144
|
+
detailsUrl: head.detailsUrl,
|
|
145
|
+
failureLines: head.failureLines,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const linked = checkTouchesFiles(name, changedFiles);
|
|
149
|
+
if (baseForCheck.length > 0 && linked.length > 0) {
|
|
150
|
+
return {
|
|
151
|
+
name,
|
|
152
|
+
conclusion: head.conclusion,
|
|
153
|
+
class: "likely-yours",
|
|
154
|
+
evidence: `passing on base but failing on head, and the PR touches ${linked.join(", ")} which relates to this check`,
|
|
155
|
+
detailsUrl: head.detailsUrl,
|
|
156
|
+
failureLines: head.failureLines,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (linked.length > 0) {
|
|
160
|
+
return {
|
|
161
|
+
name,
|
|
162
|
+
conclusion: head.conclusion,
|
|
163
|
+
class: "indeterminate",
|
|
164
|
+
lean: "yours",
|
|
165
|
+
evidence: `no base history for this check to compare against; the PR touches ${linked.join(", ")} which relates to it, so it leans yours`,
|
|
166
|
+
detailsUrl: head.detailsUrl,
|
|
167
|
+
failureLines: head.failureLines,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
name,
|
|
172
|
+
conclusion: head.conclusion,
|
|
173
|
+
class: "indeterminate",
|
|
174
|
+
lean: "pre-existing",
|
|
175
|
+
evidence: `no base history for this check to compare against; none of the PR's files relate to it, so it leans pre-existing (an unrelated flake is more likely than a new failure caused by this PR)`,
|
|
176
|
+
detailsUrl: head.detailsUrl,
|
|
177
|
+
failureLines: head.failureLines,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/** Roll up per-check classes into the one-line verdict. */
|
|
181
|
+
export function triageVerdict(checks) {
|
|
182
|
+
if (checks.length === 0)
|
|
183
|
+
return "NO FAILURES";
|
|
184
|
+
if (checks.some((c) => c.class === "likely-yours"))
|
|
185
|
+
return "HAS LIKELY-YOURS";
|
|
186
|
+
if (checks.some((c) => c.class === "indeterminate"))
|
|
187
|
+
return "INDETERMINATE";
|
|
188
|
+
return "ALL PRE-EXISTING";
|
|
189
|
+
}
|
|
190
|
+
/** The pure engine: classify every failing head check, then roll up. */
|
|
191
|
+
export function classifyCiTriage(input) {
|
|
192
|
+
const checks = input.headChecks.map((h) => classifyCheck(h, input.baseRecords, input.changedFiles));
|
|
193
|
+
return {
|
|
194
|
+
pr: input.prUrl,
|
|
195
|
+
title: input.prTitle,
|
|
196
|
+
baseRef: input.baseRef,
|
|
197
|
+
baseSha: input.baseSha,
|
|
198
|
+
headSha: input.headSha,
|
|
199
|
+
checks,
|
|
200
|
+
verdict: triageVerdict(checks),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
async function fetchPullFiles(owner, repo, number) {
|
|
204
|
+
const files = [];
|
|
205
|
+
for (let page = 1; page <= 5; page++) {
|
|
206
|
+
const data = (await apiGet(`/repos/${owner}/${repo}/pulls/${number}/files?per_page=100&page=${page}`));
|
|
207
|
+
for (const f of data ?? []) {
|
|
208
|
+
if (typeof f.filename === "string")
|
|
209
|
+
files.push(f.filename);
|
|
210
|
+
}
|
|
211
|
+
if ((data ?? []).length < 100)
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
return files;
|
|
215
|
+
}
|
|
216
|
+
async function defaultFetchPull(owner, repo, number) {
|
|
217
|
+
const p = (await apiGet(`/repos/${owner}/${repo}/pulls/${number}`));
|
|
218
|
+
return {
|
|
219
|
+
title: String(p.title ?? ""),
|
|
220
|
+
baseRef: String(p.base?.ref ?? ""),
|
|
221
|
+
baseSha: String(p.base?.sha ?? ""),
|
|
222
|
+
headSha: String(p.head?.sha ?? ""),
|
|
223
|
+
changedFiles: await fetchPullFiles(owner, repo, number),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const ANNOTATION_FETCH_LIMIT = 6;
|
|
227
|
+
async function fetchFailureLines(owner, repo, checkRunId) {
|
|
228
|
+
try {
|
|
229
|
+
const data = (await apiGet(`/repos/${owner}/${repo}/check-runs/${checkRunId}/annotations?per_page=50`));
|
|
230
|
+
const lines = [];
|
|
231
|
+
for (const a of data ?? []) {
|
|
232
|
+
const level = String(a.annotation_level ?? "");
|
|
233
|
+
if (level !== "failure" && level !== "error")
|
|
234
|
+
continue;
|
|
235
|
+
const msg = String(a.message ?? "").trim();
|
|
236
|
+
if (msg)
|
|
237
|
+
lines.push(msg.split("\n")[0].slice(0, 240));
|
|
238
|
+
if (lines.length >= 3)
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
return lines;
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async function defaultFetchHeadChecks(owner, repo, headSha) {
|
|
248
|
+
const data = (await apiGet(`/repos/${owner}/${repo}/commits/${headSha}/check-runs?per_page=100`));
|
|
249
|
+
const failing = (data.check_runs ?? []).filter((c) => c.status === "completed" && FAILING.has(String(c.conclusion ?? "")));
|
|
250
|
+
const out = [];
|
|
251
|
+
let annotationBudget = ANNOTATION_FETCH_LIMIT;
|
|
252
|
+
for (const c of failing) {
|
|
253
|
+
const id = typeof c.id === "number" ? c.id : undefined;
|
|
254
|
+
let failureLines;
|
|
255
|
+
if (id !== undefined && annotationBudget > 0) {
|
|
256
|
+
annotationBudget -= 1;
|
|
257
|
+
const lines = await fetchFailureLines(owner, repo, id);
|
|
258
|
+
if (lines.length > 0)
|
|
259
|
+
failureLines = lines;
|
|
260
|
+
}
|
|
261
|
+
out.push({
|
|
262
|
+
name: String(c.name ?? "unnamed check"),
|
|
263
|
+
conclusion: String(c.conclusion ?? "unknown"),
|
|
264
|
+
detailsUrl: typeof c.details_url === "string" ? c.details_url : undefined,
|
|
265
|
+
failureLines,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
/** How many recent base-branch commits to inspect for each check's history. */
|
|
271
|
+
const BASE_COMMIT_WINDOW = 5;
|
|
272
|
+
async function defaultFetchBaseRecords(owner, repo, baseRef) {
|
|
273
|
+
const records = [];
|
|
274
|
+
let commits;
|
|
275
|
+
try {
|
|
276
|
+
commits = (await apiGet(`/repos/${owner}/${repo}/commits?sha=${encodeURIComponent(baseRef)}&per_page=${BASE_COMMIT_WINDOW}`));
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return records;
|
|
280
|
+
}
|
|
281
|
+
for (const commit of commits ?? []) {
|
|
282
|
+
const sha = typeof commit.sha === "string" ? commit.sha : undefined;
|
|
283
|
+
if (!sha)
|
|
284
|
+
continue;
|
|
285
|
+
try {
|
|
286
|
+
const data = (await apiGet(`/repos/${owner}/${repo}/commits/${sha}/check-runs?per_page=100`));
|
|
287
|
+
for (const c of data.check_runs ?? []) {
|
|
288
|
+
if (c.status !== "completed" || !c.conclusion)
|
|
289
|
+
continue;
|
|
290
|
+
records.push({
|
|
291
|
+
name: String(c.name ?? "unnamed check"),
|
|
292
|
+
conclusion: String(c.conclusion),
|
|
293
|
+
completedAt: typeof c.completed_at === "string" ? c.completed_at : "",
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
// One bad commit in the window is not the whole story; keep going.
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// completedAt: check-runs list on commits lacks completed_at in some shapes;
|
|
302
|
+
// fall back to "recent run" ordering when absent.
|
|
303
|
+
return records;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Full triage for a PR URL. Best-effort: if the base window cannot be
|
|
307
|
+
* read, every check is classified without base data (indeterminate with a
|
|
308
|
+
* lean) rather than erroring.
|
|
309
|
+
*/
|
|
310
|
+
export async function triagePr(prUrl, deps = {}) {
|
|
311
|
+
const { owner, repo, number } = parsePullUrl(prUrl);
|
|
312
|
+
const fetchPull = deps.fetchPull ?? defaultFetchPull;
|
|
313
|
+
const fetchHeadChecks = deps.fetchHeadChecks ?? defaultFetchHeadChecks;
|
|
314
|
+
const fetchBaseRecords = deps.fetchBaseRecords ?? defaultFetchBaseRecords;
|
|
315
|
+
const pull = await fetchPull(owner, repo, number);
|
|
316
|
+
const headChecks = await fetchHeadChecks(owner, repo, pull.headSha);
|
|
317
|
+
let baseRecords;
|
|
318
|
+
try {
|
|
319
|
+
baseRecords = await fetchBaseRecords(owner, repo, pull.baseRef);
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
baseRecords = [];
|
|
323
|
+
}
|
|
324
|
+
return classifyCiTriage({
|
|
325
|
+
prUrl,
|
|
326
|
+
prTitle: pull.title,
|
|
327
|
+
baseRef: pull.baseRef,
|
|
328
|
+
baseSha: pull.baseSha,
|
|
329
|
+
headSha: pull.headSha,
|
|
330
|
+
headChecks,
|
|
331
|
+
baseRecords,
|
|
332
|
+
changedFiles: pull.changedFiles,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
//# sourceMappingURL=ci-triage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ci-triage.js","sourceRoot":"","sources":["../src/ci-triage.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* greenlight scan <owner/repo> [--json] [--no-fallback] [--gates-file path]
|
|
7
7
|
* greenlight mcp
|
|
8
8
|
* greenlight friction <owner/repo> [--json]
|
|
9
|
+
* greenlight ci <pr-url> [--json]
|
|
10
|
+
* greenlight review <pr-url> [--json]
|
|
9
11
|
* greenlight claim <issue-url> [--yes] [--force] [--json]
|
|
10
12
|
* greenlight prep <issue-url> [--dir path] [--yes] [--json]
|
|
11
13
|
* greenlight pr <issue-url> --branch <name> [--title t] [--yes] [--json]
|
|
@@ -21,6 +23,8 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
21
23
|
import { scoreFriction } from "./friction.js";
|
|
22
24
|
import { createRestClient, claimIssue, prepIssue, openPr, AutomationRefusal, } from "./automate.js";
|
|
23
25
|
import { fetchMergeVelocity, describeMergeVelocity } from "./merge-velocity.js";
|
|
26
|
+
import { triagePr } from "./ci-triage.js";
|
|
27
|
+
import { triageReview } from "./review-triage.js";
|
|
24
28
|
const SYMBOL = {
|
|
25
29
|
GO: "[GO]",
|
|
26
30
|
RISKY: "[RISKY]",
|
|
@@ -282,6 +286,68 @@ async function cmdPr(issueUrl, args) {
|
|
|
282
286
|
console.log(`Re-run with --yes (and GITHUB_TOKEN set) to open it.`);
|
|
283
287
|
}
|
|
284
288
|
}
|
|
289
|
+
async function cmdCi(prUrl, asJson) {
|
|
290
|
+
const result = await triagePr(prUrl);
|
|
291
|
+
if (asJson) {
|
|
292
|
+
console.log(JSON.stringify(result, null, 2));
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
console.log(`CI triage for ${result.pr}`);
|
|
296
|
+
console.log(` ${result.title}`);
|
|
297
|
+
console.log(` head ${result.headSha.slice(0, 7)} <- base ${result.baseRef}`);
|
|
298
|
+
if (result.checks.length === 0) {
|
|
299
|
+
console.log(` No failing checks on the PR head.`);
|
|
300
|
+
console.log(`Verdict: ${result.verdict}`);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
for (const c of result.checks) {
|
|
304
|
+
console.log(` [${c.class}] ${c.name}`);
|
|
305
|
+
console.log(` ${c.evidence}`);
|
|
306
|
+
if (c.lean)
|
|
307
|
+
console.log(` lean: ${c.lean}`);
|
|
308
|
+
if (c.detailsUrl)
|
|
309
|
+
console.log(` ${c.detailsUrl}`);
|
|
310
|
+
if (c.failureLines)
|
|
311
|
+
for (const line of c.failureLines)
|
|
312
|
+
console.log(` > ${line}`);
|
|
313
|
+
}
|
|
314
|
+
console.log(`Verdict: ${result.verdict}`);
|
|
315
|
+
}
|
|
316
|
+
async function cmdReview(prUrl, asJson) {
|
|
317
|
+
const result = await triageReview(prUrl);
|
|
318
|
+
if (asJson) {
|
|
319
|
+
console.log(JSON.stringify(result, null, 2));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
console.log(`Review triage for ${result.pr}`);
|
|
323
|
+
console.log(` ${result.title}`);
|
|
324
|
+
if (result.findings.length === 0) {
|
|
325
|
+
const c = result.counts;
|
|
326
|
+
console.log(` No actionable findings (noise: ${c.noise}, resolved: ${c.resolved}).`);
|
|
327
|
+
console.log(`Verdict: ${result.verdict}`);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const groups = [
|
|
331
|
+
["likely-real", "LIKELY REAL - fix these"],
|
|
332
|
+
["needs-decision", "NEEDS DECISION - your call"],
|
|
333
|
+
["likely-noise", "LIKELY NOISE - probably push back"],
|
|
334
|
+
];
|
|
335
|
+
for (const [cls, heading] of groups) {
|
|
336
|
+
const items = result.findings.filter((f) => f.class === cls);
|
|
337
|
+
if (items.length === 0)
|
|
338
|
+
continue;
|
|
339
|
+
console.log(`\n${heading}`);
|
|
340
|
+
for (const f of items) {
|
|
341
|
+
const anchor = f.path ? ` (${f.path}${f.line ? `:${f.line}` : ""})` : "";
|
|
342
|
+
console.log(` [${f.author}] ${f.summary}${anchor}`);
|
|
343
|
+
console.log(` why: ${f.reason}`);
|
|
344
|
+
console.log(` draft reply: ${f.draft}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const c = result.counts;
|
|
348
|
+
console.log(`\nSummary: ${c.likelyReal} likely-real, ${c.needsDecision} needs-decision, ${c.likelyNoise} likely-noise (noise: ${c.noise}, resolved: ${c.resolved})`);
|
|
349
|
+
console.log(`Verdict: ${result.verdict}`);
|
|
350
|
+
}
|
|
285
351
|
function usage() {
|
|
286
352
|
console.log("greenlight: pre-flight checks for open-source contributions");
|
|
287
353
|
console.log("");
|
|
@@ -289,6 +355,8 @@ function usage() {
|
|
|
289
355
|
console.log(" greenlight scan <owner/repo> [--json] [--no-fallback] [--gates-file path]");
|
|
290
356
|
console.log(" greenlight mcp (run the MCP server on stdio)");
|
|
291
357
|
console.log(" greenlight friction <owner/repo> [--json]");
|
|
358
|
+
console.log(" greenlight ci <pr-url> [--json] (CI failure triage: is the red check yours or pre-existing?)");
|
|
359
|
+
console.log(" greenlight review <pr-url> [--json] (review triage: which review comments need you, with reply drafts)");
|
|
292
360
|
console.log(" greenlight claim <issue-url> [--yes] [--force] [--json]");
|
|
293
361
|
console.log(" greenlight prep <issue-url> [--dir path] [--yes] [--json]");
|
|
294
362
|
console.log(" greenlight pr <issue-url> --branch <name> [--title t] [--yes] [--json]");
|
|
@@ -326,6 +394,10 @@ async function main() {
|
|
|
326
394
|
return await cmdScan(target, args.includes("--json"), args);
|
|
327
395
|
if (command === "friction" && target)
|
|
328
396
|
return await cmdFriction(target, args.includes("--json"));
|
|
397
|
+
if (command === "ci" && target)
|
|
398
|
+
return await cmdCi(target, args.includes("--json"));
|
|
399
|
+
if (command === "review" && target)
|
|
400
|
+
return await cmdReview(target, args.includes("--json"));
|
|
329
401
|
if (command === "claim" && target)
|
|
330
402
|
return await cmdClaim(target, args);
|
|
331
403
|
if (command === "prep" && target)
|