@blogic-cz/agent-tools 1.16.1 → 1.17.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 +25 -0
- package/dist/gh-tool/issue/commands.d.ts +5 -0
- package/dist/gh-tool/issue/commands.d.ts.map +1 -1
- package/dist/gh-tool/issue/core.d.ts +19 -0
- package/dist/gh-tool/issue/core.d.ts.map +1 -1
- package/dist/gh-tool/issue/index.d.ts +1 -1
- package/dist/gh-tool/issue/index.d.ts.map +1 -1
- package/dist/gh-tool/issue/triage.d.ts +1 -1
- package/dist/gh-tool/issue/triage.d.ts.map +1 -1
- package/dist/gh-tool/main.d.ts.map +1 -1
- package/dist/gh-tool/pr/commands.d.ts +7 -7
- package/dist/gh-tool/pr/commands.d.ts.map +1 -1
- package/dist/gh-tool/pr/core.d.ts +9 -9
- package/dist/gh-tool/pr/review.d.ts +3 -3
- package/dist/gh-tool/pr/review.d.ts.map +1 -1
- package/dist/gh-tool/read/graphql-fallback.d.ts +21 -0
- package/dist/gh-tool/read/graphql-fallback.d.ts.map +1 -0
- package/dist/gh-tool/read/issues.d.ts +11 -1
- package/dist/gh-tool/read/issues.d.ts.map +1 -1
- package/dist/gh-tool/read/pulls.d.ts +13 -12
- package/dist/gh-tool/read/pulls.d.ts.map +1 -1
- package/dist/gh-tool/read/reads.d.ts +22 -5
- package/dist/gh-tool/read/reads.d.ts.map +1 -1
- package/dist/gh-tool/read/rest-porcelain.d.ts +131 -0
- package/dist/gh-tool/read/rest-porcelain.d.ts.map +1 -0
- package/dist/gh-tool/read/review-guards.d.ts +1 -1
- package/dist/gh-tool/read/review-guards.d.ts.map +1 -1
- package/dist/gh-tool/read/review-model.d.ts +1 -1
- package/dist/gh-tool/read/review-model.d.ts.map +1 -1
- package/dist/gh-tool/read/reviews.d.ts.map +1 -1
- package/dist/gh-tool/read/telemetry.d.ts +1 -1
- package/dist/gh-tool/read/telemetry.d.ts.map +1 -1
- package/dist/gh-tool/read/transport.d.ts.map +1 -1
- package/dist/gh-tool/repo.d.ts +3 -0
- package/dist/gh-tool/repo.d.ts.map +1 -1
- package/dist/gh-tool/types.d.ts +2 -2
- package/dist/gh-tool/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/gh-tool/issue/commands.ts +18 -0
- package/src/gh-tool/issue/core.ts +45 -1
- package/src/gh-tool/issue/index.ts +1 -0
- package/src/gh-tool/issue/triage.ts +1 -1
- package/src/gh-tool/main.ts +7 -2
- package/src/gh-tool/pr/commands.ts +15 -6
- package/src/gh-tool/pr/review.ts +27 -7
- package/src/gh-tool/read/graphql-fallback.ts +366 -0
- package/src/gh-tool/read/issues.ts +23 -1
- package/src/gh-tool/read/pulls.ts +60 -10
- package/src/gh-tool/read/reads.ts +12 -1
- package/src/gh-tool/read/rest-porcelain.ts +571 -0
- package/src/gh-tool/read/review-guards.ts +8 -2
- package/src/gh-tool/read/review-model.ts +1 -1
- package/src/gh-tool/read/reviews.ts +19 -3
- package/src/gh-tool/read/telemetry.ts +6 -3
- package/src/gh-tool/read/transport.ts +314 -7
- package/src/gh-tool/repo.ts +7 -1
- package/src/gh-tool/types.ts +2 -2
|
@@ -6,7 +6,7 @@ export type RequestEvent = {
|
|
|
6
6
|
method: string;
|
|
7
7
|
endpoint: string;
|
|
8
8
|
status: number | null;
|
|
9
|
-
outcome: "cache" | "304" | "200" | "bypass" | "error" | "subprocess";
|
|
9
|
+
outcome: "cache" | "304" | "200" | "bypass" | "error" | "subprocess" | "fallback";
|
|
10
10
|
resource: string | null;
|
|
11
11
|
used: number | null;
|
|
12
12
|
remaining: number | null;
|
|
@@ -132,13 +132,16 @@ const subprocessSpendsResource = (row: RequestRow, resource: string): boolean =>
|
|
|
132
132
|
};
|
|
133
133
|
|
|
134
134
|
export const summarizeRequests = (rows: readonly RequestRow[]) => {
|
|
135
|
-
const requests = rows.filter(
|
|
135
|
+
const requests = rows.filter(
|
|
136
|
+
(row) => row.outcome !== "cache" && row.outcome !== "subprocess" && row.outcome !== "fallback",
|
|
137
|
+
);
|
|
136
138
|
const subprocesses = rows.filter((row) => row.outcome === "subprocess");
|
|
137
139
|
const previous = new Map<string, { used: number; index: number }>();
|
|
138
140
|
let externalUsageEstimate = 0;
|
|
139
141
|
let intervals = 0;
|
|
140
142
|
for (const [index, row] of rows.entries()) {
|
|
141
|
-
if (row.outcome === "cache" || row.outcome === "subprocess")
|
|
143
|
+
if (row.outcome === "cache" || row.outcome === "subprocess" || row.outcome === "fallback")
|
|
144
|
+
continue;
|
|
142
145
|
if (row.resource === null || row.used === null || row.reset === null) continue;
|
|
143
146
|
const resource = row.resource;
|
|
144
147
|
const key = `${resource}:${row.reset}`;
|
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { ghSubprocessEvent, recordRequest } from "./telemetry";
|
|
2
2
|
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
3
3
|
import { Context, Effect, Layer, Schema, Stream } from "effect";
|
|
4
|
+
import {
|
|
5
|
+
graphFieldsRequested,
|
|
6
|
+
restPorcelain,
|
|
7
|
+
restReviewDecision,
|
|
8
|
+
parseRest,
|
|
9
|
+
type ReviewDecisionInput,
|
|
10
|
+
type ReviewDecision,
|
|
11
|
+
} from "./rest-porcelain";
|
|
12
|
+
import {
|
|
13
|
+
graphqlLimitError,
|
|
14
|
+
isFallbackResult,
|
|
15
|
+
isGraphqlLimit,
|
|
16
|
+
markFallbackResult,
|
|
17
|
+
reportFallback,
|
|
18
|
+
restGraphql,
|
|
19
|
+
rateLimitResponse,
|
|
20
|
+
} from "./graphql-fallback";
|
|
4
21
|
|
|
5
22
|
import type { GitHubRepoConfig } from "#config";
|
|
6
23
|
import type { RepoInfo } from "#gh/types";
|
|
@@ -177,14 +194,14 @@ export class GitHubService extends Context.Service<
|
|
|
177
194
|
return getGitHubConfig(config);
|
|
178
195
|
});
|
|
179
196
|
|
|
180
|
-
const executeGh = (args: string[]) =>
|
|
197
|
+
const executeGh = (args: string[], binary: string = GH_BINARY) =>
|
|
181
198
|
Effect.scoped(
|
|
182
199
|
Effect.gen(function* () {
|
|
183
200
|
const ghRepo = yield* RepoTarget;
|
|
184
201
|
const cache = yield* ReadCache;
|
|
185
202
|
const credential = yield* SessionCredential;
|
|
186
203
|
const cacheDirectory = cache?.store?.ghCacheDirectory;
|
|
187
|
-
const command = ChildProcess.make(
|
|
204
|
+
const command = ChildProcess.make(binary, args, {
|
|
188
205
|
stdout: "pipe",
|
|
189
206
|
stderr: "pipe",
|
|
190
207
|
env: {
|
|
@@ -207,7 +224,8 @@ export class GitHubService extends Context.Service<
|
|
|
207
224
|
const stdout = stdoutChunk.join("");
|
|
208
225
|
const stderr = stderrChunk.join("");
|
|
209
226
|
const exitCode = yield* proc.exitCode;
|
|
210
|
-
|
|
227
|
+
if (binary === GH_BINARY)
|
|
228
|
+
yield* recordRequest(ghSubprocessEvent(args, Number(exitCode)));
|
|
211
229
|
|
|
212
230
|
return {
|
|
213
231
|
stdout,
|
|
@@ -233,6 +251,15 @@ export class GitHubService extends Context.Service<
|
|
|
233
251
|
const result = yield* executeGh(args);
|
|
234
252
|
|
|
235
253
|
if (result.exitCode !== 0) {
|
|
254
|
+
const limit =
|
|
255
|
+
args[0] === "api" && args[1] === "graphql" ? rateLimitResponse(result.stdout) : null;
|
|
256
|
+
if (limit !== null)
|
|
257
|
+
return yield* new GitHubCommandError({
|
|
258
|
+
command: "gh api graphql",
|
|
259
|
+
exitCode: result.exitCode,
|
|
260
|
+
message: limit,
|
|
261
|
+
stderr: limit,
|
|
262
|
+
});
|
|
236
263
|
if (
|
|
237
264
|
result.stderr.includes("not logged in") ||
|
|
238
265
|
result.stderr.includes("gh auth login")
|
|
@@ -326,7 +353,7 @@ export class GitHubService extends Context.Service<
|
|
|
326
353
|
});
|
|
327
354
|
|
|
328
355
|
// Auto-retry transient failures, but only for idempotent reads (never replay a mutation).
|
|
329
|
-
const
|
|
356
|
+
const runGhRaw = (args: string[]): Effect.Effect<GhResult, GhError> => {
|
|
330
357
|
const canRetry = isSafeRetryRead(args);
|
|
331
358
|
return Effect.gen(function* () {
|
|
332
359
|
yield* validateOutboundText(args.join("\n"), "gh-tool");
|
|
@@ -339,6 +366,248 @@ export class GitHubService extends Context.Service<
|
|
|
339
366
|
});
|
|
340
367
|
};
|
|
341
368
|
|
|
369
|
+
let graphqlBlocked = false;
|
|
370
|
+
const ruleReads = new Map<string, Effect.Effect<unknown, GhError>>();
|
|
371
|
+
const uncachedJson = (args: string[]) =>
|
|
372
|
+
runGhRaw(args).pipe(
|
|
373
|
+
Effect.flatMap((result) =>
|
|
374
|
+
Effect.try({
|
|
375
|
+
try: () => JSON.parse(result.stdout) as unknown,
|
|
376
|
+
catch: (error) =>
|
|
377
|
+
new GitHubCommandError({
|
|
378
|
+
command: "gh REST",
|
|
379
|
+
exitCode: 1,
|
|
380
|
+
message: String(error),
|
|
381
|
+
stderr: String(error),
|
|
382
|
+
}),
|
|
383
|
+
}),
|
|
384
|
+
),
|
|
385
|
+
);
|
|
386
|
+
const rawJson = (args: string[]): Effect.Effect<unknown, GhError> =>
|
|
387
|
+
Effect.gen(function* () {
|
|
388
|
+
const path = args[3];
|
|
389
|
+
if (
|
|
390
|
+
args[0] !== "api" ||
|
|
391
|
+
args[2] !== "GET" ||
|
|
392
|
+
path === undefined ||
|
|
393
|
+
!/^repos\/[^/]+\/[^/]+\/(?:rules\/branches\/[^/]+|branches\/[^/]+(?:\/protection\/required_pull_request_reviews)?)$/.test(
|
|
394
|
+
path,
|
|
395
|
+
)
|
|
396
|
+
)
|
|
397
|
+
return yield* uncachedJson(args);
|
|
398
|
+
let read = ruleReads.get(path);
|
|
399
|
+
if (read === undefined) {
|
|
400
|
+
read = yield* Effect.cached(uncachedJson(args));
|
|
401
|
+
ruleReads.set(path, read);
|
|
402
|
+
}
|
|
403
|
+
return yield* read;
|
|
404
|
+
});
|
|
405
|
+
const resolveDecision = (
|
|
406
|
+
input: ReviewDecisionInput,
|
|
407
|
+
): Effect.Effect<ReviewDecision, GhError> =>
|
|
408
|
+
Effect.gen(function* () {
|
|
409
|
+
const decision = yield* restReviewDecision(rawJson, input);
|
|
410
|
+
if (decision !== "UNKNOWN" || graphqlBlocked) return decision;
|
|
411
|
+
return yield* rawJson([
|
|
412
|
+
"pr",
|
|
413
|
+
"view",
|
|
414
|
+
String(input.number),
|
|
415
|
+
"--repo",
|
|
416
|
+
input.repo,
|
|
417
|
+
"--json",
|
|
418
|
+
"reviewDecision",
|
|
419
|
+
]).pipe(
|
|
420
|
+
Effect.flatMap((value) =>
|
|
421
|
+
parseRest(
|
|
422
|
+
Schema.Struct({
|
|
423
|
+
reviewDecision: Schema.NullOr(
|
|
424
|
+
Schema.Literals([
|
|
425
|
+
"APPROVED",
|
|
426
|
+
"CHANGES_REQUESTED",
|
|
427
|
+
"REVIEW_REQUIRED",
|
|
428
|
+
"UNKNOWN",
|
|
429
|
+
"",
|
|
430
|
+
]),
|
|
431
|
+
),
|
|
432
|
+
}),
|
|
433
|
+
value,
|
|
434
|
+
),
|
|
435
|
+
),
|
|
436
|
+
Effect.map((value) => value.reviewDecision ?? ""),
|
|
437
|
+
Effect.catch((error) => {
|
|
438
|
+
if (!isGraphqlLimit(error)) return Effect.fail(error);
|
|
439
|
+
graphqlBlocked = true;
|
|
440
|
+
return Effect.succeed("UNKNOWN" as const);
|
|
441
|
+
}),
|
|
442
|
+
);
|
|
443
|
+
});
|
|
444
|
+
const runGh = (args: string[]): Effect.Effect<GhResult, GhError> =>
|
|
445
|
+
Effect.gen(function* () {
|
|
446
|
+
yield* validateOutboundText(args.join("\n"), "gh-tool");
|
|
447
|
+
const target = yield* RepoTarget;
|
|
448
|
+
if (
|
|
449
|
+
args[0] === "pr" &&
|
|
450
|
+
["view", "checks"].includes(args[1] ?? "") &&
|
|
451
|
+
(args[2] === undefined || args[2].startsWith("-"))
|
|
452
|
+
) {
|
|
453
|
+
const branch = yield* executeGh(["branch", "--show-current"], "git");
|
|
454
|
+
if (branch.exitCode !== 0 || branch.stdout.trim() === "")
|
|
455
|
+
return yield* new GitHubCommandError({
|
|
456
|
+
command: "git branch --show-current",
|
|
457
|
+
exitCode: 1,
|
|
458
|
+
message: "Cannot determine the current branch; pass --pr",
|
|
459
|
+
stderr: branch.stderr,
|
|
460
|
+
});
|
|
461
|
+
const localBranch = branch.stdout.trim();
|
|
462
|
+
const trackingBranch = yield* executeGh(
|
|
463
|
+
["config", "--get", "branch." + localBranch + ".merge"],
|
|
464
|
+
"git",
|
|
465
|
+
);
|
|
466
|
+
const trackingRemote = yield* executeGh(
|
|
467
|
+
["config", "--get", "branch." + localBranch + ".remote"],
|
|
468
|
+
"git",
|
|
469
|
+
);
|
|
470
|
+
if (trackingBranch.exitCode > 1 || trackingRemote.exitCode > 1)
|
|
471
|
+
return yield* new GitHubCommandError({
|
|
472
|
+
command: "pr view",
|
|
473
|
+
exitCode: 1,
|
|
474
|
+
message: "Cannot read tracking configuration; pass --pr",
|
|
475
|
+
stderr: trackingBranch.stderr + trackingRemote.stderr,
|
|
476
|
+
});
|
|
477
|
+
const pullNumber =
|
|
478
|
+
trackingBranch.exitCode === 0
|
|
479
|
+
? /^refs\/pull\/(\d+)\/head$/.exec(trackingBranch.stdout.trim())?.[1]
|
|
480
|
+
: undefined;
|
|
481
|
+
if (pullNumber !== undefined) {
|
|
482
|
+
args = [args[0], args[1] ?? "view", pullNumber, ...args.slice(2)];
|
|
483
|
+
} else {
|
|
484
|
+
let head =
|
|
485
|
+
trackingBranch.exitCode === 0
|
|
486
|
+
? trackingBranch.stdout.trim().replace(/^refs\/heads\//, "")
|
|
487
|
+
: localBranch;
|
|
488
|
+
if (trackingRemote.exitCode === 0 && trackingRemote.stdout.trim() !== ".") {
|
|
489
|
+
const remote = yield* executeGh(
|
|
490
|
+
["remote", "get-url", trackingRemote.stdout.trim()],
|
|
491
|
+
"git",
|
|
492
|
+
);
|
|
493
|
+
const owner =
|
|
494
|
+
remote.exitCode === 0
|
|
495
|
+
? /[:/]([^/:]+)\/[^/]+?(?:\.git)?$/.exec(remote.stdout.trim())?.[1]
|
|
496
|
+
: undefined;
|
|
497
|
+
if (owner === undefined)
|
|
498
|
+
return yield* new GitHubCommandError({
|
|
499
|
+
command: "pr view",
|
|
500
|
+
exitCode: 1,
|
|
501
|
+
message: "Cannot determine the tracking repository; pass --pr",
|
|
502
|
+
stderr: remote.stderr,
|
|
503
|
+
});
|
|
504
|
+
head = owner + ":" + head;
|
|
505
|
+
}
|
|
506
|
+
const repoIndex = args.indexOf("--repo");
|
|
507
|
+
const selectedRepo = repoIndex < 0 ? target : args[repoIndex + 1];
|
|
508
|
+
const pulls = yield* runGh([
|
|
509
|
+
"pr",
|
|
510
|
+
"list",
|
|
511
|
+
"--head",
|
|
512
|
+
head,
|
|
513
|
+
"--state",
|
|
514
|
+
"all",
|
|
515
|
+
"--limit",
|
|
516
|
+
"100",
|
|
517
|
+
"--json",
|
|
518
|
+
"number,state",
|
|
519
|
+
...(selectedRepo === undefined ? [] : ["--repo", selectedRepo]),
|
|
520
|
+
]);
|
|
521
|
+
const candidates = yield* Effect.try({
|
|
522
|
+
try: () =>
|
|
523
|
+
Schema.decodeUnknownSync(
|
|
524
|
+
Schema.Array(Schema.Struct({ number: Schema.Number, state: Schema.String })),
|
|
525
|
+
)(JSON.parse(pulls.stdout)),
|
|
526
|
+
catch: (error) =>
|
|
527
|
+
new GitHubCommandError({
|
|
528
|
+
command: "pr view",
|
|
529
|
+
exitCode: 1,
|
|
530
|
+
message: String(error),
|
|
531
|
+
stderr: String(error),
|
|
532
|
+
}),
|
|
533
|
+
});
|
|
534
|
+
const open = candidates.filter((pull) => pull.state === "OPEN");
|
|
535
|
+
const matches = open.length > 0 ? open : candidates.slice(0, 1);
|
|
536
|
+
if (matches.length !== 1 || matches[0] === undefined)
|
|
537
|
+
return yield* new GitHubCommandError({
|
|
538
|
+
command: "pr view",
|
|
539
|
+
exitCode: 1,
|
|
540
|
+
message: "Cannot identify one PR for the current branch; pass --pr",
|
|
541
|
+
stderr: "Current branch has zero or multiple PRs",
|
|
542
|
+
});
|
|
543
|
+
args = [args[0], args[1] ?? "view", String(matches[0].number), ...args.slice(2)];
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const rest = restPorcelain(args, rawJson, target, resolveDecision);
|
|
547
|
+
const usesGraphql =
|
|
548
|
+
args[0] === "pr" ||
|
|
549
|
+
args[0] === "repo" ||
|
|
550
|
+
(args[0] === "api" && args[1] === "graphql");
|
|
551
|
+
const mutation = args[0] === "pr" && args[1] === "merge";
|
|
552
|
+
const fallback = (
|
|
553
|
+
error: GhError,
|
|
554
|
+
resolved?: { value: unknown },
|
|
555
|
+
): Effect.Effect<GhResult, GhError> =>
|
|
556
|
+
Effect.gen(function* () {
|
|
557
|
+
if (!usesGraphql || !isGraphqlLimit(error)) return yield* Effect.fail(error);
|
|
558
|
+
graphqlBlocked = true;
|
|
559
|
+
if (rest === null) return yield* Effect.fail(error);
|
|
560
|
+
const value = resolved === undefined ? yield* rest : resolved.value;
|
|
561
|
+
const values = Array.isArray(value) ? value : [value];
|
|
562
|
+
const unknown = ["mergeStateStatus"].filter((field) =>
|
|
563
|
+
args.some((arg) => arg.split(",").includes(field)),
|
|
564
|
+
);
|
|
565
|
+
if (
|
|
566
|
+
args.some((arg) => arg.split(",").includes("reviewDecision")) &&
|
|
567
|
+
values.some(
|
|
568
|
+
(item) =>
|
|
569
|
+
typeof item === "object" &&
|
|
570
|
+
item !== null &&
|
|
571
|
+
"reviewDecision" in item &&
|
|
572
|
+
item.reviewDecision === "UNKNOWN",
|
|
573
|
+
)
|
|
574
|
+
)
|
|
575
|
+
unknown.push("reviewDecision");
|
|
576
|
+
const notice = yield* reportFallback(unknown);
|
|
577
|
+
const output = Array.isArray(value)
|
|
578
|
+
? value.map((item) => ({ ...item, fallback: notice }))
|
|
579
|
+
: { ...Object(value), fallback: notice };
|
|
580
|
+
return markFallbackResult({
|
|
581
|
+
stdout: JSON.stringify(output),
|
|
582
|
+
stderr: "",
|
|
583
|
+
exitCode: 0,
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
const jsonFields = args[args.indexOf("--json") + 1]?.split(",") ?? [];
|
|
587
|
+
if (
|
|
588
|
+
rest !== null &&
|
|
589
|
+
!mutation &&
|
|
590
|
+
(!graphFieldsRequested(args) || !jsonFields.includes("mergeStateStatus"))
|
|
591
|
+
) {
|
|
592
|
+
const value = yield* rest;
|
|
593
|
+
if (graphqlBlocked && graphFieldsRequested(args))
|
|
594
|
+
return yield* fallback(graphqlLimitError(), { value });
|
|
595
|
+
return { stdout: JSON.stringify(value), stderr: "", exitCode: 0 };
|
|
596
|
+
}
|
|
597
|
+
if (graphqlBlocked && rest !== null) return yield* fallback(graphqlLimitError());
|
|
598
|
+
if (graphqlBlocked && args[0] === "api" && args[1] === "graphql")
|
|
599
|
+
return yield* graphqlLimitError();
|
|
600
|
+
if (graphqlBlocked && args[0] === "pr" && args[1] === "ready")
|
|
601
|
+
return yield* new GitHubCommandError({
|
|
602
|
+
command: "gh pr ready",
|
|
603
|
+
exitCode: 1,
|
|
604
|
+
message: "GitHub GraphQL is blocked; REST cannot change draft status",
|
|
605
|
+
stderr: "GitHub GraphQL is blocked; REST cannot change draft status",
|
|
606
|
+
});
|
|
607
|
+
if (graphqlBlocked && usesGraphql) return yield* graphqlLimitError();
|
|
608
|
+
return yield* runGhRaw(args).pipe(Effect.catch(fallback));
|
|
609
|
+
});
|
|
610
|
+
|
|
342
611
|
const writePRCheckbox = Effect.fn("GitHubService.writePRCheckbox")(function* (
|
|
343
612
|
opts: PRCheckboxWrite,
|
|
344
613
|
) {
|
|
@@ -423,10 +692,12 @@ export class GitHubService extends Context.Service<
|
|
|
423
692
|
}),
|
|
424
693
|
}).pipe(Effect.mapError((error) => error as GhError));
|
|
425
694
|
|
|
426
|
-
return parsed
|
|
695
|
+
return isFallbackResult(result) && typeof parsed === "object" && parsed !== null
|
|
696
|
+
? markFallbackResult(parsed)
|
|
697
|
+
: parsed;
|
|
427
698
|
}).pipe(Effect.withSpan("GitHubService.runGhJson"));
|
|
428
699
|
|
|
429
|
-
const
|
|
700
|
+
const runGraphQLRaw = Effect.fn("GitHubService.runGraphQL")(function* (
|
|
430
701
|
query: string,
|
|
431
702
|
variables: Record<string, string | number | null>,
|
|
432
703
|
) {
|
|
@@ -447,7 +718,13 @@ export class GitHubService extends Context.Service<
|
|
|
447
718
|
const result = yield* runGh(args);
|
|
448
719
|
|
|
449
720
|
const response = yield* Effect.try({
|
|
450
|
-
try: () =>
|
|
721
|
+
try: () =>
|
|
722
|
+
JSON.parse(result.stdout) as {
|
|
723
|
+
errors?: unknown[];
|
|
724
|
+
data?: unknown;
|
|
725
|
+
type?: string;
|
|
726
|
+
code?: string;
|
|
727
|
+
},
|
|
451
728
|
catch: (error) =>
|
|
452
729
|
new GitHubCommandError({
|
|
453
730
|
message: `Failed to parse GraphQL response: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -466,9 +743,39 @@ export class GitHubService extends Context.Service<
|
|
|
466
743
|
});
|
|
467
744
|
}
|
|
468
745
|
|
|
746
|
+
if (response.type === "RATE_LIMIT" || response.code === "graphql_rate_limit")
|
|
747
|
+
return yield* new GitHubCommandError({
|
|
748
|
+
command: "gh api graphql",
|
|
749
|
+
exitCode: 0,
|
|
750
|
+
message: "GitHub GraphQL RATE_LIMIT",
|
|
751
|
+
stderr: "GitHub GraphQL RATE_LIMIT",
|
|
752
|
+
});
|
|
469
753
|
return response.data as unknown;
|
|
470
754
|
});
|
|
471
755
|
|
|
756
|
+
const runGraphQL = (query: string, variables: Record<string, string | number | null>) =>
|
|
757
|
+
Effect.gen(function* () {
|
|
758
|
+
const rest = restGraphql(query, variables, rawJson);
|
|
759
|
+
const fallback = (error: GhError) => {
|
|
760
|
+
if (!isGraphqlLimit(error)) return Effect.fail(error);
|
|
761
|
+
graphqlBlocked = true;
|
|
762
|
+
return (
|
|
763
|
+
rest ??
|
|
764
|
+
Effect.fail(
|
|
765
|
+
new GitHubCommandError({
|
|
766
|
+
command: "gh api graphql",
|
|
767
|
+
exitCode: 1,
|
|
768
|
+
message: "GitHub GraphQL is blocked; REST cannot perform this operation",
|
|
769
|
+
stderr: "GitHub GraphQL is blocked; REST cannot perform this operation",
|
|
770
|
+
hint: "Retry after GitHub lifts the GraphQL limit. No equivalent REST operation is available.",
|
|
771
|
+
}),
|
|
772
|
+
)
|
|
773
|
+
);
|
|
774
|
+
};
|
|
775
|
+
if (graphqlBlocked) return yield* fallback(graphqlLimitError());
|
|
776
|
+
return yield* runGraphQLRaw(query, variables).pipe(Effect.catch(fallback));
|
|
777
|
+
});
|
|
778
|
+
|
|
472
779
|
const getRepoInfo = Effect.fn("GitHubService.getRepoInfo")(function* () {
|
|
473
780
|
const ghRepo = yield* RepoTarget;
|
|
474
781
|
const cacheKey = ghRepo ?? null;
|
package/src/gh-tool/repo.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { formatOption, logFormatted } from "#shared";
|
|
|
5
5
|
import { passthrough } from "./read/catalog";
|
|
6
6
|
import { GitHubService } from "./read/transport";
|
|
7
7
|
import { commitAuthor } from "./read/commits";
|
|
8
|
-
import { resolveRepo } from "./read/reads";
|
|
8
|
+
import { reads, resolveRepo } from "./read/reads";
|
|
9
9
|
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
// Types
|
|
@@ -58,6 +58,12 @@ export const repoInfoCommand = Command.make(
|
|
|
58
58
|
}),
|
|
59
59
|
).pipe(Command.withDescription("Show repository information (owner, name, default branch, URL)"));
|
|
60
60
|
|
|
61
|
+
export const viewerCommand = Command.make("viewer", { format: formatOption }, ({ format }) =>
|
|
62
|
+
Effect.gen(function* () {
|
|
63
|
+
yield* logFormatted(yield* reads.viewerLogin(), format);
|
|
64
|
+
}),
|
|
65
|
+
).pipe(Command.withDescription("Show the authenticated GitHub user's login"));
|
|
66
|
+
|
|
61
67
|
export const repoListCommand = Command.make(
|
|
62
68
|
"list",
|
|
63
69
|
{
|
package/src/gh-tool/types.ts
CHANGED
|
@@ -22,7 +22,7 @@ export type PRViewInfo = PRInfo & {
|
|
|
22
22
|
baseSha: string | null;
|
|
23
23
|
body: string;
|
|
24
24
|
author: { login: string; is_bot: boolean };
|
|
25
|
-
reviewDecision: "APPROVED" | "CHANGES_REQUESTED" | "REVIEW_REQUIRED" | "";
|
|
25
|
+
reviewDecision: "APPROVED" | "CHANGES_REQUESTED" | "REVIEW_REQUIRED" | "UNKNOWN" | "";
|
|
26
26
|
reviewRequests: ReviewRequest[];
|
|
27
27
|
};
|
|
28
28
|
|
|
@@ -40,7 +40,7 @@ export type ReviewThread = {
|
|
|
40
40
|
originalCommitSha: string | null;
|
|
41
41
|
diffHunk: string | null;
|
|
42
42
|
body: string;
|
|
43
|
-
isResolved: boolean;
|
|
43
|
+
isResolved: boolean | null;
|
|
44
44
|
hasReply: boolean;
|
|
45
45
|
replyCount: number;
|
|
46
46
|
needsHumanReply: boolean;
|