@neriros/ralphy 2.19.0 → 2.20.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/README.md +16 -11
- package/dist/cli/index.js +220 -39
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -171,6 +171,8 @@ A default `ralphy.config.json` is written on first run with every defaulted sett
|
|
|
171
171
|
"updateEveryIterations": 10,
|
|
172
172
|
"mentionTrigger": true,
|
|
173
173
|
"mentionHandle": "@ralphy",
|
|
174
|
+
"codeReviewTrigger": true,
|
|
175
|
+
"codeReviewStaleHours": 24,
|
|
174
176
|
"indicators": {
|
|
175
177
|
"getTodo": { "filter": [{ "type": "status", "value": "Todo" }] },
|
|
176
178
|
"getInProgress": { "filter": [{ "type": "status", "value": "In Progress" }] },
|
|
@@ -217,6 +219,8 @@ Linear is the source of truth for which issues Ralph has touched. Each `linear.i
|
|
|
217
219
|
|
|
218
220
|
**`@ralphy` mention trigger.** Set `linear.mentionTrigger: true` (default `false`) to scan done-issue comments on both Linear and their linked GitHub PR for `@ralphy` mentions (configurable via `linear.mentionHandle`). New mentions enqueue the issue as a review run with the mention text used verbatim as the prepended task. Idempotency: a mention is considered processed when its `createdAt` is older than the most recent Ralph `🔁 picked up` comment on the Linear issue, so the same comment never re-fires across polls. Requires the `gh` CLI authenticated for the GitHub side.
|
|
219
221
|
|
|
222
|
+
**Code-review iteration.** Set `linear.codeReviewTrigger: true` (or pass `--code-review`) to watch open, unmerged, unapproved tracked PRs for unresolved review-thread comments. When any unresolved thread has activity newer than Ralph's last `🔁 picked up` ack, the issue is queued as a review run whose prepended task is a digest of every unresolved thread plus instructions: fix-and-push for comments Ralph agrees with (resolve the thread after the commit lands), or reply on GitHub with reasoning for ones it disagrees with. The cycle repeats every poll until the PR is approved or merged. If the reviewer has been silent for more than `linear.codeReviewStaleHours` (default 24) while Ralph is the last actor, a one-shot `@`-mention ping comment is posted on the GitHub PR.
|
|
223
|
+
|
|
220
224
|
Marker types are `"label"` or `"status"`. Combine markers under `apply` when one event needs to set multiple — e.g. `setDone` flipping a status _and_ adding a "shipped" label.
|
|
221
225
|
|
|
222
226
|
#### Per-task git worktrees
|
|
@@ -260,17 +264,18 @@ Failed workers (non-zero exit) are not marked processed, so they'll be retried o
|
|
|
260
264
|
|
|
261
265
|
### Agent mode flags
|
|
262
266
|
|
|
263
|
-
| Option | Description
|
|
264
|
-
| ------------------------- |
|
|
265
|
-
| `--linear-team <key>` | Linear team key (e.g. `ENG`)
|
|
266
|
-
| `--linear-assignee <id>` | Filter by assignee (user id, email, or `me`)
|
|
267
|
-
| `--poll-interval <s>` | Seconds between Linear polls (default: 60)
|
|
268
|
-
| `--concurrency <n>` | Max concurrent task loops (default: 1)
|
|
269
|
-
| `--max-tickets <n>` | Stop picking up new issues after N have been started this run (0 = unlimited)
|
|
270
|
-
| `--worktree` | Run each task in its own git worktree
|
|
271
|
-
| `--indicator <k>:<t>:<v>` | Override a `linear.indicators` entry; repeatable (e.g. `setDone:status:Done`)
|
|
272
|
-
| `--create-pr` | Push worker branch + open a GitHub PR on success (needs `--worktree`)
|
|
273
|
-
| `--fix-ci` | After PR opens, re-run task on CI failures until green (needs `--create-pr`)
|
|
267
|
+
| Option | Description |
|
|
268
|
+
| ------------------------- | ------------------------------------------------------------------------------------ |
|
|
269
|
+
| `--linear-team <key>` | Linear team key (e.g. `ENG`) |
|
|
270
|
+
| `--linear-assignee <id>` | Filter by assignee (user id, email, or `me`) |
|
|
271
|
+
| `--poll-interval <s>` | Seconds between Linear polls (default: 60) |
|
|
272
|
+
| `--concurrency <n>` | Max concurrent task loops (default: 1) |
|
|
273
|
+
| `--max-tickets <n>` | Stop picking up new issues after N have been started this run (0 = unlimited) |
|
|
274
|
+
| `--worktree` | Run each task in its own git worktree |
|
|
275
|
+
| `--indicator <k>:<t>:<v>` | Override a `linear.indicators` entry; repeatable (e.g. `setDone:status:Done`) |
|
|
276
|
+
| `--create-pr` | Push worker branch + open a GitHub PR on success (needs `--worktree`) |
|
|
277
|
+
| `--fix-ci` | After PR opens, re-run task on CI failures until green (needs `--create-pr`) |
|
|
278
|
+
| `--code-review` | Watch open tracked PRs for unresolved review comments and prepend a code-review task |
|
|
274
279
|
|
|
275
280
|
#### `--max-tickets`
|
|
276
281
|
|
package/dist/cli/index.js
CHANGED
|
@@ -35029,8 +35029,8 @@ import { readFileSync as readFileSync2 } from "fs";
|
|
|
35029
35029
|
import { resolve } from "path";
|
|
35030
35030
|
function getVersion() {
|
|
35031
35031
|
try {
|
|
35032
|
-
if ("2.
|
|
35033
|
-
return "2.
|
|
35032
|
+
if ("2.20.0")
|
|
35033
|
+
return "2.20.0";
|
|
35034
35034
|
} catch {}
|
|
35035
35035
|
const dirsToTry = [];
|
|
35036
35036
|
try {
|
|
@@ -35130,6 +35130,7 @@ async function parseArgs(argv) {
|
|
|
35130
35130
|
indicators: {},
|
|
35131
35131
|
createPr: false,
|
|
35132
35132
|
fixCi: false,
|
|
35133
|
+
codeReview: false,
|
|
35133
35134
|
maxTickets: 0,
|
|
35134
35135
|
projectRoot: undefined,
|
|
35135
35136
|
jsonOutput: false
|
|
@@ -35323,6 +35324,9 @@ async function parseArgs(argv) {
|
|
|
35323
35324
|
case "--fix-ci":
|
|
35324
35325
|
result2.fixCi = true;
|
|
35325
35326
|
break;
|
|
35327
|
+
case "--code-review":
|
|
35328
|
+
result2.codeReview = true;
|
|
35329
|
+
break;
|
|
35326
35330
|
case "--json-output":
|
|
35327
35331
|
result2.jsonOutput = true;
|
|
35328
35332
|
break;
|
|
@@ -35417,6 +35421,7 @@ var init_cli = __esm(() => {
|
|
|
35417
35421
|
" Types: label, status",
|
|
35418
35422
|
" --create-pr Push the worker branch and open a GitHub PR on success (needs --worktree)",
|
|
35419
35423
|
" --fix-ci After opening the PR, re-run on CI failures until green (needs --create-pr)",
|
|
35424
|
+
" --code-review Watch open tracked PRs for unresolved review comments and prepend a code-review task",
|
|
35420
35425
|
" --max-tickets <n> Stop picking up new issues after N have been started (0 = unlimited)",
|
|
35421
35426
|
" --json-output Emit JSONL to stdout instead of the Ink dashboard (for scripting/CI)",
|
|
35422
35427
|
"",
|
|
@@ -59627,6 +59632,12 @@ var MarkerSchema, GetIndicatorSchema, SetIndicatorSchema, IndicatorsSchema, Ralp
|
|
|
59627
59632
|
"mentionTrigger": false,
|
|
59628
59633
|
// "mentionHandle": "@ralphy",
|
|
59629
59634
|
|
|
59635
|
+
// Watch open tracked PRs for unresolved review-thread comments and
|
|
59636
|
+
// prepend a code-review task. Pings the reviewer on the GitHub PR
|
|
59637
|
+
// when stalled for more than codeReviewStaleHours.
|
|
59638
|
+
"codeReviewTrigger": false,
|
|
59639
|
+
"codeReviewStaleHours": 24,
|
|
59640
|
+
|
|
59630
59641
|
// Indicators map Ralph lifecycle events to Linear labels/statuses.
|
|
59631
59642
|
// WARNING: activating indicators will query AND mutate your Linear workspace.
|
|
59632
59643
|
// Uncomment each entry after confirming the label/status names match your workspace.
|
|
@@ -59738,12 +59749,16 @@ var init_config = __esm(() => {
|
|
|
59738
59749
|
updateEveryIterations: exports_external.number().int().nonnegative().default(10),
|
|
59739
59750
|
mentionTrigger: exports_external.boolean().default(false),
|
|
59740
59751
|
mentionHandle: exports_external.string().default("@ralphy"),
|
|
59752
|
+
codeReviewTrigger: exports_external.boolean().default(false),
|
|
59753
|
+
codeReviewStaleHours: exports_external.number().nonnegative().default(24),
|
|
59741
59754
|
indicators: IndicatorsSchema.default({})
|
|
59742
59755
|
}).strict().default({
|
|
59743
59756
|
postComments: true,
|
|
59744
59757
|
updateEveryIterations: 10,
|
|
59745
59758
|
mentionTrigger: false,
|
|
59746
59759
|
mentionHandle: "@ralphy",
|
|
59760
|
+
codeReviewTrigger: false,
|
|
59761
|
+
codeReviewStaleHours: 24,
|
|
59747
59762
|
indicators: {}
|
|
59748
59763
|
})
|
|
59749
59764
|
}).default({
|
|
@@ -59759,6 +59774,8 @@ var init_config = __esm(() => {
|
|
|
59759
59774
|
updateEveryIterations: 10,
|
|
59760
59775
|
mentionTrigger: false,
|
|
59761
59776
|
mentionHandle: "@ralphy",
|
|
59777
|
+
codeReviewTrigger: false,
|
|
59778
|
+
codeReviewStaleHours: 24,
|
|
59762
59779
|
indicators: {}
|
|
59763
59780
|
}
|
|
59764
59781
|
});
|
|
@@ -60348,7 +60365,7 @@ class AgentCoordinator {
|
|
|
60348
60365
|
}
|
|
60349
60366
|
}
|
|
60350
60367
|
if (mode === "review" && this.opts.postComments !== false) {
|
|
60351
|
-
const sourceTag = trigger ? trigger.source === "github" ? " (GitHub @mention)" : " (Linear @mention)" : "";
|
|
60368
|
+
const sourceTag = trigger ? trigger.source === "github" ? " (GitHub @mention)" : trigger.source === "github-review" ? " (GitHub code review)" : " (Linear @mention)" : "";
|
|
60352
60369
|
try {
|
|
60353
60370
|
await this.deps.postComment(issue, `\uD83D\uDD01 Ralph picked up new review comments${sourceTag}. Tracking change: \`${prep.changeName}\``);
|
|
60354
60371
|
} catch (err) {
|
|
@@ -61145,6 +61162,26 @@ function escapeRegex(s) {
|
|
|
61145
61162
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
61146
61163
|
}
|
|
61147
61164
|
function buildMentionTaskBody(trigger, issueUrl) {
|
|
61165
|
+
if (trigger.source === "github-review") {
|
|
61166
|
+
return [
|
|
61167
|
+
`Open code-review on ${trigger.url ?? issueUrl} has unresolved comments:`,
|
|
61168
|
+
"",
|
|
61169
|
+
trigger.body.trim(),
|
|
61170
|
+
"",
|
|
61171
|
+
"For every comment above, decide:",
|
|
61172
|
+
"- If you agree, fix the code, commit, and push. The push will surface",
|
|
61173
|
+
" the new commit on the PR; the worker should then resolve the thread",
|
|
61174
|
+
" via `gh api graphql` (`resolveReviewThread`) \u2014 see GitHub docs.",
|
|
61175
|
+
"- If you disagree, post a polite reply on the thread explaining your",
|
|
61176
|
+
" reasoning via `gh api repos/{owner}/{repo}/pulls/{num}/comments/{id}/replies`,",
|
|
61177
|
+
" and leave the thread unresolved.",
|
|
61178
|
+
"",
|
|
61179
|
+
"When this round is done the loop exits; the agent will re-poll the",
|
|
61180
|
+
"PR on the next cycle and pick up any new reviewer activity until the",
|
|
61181
|
+
"PR is approved or merged."
|
|
61182
|
+
].join(`
|
|
61183
|
+
`);
|
|
61184
|
+
}
|
|
61148
61185
|
const sourceLabel = trigger.source === "github" ? "GitHub PR" : "Linear issue";
|
|
61149
61186
|
const permalink = trigger.url ?? issueUrl;
|
|
61150
61187
|
const header = `${trigger.author ?? "unknown"} \u2014 ${trigger.createdAt} (${sourceLabel})`;
|
|
@@ -61320,6 +61357,7 @@ function buildAgentCoordinator(input) {
|
|
|
61320
61357
|
const issueByChange = new Map;
|
|
61321
61358
|
const prByChange = new Map;
|
|
61322
61359
|
const prUnavailable = new Set;
|
|
61360
|
+
const stalePingedAt = new Map;
|
|
61323
61361
|
const useWorktree = args.worktree || cfg.useWorktree;
|
|
61324
61362
|
const scriptRunner = input.runners?.runScript ?? (async (cmd, cwd2) => {
|
|
61325
61363
|
const proc = Bun.spawn({
|
|
@@ -61691,7 +61729,9 @@ PR: ${prUrl}` : ""
|
|
|
61691
61729
|
return fetchOpenIssues(apiKey, { team, assignee, include, exclude });
|
|
61692
61730
|
}
|
|
61693
61731
|
async function fetchMentions() {
|
|
61694
|
-
|
|
61732
|
+
const wantMention = cfg.linear.mentionTrigger;
|
|
61733
|
+
const wantCodeReview = args.codeReview || cfg.linear.codeReviewTrigger;
|
|
61734
|
+
if (!wantMention && !wantCodeReview)
|
|
61695
61735
|
return [];
|
|
61696
61736
|
const handle = cfg.linear.mentionHandle;
|
|
61697
61737
|
let candidates = [];
|
|
@@ -61702,6 +61742,7 @@ PR: ${prUrl}` : ""
|
|
|
61702
61742
|
return [];
|
|
61703
61743
|
}
|
|
61704
61744
|
const out = [];
|
|
61745
|
+
const queued = new Set;
|
|
61705
61746
|
for (const issue of candidates) {
|
|
61706
61747
|
let comments = [];
|
|
61707
61748
|
try {
|
|
@@ -61711,51 +61752,191 @@ PR: ${prUrl}` : ""
|
|
|
61711
61752
|
continue;
|
|
61712
61753
|
}
|
|
61713
61754
|
const lastRalphPickup = findLastRalphPickupISO(comments);
|
|
61714
|
-
|
|
61715
|
-
|
|
61716
|
-
|
|
61717
|
-
|
|
61718
|
-
|
|
61719
|
-
|
|
61755
|
+
if (wantMention) {
|
|
61756
|
+
for (const c of comments) {
|
|
61757
|
+
if (isRalphComment(c.body))
|
|
61758
|
+
continue;
|
|
61759
|
+
if (!containsHandle(c.body, handle))
|
|
61760
|
+
continue;
|
|
61761
|
+
if (lastRalphPickup && c.createdAt <= lastRalphPickup)
|
|
61762
|
+
continue;
|
|
61763
|
+
out.push({
|
|
61764
|
+
issue,
|
|
61765
|
+
trigger: {
|
|
61766
|
+
source: "linear",
|
|
61767
|
+
body: c.body,
|
|
61768
|
+
createdAt: c.createdAt,
|
|
61769
|
+
...c.user?.name ? { author: c.user.name } : {},
|
|
61770
|
+
url: issue.url
|
|
61771
|
+
}
|
|
61772
|
+
});
|
|
61773
|
+
queued.add(issue.id);
|
|
61774
|
+
break;
|
|
61775
|
+
}
|
|
61776
|
+
if (queued.has(issue.id))
|
|
61720
61777
|
continue;
|
|
61721
|
-
out.push({
|
|
61722
|
-
issue,
|
|
61723
|
-
trigger: {
|
|
61724
|
-
source: "linear",
|
|
61725
|
-
body: c.body,
|
|
61726
|
-
createdAt: c.createdAt,
|
|
61727
|
-
...c.user?.name ? { author: c.user.name } : {},
|
|
61728
|
-
url: issue.url
|
|
61729
|
-
}
|
|
61730
|
-
});
|
|
61731
|
-
break;
|
|
61732
61778
|
}
|
|
61733
|
-
if (out.length > 0 && out[out.length - 1].issue.id === issue.id)
|
|
61734
|
-
continue;
|
|
61735
61779
|
const prUrl = await resolvePrUrlForIssue(issue);
|
|
61736
61780
|
if (!prUrl)
|
|
61737
61781
|
continue;
|
|
61738
|
-
|
|
61739
|
-
|
|
61740
|
-
|
|
61741
|
-
|
|
61742
|
-
|
|
61782
|
+
if (wantMention) {
|
|
61783
|
+
const ghComments = await fetchPrIssueComments(prUrl);
|
|
61784
|
+
for (const c of ghComments) {
|
|
61785
|
+
if (!containsHandle(c.body, handle))
|
|
61786
|
+
continue;
|
|
61787
|
+
if (lastRalphPickup && c.createdAt <= lastRalphPickup)
|
|
61788
|
+
continue;
|
|
61789
|
+
out.push({
|
|
61790
|
+
issue,
|
|
61791
|
+
trigger: {
|
|
61792
|
+
source: "github",
|
|
61793
|
+
body: c.body,
|
|
61794
|
+
createdAt: c.createdAt,
|
|
61795
|
+
...c.author ? { author: c.author } : {},
|
|
61796
|
+
url: c.url
|
|
61797
|
+
}
|
|
61798
|
+
});
|
|
61799
|
+
queued.add(issue.id);
|
|
61800
|
+
break;
|
|
61801
|
+
}
|
|
61802
|
+
if (queued.has(issue.id))
|
|
61743
61803
|
continue;
|
|
61744
|
-
|
|
61745
|
-
|
|
61746
|
-
|
|
61747
|
-
|
|
61748
|
-
|
|
61749
|
-
|
|
61750
|
-
|
|
61751
|
-
url: c.url
|
|
61752
|
-
}
|
|
61753
|
-
});
|
|
61754
|
-
break;
|
|
61804
|
+
}
|
|
61805
|
+
if (wantCodeReview) {
|
|
61806
|
+
const trigger = await scanCodeReview(issue, prUrl, lastRalphPickup);
|
|
61807
|
+
if (trigger) {
|
|
61808
|
+
out.push({ issue, trigger });
|
|
61809
|
+
queued.add(issue.id);
|
|
61810
|
+
}
|
|
61755
61811
|
}
|
|
61756
61812
|
}
|
|
61757
61813
|
return out;
|
|
61758
61814
|
}
|
|
61815
|
+
async function scanCodeReview(issue, prUrl, lastRalphPickup) {
|
|
61816
|
+
const state = await fetchPrReviewState(prUrl);
|
|
61817
|
+
if (!state || !state.isOpen || state.merged || state.approved)
|
|
61818
|
+
return null;
|
|
61819
|
+
const unresolved = state.threads.filter((t) => !t.isResolved && t.comments.length > 0);
|
|
61820
|
+
if (unresolved.length === 0)
|
|
61821
|
+
return null;
|
|
61822
|
+
const newestReviewerActivity = unresolved.reduce((acc, t) => {
|
|
61823
|
+
const last2 = t.comments[t.comments.length - 1].createdAt;
|
|
61824
|
+
return last2 > acc ? last2 : acc;
|
|
61825
|
+
}, "");
|
|
61826
|
+
if (!lastRalphPickup || newestReviewerActivity > lastRalphPickup) {
|
|
61827
|
+
const body = unresolved.map((t) => {
|
|
61828
|
+
const head3 = t.path ? `_${t.path}${t.line ? `:${t.line}` : ""}_` : "_(general)_";
|
|
61829
|
+
const lines = t.comments.map((c) => `> **${c.author ?? "reviewer"}** (${c.createdAt})
|
|
61830
|
+
>
|
|
61831
|
+
> ${c.body.trim().replace(/\n/g, `
|
|
61832
|
+
> `)}`);
|
|
61833
|
+
return [head3, "", ...lines].join(`
|
|
61834
|
+
`);
|
|
61835
|
+
}).join(`
|
|
61836
|
+
|
|
61837
|
+
---
|
|
61838
|
+
|
|
61839
|
+
`);
|
|
61840
|
+
return {
|
|
61841
|
+
source: "github-review",
|
|
61842
|
+
body,
|
|
61843
|
+
createdAt: newestReviewerActivity || new Date().toISOString(),
|
|
61844
|
+
...state.lastReviewer ? { author: state.lastReviewer } : {},
|
|
61845
|
+
url: prUrl
|
|
61846
|
+
};
|
|
61847
|
+
}
|
|
61848
|
+
await maybePingStaleReviewer(issue, prUrl, state, newestReviewerActivity);
|
|
61849
|
+
return null;
|
|
61850
|
+
}
|
|
61851
|
+
async function maybePingStaleReviewer(issue, prUrl, state, newestReviewerActivity) {
|
|
61852
|
+
const staleHours = cfg.linear.codeReviewStaleHours;
|
|
61853
|
+
if (staleHours <= 0)
|
|
61854
|
+
return;
|
|
61855
|
+
const reviewer = state.requestedReviewer ?? state.lastReviewer;
|
|
61856
|
+
if (!reviewer)
|
|
61857
|
+
return;
|
|
61858
|
+
const lastPinged = stalePingedAt.get(prUrl);
|
|
61859
|
+
const now2 = Date.now();
|
|
61860
|
+
if (lastPinged && now2 - lastPinged < staleHours * 3600000)
|
|
61861
|
+
return;
|
|
61862
|
+
const elapsedH = newestReviewerActivity ? (now2 - Date.parse(newestReviewerActivity)) / 3600000 : Infinity;
|
|
61863
|
+
if (elapsedH < staleHours)
|
|
61864
|
+
return;
|
|
61865
|
+
const m = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/.exec(prUrl);
|
|
61866
|
+
if (!m)
|
|
61867
|
+
return;
|
|
61868
|
+
const [, owner, repo, num] = m;
|
|
61869
|
+
const body = `\uD83D\uDD14 @${reviewer} \u2014 Ralph has been waiting ${elapsedH.toFixed(0)}h on a re-review for ${prUrl}. Could you take another look when you have a moment?`;
|
|
61870
|
+
try {
|
|
61871
|
+
await cmdRunner.run(["gh", "api", `repos/${owner}/${repo}/issues/${num}/comments`, "-f", `body=${body}`], projectRoot);
|
|
61872
|
+
stalePingedAt.set(prUrl, now2);
|
|
61873
|
+
onLog(` ${issue.identifier}: pinged reviewer @${reviewer} on ${prUrl}`, "gray");
|
|
61874
|
+
} catch (err) {
|
|
61875
|
+
onLog(`! reviewer ping failed for ${prUrl}: ${err.message}`, "yellow");
|
|
61876
|
+
}
|
|
61877
|
+
}
|
|
61878
|
+
async function fetchPrReviewState(prUrl) {
|
|
61879
|
+
const m = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/.exec(prUrl);
|
|
61880
|
+
if (!m)
|
|
61881
|
+
return null;
|
|
61882
|
+
const [, owner, repo, num] = m;
|
|
61883
|
+
const query = `query($owner:String!,$repo:String!,$num:Int!){
|
|
61884
|
+
repository(owner:$owner,name:$repo){
|
|
61885
|
+
pullRequest(number:$num){
|
|
61886
|
+
state merged reviewDecision
|
|
61887
|
+
reviewRequests(first:5){nodes{requestedReviewer{... on User{login}}}}
|
|
61888
|
+
latestReviews(first:5){nodes{author{login} state submittedAt}}
|
|
61889
|
+
reviewThreads(first:50){nodes{
|
|
61890
|
+
isResolved path line
|
|
61891
|
+
comments(first:20){nodes{body author{login} createdAt url}}
|
|
61892
|
+
}}
|
|
61893
|
+
}
|
|
61894
|
+
}
|
|
61895
|
+
}`;
|
|
61896
|
+
try {
|
|
61897
|
+
const res = await cmdRunner.run([
|
|
61898
|
+
"gh",
|
|
61899
|
+
"api",
|
|
61900
|
+
"graphql",
|
|
61901
|
+
"-f",
|
|
61902
|
+
`query=${query}`,
|
|
61903
|
+
"-F",
|
|
61904
|
+
`owner=${owner}`,
|
|
61905
|
+
"-F",
|
|
61906
|
+
`repo=${repo}`,
|
|
61907
|
+
"-F",
|
|
61908
|
+
`num=${num}`
|
|
61909
|
+
], projectRoot);
|
|
61910
|
+
const parsed = JSON.parse(res.stdout);
|
|
61911
|
+
const pr = parsed.data?.repository?.pullRequest;
|
|
61912
|
+
if (!pr)
|
|
61913
|
+
return null;
|
|
61914
|
+
const requested = pr.reviewRequests?.nodes.map((n) => n.requestedReviewer?.login).filter((x) => !!x)[0];
|
|
61915
|
+
const latestReviews = pr.latestReviews?.nodes ?? [];
|
|
61916
|
+
const lastReviewer = latestReviews.slice().sort((a, b) => b.submittedAt > a.submittedAt ? 1 : -1).map((n) => n.author?.login).filter((x) => !!x)[0];
|
|
61917
|
+
return {
|
|
61918
|
+
isOpen: pr.state === "OPEN",
|
|
61919
|
+
merged: pr.merged,
|
|
61920
|
+
approved: pr.reviewDecision === "APPROVED",
|
|
61921
|
+
threads: (pr.reviewThreads?.nodes ?? []).map((t) => ({
|
|
61922
|
+
isResolved: t.isResolved,
|
|
61923
|
+
...t.path ? { path: t.path } : {},
|
|
61924
|
+
...t.line != null ? { line: t.line } : {},
|
|
61925
|
+
comments: t.comments.nodes.map((c) => ({
|
|
61926
|
+
...c.author?.login ? { author: c.author.login } : {},
|
|
61927
|
+
body: c.body,
|
|
61928
|
+
createdAt: c.createdAt,
|
|
61929
|
+
...c.url ? { url: c.url } : {}
|
|
61930
|
+
}))
|
|
61931
|
+
})),
|
|
61932
|
+
...requested ? { requestedReviewer: requested } : {},
|
|
61933
|
+
...lastReviewer ? { lastReviewer } : {}
|
|
61934
|
+
};
|
|
61935
|
+
} catch (err) {
|
|
61936
|
+
onLog(`! gh graphql review-state failed for ${prUrl}: ${err.message}`, "yellow");
|
|
61937
|
+
return null;
|
|
61938
|
+
}
|
|
61939
|
+
}
|
|
61759
61940
|
function findLastRalphPickupISO(comments) {
|
|
61760
61941
|
let latest = null;
|
|
61761
61942
|
for (const c of comments) {
|