@blogic-cz/agent-tools 0.8.14 → 0.8.16
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 +3 -1
- package/src/gh-tool/issue/commands.ts +72 -9
- package/src/gh-tool/pr/commands.ts +99 -19
- package/src/gh-tool/pr/core.ts +211 -6
- package/src/gh-tool/release.ts +31 -3
- package/src/gh-tool/text-input.ts +146 -0
- package/src/gh-tool/types.ts +56 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blogic-cz/agent-tools",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.16",
|
|
4
4
|
"description": "CLI tools for AI coding agent workflows — GitHub, database, Kubernetes, Azure DevOps, logs, sessions, and audit",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -112,8 +112,10 @@
|
|
|
112
112
|
"check:ci": "bun check.ts ci",
|
|
113
113
|
"format": "oxfmt",
|
|
114
114
|
"format:check": "oxfmt --check",
|
|
115
|
+
"gh-tool": "bun src/gh-tool/index.ts",
|
|
115
116
|
"lint": "oxlint -c ./.oxlintrc.json --deny-warnings",
|
|
116
117
|
"lint:fix": "oxlint -c ./.oxlintrc.json --fix",
|
|
118
|
+
"session-tool": "bun src/session-tool/index.ts",
|
|
117
119
|
"update:skills": "bun run .agents/skills/update-packages/references/skills-update-local.ts",
|
|
118
120
|
"test": "vitest run"
|
|
119
121
|
},
|
|
@@ -2,6 +2,7 @@ import { Command, Flag } from "effect/unstable/cli";
|
|
|
2
2
|
import { Effect, Option } from "effect";
|
|
3
3
|
|
|
4
4
|
import { formatOption, logFormatted } from "#shared";
|
|
5
|
+
import { resolveOptionalTextInput, resolveRequiredTextInput } from "#gh/text-input";
|
|
5
6
|
|
|
6
7
|
import {
|
|
7
8
|
closeIssue,
|
|
@@ -91,6 +92,10 @@ export const issueCloseCommand = Command.make(
|
|
|
91
92
|
Flag.withDescription("Comment to add when closing"),
|
|
92
93
|
Flag.optional,
|
|
93
94
|
),
|
|
95
|
+
commentFile: Flag.string("comment-file").pipe(
|
|
96
|
+
Flag.withDescription("Read close comment from a file path or '-' for stdin"),
|
|
97
|
+
Flag.optional,
|
|
98
|
+
),
|
|
94
99
|
format: formatOption,
|
|
95
100
|
issue: Flag.integer("issue").pipe(Flag.withDescription("Issue number to close")),
|
|
96
101
|
reason: Flag.choice("reason", ["completed", "not planned"]).pipe(
|
|
@@ -98,10 +103,19 @@ export const issueCloseCommand = Command.make(
|
|
|
98
103
|
Flag.withDefault("completed"),
|
|
99
104
|
),
|
|
100
105
|
},
|
|
101
|
-
({ comment, format, issue, reason }) =>
|
|
106
|
+
({ comment, commentFile, format, issue, reason }) =>
|
|
102
107
|
Effect.gen(function* () {
|
|
108
|
+
const resolvedComment = yield* resolveOptionalTextInput(
|
|
109
|
+
"gh-tool issue close",
|
|
110
|
+
Option.getOrNull(comment),
|
|
111
|
+
Option.getOrNull(commentFile),
|
|
112
|
+
"--comment",
|
|
113
|
+
"--comment-file",
|
|
114
|
+
"comment",
|
|
115
|
+
);
|
|
116
|
+
|
|
103
117
|
const result = yield* closeIssue({
|
|
104
|
-
comment:
|
|
118
|
+
comment: resolvedComment,
|
|
105
119
|
issue,
|
|
106
120
|
reason,
|
|
107
121
|
});
|
|
@@ -116,13 +130,26 @@ export const issueReopenCommand = Command.make(
|
|
|
116
130
|
Flag.withDescription("Comment to add when reopening"),
|
|
117
131
|
Flag.optional,
|
|
118
132
|
),
|
|
133
|
+
commentFile: Flag.string("comment-file").pipe(
|
|
134
|
+
Flag.withDescription("Read reopen comment from a file path or '-' for stdin"),
|
|
135
|
+
Flag.optional,
|
|
136
|
+
),
|
|
119
137
|
format: formatOption,
|
|
120
138
|
issue: Flag.integer("issue").pipe(Flag.withDescription("Issue number to reopen")),
|
|
121
139
|
},
|
|
122
|
-
({ comment, format, issue }) =>
|
|
140
|
+
({ comment, commentFile, format, issue }) =>
|
|
123
141
|
Effect.gen(function* () {
|
|
142
|
+
const resolvedComment = yield* resolveOptionalTextInput(
|
|
143
|
+
"gh-tool issue reopen",
|
|
144
|
+
Option.getOrNull(comment),
|
|
145
|
+
Option.getOrNull(commentFile),
|
|
146
|
+
"--comment",
|
|
147
|
+
"--comment-file",
|
|
148
|
+
"comment",
|
|
149
|
+
);
|
|
150
|
+
|
|
124
151
|
const result = yield* reopenIssue({
|
|
125
|
-
comment:
|
|
152
|
+
comment: resolvedComment,
|
|
126
153
|
issue,
|
|
127
154
|
});
|
|
128
155
|
yield* logFormatted(result, format);
|
|
@@ -132,13 +159,26 @@ export const issueReopenCommand = Command.make(
|
|
|
132
159
|
export const issueCommentCommand = Command.make(
|
|
133
160
|
"comment",
|
|
134
161
|
{
|
|
135
|
-
body: Flag.string("body").pipe(Flag.withDescription("Comment body text")),
|
|
162
|
+
body: Flag.string("body").pipe(Flag.withDescription("Comment body text"), Flag.optional),
|
|
163
|
+
bodyFile: Flag.string("body-file").pipe(
|
|
164
|
+
Flag.withDescription("Read comment body from a file path or '-' for stdin"),
|
|
165
|
+
Flag.optional,
|
|
166
|
+
),
|
|
136
167
|
format: formatOption,
|
|
137
168
|
issue: Flag.integer("issue").pipe(Flag.withDescription("Issue number to comment on")),
|
|
138
169
|
},
|
|
139
|
-
({ body, format, issue }) =>
|
|
170
|
+
({ body, bodyFile, format, issue }) =>
|
|
140
171
|
Effect.gen(function* () {
|
|
141
|
-
const
|
|
172
|
+
const resolvedBody = yield* resolveRequiredTextInput(
|
|
173
|
+
"gh-tool issue comment",
|
|
174
|
+
Option.getOrNull(body),
|
|
175
|
+
Option.getOrNull(bodyFile),
|
|
176
|
+
"--body",
|
|
177
|
+
"--body-file",
|
|
178
|
+
"body",
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const result = yield* commentOnIssue({ body: resolvedBody, issue });
|
|
142
182
|
yield* logFormatted(result, format);
|
|
143
183
|
}),
|
|
144
184
|
).pipe(Command.withDescription("Post a comment on an issue"));
|
|
@@ -155,6 +195,10 @@ export const issueEditCommand = Command.make(
|
|
|
155
195
|
Flag.optional,
|
|
156
196
|
),
|
|
157
197
|
body: Flag.string("body").pipe(Flag.withDescription("New issue body"), Flag.optional),
|
|
198
|
+
bodyFile: Flag.string("body-file").pipe(
|
|
199
|
+
Flag.withDescription("Read issue body from a file path or '-' for stdin"),
|
|
200
|
+
Flag.optional,
|
|
201
|
+
),
|
|
158
202
|
format: formatOption,
|
|
159
203
|
issue: Flag.integer("issue").pipe(Flag.withDescription("Issue number to edit")),
|
|
160
204
|
removeAssignee: Flag.string("remove-assignee").pipe(
|
|
@@ -167,12 +211,31 @@ export const issueEditCommand = Command.make(
|
|
|
167
211
|
),
|
|
168
212
|
title: Flag.string("title").pipe(Flag.withDescription("New issue title"), Flag.optional),
|
|
169
213
|
},
|
|
170
|
-
({
|
|
214
|
+
({
|
|
215
|
+
addAssignee,
|
|
216
|
+
addLabels,
|
|
217
|
+
body,
|
|
218
|
+
bodyFile,
|
|
219
|
+
format,
|
|
220
|
+
issue,
|
|
221
|
+
removeAssignee,
|
|
222
|
+
removeLabels,
|
|
223
|
+
title,
|
|
224
|
+
}) =>
|
|
171
225
|
Effect.gen(function* () {
|
|
226
|
+
const resolvedBody = yield* resolveOptionalTextInput(
|
|
227
|
+
"gh-tool issue edit",
|
|
228
|
+
Option.getOrNull(body),
|
|
229
|
+
Option.getOrNull(bodyFile),
|
|
230
|
+
"--body",
|
|
231
|
+
"--body-file",
|
|
232
|
+
"body",
|
|
233
|
+
);
|
|
234
|
+
|
|
172
235
|
const result = yield* editIssue({
|
|
173
236
|
addAssignee: Option.getOrNull(addAssignee),
|
|
174
237
|
addLabels: Option.getOrNull(addLabels),
|
|
175
|
-
body:
|
|
238
|
+
body: resolvedBody,
|
|
176
239
|
issue,
|
|
177
240
|
removeAssignee: Option.getOrNull(removeAssignee),
|
|
178
241
|
removeLabels: Option.getOrNull(removeLabels),
|
|
@@ -4,6 +4,11 @@ import { Effect, Option } from "effect";
|
|
|
4
4
|
import type { PRStatusResult } from "#gh/types";
|
|
5
5
|
|
|
6
6
|
import { formatOption, logFormatted } from "#shared";
|
|
7
|
+
import {
|
|
8
|
+
resolveDefaultTextInput,
|
|
9
|
+
resolveOptionalTextInput,
|
|
10
|
+
resolveRequiredTextInput,
|
|
11
|
+
} from "#gh/text-input";
|
|
7
12
|
import {
|
|
8
13
|
CI_CHECK_WATCH_TIMEOUT_MS,
|
|
9
14
|
DEFAULT_DELETE_BRANCH,
|
|
@@ -16,6 +21,7 @@ import {
|
|
|
16
21
|
detectPRStatus,
|
|
17
22
|
editPR,
|
|
18
23
|
fetchChecks,
|
|
24
|
+
fetchChecksForCommand,
|
|
19
25
|
fetchFailedChecks,
|
|
20
26
|
mergePR,
|
|
21
27
|
rerunChecks,
|
|
@@ -70,9 +76,10 @@ export const prCreateCommand = Command.make(
|
|
|
70
76
|
Flag.withDescription("Base branch for the PR"),
|
|
71
77
|
Flag.withDefault("test"),
|
|
72
78
|
),
|
|
73
|
-
body: Flag.string("body").pipe(
|
|
74
|
-
|
|
75
|
-
Flag.
|
|
79
|
+
body: Flag.string("body").pipe(Flag.withDescription("PR body/description"), Flag.optional),
|
|
80
|
+
bodyFile: Flag.string("body-file").pipe(
|
|
81
|
+
Flag.withDescription("Read PR body from a file path or '-' for stdin"),
|
|
82
|
+
Flag.optional,
|
|
76
83
|
),
|
|
77
84
|
draft: Flag.boolean("draft").pipe(
|
|
78
85
|
Flag.withDescription("Create as draft PR"),
|
|
@@ -85,11 +92,21 @@ export const prCreateCommand = Command.make(
|
|
|
85
92
|
),
|
|
86
93
|
title: Flag.string("title").pipe(Flag.withDescription("PR title")),
|
|
87
94
|
},
|
|
88
|
-
({ base, body, draft, format, head, title }) =>
|
|
95
|
+
({ base, body, bodyFile, draft, format, head, title }) =>
|
|
89
96
|
Effect.gen(function* () {
|
|
97
|
+
const resolvedBody = yield* resolveDefaultTextInput(
|
|
98
|
+
"gh-tool pr create",
|
|
99
|
+
Option.getOrNull(body),
|
|
100
|
+
Option.getOrNull(bodyFile),
|
|
101
|
+
"--body",
|
|
102
|
+
"--body-file",
|
|
103
|
+
"body",
|
|
104
|
+
"",
|
|
105
|
+
);
|
|
106
|
+
|
|
90
107
|
const info = yield* createPR({
|
|
91
108
|
base,
|
|
92
|
-
body,
|
|
109
|
+
body: resolvedBody,
|
|
93
110
|
draft,
|
|
94
111
|
head: Option.getOrNull(head),
|
|
95
112
|
title,
|
|
@@ -102,16 +119,29 @@ export const prEditCommand = Command.make(
|
|
|
102
119
|
"edit",
|
|
103
120
|
{
|
|
104
121
|
body: Flag.string("body").pipe(Flag.withDescription("New PR body/description"), Flag.optional),
|
|
122
|
+
bodyFile: Flag.string("body-file").pipe(
|
|
123
|
+
Flag.withDescription("Read PR body from a file path or '-' for stdin"),
|
|
124
|
+
Flag.optional,
|
|
125
|
+
),
|
|
105
126
|
format: formatOption,
|
|
106
127
|
pr: Flag.integer("pr").pipe(Flag.withDescription("PR number to edit")),
|
|
107
128
|
title: Flag.string("title").pipe(Flag.withDescription("New PR title"), Flag.optional),
|
|
108
129
|
},
|
|
109
|
-
({ body, format, pr, title }) =>
|
|
130
|
+
({ body, bodyFile, format, pr, title }) =>
|
|
110
131
|
Effect.gen(function* () {
|
|
132
|
+
const resolvedBody = yield* resolveOptionalTextInput(
|
|
133
|
+
"gh-tool pr edit",
|
|
134
|
+
Option.getOrNull(body),
|
|
135
|
+
Option.getOrNull(bodyFile),
|
|
136
|
+
"--body",
|
|
137
|
+
"--body-file",
|
|
138
|
+
"body",
|
|
139
|
+
);
|
|
140
|
+
|
|
111
141
|
const info = yield* editPR({
|
|
112
142
|
pr,
|
|
113
143
|
title: Option.getOrNull(title),
|
|
114
|
-
body:
|
|
144
|
+
body: resolvedBody,
|
|
115
145
|
});
|
|
116
146
|
yield* logFormatted(info, format);
|
|
117
147
|
}),
|
|
@@ -171,7 +201,7 @@ export const prChecksCommand = Command.make(
|
|
|
171
201
|
({ failFast, format, pr, timeout, watch }) =>
|
|
172
202
|
Effect.gen(function* () {
|
|
173
203
|
const prNumber = Option.getOrNull(pr);
|
|
174
|
-
const checks = yield*
|
|
204
|
+
const checks = yield* fetchChecksForCommand(prNumber, watch, failFast, timeout);
|
|
175
205
|
yield* logFormatted(checks, format);
|
|
176
206
|
}),
|
|
177
207
|
).pipe(Command.withDescription("Fetch CI check status for a PR (optionally watch with timeout)"));
|
|
@@ -338,17 +368,32 @@ export const prIssueCommentsLatestCommand = Command.make(
|
|
|
338
368
|
export const prCommentCommand = Command.make(
|
|
339
369
|
"comment",
|
|
340
370
|
{
|
|
341
|
-
body: Flag.string("body").pipe(
|
|
371
|
+
body: Flag.string("body").pipe(
|
|
372
|
+
Flag.withDescription("General PR comment body text"),
|
|
373
|
+
Flag.optional,
|
|
374
|
+
),
|
|
375
|
+
bodyFile: Flag.string("body-file").pipe(
|
|
376
|
+
Flag.withDescription("Read general PR comment body from a file path or '-' for stdin"),
|
|
377
|
+
Flag.optional,
|
|
378
|
+
),
|
|
342
379
|
format: formatOption,
|
|
343
380
|
pr: Flag.integer("pr").pipe(
|
|
344
381
|
Flag.withDescription("PR number (default: current branch PR)"),
|
|
345
382
|
Flag.optional,
|
|
346
383
|
),
|
|
347
384
|
},
|
|
348
|
-
({ body, format, pr }) =>
|
|
385
|
+
({ body, bodyFile, format, pr }) =>
|
|
349
386
|
Effect.gen(function* () {
|
|
350
387
|
const prNumber = Option.getOrNull(pr);
|
|
351
|
-
const
|
|
388
|
+
const resolvedBody = yield* resolveRequiredTextInput(
|
|
389
|
+
"gh-tool pr comment",
|
|
390
|
+
Option.getOrNull(body),
|
|
391
|
+
Option.getOrNull(bodyFile),
|
|
392
|
+
"--body",
|
|
393
|
+
"--body-file",
|
|
394
|
+
"body",
|
|
395
|
+
);
|
|
396
|
+
const result = yield* postIssueComment(prNumber, resolvedBody);
|
|
352
397
|
yield* logFormatted(result, format);
|
|
353
398
|
}),
|
|
354
399
|
).pipe(Command.withDescription("Post a general PR discussion comment"));
|
|
@@ -375,7 +420,11 @@ export const prDiscussionSummaryCommand = Command.make(
|
|
|
375
420
|
export const prReplyCommand = Command.make(
|
|
376
421
|
"reply",
|
|
377
422
|
{
|
|
378
|
-
body: Flag.string("body").pipe(Flag.withDescription("Reply body text")),
|
|
423
|
+
body: Flag.string("body").pipe(Flag.withDescription("Reply body text"), Flag.optional),
|
|
424
|
+
bodyFile: Flag.string("body-file").pipe(
|
|
425
|
+
Flag.withDescription("Read reply body from a file path or '-' for stdin"),
|
|
426
|
+
Flag.optional,
|
|
427
|
+
),
|
|
379
428
|
commentId: Flag.integer("comment-id").pipe(
|
|
380
429
|
Flag.withDescription("ID of the comment to reply to"),
|
|
381
430
|
),
|
|
@@ -385,10 +434,18 @@ export const prReplyCommand = Command.make(
|
|
|
385
434
|
Flag.optional,
|
|
386
435
|
),
|
|
387
436
|
},
|
|
388
|
-
({ body, commentId, format, pr }) =>
|
|
437
|
+
({ body, bodyFile, commentId, format, pr }) =>
|
|
389
438
|
Effect.gen(function* () {
|
|
390
439
|
const prNumber = Option.getOrNull(pr);
|
|
391
|
-
const
|
|
440
|
+
const resolvedBody = yield* resolveRequiredTextInput(
|
|
441
|
+
"gh-tool pr reply",
|
|
442
|
+
Option.getOrNull(body),
|
|
443
|
+
Option.getOrNull(bodyFile),
|
|
444
|
+
"--body",
|
|
445
|
+
"--body-file",
|
|
446
|
+
"body",
|
|
447
|
+
);
|
|
448
|
+
const result = yield* replyToComment(prNumber, commentId, resolvedBody);
|
|
392
449
|
yield* logFormatted(result, format);
|
|
393
450
|
}),
|
|
394
451
|
).pipe(Command.withDescription("Reply to an inline review comment"));
|
|
@@ -415,6 +472,10 @@ export const prSubmitReviewCommand = Command.make(
|
|
|
415
472
|
Flag.withDescription("Optional review body text when submitting"),
|
|
416
473
|
Flag.optional,
|
|
417
474
|
),
|
|
475
|
+
bodyFile: Flag.string("body-file").pipe(
|
|
476
|
+
Flag.withDescription("Read review body from a file path or '-' for stdin"),
|
|
477
|
+
Flag.optional,
|
|
478
|
+
),
|
|
418
479
|
format: formatOption,
|
|
419
480
|
pr: Flag.integer("pr").pipe(
|
|
420
481
|
Flag.withDescription("PR number (default: current branch PR)"),
|
|
@@ -427,11 +488,18 @@ export const prSubmitReviewCommand = Command.make(
|
|
|
427
488
|
Flag.optional,
|
|
428
489
|
),
|
|
429
490
|
},
|
|
430
|
-
({ body, format, pr, reviewId }) =>
|
|
491
|
+
({ body, bodyFile, format, pr, reviewId }) =>
|
|
431
492
|
Effect.gen(function* () {
|
|
432
493
|
const prNumber = Option.getOrNull(pr);
|
|
433
494
|
const reviewIdValue = Option.getOrNull(reviewId);
|
|
434
|
-
const bodyValue =
|
|
495
|
+
const bodyValue = yield* resolveOptionalTextInput(
|
|
496
|
+
"gh-tool pr submit-review",
|
|
497
|
+
Option.getOrNull(body),
|
|
498
|
+
Option.getOrNull(bodyFile),
|
|
499
|
+
"--body",
|
|
500
|
+
"--body-file",
|
|
501
|
+
"body",
|
|
502
|
+
);
|
|
435
503
|
const result = yield* submitPendingReview(prNumber, reviewIdValue, bodyValue);
|
|
436
504
|
yield* logFormatted(result, format);
|
|
437
505
|
}),
|
|
@@ -471,7 +539,11 @@ export const prReviewTriageCommand = Command.make(
|
|
|
471
539
|
export const prReplyAndResolveCommand = Command.make(
|
|
472
540
|
"reply-and-resolve",
|
|
473
541
|
{
|
|
474
|
-
body: Flag.string("body").pipe(Flag.withDescription("Reply body text")),
|
|
542
|
+
body: Flag.string("body").pipe(Flag.withDescription("Reply body text"), Flag.optional),
|
|
543
|
+
bodyFile: Flag.string("body-file").pipe(
|
|
544
|
+
Flag.withDescription("Read reply body from a file path or '-' for stdin"),
|
|
545
|
+
Flag.optional,
|
|
546
|
+
),
|
|
475
547
|
commentId: Flag.integer("comment-id").pipe(
|
|
476
548
|
Flag.withDescription("ID of the comment to reply to"),
|
|
477
549
|
),
|
|
@@ -484,10 +556,18 @@ export const prReplyAndResolveCommand = Command.make(
|
|
|
484
556
|
Flag.withDescription("GraphQL node ID of the thread to resolve"),
|
|
485
557
|
),
|
|
486
558
|
},
|
|
487
|
-
({ body, commentId, format, pr, threadId }) =>
|
|
559
|
+
({ body, bodyFile, commentId, format, pr, threadId }) =>
|
|
488
560
|
Effect.gen(function* () {
|
|
489
561
|
const prNumber = Option.getOrNull(pr);
|
|
490
|
-
const
|
|
562
|
+
const resolvedBody = yield* resolveRequiredTextInput(
|
|
563
|
+
"gh-tool pr reply-and-resolve",
|
|
564
|
+
Option.getOrNull(body),
|
|
565
|
+
Option.getOrNull(bodyFile),
|
|
566
|
+
"--body",
|
|
567
|
+
"--body-file",
|
|
568
|
+
"body",
|
|
569
|
+
);
|
|
570
|
+
const replyResult = yield* replyToComment(prNumber, commentId, resolvedBody);
|
|
491
571
|
const resolveResult = yield* resolveThread(threadId);
|
|
492
572
|
yield* logFormatted({ reply: replyResult, resolve: resolveResult }, format);
|
|
493
573
|
}),
|
package/src/gh-tool/pr/core.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { Console, Effect, Option } from "effect";
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
BranchPRDetail,
|
|
5
|
+
CheckResult,
|
|
6
|
+
FailedCheckDetail,
|
|
7
|
+
FailedCheckRunContext,
|
|
8
|
+
MergeResult,
|
|
9
|
+
MergeStrategy,
|
|
10
|
+
PRInfo,
|
|
11
|
+
WorkflowRunDetail,
|
|
12
|
+
} from "#gh/types";
|
|
4
13
|
|
|
5
14
|
import { GitHubCommandError, GitHubMergeError, GitHubTimeoutError } from "#gh/errors";
|
|
6
15
|
import { GitHubService } from "#gh/service";
|
|
@@ -8,6 +17,174 @@ import { GitHubService } from "#gh/service";
|
|
|
8
17
|
import type { ButStatusJson, PRViewJsonResult } from "./helpers";
|
|
9
18
|
import { runLocalCommand } from "./helpers";
|
|
10
19
|
|
|
20
|
+
const CHECK_JSON_FIELDS = "name,state,bucket,link";
|
|
21
|
+
const GITHUB_ACTIONS_RUN_ID_RE = /github\.com\/[^/]+\/[^/]+\/actions\/runs\/(\d+)/;
|
|
22
|
+
|
|
23
|
+
const buildChecksCommand = (pr: number | null, includeWatch: boolean): string =>
|
|
24
|
+
`bun agent-tools-gh pr checks${pr !== null ? ` --pr ${pr}` : ""}${includeWatch ? " --watch" : ""}`;
|
|
25
|
+
|
|
26
|
+
const buildChecksFailedCommand = (pr: number | null): string =>
|
|
27
|
+
`bun agent-tools-gh pr checks-failed${pr !== null ? ` --pr ${pr}` : ""}`;
|
|
28
|
+
|
|
29
|
+
const extractRunIdFromCheckLink = (link: string): number | null => {
|
|
30
|
+
const match = link.match(GITHUB_ACTIONS_RUN_ID_RE);
|
|
31
|
+
if (!match?.[1]) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const runId = Number(match[1]);
|
|
36
|
+
return Number.isFinite(runId) ? runId : null;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const fetchWorkflowRunFailureContext = Effect.fn("pr.fetchWorkflowRunFailureContext")(function* (
|
|
40
|
+
runId: number,
|
|
41
|
+
) {
|
|
42
|
+
const gh = yield* GitHubService;
|
|
43
|
+
|
|
44
|
+
const run = yield* gh
|
|
45
|
+
.runGhJson<WorkflowRunDetail>([
|
|
46
|
+
"run",
|
|
47
|
+
"view",
|
|
48
|
+
String(runId),
|
|
49
|
+
"--json",
|
|
50
|
+
"databaseId,url,workflowName,status,conclusion,jobs",
|
|
51
|
+
])
|
|
52
|
+
.pipe(Effect.catchTag("GitHubCommandError", () => Effect.succeed(null)));
|
|
53
|
+
|
|
54
|
+
if (run === null) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const failedJobs = run.jobs
|
|
59
|
+
.filter((job) => job.conclusion === "failure" || job.status === "failure")
|
|
60
|
+
.map((job) => ({
|
|
61
|
+
name: job.name,
|
|
62
|
+
status: job.status,
|
|
63
|
+
conclusion: job.conclusion,
|
|
64
|
+
url: job.url,
|
|
65
|
+
failedSteps: job.steps
|
|
66
|
+
.filter((step) => step.conclusion === "failure" || step.status === "failure")
|
|
67
|
+
.map((step) => step.name),
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
const context: FailedCheckRunContext = {
|
|
71
|
+
runId: run.databaseId,
|
|
72
|
+
url: run.url,
|
|
73
|
+
workflowName: run.workflowName,
|
|
74
|
+
status: run.status,
|
|
75
|
+
conclusion: run.conclusion,
|
|
76
|
+
failedJobs,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return context;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const fetchCheckResults = Effect.fn("pr.fetchCheckResults")(function* (pr: number | null) {
|
|
83
|
+
const gh = yield* GitHubService;
|
|
84
|
+
|
|
85
|
+
const args = ["pr", "checks"];
|
|
86
|
+
if (pr !== null) {
|
|
87
|
+
args.push(String(pr));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return yield* gh.runGhJson<CheckResult[]>([...args, "--json", CHECK_JSON_FIELDS]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const buildFailedChecksReport = Effect.fn("pr.buildFailedChecksReport")(function* (
|
|
94
|
+
pr: number | null,
|
|
95
|
+
checks: CheckResult[],
|
|
96
|
+
) {
|
|
97
|
+
const failedChecks = checks.filter((check) => check.bucket === "fail");
|
|
98
|
+
const pendingChecks = checks.filter((check) => check.bucket === "pending");
|
|
99
|
+
const passedChecks = checks.filter((check) => check.bucket === "pass");
|
|
100
|
+
|
|
101
|
+
const runIds = [
|
|
102
|
+
...new Set(
|
|
103
|
+
failedChecks
|
|
104
|
+
.map((check) => extractRunIdFromCheckLink(check.link))
|
|
105
|
+
.filter((id) => id !== null),
|
|
106
|
+
),
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
const runContexts = new Map<number, FailedCheckRunContext | null>();
|
|
110
|
+
const contexts = yield* Effect.forEach(
|
|
111
|
+
runIds,
|
|
112
|
+
(runId) =>
|
|
113
|
+
fetchWorkflowRunFailureContext(runId).pipe(
|
|
114
|
+
Effect.map((context) => [runId, context] as const),
|
|
115
|
+
),
|
|
116
|
+
{ concurrency: "unbounded" },
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
for (const [runId, context] of contexts) {
|
|
120
|
+
runContexts.set(runId, context);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const enrichedFailedChecks: FailedCheckDetail[] = failedChecks.map((check) => {
|
|
124
|
+
const runId = extractRunIdFromCheckLink(check.link);
|
|
125
|
+
return {
|
|
126
|
+
...check,
|
|
127
|
+
runId,
|
|
128
|
+
run: runId === null ? null : (runContexts.get(runId) ?? null),
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const nextCommands = [
|
|
133
|
+
buildChecksFailedCommand(pr),
|
|
134
|
+
...new Set(
|
|
135
|
+
enrichedFailedChecks.flatMap((check) => {
|
|
136
|
+
if (check.runId === null) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const commands = [`bun agent-tools-gh workflow view --run ${check.runId}`];
|
|
141
|
+
const firstFailedJob = check.run?.failedJobs[0];
|
|
142
|
+
if (firstFailedJob) {
|
|
143
|
+
commands.push(
|
|
144
|
+
`bun agent-tools-gh workflow job-logs --run ${check.runId} --job ${JSON.stringify(firstFailedJob.name)} --failed-steps-only`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return commands;
|
|
149
|
+
}),
|
|
150
|
+
),
|
|
151
|
+
...(pendingChecks.length > 0 ? [buildChecksCommand(pr, true)] : []),
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
const message =
|
|
155
|
+
failedChecks.length === 0
|
|
156
|
+
? pendingChecks.length > 0
|
|
157
|
+
? `No failed checks yet; ${pendingChecks.length} check(s) are still running.`
|
|
158
|
+
: "No failed checks detected."
|
|
159
|
+
: pendingChecks.length > 0
|
|
160
|
+
? `Detected ${failedChecks.length} failed check(s) while ${pendingChecks.length} check(s) are still running.`
|
|
161
|
+
: `Detected ${failedChecks.length} failed check(s).`;
|
|
162
|
+
|
|
163
|
+
const hint =
|
|
164
|
+
failedChecks.length === 0
|
|
165
|
+
? pendingChecks.length > 0
|
|
166
|
+
? "Wait for the remaining checks to finish, or use --watch to block until CI settles."
|
|
167
|
+
: "All current checks are green."
|
|
168
|
+
: pendingChecks.length > 0
|
|
169
|
+
? "Inspect the failed workflow run first. Other checks are still running and may change overall merge readiness."
|
|
170
|
+
: "Inspect the failed workflow run and failed job logs to get the first concrete error, then rerun only if the failure is understood.";
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
status: failedChecks.length > 0 ? "failed" : "no_failures",
|
|
174
|
+
message,
|
|
175
|
+
summary: {
|
|
176
|
+
total: checks.length,
|
|
177
|
+
failed: failedChecks.length,
|
|
178
|
+
pending: pendingChecks.length,
|
|
179
|
+
passed: passedChecks.length,
|
|
180
|
+
},
|
|
181
|
+
failedChecks: enrichedFailedChecks,
|
|
182
|
+
pendingChecks,
|
|
183
|
+
hint,
|
|
184
|
+
nextCommands,
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
|
|
11
188
|
export const viewPR = Effect.fn("pr.viewPR")(function* (prNumber: number | null) {
|
|
12
189
|
const gh = yield* GitHubService;
|
|
13
190
|
|
|
@@ -446,22 +623,50 @@ export const fetchChecks = Effect.fn("pr.fetchChecks")(function* (
|
|
|
446
623
|
}),
|
|
447
624
|
);
|
|
448
625
|
|
|
449
|
-
return yield*
|
|
626
|
+
return yield* fetchCheckResults(pr);
|
|
450
627
|
}
|
|
451
628
|
|
|
452
|
-
const results = yield*
|
|
629
|
+
const results = yield* fetchCheckResults(pr);
|
|
453
630
|
if (results.some((c) => c.bucket === "pending")) {
|
|
454
631
|
yield* Console.warn(
|
|
455
632
|
`ℹ️ Some checks are still running. Prefer --watch to block until completion instead of polling:\n` +
|
|
456
|
-
`
|
|
633
|
+
` ${buildChecksCommand(pr, true)}`,
|
|
457
634
|
);
|
|
458
635
|
}
|
|
459
636
|
return results;
|
|
460
637
|
});
|
|
461
638
|
|
|
462
639
|
export const fetchFailedChecks = Effect.fn("pr.fetchFailedChecks")(function* (pr: number | null) {
|
|
463
|
-
const checks = yield*
|
|
464
|
-
return
|
|
640
|
+
const checks = yield* fetchCheckResults(pr);
|
|
641
|
+
return yield* buildFailedChecksReport(pr, checks);
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
export const fetchChecksForCommand = Effect.fn("pr.fetchChecksForCommand")(function* (
|
|
645
|
+
pr: number | null,
|
|
646
|
+
watch: boolean,
|
|
647
|
+
failFast: boolean,
|
|
648
|
+
timeoutSeconds: number,
|
|
649
|
+
) {
|
|
650
|
+
if (!watch) {
|
|
651
|
+
return yield* fetchChecks(pr, false, failFast, timeoutSeconds);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const watchedChecks = yield* fetchChecks(pr, true, failFast, timeoutSeconds).pipe(
|
|
655
|
+
Effect.catchTag("GitHubCommandError", (error) =>
|
|
656
|
+
Effect.succeed({ _tag: "command_error" as const, error }),
|
|
657
|
+
),
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
if (Array.isArray(watchedChecks)) {
|
|
661
|
+
return watchedChecks;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const finalChecks = yield* fetchCheckResults(pr);
|
|
665
|
+
if (finalChecks.some((check) => check.bucket === "fail")) {
|
|
666
|
+
return yield* buildFailedChecksReport(pr, finalChecks);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return yield* Effect.fail(watchedChecks.error);
|
|
465
670
|
});
|
|
466
671
|
|
|
467
672
|
export const rerunChecks = Effect.fn("pr.rerunChecks")(function* (
|
package/src/gh-tool/release.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Command, Flag } from "effect/unstable/cli";
|
|
|
2
2
|
import { Effect, Option } from "effect";
|
|
3
3
|
|
|
4
4
|
import { formatOption, logFormatted } from "#shared";
|
|
5
|
+
import { resolveOptionalTextInput } from "#gh/text-input";
|
|
5
6
|
import { GitHubService } from "./service";
|
|
6
7
|
|
|
7
8
|
type ReleaseListItem = {
|
|
@@ -343,6 +344,10 @@ export const releaseCreateCommand = Command.make(
|
|
|
343
344
|
Flag.withDescription("Release notes body (markdown)"),
|
|
344
345
|
Flag.optional,
|
|
345
346
|
),
|
|
347
|
+
bodyFile: Flag.string("body-file").pipe(
|
|
348
|
+
Flag.withDescription("Read release notes body from a file path or '-' for stdin"),
|
|
349
|
+
Flag.optional,
|
|
350
|
+
),
|
|
346
351
|
draft: Flag.boolean("draft").pipe(
|
|
347
352
|
Flag.withDescription("Create as draft release"),
|
|
348
353
|
Flag.withDefault(false),
|
|
@@ -388,6 +393,7 @@ export const releaseCreateCommand = Command.make(
|
|
|
388
393
|
},
|
|
389
394
|
({
|
|
390
395
|
body,
|
|
396
|
+
bodyFile,
|
|
391
397
|
draft,
|
|
392
398
|
format,
|
|
393
399
|
generateNotes,
|
|
@@ -402,10 +408,19 @@ export const releaseCreateCommand = Command.make(
|
|
|
402
408
|
verifyTag,
|
|
403
409
|
}) =>
|
|
404
410
|
Effect.gen(function* () {
|
|
411
|
+
const resolvedBody = yield* resolveOptionalTextInput(
|
|
412
|
+
"gh-tool release create",
|
|
413
|
+
Option.getOrNull(body),
|
|
414
|
+
Option.getOrNull(bodyFile),
|
|
415
|
+
"--body",
|
|
416
|
+
"--body-file",
|
|
417
|
+
"body",
|
|
418
|
+
);
|
|
419
|
+
|
|
405
420
|
const result = yield* createRelease({
|
|
406
421
|
tag,
|
|
407
422
|
title: Option.getOrNull(title),
|
|
408
|
-
body:
|
|
423
|
+
body: resolvedBody,
|
|
409
424
|
notesFile: Option.getOrNull(notesFile),
|
|
410
425
|
draft,
|
|
411
426
|
prerelease,
|
|
@@ -477,6 +492,10 @@ export const releaseEditCommand = Command.make(
|
|
|
477
492
|
Flag.withDescription("New release notes body (markdown)"),
|
|
478
493
|
Flag.optional,
|
|
479
494
|
),
|
|
495
|
+
bodyFile: Flag.string("body-file").pipe(
|
|
496
|
+
Flag.withDescription("Read release notes body from a file path or '-' for stdin"),
|
|
497
|
+
Flag.optional,
|
|
498
|
+
),
|
|
480
499
|
draft: Flag.boolean("draft").pipe(
|
|
481
500
|
Flag.withDescription("Set draft status (true/false). Omit to keep current value"),
|
|
482
501
|
Flag.optional,
|
|
@@ -497,12 +516,21 @@ export const releaseEditCommand = Command.make(
|
|
|
497
516
|
tag: Flag.string("tag").pipe(Flag.withDescription("Release tag to edit (e.g., v1.2.3)")),
|
|
498
517
|
title: Flag.string("title").pipe(Flag.withDescription("New release title"), Flag.optional),
|
|
499
518
|
},
|
|
500
|
-
({ body, draft, format, latest, prerelease, repo, tag, title }) =>
|
|
519
|
+
({ body, bodyFile, draft, format, latest, prerelease, repo, tag, title }) =>
|
|
501
520
|
Effect.gen(function* () {
|
|
521
|
+
const resolvedBody = yield* resolveOptionalTextInput(
|
|
522
|
+
"gh-tool release edit",
|
|
523
|
+
Option.getOrNull(body),
|
|
524
|
+
Option.getOrNull(bodyFile),
|
|
525
|
+
"--body",
|
|
526
|
+
"--body-file",
|
|
527
|
+
"body",
|
|
528
|
+
);
|
|
529
|
+
|
|
502
530
|
const edited = yield* editRelease({
|
|
503
531
|
tag,
|
|
504
532
|
title: Option.getOrNull(title),
|
|
505
|
-
body:
|
|
533
|
+
body: resolvedBody,
|
|
506
534
|
draft: Option.getOrNull(draft),
|
|
507
535
|
prerelease: Option.getOrNull(prerelease),
|
|
508
536
|
latest: Option.getOrNull(latest),
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
import { GitHubCommandError } from "#gh/errors";
|
|
4
|
+
|
|
5
|
+
const STDIN_SENTINEL = "-";
|
|
6
|
+
const SENSITIVE_PATH_PATTERNS = [/\.env(\..+)?$/, /\.envrc$/, /\.(pem|key|p12|pfx|cer|crt)$/i];
|
|
7
|
+
const MissingMode = Schema.Literals(["error", "null", "default"]);
|
|
8
|
+
|
|
9
|
+
const readTextFromStdin = () => Bun.stdin.text();
|
|
10
|
+
|
|
11
|
+
const readTextFile = (filePath: string) => {
|
|
12
|
+
if (SENSITIVE_PATH_PATTERNS.some((pattern) => pattern.test(filePath))) {
|
|
13
|
+
return Promise.reject(new Error(`Refusing to read sensitive file: ${filePath}`));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return Bun.file(filePath).text();
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const ensureResolvedText = (resolvedValue: string | null, context: string) => {
|
|
20
|
+
if (resolvedValue === null) {
|
|
21
|
+
throw new Error(`Invariant violation: ${context} resolved to null`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return resolvedValue;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type ResolveTextInputOptions = {
|
|
28
|
+
command: string;
|
|
29
|
+
value: string | null;
|
|
30
|
+
fileValue: string | null;
|
|
31
|
+
valueFlag: string;
|
|
32
|
+
fileFlag: string;
|
|
33
|
+
missingMode: Schema.Schema.Type<typeof MissingMode>;
|
|
34
|
+
missingValue?: string;
|
|
35
|
+
label: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const resolveTextInputInternal = Effect.fn("gh.resolveTextInputInternal")(function* (
|
|
39
|
+
options: ResolveTextInputOptions,
|
|
40
|
+
) {
|
|
41
|
+
const { command, fileFlag, fileValue, label, missingMode, missingValue, value, valueFlag } =
|
|
42
|
+
options;
|
|
43
|
+
|
|
44
|
+
if (value !== null && fileValue !== null) {
|
|
45
|
+
return yield* Effect.fail(
|
|
46
|
+
new GitHubCommandError({
|
|
47
|
+
command,
|
|
48
|
+
exitCode: 1,
|
|
49
|
+
stderr: `Provide exactly one of ${valueFlag} or ${fileFlag}`,
|
|
50
|
+
message: `Provide exactly one of ${valueFlag} or ${fileFlag}`,
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (value !== null) {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (fileValue !== null) {
|
|
60
|
+
const source = fileValue === STDIN_SENTINEL ? "stdin" : fileValue;
|
|
61
|
+
|
|
62
|
+
return yield* Effect.tryPromise({
|
|
63
|
+
try: () => (fileValue === STDIN_SENTINEL ? readTextFromStdin() : readTextFile(fileValue)),
|
|
64
|
+
catch: (error) =>
|
|
65
|
+
new GitHubCommandError({
|
|
66
|
+
command,
|
|
67
|
+
exitCode: 1,
|
|
68
|
+
stderr: `Failed to read ${label} from ${source}: ${error instanceof Error ? error.message : String(error)}`,
|
|
69
|
+
message: `Failed to read ${label} from ${source}: ${error instanceof Error ? error.message : String(error)}`,
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (missingMode === "null") {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (missingMode === "default") {
|
|
79
|
+
return missingValue ?? "";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return yield* Effect.fail(
|
|
83
|
+
new GitHubCommandError({
|
|
84
|
+
command,
|
|
85
|
+
exitCode: 1,
|
|
86
|
+
stderr: `Missing ${label}. Provide ${valueFlag} or ${fileFlag}`,
|
|
87
|
+
message: `Missing ${label}. Provide ${valueFlag} or ${fileFlag}`,
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
export const resolveRequiredTextInput = (
|
|
93
|
+
command: string,
|
|
94
|
+
value: string | null,
|
|
95
|
+
fileValue: string | null,
|
|
96
|
+
valueFlag: string,
|
|
97
|
+
fileFlag: string,
|
|
98
|
+
label: string,
|
|
99
|
+
): Effect.Effect<string, GitHubCommandError> =>
|
|
100
|
+
resolveTextInputInternal({
|
|
101
|
+
command,
|
|
102
|
+
value,
|
|
103
|
+
fileValue,
|
|
104
|
+
valueFlag,
|
|
105
|
+
fileFlag,
|
|
106
|
+
missingMode: "error",
|
|
107
|
+
label,
|
|
108
|
+
}).pipe(Effect.map((resolvedValue) => ensureResolvedText(resolvedValue, "required text input")));
|
|
109
|
+
|
|
110
|
+
export const resolveOptionalTextInput = (
|
|
111
|
+
command: string,
|
|
112
|
+
value: string | null,
|
|
113
|
+
fileValue: string | null,
|
|
114
|
+
valueFlag: string,
|
|
115
|
+
fileFlag: string,
|
|
116
|
+
label: string,
|
|
117
|
+
): Effect.Effect<string | null, GitHubCommandError> =>
|
|
118
|
+
resolveTextInputInternal({
|
|
119
|
+
command,
|
|
120
|
+
value,
|
|
121
|
+
fileValue,
|
|
122
|
+
valueFlag,
|
|
123
|
+
fileFlag,
|
|
124
|
+
missingMode: "null",
|
|
125
|
+
label,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
export const resolveDefaultTextInput = (
|
|
129
|
+
command: string,
|
|
130
|
+
value: string | null,
|
|
131
|
+
fileValue: string | null,
|
|
132
|
+
valueFlag: string,
|
|
133
|
+
fileFlag: string,
|
|
134
|
+
label: string,
|
|
135
|
+
defaultValue: string,
|
|
136
|
+
): Effect.Effect<string, GitHubCommandError> =>
|
|
137
|
+
resolveTextInputInternal({
|
|
138
|
+
command,
|
|
139
|
+
value,
|
|
140
|
+
fileValue,
|
|
141
|
+
valueFlag,
|
|
142
|
+
fileFlag,
|
|
143
|
+
missingMode: "default",
|
|
144
|
+
missingValue: defaultValue,
|
|
145
|
+
label,
|
|
146
|
+
}).pipe(Effect.map((resolvedValue) => ensureResolvedText(resolvedValue, "default text input")));
|
package/src/gh-tool/types.ts
CHANGED
|
@@ -62,6 +62,62 @@ export type CheckResult = {
|
|
|
62
62
|
link: string;
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
+
export type FailedCheckJob = {
|
|
66
|
+
name: string;
|
|
67
|
+
status: string;
|
|
68
|
+
conclusion: string | null;
|
|
69
|
+
url: string;
|
|
70
|
+
failedSteps: string[];
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type FailedCheckRunContext = {
|
|
74
|
+
runId: number;
|
|
75
|
+
url: string | null;
|
|
76
|
+
workflowName: string | null;
|
|
77
|
+
status: string;
|
|
78
|
+
conclusion: string | null;
|
|
79
|
+
failedJobs: FailedCheckJob[];
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export type FailedCheckDetail = CheckResult & {
|
|
83
|
+
runId: number | null;
|
|
84
|
+
run: FailedCheckRunContext | null;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export type FailedChecksReport = {
|
|
88
|
+
status: "failed" | "no_failures";
|
|
89
|
+
message: string;
|
|
90
|
+
summary: {
|
|
91
|
+
total: number;
|
|
92
|
+
failed: number;
|
|
93
|
+
pending: number;
|
|
94
|
+
passed: number;
|
|
95
|
+
};
|
|
96
|
+
failedChecks: FailedCheckDetail[];
|
|
97
|
+
pendingChecks: CheckResult[];
|
|
98
|
+
hint: string;
|
|
99
|
+
nextCommands: string[];
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export type WorkflowRunDetail = {
|
|
103
|
+
databaseId: number;
|
|
104
|
+
url: string;
|
|
105
|
+
workflowName: string | null;
|
|
106
|
+
status: string;
|
|
107
|
+
conclusion: string | null;
|
|
108
|
+
jobs: Array<{
|
|
109
|
+
name: string;
|
|
110
|
+
status: string;
|
|
111
|
+
conclusion: string | null;
|
|
112
|
+
url: string;
|
|
113
|
+
steps: Array<{
|
|
114
|
+
name: string;
|
|
115
|
+
status: string;
|
|
116
|
+
conclusion: string | null;
|
|
117
|
+
}>;
|
|
118
|
+
}>;
|
|
119
|
+
};
|
|
120
|
+
|
|
65
121
|
export type MergeResult = {
|
|
66
122
|
merged: boolean;
|
|
67
123
|
strategy: MergeStrategy;
|