@blogic-cz/agent-tools 0.14.58 → 0.14.60
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 +19 -0
- package/package.json +1 -1
- package/src/gh-tool/index.ts +2 -0
- package/src/gh-tool/issue/core.ts +2 -0
- package/src/gh-tool/pr/commands.ts +199 -39
- package/src/gh-tool/pr/core.ts +557 -89
- package/src/gh-tool/pr/index.ts +1 -0
- package/src/gh-tool/pr/review.ts +178 -11
- package/src/gh-tool/types.ts +91 -0
- package/src/gh-tool/workflow.ts +68 -9
package/src/gh-tool/pr/core.ts
CHANGED
|
@@ -9,6 +9,10 @@ import type {
|
|
|
9
9
|
MergeStrategy,
|
|
10
10
|
PRInfo,
|
|
11
11
|
PRViewInfo,
|
|
12
|
+
RerunCheckAttempt,
|
|
13
|
+
RerunChecksReport,
|
|
14
|
+
RerunChecksRun,
|
|
15
|
+
RerunRetryEvidence,
|
|
12
16
|
WorkflowRunDetail,
|
|
13
17
|
} from "#gh/types";
|
|
14
18
|
|
|
@@ -17,9 +21,10 @@ import { GitHubService } from "#gh/service";
|
|
|
17
21
|
|
|
18
22
|
import type { ButStatusJson, PRViewJsonResult } from "./helpers";
|
|
19
23
|
import { runLocalCommand } from "./helpers";
|
|
20
|
-
import { fetchJobLogs } from "#gh/workflow";
|
|
24
|
+
import { diagnoseLogEntries, fetchJobLogs, formatLogEntries, parseRawJobLogs } from "#gh/workflow";
|
|
21
25
|
|
|
22
26
|
const CHECK_JSON_FIELDS = "name,state,bucket,link";
|
|
27
|
+
const STABLE_SNAPSHOT_ATTEMPTS = 3;
|
|
23
28
|
const GITHUB_ACTIONS_RUN_ID_RE = /github\.com\/[^/]+\/[^/]+\/actions\/runs\/(\d+)/;
|
|
24
29
|
|
|
25
30
|
const validatePRTitle = Effect.fn("pr.validatePRTitle")(function* (title: string) {
|
|
@@ -61,16 +66,6 @@ const validatePRTitle = Effect.fn("pr.validatePRTitle")(function* (title: string
|
|
|
61
66
|
});
|
|
62
67
|
});
|
|
63
68
|
|
|
64
|
-
type WorkflowRunJobsForRerun = {
|
|
65
|
-
databaseId: number;
|
|
66
|
-
jobs: Array<{
|
|
67
|
-
databaseId: number;
|
|
68
|
-
name: string;
|
|
69
|
-
status: string;
|
|
70
|
-
conclusion: string | null;
|
|
71
|
-
}>;
|
|
72
|
-
};
|
|
73
|
-
|
|
74
69
|
const buildChecksCommand = (pr: number | null, includeWatch: boolean): string =>
|
|
75
70
|
`bun agent-tools-gh pr checks${pr !== null ? ` --pr ${pr}` : ""}${includeWatch ? " --watch" : ""}`;
|
|
76
71
|
|
|
@@ -117,7 +112,7 @@ const failedJobsMatchingCheck = <Job extends { name: string }>(
|
|
|
117
112
|
|
|
118
113
|
const resolveJobIdsForFailedChecks = (
|
|
119
114
|
checks: CheckResult[],
|
|
120
|
-
jobs:
|
|
115
|
+
jobs: RerunCheckAttempt["jobs"],
|
|
121
116
|
): number[] | null => {
|
|
122
117
|
const failedJobs = jobs.filter(isFailedWorkflowJob);
|
|
123
118
|
const jobIds = new Set<number>();
|
|
@@ -154,7 +149,7 @@ const fetchWorkflowRunFailureContext = Effect.fn("pr.fetchWorkflowRunFailureCont
|
|
|
154
149
|
"view",
|
|
155
150
|
String(runId),
|
|
156
151
|
"--json",
|
|
157
|
-
"databaseId,url,workflowName,status,conclusion,jobs",
|
|
152
|
+
"databaseId,attempt,url,workflowName,status,conclusion,jobs",
|
|
158
153
|
])
|
|
159
154
|
.pipe(Effect.catchTag("GitHubCommandError", () => Effect.succeed(null)));
|
|
160
155
|
|
|
@@ -166,6 +161,8 @@ const fetchWorkflowRunFailureContext = Effect.fn("pr.fetchWorkflowRunFailureCont
|
|
|
166
161
|
.filter((job) => job.conclusion === "failure" || job.status === "failure")
|
|
167
162
|
.map((job) => ({
|
|
168
163
|
databaseId: job.databaseId,
|
|
164
|
+
jobId: job.databaseId,
|
|
165
|
+
checkId: null,
|
|
169
166
|
name: job.name,
|
|
170
167
|
status: job.status,
|
|
171
168
|
conclusion: job.conclusion,
|
|
@@ -177,6 +174,7 @@ const fetchWorkflowRunFailureContext = Effect.fn("pr.fetchWorkflowRunFailureCont
|
|
|
177
174
|
|
|
178
175
|
const context: FailedCheckRunContext = {
|
|
179
176
|
runId: run.databaseId,
|
|
177
|
+
attempt: run.attempt ?? null,
|
|
180
178
|
url: run.url,
|
|
181
179
|
workflowName: run.workflowName,
|
|
182
180
|
status: run.status,
|
|
@@ -201,7 +199,10 @@ const fetchCheckResults = Effect.fn("pr.fetchCheckResults")(function* (pr: numbe
|
|
|
201
199
|
const buildFailedChecksReport = Effect.fn("pr.buildFailedChecksReport")(function* (
|
|
202
200
|
pr: number | null,
|
|
203
201
|
checks: CheckResult[],
|
|
204
|
-
options: {
|
|
202
|
+
options: {
|
|
203
|
+
withLogs: boolean;
|
|
204
|
+
evidence?: { headSha: string | null; baseSha: string | null } | null;
|
|
205
|
+
} = { withLogs: false },
|
|
205
206
|
) {
|
|
206
207
|
const failedChecks = checks.filter((check) => check.bucket === "fail");
|
|
207
208
|
const pendingChecks = checks.filter((check) => check.bucket === "pending");
|
|
@@ -215,6 +216,13 @@ const buildFailedChecksReport = Effect.fn("pr.buildFailedChecksReport")(function
|
|
|
215
216
|
),
|
|
216
217
|
];
|
|
217
218
|
|
|
219
|
+
const evidence =
|
|
220
|
+
options.evidence ??
|
|
221
|
+
(yield* viewPR(pr).pipe(
|
|
222
|
+
Effect.map((info) => ({ headSha: info.headSha ?? null, baseSha: info.baseSha ?? null })),
|
|
223
|
+
Effect.catchTag("GitHubCommandError", () => Effect.succeed(null)),
|
|
224
|
+
));
|
|
225
|
+
|
|
218
226
|
const runContexts = new Map<number, FailedCheckRunContext | null>();
|
|
219
227
|
const contexts = yield* Effect.forEach(
|
|
220
228
|
runIds,
|
|
@@ -252,14 +260,21 @@ const buildFailedChecksReport = Effect.fn("pr.buildFailedChecksReport")(function
|
|
|
252
260
|
jobId: matchedJob.databaseId,
|
|
253
261
|
failedStepNames: matchedJob.failedSteps,
|
|
254
262
|
failedStepsOnly: true,
|
|
255
|
-
format: "
|
|
263
|
+
format: "json",
|
|
256
264
|
repo: null,
|
|
257
|
-
}).pipe(
|
|
258
|
-
Effect.map((result) => ("formatted" in result ? result.formatted : "")),
|
|
259
|
-
Effect.catch(() => Effect.succeed("")),
|
|
260
|
-
);
|
|
265
|
+
}).pipe(Effect.catch(() => Effect.succeed(null)));
|
|
261
266
|
|
|
262
|
-
|
|
267
|
+
if (failedStepLogs === null || !("entries" in failedStepLogs) || !failedStepLogs.entries) {
|
|
268
|
+
return detail;
|
|
269
|
+
}
|
|
270
|
+
const formatted = formatLogEntries(failedStepLogs.entries);
|
|
271
|
+
return formatted.length > 0
|
|
272
|
+
? {
|
|
273
|
+
...detail,
|
|
274
|
+
failedStepLogs: formatted,
|
|
275
|
+
diagnosis: diagnoseLogEntries(failedStepLogs.entries),
|
|
276
|
+
}
|
|
277
|
+
: detail;
|
|
263
278
|
}),
|
|
264
279
|
{ concurrency: 5 },
|
|
265
280
|
);
|
|
@@ -305,6 +320,7 @@ const buildFailedChecksReport = Effect.fn("pr.buildFailedChecksReport")(function
|
|
|
305
320
|
: "Inspect the failed workflow run and failed job logs to get the first concrete error, then rerun only if the failure is understood.";
|
|
306
321
|
|
|
307
322
|
return {
|
|
323
|
+
evidence,
|
|
308
324
|
status: failedChecks.length > 0 ? "failed" : "no_failures",
|
|
309
325
|
message,
|
|
310
326
|
summary: {
|
|
@@ -329,11 +345,15 @@ export const viewPR = Effect.fn("pr.viewPR")(function* (prNumber: number | null)
|
|
|
329
345
|
}
|
|
330
346
|
args.push(
|
|
331
347
|
"--json",
|
|
332
|
-
"number,url,title,headRefName,baseRefName,state,isDraft,mergeable,body,author,reviewDecision,reviewRequests",
|
|
348
|
+
"number,url,title,headRefName,baseRefName,headRefOid,baseRefOid,state,isDraft,mergeable,body,author,reviewDecision,reviewRequests",
|
|
333
349
|
);
|
|
334
350
|
|
|
335
|
-
const info = yield* gh.runGhJson<PRViewInfo>(args);
|
|
336
|
-
return
|
|
351
|
+
const info = yield* gh.runGhJson<PRViewInfo & { headRefOid?: string; baseRefOid?: string }>(args);
|
|
352
|
+
return {
|
|
353
|
+
...info,
|
|
354
|
+
headSha: info.headRefOid ?? info.headSha ?? null,
|
|
355
|
+
baseSha: info.baseRefOid ?? info.baseSha ?? null,
|
|
356
|
+
};
|
|
337
357
|
});
|
|
338
358
|
|
|
339
359
|
export const detectPRStatus = Effect.fn("pr.detectPRStatus")(function* () {
|
|
@@ -713,7 +733,7 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
|
|
|
713
733
|
yield* Console.log(
|
|
714
734
|
`DRY RUN: Would merge PR #${info.number} "${info.title}" via ${opts.strategy.toUpperCase()}. ` +
|
|
715
735
|
`Branch \`${info.headRefName}\` → \`${info.baseRefName}\`. ` +
|
|
716
|
-
(opts.deleteBranch ? `
|
|
736
|
+
(opts.deleteBranch ? `Remote branch \`${info.headRefName}\` will be deleted. ` : "") +
|
|
717
737
|
dependentNote +
|
|
718
738
|
mergeableNote,
|
|
719
739
|
);
|
|
@@ -732,10 +752,9 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
|
|
|
732
752
|
let willDeleteBranch = opts.deleteBranch;
|
|
733
753
|
let branchDeleteSkipped = false;
|
|
734
754
|
const retargetedChildren: number[] = [];
|
|
755
|
+
const repo = opts.deleteBranch ? yield* gh.getRepoInfo() : null;
|
|
735
756
|
|
|
736
|
-
if (opts.deleteBranch && dependentOpenPrs.length > 0) {
|
|
737
|
-
const repo = yield* gh.getRepoInfo();
|
|
738
|
-
|
|
757
|
+
if (opts.deleteBranch && dependentOpenPrs.length > 0 && repo) {
|
|
739
758
|
for (const child of dependentOpenPrs) {
|
|
740
759
|
const retargeted = yield* gh
|
|
741
760
|
.runGh([
|
|
@@ -763,10 +782,6 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
|
|
|
763
782
|
|
|
764
783
|
const mergeArgs = ["pr", "merge", String(opts.pr), `--${opts.strategy}`];
|
|
765
784
|
|
|
766
|
-
if (willDeleteBranch) {
|
|
767
|
-
mergeArgs.push("--delete-branch");
|
|
768
|
-
}
|
|
769
|
-
|
|
770
785
|
const mergeResult = yield* gh.runGh(mergeArgs).pipe(
|
|
771
786
|
Effect.catchTag("GitHubCommandError", (error) => {
|
|
772
787
|
const stderr = error.stderr.toLowerCase();
|
|
@@ -817,6 +832,27 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
|
|
|
817
832
|
|
|
818
833
|
const shaMatch = mergeResult.stdout.match(/([0-9a-f]{7,40})/);
|
|
819
834
|
|
|
835
|
+
// `gh pr merge --delete-branch` aborts its remote delete when the head branch is checked out in a
|
|
836
|
+
// worktree; deleting the remote ref explicitly is worktree-independent. Local cleanup is separate.
|
|
837
|
+
if (willDeleteBranch && repo && info.headRefName) {
|
|
838
|
+
const remoteDeleted = yield* gh
|
|
839
|
+
.runGh([
|
|
840
|
+
"api",
|
|
841
|
+
"--method",
|
|
842
|
+
"DELETE",
|
|
843
|
+
`repos/${repo.owner}/${repo.name}/git/refs/heads/${info.headRefName}`,
|
|
844
|
+
])
|
|
845
|
+
.pipe(
|
|
846
|
+
Effect.as(true),
|
|
847
|
+
Effect.orElseSucceed(() => false),
|
|
848
|
+
);
|
|
849
|
+
|
|
850
|
+
if (!remoteDeleted) {
|
|
851
|
+
willDeleteBranch = false;
|
|
852
|
+
branchDeleteSkipped = true;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
820
856
|
const result: MergeResult = {
|
|
821
857
|
merged: true,
|
|
822
858
|
strategy: opts.strategy,
|
|
@@ -901,6 +937,7 @@ export const fetchChecks = Effect.fn("pr.fetchChecks")(function* (
|
|
|
901
937
|
watch: boolean,
|
|
902
938
|
failFast: boolean,
|
|
903
939
|
timeoutSeconds: number,
|
|
940
|
+
quiet = false,
|
|
904
941
|
) {
|
|
905
942
|
const gh = yield* GitHubService;
|
|
906
943
|
|
|
@@ -926,7 +963,7 @@ export const fetchChecks = Effect.fn("pr.fetchChecks")(function* (
|
|
|
926
963
|
);
|
|
927
964
|
|
|
928
965
|
const results = yield* fetchCheckResults(pr);
|
|
929
|
-
if (watchOutcome === null && results.some((c) => c.bucket === "pending")) {
|
|
966
|
+
if (!quiet && watchOutcome === null && results.some((c) => c.bucket === "pending")) {
|
|
930
967
|
const pending = results.filter((c) => c.bucket === "pending").length;
|
|
931
968
|
yield* Console.warn(
|
|
932
969
|
`ℹ️ Watch timed out after ${timeoutSeconds}s; ${pending} check(s) still pending (snapshot returned). ` +
|
|
@@ -937,7 +974,7 @@ export const fetchChecks = Effect.fn("pr.fetchChecks")(function* (
|
|
|
937
974
|
}
|
|
938
975
|
|
|
939
976
|
const results = yield* fetchCheckResults(pr);
|
|
940
|
-
if (results.some((c) => c.bucket === "pending")) {
|
|
977
|
+
if (!quiet && results.some((c) => c.bucket === "pending")) {
|
|
941
978
|
yield* Console.warn(
|
|
942
979
|
`ℹ️ Some checks are still running. Re-run to refresh — each call returns the latest snapshot:\n` +
|
|
943
980
|
` ${buildChecksCommand(pr, false)}`,
|
|
@@ -946,12 +983,49 @@ export const fetchChecks = Effect.fn("pr.fetchChecks")(function* (
|
|
|
946
983
|
return results;
|
|
947
984
|
});
|
|
948
985
|
|
|
986
|
+
export const collectWithStableState = <S, A, E1, R1, E2, R2>(
|
|
987
|
+
initial: S,
|
|
988
|
+
collect: (state: S) => Effect.Effect<A, E1, R1>,
|
|
989
|
+
refresh: (state: S) => Effect.Effect<S, E2, R2>,
|
|
990
|
+
unchanged: (before: S, after: S) => boolean,
|
|
991
|
+
): Effect.Effect<{ state: S; value: A } | null, E1 | E2, R1 | R2> =>
|
|
992
|
+
Effect.gen(function* () {
|
|
993
|
+
let before = initial;
|
|
994
|
+
for (let attempt = 0; attempt < STABLE_SNAPSHOT_ATTEMPTS; attempt += 1) {
|
|
995
|
+
const value = yield* collect(before);
|
|
996
|
+
const after = yield* refresh(before);
|
|
997
|
+
if (unchanged(before, after)) return { state: after, value };
|
|
998
|
+
before = after;
|
|
999
|
+
}
|
|
1000
|
+
return null;
|
|
1001
|
+
});
|
|
1002
|
+
|
|
949
1003
|
export const fetchFailedChecks = Effect.fn("pr.fetchFailedChecks")(function* (
|
|
950
1004
|
pr: number | null,
|
|
951
1005
|
withLogs = false,
|
|
952
1006
|
) {
|
|
953
|
-
const
|
|
954
|
-
|
|
1007
|
+
const initial = yield* viewPR(pr);
|
|
1008
|
+
const snapshot = yield* collectWithStableState(
|
|
1009
|
+
initial,
|
|
1010
|
+
(info) => fetchCheckResults(info.number),
|
|
1011
|
+
(info) => viewPR(info.number),
|
|
1012
|
+
(before, after) => after.headSha === before.headSha,
|
|
1013
|
+
);
|
|
1014
|
+
if (snapshot !== null) {
|
|
1015
|
+
return yield* buildFailedChecksReport(snapshot.state.number, snapshot.value, {
|
|
1016
|
+
withLogs,
|
|
1017
|
+
evidence: { headSha: snapshot.state.headSha, baseSha: snapshot.state.baseSha },
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
return yield* Effect.fail(
|
|
1021
|
+
new GitHubCommandError({
|
|
1022
|
+
command: "gh-tool pr checks-failed",
|
|
1023
|
+
exitCode: 1,
|
|
1024
|
+
stderr: "",
|
|
1025
|
+
message: "PR head changed repeatedly while collecting checks",
|
|
1026
|
+
retryable: true,
|
|
1027
|
+
}),
|
|
1028
|
+
);
|
|
955
1029
|
});
|
|
956
1030
|
|
|
957
1031
|
export const fetchChecksForCommand = Effect.fn("pr.fetchChecksForCommand")(function* (
|
|
@@ -959,12 +1033,13 @@ export const fetchChecksForCommand = Effect.fn("pr.fetchChecksForCommand")(funct
|
|
|
959
1033
|
watch: boolean,
|
|
960
1034
|
failFast: boolean,
|
|
961
1035
|
timeoutSeconds: number,
|
|
1036
|
+
quiet = false,
|
|
962
1037
|
) {
|
|
963
1038
|
if (!watch) {
|
|
964
|
-
return yield* fetchChecks(pr, false, failFast, timeoutSeconds);
|
|
1039
|
+
return yield* fetchChecks(pr, false, failFast, timeoutSeconds, quiet);
|
|
965
1040
|
}
|
|
966
1041
|
|
|
967
|
-
const watchedChecks = yield* fetchChecks(pr, true, failFast, timeoutSeconds).pipe(
|
|
1042
|
+
const watchedChecks = yield* fetchChecks(pr, true, failFast, timeoutSeconds, quiet).pipe(
|
|
968
1043
|
Effect.result,
|
|
969
1044
|
Effect.flatMap((result) => {
|
|
970
1045
|
if (Result.isFailure(result) && result.failure._tag !== "GitHubCommandError") {
|
|
@@ -987,84 +1062,477 @@ export const fetchChecksForCommand = Effect.fn("pr.fetchChecksForCommand")(funct
|
|
|
987
1062
|
return yield* Effect.fail(watchedChecks.failure);
|
|
988
1063
|
});
|
|
989
1064
|
|
|
1065
|
+
type WatchPR = { number: number; state: string; headRefOid?: string | null };
|
|
1066
|
+
type WatchRun = {
|
|
1067
|
+
databaseId: number;
|
|
1068
|
+
attempt?: number | null;
|
|
1069
|
+
headSha?: string | null;
|
|
1070
|
+
jobs?: Array<{ databaseId: number; name: string }>;
|
|
1071
|
+
};
|
|
1072
|
+
|
|
1073
|
+
const watchPRState = Effect.fn("pr.watchPRState")(function* (pr: number) {
|
|
1074
|
+
const gh = yield* GitHubService;
|
|
1075
|
+
return yield* gh.runGhJson<WatchPR>([
|
|
1076
|
+
"pr",
|
|
1077
|
+
"view",
|
|
1078
|
+
String(pr),
|
|
1079
|
+
"--json",
|
|
1080
|
+
"number,state,headRefOid",
|
|
1081
|
+
]);
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
export const watchPRs = Effect.fn("pr.watchPRs")(function* (
|
|
1085
|
+
prs: readonly number[],
|
|
1086
|
+
options: { intervalSeconds: number; timeoutSeconds: number; until?: "terminal" },
|
|
1087
|
+
emit: (event: Record<string, unknown>) => Effect.Effect<void>,
|
|
1088
|
+
) {
|
|
1089
|
+
if ((options.until ?? "terminal") !== "terminal") {
|
|
1090
|
+
return yield* Effect.fail(
|
|
1091
|
+
new GitHubCommandError({
|
|
1092
|
+
command: "gh-tool pr watch",
|
|
1093
|
+
exitCode: 1,
|
|
1094
|
+
stderr: "",
|
|
1095
|
+
message: `Unsupported --until value: ${String(options.until)}`,
|
|
1096
|
+
}),
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
const gh = yield* GitHubService;
|
|
1100
|
+
const repo = yield* gh.getRepoInfo();
|
|
1101
|
+
const currentIdentity = new Map<string, string>();
|
|
1102
|
+
const lastRevision = new Map<string, string>();
|
|
1103
|
+
const emptySnapshots = new Map<string, number>();
|
|
1104
|
+
const checksObserved = new Set<string>();
|
|
1105
|
+
const headByPR = new Map<number, string>();
|
|
1106
|
+
const terminal = new Set<number>();
|
|
1107
|
+
const started = Number(yield* Clock.currentTimeMillis);
|
|
1108
|
+
const deadline = started + options.timeoutSeconds * 1000;
|
|
1109
|
+
const beforeDeadline = Effect.gen(function* () {
|
|
1110
|
+
return Number(yield* Clock.currentTimeMillis) < deadline;
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
const snapshot = (pr: number) =>
|
|
1114
|
+
Effect.gen(function* () {
|
|
1115
|
+
const requireDeadline = Effect.gen(function* () {
|
|
1116
|
+
if (!(yield* beforeDeadline)) return yield* Effect.fail("timeout" as const);
|
|
1117
|
+
});
|
|
1118
|
+
yield* requireDeadline;
|
|
1119
|
+
const initial = yield* watchPRState(pr);
|
|
1120
|
+
const stableResult = yield* collectWithStableState(
|
|
1121
|
+
initial,
|
|
1122
|
+
() => requireDeadline.pipe(Effect.andThen(fetchCheckResults(pr))),
|
|
1123
|
+
() => requireDeadline.pipe(Effect.andThen(watchPRState(pr))),
|
|
1124
|
+
(before, after) => before.headRefOid === after.headRefOid && before.state === after.state,
|
|
1125
|
+
).pipe(Effect.result);
|
|
1126
|
+
if (Result.isFailure(stableResult)) {
|
|
1127
|
+
if (stableResult.failure === "timeout") return "timeout" as const;
|
|
1128
|
+
return yield* Effect.fail(stableResult.failure);
|
|
1129
|
+
}
|
|
1130
|
+
if (stableResult.success === null) return null;
|
|
1131
|
+
const { state: after, value: checks } = stableResult.success;
|
|
1132
|
+
const head = after.headRefOid ?? null;
|
|
1133
|
+
const runs = new Map<number, WatchRun | null>();
|
|
1134
|
+
yield* Effect.forEach(
|
|
1135
|
+
[...new Set(checks.map((check) => extractRunIdFromCheckLink(check.link)))].filter(
|
|
1136
|
+
(runId): runId is number => runId !== null,
|
|
1137
|
+
),
|
|
1138
|
+
(runId) =>
|
|
1139
|
+
Effect.gen(function* () {
|
|
1140
|
+
if (!(yield* beforeDeadline)) return;
|
|
1141
|
+
const run = yield* gh
|
|
1142
|
+
.runGhJson<WatchRun>([
|
|
1143
|
+
"run",
|
|
1144
|
+
"view",
|
|
1145
|
+
String(runId),
|
|
1146
|
+
"--json",
|
|
1147
|
+
"databaseId,attempt,headSha,jobs",
|
|
1148
|
+
])
|
|
1149
|
+
.pipe(Effect.catchTag("GitHubCommandError", () => Effect.succeed(null)));
|
|
1150
|
+
runs.set(runId, run);
|
|
1151
|
+
}),
|
|
1152
|
+
{ concurrency: 5 },
|
|
1153
|
+
);
|
|
1154
|
+
if (!(yield* beforeDeadline)) return "timeout" as const;
|
|
1155
|
+
return {
|
|
1156
|
+
pr: after,
|
|
1157
|
+
head,
|
|
1158
|
+
checks: checks
|
|
1159
|
+
.map((check) => ({
|
|
1160
|
+
check,
|
|
1161
|
+
run:
|
|
1162
|
+
extractRunIdFromCheckLink(check.link) === null
|
|
1163
|
+
? null
|
|
1164
|
+
: (runs.get(extractRunIdFromCheckLink(check.link) ?? -1) ?? null),
|
|
1165
|
+
}))
|
|
1166
|
+
.filter(({ run }) => run === null || run.headSha === null || run.headSha === head),
|
|
1167
|
+
};
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
let timedOut = false;
|
|
1171
|
+
yield* Effect.whileLoop({
|
|
1172
|
+
while: () => terminal.size < prs.length && !timedOut,
|
|
1173
|
+
body: () =>
|
|
1174
|
+
Effect.gen(function* () {
|
|
1175
|
+
for (const number of prs) {
|
|
1176
|
+
if (terminal.has(number)) continue;
|
|
1177
|
+
if (!(yield* beforeDeadline)) {
|
|
1178
|
+
timedOut = true;
|
|
1179
|
+
break;
|
|
1180
|
+
}
|
|
1181
|
+
const state = yield* snapshot(number);
|
|
1182
|
+
if (state === "timeout") {
|
|
1183
|
+
timedOut = true;
|
|
1184
|
+
break;
|
|
1185
|
+
}
|
|
1186
|
+
if (state === null) continue;
|
|
1187
|
+
const headKey = `${number}/${state.head ?? ""}`;
|
|
1188
|
+
const oldHeadKey = headByPR.get(number);
|
|
1189
|
+
if (oldHeadKey !== undefined && oldHeadKey !== headKey) {
|
|
1190
|
+
checksObserved.delete(oldHeadKey);
|
|
1191
|
+
emptySnapshots.delete(oldHeadKey);
|
|
1192
|
+
}
|
|
1193
|
+
headByPR.set(number, headKey);
|
|
1194
|
+
if (state.checks.length > 0) {
|
|
1195
|
+
checksObserved.add(headKey);
|
|
1196
|
+
emptySnapshots.set(headKey, 0);
|
|
1197
|
+
} else {
|
|
1198
|
+
emptySnapshots.set(headKey, (emptySnapshots.get(headKey) ?? 0) + 1);
|
|
1199
|
+
}
|
|
1200
|
+
for (const { check, run } of state.checks) {
|
|
1201
|
+
const matches = failedJobsMatchingCheck(check.name, run?.jobs ?? []);
|
|
1202
|
+
const jobId = matches.length === 1 ? (matches[0]?.databaseId ?? null) : null;
|
|
1203
|
+
const fallback = jobId === null ? `${check.name}|${check.link}` : String(jobId);
|
|
1204
|
+
const identity = [
|
|
1205
|
+
repo.owner + "/" + repo.name,
|
|
1206
|
+
number,
|
|
1207
|
+
state.head ?? "",
|
|
1208
|
+
run?.databaseId ?? "external",
|
|
1209
|
+
run?.attempt ?? "",
|
|
1210
|
+
fallback,
|
|
1211
|
+
].join("/");
|
|
1212
|
+
const logical = `${number}/${check.name}`;
|
|
1213
|
+
const previousIdentity = currentIdentity.get(logical);
|
|
1214
|
+
const revision = `${check.state}/${check.bucket}`;
|
|
1215
|
+
if (lastRevision.get(identity) !== revision) {
|
|
1216
|
+
const event: Record<string, unknown> = {
|
|
1217
|
+
type: "check",
|
|
1218
|
+
repo: `${repo.owner}/${repo.name}`,
|
|
1219
|
+
pr: number,
|
|
1220
|
+
headSha: state.head,
|
|
1221
|
+
runId: run?.databaseId ?? null,
|
|
1222
|
+
attempt: run?.attempt ?? null,
|
|
1223
|
+
jobId,
|
|
1224
|
+
checkId: null,
|
|
1225
|
+
name: check.name,
|
|
1226
|
+
state: check.state,
|
|
1227
|
+
bucket: check.bucket,
|
|
1228
|
+
link: check.link,
|
|
1229
|
+
identity,
|
|
1230
|
+
};
|
|
1231
|
+
if (previousIdentity !== undefined && previousIdentity !== identity) {
|
|
1232
|
+
event.supersedes = previousIdentity;
|
|
1233
|
+
}
|
|
1234
|
+
if (!(yield* beforeDeadline)) {
|
|
1235
|
+
timedOut = true;
|
|
1236
|
+
break;
|
|
1237
|
+
}
|
|
1238
|
+
yield* emit(event);
|
|
1239
|
+
lastRevision.set(identity, revision);
|
|
1240
|
+
}
|
|
1241
|
+
currentIdentity.set(logical, identity);
|
|
1242
|
+
}
|
|
1243
|
+
if (timedOut) break;
|
|
1244
|
+
// Empty snapshots need three consecutive observations, including after checks were seen.
|
|
1245
|
+
const hasTerminalCoverage =
|
|
1246
|
+
state.checks.length > 0 || (emptySnapshots.get(headKey) ?? 0) >= 3;
|
|
1247
|
+
const checksTerminal =
|
|
1248
|
+
state.checks.length === 0 ||
|
|
1249
|
+
state.checks.every(({ check }) => check.bucket !== "pending");
|
|
1250
|
+
if (state.pr.state !== "OPEN" || (hasTerminalCoverage && checksTerminal)) {
|
|
1251
|
+
if (!(yield* beforeDeadline)) {
|
|
1252
|
+
timedOut = true;
|
|
1253
|
+
break;
|
|
1254
|
+
}
|
|
1255
|
+
const identity = `${repo.owner}/${repo.name}/${number}/${state.head ?? ""}/terminal/${state.pr.state}`;
|
|
1256
|
+
yield* emit({
|
|
1257
|
+
type: "pr_terminal",
|
|
1258
|
+
repo: `${repo.owner}/${repo.name}`,
|
|
1259
|
+
pr: number,
|
|
1260
|
+
headSha: state.head,
|
|
1261
|
+
state: state.pr.state,
|
|
1262
|
+
checksObserved: checksObserved.has(headKey),
|
|
1263
|
+
identity,
|
|
1264
|
+
});
|
|
1265
|
+
terminal.add(number);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
const now = Number(yield* Clock.currentTimeMillis);
|
|
1269
|
+
timedOut = timedOut || (terminal.size < prs.length && now >= deadline);
|
|
1270
|
+
if (!timedOut && terminal.size < prs.length) {
|
|
1271
|
+
yield* Effect.sleep(
|
|
1272
|
+
Duration.millis(Math.min(options.intervalSeconds * 1000, deadline - now)),
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
}),
|
|
1276
|
+
step: () => undefined,
|
|
1277
|
+
});
|
|
1278
|
+
yield* emit({
|
|
1279
|
+
type: "watcher_terminal",
|
|
1280
|
+
repo: `${repo.owner}/${repo.name}`,
|
|
1281
|
+
status: terminal.size === prs.length ? "terminal" : "timeout",
|
|
1282
|
+
terminal: [...terminal].toSorted((a, b) => a - b),
|
|
1283
|
+
});
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
type RerunDiscovery = RerunCheckAttempt & { status?: string };
|
|
1287
|
+
|
|
1288
|
+
const fetchAttemptJobs = Effect.fn("pr.fetchAttemptJobs")(function* (
|
|
1289
|
+
runId: string,
|
|
1290
|
+
attempt: number,
|
|
1291
|
+
repo: string,
|
|
1292
|
+
) {
|
|
1293
|
+
const gh = yield* GitHubService;
|
|
1294
|
+
const jobs: Array<{ id: number; name: string }> = [];
|
|
1295
|
+
for (let page = 1; ; page += 1) {
|
|
1296
|
+
const response = yield* gh
|
|
1297
|
+
.runGhJson<{ jobs?: Array<{ id: number; name: string }> }>([
|
|
1298
|
+
"api",
|
|
1299
|
+
`repos/${repo}/actions/runs/${runId}/attempts/${attempt}/jobs?per_page=100&page=${page}`,
|
|
1300
|
+
])
|
|
1301
|
+
.pipe(Effect.catchTag("GitHubCommandError", () => Effect.succeed(null)));
|
|
1302
|
+
if (response?.jobs === undefined) return null;
|
|
1303
|
+
jobs.push(...response.jobs);
|
|
1304
|
+
if (response.jobs.length < 100) return jobs;
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
const readJobDiagnosis = Effect.fn("pr.readJobDiagnosis")(function* (
|
|
1309
|
+
runId: string,
|
|
1310
|
+
jobName: string,
|
|
1311
|
+
attempt: number,
|
|
1312
|
+
repo: string,
|
|
1313
|
+
) {
|
|
1314
|
+
const gh = yield* GitHubService;
|
|
1315
|
+
const jobs = yield* fetchAttemptJobs(runId, attempt, repo);
|
|
1316
|
+
if (jobs === null) return null;
|
|
1317
|
+
const matches = jobs.filter((job) => job.name === jobName);
|
|
1318
|
+
if (matches.length !== 1) return null;
|
|
1319
|
+
const logs = yield* gh
|
|
1320
|
+
.runGh(["api", `repos/${repo}/actions/jobs/${matches[0]?.id}/logs`])
|
|
1321
|
+
.pipe(Effect.catchTag("GitHubCommandError", () => Effect.succeed(null)));
|
|
1322
|
+
return logs === null ? null : diagnoseLogEntries(parseRawJobLogs(logs.stdout));
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
const discoverRerun = Effect.fn("pr.discoverRerun")(function* (
|
|
1326
|
+
runId: string,
|
|
1327
|
+
currentAttempt: number | null,
|
|
1328
|
+
currentJobIds: readonly number[],
|
|
1329
|
+
deadline: number,
|
|
1330
|
+
) {
|
|
1331
|
+
const gh = yield* GitHubService;
|
|
1332
|
+
let latest: RerunDiscovery | null = null;
|
|
1333
|
+
while (Number(yield* Clock.currentTimeMillis) <= deadline) {
|
|
1334
|
+
latest = yield* gh
|
|
1335
|
+
.runGhJson<RerunDiscovery>(["run", "view", runId, "--json", "databaseId,attempt,status,jobs"])
|
|
1336
|
+
.pipe(Effect.catchTag("GitHubCommandError", () => Effect.succeed(null)));
|
|
1337
|
+
if (
|
|
1338
|
+
latest !== null &&
|
|
1339
|
+
((latest.attempt ?? 0) > (currentAttempt ?? 0) ||
|
|
1340
|
+
latest.jobs.some((job) => !currentJobIds.includes(job.databaseId)))
|
|
1341
|
+
) {
|
|
1342
|
+
return latest;
|
|
1343
|
+
}
|
|
1344
|
+
const remaining = deadline - Number(yield* Clock.currentTimeMillis);
|
|
1345
|
+
if (remaining <= 0) return null;
|
|
1346
|
+
yield* Effect.sleep(Duration.millis(Math.min(1000, remaining)));
|
|
1347
|
+
}
|
|
1348
|
+
return null;
|
|
1349
|
+
});
|
|
1350
|
+
|
|
990
1351
|
export const rerunChecks = Effect.fn("pr.rerunChecks")(function* (
|
|
991
1352
|
pr: number | null,
|
|
992
1353
|
failedOnly: boolean,
|
|
1354
|
+
options: { watch?: boolean; timeoutSeconds?: number } = {},
|
|
993
1355
|
) {
|
|
994
1356
|
const gh = yield* GitHubService;
|
|
995
|
-
|
|
996
1357
|
const checks = yield* fetchCheckResults(pr);
|
|
997
|
-
|
|
998
1358
|
const targetChecks = failedOnly ? checks.filter((check) => check.bucket === "fail") : checks;
|
|
999
|
-
|
|
1000
|
-
// Extract unique GitHub Actions run IDs from links
|
|
1001
|
-
const runIds = new Set<string>();
|
|
1002
1359
|
const checksByRun = new Map<string, CheckResult[]>();
|
|
1003
1360
|
for (const check of targetChecks) {
|
|
1004
|
-
const
|
|
1005
|
-
if (
|
|
1006
|
-
runIds.add(match[1]);
|
|
1007
|
-
const existing = checksByRun.get(match[1]) ?? [];
|
|
1008
|
-
existing.push(check);
|
|
1009
|
-
checksByRun.set(match[1], existing);
|
|
1010
|
-
}
|
|
1361
|
+
const runId = check.link.match(GITHUB_ACTIONS_RUN_ID_RE)?.[1];
|
|
1362
|
+
if (runId !== undefined) checksByRun.set(runId, [...(checksByRun.get(runId) ?? []), check]);
|
|
1011
1363
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
return {
|
|
1364
|
+
if (checksByRun.size === 0) {
|
|
1365
|
+
const report: RerunChecksReport = {
|
|
1015
1366
|
rerun: 0,
|
|
1016
1367
|
message: failedOnly
|
|
1017
1368
|
? "No failed GitHub Actions runs found to rerun"
|
|
1018
1369
|
: "No GitHub Actions runs found to rerun",
|
|
1019
1370
|
};
|
|
1371
|
+
return report;
|
|
1020
1372
|
}
|
|
1021
1373
|
|
|
1022
|
-
const
|
|
1374
|
+
const repoInfo = yield* gh.getRepoInfo();
|
|
1375
|
+
const repo = `${repoInfo.owner}/${repoInfo.name}`;
|
|
1376
|
+
const candidates: Array<{
|
|
1023
1377
|
runId: string;
|
|
1024
|
-
|
|
1378
|
+
run: RerunCheckAttempt | null;
|
|
1379
|
+
evidence: RerunRetryEvidence;
|
|
1025
1380
|
}> = [];
|
|
1026
|
-
for (const runId of
|
|
1027
|
-
const
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1381
|
+
for (const [runId, runChecks] of checksByRun) {
|
|
1382
|
+
const run = yield* gh
|
|
1383
|
+
.runGhJson<RerunCheckAttempt>(["run", "view", runId, "--json", "databaseId,attempt,jobs"])
|
|
1384
|
+
.pipe(Effect.catchTag("GitHubCommandError", () => Effect.succeed(null)));
|
|
1385
|
+
let evidence: RerunRetryEvidence = { state: "eligible", diagnosis: diagnoseLogEntries([]) };
|
|
1386
|
+
if (failedOnly) {
|
|
1387
|
+
const jobIds = run === null ? null : resolveJobIdsForFailedChecks(runChecks, run.jobs);
|
|
1388
|
+
if (
|
|
1389
|
+
run === null ||
|
|
1390
|
+
run.attempt === null ||
|
|
1391
|
+
run.attempt === undefined ||
|
|
1392
|
+
jobIds === null ||
|
|
1393
|
+
jobIds.length === 0
|
|
1394
|
+
) {
|
|
1395
|
+
evidence = { state: "unavailable", reason: "failed jobs or current attempt unavailable" };
|
|
1396
|
+
} else {
|
|
1397
|
+
const targetJobs = run.jobs.filter((job) => jobIds.includes(job.databaseId));
|
|
1398
|
+
for (const job of targetJobs) {
|
|
1399
|
+
const current = yield* readJobDiagnosis(runId, job.name, run.attempt, repo);
|
|
1400
|
+
if (current === null) {
|
|
1401
|
+
evidence = {
|
|
1402
|
+
state: "unavailable",
|
|
1403
|
+
reason: `retry evidence unavailable for ${job.name}`,
|
|
1404
|
+
};
|
|
1405
|
+
break;
|
|
1406
|
+
}
|
|
1407
|
+
evidence = { state: "eligible", diagnosis: current };
|
|
1408
|
+
const retryablePreTest =
|
|
1409
|
+
current.testsStarted === false &&
|
|
1410
|
+
["infrastructure", "network", "timeout"].includes(current.category);
|
|
1411
|
+
if (!retryablePreTest) continue;
|
|
1412
|
+
for (let attempt = 1; attempt < run.attempt; attempt += 1) {
|
|
1413
|
+
const prior = yield* readJobDiagnosis(runId, job.name, attempt, repo);
|
|
1414
|
+
if (prior === null) {
|
|
1415
|
+
evidence = {
|
|
1416
|
+
state: "unavailable",
|
|
1417
|
+
reason: `retry evidence unavailable for ${job.name} attempt ${attempt}`,
|
|
1418
|
+
};
|
|
1419
|
+
break;
|
|
1420
|
+
}
|
|
1421
|
+
if (prior.fingerprint === current.fingerprint) {
|
|
1422
|
+
evidence = {
|
|
1423
|
+
state: "ineligible",
|
|
1424
|
+
diagnosis: current,
|
|
1425
|
+
reason: "matching pre-test failure already retried",
|
|
1426
|
+
};
|
|
1427
|
+
break;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
if (evidence.state !== "eligible") break;
|
|
1431
|
+
}
|
|
1033
1432
|
}
|
|
1433
|
+
}
|
|
1434
|
+
candidates.push({ runId, run, evidence });
|
|
1435
|
+
}
|
|
1034
1436
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1437
|
+
if (candidates.some((candidate) => candidate.evidence.state !== "eligible")) {
|
|
1438
|
+
const unavailable = candidates.some((candidate) => candidate.evidence.state === "unavailable");
|
|
1439
|
+
const status = unavailable ? "evidence_unavailable" : "escalation_required";
|
|
1440
|
+
const runs: RerunChecksRun[] = candidates.map((candidate) => ({
|
|
1441
|
+
runId: candidate.runId,
|
|
1442
|
+
success: false,
|
|
1443
|
+
currentAttempt: candidate.run?.attempt ?? null,
|
|
1444
|
+
currentJobIds: candidate.run?.jobs?.map((job) => job.databaseId) ?? null,
|
|
1445
|
+
status: candidate.evidence.state === "eligible" ? "blocked" : status,
|
|
1446
|
+
evidence: candidate.evidence,
|
|
1447
|
+
}));
|
|
1448
|
+
const report: RerunChecksReport = {
|
|
1449
|
+
status,
|
|
1450
|
+
rerun: 0,
|
|
1451
|
+
failed: runs.length,
|
|
1452
|
+
runs,
|
|
1453
|
+
message: unavailable
|
|
1454
|
+
? "Required retry evidence unavailable; no runs rerun"
|
|
1455
|
+
: "Escalation required; no runs rerun",
|
|
1456
|
+
};
|
|
1457
|
+
return report;
|
|
1458
|
+
}
|
|
1047
1459
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1460
|
+
const watch = options.watch === true;
|
|
1461
|
+
const deadline = Number(yield* Clock.currentTimeMillis) + (options.timeoutSeconds ?? 60) * 1000;
|
|
1462
|
+
const results: RerunChecksRun[] = [];
|
|
1463
|
+
for (const candidate of candidates) {
|
|
1464
|
+
const success = yield* gh
|
|
1465
|
+
.runGh(["run", "rerun", candidate.runId, ...(failedOnly ? ["--failed"] : [])])
|
|
1466
|
+
.pipe(
|
|
1467
|
+
Effect.as(true),
|
|
1468
|
+
Effect.catch(() => Effect.succeed(false)),
|
|
1056
1469
|
);
|
|
1057
|
-
|
|
1058
|
-
|
|
1470
|
+
results.push({
|
|
1471
|
+
runId: candidate.runId,
|
|
1472
|
+
success,
|
|
1473
|
+
status: success ? "rerun_started" : "failed",
|
|
1474
|
+
currentAttempt: candidate.run?.attempt ?? null,
|
|
1475
|
+
currentJobIds: candidate.run?.jobs?.map((job) => job.databaseId) ?? null,
|
|
1476
|
+
evidence: candidate.evidence,
|
|
1059
1477
|
});
|
|
1060
|
-
|
|
1061
|
-
results.push({ runId, success });
|
|
1062
1478
|
}
|
|
1063
1479
|
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1480
|
+
if (watch) {
|
|
1481
|
+
const discoveries = yield* Effect.forEach(
|
|
1482
|
+
candidates,
|
|
1483
|
+
(candidate, index) =>
|
|
1484
|
+
results[index]?.success
|
|
1485
|
+
? discoverRerun(
|
|
1486
|
+
candidate.runId,
|
|
1487
|
+
candidate.run?.attempt ?? null,
|
|
1488
|
+
candidate.run?.jobs?.map((job) => job.databaseId) ?? [],
|
|
1489
|
+
deadline,
|
|
1490
|
+
)
|
|
1491
|
+
: Effect.succeed(null),
|
|
1492
|
+
{ concurrency: "unbounded" },
|
|
1493
|
+
);
|
|
1494
|
+
yield* Effect.forEach(
|
|
1495
|
+
candidates,
|
|
1496
|
+
(candidate, index) =>
|
|
1497
|
+
Effect.gen(function* () {
|
|
1498
|
+
const result = results[index];
|
|
1499
|
+
if (result === undefined || !result.success) return;
|
|
1500
|
+
const next = discoveries[index] ?? null;
|
|
1501
|
+
result.newAttempt = next?.attempt ?? null;
|
|
1502
|
+
result.newJobIds = next?.jobs.map((job) => job.databaseId) ?? null;
|
|
1503
|
+
if (next === null) {
|
|
1504
|
+
result.status = "discovery_timeout";
|
|
1505
|
+
result.latestAttempt = candidate.run;
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
let latest = next;
|
|
1509
|
+
while (latest.status !== "completed") {
|
|
1510
|
+
const remaining = deadline - Number(yield* Clock.currentTimeMillis);
|
|
1511
|
+
if (remaining <= 0) break;
|
|
1512
|
+
yield* Effect.sleep(Duration.millis(Math.min(1000, remaining)));
|
|
1513
|
+
if (Number(yield* Clock.currentTimeMillis) >= deadline) break;
|
|
1514
|
+
latest = yield* gh
|
|
1515
|
+
.runGhJson<RerunDiscovery>([
|
|
1516
|
+
"run",
|
|
1517
|
+
"view",
|
|
1518
|
+
candidate.runId,
|
|
1519
|
+
"--json",
|
|
1520
|
+
"databaseId,attempt,status,jobs",
|
|
1521
|
+
])
|
|
1522
|
+
.pipe(Effect.catchTag("GitHubCommandError", () => Effect.succeed(latest)));
|
|
1523
|
+
}
|
|
1524
|
+
result.latestAttempt = latest;
|
|
1525
|
+
result.status = latest.status === "completed" ? "completed" : "watch_timeout";
|
|
1526
|
+
}),
|
|
1527
|
+
{ concurrency: "unbounded" },
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1530
|
+
const report: RerunChecksReport = {
|
|
1531
|
+
status: results.some((result) => !result.success) ? "failed" : "rerun_started",
|
|
1532
|
+
rerun: results.filter((result) => result.success).length,
|
|
1533
|
+
failed: results.filter((result) => !result.success).length,
|
|
1067
1534
|
runs: results,
|
|
1068
|
-
message: `Rerun ${results.filter((
|
|
1535
|
+
message: `Rerun ${results.filter((result) => result.success).length}/${results.length} GitHub Actions runs`,
|
|
1069
1536
|
};
|
|
1537
|
+
return report;
|
|
1070
1538
|
});
|