@blogic-cz/agent-tools 0.8.15 → 0.8.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blogic-cz/agent-tools",
3
- "version": "0.8.15",
3
+ "version": "0.8.16",
4
4
  "description": "CLI tools for AI coding agent workflows — GitHub, database, Kubernetes, Azure DevOps, logs, sessions, and audit",
5
5
  "keywords": [
6
6
  "agent",
@@ -21,6 +21,7 @@ import {
21
21
  detectPRStatus,
22
22
  editPR,
23
23
  fetchChecks,
24
+ fetchChecksForCommand,
24
25
  fetchFailedChecks,
25
26
  mergePR,
26
27
  rerunChecks,
@@ -200,7 +201,7 @@ export const prChecksCommand = Command.make(
200
201
  ({ failFast, format, pr, timeout, watch }) =>
201
202
  Effect.gen(function* () {
202
203
  const prNumber = Option.getOrNull(pr);
203
- const checks = yield* fetchChecks(prNumber, watch, failFast, timeout);
204
+ const checks = yield* fetchChecksForCommand(prNumber, watch, failFast, timeout);
204
205
  yield* logFormatted(checks, format);
205
206
  }),
206
207
  ).pipe(Command.withDescription("Fetch CI check status for a PR (optionally watch with timeout)"));
@@ -1,6 +1,15 @@
1
1
  import { Console, Effect, Option } from "effect";
2
2
 
3
- import type { BranchPRDetail, CheckResult, MergeResult, MergeStrategy, PRInfo } from "#gh/types";
3
+ import type {
4
+ BranchPRDetail,
5
+ CheckResult,
6
+ FailedCheckDetail,
7
+ FailedCheckRunContext,
8
+ MergeResult,
9
+ MergeStrategy,
10
+ PRInfo,
11
+ WorkflowRunDetail,
12
+ } from "#gh/types";
4
13
 
5
14
  import { GitHubCommandError, GitHubMergeError, GitHubTimeoutError } from "#gh/errors";
6
15
  import { GitHubService } from "#gh/service";
@@ -8,6 +17,174 @@ import { GitHubService } from "#gh/service";
8
17
  import type { ButStatusJson, PRViewJsonResult } from "./helpers";
9
18
  import { runLocalCommand } from "./helpers";
10
19
 
20
+ const CHECK_JSON_FIELDS = "name,state,bucket,link";
21
+ const GITHUB_ACTIONS_RUN_ID_RE = /github\.com\/[^/]+\/[^/]+\/actions\/runs\/(\d+)/;
22
+
23
+ const buildChecksCommand = (pr: number | null, includeWatch: boolean): string =>
24
+ `bun agent-tools-gh pr checks${pr !== null ? ` --pr ${pr}` : ""}${includeWatch ? " --watch" : ""}`;
25
+
26
+ const buildChecksFailedCommand = (pr: number | null): string =>
27
+ `bun agent-tools-gh pr checks-failed${pr !== null ? ` --pr ${pr}` : ""}`;
28
+
29
+ const extractRunIdFromCheckLink = (link: string): number | null => {
30
+ const match = link.match(GITHUB_ACTIONS_RUN_ID_RE);
31
+ if (!match?.[1]) {
32
+ return null;
33
+ }
34
+
35
+ const runId = Number(match[1]);
36
+ return Number.isFinite(runId) ? runId : null;
37
+ };
38
+
39
+ const fetchWorkflowRunFailureContext = Effect.fn("pr.fetchWorkflowRunFailureContext")(function* (
40
+ runId: number,
41
+ ) {
42
+ const gh = yield* GitHubService;
43
+
44
+ const run = yield* gh
45
+ .runGhJson<WorkflowRunDetail>([
46
+ "run",
47
+ "view",
48
+ String(runId),
49
+ "--json",
50
+ "databaseId,url,workflowName,status,conclusion,jobs",
51
+ ])
52
+ .pipe(Effect.catchTag("GitHubCommandError", () => Effect.succeed(null)));
53
+
54
+ if (run === null) {
55
+ return null;
56
+ }
57
+
58
+ const failedJobs = run.jobs
59
+ .filter((job) => job.conclusion === "failure" || job.status === "failure")
60
+ .map((job) => ({
61
+ name: job.name,
62
+ status: job.status,
63
+ conclusion: job.conclusion,
64
+ url: job.url,
65
+ failedSteps: job.steps
66
+ .filter((step) => step.conclusion === "failure" || step.status === "failure")
67
+ .map((step) => step.name),
68
+ }));
69
+
70
+ const context: FailedCheckRunContext = {
71
+ runId: run.databaseId,
72
+ url: run.url,
73
+ workflowName: run.workflowName,
74
+ status: run.status,
75
+ conclusion: run.conclusion,
76
+ failedJobs,
77
+ };
78
+
79
+ return context;
80
+ });
81
+
82
+ const fetchCheckResults = Effect.fn("pr.fetchCheckResults")(function* (pr: number | null) {
83
+ const gh = yield* GitHubService;
84
+
85
+ const args = ["pr", "checks"];
86
+ if (pr !== null) {
87
+ args.push(String(pr));
88
+ }
89
+
90
+ return yield* gh.runGhJson<CheckResult[]>([...args, "--json", CHECK_JSON_FIELDS]);
91
+ });
92
+
93
+ const buildFailedChecksReport = Effect.fn("pr.buildFailedChecksReport")(function* (
94
+ pr: number | null,
95
+ checks: CheckResult[],
96
+ ) {
97
+ const failedChecks = checks.filter((check) => check.bucket === "fail");
98
+ const pendingChecks = checks.filter((check) => check.bucket === "pending");
99
+ const passedChecks = checks.filter((check) => check.bucket === "pass");
100
+
101
+ const runIds = [
102
+ ...new Set(
103
+ failedChecks
104
+ .map((check) => extractRunIdFromCheckLink(check.link))
105
+ .filter((id) => id !== null),
106
+ ),
107
+ ];
108
+
109
+ const runContexts = new Map<number, FailedCheckRunContext | null>();
110
+ const contexts = yield* Effect.forEach(
111
+ runIds,
112
+ (runId) =>
113
+ fetchWorkflowRunFailureContext(runId).pipe(
114
+ Effect.map((context) => [runId, context] as const),
115
+ ),
116
+ { concurrency: "unbounded" },
117
+ );
118
+
119
+ for (const [runId, context] of contexts) {
120
+ runContexts.set(runId, context);
121
+ }
122
+
123
+ const enrichedFailedChecks: FailedCheckDetail[] = failedChecks.map((check) => {
124
+ const runId = extractRunIdFromCheckLink(check.link);
125
+ return {
126
+ ...check,
127
+ runId,
128
+ run: runId === null ? null : (runContexts.get(runId) ?? null),
129
+ };
130
+ });
131
+
132
+ const nextCommands = [
133
+ buildChecksFailedCommand(pr),
134
+ ...new Set(
135
+ enrichedFailedChecks.flatMap((check) => {
136
+ if (check.runId === null) {
137
+ return [];
138
+ }
139
+
140
+ const commands = [`bun agent-tools-gh workflow view --run ${check.runId}`];
141
+ const firstFailedJob = check.run?.failedJobs[0];
142
+ if (firstFailedJob) {
143
+ commands.push(
144
+ `bun agent-tools-gh workflow job-logs --run ${check.runId} --job ${JSON.stringify(firstFailedJob.name)} --failed-steps-only`,
145
+ );
146
+ }
147
+
148
+ return commands;
149
+ }),
150
+ ),
151
+ ...(pendingChecks.length > 0 ? [buildChecksCommand(pr, true)] : []),
152
+ ];
153
+
154
+ const message =
155
+ failedChecks.length === 0
156
+ ? pendingChecks.length > 0
157
+ ? `No failed checks yet; ${pendingChecks.length} check(s) are still running.`
158
+ : "No failed checks detected."
159
+ : pendingChecks.length > 0
160
+ ? `Detected ${failedChecks.length} failed check(s) while ${pendingChecks.length} check(s) are still running.`
161
+ : `Detected ${failedChecks.length} failed check(s).`;
162
+
163
+ const hint =
164
+ failedChecks.length === 0
165
+ ? pendingChecks.length > 0
166
+ ? "Wait for the remaining checks to finish, or use --watch to block until CI settles."
167
+ : "All current checks are green."
168
+ : pendingChecks.length > 0
169
+ ? "Inspect the failed workflow run first. Other checks are still running and may change overall merge readiness."
170
+ : "Inspect the failed workflow run and failed job logs to get the first concrete error, then rerun only if the failure is understood.";
171
+
172
+ return {
173
+ status: failedChecks.length > 0 ? "failed" : "no_failures",
174
+ message,
175
+ summary: {
176
+ total: checks.length,
177
+ failed: failedChecks.length,
178
+ pending: pendingChecks.length,
179
+ passed: passedChecks.length,
180
+ },
181
+ failedChecks: enrichedFailedChecks,
182
+ pendingChecks,
183
+ hint,
184
+ nextCommands,
185
+ };
186
+ });
187
+
11
188
  export const viewPR = Effect.fn("pr.viewPR")(function* (prNumber: number | null) {
12
189
  const gh = yield* GitHubService;
13
190
 
@@ -446,22 +623,50 @@ export const fetchChecks = Effect.fn("pr.fetchChecks")(function* (
446
623
  }),
447
624
  );
448
625
 
449
- return yield* gh.runGhJson<CheckResult[]>([...args, "--json", "name,state,bucket,link"]);
626
+ return yield* fetchCheckResults(pr);
450
627
  }
451
628
 
452
- const results = yield* gh.runGhJson<CheckResult[]>([...args, "--json", "name,state,bucket,link"]);
629
+ const results = yield* fetchCheckResults(pr);
453
630
  if (results.some((c) => c.bucket === "pending")) {
454
631
  yield* Console.warn(
455
632
  `ℹ️ Some checks are still running. Prefer --watch to block until completion instead of polling:\n` +
456
- ` bun agent-tools-gh pr checks${pr !== null ? ` --pr ${pr}` : ""} --watch`,
633
+ ` ${buildChecksCommand(pr, true)}`,
457
634
  );
458
635
  }
459
636
  return results;
460
637
  });
461
638
 
462
639
  export const fetchFailedChecks = Effect.fn("pr.fetchFailedChecks")(function* (pr: number | null) {
463
- const checks = yield* fetchChecks(pr, false, false, 0);
464
- return checks.filter((check) => check.bucket === "fail");
640
+ const checks = yield* fetchCheckResults(pr);
641
+ return yield* buildFailedChecksReport(pr, checks);
642
+ });
643
+
644
+ export const fetchChecksForCommand = Effect.fn("pr.fetchChecksForCommand")(function* (
645
+ pr: number | null,
646
+ watch: boolean,
647
+ failFast: boolean,
648
+ timeoutSeconds: number,
649
+ ) {
650
+ if (!watch) {
651
+ return yield* fetchChecks(pr, false, failFast, timeoutSeconds);
652
+ }
653
+
654
+ const watchedChecks = yield* fetchChecks(pr, true, failFast, timeoutSeconds).pipe(
655
+ Effect.catchTag("GitHubCommandError", (error) =>
656
+ Effect.succeed({ _tag: "command_error" as const, error }),
657
+ ),
658
+ );
659
+
660
+ if (Array.isArray(watchedChecks)) {
661
+ return watchedChecks;
662
+ }
663
+
664
+ const finalChecks = yield* fetchCheckResults(pr);
665
+ if (finalChecks.some((check) => check.bucket === "fail")) {
666
+ return yield* buildFailedChecksReport(pr, finalChecks);
667
+ }
668
+
669
+ return yield* Effect.fail(watchedChecks.error);
465
670
  });
466
671
 
467
672
  export const rerunChecks = Effect.fn("pr.rerunChecks")(function* (
@@ -62,6 +62,62 @@ export type CheckResult = {
62
62
  link: string;
63
63
  };
64
64
 
65
+ export type FailedCheckJob = {
66
+ name: string;
67
+ status: string;
68
+ conclusion: string | null;
69
+ url: string;
70
+ failedSteps: string[];
71
+ };
72
+
73
+ export type FailedCheckRunContext = {
74
+ runId: number;
75
+ url: string | null;
76
+ workflowName: string | null;
77
+ status: string;
78
+ conclusion: string | null;
79
+ failedJobs: FailedCheckJob[];
80
+ };
81
+
82
+ export type FailedCheckDetail = CheckResult & {
83
+ runId: number | null;
84
+ run: FailedCheckRunContext | null;
85
+ };
86
+
87
+ export type FailedChecksReport = {
88
+ status: "failed" | "no_failures";
89
+ message: string;
90
+ summary: {
91
+ total: number;
92
+ failed: number;
93
+ pending: number;
94
+ passed: number;
95
+ };
96
+ failedChecks: FailedCheckDetail[];
97
+ pendingChecks: CheckResult[];
98
+ hint: string;
99
+ nextCommands: string[];
100
+ };
101
+
102
+ export type WorkflowRunDetail = {
103
+ databaseId: number;
104
+ url: string;
105
+ workflowName: string | null;
106
+ status: string;
107
+ conclusion: string | null;
108
+ jobs: Array<{
109
+ name: string;
110
+ status: string;
111
+ conclusion: string | null;
112
+ url: string;
113
+ steps: Array<{
114
+ name: string;
115
+ status: string;
116
+ conclusion: string | null;
117
+ }>;
118
+ }>;
119
+ };
120
+
65
121
  export type MergeResult = {
66
122
  merged: boolean;
67
123
  strategy: MergeStrategy;