@blogic-cz/agent-tools 0.14.33 → 0.14.35
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/package.json
CHANGED
package/src/gh-tool/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
issueEditCommand,
|
|
14
14
|
issueListCommand,
|
|
15
15
|
issueReopenCommand,
|
|
16
|
+
issueSnapshotBatchCommand,
|
|
16
17
|
issueTriageCommand,
|
|
17
18
|
issueViewCommand,
|
|
18
19
|
} from "./issue";
|
|
@@ -36,6 +37,7 @@ import {
|
|
|
36
37
|
prChecksFailedCommand,
|
|
37
38
|
prRerunChecksCommand,
|
|
38
39
|
prReplyAndResolveCommand,
|
|
40
|
+
prReviewTriageBatchCommand,
|
|
39
41
|
prReviewTriageCommand,
|
|
40
42
|
} from "./pr/index";
|
|
41
43
|
import { branchRenameCommand } from "./branch";
|
|
@@ -84,18 +86,20 @@ const prCommand = Command.make("pr", {}).pipe(
|
|
|
84
86
|
prRerunChecksCommand,
|
|
85
87
|
prReplyAndResolveCommand,
|
|
86
88
|
prReviewTriageCommand,
|
|
89
|
+
prReviewTriageBatchCommand,
|
|
87
90
|
]),
|
|
88
91
|
);
|
|
89
92
|
|
|
90
93
|
const issueCommand = Command.make("issue", {}).pipe(
|
|
91
94
|
Command.withDescription(
|
|
92
|
-
"Issue operations (list, view, comments, triage, close, reopen, comment, edit)",
|
|
95
|
+
"Issue operations (list, view, comments, triage, snapshot-batch, close, reopen, comment, edit)",
|
|
93
96
|
),
|
|
94
97
|
Command.withSubcommands([
|
|
95
98
|
issueListCommand,
|
|
96
99
|
issueViewCommand,
|
|
97
100
|
issueCommentsCommand,
|
|
98
101
|
issueTriageCommand,
|
|
102
|
+
issueSnapshotBatchCommand,
|
|
99
103
|
issueCloseCommand,
|
|
100
104
|
issueReopenCommand,
|
|
101
105
|
issueCommentCommand,
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Command, Flag } from "effect/unstable/cli";
|
|
2
2
|
import { Effect, Option, Schema } from "effect";
|
|
3
3
|
|
|
4
|
-
import type { IssueComment } from "#gh/types";
|
|
4
|
+
import type { CheckResult, IssueComment, PRViewInfo, ReviewThread } from "#gh/types";
|
|
5
5
|
|
|
6
6
|
import { formatOption, logFormatted } from "#shared";
|
|
7
7
|
import { GitHubService } from "#gh/service";
|
|
8
|
+
import { fetchReviewTriage } from "#gh/pr/commands";
|
|
8
9
|
|
|
9
10
|
import { fetchIssueComments } from "./core";
|
|
10
11
|
|
|
@@ -61,6 +62,39 @@ type FullIssueTriage = {
|
|
|
61
62
|
comments: IssueComment[];
|
|
62
63
|
};
|
|
63
64
|
|
|
65
|
+
type ReviewTriageSnapshot = {
|
|
66
|
+
readonly classification: {
|
|
67
|
+
readonly status: "clear" | "needs_investigation";
|
|
68
|
+
readonly reasons: readonly string[];
|
|
69
|
+
};
|
|
70
|
+
readonly info: PRViewInfo;
|
|
71
|
+
readonly unresolvedThreads: readonly ReviewThread[];
|
|
72
|
+
readonly visibleOpenThreads: readonly ReviewThread[];
|
|
73
|
+
readonly summary: {
|
|
74
|
+
readonly visibleOpenReviewThreadsCount: number;
|
|
75
|
+
readonly unrepliedReviewThreadsCount: number;
|
|
76
|
+
readonly unresolvedReviewThreadsCount: number;
|
|
77
|
+
readonly latestIssueComment: IssueComment | null;
|
|
78
|
+
};
|
|
79
|
+
readonly checks: readonly CheckResult[];
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
type IssueSnapshotClassification = {
|
|
83
|
+
readonly status: "clear" | "needs_investigation";
|
|
84
|
+
readonly reasons: readonly string[];
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
type IssueSnapshot = {
|
|
88
|
+
readonly issue: TriageIssue;
|
|
89
|
+
readonly body: string;
|
|
90
|
+
readonly commentsCount: number;
|
|
91
|
+
readonly comments: readonly IssueComment[];
|
|
92
|
+
readonly eligible: boolean;
|
|
93
|
+
readonly linkedPullRequestNumbers: readonly number[];
|
|
94
|
+
readonly linkedPullRequests: readonly ReviewTriageSnapshot[];
|
|
95
|
+
readonly classification: IssueSnapshotClassification;
|
|
96
|
+
};
|
|
97
|
+
|
|
64
98
|
function truncateBody(body: string, maxLength = 500): string {
|
|
65
99
|
if (body.length <= maxLength) return body;
|
|
66
100
|
return body.slice(0, maxLength) + "…";
|
|
@@ -129,6 +163,97 @@ export const fetchIssueTriage = Effect.fn("issue.fetchIssueTriage")(function* (o
|
|
|
129
163
|
return result;
|
|
130
164
|
});
|
|
131
165
|
|
|
166
|
+
export const parseIssueNumbers = (input: string): readonly number[] =>
|
|
167
|
+
input
|
|
168
|
+
.split(",")
|
|
169
|
+
.map((part) => Number.parseInt(part.trim(), 10))
|
|
170
|
+
.filter((number) => Number.isInteger(number) && number > 0);
|
|
171
|
+
|
|
172
|
+
export const collectLinkedPullRequestNumbers = (
|
|
173
|
+
body: string,
|
|
174
|
+
comments: readonly { readonly body: string }[],
|
|
175
|
+
): readonly number[] => {
|
|
176
|
+
const text = [body, ...comments.map((comment) => comment.body)].join("\n");
|
|
177
|
+
const numbers = new Set<number>();
|
|
178
|
+
for (const match of text.matchAll(/(?:pull\/|\/pulls\/)(\d+)|\bPR\s+#?(\d+)/gi)) {
|
|
179
|
+
const number = Number.parseInt(match[1] ?? match[2] ?? "", 10);
|
|
180
|
+
if (Number.isInteger(number) && number > 0) {
|
|
181
|
+
numbers.add(number);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return Array.from(numbers).toSorted((left, right) => left - right);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
function classifyIssueSnapshot(opts: {
|
|
189
|
+
eligible: boolean;
|
|
190
|
+
linkedPullRequestNumbers: readonly number[];
|
|
191
|
+
linkedPullRequests: readonly ReviewTriageSnapshot[];
|
|
192
|
+
}): IssueSnapshotClassification {
|
|
193
|
+
const reasons = [
|
|
194
|
+
...(!opts.eligible ? ["not_owned_by_automation_owner"] : []),
|
|
195
|
+
...(opts.eligible && opts.linkedPullRequestNumbers.length === 0
|
|
196
|
+
? ["no_linked_pull_request"]
|
|
197
|
+
: []),
|
|
198
|
+
...opts.linkedPullRequests.flatMap((pr) =>
|
|
199
|
+
pr.classification.status === "needs_investigation"
|
|
200
|
+
? pr.classification.reasons.map((reason) => `linked_pr_${pr.info.number}_${reason}`)
|
|
201
|
+
: [],
|
|
202
|
+
),
|
|
203
|
+
];
|
|
204
|
+
return { status: reasons.length > 0 ? "needs_investigation" : "clear", reasons };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export const fetchIssueSnapshot = Effect.fn("issue.fetchIssueSnapshot")(function* (opts: {
|
|
208
|
+
issue: number;
|
|
209
|
+
owner: string | null;
|
|
210
|
+
}) {
|
|
211
|
+
const triage = yield* fetchIssueTriage({ issue: opts.issue, verbosity: "full" });
|
|
212
|
+
if (!("comments" in triage)) {
|
|
213
|
+
throw new Error("Expected full issue triage");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const owner = opts.owner?.toLowerCase() ?? null;
|
|
217
|
+
const eligible =
|
|
218
|
+
owner !== null &&
|
|
219
|
+
triage.issue.assignees.length === 1 &&
|
|
220
|
+
triage.issue.assignees[0]?.toLowerCase() === owner;
|
|
221
|
+
const linkedPullRequestNumbers = collectLinkedPullRequestNumbers(triage.body, triage.comments);
|
|
222
|
+
const linkedPullRequests = eligible
|
|
223
|
+
? yield* Effect.all(
|
|
224
|
+
linkedPullRequestNumbers.map((prNumber) => fetchReviewTriage(prNumber)),
|
|
225
|
+
{ concurrency: "unbounded" },
|
|
226
|
+
)
|
|
227
|
+
: [];
|
|
228
|
+
const classification = classifyIssueSnapshot({
|
|
229
|
+
eligible,
|
|
230
|
+
linkedPullRequestNumbers,
|
|
231
|
+
linkedPullRequests,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const result: IssueSnapshot = {
|
|
235
|
+
issue: triage.issue,
|
|
236
|
+
body: triage.body,
|
|
237
|
+
commentsCount: triage.commentsCount,
|
|
238
|
+
comments: triage.comments,
|
|
239
|
+
eligible,
|
|
240
|
+
linkedPullRequestNumbers,
|
|
241
|
+
linkedPullRequests,
|
|
242
|
+
classification,
|
|
243
|
+
};
|
|
244
|
+
return result;
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
export const fetchIssueSnapshotBatch = Effect.fn("issue.fetchIssueSnapshotBatch")(function* (opts: {
|
|
248
|
+
issues: readonly number[];
|
|
249
|
+
owner: string | null;
|
|
250
|
+
}) {
|
|
251
|
+
return yield* Effect.all(
|
|
252
|
+
opts.issues.map((issue) => fetchIssueSnapshot({ issue, owner: opts.owner })),
|
|
253
|
+
{ concurrency: "unbounded" },
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
|
|
132
257
|
export const issueTriageCommand = Command.make(
|
|
133
258
|
"triage",
|
|
134
259
|
{
|
|
@@ -151,3 +276,31 @@ export const issueTriageCommand = Command.make(
|
|
|
151
276
|
).pipe(
|
|
152
277
|
Command.withDescription("Composite: fetch issue details and discussion comments in one call"),
|
|
153
278
|
);
|
|
279
|
+
|
|
280
|
+
export const issueSnapshotBatchCommand = Command.make(
|
|
281
|
+
"snapshot-batch",
|
|
282
|
+
{
|
|
283
|
+
format: formatOption,
|
|
284
|
+
issues: Flag.string("issues").pipe(Flag.withDescription("Comma-separated issue numbers")),
|
|
285
|
+
owner: Flag.string("owner").pipe(
|
|
286
|
+
Flag.withDescription("Automation owner login used to mark eligible issues"),
|
|
287
|
+
Flag.optional,
|
|
288
|
+
),
|
|
289
|
+
repo: repoOption,
|
|
290
|
+
},
|
|
291
|
+
({ format, issues, owner, repo }) =>
|
|
292
|
+
withRepo(
|
|
293
|
+
repo,
|
|
294
|
+
Effect.gen(function* () {
|
|
295
|
+
const result = yield* fetchIssueSnapshotBatch({
|
|
296
|
+
issues: parseIssueNumbers(issues),
|
|
297
|
+
owner: Option.getOrNull(owner),
|
|
298
|
+
});
|
|
299
|
+
yield* logFormatted(result, format);
|
|
300
|
+
}),
|
|
301
|
+
),
|
|
302
|
+
).pipe(
|
|
303
|
+
Command.withDescription(
|
|
304
|
+
"Composite: fetch full issue triage plus linked PR review-triage for multiple issues",
|
|
305
|
+
),
|
|
306
|
+
);
|
|
@@ -62,10 +62,15 @@ type ReviewTriageSummary = {
|
|
|
62
62
|
readonly unresolvedReviewThreadsCount: number;
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
+
type ReviewTriageClassification = {
|
|
66
|
+
readonly status: "clear" | "needs_investigation";
|
|
67
|
+
readonly reasons: readonly string[];
|
|
68
|
+
};
|
|
69
|
+
|
|
65
70
|
export const classifyReviewTriage = (
|
|
66
71
|
summary: ReviewTriageSummary,
|
|
67
72
|
checks: readonly CheckResult[],
|
|
68
|
-
) => {
|
|
73
|
+
): ReviewTriageClassification => {
|
|
69
74
|
const reasons = [
|
|
70
75
|
...(checks.some((check) => check.bucket === "fail") ? ["failed_checks"] : []),
|
|
71
76
|
...(summary.visibleOpenReviewThreadsCount > 0 ? ["visible_open_review_threads"] : []),
|
|
@@ -75,6 +80,26 @@ export const classifyReviewTriage = (
|
|
|
75
80
|
return { status: reasons.length > 0 ? "needs_investigation" : "clear", reasons };
|
|
76
81
|
};
|
|
77
82
|
|
|
83
|
+
export const parsePrNumbers = (input: string): readonly number[] =>
|
|
84
|
+
input
|
|
85
|
+
.split(",")
|
|
86
|
+
.map((part) => Number.parseInt(part.trim(), 10))
|
|
87
|
+
.filter((number) => Number.isInteger(number) && number > 0);
|
|
88
|
+
|
|
89
|
+
export const fetchReviewTriage = Effect.fn("pr.fetchReviewTriage")(function* (
|
|
90
|
+
prNumber: number | null,
|
|
91
|
+
) {
|
|
92
|
+
const [info, unresolvedThreads, visibleOpenThreads, summary, checks] = yield* Effect.all([
|
|
93
|
+
viewPR(prNumber),
|
|
94
|
+
fetchThreads(prNumber, true),
|
|
95
|
+
fetchThreads(prNumber, false, true),
|
|
96
|
+
fetchDiscussionSummary(prNumber),
|
|
97
|
+
fetchChecks(prNumber, false, false, 0),
|
|
98
|
+
]);
|
|
99
|
+
const classification = classifyReviewTriage(summary, checks);
|
|
100
|
+
return { classification, info, unresolvedThreads, visibleOpenThreads, summary, checks };
|
|
101
|
+
});
|
|
102
|
+
|
|
78
103
|
export const prViewCommand = Command.make(
|
|
79
104
|
"view",
|
|
80
105
|
{
|
|
@@ -682,18 +707,8 @@ export const prReviewTriageCommand = Command.make(
|
|
|
682
707
|
repo,
|
|
683
708
|
Effect.gen(function* () {
|
|
684
709
|
const prNumber = Option.getOrNull(pr);
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
fetchThreads(prNumber, true),
|
|
688
|
-
fetchThreads(prNumber, false, true),
|
|
689
|
-
fetchDiscussionSummary(prNumber),
|
|
690
|
-
fetchChecks(prNumber, false, false, 0),
|
|
691
|
-
]);
|
|
692
|
-
const classification = classifyReviewTriage(summary, checks);
|
|
693
|
-
yield* logFormatted(
|
|
694
|
-
{ classification, info, unresolvedThreads, visibleOpenThreads, summary, checks },
|
|
695
|
-
format,
|
|
696
|
-
);
|
|
710
|
+
const result = yield* fetchReviewTriage(prNumber);
|
|
711
|
+
yield* logFormatted(result, format);
|
|
697
712
|
}),
|
|
698
713
|
),
|
|
699
714
|
).pipe(
|
|
@@ -702,6 +717,30 @@ export const prReviewTriageCommand = Command.make(
|
|
|
702
717
|
),
|
|
703
718
|
);
|
|
704
719
|
|
|
720
|
+
export const prReviewTriageBatchCommand = Command.make(
|
|
721
|
+
"review-triage-batch",
|
|
722
|
+
{
|
|
723
|
+
format: formatOption,
|
|
724
|
+
prs: Flag.string("prs").pipe(Flag.withDescription("Comma-separated PR numbers")),
|
|
725
|
+
repo: repoOption,
|
|
726
|
+
},
|
|
727
|
+
({ format, prs, repo }) =>
|
|
728
|
+
withRepo(
|
|
729
|
+
repo,
|
|
730
|
+
Effect.gen(function* () {
|
|
731
|
+
const results = yield* Effect.all(
|
|
732
|
+
parsePrNumbers(prs).map((prNumber) => fetchReviewTriage(prNumber)),
|
|
733
|
+
{ concurrency: "unbounded" },
|
|
734
|
+
);
|
|
735
|
+
yield* logFormatted(results, format);
|
|
736
|
+
}),
|
|
737
|
+
),
|
|
738
|
+
).pipe(
|
|
739
|
+
Command.withDescription(
|
|
740
|
+
"Composite: fetch review-triage output for multiple PRs in one gh-tool invocation",
|
|
741
|
+
),
|
|
742
|
+
);
|
|
743
|
+
|
|
705
744
|
export const prReplyAndResolveCommand = Command.make(
|
|
706
745
|
"reply-and-resolve",
|
|
707
746
|
{
|