@blogic-cz/agent-tools 0.14.34 → 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";
|
|
@@ -91,13 +92,14 @@ const prCommand = Command.make("pr", {}).pipe(
|
|
|
91
92
|
|
|
92
93
|
const issueCommand = Command.make("issue", {}).pipe(
|
|
93
94
|
Command.withDescription(
|
|
94
|
-
"Issue operations (list, view, comments, triage, close, reopen, comment, edit)",
|
|
95
|
+
"Issue operations (list, view, comments, triage, snapshot-batch, close, reopen, comment, edit)",
|
|
95
96
|
),
|
|
96
97
|
Command.withSubcommands([
|
|
97
98
|
issueListCommand,
|
|
98
99
|
issueViewCommand,
|
|
99
100
|
issueCommentsCommand,
|
|
100
101
|
issueTriageCommand,
|
|
102
|
+
issueSnapshotBatchCommand,
|
|
101
103
|
issueCloseCommand,
|
|
102
104
|
issueReopenCommand,
|
|
103
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
|
+
);
|