@blogic-cz/agent-tools 1.7.4 → 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 +179 -74
- 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 +19 -23
- package/src/gh-tool/pr/core.ts +542 -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) {
|
|
@@ -265,7 +247,8 @@ const fetchWorkflowRunFailureContext = Effect.fn("pr.fetchWorkflowRunFailureCont
|
|
|
265
247
|
});
|
|
266
248
|
|
|
267
249
|
// `gh pr checks` exits 1 on an *empty* result ("no checks reported on the 'x' branch"). Zero checks
|
|
268
|
-
// is an ordinary state, so
|
|
250
|
+
// is an ordinary state, so it is not an error, but it is also not an empty list: a PR with green
|
|
251
|
+
// checks (sabservis/nexus-be#1396) once came back empty.
|
|
269
252
|
export const NO_CHECKS_REPORTED_RE = /no checks reported/i;
|
|
270
253
|
// `gh` reports "no checks reported" both for the seconds after a push, before its checks
|
|
271
254
|
// register, and forever for a PR that has none — the message cannot tell them apart. So the
|
|
@@ -274,6 +257,12 @@ export const NO_CHECKS_REPORTED_RE = /no checks reported/i;
|
|
|
274
257
|
// for the caller's whole --timeout.
|
|
275
258
|
const CHECK_REGISTRATION_POLL_SECONDS = 5;
|
|
276
259
|
const CHECK_REGISTRATION_GRACE_SECONDS = 60;
|
|
260
|
+
const NO_CHECKS_REPORTED_REASON =
|
|
261
|
+
"gh reported no checks for this head. Checks may not be registered yet, the PR may have no CI, or GitHub returned an empty rollup. This is not a CI verdict.";
|
|
262
|
+
|
|
263
|
+
export type CheckReport =
|
|
264
|
+
| { checksStatus: "reported"; checks: CheckResult[] }
|
|
265
|
+
| { checksStatus: "none_reported"; reason: string; checks: null };
|
|
277
266
|
|
|
278
267
|
const summarizeFailedStepEntries = (entries: Array<{ step: string; message: string }>) => {
|
|
279
268
|
const selected = entries
|
|
@@ -304,72 +293,31 @@ export const fetchCheckResults = Effect.fn("pr.fetchCheckResults")(function* (
|
|
|
304
293
|
pr: number | null,
|
|
305
294
|
cache?: { headSha: string; watchPending?: boolean },
|
|
306
295
|
) {
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (pr !== null) {
|
|
311
|
-
args.push(String(pr));
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const directory = yield* CheckStatusCacheDirectory;
|
|
315
|
-
const now = yield* Clock.currentTimeMillis;
|
|
316
|
-
let repo: string | null = null;
|
|
317
|
-
let identity: string | null = null;
|
|
318
|
-
if (cache !== undefined && pr !== null && directory !== undefined) {
|
|
319
|
-
const repoInfo = yield* gh.getRepoInfo().pipe(Effect.result);
|
|
320
|
-
const token = yield* resolveGitHubToken().pipe(Effect.result);
|
|
321
|
-
if (Result.isSuccess(repoInfo) && Result.isSuccess(token)) {
|
|
322
|
-
const host = yield* Effect.try({
|
|
323
|
-
try: () => new URL(repoInfo.success.url).host,
|
|
324
|
-
catch: () => "",
|
|
325
|
-
}).pipe(Effect.orElseSucceed(() => ""));
|
|
326
|
-
if (host.length > 0) {
|
|
327
|
-
const cacheRepo = `${host}/${repoInfo.success.owner}/${repoInfo.success.name}`;
|
|
328
|
-
const cacheIdentity = createHash("sha256").update(token.success).digest("hex");
|
|
329
|
-
const cached = yield* Effect.tryPromise({
|
|
330
|
-
try: () =>
|
|
331
|
-
readCheckStatusCache(
|
|
332
|
-
cacheIdentity,
|
|
333
|
-
cacheRepo,
|
|
334
|
-
pr,
|
|
335
|
-
cache.headSha,
|
|
336
|
-
now,
|
|
337
|
-
cache.watchPending,
|
|
338
|
-
directory,
|
|
339
|
-
),
|
|
340
|
-
catch: () => null,
|
|
341
|
-
}).pipe(Effect.orElseSucceed(() => null));
|
|
342
|
-
if (cached !== null) return cached;
|
|
343
|
-
repo = cacheRepo;
|
|
344
|
-
identity = cacheIdentity;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
296
|
+
const report = yield* fetchCheckReport(pr, cache);
|
|
297
|
+
return report.checks ?? [];
|
|
298
|
+
});
|
|
348
299
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
300
|
+
export const fetchCheckReport = Effect.fn("pr.fetchCheckReport")(function* (
|
|
301
|
+
pr: number | null,
|
|
302
|
+
cache?: { headSha: string; watchPending?: boolean },
|
|
303
|
+
) {
|
|
304
|
+
const checks = yield* reads
|
|
305
|
+
.prChecks(yield* resolveRepo(null), pr, cache?.headSha ?? null)
|
|
352
306
|
.pipe(
|
|
353
307
|
Effect.catchTag("GitHubCommandError", (error) =>
|
|
354
308
|
NO_CHECKS_REPORTED_RE.test(error.stderr) || NO_CHECKS_REPORTED_RE.test(error.message)
|
|
355
|
-
? Effect.succeed
|
|
309
|
+
? Effect.succeed(null)
|
|
356
310
|
: Effect.fail(error),
|
|
357
311
|
),
|
|
358
312
|
);
|
|
359
|
-
if (
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
) {
|
|
366
|
-
yield* Effect.tryPromise({
|
|
367
|
-
try: () =>
|
|
368
|
-
writeCheckStatusCache(identity, repo, pr, cache.headSha, checks, fetchedAt, directory),
|
|
369
|
-
catch: () => undefined,
|
|
370
|
-
}).pipe(Effect.orElseSucceed(() => undefined));
|
|
313
|
+
if (checks === null || checks.length === 0) {
|
|
314
|
+
return {
|
|
315
|
+
checksStatus: "none_reported",
|
|
316
|
+
reason: NO_CHECKS_REPORTED_REASON,
|
|
317
|
+
checks: null,
|
|
318
|
+
} satisfies CheckReport;
|
|
371
319
|
}
|
|
372
|
-
return checks;
|
|
320
|
+
return { checksStatus: "reported", checks } satisfies CheckReport;
|
|
373
321
|
});
|
|
374
322
|
|
|
375
323
|
const buildFailedChecksReport = Effect.fn("pr.buildFailedChecksReport")(function* (
|
|
@@ -405,7 +353,7 @@ const buildFailedChecksReport = Effect.fn("pr.buildFailedChecksReport")(function
|
|
|
405
353
|
runIds,
|
|
406
354
|
(runId) =>
|
|
407
355
|
fetchWorkflowRunFailureContext(runId).pipe(Effect.map((found) => [runId, found] as const)),
|
|
408
|
-
{ concurrency:
|
|
356
|
+
{ concurrency: 2 },
|
|
409
357
|
),
|
|
410
358
|
);
|
|
411
359
|
|
|
@@ -430,7 +378,7 @@ const buildFailedChecksReport = Effect.fn("pr.buildFailedChecksReport")(function
|
|
|
430
378
|
const failedStepLogs = yield* fetchJobLogs({
|
|
431
379
|
runId,
|
|
432
380
|
job: matchedJob.name,
|
|
433
|
-
|
|
381
|
+
jobRef: { id: matchedJob.databaseId, status: matchedJob.status },
|
|
434
382
|
failedStepNames: matchedJob.failedSteps,
|
|
435
383
|
steps: found?.stepsByJob.get(matchedJob.databaseId) ?? [],
|
|
436
384
|
failedStepsOnly: true,
|
|
@@ -534,19 +482,32 @@ const buildFailedChecksReport = Effect.fn("pr.buildFailedChecksReport")(function
|
|
|
534
482
|
};
|
|
535
483
|
});
|
|
536
484
|
|
|
537
|
-
export const fetchPRView = Effect.fn("pr.fetchPRView")(function* (
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
"--json",
|
|
546
|
-
"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,
|
|
547
493
|
);
|
|
548
|
-
|
|
549
|
-
|
|
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
|
+
]);
|
|
550
511
|
return {
|
|
551
512
|
...info,
|
|
552
513
|
body: info.body ?? "",
|
|
@@ -555,17 +516,15 @@ export const fetchPRView = Effect.fn("pr.fetchPRView")(function* (prNumber: numb
|
|
|
555
516
|
};
|
|
556
517
|
});
|
|
557
518
|
|
|
558
|
-
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
|
+
) {
|
|
559
523
|
const gh = yield* GitHubService;
|
|
560
524
|
|
|
561
|
-
const info = yield* fetchPRView(prNumber);
|
|
525
|
+
const info = yield* fetchPRView(prNumber, consistency);
|
|
562
526
|
const repo = yield* gh.getRepoInfo();
|
|
563
|
-
const baseSha = yield*
|
|
564
|
-
"api",
|
|
565
|
-
`repos/${repo.owner}/${repo.name}/pulls/${info.number}`,
|
|
566
|
-
"--jq",
|
|
567
|
-
".base.sha",
|
|
568
|
-
]);
|
|
527
|
+
const baseSha = yield* passthrough.baseSha(repo, info.number);
|
|
569
528
|
return {
|
|
570
529
|
...info,
|
|
571
530
|
baseSha: baseSha.stdout.trim() || null,
|
|
@@ -625,7 +584,7 @@ export const detectPRStatus = Effect.fn("pr.detectPRStatus")(function* () {
|
|
|
625
584
|
),
|
|
626
585
|
];
|
|
627
586
|
|
|
628
|
-
const
|
|
587
|
+
const repo = yield* resolveRepo(null);
|
|
629
588
|
|
|
630
589
|
type BranchResult = {
|
|
631
590
|
branch: string;
|
|
@@ -642,40 +601,41 @@ export const detectPRStatus = Effect.fn("pr.detectPRStatus")(function* () {
|
|
|
642
601
|
branchNames.map((branchName) =>
|
|
643
602
|
Effect.all(
|
|
644
603
|
{
|
|
645
|
-
openPr:
|
|
646
|
-
.
|
|
647
|
-
"
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
"1",
|
|
655
|
-
])
|
|
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
|
+
})
|
|
656
613
|
.pipe(
|
|
657
|
-
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
|
+
),
|
|
658
628
|
Effect.catchTag("GitHubCommandError", () => Effect.succeed(null)),
|
|
659
629
|
),
|
|
660
|
-
closedPr:
|
|
661
|
-
.
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
"list",
|
|
670
|
-
"--head",
|
|
671
|
-
branchName,
|
|
672
|
-
"--state",
|
|
673
|
-
"closed",
|
|
674
|
-
"--json",
|
|
675
|
-
"number,url,state",
|
|
676
|
-
"--limit",
|
|
677
|
-
"1",
|
|
678
|
-
])
|
|
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
|
+
})
|
|
679
639
|
.pipe(
|
|
680
640
|
Effect.map((prs) => {
|
|
681
641
|
const pr = prs[0];
|
|
@@ -693,7 +653,7 @@ export const detectPRStatus = Effect.fn("pr.detectPRStatus")(function* () {
|
|
|
693
653
|
Effect.catch(() => Effect.succeed(false)),
|
|
694
654
|
),
|
|
695
655
|
},
|
|
696
|
-
{ concurrency:
|
|
656
|
+
{ concurrency: 2 },
|
|
697
657
|
).pipe(
|
|
698
658
|
Effect.map((r): BranchResult => ({
|
|
699
659
|
branch: branchName,
|
|
@@ -701,7 +661,7 @@ export const detectPRStatus = Effect.fn("pr.detectPRStatus")(function* () {
|
|
|
701
661
|
})),
|
|
702
662
|
),
|
|
703
663
|
),
|
|
704
|
-
{ concurrency:
|
|
664
|
+
{ concurrency: 2 },
|
|
705
665
|
);
|
|
706
666
|
|
|
707
667
|
const foundPrs = branchResults.flatMap((r) => (r.openPr === null ? [] : [r.openPr]));
|
|
@@ -739,59 +699,44 @@ export const listPRs = Effect.fn("pr.listPRs")(function* (opts: {
|
|
|
739
699
|
head: string | null;
|
|
740
700
|
search: string | null;
|
|
741
701
|
}) {
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
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
|
+
);
|
|
758
717
|
});
|
|
759
718
|
|
|
760
719
|
// GitHub recomputes mergeability asynchronously after CI; poll until it settles out of "UNKNOWN".
|
|
761
720
|
const MAX_MERGEABLE_WAIT_SECONDS = 180;
|
|
762
|
-
const MERGEABLE_POLL_INTERVAL_MS =
|
|
721
|
+
const MERGEABLE_POLL_INTERVAL_MS = 5000;
|
|
763
722
|
|
|
764
723
|
export const waitForMergeable = Effect.fn("pr.waitForMergeable")(function* (
|
|
765
724
|
pr: number | null,
|
|
766
725
|
timeoutSeconds: number,
|
|
767
726
|
) {
|
|
768
727
|
const cappedSeconds = Math.min(timeoutSeconds, MAX_MERGEABLE_WAIT_SECONDS);
|
|
769
|
-
const
|
|
770
|
-
const
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
timedOut = true;
|
|
782
|
-
return;
|
|
783
|
-
}
|
|
784
|
-
// Cap the sleep to the remaining budget so the total wait doesn't overshoot the deadline.
|
|
785
|
-
const remaining = deadlineMs - Number(now);
|
|
786
|
-
yield* Effect.sleep(Duration.millis(Math.min(MERGEABLE_POLL_INTERVAL_MS, remaining)));
|
|
787
|
-
latest = yield* fetchPRView(pr).pipe(
|
|
788
|
-
Effect.map((info) => ({ ...info, baseSha: latest.baseSha })),
|
|
789
|
-
);
|
|
790
|
-
}),
|
|
791
|
-
step: () => undefined,
|
|
792
|
-
});
|
|
793
|
-
|
|
794
|
-
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;
|
|
795
740
|
});
|
|
796
741
|
|
|
797
742
|
export const createPR = Effect.fn("pr.createPR")(function* (opts: {
|
|
@@ -816,24 +761,12 @@ export const createPR = Effect.fn("pr.createPR")(function* (opts: {
|
|
|
816
761
|
headSeparator === -1 ? repo.owner : head.slice(0, headSeparator)
|
|
817
762
|
).toLowerCase();
|
|
818
763
|
const headBranch = headSeparator === -1 ? head : head.slice(headSeparator + 1);
|
|
819
|
-
const
|
|
820
|
-
|
|
821
|
-
"
|
|
822
|
-
"
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
"open",
|
|
826
|
-
"--head",
|
|
827
|
-
headBranch,
|
|
828
|
-
"--base",
|
|
829
|
-
baseBranch,
|
|
830
|
-
"--json",
|
|
831
|
-
"number,url,title,headRefName,baseRefName,state,isDraft,mergeable,headRepositoryOwner",
|
|
832
|
-
"--limit",
|
|
833
|
-
"2",
|
|
834
|
-
];
|
|
835
|
-
type Candidate = PRInfo & { headRepositoryOwner?: { login: string } };
|
|
836
|
-
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;
|
|
837
770
|
// The limited query may include another fork's branch. Refuse a saturated result
|
|
838
771
|
// rather than assuming an owner-filtered match beyond the limit does not exist.
|
|
839
772
|
if (candidates.length > 1) {
|
|
@@ -887,7 +820,7 @@ export const createPR = Effect.fn("pr.createPR")(function* (opts: {
|
|
|
887
820
|
hint: "Use --update-if-exists to update it, or 'pr edit --pr <number>'.",
|
|
888
821
|
});
|
|
889
822
|
}
|
|
890
|
-
const current = yield* fetchPRView(existing.number);
|
|
823
|
+
const current = yield* fetchPRView(existing.number, "origin");
|
|
891
824
|
if (
|
|
892
825
|
current.state !== "OPEN" ||
|
|
893
826
|
current.headRefName !== headBranch ||
|
|
@@ -929,14 +862,14 @@ export const createPR = Effect.fn("pr.createPR")(function* (opts: {
|
|
|
929
862
|
createArgs.push("--draft");
|
|
930
863
|
}
|
|
931
864
|
|
|
932
|
-
const createResult = yield*
|
|
865
|
+
const createResult = yield* writes.run(createArgs, [`repo:${repoSlug(repo)}`]);
|
|
933
866
|
|
|
934
867
|
const urlMatch = createResult.stdout.match(/\/pull\/(\d+)/);
|
|
935
868
|
if (urlMatch?.[1]) {
|
|
936
869
|
return { ...(yield* viewPR(Number(urlMatch[1]))), updated: false };
|
|
937
870
|
}
|
|
938
871
|
|
|
939
|
-
const prs = yield*
|
|
872
|
+
const prs = yield* lookup;
|
|
940
873
|
if (
|
|
941
874
|
prs.length === 1 &&
|
|
942
875
|
prs[0]?.state === "OPEN" &&
|
|
@@ -960,14 +893,9 @@ export const createPR = Effect.fn("pr.createPR")(function* (opts: {
|
|
|
960
893
|
// GitHub rejects GraphQL `mergePullRequest` for a PR that belongs to a native stack and points at
|
|
961
894
|
// the asynchronous REST merge instead. `gh pr merge` only speaks GraphQL.
|
|
962
895
|
const ASYNC_MERGE_REQUIRED_RE = /asynchronous merge/i;
|
|
963
|
-
const ASYNC_MERGE_POLL_INTERVAL_MS =
|
|
896
|
+
const ASYNC_MERGE_POLL_INTERVAL_MS = 5000;
|
|
964
897
|
const MAX_ASYNC_MERGE_WAIT_SECONDS = 120;
|
|
965
898
|
|
|
966
|
-
type AsyncMergeResult = {
|
|
967
|
-
status: "pending" | "merged" | "enqueued" | "failed";
|
|
968
|
-
details?: { message?: string; uuid?: string; sha?: string };
|
|
969
|
-
};
|
|
970
|
-
|
|
971
899
|
const asyncMergeFailure = (
|
|
972
900
|
pr: number,
|
|
973
901
|
message: string,
|
|
@@ -989,24 +917,24 @@ const mergeViaAsyncApi = Effect.fn("pr.mergeViaAsyncApi")(function* (opts: {
|
|
|
989
917
|
const repo = yield* gh.getRepoInfo();
|
|
990
918
|
const asyncPath = `repos/${repo.owner}/${repo.name}/pulls/${opts.pr}/merge-async`;
|
|
991
919
|
|
|
992
|
-
let latest = yield*
|
|
993
|
-
"api",
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
asyncPath,
|
|
997
|
-
"-f",
|
|
998
|
-
`merge_method=${opts.strategy}`,
|
|
999
|
-
]);
|
|
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
|
+
);
|
|
1000
924
|
|
|
1001
925
|
const uuid = latest.details?.uuid;
|
|
1002
926
|
if (uuid !== undefined) {
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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;
|
|
1010
938
|
}
|
|
1011
939
|
|
|
1012
940
|
if (latest.status === "merged") {
|
|
@@ -1056,27 +984,18 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
|
|
|
1056
984
|
});
|
|
1057
985
|
}
|
|
1058
986
|
|
|
1059
|
-
const info = yield*
|
|
1060
|
-
.
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
? error
|
|
1072
|
-
: new GitHubMergeError({
|
|
1073
|
-
message: `Could not verify the head commit of PR #${opts.pr}: ${error.message}`,
|
|
1074
|
-
reason: "unknown",
|
|
1075
|
-
hint: "The merge is refused while the head is unknown. Re-read the PR and verify its reviewed head before retrying.",
|
|
1076
|
-
nextCommand: `agent-tools-gh pr view --pr ${opts.pr}`,
|
|
1077
|
-
}),
|
|
1078
|
-
),
|
|
1079
|
-
);
|
|
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
|
+
);
|
|
1080
999
|
|
|
1081
1000
|
if (
|
|
1082
1001
|
matchHeadCommit !== undefined &&
|
|
@@ -1158,18 +1077,18 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
|
|
|
1158
1077
|
// branch if EVERY retarget succeeds (fail-closed).
|
|
1159
1078
|
const dependentOpenPrs =
|
|
1160
1079
|
opts.deleteBranch && !headIsLongLived && info.headRefName
|
|
1161
|
-
? yield*
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
"
|
|
1172
|
-
|
|
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
|
+
)
|
|
1173
1092
|
: [];
|
|
1174
1093
|
|
|
1175
1094
|
if (!opts.confirm) {
|
|
@@ -1219,15 +1138,18 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
|
|
|
1219
1138
|
// closed.
|
|
1220
1139
|
if (willDeleteBranch && dependentOpenPrs.length > 0 && repo) {
|
|
1221
1140
|
for (const child of dependentOpenPrs) {
|
|
1222
|
-
const retargeted = yield*
|
|
1223
|
-
.
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
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
|
+
)
|
|
1231
1153
|
.pipe(
|
|
1232
1154
|
Effect.as(true),
|
|
1233
1155
|
Effect.orElseSucceed(() => false),
|
|
@@ -1249,15 +1171,18 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
|
|
|
1249
1171
|
return failed;
|
|
1250
1172
|
}
|
|
1251
1173
|
for (const child of retargetedChildren) {
|
|
1252
|
-
const rolledBack = yield*
|
|
1253
|
-
.
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
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
|
+
)
|
|
1261
1186
|
.pipe(
|
|
1262
1187
|
Effect.as(true),
|
|
1263
1188
|
Effect.orElseSucceed(() => false),
|
|
@@ -1269,13 +1194,14 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
|
|
|
1269
1194
|
return failed;
|
|
1270
1195
|
});
|
|
1271
1196
|
|
|
1197
|
+
const mergeRepo = yield* resolveRepo(null);
|
|
1272
1198
|
const mergeArgs = ["pr", "merge", String(opts.pr), `--${opts.strategy}`];
|
|
1273
1199
|
if (matchHeadCommit !== undefined) {
|
|
1274
1200
|
mergeArgs.push("--match-head-commit", matchHeadCommit);
|
|
1275
1201
|
}
|
|
1276
1202
|
|
|
1277
|
-
const shaFromAsyncMerge = yield*
|
|
1278
|
-
.
|
|
1203
|
+
const shaFromAsyncMerge = yield* writes
|
|
1204
|
+
.run(mergeArgs, [`pr:${repoSlug(mergeRepo)}#${opts.pr}`, `pulls:${repoSlug(mergeRepo)}`])
|
|
1279
1205
|
.pipe(
|
|
1280
1206
|
Effect.map(() => null),
|
|
1281
1207
|
Effect.catchTag("GitHubCommandError", (error) =>
|
|
@@ -1377,13 +1303,16 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
|
|
|
1377
1303
|
// `gh pr merge --delete-branch` aborts its remote delete when the head branch is checked out in a
|
|
1378
1304
|
// worktree; deleting the remote ref explicitly is worktree-independent. Local cleanup is separate.
|
|
1379
1305
|
if (willDeleteBranch && repo && info.headRefName) {
|
|
1380
|
-
const remoteDeleted = yield*
|
|
1381
|
-
.
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
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
|
+
)
|
|
1387
1316
|
.pipe(
|
|
1388
1317
|
Effect.as(true),
|
|
1389
1318
|
Effect.orElseSucceed(() => false),
|
|
@@ -1411,8 +1340,6 @@ export const closePR = Effect.fn("pr.closePR")(function* (opts: {
|
|
|
1411
1340
|
comment: string | null;
|
|
1412
1341
|
deleteBranch: boolean;
|
|
1413
1342
|
}) {
|
|
1414
|
-
const gh = yield* GitHubService;
|
|
1415
|
-
|
|
1416
1343
|
const args = ["pr", "close", String(opts.pr)];
|
|
1417
1344
|
|
|
1418
1345
|
if (opts.comment !== null) {
|
|
@@ -1423,15 +1350,18 @@ export const closePR = Effect.fn("pr.closePR")(function* (opts: {
|
|
|
1423
1350
|
args.push("--delete-branch");
|
|
1424
1351
|
}
|
|
1425
1352
|
|
|
1426
|
-
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
|
+
]);
|
|
1427
1359
|
|
|
1428
1360
|
return yield* viewPR(opts.pr);
|
|
1429
1361
|
});
|
|
1430
1362
|
|
|
1431
1363
|
export const readyPR = Effect.fn("pr.readyPR")(function* (opts: { pr: number | null }) {
|
|
1432
|
-
const
|
|
1433
|
-
|
|
1434
|
-
const info = yield* viewPR(opts.pr);
|
|
1364
|
+
const info = yield* viewPR(opts.pr, "origin");
|
|
1435
1365
|
|
|
1436
1366
|
// Idempotent: a PR that's already ready needs no mutation, and `gh pr ready` on a
|
|
1437
1367
|
// non-draft PR is a no-op that would just add a pointless call.
|
|
@@ -1439,22 +1369,28 @@ export const readyPR = Effect.fn("pr.readyPR")(function* (opts: { pr: number | n
|
|
|
1439
1369
|
return { ...info, wasAlreadyReady: true };
|
|
1440
1370
|
}
|
|
1441
1371
|
|
|
1442
|
-
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
|
+
);
|
|
1443
1377
|
|
|
1444
1378
|
const updated = yield* viewPR(info.number);
|
|
1445
1379
|
return { ...updated, wasAlreadyReady: false };
|
|
1446
1380
|
});
|
|
1447
1381
|
|
|
1448
1382
|
export const draftPR = Effect.fn("pr.draftPR")(function* (opts: { pr: number | null }) {
|
|
1449
|
-
const
|
|
1450
|
-
|
|
1451
|
-
const info = yield* viewPR(opts.pr);
|
|
1383
|
+
const info = yield* viewPR(opts.pr, "origin");
|
|
1452
1384
|
|
|
1453
1385
|
if (info.isDraft) {
|
|
1454
1386
|
return { ...info, wasAlreadyDraft: true };
|
|
1455
1387
|
}
|
|
1456
1388
|
|
|
1457
|
-
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
|
+
);
|
|
1458
1394
|
|
|
1459
1395
|
const updated = yield* viewPR(info.number);
|
|
1460
1396
|
return { ...updated, wasAlreadyDraft: false };
|
|
@@ -1517,7 +1453,7 @@ export const editPR = Effect.fn("pr.editPR")(function* (opts: {
|
|
|
1517
1453
|
editArgs.push("-f", `base=${opts.base}`);
|
|
1518
1454
|
}
|
|
1519
1455
|
|
|
1520
|
-
yield*
|
|
1456
|
+
yield* writes.run(editArgs, [`pr:${repoSlug(repo)}#${opts.pr}`, `pulls:${repoSlug(repo)}`]);
|
|
1521
1457
|
|
|
1522
1458
|
return yield* viewPR(opts.pr);
|
|
1523
1459
|
});
|
|
@@ -1529,62 +1465,44 @@ export const fetchChecks = Effect.fn("pr.fetchChecks")(function* (
|
|
|
1529
1465
|
timeoutSeconds: number,
|
|
1530
1466
|
quiet = false,
|
|
1531
1467
|
) {
|
|
1532
|
-
const gh = yield* GitHubService;
|
|
1533
|
-
|
|
1534
|
-
const args = ["pr", "checks"];
|
|
1535
|
-
if (pr !== null) {
|
|
1536
|
-
args.push(String(pr));
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
1468
|
if (watch) {
|
|
1540
|
-
const
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
// within the caller's timeout, then report the final snapshot.
|
|
1547
|
-
let watchResult: GhResult | null = null;
|
|
1548
|
-
let registered = false;
|
|
1549
|
-
let graceExpired = false;
|
|
1550
|
-
const watchThroughRegistration = Effect.gen(function* () {
|
|
1551
|
-
const graceStart = yield* Clock.currentTimeMillis;
|
|
1552
|
-
const graceDeadlineMs =
|
|
1553
|
-
Number(graceStart) + Math.min(CHECK_REGISTRATION_GRACE_SECONDS, timeoutSeconds) * 1000;
|
|
1554
|
-
yield* Effect.whileLoop({
|
|
1555
|
-
while: () => !registered && !graceExpired,
|
|
1556
|
-
body: () =>
|
|
1557
|
-
gh.runGh(watchArgs).pipe(
|
|
1558
|
-
Effect.flatMap((result) => {
|
|
1559
|
-
watchResult = result;
|
|
1560
|
-
registered = true;
|
|
1561
|
-
return Effect.void;
|
|
1562
|
-
}),
|
|
1563
|
-
Effect.catchTag("GitHubCommandError", (error) =>
|
|
1564
|
-
NO_CHECKS_REPORTED_RE.test(error.stderr) || NO_CHECKS_REPORTED_RE.test(error.message)
|
|
1565
|
-
? Effect.gen(function* () {
|
|
1566
|
-
const now = yield* Clock.currentTimeMillis;
|
|
1567
|
-
if (Number(now) >= graceDeadlineMs) {
|
|
1568
|
-
graceExpired = true;
|
|
1569
|
-
return;
|
|
1570
|
-
}
|
|
1571
|
-
yield* Effect.sleep(Duration.seconds(CHECK_REGISTRATION_POLL_SECONDS));
|
|
1572
|
-
})
|
|
1573
|
-
: Effect.fail(error),
|
|
1574
|
-
),
|
|
1575
|
-
),
|
|
1576
|
-
step: () => undefined,
|
|
1577
|
-
});
|
|
1578
|
-
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) };
|
|
1579
1475
|
});
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
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
|
+
});
|
|
1588
1506
|
const results = yield* fetchCheckResults(pr);
|
|
1589
1507
|
if (!quiet && graceExpired) {
|
|
1590
1508
|
const prRef = pr === null ? "<number>" : String(pr);
|
|
@@ -1595,7 +1513,10 @@ export const fetchChecks = Effect.fn("pr.fetchChecks")(function* (
|
|
|
1595
1513
|
`head instead:\n agent-tools-gh pr trigger-checks --pr ${prRef} --workflow <file.yml>`,
|
|
1596
1514
|
);
|
|
1597
1515
|
}
|
|
1598
|
-
if (
|
|
1516
|
+
if (
|
|
1517
|
+
(watchOutcome === null || watchOutcome.kind === "timedOut") &&
|
|
1518
|
+
results.some((check) => check.bucket === "pending")
|
|
1519
|
+
) {
|
|
1599
1520
|
const pending = results.filter((check) => check.bucket === "pending").length;
|
|
1600
1521
|
yield* logFormatted(results, quiet ? "json" : "toon");
|
|
1601
1522
|
return yield* new GitHubTimeoutError({
|
|
@@ -1679,23 +1600,6 @@ export const triggerChecks = Effect.fn("pr.triggerChecks")(function* (
|
|
|
1679
1600
|
};
|
|
1680
1601
|
});
|
|
1681
1602
|
|
|
1682
|
-
export const collectWithStableState = <S, A, E1, R1, E2, R2>(
|
|
1683
|
-
initial: S,
|
|
1684
|
-
collect: (state: S) => Effect.Effect<A, E1, R1>,
|
|
1685
|
-
refresh: (state: S) => Effect.Effect<S, E2, R2>,
|
|
1686
|
-
unchanged: (before: S, after: S) => boolean,
|
|
1687
|
-
): Effect.Effect<{ state: S; value: A } | null, E1 | E2, R1 | R2> =>
|
|
1688
|
-
Effect.gen(function* () {
|
|
1689
|
-
let before = initial;
|
|
1690
|
-
for (let attempt = 0; attempt < STABLE_SNAPSHOT_ATTEMPTS; attempt += 1) {
|
|
1691
|
-
const value = yield* collect(before);
|
|
1692
|
-
const after = yield* refresh(before);
|
|
1693
|
-
if (unchanged(before, after)) return { state: after, value };
|
|
1694
|
-
before = after;
|
|
1695
|
-
}
|
|
1696
|
-
return null;
|
|
1697
|
-
});
|
|
1698
|
-
|
|
1699
1603
|
export const fetchFailedChecks = Effect.fn("pr.fetchFailedChecks")(function* (
|
|
1700
1604
|
pr: number | null,
|
|
1701
1605
|
withLogs = false,
|
|
@@ -1706,7 +1610,7 @@ export const fetchFailedChecks = Effect.fn("pr.fetchFailedChecks")(function* (
|
|
|
1706
1610
|
initial,
|
|
1707
1611
|
(info) => fetchCheckResults(info.number),
|
|
1708
1612
|
(info) =>
|
|
1709
|
-
fetchPRView(info.number).pipe(
|
|
1613
|
+
fetchPRView(info.number, "origin").pipe(
|
|
1710
1614
|
Effect.map((refreshed) => ({ ...refreshed, baseSha: initial.baseSha })),
|
|
1711
1615
|
),
|
|
1712
1616
|
(before, after) => after.headSha === before.headSha,
|
|
@@ -1763,23 +1667,58 @@ export const fetchChecksForCommand = Effect.fn("pr.fetchChecksForCommand")(funct
|
|
|
1763
1667
|
return yield* Effect.fail(watchedChecks.failure);
|
|
1764
1668
|
});
|
|
1765
1669
|
|
|
1766
|
-
type WatchPR = { number: number; state: string; headRefOid?: string | null };
|
|
1767
1670
|
type WatchRun = {
|
|
1768
1671
|
databaseId: number;
|
|
1769
1672
|
attempt?: number | null;
|
|
1770
1673
|
headSha?: string | null;
|
|
1771
|
-
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;
|
|
1772
1682
|
};
|
|
1773
1683
|
|
|
1774
1684
|
const watchPRState = Effect.fn("pr.watchPRState")(function* (pr: number) {
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
"
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
]
|
|
1685
|
+
return project(
|
|
1686
|
+
yield* reads.prView(yield* resolveRepo(null), { by: "number", number: pr }, "origin"),
|
|
1687
|
+
["number", "state", "headRefOid"],
|
|
1688
|
+
);
|
|
1689
|
+
});
|
|
1690
|
+
|
|
1691
|
+
export const fetchChecksBatch = Effect.fn("pr.fetchChecksBatch")(function* (
|
|
1692
|
+
prs: readonly number[],
|
|
1693
|
+
) {
|
|
1694
|
+
return yield* Effect.forEach(
|
|
1695
|
+
prs,
|
|
1696
|
+
(pr) =>
|
|
1697
|
+
Effect.gen(function* () {
|
|
1698
|
+
const snapshot = yield* collectWithStableState(
|
|
1699
|
+
yield* watchPRState(pr),
|
|
1700
|
+
() => fetchCheckReport(pr),
|
|
1701
|
+
() => watchPRState(pr),
|
|
1702
|
+
(before, after) => before.headRefOid === after.headRefOid && before.state === after.state,
|
|
1703
|
+
);
|
|
1704
|
+
if (snapshot === null) {
|
|
1705
|
+
return yield* new GitHubCommandError({
|
|
1706
|
+
command: "gh-tool pr checks --prs",
|
|
1707
|
+
exitCode: 1,
|
|
1708
|
+
stderr: "",
|
|
1709
|
+
message: `PR #${pr} head changed repeatedly while collecting checks`,
|
|
1710
|
+
retryable: true,
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
return {
|
|
1714
|
+
pr,
|
|
1715
|
+
state: snapshot.state.state,
|
|
1716
|
+
headSha: snapshot.state.headRefOid ?? null,
|
|
1717
|
+
...snapshot.value,
|
|
1718
|
+
};
|
|
1719
|
+
}),
|
|
1720
|
+
{ concurrency: 5 },
|
|
1721
|
+
);
|
|
1783
1722
|
});
|
|
1784
1723
|
|
|
1785
1724
|
export const watchPRs = Effect.fn("pr.watchPRs")(function* (
|
|
@@ -1839,159 +1778,184 @@ export const watchPRs = Effect.fn("pr.watchPRs")(function* (
|
|
|
1839
1778
|
const { state: after, value: checks } = stableResult.success;
|
|
1840
1779
|
const head = after.headRefOid ?? null;
|
|
1841
1780
|
const runs = new Map<number, WatchRun | null>();
|
|
1781
|
+
const identities = new Map<number, { runId: number; attempt: number } | null>();
|
|
1842
1782
|
yield* Effect.forEach(
|
|
1843
|
-
[...new Set(checks.map((check) =>
|
|
1844
|
-
(
|
|
1783
|
+
[...new Set(checks.map((check) => checkJobId(check.link)))].filter(
|
|
1784
|
+
(id): id is number => id !== null,
|
|
1845
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),
|
|
1846
1806
|
(runId) =>
|
|
1847
1807
|
Effect.gen(function* () {
|
|
1848
1808
|
if (!(yield* beforeDeadline)) return;
|
|
1849
|
-
const run = yield*
|
|
1850
|
-
.
|
|
1851
|
-
"run",
|
|
1852
|
-
"view",
|
|
1853
|
-
String(runId),
|
|
1854
|
-
"--json",
|
|
1855
|
-
"databaseId,attempt,headSha,jobs",
|
|
1856
|
-
])
|
|
1809
|
+
const run = yield* reads
|
|
1810
|
+
.runView(repo, runId)
|
|
1857
1811
|
.pipe(Effect.catchTag("GitHubCommandError", () => Effect.succeed(null)));
|
|
1858
1812
|
runs.set(runId, run);
|
|
1859
1813
|
}),
|
|
1860
|
-
{ concurrency:
|
|
1814
|
+
{ concurrency: 2 },
|
|
1861
1815
|
);
|
|
1862
1816
|
if (!(yield* beforeDeadline)) return "timeout" as const;
|
|
1863
1817
|
return {
|
|
1864
1818
|
pr: after,
|
|
1865
1819
|
head,
|
|
1866
1820
|
checks: checks
|
|
1867
|
-
.map((check) =>
|
|
1868
|
-
check
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
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
|
+
})
|
|
1874
1841
|
.filter(({ run }) => run === null || run.headSha === null || run.headSha === head),
|
|
1875
1842
|
};
|
|
1876
1843
|
});
|
|
1877
1844
|
|
|
1878
1845
|
let timedOut = false;
|
|
1879
|
-
yield*
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
Effect.
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
const
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
if (previousIdentity !== undefined && previousIdentity !== identity) {
|
|
1949
|
-
event.supersedes = previousIdentity;
|
|
1950
|
-
}
|
|
1951
|
-
if (!(yield* beforeDeadline)) {
|
|
1952
|
-
timedOut = true;
|
|
1953
|
-
break;
|
|
1954
|
-
}
|
|
1955
|
-
yield* emit(event);
|
|
1956
|
-
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;
|
|
1957
1915
|
}
|
|
1958
|
-
currentIdentity.set(logical, identity);
|
|
1959
|
-
}
|
|
1960
|
-
if (timedOut) break;
|
|
1961
|
-
// Empty snapshots need three consecutive observations, including after checks were seen.
|
|
1962
|
-
const hasTerminalCoverage =
|
|
1963
|
-
state.checks.length > 0 || (emptySnapshots.get(headKey) ?? 0) >= 3;
|
|
1964
|
-
const checksTerminal =
|
|
1965
|
-
state.checks.length === 0 ||
|
|
1966
|
-
state.checks.every(({ check }) => check.bucket !== "pending");
|
|
1967
|
-
if (state.pr.state !== "OPEN" || (hasTerminalCoverage && checksTerminal)) {
|
|
1968
1916
|
if (!(yield* beforeDeadline)) {
|
|
1969
1917
|
timedOut = true;
|
|
1970
1918
|
break;
|
|
1971
1919
|
}
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
type: "pr_terminal",
|
|
1975
|
-
repo: `${repo.owner}/${repo.name}`,
|
|
1976
|
-
pr: number,
|
|
1977
|
-
headSha: state.head,
|
|
1978
|
-
state: state.pr.state,
|
|
1979
|
-
checksObserved: checksObserved.has(headKey),
|
|
1980
|
-
identity,
|
|
1981
|
-
});
|
|
1982
|
-
terminal.add(number);
|
|
1920
|
+
yield* emit(event);
|
|
1921
|
+
lastRevision.set(identity, revision);
|
|
1983
1922
|
}
|
|
1923
|
+
currentIdentity.set(logical, identity);
|
|
1984
1924
|
}
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
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);
|
|
1991
1948
|
}
|
|
1992
|
-
}
|
|
1993
|
-
|
|
1994
|
-
|
|
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
|
+
);
|
|
1995
1959
|
const watcherTimedOut = terminal.size < prs.length;
|
|
1996
1960
|
yield* emit({
|
|
1997
1961
|
type: "watcher_terminal",
|
|
@@ -2015,27 +1979,21 @@ export const watchPRs = Effect.fn("pr.watchPRs")(function* (
|
|
|
2015
1979
|
}
|
|
2016
1980
|
});
|
|
2017
1981
|
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
attempt: number,
|
|
2023
|
-
repo: string,
|
|
2024
|
-
) {
|
|
2025
|
-
const gh = yield* GitHubService;
|
|
2026
|
-
const jobs: Array<{ id: number; name: string }> = [];
|
|
2027
|
-
for (let page = 1; ; page += 1) {
|
|
2028
|
-
const response = yield* gh
|
|
2029
|
-
.runGhJson<{ jobs?: Array<{ id: number; name: string }> }>([
|
|
2030
|
-
"api",
|
|
2031
|
-
`repos/${repo}/actions/runs/${runId}/attempts/${attempt}/jobs?per_page=100&page=${page}`,
|
|
2032
|
-
])
|
|
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)
|
|
2033
1986
|
.pipe(Effect.catchTag("GitHubCommandError", () => Effect.succeed(null)));
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
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
|
+
});
|
|
2039
1997
|
|
|
2040
1998
|
const readJobDiagnosis = Effect.fn("pr.readJobDiagnosis")(function* (
|
|
2041
1999
|
runId: string,
|
|
@@ -2059,24 +2017,22 @@ const discoverRerun = Effect.fn("pr.discoverRerun")(function* (
|
|
|
2059
2017
|
currentJobIds: readonly number[],
|
|
2060
2018
|
deadline: number,
|
|
2061
2019
|
) {
|
|
2062
|
-
const
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
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) =>
|
|
2069
2026
|
latest !== null &&
|
|
2070
2027
|
((latest.attempt ?? 0) > (currentAttempt ?? 0) ||
|
|
2071
|
-
latest.jobs.some((job) => !currentJobIds.includes(job.databaseId)))
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
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;
|
|
2080
2036
|
});
|
|
2081
2037
|
|
|
2082
2038
|
export const rerunChecks = Effect.fn("pr.rerunChecks")(function* (
|
|
@@ -2110,9 +2066,9 @@ export const rerunChecks = Effect.fn("pr.rerunChecks")(function* (
|
|
|
2110
2066
|
evidence: RerunRetryEvidence;
|
|
2111
2067
|
}> = [];
|
|
2112
2068
|
for (const [runId, runChecks] of checksByRun) {
|
|
2113
|
-
const run = yield*
|
|
2114
|
-
.
|
|
2115
|
-
|
|
2069
|
+
const run = yield* readRerunState(runId, false).pipe(
|
|
2070
|
+
Effect.catchTag("GitHubCommandError", () => Effect.succeed(null)),
|
|
2071
|
+
);
|
|
2116
2072
|
let evidence: RerunRetryEvidence = { state: "eligible", diagnosis: diagnoseLogEntries([]) };
|
|
2117
2073
|
if (failedOnly) {
|
|
2118
2074
|
const jobIds = run === null ? null : resolveJobIdsForFailedChecks(runChecks, run.jobs);
|
|
@@ -2192,8 +2148,8 @@ export const rerunChecks = Effect.fn("pr.rerunChecks")(function* (
|
|
|
2192
2148
|
const deadline = Number(yield* Clock.currentTimeMillis) + (options.timeoutSeconds ?? 60) * 1000;
|
|
2193
2149
|
const results: RerunChecksRun[] = [];
|
|
2194
2150
|
for (const candidate of candidates) {
|
|
2195
|
-
const success = yield*
|
|
2196
|
-
.
|
|
2151
|
+
const success = yield* writes
|
|
2152
|
+
.run(["run", "rerun", candidate.runId, ...(failedOnly ? ["--failed"] : [])], [`repo:${repo}`])
|
|
2197
2153
|
.pipe(
|
|
2198
2154
|
Effect.as(true),
|
|
2199
2155
|
Effect.catch(() => Effect.succeed(false)),
|
|
@@ -2220,7 +2176,7 @@ export const rerunChecks = Effect.fn("pr.rerunChecks")(function* (
|
|
|
2220
2176
|
deadline,
|
|
2221
2177
|
)
|
|
2222
2178
|
: Effect.succeed(null),
|
|
2223
|
-
{ concurrency:
|
|
2179
|
+
{ concurrency: 2 },
|
|
2224
2180
|
);
|
|
2225
2181
|
yield* Effect.forEach(
|
|
2226
2182
|
candidates,
|
|
@@ -2237,25 +2193,29 @@ export const rerunChecks = Effect.fn("pr.rerunChecks")(function* (
|
|
|
2237
2193
|
return;
|
|
2238
2194
|
}
|
|
2239
2195
|
let latest = next;
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
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;
|
|
2255
2215
|
result.latestAttempt = latest;
|
|
2256
2216
|
result.status = latest.status === "completed" ? "completed" : "watch_timeout";
|
|
2257
2217
|
}),
|
|
2258
|
-
{ concurrency:
|
|
2218
|
+
{ concurrency: 2 },
|
|
2259
2219
|
);
|
|
2260
2220
|
}
|
|
2261
2221
|
const report: RerunChecksReport = {
|