@blogic-cz/agent-tools 1.7.5 → 1.8.0
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 +2 -1
- package/dist/db-tool/service.d.ts.map +1 -1
- package/dist/gh-tool/branch.d.ts +2 -3
- package/dist/gh-tool/branch.d.ts.map +1 -1
- package/dist/gh-tool/environment.d.ts +1 -1
- package/dist/gh-tool/environment.d.ts.map +1 -1
- package/dist/gh-tool/gist.d.ts +8 -28
- package/dist/gh-tool/gist.d.ts.map +1 -1
- package/dist/gh-tool/issue/commands.d.ts +9 -9
- package/dist/gh-tool/issue/commands.d.ts.map +1 -1
- package/dist/gh-tool/issue/core.d.ts +103 -9
- package/dist/gh-tool/issue/core.d.ts.map +1 -1
- package/dist/gh-tool/issue/triage.d.ts +6 -6
- package/dist/gh-tool/issue/triage.d.ts.map +1 -1
- package/dist/gh-tool/pr/commands.d.ts +53 -46
- package/dist/gh-tool/pr/commands.d.ts.map +1 -1
- package/dist/gh-tool/pr/core.d.ts +156 -86
- package/dist/gh-tool/pr/core.d.ts.map +1 -1
- package/dist/gh-tool/pr/review.d.ts +16 -16
- package/dist/gh-tool/pr/review.d.ts.map +1 -1
- package/dist/gh-tool/pr/snapshot.d.ts +9 -0
- package/dist/gh-tool/pr/snapshot.d.ts.map +1 -0
- package/dist/gh-tool/pr/stack-read.d.ts +1 -6
- package/dist/gh-tool/pr/stack-read.d.ts.map +1 -1
- package/dist/gh-tool/pr/stack.d.ts +3 -3
- package/dist/gh-tool/pr/stack.d.ts.map +1 -1
- package/dist/gh-tool/{api.d.ts → read/api.d.ts} +9 -3
- package/dist/gh-tool/read/api.d.ts.map +1 -0
- package/dist/gh-tool/read/cache.d.ts +44 -0
- package/dist/gh-tool/read/cache.d.ts.map +1 -0
- package/dist/gh-tool/read/catalog.d.ts +170 -0
- package/dist/gh-tool/read/catalog.d.ts.map +1 -0
- package/dist/gh-tool/read/classes.d.ts +94 -0
- package/dist/gh-tool/read/classes.d.ts.map +1 -0
- package/dist/gh-tool/read/commands.d.ts +3 -0
- package/dist/gh-tool/read/commands.d.ts.map +1 -0
- package/dist/gh-tool/read/gated.d.ts +14 -0
- package/dist/gh-tool/read/gated.d.ts.map +1 -0
- package/dist/gh-tool/read/index.d.ts +5 -0
- package/dist/gh-tool/read/index.d.ts.map +1 -0
- package/dist/gh-tool/read/issues.d.ts +68 -0
- package/dist/gh-tool/read/issues.d.ts.map +1 -0
- package/dist/gh-tool/read/keys.d.ts +61 -0
- package/dist/gh-tool/read/keys.d.ts.map +1 -0
- package/dist/gh-tool/read/porcelain.d.ts +68 -0
- package/dist/gh-tool/read/porcelain.d.ts.map +1 -0
- package/dist/gh-tool/read/pulls.d.ts +137 -0
- package/dist/gh-tool/read/pulls.d.ts.map +1 -0
- package/dist/gh-tool/read/reads.d.ts +209 -0
- package/dist/gh-tool/read/reads.d.ts.map +1 -0
- package/dist/gh-tool/read/rest.d.ts +49 -0
- package/dist/gh-tool/read/rest.d.ts.map +1 -0
- package/dist/gh-tool/read/review-guards.d.ts +20 -0
- package/dist/gh-tool/read/review-guards.d.ts.map +1 -0
- package/dist/gh-tool/read/review-model.d.ts +190 -0
- package/dist/gh-tool/read/review-model.d.ts.map +1 -0
- package/dist/gh-tool/read/reviews.d.ts +9 -0
- package/dist/gh-tool/read/reviews.d.ts.map +1 -0
- package/dist/gh-tool/read/stacks.d.ts +7 -0
- package/dist/gh-tool/read/stacks.d.ts.map +1 -0
- package/dist/gh-tool/read/store.d.ts +70 -0
- package/dist/gh-tool/read/store.d.ts.map +1 -0
- package/dist/gh-tool/{service.d.ts → read/transport.d.ts} +5 -3
- package/dist/gh-tool/read/transport.d.ts.map +1 -0
- package/dist/gh-tool/read/waits.d.ts +22 -0
- package/dist/gh-tool/read/waits.d.ts.map +1 -0
- package/dist/gh-tool/read/writes.d.ts +16 -0
- package/dist/gh-tool/read/writes.d.ts.map +1 -0
- package/dist/gh-tool/release.d.ts +10 -26
- package/dist/gh-tool/release.d.ts.map +1 -1
- package/dist/gh-tool/repo.d.ts +4 -4
- package/dist/gh-tool/repo.d.ts.map +1 -1
- package/dist/gh-tool/types.d.ts +1 -1
- package/dist/gh-tool/types.d.ts.map +1 -1
- package/dist/gh-tool/workflow.d.ts +43 -21
- package/dist/gh-tool/workflow.d.ts.map +1 -1
- package/dist/observability-tool/shared.d.ts.map +1 -1
- package/dist/shared/content-security.d.ts +2 -0
- package/dist/shared/content-security.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/api-tool/proxy-entry.ts +3 -1
- package/src/db-tool/service.ts +4 -1
- package/src/gh-tool/branch.ts +5 -8
- package/src/gh-tool/environment.ts +2 -1
- package/src/gh-tool/gist.ts +8 -35
- package/src/gh-tool/issue/commands.ts +1 -1
- package/src/gh-tool/issue/core.ts +55 -122
- package/src/gh-tool/issue/triage.ts +4 -26
- package/src/gh-tool/main.ts +5 -2
- package/src/gh-tool/pr/commands.ts +17 -15
- package/src/gh-tool/pr/core.ts +486 -582
- package/src/gh-tool/pr/review.ts +106 -454
- package/src/gh-tool/pr/snapshot.ts +23 -0
- package/src/gh-tool/pr/stack-read.ts +1 -73
- package/src/gh-tool/pr/stack.ts +37 -36
- package/src/gh-tool/{api.ts → read/api.ts} +40 -7
- package/src/gh-tool/read/cache.ts +192 -0
- package/src/gh-tool/read/catalog.ts +310 -0
- package/src/gh-tool/read/classes.ts +30 -0
- package/src/gh-tool/read/commands.ts +25 -0
- package/src/gh-tool/read/gated.ts +56 -0
- package/src/gh-tool/read/index.ts +4 -0
- package/src/gh-tool/read/issues.ts +107 -0
- package/src/gh-tool/read/keys.ts +102 -0
- package/src/gh-tool/read/porcelain.ts +58 -0
- package/src/gh-tool/read/pulls.ts +266 -0
- package/src/gh-tool/read/reads.ts +306 -0
- package/src/gh-tool/read/rest.ts +111 -0
- package/src/gh-tool/read/review-guards.ts +61 -0
- package/src/gh-tool/read/review-model.ts +286 -0
- package/src/gh-tool/read/reviews.ts +114 -0
- package/src/gh-tool/read/stacks.ts +73 -0
- package/src/gh-tool/read/store.ts +504 -0
- package/src/gh-tool/{service.ts → read/transport.ts} +26 -6
- package/src/gh-tool/read/waits.ts +52 -0
- package/src/gh-tool/read/writes.ts +37 -0
- package/src/gh-tool/release.ts +15 -168
- package/src/gh-tool/repo.ts +5 -78
- package/src/gh-tool/types.ts +1 -1
- package/src/gh-tool/workflow.ts +123 -214
- package/src/observability-tool/shared.ts +2 -1
- package/src/shared/content-security.ts +10 -1
- package/src/shared/credential-patterns.ts +1 -1
- package/dist/gh-tool/api.d.ts.map +0 -1
- package/dist/gh-tool/pr/check-status-cache.d.ts +0 -6
- package/dist/gh-tool/pr/check-status-cache.d.ts.map +0 -1
- package/dist/gh-tool/service.d.ts.map +0 -1
- package/src/gh-tool/pr/check-status-cache.ts +0 -142
package/src/gh-tool/pr/core.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { Clock, Console,
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { Clock, Console, Effect, Option, Result } from "effect";
|
|
3
2
|
|
|
4
3
|
import type {
|
|
5
4
|
BranchPRDetail,
|
|
@@ -9,24 +8,19 @@ import type {
|
|
|
9
8
|
MergeResult,
|
|
10
9
|
MergeStrategy,
|
|
11
10
|
PRInfo,
|
|
12
|
-
PRViewInfo,
|
|
13
11
|
RerunCheckAttempt,
|
|
14
12
|
RerunChecksReport,
|
|
15
13
|
RerunChecksRun,
|
|
16
14
|
RerunRetryEvidence,
|
|
17
|
-
WorkflowRunDetail,
|
|
18
15
|
} from "#gh/types";
|
|
19
16
|
|
|
20
17
|
import type { GitHubAuthError, GitHubNotFoundError } from "#gh/errors";
|
|
21
18
|
import { GitHubCommandError, GitHubMergeError, GitHubTimeoutError } from "#gh/errors";
|
|
22
|
-
import { GitHubService } from "#gh/
|
|
23
|
-
import { resolveGitHubToken } from "#gh/api";
|
|
24
|
-
import type { GhResult } from "#gh/service";
|
|
19
|
+
import { GitHubService } from "#gh/read/transport";
|
|
25
20
|
import { logFormatted, logText } from "#shared";
|
|
26
21
|
import { redactSensitiveText } from "#shared/content-security";
|
|
27
22
|
|
|
28
|
-
import type { ButStatusJson
|
|
29
|
-
import { pollUntilResolved } from "#shared/poll-until-resolved";
|
|
23
|
+
import type { ButStatusJson } from "./helpers";
|
|
30
24
|
import { runLocalCommand } from "./helpers";
|
|
31
25
|
import { readStack } from "./stack-read";
|
|
32
26
|
import {
|
|
@@ -39,17 +33,19 @@ import {
|
|
|
39
33
|
parseRawJobLogs,
|
|
40
34
|
readJobLogApi,
|
|
41
35
|
} from "#gh/workflow";
|
|
42
|
-
import {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
} from "
|
|
36
|
+
import { reads, resolveRepo } from "#gh/read/reads";
|
|
37
|
+
import { waits } from "#gh/read/waits";
|
|
38
|
+
import { project } from "#gh/read/porcelain";
|
|
39
|
+
import { writes } from "#gh/read/writes";
|
|
40
|
+
import { passthrough } from "#gh/read/catalog";
|
|
41
|
+
import type { AsyncMergeResult } from "#gh/read/catalog";
|
|
42
|
+
import { repoSlug } from "#gh/read/keys";
|
|
43
|
+
import { stableHead as collectWithStableState } from "./snapshot";
|
|
44
|
+
export { stableHead as collectWithStableState } from "./snapshot";
|
|
47
45
|
|
|
48
|
-
const CHECK_JSON_FIELDS = "name,state,bucket,link";
|
|
49
46
|
const LONG_LIVED_BRANCHES = new Set(["main", "master", "develop", "staging", "production"]);
|
|
50
|
-
const STABLE_SNAPSHOT_ATTEMPTS = 3;
|
|
51
47
|
const GITHUB_ACTIONS_RUN_ID_RE = /github\.com\/[^/]+\/[^/]+\/actions\/runs\/(\d+)/;
|
|
52
|
-
const MERGE_SHA_POLL_INTERVAL_MS =
|
|
48
|
+
const MERGE_SHA_POLL_INTERVAL_MS = 5000;
|
|
53
49
|
const MERGE_SHA_WAIT_SECONDS = 5;
|
|
54
50
|
const isFullCommitSha = (value: unknown): value is string =>
|
|
55
51
|
typeof value === "string" && value.length === 40 && /^[0-9a-f]+$/i.test(value);
|
|
@@ -101,26 +97,20 @@ const resolveMergeSha = Effect.fn("pr.resolveMergeSha")(function* (
|
|
|
101
97
|
return knownSha;
|
|
102
98
|
}
|
|
103
99
|
|
|
104
|
-
const gh = yield* GitHubService;
|
|
105
100
|
const readMergeSha = () =>
|
|
106
|
-
|
|
107
|
-
.
|
|
108
|
-
"pr",
|
|
109
|
-
"view",
|
|
110
|
-
String(pr),
|
|
111
|
-
"--json",
|
|
112
|
-
"mergeCommit",
|
|
113
|
-
])
|
|
101
|
+
reads
|
|
102
|
+
.prView(null, { by: "number", number: pr }, "origin")
|
|
114
103
|
.pipe(Effect.map((result) => result.mergeCommit?.oid ?? null));
|
|
115
104
|
const result = yield* readMergeSha().pipe(
|
|
116
105
|
Effect.andThen((initial) =>
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
106
|
+
waits
|
|
107
|
+
.until(readMergeSha(), (sha) => sha !== null, {
|
|
108
|
+
initial: { value: initial },
|
|
109
|
+
intervalMs: MERGE_SHA_POLL_INTERVAL_MS,
|
|
110
|
+
timeoutMs: MERGE_SHA_WAIT_SECONDS * 1000,
|
|
111
|
+
readAtDeadline: true,
|
|
112
|
+
})
|
|
113
|
+
.pipe(Effect.map((outcome) => (outcome.kind === "done" ? outcome.value : outcome.last))),
|
|
124
114
|
),
|
|
125
115
|
Effect.map((sha) => ({ sha, failed: false as const })),
|
|
126
116
|
Effect.catch((error) =>
|
|
@@ -218,16 +208,8 @@ const matchFailedJobForCheck = <Job extends { name: string }>(
|
|
|
218
208
|
const fetchWorkflowRunFailureContext = Effect.fn("pr.fetchWorkflowRunFailureContext")(function* (
|
|
219
209
|
runId: number,
|
|
220
210
|
) {
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
const run = yield* gh
|
|
224
|
-
.runGhJson<WorkflowRunDetail>([
|
|
225
|
-
"run",
|
|
226
|
-
"view",
|
|
227
|
-
String(runId),
|
|
228
|
-
"--json",
|
|
229
|
-
"databaseId,attempt,headSha,url,workflowName,status,conclusion,jobs",
|
|
230
|
-
])
|
|
211
|
+
const run = yield* reads
|
|
212
|
+
.runView(yield* resolveRepo(null), runId)
|
|
231
213
|
.pipe(Effect.catchTag("GitHubCommandError", () => Effect.succeed(null)));
|
|
232
214
|
|
|
233
215
|
if (run === null) {
|
|
@@ -319,52 +301,8 @@ export const fetchCheckReport = Effect.fn("pr.fetchCheckReport")(function* (
|
|
|
319
301
|
pr: number | null,
|
|
320
302
|
cache?: { headSha: string; watchPending?: boolean },
|
|
321
303
|
) {
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
const args = ["pr", "checks"];
|
|
325
|
-
if (pr !== null) {
|
|
326
|
-
args.push(String(pr));
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const directory = yield* CheckStatusCacheDirectory;
|
|
330
|
-
const now = yield* Clock.currentTimeMillis;
|
|
331
|
-
let repo: string | null = null;
|
|
332
|
-
let identity: string | null = null;
|
|
333
|
-
if (cache !== undefined && pr !== null && directory !== undefined) {
|
|
334
|
-
const repoInfo = yield* gh.getRepoInfo().pipe(Effect.result);
|
|
335
|
-
const token = yield* resolveGitHubToken().pipe(Effect.result);
|
|
336
|
-
if (Result.isSuccess(repoInfo) && Result.isSuccess(token)) {
|
|
337
|
-
const host = yield* Effect.try({
|
|
338
|
-
try: () => new URL(repoInfo.success.url).host,
|
|
339
|
-
catch: () => "",
|
|
340
|
-
}).pipe(Effect.orElseSucceed(() => ""));
|
|
341
|
-
if (host.length > 0) {
|
|
342
|
-
const cacheRepo = `${host}/${repoInfo.success.owner}/${repoInfo.success.name}`;
|
|
343
|
-
const cacheIdentity = createHash("sha256").update(token.success).digest("hex");
|
|
344
|
-
const cached = yield* Effect.tryPromise({
|
|
345
|
-
try: () =>
|
|
346
|
-
readCheckStatusCache(
|
|
347
|
-
cacheIdentity,
|
|
348
|
-
cacheRepo,
|
|
349
|
-
pr,
|
|
350
|
-
cache.headSha,
|
|
351
|
-
now,
|
|
352
|
-
cache.watchPending,
|
|
353
|
-
directory,
|
|
354
|
-
),
|
|
355
|
-
catch: () => null,
|
|
356
|
-
}).pipe(Effect.orElseSucceed(() => null));
|
|
357
|
-
if (cached !== null)
|
|
358
|
-
return { checksStatus: "reported", checks: cached } satisfies CheckReport;
|
|
359
|
-
repo = cacheRepo;
|
|
360
|
-
identity = cacheIdentity;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const fetchedAt = yield* Clock.currentTimeMillis;
|
|
366
|
-
const checks = yield* gh
|
|
367
|
-
.runGhJson<CheckResult[]>([...args, "--json", CHECK_JSON_FIELDS])
|
|
304
|
+
const checks = yield* reads
|
|
305
|
+
.prChecks(yield* resolveRepo(null), pr, cache?.headSha ?? null)
|
|
368
306
|
.pipe(
|
|
369
307
|
Effect.catchTag("GitHubCommandError", (error) =>
|
|
370
308
|
NO_CHECKS_REPORTED_RE.test(error.stderr) || NO_CHECKS_REPORTED_RE.test(error.message)
|
|
@@ -379,19 +317,6 @@ export const fetchCheckReport = Effect.fn("pr.fetchCheckReport")(function* (
|
|
|
379
317
|
checks: null,
|
|
380
318
|
} satisfies CheckReport;
|
|
381
319
|
}
|
|
382
|
-
if (
|
|
383
|
-
cache !== undefined &&
|
|
384
|
-
pr !== null &&
|
|
385
|
-
repo !== null &&
|
|
386
|
-
identity !== null &&
|
|
387
|
-
checks.length > 0
|
|
388
|
-
) {
|
|
389
|
-
yield* Effect.tryPromise({
|
|
390
|
-
try: () =>
|
|
391
|
-
writeCheckStatusCache(identity, repo, pr, cache.headSha, checks, fetchedAt, directory),
|
|
392
|
-
catch: () => undefined,
|
|
393
|
-
}).pipe(Effect.orElseSucceed(() => undefined));
|
|
394
|
-
}
|
|
395
320
|
return { checksStatus: "reported", checks } satisfies CheckReport;
|
|
396
321
|
});
|
|
397
322
|
|
|
@@ -428,7 +353,7 @@ const buildFailedChecksReport = Effect.fn("pr.buildFailedChecksReport")(function
|
|
|
428
353
|
runIds,
|
|
429
354
|
(runId) =>
|
|
430
355
|
fetchWorkflowRunFailureContext(runId).pipe(Effect.map((found) => [runId, found] as const)),
|
|
431
|
-
{ concurrency:
|
|
356
|
+
{ concurrency: 2 },
|
|
432
357
|
),
|
|
433
358
|
);
|
|
434
359
|
|
|
@@ -453,7 +378,7 @@ const buildFailedChecksReport = Effect.fn("pr.buildFailedChecksReport")(function
|
|
|
453
378
|
const failedStepLogs = yield* fetchJobLogs({
|
|
454
379
|
runId,
|
|
455
380
|
job: matchedJob.name,
|
|
456
|
-
|
|
381
|
+
jobRef: { id: matchedJob.databaseId, status: matchedJob.status },
|
|
457
382
|
failedStepNames: matchedJob.failedSteps,
|
|
458
383
|
steps: found?.stepsByJob.get(matchedJob.databaseId) ?? [],
|
|
459
384
|
failedStepsOnly: true,
|
|
@@ -557,19 +482,32 @@ const buildFailedChecksReport = Effect.fn("pr.buildFailedChecksReport")(function
|
|
|
557
482
|
};
|
|
558
483
|
});
|
|
559
484
|
|
|
560
|
-
export const fetchPRView = Effect.fn("pr.fetchPRView")(function* (
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
"--json",
|
|
569
|
-
"number,url,title,headRefName,baseRefName,headRefOid,state,isDraft,mergeable,body,author,reviewDecision,reviewRequests",
|
|
485
|
+
export const fetchPRView = Effect.fn("pr.fetchPRView")(function* (
|
|
486
|
+
prNumber: number | null,
|
|
487
|
+
consistency: "cached" | "origin" = "cached",
|
|
488
|
+
) {
|
|
489
|
+
const raw = yield* reads.prView(
|
|
490
|
+
yield* resolveRepo(null),
|
|
491
|
+
prNumber === null ? { by: "currentBranch" } : { by: "number", number: prNumber },
|
|
492
|
+
consistency,
|
|
570
493
|
);
|
|
571
|
-
|
|
572
|
-
|
|
494
|
+
const info = project(raw, [
|
|
495
|
+
"number",
|
|
496
|
+
"url",
|
|
497
|
+
"title",
|
|
498
|
+
"headRefName",
|
|
499
|
+
"baseRefName",
|
|
500
|
+
"headRefOid",
|
|
501
|
+
"state",
|
|
502
|
+
"isDraft",
|
|
503
|
+
"mergeable",
|
|
504
|
+
"body",
|
|
505
|
+
"author",
|
|
506
|
+
"reviewDecision",
|
|
507
|
+
"reviewRequests",
|
|
508
|
+
"headSha",
|
|
509
|
+
"baseSha",
|
|
510
|
+
]);
|
|
573
511
|
return {
|
|
574
512
|
...info,
|
|
575
513
|
body: info.body ?? "",
|
|
@@ -578,17 +516,15 @@ export const fetchPRView = Effect.fn("pr.fetchPRView")(function* (prNumber: numb
|
|
|
578
516
|
};
|
|
579
517
|
});
|
|
580
518
|
|
|
581
|
-
export const viewPR = Effect.fn("pr.viewPR")(function* (
|
|
519
|
+
export const viewPR = Effect.fn("pr.viewPR")(function* (
|
|
520
|
+
prNumber: number | null,
|
|
521
|
+
consistency: "cached" | "origin" = "cached",
|
|
522
|
+
) {
|
|
582
523
|
const gh = yield* GitHubService;
|
|
583
524
|
|
|
584
|
-
const info = yield* fetchPRView(prNumber);
|
|
525
|
+
const info = yield* fetchPRView(prNumber, consistency);
|
|
585
526
|
const repo = yield* gh.getRepoInfo();
|
|
586
|
-
const baseSha = yield*
|
|
587
|
-
"api",
|
|
588
|
-
`repos/${repo.owner}/${repo.name}/pulls/${info.number}`,
|
|
589
|
-
"--jq",
|
|
590
|
-
".base.sha",
|
|
591
|
-
]);
|
|
527
|
+
const baseSha = yield* passthrough.baseSha(repo, info.number);
|
|
592
528
|
return {
|
|
593
529
|
...info,
|
|
594
530
|
baseSha: baseSha.stdout.trim() || null,
|
|
@@ -648,7 +584,7 @@ export const detectPRStatus = Effect.fn("pr.detectPRStatus")(function* () {
|
|
|
648
584
|
),
|
|
649
585
|
];
|
|
650
586
|
|
|
651
|
-
const
|
|
587
|
+
const repo = yield* resolveRepo(null);
|
|
652
588
|
|
|
653
589
|
type BranchResult = {
|
|
654
590
|
branch: string;
|
|
@@ -665,40 +601,41 @@ export const detectPRStatus = Effect.fn("pr.detectPRStatus")(function* () {
|
|
|
665
601
|
branchNames.map((branchName) =>
|
|
666
602
|
Effect.all(
|
|
667
603
|
{
|
|
668
|
-
openPr:
|
|
669
|
-
.
|
|
670
|
-
"
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
"1",
|
|
678
|
-
])
|
|
604
|
+
openPr: reads
|
|
605
|
+
.prList(repo, {
|
|
606
|
+
state: "open",
|
|
607
|
+
limit: 1,
|
|
608
|
+
author: null,
|
|
609
|
+
base: null,
|
|
610
|
+
head: branchName,
|
|
611
|
+
search: null,
|
|
612
|
+
})
|
|
679
613
|
.pipe(
|
|
680
|
-
Effect.map((prs) =>
|
|
614
|
+
Effect.map((prs) =>
|
|
615
|
+
prs[0] === undefined
|
|
616
|
+
? null
|
|
617
|
+
: project(prs[0], [
|
|
618
|
+
"number",
|
|
619
|
+
"url",
|
|
620
|
+
"title",
|
|
621
|
+
"headRefName",
|
|
622
|
+
"baseRefName",
|
|
623
|
+
"state",
|
|
624
|
+
"isDraft",
|
|
625
|
+
"mergeable",
|
|
626
|
+
]),
|
|
627
|
+
),
|
|
681
628
|
Effect.catchTag("GitHubCommandError", () => Effect.succeed(null)),
|
|
682
629
|
),
|
|
683
|
-
closedPr:
|
|
684
|
-
.
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
"list",
|
|
693
|
-
"--head",
|
|
694
|
-
branchName,
|
|
695
|
-
"--state",
|
|
696
|
-
"closed",
|
|
697
|
-
"--json",
|
|
698
|
-
"number,url,state",
|
|
699
|
-
"--limit",
|
|
700
|
-
"1",
|
|
701
|
-
])
|
|
630
|
+
closedPr: reads
|
|
631
|
+
.prList(repo, {
|
|
632
|
+
state: "closed",
|
|
633
|
+
limit: 1,
|
|
634
|
+
author: null,
|
|
635
|
+
base: null,
|
|
636
|
+
head: branchName,
|
|
637
|
+
search: null,
|
|
638
|
+
})
|
|
702
639
|
.pipe(
|
|
703
640
|
Effect.map((prs) => {
|
|
704
641
|
const pr = prs[0];
|
|
@@ -716,7 +653,7 @@ export const detectPRStatus = Effect.fn("pr.detectPRStatus")(function* () {
|
|
|
716
653
|
Effect.catch(() => Effect.succeed(false)),
|
|
717
654
|
),
|
|
718
655
|
},
|
|
719
|
-
{ concurrency:
|
|
656
|
+
{ concurrency: 2 },
|
|
720
657
|
).pipe(
|
|
721
658
|
Effect.map((r): BranchResult => ({
|
|
722
659
|
branch: branchName,
|
|
@@ -724,7 +661,7 @@ export const detectPRStatus = Effect.fn("pr.detectPRStatus")(function* () {
|
|
|
724
661
|
})),
|
|
725
662
|
),
|
|
726
663
|
),
|
|
727
|
-
{ concurrency:
|
|
664
|
+
{ concurrency: 2 },
|
|
728
665
|
);
|
|
729
666
|
|
|
730
667
|
const foundPrs = branchResults.flatMap((r) => (r.openPr === null ? [] : [r.openPr]));
|
|
@@ -762,59 +699,44 @@ export const listPRs = Effect.fn("pr.listPRs")(function* (opts: {
|
|
|
762
699
|
head: string | null;
|
|
763
700
|
search: string | null;
|
|
764
701
|
}) {
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
return yield* gh.runGhJson<PRInfo[]>(args);
|
|
702
|
+
const values = yield* reads.prList(yield* resolveRepo(null), opts);
|
|
703
|
+
return values.map((value) =>
|
|
704
|
+
project(value, [
|
|
705
|
+
"number",
|
|
706
|
+
"url",
|
|
707
|
+
"title",
|
|
708
|
+
"headRefName",
|
|
709
|
+
"baseRefName",
|
|
710
|
+
"state",
|
|
711
|
+
"isDraft",
|
|
712
|
+
"author",
|
|
713
|
+
"createdAt",
|
|
714
|
+
"reviewDecision",
|
|
715
|
+
]),
|
|
716
|
+
);
|
|
781
717
|
});
|
|
782
718
|
|
|
783
719
|
// GitHub recomputes mergeability asynchronously after CI; poll until it settles out of "UNKNOWN".
|
|
784
720
|
const MAX_MERGEABLE_WAIT_SECONDS = 180;
|
|
785
|
-
const MERGEABLE_POLL_INTERVAL_MS =
|
|
721
|
+
const MERGEABLE_POLL_INTERVAL_MS = 5000;
|
|
786
722
|
|
|
787
723
|
export const waitForMergeable = Effect.fn("pr.waitForMergeable")(function* (
|
|
788
724
|
pr: number | null,
|
|
789
725
|
timeoutSeconds: number,
|
|
790
726
|
) {
|
|
791
727
|
const cappedSeconds = Math.min(timeoutSeconds, MAX_MERGEABLE_WAIT_SECONDS);
|
|
792
|
-
const
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
timedOut = true;
|
|
805
|
-
return;
|
|
806
|
-
}
|
|
807
|
-
// Cap the sleep to the remaining budget so the total wait doesn't overshoot the deadline.
|
|
808
|
-
const remaining = deadlineMs - Number(now);
|
|
809
|
-
yield* Effect.sleep(Duration.millis(Math.min(MERGEABLE_POLL_INTERVAL_MS, remaining)));
|
|
810
|
-
latest = yield* fetchPRView(pr).pipe(
|
|
811
|
-
Effect.map((info) => ({ ...info, baseSha: latest.baseSha })),
|
|
812
|
-
);
|
|
813
|
-
}),
|
|
814
|
-
step: () => undefined,
|
|
815
|
-
});
|
|
816
|
-
|
|
817
|
-
return latest;
|
|
728
|
+
const initial = yield* viewPR(pr, "origin");
|
|
729
|
+
const outcome = yield* waits.until(
|
|
730
|
+
fetchPRView(pr, "origin").pipe(Effect.map((info) => ({ ...info, baseSha: initial.baseSha }))),
|
|
731
|
+
(info) => info.mergeable !== "UNKNOWN",
|
|
732
|
+
{
|
|
733
|
+
initial: { value: initial },
|
|
734
|
+
intervalMs: MERGEABLE_POLL_INTERVAL_MS,
|
|
735
|
+
timeoutMs: cappedSeconds * 1000,
|
|
736
|
+
readAtDeadline: true,
|
|
737
|
+
},
|
|
738
|
+
);
|
|
739
|
+
return outcome.kind === "done" ? outcome.value : outcome.last;
|
|
818
740
|
});
|
|
819
741
|
|
|
820
742
|
export const createPR = Effect.fn("pr.createPR")(function* (opts: {
|
|
@@ -839,24 +761,12 @@ export const createPR = Effect.fn("pr.createPR")(function* (opts: {
|
|
|
839
761
|
headSeparator === -1 ? repo.owner : head.slice(0, headSeparator)
|
|
840
762
|
).toLowerCase();
|
|
841
763
|
const headBranch = headSeparator === -1 ? head : head.slice(headSeparator + 1);
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
"
|
|
845
|
-
"
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
"open",
|
|
849
|
-
"--head",
|
|
850
|
-
headBranch,
|
|
851
|
-
"--base",
|
|
852
|
-
baseBranch,
|
|
853
|
-
"--json",
|
|
854
|
-
"number,url,title,headRefName,baseRefName,state,isDraft,mergeable,headRepositoryOwner",
|
|
855
|
-
"--limit",
|
|
856
|
-
"2",
|
|
857
|
-
];
|
|
858
|
-
type Candidate = PRInfo & { headRepositoryOwner?: { login: string } };
|
|
859
|
-
const candidates = yield* gh.runGhJson<Candidate[]>(lookupArgs);
|
|
764
|
+
const lookup = reads.prList(
|
|
765
|
+
repo,
|
|
766
|
+
{ state: "open", head: headBranch, base: baseBranch, limit: 2, author: null, search: null },
|
|
767
|
+
"origin",
|
|
768
|
+
);
|
|
769
|
+
const candidates = yield* lookup;
|
|
860
770
|
// The limited query may include another fork's branch. Refuse a saturated result
|
|
861
771
|
// rather than assuming an owner-filtered match beyond the limit does not exist.
|
|
862
772
|
if (candidates.length > 1) {
|
|
@@ -910,7 +820,7 @@ export const createPR = Effect.fn("pr.createPR")(function* (opts: {
|
|
|
910
820
|
hint: "Use --update-if-exists to update it, or 'pr edit --pr <number>'.",
|
|
911
821
|
});
|
|
912
822
|
}
|
|
913
|
-
const current = yield* fetchPRView(existing.number);
|
|
823
|
+
const current = yield* fetchPRView(existing.number, "origin");
|
|
914
824
|
if (
|
|
915
825
|
current.state !== "OPEN" ||
|
|
916
826
|
current.headRefName !== headBranch ||
|
|
@@ -952,14 +862,14 @@ export const createPR = Effect.fn("pr.createPR")(function* (opts: {
|
|
|
952
862
|
createArgs.push("--draft");
|
|
953
863
|
}
|
|
954
864
|
|
|
955
|
-
const createResult = yield*
|
|
865
|
+
const createResult = yield* writes.run(createArgs, [`repo:${repoSlug(repo)}`]);
|
|
956
866
|
|
|
957
867
|
const urlMatch = createResult.stdout.match(/\/pull\/(\d+)/);
|
|
958
868
|
if (urlMatch?.[1]) {
|
|
959
869
|
return { ...(yield* viewPR(Number(urlMatch[1]))), updated: false };
|
|
960
870
|
}
|
|
961
871
|
|
|
962
|
-
const prs = yield*
|
|
872
|
+
const prs = yield* lookup;
|
|
963
873
|
if (
|
|
964
874
|
prs.length === 1 &&
|
|
965
875
|
prs[0]?.state === "OPEN" &&
|
|
@@ -983,14 +893,9 @@ export const createPR = Effect.fn("pr.createPR")(function* (opts: {
|
|
|
983
893
|
// GitHub rejects GraphQL `mergePullRequest` for a PR that belongs to a native stack and points at
|
|
984
894
|
// the asynchronous REST merge instead. `gh pr merge` only speaks GraphQL.
|
|
985
895
|
const ASYNC_MERGE_REQUIRED_RE = /asynchronous merge/i;
|
|
986
|
-
const ASYNC_MERGE_POLL_INTERVAL_MS =
|
|
896
|
+
const ASYNC_MERGE_POLL_INTERVAL_MS = 5000;
|
|
987
897
|
const MAX_ASYNC_MERGE_WAIT_SECONDS = 120;
|
|
988
898
|
|
|
989
|
-
type AsyncMergeResult = {
|
|
990
|
-
status: "pending" | "merged" | "enqueued" | "failed";
|
|
991
|
-
details?: { message?: string; uuid?: string; sha?: string };
|
|
992
|
-
};
|
|
993
|
-
|
|
994
899
|
const asyncMergeFailure = (
|
|
995
900
|
pr: number,
|
|
996
901
|
message: string,
|
|
@@ -1012,24 +917,24 @@ const mergeViaAsyncApi = Effect.fn("pr.mergeViaAsyncApi")(function* (opts: {
|
|
|
1012
917
|
const repo = yield* gh.getRepoInfo();
|
|
1013
918
|
const asyncPath = `repos/${repo.owner}/${repo.name}/pulls/${opts.pr}/merge-async`;
|
|
1014
919
|
|
|
1015
|
-
let latest = yield*
|
|
1016
|
-
"api",
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
asyncPath,
|
|
1020
|
-
"-f",
|
|
1021
|
-
`merge_method=${opts.strategy}`,
|
|
1022
|
-
]);
|
|
920
|
+
let latest = yield* writes.json<AsyncMergeResult>(
|
|
921
|
+
["api", "--method", "PUT", asyncPath, "-f", `merge_method=${opts.strategy}`],
|
|
922
|
+
[`pr:${repoSlug(repo)}#${opts.pr}`, `pulls:${repoSlug(repo)}`],
|
|
923
|
+
);
|
|
1023
924
|
|
|
1024
925
|
const uuid = latest.details?.uuid;
|
|
1025
926
|
if (uuid !== undefined) {
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
927
|
+
const outcome = yield* waits.until(
|
|
928
|
+
passthrough.asyncMergeStatus(`${asyncPath}/${uuid}`),
|
|
929
|
+
(value) => value.status !== "pending",
|
|
930
|
+
{
|
|
931
|
+
initial: { value: latest },
|
|
932
|
+
intervalMs: ASYNC_MERGE_POLL_INTERVAL_MS,
|
|
933
|
+
timeoutMs: MAX_ASYNC_MERGE_WAIT_SECONDS * 1000,
|
|
934
|
+
readAtDeadline: true,
|
|
935
|
+
},
|
|
936
|
+
);
|
|
937
|
+
latest = outcome.kind === "done" ? outcome.value : outcome.last;
|
|
1033
938
|
}
|
|
1034
939
|
|
|
1035
940
|
if (latest.status === "merged") {
|
|
@@ -1079,27 +984,18 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
|
|
|
1079
984
|
});
|
|
1080
985
|
}
|
|
1081
986
|
|
|
1082
|
-
const info = yield*
|
|
1083
|
-
.
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
? error
|
|
1095
|
-
: new GitHubMergeError({
|
|
1096
|
-
message: `Could not verify the head commit of PR #${opts.pr}: ${error.message}`,
|
|
1097
|
-
reason: "unknown",
|
|
1098
|
-
hint: "The merge is refused while the head is unknown. Re-read the PR and verify its reviewed head before retrying.",
|
|
1099
|
-
nextCommand: `agent-tools-gh pr view --pr ${opts.pr}`,
|
|
1100
|
-
}),
|
|
1101
|
-
),
|
|
1102
|
-
);
|
|
987
|
+
const info = yield* reads.prView(null, { by: "number", number: opts.pr }, "origin").pipe(
|
|
988
|
+
Effect.mapError((error) =>
|
|
989
|
+
matchHeadCommit === undefined
|
|
990
|
+
? error
|
|
991
|
+
: new GitHubMergeError({
|
|
992
|
+
message: `Could not verify the head commit of PR #${opts.pr}: ${error.message}`,
|
|
993
|
+
reason: "unknown",
|
|
994
|
+
hint: "The merge is refused while the head is unknown. Re-read the PR and verify its reviewed head before retrying.",
|
|
995
|
+
nextCommand: `agent-tools-gh pr view --pr ${opts.pr}`,
|
|
996
|
+
}),
|
|
997
|
+
),
|
|
998
|
+
);
|
|
1103
999
|
|
|
1104
1000
|
if (
|
|
1105
1001
|
matchHeadCommit !== undefined &&
|
|
@@ -1181,18 +1077,18 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
|
|
|
1181
1077
|
// branch if EVERY retarget succeeds (fail-closed).
|
|
1182
1078
|
const dependentOpenPrs =
|
|
1183
1079
|
opts.deleteBranch && !headIsLongLived && info.headRefName
|
|
1184
|
-
? yield*
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
"
|
|
1195
|
-
|
|
1080
|
+
? yield* reads.prList(
|
|
1081
|
+
yield* resolveRepo(null),
|
|
1082
|
+
{
|
|
1083
|
+
state: "open",
|
|
1084
|
+
limit: 100,
|
|
1085
|
+
base: info.headRefName,
|
|
1086
|
+
head: null,
|
|
1087
|
+
author: null,
|
|
1088
|
+
search: null,
|
|
1089
|
+
},
|
|
1090
|
+
"origin",
|
|
1091
|
+
)
|
|
1196
1092
|
: [];
|
|
1197
1093
|
|
|
1198
1094
|
if (!opts.confirm) {
|
|
@@ -1242,15 +1138,18 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
|
|
|
1242
1138
|
// closed.
|
|
1243
1139
|
if (willDeleteBranch && dependentOpenPrs.length > 0 && repo) {
|
|
1244
1140
|
for (const child of dependentOpenPrs) {
|
|
1245
|
-
const retargeted = yield*
|
|
1246
|
-
.
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1141
|
+
const retargeted = yield* writes
|
|
1142
|
+
.run(
|
|
1143
|
+
[
|
|
1144
|
+
"api",
|
|
1145
|
+
"--method",
|
|
1146
|
+
"PATCH",
|
|
1147
|
+
`repos/${repo.owner}/${repo.name}/pulls/${child.number}`,
|
|
1148
|
+
"-f",
|
|
1149
|
+
`base=${info.baseRefName}`,
|
|
1150
|
+
],
|
|
1151
|
+
[`pr:${repoSlug(repo)}#${child.number}`, `pulls:${repoSlug(repo)}`],
|
|
1152
|
+
)
|
|
1254
1153
|
.pipe(
|
|
1255
1154
|
Effect.as(true),
|
|
1256
1155
|
Effect.orElseSucceed(() => false),
|
|
@@ -1272,15 +1171,18 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
|
|
|
1272
1171
|
return failed;
|
|
1273
1172
|
}
|
|
1274
1173
|
for (const child of retargetedChildren) {
|
|
1275
|
-
const rolledBack = yield*
|
|
1276
|
-
.
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1174
|
+
const rolledBack = yield* writes
|
|
1175
|
+
.run(
|
|
1176
|
+
[
|
|
1177
|
+
"api",
|
|
1178
|
+
"--method",
|
|
1179
|
+
"PATCH",
|
|
1180
|
+
`repos/${repo.owner}/${repo.name}/pulls/${child}`,
|
|
1181
|
+
"-f",
|
|
1182
|
+
`base=${info.headRefName}`,
|
|
1183
|
+
],
|
|
1184
|
+
[`pr:${repoSlug(repo)}#${child}`, `pulls:${repoSlug(repo)}`],
|
|
1185
|
+
)
|
|
1284
1186
|
.pipe(
|
|
1285
1187
|
Effect.as(true),
|
|
1286
1188
|
Effect.orElseSucceed(() => false),
|
|
@@ -1292,13 +1194,14 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
|
|
|
1292
1194
|
return failed;
|
|
1293
1195
|
});
|
|
1294
1196
|
|
|
1197
|
+
const mergeRepo = yield* resolveRepo(null);
|
|
1295
1198
|
const mergeArgs = ["pr", "merge", String(opts.pr), `--${opts.strategy}`];
|
|
1296
1199
|
if (matchHeadCommit !== undefined) {
|
|
1297
1200
|
mergeArgs.push("--match-head-commit", matchHeadCommit);
|
|
1298
1201
|
}
|
|
1299
1202
|
|
|
1300
|
-
const shaFromAsyncMerge = yield*
|
|
1301
|
-
.
|
|
1203
|
+
const shaFromAsyncMerge = yield* writes
|
|
1204
|
+
.run(mergeArgs, [`pr:${repoSlug(mergeRepo)}#${opts.pr}`, `pulls:${repoSlug(mergeRepo)}`])
|
|
1302
1205
|
.pipe(
|
|
1303
1206
|
Effect.map(() => null),
|
|
1304
1207
|
Effect.catchTag("GitHubCommandError", (error) =>
|
|
@@ -1400,13 +1303,16 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
|
|
|
1400
1303
|
// `gh pr merge --delete-branch` aborts its remote delete when the head branch is checked out in a
|
|
1401
1304
|
// worktree; deleting the remote ref explicitly is worktree-independent. Local cleanup is separate.
|
|
1402
1305
|
if (willDeleteBranch && repo && info.headRefName) {
|
|
1403
|
-
const remoteDeleted = yield*
|
|
1404
|
-
.
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1306
|
+
const remoteDeleted = yield* writes
|
|
1307
|
+
.run(
|
|
1308
|
+
[
|
|
1309
|
+
"api",
|
|
1310
|
+
"--method",
|
|
1311
|
+
"DELETE",
|
|
1312
|
+
`repos/${repo.owner}/${repo.name}/git/refs/heads/${info.headRefName}`,
|
|
1313
|
+
],
|
|
1314
|
+
[`pr:${repoSlug(repo)}#${opts.pr}`, `pulls:${repoSlug(repo)}`],
|
|
1315
|
+
)
|
|
1410
1316
|
.pipe(
|
|
1411
1317
|
Effect.as(true),
|
|
1412
1318
|
Effect.orElseSucceed(() => false),
|
|
@@ -1434,8 +1340,6 @@ export const closePR = Effect.fn("pr.closePR")(function* (opts: {
|
|
|
1434
1340
|
comment: string | null;
|
|
1435
1341
|
deleteBranch: boolean;
|
|
1436
1342
|
}) {
|
|
1437
|
-
const gh = yield* GitHubService;
|
|
1438
|
-
|
|
1439
1343
|
const args = ["pr", "close", String(opts.pr)];
|
|
1440
1344
|
|
|
1441
1345
|
if (opts.comment !== null) {
|
|
@@ -1446,15 +1350,18 @@ export const closePR = Effect.fn("pr.closePR")(function* (opts: {
|
|
|
1446
1350
|
args.push("--delete-branch");
|
|
1447
1351
|
}
|
|
1448
1352
|
|
|
1449
|
-
yield*
|
|
1353
|
+
const repo = yield* resolveRepo(null);
|
|
1354
|
+
yield* writes.run(args, [
|
|
1355
|
+
`pr:${repoSlug(repo)}#${opts.pr}`,
|
|
1356
|
+
`pulls:${repoSlug(repo)}`,
|
|
1357
|
+
`issue-comments:${repoSlug(repo)}#${opts.pr}`,
|
|
1358
|
+
]);
|
|
1450
1359
|
|
|
1451
1360
|
return yield* viewPR(opts.pr);
|
|
1452
1361
|
});
|
|
1453
1362
|
|
|
1454
1363
|
export const readyPR = Effect.fn("pr.readyPR")(function* (opts: { pr: number | null }) {
|
|
1455
|
-
const
|
|
1456
|
-
|
|
1457
|
-
const info = yield* viewPR(opts.pr);
|
|
1364
|
+
const info = yield* viewPR(opts.pr, "origin");
|
|
1458
1365
|
|
|
1459
1366
|
// Idempotent: a PR that's already ready needs no mutation, and `gh pr ready` on a
|
|
1460
1367
|
// non-draft PR is a no-op that would just add a pointless call.
|
|
@@ -1462,22 +1369,28 @@ export const readyPR = Effect.fn("pr.readyPR")(function* (opts: { pr: number | n
|
|
|
1462
1369
|
return { ...info, wasAlreadyReady: true };
|
|
1463
1370
|
}
|
|
1464
1371
|
|
|
1465
|
-
yield*
|
|
1372
|
+
const repo = yield* resolveRepo(null);
|
|
1373
|
+
yield* writes.run(
|
|
1374
|
+
["pr", "ready", String(info.number)],
|
|
1375
|
+
[`pr:${repoSlug(repo)}#${info.number}`, `pulls:${repoSlug(repo)}`],
|
|
1376
|
+
);
|
|
1466
1377
|
|
|
1467
1378
|
const updated = yield* viewPR(info.number);
|
|
1468
1379
|
return { ...updated, wasAlreadyReady: false };
|
|
1469
1380
|
});
|
|
1470
1381
|
|
|
1471
1382
|
export const draftPR = Effect.fn("pr.draftPR")(function* (opts: { pr: number | null }) {
|
|
1472
|
-
const
|
|
1473
|
-
|
|
1474
|
-
const info = yield* viewPR(opts.pr);
|
|
1383
|
+
const info = yield* viewPR(opts.pr, "origin");
|
|
1475
1384
|
|
|
1476
1385
|
if (info.isDraft) {
|
|
1477
1386
|
return { ...info, wasAlreadyDraft: true };
|
|
1478
1387
|
}
|
|
1479
1388
|
|
|
1480
|
-
yield*
|
|
1389
|
+
const repo = yield* resolveRepo(null);
|
|
1390
|
+
yield* writes.run(
|
|
1391
|
+
["pr", "ready", String(info.number), "--undo"],
|
|
1392
|
+
[`pr:${repoSlug(repo)}#${info.number}`, `pulls:${repoSlug(repo)}`],
|
|
1393
|
+
);
|
|
1481
1394
|
|
|
1482
1395
|
const updated = yield* viewPR(info.number);
|
|
1483
1396
|
return { ...updated, wasAlreadyDraft: false };
|
|
@@ -1540,7 +1453,7 @@ export const editPR = Effect.fn("pr.editPR")(function* (opts: {
|
|
|
1540
1453
|
editArgs.push("-f", `base=${opts.base}`);
|
|
1541
1454
|
}
|
|
1542
1455
|
|
|
1543
|
-
yield*
|
|
1456
|
+
yield* writes.run(editArgs, [`pr:${repoSlug(repo)}#${opts.pr}`, `pulls:${repoSlug(repo)}`]);
|
|
1544
1457
|
|
|
1545
1458
|
return yield* viewPR(opts.pr);
|
|
1546
1459
|
});
|
|
@@ -1552,62 +1465,44 @@ export const fetchChecks = Effect.fn("pr.fetchChecks")(function* (
|
|
|
1552
1465
|
timeoutSeconds: number,
|
|
1553
1466
|
quiet = false,
|
|
1554
1467
|
) {
|
|
1555
|
-
const gh = yield* GitHubService;
|
|
1556
|
-
|
|
1557
|
-
const args = ["pr", "checks"];
|
|
1558
|
-
if (pr !== null) {
|
|
1559
|
-
args.push(String(pr));
|
|
1560
|
-
}
|
|
1561
|
-
|
|
1562
1468
|
if (watch) {
|
|
1563
|
-
const
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
// within the caller's timeout, then report the final snapshot.
|
|
1570
|
-
let watchResult: GhResult | null = null;
|
|
1571
|
-
let registered = false;
|
|
1572
|
-
let graceExpired = false;
|
|
1573
|
-
const watchThroughRegistration = Effect.gen(function* () {
|
|
1574
|
-
const graceStart = yield* Clock.currentTimeMillis;
|
|
1575
|
-
const graceDeadlineMs =
|
|
1576
|
-
Number(graceStart) + Math.min(CHECK_REGISTRATION_GRACE_SECONDS, timeoutSeconds) * 1000;
|
|
1577
|
-
yield* Effect.whileLoop({
|
|
1578
|
-
while: () => !registered && !graceExpired,
|
|
1579
|
-
body: () =>
|
|
1580
|
-
gh.runGh(watchArgs).pipe(
|
|
1581
|
-
Effect.flatMap((result) => {
|
|
1582
|
-
watchResult = result;
|
|
1583
|
-
registered = true;
|
|
1584
|
-
return Effect.void;
|
|
1585
|
-
}),
|
|
1586
|
-
Effect.catchTag("GitHubCommandError", (error) =>
|
|
1587
|
-
NO_CHECKS_REPORTED_RE.test(error.stderr) || NO_CHECKS_REPORTED_RE.test(error.message)
|
|
1588
|
-
? Effect.gen(function* () {
|
|
1589
|
-
const now = yield* Clock.currentTimeMillis;
|
|
1590
|
-
if (Number(now) >= graceDeadlineMs) {
|
|
1591
|
-
graceExpired = true;
|
|
1592
|
-
return;
|
|
1593
|
-
}
|
|
1594
|
-
yield* Effect.sleep(Duration.seconds(CHECK_REGISTRATION_POLL_SECONDS));
|
|
1595
|
-
})
|
|
1596
|
-
: Effect.fail(error),
|
|
1597
|
-
),
|
|
1598
|
-
),
|
|
1599
|
-
step: () => undefined,
|
|
1600
|
-
});
|
|
1601
|
-
return watchResult;
|
|
1469
|
+
const started = Number(yield* Clock.currentTimeMillis);
|
|
1470
|
+
const graceDeadlineMs =
|
|
1471
|
+
started + Math.min(CHECK_REGISTRATION_GRACE_SECONDS, timeoutSeconds) * 1000;
|
|
1472
|
+
const poll = Effect.gen(function* () {
|
|
1473
|
+
const checks = yield* fetchCheckResults(pr);
|
|
1474
|
+
return { checks, at: Number(yield* Clock.currentTimeMillis) };
|
|
1602
1475
|
});
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1476
|
+
const watchOutcome = yield* waits
|
|
1477
|
+
.until(
|
|
1478
|
+
poll,
|
|
1479
|
+
({ checks, at }) =>
|
|
1480
|
+
checks.length === 0
|
|
1481
|
+
? at >= graceDeadlineMs
|
|
1482
|
+
: (failFast && checks.some((check) => check.bucket === "fail")) ||
|
|
1483
|
+
checks.every((check) => check.bucket !== "pending"),
|
|
1484
|
+
{ intervalMs: CHECK_REGISTRATION_POLL_SECONDS * 1000, timeoutMs: timeoutSeconds * 1000 },
|
|
1485
|
+
)
|
|
1486
|
+
.pipe(
|
|
1487
|
+
Effect.timeoutOrElse({
|
|
1488
|
+
duration: timeoutSeconds * 1000,
|
|
1489
|
+
orElse: () => Effect.succeed(null),
|
|
1490
|
+
}),
|
|
1491
|
+
);
|
|
1492
|
+
const last =
|
|
1493
|
+
watchOutcome === null
|
|
1494
|
+
? null
|
|
1495
|
+
: watchOutcome.kind === "done"
|
|
1496
|
+
? watchOutcome.value
|
|
1497
|
+
: watchOutcome.last;
|
|
1498
|
+
const graceExpired = last !== null && last.checks.length === 0 && last.at >= graceDeadlineMs;
|
|
1499
|
+
if (watchOutcome?.kind === "done" && last?.checks.some((check) => check.bucket === "fail"))
|
|
1500
|
+
return yield* new GitHubCommandError({
|
|
1501
|
+
command: buildChecksCommand(pr, true),
|
|
1502
|
+
exitCode: 1,
|
|
1503
|
+
message: "",
|
|
1504
|
+
stderr: "",
|
|
1505
|
+
});
|
|
1611
1506
|
const results = yield* fetchCheckResults(pr);
|
|
1612
1507
|
if (!quiet && graceExpired) {
|
|
1613
1508
|
const prRef = pr === null ? "<number>" : String(pr);
|
|
@@ -1618,7 +1513,10 @@ export const fetchChecks = Effect.fn("pr.fetchChecks")(function* (
|
|
|
1618
1513
|
`head instead:\n agent-tools-gh pr trigger-checks --pr ${prRef} --workflow <file.yml>`,
|
|
1619
1514
|
);
|
|
1620
1515
|
}
|
|
1621
|
-
if (
|
|
1516
|
+
if (
|
|
1517
|
+
(watchOutcome === null || watchOutcome.kind === "timedOut") &&
|
|
1518
|
+
results.some((check) => check.bucket === "pending")
|
|
1519
|
+
) {
|
|
1622
1520
|
const pending = results.filter((check) => check.bucket === "pending").length;
|
|
1623
1521
|
yield* logFormatted(results, quiet ? "json" : "toon");
|
|
1624
1522
|
return yield* new GitHubTimeoutError({
|
|
@@ -1702,23 +1600,6 @@ export const triggerChecks = Effect.fn("pr.triggerChecks")(function* (
|
|
|
1702
1600
|
};
|
|
1703
1601
|
});
|
|
1704
1602
|
|
|
1705
|
-
export const collectWithStableState = <S, A, E1, R1, E2, R2>(
|
|
1706
|
-
initial: S,
|
|
1707
|
-
collect: (state: S) => Effect.Effect<A, E1, R1>,
|
|
1708
|
-
refresh: (state: S) => Effect.Effect<S, E2, R2>,
|
|
1709
|
-
unchanged: (before: S, after: S) => boolean,
|
|
1710
|
-
): Effect.Effect<{ state: S; value: A } | null, E1 | E2, R1 | R2> =>
|
|
1711
|
-
Effect.gen(function* () {
|
|
1712
|
-
let before = initial;
|
|
1713
|
-
for (let attempt = 0; attempt < STABLE_SNAPSHOT_ATTEMPTS; attempt += 1) {
|
|
1714
|
-
const value = yield* collect(before);
|
|
1715
|
-
const after = yield* refresh(before);
|
|
1716
|
-
if (unchanged(before, after)) return { state: after, value };
|
|
1717
|
-
before = after;
|
|
1718
|
-
}
|
|
1719
|
-
return null;
|
|
1720
|
-
});
|
|
1721
|
-
|
|
1722
1603
|
export const fetchFailedChecks = Effect.fn("pr.fetchFailedChecks")(function* (
|
|
1723
1604
|
pr: number | null,
|
|
1724
1605
|
withLogs = false,
|
|
@@ -1729,7 +1610,7 @@ export const fetchFailedChecks = Effect.fn("pr.fetchFailedChecks")(function* (
|
|
|
1729
1610
|
initial,
|
|
1730
1611
|
(info) => fetchCheckResults(info.number),
|
|
1731
1612
|
(info) =>
|
|
1732
|
-
fetchPRView(info.number).pipe(
|
|
1613
|
+
fetchPRView(info.number, "origin").pipe(
|
|
1733
1614
|
Effect.map((refreshed) => ({ ...refreshed, baseSha: initial.baseSha })),
|
|
1734
1615
|
),
|
|
1735
1616
|
(before, after) => after.headSha === before.headSha,
|
|
@@ -1786,23 +1667,25 @@ export const fetchChecksForCommand = Effect.fn("pr.fetchChecksForCommand")(funct
|
|
|
1786
1667
|
return yield* Effect.fail(watchedChecks.failure);
|
|
1787
1668
|
});
|
|
1788
1669
|
|
|
1789
|
-
type WatchPR = { number: number; state: string; headRefOid?: string | null };
|
|
1790
1670
|
type WatchRun = {
|
|
1791
1671
|
databaseId: number;
|
|
1792
1672
|
attempt?: number | null;
|
|
1793
1673
|
headSha?: string | null;
|
|
1794
|
-
jobs?:
|
|
1674
|
+
jobs?: ReadonlyArray<{ databaseId: number; name: string }>;
|
|
1675
|
+
};
|
|
1676
|
+
|
|
1677
|
+
const checkJobId = (link: string): number | null => {
|
|
1678
|
+
const modern = link.match(/github\.com\/[^/]+\/[^/]+\/actions\/runs\/\d+\/job\/(\d+)/);
|
|
1679
|
+
const legacy = link.match(/github\.com\/[^/]+\/[^/]+\/runs\/(\d+)/);
|
|
1680
|
+
const id = Number(modern?.[1] ?? legacy?.[1]);
|
|
1681
|
+
return Number.isSafeInteger(id) && id > 0 ? id : null;
|
|
1795
1682
|
};
|
|
1796
1683
|
|
|
1797
1684
|
const watchPRState = Effect.fn("pr.watchPRState")(function* (pr: number) {
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
"
|
|
1801
|
-
|
|
1802
|
-
String(pr),
|
|
1803
|
-
"--json",
|
|
1804
|
-
"number,state,headRefOid",
|
|
1805
|
-
]);
|
|
1685
|
+
return project(
|
|
1686
|
+
yield* reads.prView(yield* resolveRepo(null), { by: "number", number: pr }, "origin"),
|
|
1687
|
+
["number", "state", "headRefOid"],
|
|
1688
|
+
);
|
|
1806
1689
|
});
|
|
1807
1690
|
|
|
1808
1691
|
export const fetchChecksBatch = Effect.fn("pr.fetchChecksBatch")(function* (
|
|
@@ -1895,159 +1778,184 @@ export const watchPRs = Effect.fn("pr.watchPRs")(function* (
|
|
|
1895
1778
|
const { state: after, value: checks } = stableResult.success;
|
|
1896
1779
|
const head = after.headRefOid ?? null;
|
|
1897
1780
|
const runs = new Map<number, WatchRun | null>();
|
|
1781
|
+
const identities = new Map<number, { runId: number; attempt: number } | null>();
|
|
1898
1782
|
yield* Effect.forEach(
|
|
1899
|
-
[...new Set(checks.map((check) =>
|
|
1900
|
-
(
|
|
1783
|
+
[...new Set(checks.map((check) => checkJobId(check.link)))].filter(
|
|
1784
|
+
(id): id is number => id !== null,
|
|
1901
1785
|
),
|
|
1786
|
+
(id) =>
|
|
1787
|
+
Effect.gen(function* () {
|
|
1788
|
+
if (!(yield* beforeDeadline)) return;
|
|
1789
|
+
identities.set(
|
|
1790
|
+
id,
|
|
1791
|
+
yield* reads
|
|
1792
|
+
.jobIdentity(repo, id)
|
|
1793
|
+
.pipe(Effect.catchTag("GitHubCommandError", () => Effect.succeed(null))),
|
|
1794
|
+
);
|
|
1795
|
+
}),
|
|
1796
|
+
{ concurrency: 2 },
|
|
1797
|
+
);
|
|
1798
|
+
yield* Effect.forEach(
|
|
1799
|
+
[
|
|
1800
|
+
...new Set(
|
|
1801
|
+
checks
|
|
1802
|
+
.filter((check) => checkJobId(check.link) === null)
|
|
1803
|
+
.map((check) => extractRunIdFromCheckLink(check.link)),
|
|
1804
|
+
),
|
|
1805
|
+
].filter((runId): runId is number => runId !== null),
|
|
1902
1806
|
(runId) =>
|
|
1903
1807
|
Effect.gen(function* () {
|
|
1904
1808
|
if (!(yield* beforeDeadline)) return;
|
|
1905
|
-
const run = yield*
|
|
1906
|
-
.
|
|
1907
|
-
"run",
|
|
1908
|
-
"view",
|
|
1909
|
-
String(runId),
|
|
1910
|
-
"--json",
|
|
1911
|
-
"databaseId,attempt,headSha,jobs",
|
|
1912
|
-
])
|
|
1809
|
+
const run = yield* reads
|
|
1810
|
+
.runView(repo, runId)
|
|
1913
1811
|
.pipe(Effect.catchTag("GitHubCommandError", () => Effect.succeed(null)));
|
|
1914
1812
|
runs.set(runId, run);
|
|
1915
1813
|
}),
|
|
1916
|
-
{ concurrency:
|
|
1814
|
+
{ concurrency: 2 },
|
|
1917
1815
|
);
|
|
1918
1816
|
if (!(yield* beforeDeadline)) return "timeout" as const;
|
|
1919
1817
|
return {
|
|
1920
1818
|
pr: after,
|
|
1921
1819
|
head,
|
|
1922
1820
|
checks: checks
|
|
1923
|
-
.map((check) =>
|
|
1924
|
-
check
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1821
|
+
.map((check) => {
|
|
1822
|
+
const jobId = checkJobId(check.link);
|
|
1823
|
+
const identity = jobId === null ? null : identities.get(jobId);
|
|
1824
|
+
const runId = extractRunIdFromCheckLink(check.link);
|
|
1825
|
+
const resolvedRunId = runId ?? identity?.runId ?? null;
|
|
1826
|
+
const run: WatchRun | null =
|
|
1827
|
+
jobId === null
|
|
1828
|
+
? runId === null
|
|
1829
|
+
? null
|
|
1830
|
+
: (runs.get(runId) ?? null)
|
|
1831
|
+
: resolvedRunId === null
|
|
1832
|
+
? null
|
|
1833
|
+
: {
|
|
1834
|
+
databaseId: resolvedRunId,
|
|
1835
|
+
attempt: identity?.attempt ?? null,
|
|
1836
|
+
headSha: head,
|
|
1837
|
+
jobs: [{ databaseId: jobId, name: check.name }],
|
|
1838
|
+
};
|
|
1839
|
+
return { check, run };
|
|
1840
|
+
})
|
|
1930
1841
|
.filter(({ run }) => run === null || run.headSha === null || run.headSha === head),
|
|
1931
1842
|
};
|
|
1932
1843
|
});
|
|
1933
1844
|
|
|
1934
1845
|
let timedOut = false;
|
|
1935
|
-
yield*
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
Effect.
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
const
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
if (previousIdentity !== undefined && previousIdentity !== identity) {
|
|
2005
|
-
event.supersedes = previousIdentity;
|
|
2006
|
-
}
|
|
2007
|
-
if (!(yield* beforeDeadline)) {
|
|
2008
|
-
timedOut = true;
|
|
2009
|
-
break;
|
|
2010
|
-
}
|
|
2011
|
-
yield* emit(event);
|
|
2012
|
-
lastRevision.set(identity, revision);
|
|
1846
|
+
yield* waits.until(
|
|
1847
|
+
Effect.gen(function* () {
|
|
1848
|
+
for (const number of prs) {
|
|
1849
|
+
if (terminal.has(number)) continue;
|
|
1850
|
+
if (!(yield* beforeDeadline)) {
|
|
1851
|
+
timedOut = true;
|
|
1852
|
+
break;
|
|
1853
|
+
}
|
|
1854
|
+
const remainingMs = Math.max(1, deadline - Number(yield* Clock.currentTimeMillis));
|
|
1855
|
+
const state = yield* snapshot(number).pipe(
|
|
1856
|
+
Effect.timeoutOrElse({
|
|
1857
|
+
duration: remainingMs,
|
|
1858
|
+
orElse: () => Effect.succeed("timeout" as const),
|
|
1859
|
+
}),
|
|
1860
|
+
);
|
|
1861
|
+
if (state === "timeout") {
|
|
1862
|
+
timedOut = true;
|
|
1863
|
+
break;
|
|
1864
|
+
}
|
|
1865
|
+
if (state === null) continue;
|
|
1866
|
+
const headKey = `${number}/${state.head ?? ""}`;
|
|
1867
|
+
const oldHeadKey = headByPR.get(number);
|
|
1868
|
+
if (oldHeadKey !== undefined && oldHeadKey !== headKey) {
|
|
1869
|
+
checksObserved.delete(oldHeadKey);
|
|
1870
|
+
emptySnapshots.delete(oldHeadKey);
|
|
1871
|
+
}
|
|
1872
|
+
headByPR.set(number, headKey);
|
|
1873
|
+
if (state.checks.length > 0) {
|
|
1874
|
+
checksObserved.add(headKey);
|
|
1875
|
+
emptySnapshots.set(headKey, 0);
|
|
1876
|
+
} else {
|
|
1877
|
+
emptySnapshots.set(headKey, (emptySnapshots.get(headKey) ?? 0) + 1);
|
|
1878
|
+
}
|
|
1879
|
+
for (const { check, run } of state.checks) {
|
|
1880
|
+
const matches = failedJobsMatchingCheck(check.name, run?.jobs ?? []);
|
|
1881
|
+
const jobId = matches.length === 1 ? (matches[0]?.databaseId ?? null) : null;
|
|
1882
|
+
const fallback = jobId === null ? `${check.name}|${check.link}` : String(jobId);
|
|
1883
|
+
const identity = [
|
|
1884
|
+
repo.owner + "/" + repo.name,
|
|
1885
|
+
number,
|
|
1886
|
+
state.head ?? "",
|
|
1887
|
+
run?.databaseId ?? "external",
|
|
1888
|
+
run?.attempt ?? "",
|
|
1889
|
+
fallback,
|
|
1890
|
+
].join("/");
|
|
1891
|
+
const logical = `${number}/${check.name}`;
|
|
1892
|
+
const previousIdentity = currentIdentity.get(logical);
|
|
1893
|
+
const revision = `${check.state}/${check.bucket}`;
|
|
1894
|
+
if (lastRevision.get(identity) !== revision) {
|
|
1895
|
+
const event: Record<string, unknown> = {
|
|
1896
|
+
type: "check",
|
|
1897
|
+
repo: `${repo.owner}/${repo.name}`,
|
|
1898
|
+
pr: number,
|
|
1899
|
+
headSha: state.head,
|
|
1900
|
+
...(run?.databaseId === undefined || run.databaseId === null
|
|
1901
|
+
? {}
|
|
1902
|
+
: { runId: run.databaseId }),
|
|
1903
|
+
...(run?.attempt === undefined || run.attempt === null
|
|
1904
|
+
? {}
|
|
1905
|
+
: { attempt: run.attempt }),
|
|
1906
|
+
...(jobId === null ? {} : { jobId }),
|
|
1907
|
+
name: check.name,
|
|
1908
|
+
state: check.state,
|
|
1909
|
+
bucket: check.bucket,
|
|
1910
|
+
link: check.link,
|
|
1911
|
+
identity,
|
|
1912
|
+
};
|
|
1913
|
+
if (previousIdentity !== undefined && previousIdentity !== identity) {
|
|
1914
|
+
event.supersedes = previousIdentity;
|
|
2013
1915
|
}
|
|
2014
|
-
currentIdentity.set(logical, identity);
|
|
2015
|
-
}
|
|
2016
|
-
if (timedOut) break;
|
|
2017
|
-
// Empty snapshots need three consecutive observations, including after checks were seen.
|
|
2018
|
-
const hasTerminalCoverage =
|
|
2019
|
-
state.checks.length > 0 || (emptySnapshots.get(headKey) ?? 0) >= 3;
|
|
2020
|
-
const checksTerminal =
|
|
2021
|
-
state.checks.length === 0 ||
|
|
2022
|
-
state.checks.every(({ check }) => check.bucket !== "pending");
|
|
2023
|
-
if (state.pr.state !== "OPEN" || (hasTerminalCoverage && checksTerminal)) {
|
|
2024
1916
|
if (!(yield* beforeDeadline)) {
|
|
2025
1917
|
timedOut = true;
|
|
2026
1918
|
break;
|
|
2027
1919
|
}
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
type: "pr_terminal",
|
|
2031
|
-
repo: `${repo.owner}/${repo.name}`,
|
|
2032
|
-
pr: number,
|
|
2033
|
-
headSha: state.head,
|
|
2034
|
-
state: state.pr.state,
|
|
2035
|
-
checksObserved: checksObserved.has(headKey),
|
|
2036
|
-
identity,
|
|
2037
|
-
});
|
|
2038
|
-
terminal.add(number);
|
|
1920
|
+
yield* emit(event);
|
|
1921
|
+
lastRevision.set(identity, revision);
|
|
2039
1922
|
}
|
|
1923
|
+
currentIdentity.set(logical, identity);
|
|
2040
1924
|
}
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
1925
|
+
if (timedOut) break;
|
|
1926
|
+
// Empty snapshots need three consecutive observations, including after checks were seen.
|
|
1927
|
+
const hasTerminalCoverage =
|
|
1928
|
+
state.checks.length > 0 || (emptySnapshots.get(headKey) ?? 0) >= 3;
|
|
1929
|
+
const checksTerminal =
|
|
1930
|
+
state.checks.length === 0 ||
|
|
1931
|
+
state.checks.every(({ check }) => check.bucket !== "pending");
|
|
1932
|
+
if (state.pr.state !== "OPEN" || (hasTerminalCoverage && checksTerminal)) {
|
|
1933
|
+
if (!(yield* beforeDeadline)) {
|
|
1934
|
+
timedOut = true;
|
|
1935
|
+
break;
|
|
1936
|
+
}
|
|
1937
|
+
const identity = `${repo.owner}/${repo.name}/${number}/${state.head ?? ""}/terminal/${state.pr.state}`;
|
|
1938
|
+
yield* emit({
|
|
1939
|
+
type: "pr_terminal",
|
|
1940
|
+
repo: `${repo.owner}/${repo.name}`,
|
|
1941
|
+
pr: number,
|
|
1942
|
+
headSha: state.head,
|
|
1943
|
+
state: state.pr.state,
|
|
1944
|
+
checksObserved: checksObserved.has(headKey),
|
|
1945
|
+
identity,
|
|
1946
|
+
});
|
|
1947
|
+
terminal.add(number);
|
|
2047
1948
|
}
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
|
-
|
|
1949
|
+
}
|
|
1950
|
+
const now = Number(yield* Clock.currentTimeMillis);
|
|
1951
|
+
timedOut = timedOut || (terminal.size < prs.length && now >= deadline);
|
|
1952
|
+
}),
|
|
1953
|
+
() => terminal.size >= prs.length || timedOut,
|
|
1954
|
+
{
|
|
1955
|
+
intervalMs: options.intervalSeconds * 1000,
|
|
1956
|
+
timeoutMs: options.timeoutSeconds * 1000,
|
|
1957
|
+
},
|
|
1958
|
+
);
|
|
2051
1959
|
const watcherTimedOut = terminal.size < prs.length;
|
|
2052
1960
|
yield* emit({
|
|
2053
1961
|
type: "watcher_terminal",
|
|
@@ -2071,27 +1979,21 @@ export const watchPRs = Effect.fn("pr.watchPRs")(function* (
|
|
|
2071
1979
|
}
|
|
2072
1980
|
});
|
|
2073
1981
|
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
attempt: number,
|
|
2079
|
-
repo: string,
|
|
2080
|
-
) {
|
|
2081
|
-
const gh = yield* GitHubService;
|
|
2082
|
-
const jobs: Array<{ id: number; name: string }> = [];
|
|
2083
|
-
for (let page = 1; ; page += 1) {
|
|
2084
|
-
const response = yield* gh
|
|
2085
|
-
.runGhJson<{ jobs?: Array<{ id: number; name: string }> }>([
|
|
2086
|
-
"api",
|
|
2087
|
-
`repos/${repo}/actions/runs/${runId}/attempts/${attempt}/jobs?per_page=100&page=${page}`,
|
|
2088
|
-
])
|
|
1982
|
+
const fetchAttemptJobs = (runId: string, attempt: number, repo: string) =>
|
|
1983
|
+
Effect.gen(function* () {
|
|
1984
|
+
return yield* reads
|
|
1985
|
+
.completedAttemptJobs(yield* resolveRepo(repo), Number(runId), attempt)
|
|
2089
1986
|
.pipe(Effect.catchTag("GitHubCommandError", () => Effect.succeed(null)));
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
1987
|
+
});
|
|
1988
|
+
|
|
1989
|
+
const readRerunState = (runId: string, withStatus: boolean) =>
|
|
1990
|
+
Effect.gen(function* () {
|
|
1991
|
+
const run = yield* reads.runView(yield* resolveRepo(null), Number(runId), "origin");
|
|
1992
|
+
const fields = withStatus
|
|
1993
|
+
? (["databaseId", "attempt", "status", "jobs"] as const)
|
|
1994
|
+
: (["databaseId", "attempt", "jobs"] as const);
|
|
1995
|
+
return project(run, fields);
|
|
1996
|
+
});
|
|
2095
1997
|
|
|
2096
1998
|
const readJobDiagnosis = Effect.fn("pr.readJobDiagnosis")(function* (
|
|
2097
1999
|
runId: string,
|
|
@@ -2115,24 +2017,22 @@ const discoverRerun = Effect.fn("pr.discoverRerun")(function* (
|
|
|
2115
2017
|
currentJobIds: readonly number[],
|
|
2116
2018
|
deadline: number,
|
|
2117
2019
|
) {
|
|
2118
|
-
const
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
if (
|
|
2020
|
+
const read = readRerunState(runId, true).pipe(
|
|
2021
|
+
Effect.catchTag("GitHubCommandError", () => Effect.succeed(null)),
|
|
2022
|
+
);
|
|
2023
|
+
const outcome = yield* waits.until(
|
|
2024
|
+
read,
|
|
2025
|
+
(latest) =>
|
|
2125
2026
|
latest !== null &&
|
|
2126
2027
|
((latest.attempt ?? 0) > (currentAttempt ?? 0) ||
|
|
2127
|
-
latest.jobs.some((job) => !currentJobIds.includes(job.databaseId)))
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
return null;
|
|
2028
|
+
latest.jobs.some((job) => !currentJobIds.includes(job.databaseId))),
|
|
2029
|
+
{
|
|
2030
|
+
intervalMs: 5000,
|
|
2031
|
+
timeoutMs: Math.max(0, deadline - Number(yield* Clock.currentTimeMillis)),
|
|
2032
|
+
readAtDeadline: true,
|
|
2033
|
+
},
|
|
2034
|
+
);
|
|
2035
|
+
return outcome.kind === "done" ? outcome.value : null;
|
|
2136
2036
|
});
|
|
2137
2037
|
|
|
2138
2038
|
export const rerunChecks = Effect.fn("pr.rerunChecks")(function* (
|
|
@@ -2166,9 +2066,9 @@ export const rerunChecks = Effect.fn("pr.rerunChecks")(function* (
|
|
|
2166
2066
|
evidence: RerunRetryEvidence;
|
|
2167
2067
|
}> = [];
|
|
2168
2068
|
for (const [runId, runChecks] of checksByRun) {
|
|
2169
|
-
const run = yield*
|
|
2170
|
-
.
|
|
2171
|
-
|
|
2069
|
+
const run = yield* readRerunState(runId, false).pipe(
|
|
2070
|
+
Effect.catchTag("GitHubCommandError", () => Effect.succeed(null)),
|
|
2071
|
+
);
|
|
2172
2072
|
let evidence: RerunRetryEvidence = { state: "eligible", diagnosis: diagnoseLogEntries([]) };
|
|
2173
2073
|
if (failedOnly) {
|
|
2174
2074
|
const jobIds = run === null ? null : resolveJobIdsForFailedChecks(runChecks, run.jobs);
|
|
@@ -2248,8 +2148,8 @@ export const rerunChecks = Effect.fn("pr.rerunChecks")(function* (
|
|
|
2248
2148
|
const deadline = Number(yield* Clock.currentTimeMillis) + (options.timeoutSeconds ?? 60) * 1000;
|
|
2249
2149
|
const results: RerunChecksRun[] = [];
|
|
2250
2150
|
for (const candidate of candidates) {
|
|
2251
|
-
const success = yield*
|
|
2252
|
-
.
|
|
2151
|
+
const success = yield* writes
|
|
2152
|
+
.run(["run", "rerun", candidate.runId, ...(failedOnly ? ["--failed"] : [])], [`repo:${repo}`])
|
|
2253
2153
|
.pipe(
|
|
2254
2154
|
Effect.as(true),
|
|
2255
2155
|
Effect.catch(() => Effect.succeed(false)),
|
|
@@ -2276,7 +2176,7 @@ export const rerunChecks = Effect.fn("pr.rerunChecks")(function* (
|
|
|
2276
2176
|
deadline,
|
|
2277
2177
|
)
|
|
2278
2178
|
: Effect.succeed(null),
|
|
2279
|
-
{ concurrency:
|
|
2179
|
+
{ concurrency: 2 },
|
|
2280
2180
|
);
|
|
2281
2181
|
yield* Effect.forEach(
|
|
2282
2182
|
candidates,
|
|
@@ -2293,25 +2193,29 @@ export const rerunChecks = Effect.fn("pr.rerunChecks")(function* (
|
|
|
2293
2193
|
return;
|
|
2294
2194
|
}
|
|
2295
2195
|
let latest = next;
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2196
|
+
const outcome = yield* waits.until(
|
|
2197
|
+
Effect.suspend(() =>
|
|
2198
|
+
readRerunState(candidate.runId, true).pipe(
|
|
2199
|
+
Effect.catchTag("GitHubCommandError", () => Effect.succeed(latest)),
|
|
2200
|
+
Effect.tap((refreshed) =>
|
|
2201
|
+
Effect.sync(() => {
|
|
2202
|
+
latest = refreshed;
|
|
2203
|
+
}),
|
|
2204
|
+
),
|
|
2205
|
+
),
|
|
2206
|
+
),
|
|
2207
|
+
(value) => value.status === "completed",
|
|
2208
|
+
{
|
|
2209
|
+
initial: { value: latest },
|
|
2210
|
+
intervalMs: 5000,
|
|
2211
|
+
timeoutMs: Math.max(0, deadline - Number(yield* Clock.currentTimeMillis)),
|
|
2212
|
+
},
|
|
2213
|
+
);
|
|
2214
|
+
latest = outcome.kind === "done" ? outcome.value : outcome.last;
|
|
2311
2215
|
result.latestAttempt = latest;
|
|
2312
2216
|
result.status = latest.status === "completed" ? "completed" : "watch_timeout";
|
|
2313
2217
|
}),
|
|
2314
|
-
{ concurrency:
|
|
2218
|
+
{ concurrency: 2 },
|
|
2315
2219
|
);
|
|
2316
2220
|
}
|
|
2317
2221
|
const report: RerunChecksReport = {
|