@blogic-cz/agent-tools 0.14.53 → 0.14.55
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 +1 -1
- package/src/db-tool/service.ts +13 -0
- package/src/gh-tool/index.ts +2 -0
- package/src/gh-tool/pr/commands.ts +33 -2
- package/src/gh-tool/pr/core.ts +61 -14
- package/src/gh-tool/pr/index.ts +1 -0
- package/src/gh-tool/pr/review.ts +175 -1
- package/src/gh-tool/types.ts +3 -0
- package/src/gh-tool/workflow.ts +10 -3
package/package.json
CHANGED
package/src/db-tool/service.ts
CHANGED
|
@@ -53,6 +53,16 @@ export function resolveDbAccessMode(
|
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
export function isFullyReadOnly(
|
|
57
|
+
config: Pick<DbConfig, "allowMutations" | "allowedMutations" | "allowedMutationTargets">,
|
|
58
|
+
): boolean {
|
|
59
|
+
return (
|
|
60
|
+
!config.allowMutations &&
|
|
61
|
+
config.allowedMutations.length === 0 &&
|
|
62
|
+
Object.keys(config.allowedMutationTargets).length === 0
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
56
66
|
// Re-exported from the shared probe so existing `#db/service` consumers keep working.
|
|
57
67
|
export { buildApiProbeArgs } from "#shared/k8s-probe";
|
|
58
68
|
|
|
@@ -393,6 +403,9 @@ export class DbService extends Context.Service<
|
|
|
393
403
|
env: {
|
|
394
404
|
...process.env,
|
|
395
405
|
...(password ? { PGPASSWORD: password } : {}),
|
|
406
|
+
...(isFullyReadOnly(config)
|
|
407
|
+
? { PGOPTIONS: "-c default_transaction_read_only=on" }
|
|
408
|
+
: {}),
|
|
396
409
|
} as Record<string, string>,
|
|
397
410
|
});
|
|
398
411
|
};
|
package/src/gh-tool/index.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
prReplyCommand,
|
|
36
36
|
prResolveCommand,
|
|
37
37
|
prReviewsCommand,
|
|
38
|
+
prLastHumanReviewerCommand,
|
|
38
39
|
prSubmitReviewCommand,
|
|
39
40
|
prChecksCommand,
|
|
40
41
|
prChecksFailedCommand,
|
|
@@ -82,6 +83,7 @@ const prCommand = Command.make("pr", {}).pipe(
|
|
|
82
83
|
prThreadsCommand,
|
|
83
84
|
prCommentsCommand,
|
|
84
85
|
prReviewsCommand,
|
|
86
|
+
prLastHumanReviewerCommand,
|
|
85
87
|
prFeedbackCommand,
|
|
86
88
|
prIssueCommentsCommand,
|
|
87
89
|
prIssueCommentsLatestCommand,
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
fetchDiscussionSummary,
|
|
47
47
|
fetchFeedback,
|
|
48
48
|
fetchIssueComments,
|
|
49
|
+
fetchLastHumanReviewer,
|
|
49
50
|
fetchLatestIssueComment,
|
|
50
51
|
fetchReviews,
|
|
51
52
|
fetchThreads,
|
|
@@ -520,13 +521,19 @@ export const prChecksFailedCommand = Command.make(
|
|
|
520
521
|
Flag.optional,
|
|
521
522
|
),
|
|
522
523
|
repo: repoOption,
|
|
524
|
+
withLogs: Flag.boolean("with-logs").pipe(
|
|
525
|
+
Flag.withDescription(
|
|
526
|
+
"Inline the failed-step logs for each failed check so no follow-up job-logs call is needed",
|
|
527
|
+
),
|
|
528
|
+
Flag.withDefault(false),
|
|
529
|
+
),
|
|
523
530
|
},
|
|
524
|
-
({ format, pr, repo }) =>
|
|
531
|
+
({ format, pr, repo, withLogs }) =>
|
|
525
532
|
withRepo(
|
|
526
533
|
repo,
|
|
527
534
|
Effect.gen(function* () {
|
|
528
535
|
const prNumber = Option.getOrNull(pr);
|
|
529
|
-
const checks = yield* fetchFailedChecks(prNumber);
|
|
536
|
+
const checks = yield* fetchFailedChecks(prNumber, withLogs);
|
|
530
537
|
yield* logFormatted(checks, format);
|
|
531
538
|
}),
|
|
532
539
|
),
|
|
@@ -620,6 +627,30 @@ export const prCommentsCommand = Command.make(
|
|
|
620
627
|
),
|
|
621
628
|
).pipe(Command.withDescription("Fetch review comments for a PR (optionally filter by --since)"));
|
|
622
629
|
|
|
630
|
+
export const prLastHumanReviewerCommand = Command.make(
|
|
631
|
+
"last-human-reviewer",
|
|
632
|
+
{
|
|
633
|
+
format: formatOption,
|
|
634
|
+
pr: Flag.integer("pr").pipe(
|
|
635
|
+
Flag.withDescription("PR number (default: current branch PR)"),
|
|
636
|
+
Flag.optional,
|
|
637
|
+
),
|
|
638
|
+
repo: repoOption,
|
|
639
|
+
},
|
|
640
|
+
({ format, pr, repo }) =>
|
|
641
|
+
withRepo(
|
|
642
|
+
repo,
|
|
643
|
+
Effect.gen(function* () {
|
|
644
|
+
const result = yield* fetchLastHumanReviewer(Option.getOrNull(pr));
|
|
645
|
+
yield* logFormatted(result, format);
|
|
646
|
+
}),
|
|
647
|
+
),
|
|
648
|
+
).pipe(
|
|
649
|
+
Command.withDescription(
|
|
650
|
+
"Report the most recent human (non-bot) reviewer, when they reviewed, and the current requested reviewers",
|
|
651
|
+
),
|
|
652
|
+
);
|
|
653
|
+
|
|
623
654
|
export const prReviewsCommand = Command.make(
|
|
624
655
|
"reviews",
|
|
625
656
|
{
|
package/src/gh-tool/pr/core.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { GitHubService } from "#gh/service";
|
|
|
17
17
|
|
|
18
18
|
import type { ButStatusJson, PRViewJsonResult } from "./helpers";
|
|
19
19
|
import { runLocalCommand } from "./helpers";
|
|
20
|
+
import { fetchJobLogs } from "#gh/workflow";
|
|
20
21
|
|
|
21
22
|
const CHECK_JSON_FIELDS = "name,state,bucket,link";
|
|
22
23
|
const GITHUB_ACTIONS_RUN_ID_RE = /github\.com\/[^/]+\/[^/]+\/actions\/runs\/(\d+)/;
|
|
@@ -104,6 +105,16 @@ const getCheckJobNameCandidates = (checkName: string): string[] => {
|
|
|
104
105
|
return [...new Set([exact, suffix].filter((value): value is string => value !== undefined))];
|
|
105
106
|
};
|
|
106
107
|
|
|
108
|
+
const failedJobsMatchingCheck = <Job extends { name: string }>(
|
|
109
|
+
checkName: string,
|
|
110
|
+
failedJobs: readonly Job[],
|
|
111
|
+
): Job[] => {
|
|
112
|
+
const candidates = getCheckJobNameCandidates(checkName);
|
|
113
|
+
return failedJobs.filter((job) =>
|
|
114
|
+
candidates.some((candidate) => job.name.toLowerCase() === candidate.toLowerCase()),
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
|
|
107
118
|
const resolveJobIdsForFailedChecks = (
|
|
108
119
|
checks: CheckResult[],
|
|
109
120
|
jobs: WorkflowRunJobsForRerun["jobs"],
|
|
@@ -112,10 +123,7 @@ const resolveJobIdsForFailedChecks = (
|
|
|
112
123
|
const jobIds = new Set<number>();
|
|
113
124
|
|
|
114
125
|
for (const check of checks) {
|
|
115
|
-
const
|
|
116
|
-
const matches = failedJobs.filter((job) =>
|
|
117
|
-
candidates.some((candidate) => job.name.toLowerCase() === candidate.toLowerCase()),
|
|
118
|
-
);
|
|
126
|
+
const matches = failedJobsMatchingCheck(check.name, failedJobs);
|
|
119
127
|
|
|
120
128
|
if (matches.length !== 1) {
|
|
121
129
|
return null;
|
|
@@ -127,6 +135,14 @@ const resolveJobIdsForFailedChecks = (
|
|
|
127
135
|
return [...jobIds];
|
|
128
136
|
};
|
|
129
137
|
|
|
138
|
+
const matchFailedJobForCheck = <Job extends { name: string }>(
|
|
139
|
+
checkName: string,
|
|
140
|
+
failedJobs: readonly Job[],
|
|
141
|
+
): Job | null => {
|
|
142
|
+
const matches = failedJobsMatchingCheck(checkName, failedJobs);
|
|
143
|
+
return matches.length === 1 ? matches[0] : null;
|
|
144
|
+
};
|
|
145
|
+
|
|
130
146
|
const fetchWorkflowRunFailureContext = Effect.fn("pr.fetchWorkflowRunFailureContext")(function* (
|
|
131
147
|
runId: number,
|
|
132
148
|
) {
|
|
@@ -149,6 +165,7 @@ const fetchWorkflowRunFailureContext = Effect.fn("pr.fetchWorkflowRunFailureCont
|
|
|
149
165
|
const failedJobs = run.jobs
|
|
150
166
|
.filter((job) => job.conclusion === "failure" || job.status === "failure")
|
|
151
167
|
.map((job) => ({
|
|
168
|
+
databaseId: job.databaseId,
|
|
152
169
|
name: job.name,
|
|
153
170
|
status: job.status,
|
|
154
171
|
conclusion: job.conclusion,
|
|
@@ -184,6 +201,7 @@ const fetchCheckResults = Effect.fn("pr.fetchCheckResults")(function* (pr: numbe
|
|
|
184
201
|
const buildFailedChecksReport = Effect.fn("pr.buildFailedChecksReport")(function* (
|
|
185
202
|
pr: number | null,
|
|
186
203
|
checks: CheckResult[],
|
|
204
|
+
options: { withLogs: boolean } = { withLogs: false },
|
|
187
205
|
) {
|
|
188
206
|
const failedChecks = checks.filter((check) => check.bucket === "fail");
|
|
189
207
|
const pendingChecks = checks.filter((check) => check.bucket === "pending");
|
|
@@ -211,14 +229,40 @@ const buildFailedChecksReport = Effect.fn("pr.buildFailedChecksReport")(function
|
|
|
211
229
|
runContexts.set(runId, context);
|
|
212
230
|
}
|
|
213
231
|
|
|
214
|
-
const enrichedFailedChecks: FailedCheckDetail[] =
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
232
|
+
const enrichedFailedChecks: FailedCheckDetail[] = yield* Effect.forEach(
|
|
233
|
+
failedChecks,
|
|
234
|
+
(check) =>
|
|
235
|
+
Effect.gen(function* () {
|
|
236
|
+
const runId = extractRunIdFromCheckLink(check.link);
|
|
237
|
+
const run = runId === null ? null : (runContexts.get(runId) ?? null);
|
|
238
|
+
const detail: FailedCheckDetail = { ...check, runId, run };
|
|
239
|
+
|
|
240
|
+
if (!options.withLogs || runId === null) {
|
|
241
|
+
return detail;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const matchedJob = run ? matchFailedJobForCheck(check.name, run.failedJobs) : null;
|
|
245
|
+
if (!matchedJob) {
|
|
246
|
+
return detail;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const failedStepLogs = yield* fetchJobLogs({
|
|
250
|
+
runId,
|
|
251
|
+
job: matchedJob.name,
|
|
252
|
+
jobId: matchedJob.databaseId,
|
|
253
|
+
failedStepNames: matchedJob.failedSteps,
|
|
254
|
+
failedStepsOnly: true,
|
|
255
|
+
format: "text",
|
|
256
|
+
repo: null,
|
|
257
|
+
}).pipe(
|
|
258
|
+
Effect.map((result) => ("formatted" in result ? result.formatted : "")),
|
|
259
|
+
Effect.catch(() => Effect.succeed("")),
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
return failedStepLogs && failedStepLogs.length > 0 ? { ...detail, failedStepLogs } : detail;
|
|
263
|
+
}),
|
|
264
|
+
{ concurrency: 5 },
|
|
265
|
+
);
|
|
222
266
|
|
|
223
267
|
const nextCommands = [
|
|
224
268
|
buildChecksFailedCommand(pr),
|
|
@@ -902,9 +946,12 @@ export const fetchChecks = Effect.fn("pr.fetchChecks")(function* (
|
|
|
902
946
|
return results;
|
|
903
947
|
});
|
|
904
948
|
|
|
905
|
-
export const fetchFailedChecks = Effect.fn("pr.fetchFailedChecks")(function* (
|
|
949
|
+
export const fetchFailedChecks = Effect.fn("pr.fetchFailedChecks")(function* (
|
|
950
|
+
pr: number | null,
|
|
951
|
+
withLogs = false,
|
|
952
|
+
) {
|
|
906
953
|
const checks = yield* fetchCheckResults(pr);
|
|
907
|
-
return yield* buildFailedChecksReport(pr, checks);
|
|
954
|
+
return yield* buildFailedChecksReport(pr, checks, { withLogs });
|
|
908
955
|
});
|
|
909
956
|
|
|
910
957
|
export const fetchChecksForCommand = Effect.fn("pr.fetchChecksForCommand")(function* (
|
package/src/gh-tool/pr/index.ts
CHANGED
package/src/gh-tool/pr/review.ts
CHANGED
|
@@ -81,6 +81,53 @@ const SUBMIT_REVIEW_MUTATION = `
|
|
|
81
81
|
}
|
|
82
82
|
`;
|
|
83
83
|
|
|
84
|
+
const LAST_HUMAN_REVIEWER_QUERY = `
|
|
85
|
+
query($owner: String!, $name: String!, $pr: Int!) {
|
|
86
|
+
repository(owner: $owner, name: $name) {
|
|
87
|
+
pullRequest(number: $pr) {
|
|
88
|
+
reviewRequests(first: 100) {
|
|
89
|
+
nodes {
|
|
90
|
+
requestedReviewer {
|
|
91
|
+
__typename
|
|
92
|
+
... on User { login }
|
|
93
|
+
... on Team { name }
|
|
94
|
+
... on Bot { login }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
reviews(last: 100) {
|
|
99
|
+
nodes {
|
|
100
|
+
author { login }
|
|
101
|
+
state
|
|
102
|
+
submittedAt
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
timelineItems(first: 250, itemTypes: [REVIEW_REQUESTED_EVENT, REVIEW_REQUEST_REMOVED_EVENT]) {
|
|
106
|
+
nodes {
|
|
107
|
+
__typename
|
|
108
|
+
... on ReviewRequestedEvent {
|
|
109
|
+
requestedReviewer {
|
|
110
|
+
__typename
|
|
111
|
+
... on User { login }
|
|
112
|
+
... on Team { name }
|
|
113
|
+
... on Bot { login }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
... on ReviewRequestRemovedEvent {
|
|
117
|
+
requestedReviewer {
|
|
118
|
+
__typename
|
|
119
|
+
... on User { login }
|
|
120
|
+
... on Team { name }
|
|
121
|
+
... on Bot { login }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
`;
|
|
130
|
+
|
|
84
131
|
// ---------------------------------------------------------------------------
|
|
85
132
|
// Internal types
|
|
86
133
|
// ---------------------------------------------------------------------------
|
|
@@ -141,6 +188,35 @@ type SubmitReviewResult = {
|
|
|
141
188
|
};
|
|
142
189
|
};
|
|
143
190
|
|
|
191
|
+
type RequestedReviewerNode = {
|
|
192
|
+
__typename: string;
|
|
193
|
+
login?: string;
|
|
194
|
+
name?: string;
|
|
195
|
+
} | null;
|
|
196
|
+
|
|
197
|
+
type LastHumanReviewerQueryResult = {
|
|
198
|
+
repository: {
|
|
199
|
+
pullRequest: {
|
|
200
|
+
reviewRequests: {
|
|
201
|
+
nodes: Array<{ requestedReviewer: RequestedReviewerNode }>;
|
|
202
|
+
};
|
|
203
|
+
reviews: {
|
|
204
|
+
nodes: Array<{
|
|
205
|
+
author: { login: string } | null;
|
|
206
|
+
state: string;
|
|
207
|
+
submittedAt: string | null;
|
|
208
|
+
}>;
|
|
209
|
+
};
|
|
210
|
+
timelineItems: {
|
|
211
|
+
nodes: Array<{
|
|
212
|
+
__typename: string;
|
|
213
|
+
requestedReviewer: RequestedReviewerNode;
|
|
214
|
+
}>;
|
|
215
|
+
};
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
|
|
144
220
|
type RawReviewComment = {
|
|
145
221
|
id: number;
|
|
146
222
|
in_reply_to_id: number | null;
|
|
@@ -259,7 +335,7 @@ const enrichThreads = (threads: ThreadNode[], reviewComments: ReviewComment[]):
|
|
|
259
335
|
repliesByRootCommentId.set(comment.inReplyToId, replies);
|
|
260
336
|
}
|
|
261
337
|
|
|
262
|
-
|
|
338
|
+
const mapped = threads
|
|
263
339
|
.map((node) => {
|
|
264
340
|
const comment = node.comments.nodes[0];
|
|
265
341
|
if (!comment) {
|
|
@@ -295,6 +371,24 @@ const enrichThreads = (threads: ThreadNode[], reviewComments: ReviewComment[]):
|
|
|
295
371
|
};
|
|
296
372
|
})
|
|
297
373
|
.filter((thread): thread is ReviewThread => thread !== null);
|
|
374
|
+
|
|
375
|
+
const dedupedByKey = new Map<string, number>();
|
|
376
|
+
const deduped: ReviewThread[] = [];
|
|
377
|
+
for (const thread of mapped) {
|
|
378
|
+
const key = `${thread.path}${thread.line}${thread.body.trim()}`;
|
|
379
|
+
const existingIndex = dedupedByKey.get(key);
|
|
380
|
+
if (existingIndex === undefined) {
|
|
381
|
+
dedupedByKey.set(key, deduped.length);
|
|
382
|
+
deduped.push(thread);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const existing = deduped[existingIndex];
|
|
386
|
+
if (existing !== undefined && existing.isResolved && !thread.isResolved) {
|
|
387
|
+
deduped[existingIndex] = thread;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return deduped;
|
|
298
392
|
};
|
|
299
393
|
|
|
300
394
|
const fetchThreadState = Effect.fn("pr.fetchThreadState")(function* (pr: number) {
|
|
@@ -768,3 +862,83 @@ export const submitPendingReview = Effect.fn("pr.submitPendingReview")(function*
|
|
|
768
862
|
state: result.submitPullRequestReview.pullRequestReview.state,
|
|
769
863
|
};
|
|
770
864
|
});
|
|
865
|
+
|
|
866
|
+
const AUTOMATION_LOGINS = new Set(["claude", "github-actions", "dependabot"]);
|
|
867
|
+
|
|
868
|
+
const isHumanLogin = (login: string): boolean => {
|
|
869
|
+
const lower = login.toLowerCase();
|
|
870
|
+
if (lower.endsWith("[bot]")) {
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
return !AUTOMATION_LOGINS.has(lower);
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
const requestedReviewerLogin = (reviewer: RequestedReviewerNode): string | null =>
|
|
877
|
+
reviewer === null ? null : (reviewer.login ?? reviewer.name ?? null);
|
|
878
|
+
|
|
879
|
+
export const fetchLastHumanReviewer = Effect.fn("pr.fetchLastHumanReviewer")(function* (
|
|
880
|
+
pr: number | null,
|
|
881
|
+
) {
|
|
882
|
+
const service = yield* GitHubService;
|
|
883
|
+
const repoInfo = yield* service.getRepoInfo();
|
|
884
|
+
|
|
885
|
+
const resolvedPr = pr ?? (yield* viewPR(null)).number;
|
|
886
|
+
|
|
887
|
+
const response = (yield* service.runGraphQL(LAST_HUMAN_REVIEWER_QUERY, {
|
|
888
|
+
owner: repoInfo.owner,
|
|
889
|
+
name: repoInfo.name,
|
|
890
|
+
pr: resolvedPr,
|
|
891
|
+
})) as LastHumanReviewerQueryResult;
|
|
892
|
+
|
|
893
|
+
const prNode = response.repository.pullRequest;
|
|
894
|
+
|
|
895
|
+
const requestedFromTimeline: string[] = [];
|
|
896
|
+
for (const item of prNode.timelineItems.nodes) {
|
|
897
|
+
const login = requestedReviewerLogin(item.requestedReviewer);
|
|
898
|
+
if (login === null) {
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
if (item.__typename === "ReviewRequestedEvent") {
|
|
902
|
+
if (!requestedFromTimeline.includes(login)) {
|
|
903
|
+
requestedFromTimeline.push(login);
|
|
904
|
+
}
|
|
905
|
+
} else {
|
|
906
|
+
const index = requestedFromTimeline.indexOf(login);
|
|
907
|
+
if (index !== -1) {
|
|
908
|
+
requestedFromTimeline.splice(index, 1);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const currentRequestedReviewers =
|
|
914
|
+
requestedFromTimeline.length > 0
|
|
915
|
+
? requestedFromTimeline
|
|
916
|
+
: prNode.reviewRequests.nodes
|
|
917
|
+
.map((node) => requestedReviewerLogin(node.requestedReviewer))
|
|
918
|
+
.filter((login): login is string => login !== null);
|
|
919
|
+
|
|
920
|
+
const latestHumanReview = prNode.reviews.nodes.reduce<{ login: string; at: string } | null>(
|
|
921
|
+
(latest, review) => {
|
|
922
|
+
if (review.author === null || review.submittedAt === null) {
|
|
923
|
+
return latest;
|
|
924
|
+
}
|
|
925
|
+
if (!isHumanLogin(review.author.login)) {
|
|
926
|
+
return latest;
|
|
927
|
+
}
|
|
928
|
+
if (
|
|
929
|
+
latest === null ||
|
|
930
|
+
new Date(review.submittedAt).getTime() > new Date(latest.at).getTime()
|
|
931
|
+
) {
|
|
932
|
+
return { login: review.author.login, at: review.submittedAt };
|
|
933
|
+
}
|
|
934
|
+
return latest;
|
|
935
|
+
},
|
|
936
|
+
null,
|
|
937
|
+
);
|
|
938
|
+
|
|
939
|
+
return {
|
|
940
|
+
currentRequestedReviewers,
|
|
941
|
+
lastHumanReviewer: latestHumanReview?.login ?? null,
|
|
942
|
+
lastHumanReviewAt: latestHumanReview?.at ?? null,
|
|
943
|
+
};
|
|
944
|
+
});
|
package/src/gh-tool/types.ts
CHANGED
|
@@ -83,6 +83,7 @@ export type CheckResult = {
|
|
|
83
83
|
};
|
|
84
84
|
|
|
85
85
|
export type FailedCheckJob = {
|
|
86
|
+
databaseId: number;
|
|
86
87
|
name: string;
|
|
87
88
|
status: string;
|
|
88
89
|
conclusion: string | null;
|
|
@@ -102,6 +103,7 @@ export type FailedCheckRunContext = {
|
|
|
102
103
|
export type FailedCheckDetail = CheckResult & {
|
|
103
104
|
runId: number | null;
|
|
104
105
|
run: FailedCheckRunContext | null;
|
|
106
|
+
failedStepLogs?: string;
|
|
105
107
|
};
|
|
106
108
|
|
|
107
109
|
export type FailedChecksReport = {
|
|
@@ -126,6 +128,7 @@ export type WorkflowRunDetail = {
|
|
|
126
128
|
status: string;
|
|
127
129
|
conclusion: string | null;
|
|
128
130
|
jobs: Array<{
|
|
131
|
+
databaseId: number;
|
|
129
132
|
name: string;
|
|
130
133
|
status: string;
|
|
131
134
|
conclusion: string | null;
|
package/src/gh-tool/workflow.ts
CHANGED
|
@@ -494,9 +494,11 @@ const filterFailedStepEntries = Effect.fn("workflow.filterFailedStepEntries")(fu
|
|
|
494
494
|
return entries.filter((e) => failedStepNames.has(e.step));
|
|
495
495
|
});
|
|
496
496
|
|
|
497
|
-
const fetchJobLogs = Effect.fn("workflow.fetchJobLogs")(function* (opts: {
|
|
497
|
+
export const fetchJobLogs = Effect.fn("workflow.fetchJobLogs")(function* (opts: {
|
|
498
498
|
runId: number;
|
|
499
499
|
job: string;
|
|
500
|
+
jobId?: number | null;
|
|
501
|
+
failedStepNames?: readonly string[] | null;
|
|
500
502
|
failedStepsOnly: boolean;
|
|
501
503
|
format: string;
|
|
502
504
|
repo: string | null;
|
|
@@ -523,7 +525,7 @@ const fetchJobLogs = Effect.fn("workflow.fetchJobLogs")(function* (opts: {
|
|
|
523
525
|
repoName = info.name;
|
|
524
526
|
}
|
|
525
527
|
|
|
526
|
-
const jobId = yield* resolveJobId(opts.runId, opts.job, opts.repo);
|
|
528
|
+
const jobId = opts.jobId ?? (yield* resolveJobId(opts.runId, opts.job, opts.repo));
|
|
527
529
|
|
|
528
530
|
// Fetch raw logs via API (follows 302 redirect automatically)
|
|
529
531
|
const raw = yield* gh
|
|
@@ -539,7 +541,12 @@ const fetchJobLogs = Effect.fn("workflow.fetchJobLogs")(function* (opts: {
|
|
|
539
541
|
let entries = parseRawJobLogs(raw);
|
|
540
542
|
|
|
541
543
|
if (opts.failedStepsOnly) {
|
|
542
|
-
|
|
544
|
+
if (Array.isArray(opts.failedStepNames) && opts.failedStepNames.length > 0) {
|
|
545
|
+
const wanted = new Set(opts.failedStepNames);
|
|
546
|
+
entries = entries.filter((e) => wanted.has(e.step));
|
|
547
|
+
} else {
|
|
548
|
+
entries = yield* filterFailedStepEntries(opts.runId, jobId, entries, opts.repo);
|
|
549
|
+
}
|
|
543
550
|
}
|
|
544
551
|
|
|
545
552
|
if (opts.format === "json") {
|