@blogic-cz/agent-tools 1.7.1 → 1.7.3
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/dist/credential-guard/index.d.ts.map +1 -1
- package/dist/gh-tool/api.d.ts.map +1 -1
- package/dist/gh-tool/config.d.ts +1 -0
- package/dist/gh-tool/config.d.ts.map +1 -1
- package/dist/gh-tool/errors.d.ts +2 -1
- package/dist/gh-tool/errors.d.ts.map +1 -1
- package/dist/gh-tool/issue/triage.d.ts +3 -3
- package/dist/gh-tool/issue/triage.d.ts.map +1 -1
- package/dist/gh-tool/pr/check-status-cache.d.ts +6 -0
- package/dist/gh-tool/pr/check-status-cache.d.ts.map +1 -0
- package/dist/gh-tool/pr/commands.d.ts +6 -5
- package/dist/gh-tool/pr/commands.d.ts.map +1 -1
- package/dist/gh-tool/pr/core.d.ts +9 -6
- package/dist/gh-tool/pr/core.d.ts.map +1 -1
- package/dist/gh-tool/service.d.ts.map +1 -1
- package/dist/gh-tool/types.d.ts +4 -0
- package/dist/gh-tool/types.d.ts.map +1 -1
- package/dist/gh-tool/workflow.d.ts +10 -1
- package/dist/gh-tool/workflow.d.ts.map +1 -1
- package/dist/shared/retry-transient.d.ts +1 -0
- package/dist/shared/retry-transient.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/credential-guard/index.ts +21 -1
- package/src/gh-tool/api.ts +40 -4
- package/src/gh-tool/config.ts +1 -0
- package/src/gh-tool/errors.ts +4 -2
- package/src/gh-tool/pr/check-status-cache.ts +142 -0
- package/src/gh-tool/pr/commands.ts +29 -8
- package/src/gh-tool/pr/core.ts +182 -46
- package/src/gh-tool/service.ts +29 -0
- package/src/gh-tool/types.ts +4 -0
- package/src/gh-tool/workflow.ts +91 -41
- package/src/shared/retry-transient.ts +7 -3
package/src/gh-tool/types.ts
CHANGED
|
@@ -121,6 +121,7 @@ export type FailedCheckJob = {
|
|
|
121
121
|
export type FailedCheckRunContext = {
|
|
122
122
|
runId: number;
|
|
123
123
|
attempt: number | null;
|
|
124
|
+
headSha: string | null;
|
|
124
125
|
url: string | null;
|
|
125
126
|
workflowName: string | null;
|
|
126
127
|
status: string;
|
|
@@ -226,6 +227,7 @@ export type FailedChecksReport = {
|
|
|
226
227
|
export type WorkflowRunDetail = {
|
|
227
228
|
databaseId: number;
|
|
228
229
|
attempt: number | null;
|
|
230
|
+
headSha: string | null;
|
|
229
231
|
url: string;
|
|
230
232
|
workflowName: string | null;
|
|
231
233
|
status: string;
|
|
@@ -240,6 +242,8 @@ export type WorkflowRunDetail = {
|
|
|
240
242
|
name: string;
|
|
241
243
|
status: string;
|
|
242
244
|
conclusion: string | null;
|
|
245
|
+
startedAt: string | null;
|
|
246
|
+
completedAt: string | null;
|
|
243
247
|
}>;
|
|
244
248
|
}>;
|
|
245
249
|
};
|
package/src/gh-tool/workflow.ts
CHANGED
|
@@ -454,18 +454,72 @@ export function cleanLogLine(line: string): string {
|
|
|
454
454
|
.trim();
|
|
455
455
|
}
|
|
456
456
|
|
|
457
|
-
|
|
458
|
-
|
|
457
|
+
type StepTiming = {
|
|
458
|
+
name: string;
|
|
459
|
+
conclusion: string | null;
|
|
460
|
+
startedAt: string | null;
|
|
461
|
+
completedAt: string | null;
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
type StepWindow = { name: string; failed: boolean; start: number; end: number };
|
|
465
|
+
|
|
466
|
+
const LINE_SECOND_RE = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})/;
|
|
467
|
+
|
|
468
|
+
// Older gh versions reject --allow-escape-sequences; they print escapes without it.
|
|
469
|
+
export const readJobLogApi = Effect.fn("workflow.readJobLogApi")(function* (path: string) {
|
|
470
|
+
const gh = yield* GitHubService;
|
|
471
|
+
return yield* gh
|
|
472
|
+
.runGh(["api", path, "--allow-escape-sequences"])
|
|
473
|
+
.pipe(
|
|
474
|
+
Effect.catchTag("GitHubCommandError", (error) =>
|
|
475
|
+
`${error.stderr}${error.message}`.includes("unknown flag: --allow-escape-sequences")
|
|
476
|
+
? gh.runGh(["api", path])
|
|
477
|
+
: Effect.fail(error),
|
|
478
|
+
),
|
|
479
|
+
);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// Step times have one-second precision and several steps can share a second, so a
|
|
483
|
+
// boundary line goes to the failed step: a few extra lines beat a lost final error.
|
|
484
|
+
// Returns undefined for a line without a timestamp and null for a time outside every step.
|
|
485
|
+
function stepAtTime(line: string, windows: ReadonlyArray<StepWindow>) {
|
|
486
|
+
const second = LINE_SECOND_RE.exec(line)?.[1];
|
|
487
|
+
if (second === undefined) return undefined;
|
|
488
|
+
const time = Date.parse(`${second}Z`);
|
|
489
|
+
const inside = windows.filter((w) => w.start <= time && time <= w.end);
|
|
490
|
+
return (inside.find((w) => w.failed) ?? inside.at(-1))?.name ?? null;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export function parseRawJobLogs(raw: string, steps: ReadonlyArray<StepTiming> = []): LogEntry[] {
|
|
494
|
+
return parseAttributedJobLogs(raw, steps).map(({ step, message }) => ({ step, message }));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// `established` is true only when gh labelled the line or its own time falls inside a step window.
|
|
498
|
+
function parseAttributedJobLogs(raw: string, steps: ReadonlyArray<StepTiming>) {
|
|
499
|
+
const entries: Array<LogEntry & { established: boolean }> = [];
|
|
459
500
|
let currentStep = "(unknown)";
|
|
501
|
+
let timedStep: string | undefined;
|
|
502
|
+
const timed = steps
|
|
503
|
+
.flatMap((step) => (step.startedAt ? [{ ...step, start: Date.parse(step.startedAt) }] : []))
|
|
504
|
+
.toSorted((a, b) => a.start - b.start);
|
|
505
|
+
const windows = timed.map((step, index) => ({
|
|
506
|
+
name: step.name,
|
|
507
|
+
failed: step.conclusion === "failure",
|
|
508
|
+
start: step.start,
|
|
509
|
+
end: step.completedAt ? Date.parse(step.completedAt) : (timed[index + 1]?.start ?? step.start),
|
|
510
|
+
}));
|
|
460
511
|
|
|
461
512
|
for (const rawLine of raw.split("\n")) {
|
|
462
513
|
const cliLine = rawLine.match(/^[^\t]+\t([^\t]+)\t(\uFEFF?\d{4}-\d{2}-\d{2}T[\d:.]+Z\s?.*)$/);
|
|
463
514
|
const line = (cliLine?.[2] ?? rawLine).replace(/^\uFEFF/, "").replace(/\r$/, "");
|
|
515
|
+
const byTime = stepAtTime(line, windows);
|
|
516
|
+
if (byTime !== undefined) timedStep = byTime ?? undefined;
|
|
464
517
|
|
|
465
518
|
// Step group markers
|
|
466
519
|
const groupMatch = line.match(/##\[group\](.+)/);
|
|
467
520
|
if (groupMatch) {
|
|
468
521
|
if (!cliLine) currentStep = groupMatch[1].trim();
|
|
522
|
+
if (byTime === undefined) timedStep = undefined;
|
|
469
523
|
continue;
|
|
470
524
|
}
|
|
471
525
|
if (line.includes("##[endgroup]")) continue;
|
|
@@ -473,7 +527,11 @@ export function parseRawJobLogs(raw: string): LogEntry[] {
|
|
|
473
527
|
const cleaned = cleanLogLine(line);
|
|
474
528
|
if (cleaned.length === 0) continue;
|
|
475
529
|
|
|
476
|
-
entries.push({
|
|
530
|
+
entries.push({
|
|
531
|
+
step: cliLine?.[1] ?? timedStep ?? currentStep,
|
|
532
|
+
message: cleaned,
|
|
533
|
+
established: cliLine !== null || typeof byTime === "string",
|
|
534
|
+
});
|
|
477
535
|
}
|
|
478
536
|
|
|
479
537
|
return entries;
|
|
@@ -509,7 +567,15 @@ const normalizeFingerprint = (message: string) =>
|
|
|
509
567
|
.slice(0, 240);
|
|
510
568
|
|
|
511
569
|
export const diagnoseLogEntries = (entries: readonly LogEntry[]): LogDiagnosis => {
|
|
512
|
-
const
|
|
570
|
+
const fetchProgress =
|
|
571
|
+
/^(?:\s*(?:\*\s+\[new branch\]|\[new branch\]|From\s+\S+|\*\s+\[new tag\]|\s*->\s+origin\/)|\s*\+?git fetch\b|.*refs\/(?:heads|remotes)\/)/i;
|
|
572
|
+
const lines = entries
|
|
573
|
+
.filter(
|
|
574
|
+
({ message }) =>
|
|
575
|
+
/fatal:|error:|failed|exception|could not resolve|connection|timed out/i.test(message) ||
|
|
576
|
+
!fetchProgress.test(message),
|
|
577
|
+
)
|
|
578
|
+
.map((entry) => entry.message);
|
|
513
579
|
const text = lines.join("\n");
|
|
514
580
|
const find = (pattern: RegExp) => lines.find((line) => pattern.test(line)) ?? null;
|
|
515
581
|
const testsStarted =
|
|
@@ -592,31 +658,12 @@ const resolveJobId = Effect.fn("workflow.resolveJobId")(function* (
|
|
|
592
658
|
});
|
|
593
659
|
});
|
|
594
660
|
|
|
595
|
-
const filterFailedStepEntries = Effect.fn("workflow.filterFailedStepEntries")(function* (
|
|
596
|
-
runId: number,
|
|
597
|
-
jobId: number,
|
|
598
|
-
entries: LogEntry[],
|
|
599
|
-
repo: string | null,
|
|
600
|
-
) {
|
|
601
|
-
const jobs = yield* listJobs(runId, repo);
|
|
602
|
-
const job = jobs.find((j) => j.databaseId === jobId);
|
|
603
|
-
if (!job) return entries;
|
|
604
|
-
|
|
605
|
-
const failedStepNames = new Set(
|
|
606
|
-
job.steps.filter((s) => s.conclusion === "failure").map((s) => s.name),
|
|
607
|
-
);
|
|
608
|
-
|
|
609
|
-
if (failedStepNames.size === 0) return entries;
|
|
610
|
-
|
|
611
|
-
const filtered = entries.filter((e) => failedStepNames.has(e.step));
|
|
612
|
-
return filtered.length > 0 ? filtered : entries;
|
|
613
|
-
});
|
|
614
|
-
|
|
615
661
|
export const fetchJobLogs = Effect.fn("workflow.fetchJobLogs")(function* (opts: {
|
|
616
662
|
runId: number;
|
|
617
663
|
job: string;
|
|
618
664
|
jobId?: number | null;
|
|
619
665
|
failedStepNames?: readonly string[] | null;
|
|
666
|
+
steps?: ReadonlyArray<StepTiming>;
|
|
620
667
|
failedStepsOnly: boolean;
|
|
621
668
|
diagnose?: boolean;
|
|
622
669
|
format: string;
|
|
@@ -646,30 +693,33 @@ export const fetchJobLogs = Effect.fn("workflow.fetchJobLogs")(function* (opts:
|
|
|
646
693
|
|
|
647
694
|
const jobId = opts.jobId ?? (yield* resolveJobId(opts.runId, opts.job, opts.repo));
|
|
648
695
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
.pipe(Effect.map((r) => r.stdout)),
|
|
696
|
+
// `gh run view --log` can omit lines, so the job log endpoint is the primary source.
|
|
697
|
+
const apiLogs = readJobLogApi(`repos/${owner}/${repoName}/actions/jobs/${jobId}/logs`).pipe(
|
|
698
|
+
Effect.map((r) => r.stdout),
|
|
653
699
|
);
|
|
654
700
|
const cliLogs = fetchLogs(opts.runId, opts.failedStepsOnly, jobId, opts.repo).pipe(
|
|
655
701
|
Effect.map((r) => r.log),
|
|
656
702
|
);
|
|
657
|
-
const raw =
|
|
658
|
-
? yield* cliLogs.pipe(
|
|
659
|
-
Effect.flatMap((log) => (log.trim() ? Effect.succeed(log) : apiLogs)),
|
|
660
|
-
Effect.catchTag("GitHubCommandError", () => apiLogs),
|
|
661
|
-
)
|
|
662
|
-
: yield* apiLogs.pipe(Effect.catchTag("GitHubCommandError", () => cliLogs));
|
|
703
|
+
const raw = yield* apiLogs.pipe(Effect.catchTag("GitHubCommandError", () => cliLogs));
|
|
663
704
|
|
|
664
|
-
|
|
705
|
+
const steps = !opts.failedStepsOnly
|
|
706
|
+
? []
|
|
707
|
+
: (opts.steps ??
|
|
708
|
+
(yield* listJobs(opts.runId, opts.repo)).find((j) => j.databaseId === jobId)?.steps ??
|
|
709
|
+
[]);
|
|
710
|
+
const attributed = parseAttributedJobLogs(raw, steps);
|
|
711
|
+
let entries: LogEntry[] = attributed.map(({ step, message }) => ({ step, message }));
|
|
665
712
|
|
|
666
713
|
if (opts.failedStepsOnly) {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
714
|
+
const wanted = new Set(
|
|
715
|
+
opts.failedStepNames?.length
|
|
716
|
+
? opts.failedStepNames
|
|
717
|
+
: steps.filter((s) => s.conclusion === "failure").map((s) => s.name),
|
|
718
|
+
);
|
|
719
|
+
const untimed = steps.some((s) => wanted.has(s.name) && !s.startedAt);
|
|
720
|
+
const kept = attributed.filter((e) => wanted.has(e.step) || !e.established);
|
|
721
|
+
if (!untimed && kept.some((e) => wanted.has(e.step))) {
|
|
722
|
+
entries = kept.map(({ step, message }) => ({ step, message }));
|
|
673
723
|
}
|
|
674
724
|
}
|
|
675
725
|
|
|
@@ -6,6 +6,7 @@ export const retryTransient = <A, E, R>(opts: {
|
|
|
6
6
|
attempt: () => Effect.Effect<A, E, R>;
|
|
7
7
|
isTransient: (error: E) => boolean;
|
|
8
8
|
maxRetries: number;
|
|
9
|
+
retryDelayMs?: (error: E, attempt: number, fallbackMs: number) => number;
|
|
9
10
|
}): Effect.Effect<A, E, R> => {
|
|
10
11
|
const loop = (attempt: number): Effect.Effect<A, E, R> =>
|
|
11
12
|
opts
|
|
@@ -13,9 +14,12 @@ export const retryTransient = <A, E, R>(opts: {
|
|
|
13
14
|
.pipe(
|
|
14
15
|
Effect.catch((error: E) =>
|
|
15
16
|
opts.isTransient(error) && attempt < opts.maxRetries
|
|
16
|
-
? Effect.sleep(
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
? Effect.sleep(
|
|
18
|
+
Duration.millis(
|
|
19
|
+
opts.retryDelayMs?.(error, attempt, BASE_DELAY_MS * 2 ** attempt) ??
|
|
20
|
+
BASE_DELAY_MS * 2 ** attempt,
|
|
21
|
+
),
|
|
22
|
+
).pipe(Effect.flatMap(() => loop(attempt + 1)))
|
|
19
23
|
: Effect.fail(error),
|
|
20
24
|
),
|
|
21
25
|
);
|