@blogic-cz/agent-tools 0.15.13 → 0.16.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 +5 -0
- package/package.json +29 -78
- package/src/audit-tool/index.ts +0 -0
- package/src/az-tool/errors.ts +4 -4
- package/src/az-tool/service.ts +2 -2
- package/src/db-tool/errors.ts +5 -18
- package/src/db-tool/index.ts +2 -0
- package/src/db-tool/security.ts +96 -15
- package/src/db-tool/service.ts +71 -146
- package/src/db-tool/sql-client.ts +80 -0
- package/src/gh-tool/errors.ts +11 -14
- package/src/gh-tool/pr/helpers.ts +11 -9
- package/src/index.ts +2 -0
- package/src/k8s-tool/errors.ts +4 -4
- package/src/k8s-tool/service.ts +33 -28
- package/src/logs-tool/errors.ts +11 -14
- package/src/observability-tool/errors.ts +1 -1
- package/src/observability-tool/index.ts +0 -0
- package/src/session-tool/errors.ts +7 -10
- package/src/session-tool/index.ts +0 -0
- package/src/shared/binary-preflight.ts +42 -0
- package/src/shared/env-template.ts +1 -1
- package/src/shared/exec.ts +1 -1
- package/src/shared/index.ts +8 -1
- package/src/shared/prerequisites/errors.ts +1 -1
- package/src/shared/prerequisites/index.ts +12 -0
- package/src/shared/throttle.ts +1 -1
- package/src/shared/types.ts +6 -0
- package/src/db-tool/psql.ts +0 -27
package/src/db-tool/service.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
|
|
2
|
-
import { Clock, Context, Duration, Effect, Layer, Ref, Stream } from "effect";
|
|
2
|
+
import { Clock, Context, Duration, Effect, Layer, Ref, Result, Stream } from "effect";
|
|
3
3
|
|
|
4
4
|
import type { DbConfig, DbMutationOperation, QueryResult, SchemaMode } from "./types";
|
|
5
5
|
|
|
@@ -9,12 +9,12 @@ import { resolveEnvTemplate } from "#shared/env-template";
|
|
|
9
9
|
import { resolveEnvironmentScopedPrerequisites } from "#shared/prerequisites/config";
|
|
10
10
|
import { runWithProfilePrerequisites } from "#shared/prerequisites/runtime";
|
|
11
11
|
import { buildApiProbeArgs } from "#shared/k8s-probe";
|
|
12
|
+
import { missingBinaryFromSpawnFailure } from "#shared/binary-preflight";
|
|
12
13
|
import { DbConfigService, TUNNEL_CHECK_INTERVAL_MS } from "./config-service";
|
|
13
|
-
import {
|
|
14
|
+
import { DbSqlClient, type DbConnection } from "./sql-client";
|
|
14
15
|
import {
|
|
15
16
|
DbConnectionError,
|
|
16
17
|
DbMutationBlockedError,
|
|
17
|
-
DbParseError,
|
|
18
18
|
DbQueryError,
|
|
19
19
|
DbTunnelError,
|
|
20
20
|
type DbError,
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
detectSchemaError,
|
|
31
31
|
getAllowedMutationOperation,
|
|
32
32
|
getMutationTarget,
|
|
33
|
+
hasMultipleStatements,
|
|
33
34
|
isValidTableName,
|
|
34
35
|
isMutationQuery,
|
|
35
36
|
} from "./security";
|
|
@@ -87,6 +88,7 @@ export class DbService extends Context.Service<
|
|
|
87
88
|
Effect.scoped(
|
|
88
89
|
Effect.gen(function* () {
|
|
89
90
|
const executor = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
91
|
+
const sqlClient = yield* DbSqlClient;
|
|
90
92
|
const agentToolsConfig = yield* ConfigService;
|
|
91
93
|
const dbConfig = yield* DbConfigService;
|
|
92
94
|
|
|
@@ -231,23 +233,29 @@ export class DbService extends Context.Service<
|
|
|
231
233
|
return { stdout, stderr, exitCode };
|
|
232
234
|
}),
|
|
233
235
|
).pipe(
|
|
234
|
-
Effect.mapError(
|
|
235
|
-
(platformError)
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
),
|
|
236
|
+
Effect.mapError((platformError) => {
|
|
237
|
+
const missing = missingBinaryFromSpawnFailure("kubectl", String(platformError));
|
|
238
|
+
return new DbQueryError({
|
|
239
|
+
message: `Command execution failed: ${String(platformError)}`,
|
|
240
|
+
sql: "shell command",
|
|
241
|
+
stderr: String(platformError),
|
|
242
|
+
...(missing ? { hint: missing.hint } : {}),
|
|
243
|
+
});
|
|
244
|
+
}),
|
|
243
245
|
);
|
|
244
246
|
|
|
245
247
|
const checkPortOpen = (port: number) =>
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
248
|
+
Effect.tryPromise(async () => {
|
|
249
|
+
const socket = await Bun.connect({
|
|
250
|
+
hostname: "127.0.0.1",
|
|
251
|
+
port,
|
|
252
|
+
socket: { data: () => undefined },
|
|
253
|
+
});
|
|
254
|
+
socket.end();
|
|
255
|
+
return true;
|
|
256
|
+
}).pipe(
|
|
257
|
+
Effect.timeout(TUNNEL_CHECK_INTERVAL_MS),
|
|
258
|
+
Effect.orElseSucceed(() => false),
|
|
251
259
|
);
|
|
252
260
|
|
|
253
261
|
const runWithVpnPrerequisites = <E>(
|
|
@@ -284,11 +292,7 @@ export class DbService extends Context.Service<
|
|
|
284
292
|
return false;
|
|
285
293
|
}
|
|
286
294
|
|
|
287
|
-
|
|
288
|
-
Effect.catch(() => Effect.succeed({ exitCode: 1 })),
|
|
289
|
-
);
|
|
290
|
-
|
|
291
|
-
if (result.exitCode === 0) {
|
|
295
|
+
if (yield* checkPortOpen(port)) {
|
|
292
296
|
return true;
|
|
293
297
|
}
|
|
294
298
|
|
|
@@ -378,69 +382,34 @@ export class DbService extends Context.Service<
|
|
|
378
382
|
return result.exitCode === 0;
|
|
379
383
|
});
|
|
380
384
|
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
config.host,
|
|
390
|
-
"-p",
|
|
391
|
-
String(config.port),
|
|
392
|
-
"-U",
|
|
393
|
-
config.user,
|
|
394
|
-
"-d",
|
|
395
|
-
config.database,
|
|
396
|
-
];
|
|
397
|
-
|
|
398
|
-
const commandArgs = useTuplesOnly
|
|
399
|
-
? [...args, "-t", "-A", "-c", sql]
|
|
400
|
-
: [...args, "-c", sql];
|
|
401
|
-
|
|
402
|
-
return ChildProcess.make("psql", commandArgs, {
|
|
403
|
-
stdout: "pipe",
|
|
404
|
-
stderr: "pipe",
|
|
405
|
-
env: {
|
|
406
|
-
...process.env,
|
|
407
|
-
PATH: resolvePsqlSearchPath(process.env.PATH),
|
|
408
|
-
...(password ? { PGPASSWORD: password } : {}),
|
|
409
|
-
...(isFullyReadOnly(config)
|
|
410
|
-
? { PGOPTIONS: "-c default_transaction_read_only=on" }
|
|
411
|
-
: {}),
|
|
412
|
-
} as Record<string, string>,
|
|
413
|
-
});
|
|
414
|
-
};
|
|
385
|
+
const toConnection = (config: DbConfig, password: string): DbConnection => ({
|
|
386
|
+
host: config.host,
|
|
387
|
+
port: config.port,
|
|
388
|
+
user: config.user,
|
|
389
|
+
database: config.database,
|
|
390
|
+
password,
|
|
391
|
+
readOnly: isFullyReadOnly(config),
|
|
392
|
+
});
|
|
415
393
|
|
|
416
|
-
const
|
|
417
|
-
config
|
|
418
|
-
|
|
419
|
-
)
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
);
|
|
426
|
-
const result = yield* executeShellCommand(command).pipe(
|
|
427
|
-
Effect.catch(() =>
|
|
428
|
-
Effect.succeed({
|
|
429
|
-
stdout: "",
|
|
430
|
-
stderr: "",
|
|
431
|
-
exitCode: 1,
|
|
432
|
-
}),
|
|
394
|
+
const runSql = (config: DbConfig, password: string, sql: string) =>
|
|
395
|
+
sqlClient.run(toConnection(config, password), sql);
|
|
396
|
+
|
|
397
|
+
const fetchSingleColumn = (config: DbConfig, password: string, sql: string) =>
|
|
398
|
+
runSql(config, password, sql).pipe(
|
|
399
|
+
Effect.map((outcome) =>
|
|
400
|
+
outcome.rows
|
|
401
|
+
.map((row) => String(Object.values(row)[0] ?? ""))
|
|
402
|
+
.filter((value) => value.length > 0),
|
|
433
403
|
),
|
|
404
|
+
Effect.orElseSucceed(() => [] as string[]),
|
|
434
405
|
);
|
|
435
|
-
if (result.exitCode !== 0) {
|
|
436
|
-
return [] as string[];
|
|
437
|
-
}
|
|
438
406
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
407
|
+
const fetchTableNamesForError = (config: DbConfig, password: string) =>
|
|
408
|
+
fetchSingleColumn(
|
|
409
|
+
config,
|
|
410
|
+
password,
|
|
411
|
+
`SELECT schemaname || '.' || tablename FROM pg_tables WHERE schemaname NOT IN (${SYSTEM_SCHEMAS_SQL}) ORDER BY schemaname, tablename;`,
|
|
412
|
+
);
|
|
444
413
|
|
|
445
414
|
const fetchColumnNamesForError = Effect.fn("DbService.fetchColumnNamesForError")(function* (
|
|
446
415
|
config: DbConfig,
|
|
@@ -458,29 +427,11 @@ export class DbService extends Context.Service<
|
|
|
458
427
|
? `AND table_schema = '${escapedSchemaName}'`
|
|
459
428
|
: `AND table_schema NOT IN (${SYSTEM_SCHEMAS_SQL})`;
|
|
460
429
|
|
|
461
|
-
|
|
430
|
+
return yield* fetchSingleColumn(
|
|
462
431
|
config,
|
|
463
|
-
`SELECT column_name FROM information_schema.columns WHERE table_name = '${escapedTableName}' ${schemaFilter} ORDER BY table_schema, ordinal_position;`,
|
|
464
432
|
password,
|
|
465
|
-
|
|
466
|
-
);
|
|
467
|
-
const result = yield* executeShellCommand(command).pipe(
|
|
468
|
-
Effect.catch(() =>
|
|
469
|
-
Effect.succeed({
|
|
470
|
-
stdout: "",
|
|
471
|
-
stderr: "",
|
|
472
|
-
exitCode: 1,
|
|
473
|
-
}),
|
|
474
|
-
),
|
|
433
|
+
`SELECT column_name FROM information_schema.columns WHERE table_name = '${escapedTableName}' ${schemaFilter} ORDER BY table_schema, ordinal_position;`,
|
|
475
434
|
);
|
|
476
|
-
if (result.exitCode !== 0) {
|
|
477
|
-
return [] as string[];
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
return result.stdout
|
|
481
|
-
.trim()
|
|
482
|
-
.split("\n")
|
|
483
|
-
.filter((name) => name.length > 0);
|
|
484
435
|
});
|
|
485
436
|
|
|
486
437
|
const executeSelectQuery = Effect.fn("DbService.executeSelectQuery")(function* (
|
|
@@ -492,16 +443,15 @@ export class DbService extends Context.Service<
|
|
|
492
443
|
limit?: number,
|
|
493
444
|
) {
|
|
494
445
|
const selectSql = sql.trim().replace(/;\s*$/, "");
|
|
495
|
-
const
|
|
496
|
-
const command = buildPsqlCommand(config, wrappedSql, password, true);
|
|
497
|
-
const result = yield* executeShellCommand(command);
|
|
446
|
+
const outcome = yield* Effect.result(runSql(config, password, selectSql));
|
|
498
447
|
const endTime = yield* Clock.currentTimeMillis;
|
|
499
448
|
|
|
500
|
-
if (
|
|
501
|
-
const
|
|
449
|
+
if (Result.isFailure(outcome)) {
|
|
450
|
+
const failureMessage = outcome.failure.message.trim();
|
|
451
|
+
const schemaError = detectSchemaError(failureMessage, sql);
|
|
502
452
|
const baseResult: QueryResult = {
|
|
503
453
|
success: false,
|
|
504
|
-
error:
|
|
454
|
+
error: failureMessage || "Query failed",
|
|
505
455
|
executionTimeMs: Number(endTime) - startTimeMs,
|
|
506
456
|
};
|
|
507
457
|
|
|
@@ -527,32 +477,10 @@ export class DbService extends Context.Service<
|
|
|
527
477
|
};
|
|
528
478
|
}
|
|
529
479
|
|
|
530
|
-
return yield*
|
|
531
|
-
message: baseResult.error ?? "Query failed",
|
|
532
|
-
sql,
|
|
533
|
-
stderr: result.stderr.trim() || undefined,
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
const trimmedOutput = result.stdout.trim();
|
|
538
|
-
if (!trimmedOutput || trimmedOutput === "null") {
|
|
539
|
-
return {
|
|
540
|
-
success: true,
|
|
541
|
-
data: [],
|
|
542
|
-
rowCount: 0,
|
|
543
|
-
executionTimeMs: Number(endTime) - startTimeMs,
|
|
544
|
-
};
|
|
480
|
+
return yield* outcome.failure;
|
|
545
481
|
}
|
|
546
482
|
|
|
547
|
-
const rawData =
|
|
548
|
-
try: () => JSON.parse(trimmedOutput) as Record<string, unknown>[],
|
|
549
|
-
catch: () =>
|
|
550
|
-
new DbParseError({
|
|
551
|
-
message: "Failed to parse query result as JSON.",
|
|
552
|
-
rawOutput: trimmedOutput.slice(0, 500),
|
|
553
|
-
}),
|
|
554
|
-
});
|
|
555
|
-
|
|
483
|
+
const rawData = outcome.success.rows;
|
|
556
484
|
const transformed = applyTransform
|
|
557
485
|
? transformQueryResult(rawData, limit)
|
|
558
486
|
: {
|
|
@@ -577,25 +505,13 @@ export class DbService extends Context.Service<
|
|
|
577
505
|
password: string,
|
|
578
506
|
startTimeMs: number,
|
|
579
507
|
) {
|
|
580
|
-
const
|
|
581
|
-
const result = yield* executeShellCommand(command);
|
|
508
|
+
const outcome = yield* runSql(config, password, sql);
|
|
582
509
|
const endTime = yield* Clock.currentTimeMillis;
|
|
583
|
-
|
|
584
|
-
if (result.exitCode !== 0) {
|
|
585
|
-
return yield* new DbQueryError({
|
|
586
|
-
message: result.stderr.trim() || `psql exited with code ${result.exitCode}`,
|
|
587
|
-
sql,
|
|
588
|
-
stderr: result.stderr.trim() || undefined,
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
const output = result.stdout.trim();
|
|
593
|
-
const rowCountMatch = output.match(/(?:UPDATE|DELETE|INSERT \d+)\s+(\d+)/i);
|
|
594
|
-
const rowCount = rowCountMatch ? parseInt(rowCountMatch[1], 10) : 0;
|
|
510
|
+
const rowCount = outcome.rowCount;
|
|
595
511
|
|
|
596
512
|
return {
|
|
597
513
|
success: true,
|
|
598
|
-
message:
|
|
514
|
+
message: `${outcome.command} ${rowCount}`.trim(),
|
|
599
515
|
rowCount,
|
|
600
516
|
executionTimeMs: Number(endTime) - startTimeMs,
|
|
601
517
|
};
|
|
@@ -748,6 +664,15 @@ export class DbService extends Context.Service<
|
|
|
748
664
|
const startTimeMs = yield* Clock.currentTimeMillis;
|
|
749
665
|
const resolvedConfig = yield* resolveDbConfig(config, env);
|
|
750
666
|
const password = yield* resolvePassword(resolvedConfig, env);
|
|
667
|
+
if (hasMultipleStatements(sql)) {
|
|
668
|
+
return yield* new DbQueryError({
|
|
669
|
+
message:
|
|
670
|
+
"Multiple SQL statements in a single call are not allowed; the mutation guard only inspects the first statement.",
|
|
671
|
+
sql,
|
|
672
|
+
hint: "Send one statement per call.",
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
|
|
751
676
|
const mutation = isMutationQuery(sql);
|
|
752
677
|
const mutationOperation = mutation ? getAllowedMutationOperation(sql) : undefined;
|
|
753
678
|
const mutationTarget = mutation ? getMutationTarget(sql) : undefined;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
|
+
|
|
3
|
+
import { DbQueryError } from "./errors";
|
|
4
|
+
|
|
5
|
+
export type DbConnection = {
|
|
6
|
+
readonly host: string;
|
|
7
|
+
readonly port: number;
|
|
8
|
+
readonly user: string;
|
|
9
|
+
readonly database: string;
|
|
10
|
+
readonly password: string;
|
|
11
|
+
readonly readOnly: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type DbQueryOutcome = {
|
|
15
|
+
readonly rows: Record<string, unknown>[];
|
|
16
|
+
readonly rowCount: number;
|
|
17
|
+
readonly command: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type BunQueryResult = unknown[] & { count?: number; command?: string };
|
|
21
|
+
|
|
22
|
+
export const toErrorMessage = (cause: unknown): string =>
|
|
23
|
+
cause instanceof Error ? cause.message : String(cause);
|
|
24
|
+
|
|
25
|
+
const runWithBunSql = (connection: DbConnection, sql: string) =>
|
|
26
|
+
Effect.tryPromise({
|
|
27
|
+
try: async (): Promise<DbQueryOutcome> => {
|
|
28
|
+
const client = new Bun.SQL({
|
|
29
|
+
hostname: connection.host,
|
|
30
|
+
port: connection.port,
|
|
31
|
+
username: connection.user,
|
|
32
|
+
password: connection.password,
|
|
33
|
+
database: connection.database,
|
|
34
|
+
max: 1,
|
|
35
|
+
// Startup-packet GUC: covers every pooled connection, unlike a post-connect SET.
|
|
36
|
+
...(connection.readOnly ? { connection: { default_transaction_read_only: "on" } } : {}),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
if (connection.readOnly) {
|
|
41
|
+
const [state] = (await client.unsafe("SHOW transaction_read_only")) as {
|
|
42
|
+
transaction_read_only?: string;
|
|
43
|
+
}[];
|
|
44
|
+
if (state?.transaction_read_only !== "on") {
|
|
45
|
+
throw new Error(
|
|
46
|
+
"Refusing to query: the read-only session setting was not accepted by the server, so writes would not be blocked.",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const result = (await client.unsafe(sql)) as BunQueryResult;
|
|
52
|
+
const rows = Array.isArray(result) ? (result as Record<string, unknown>[]) : [];
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
rows,
|
|
56
|
+
rowCount: result.count ?? rows.length,
|
|
57
|
+
command: result.command ?? "",
|
|
58
|
+
};
|
|
59
|
+
} finally {
|
|
60
|
+
// A rejecting close() would replace the real query error via try/finally semantics.
|
|
61
|
+
await client.close().catch(() => undefined);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
catch: (cause) => {
|
|
65
|
+
const message = toErrorMessage(cause);
|
|
66
|
+
return new DbQueryError({ message, sql, stderr: message });
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export class DbSqlClient extends Context.Service<
|
|
71
|
+
DbSqlClient,
|
|
72
|
+
{
|
|
73
|
+
readonly run: (
|
|
74
|
+
connection: DbConnection,
|
|
75
|
+
sql: string,
|
|
76
|
+
) => Effect.Effect<DbQueryOutcome, DbQueryError>;
|
|
77
|
+
}
|
|
78
|
+
>()("@agent-tools/DbSqlClient") {
|
|
79
|
+
static readonly layer = Layer.succeed(DbSqlClient, { run: runWithBunSql });
|
|
80
|
+
}
|
package/src/gh-tool/errors.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
2
|
|
|
3
|
-
export class GitHubCommandError extends Schema.
|
|
3
|
+
export class GitHubCommandError extends Schema.TaggedError<GitHubCommandError>()(
|
|
4
4
|
"GitHubCommandError",
|
|
5
5
|
{
|
|
6
6
|
message: Schema.String,
|
|
@@ -13,7 +13,7 @@ export class GitHubCommandError extends Schema.TaggedErrorClass<GitHubCommandErr
|
|
|
13
13
|
},
|
|
14
14
|
) {}
|
|
15
15
|
|
|
16
|
-
export class GitHubNotFoundError extends Schema.
|
|
16
|
+
export class GitHubNotFoundError extends Schema.TaggedError<GitHubNotFoundError>()(
|
|
17
17
|
"GitHubNotFoundError",
|
|
18
18
|
{
|
|
19
19
|
message: Schema.String,
|
|
@@ -25,25 +25,22 @@ export class GitHubNotFoundError extends Schema.TaggedErrorClass<GitHubNotFoundE
|
|
|
25
25
|
},
|
|
26
26
|
) {}
|
|
27
27
|
|
|
28
|
-
export class GitHubAuthError extends Schema.
|
|
28
|
+
export class GitHubAuthError extends Schema.TaggedError<GitHubAuthError>()("GitHubAuthError", {
|
|
29
29
|
message: Schema.String,
|
|
30
30
|
hint: Schema.optionalKey(Schema.String),
|
|
31
31
|
nextCommand: Schema.optionalKey(Schema.String),
|
|
32
32
|
retryable: Schema.optionalKey(Schema.Boolean),
|
|
33
33
|
}) {}
|
|
34
34
|
|
|
35
|
-
export class GitHubMergeError extends Schema.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
retryable: Schema.optionalKey(Schema.Boolean),
|
|
43
|
-
},
|
|
44
|
-
) {}
|
|
35
|
+
export class GitHubMergeError extends Schema.TaggedError<GitHubMergeError>()("GitHubMergeError", {
|
|
36
|
+
message: Schema.String,
|
|
37
|
+
reason: Schema.Literals(["conflicts", "checks_failing", "branch_protected", "unknown"]),
|
|
38
|
+
hint: Schema.optionalKey(Schema.String),
|
|
39
|
+
nextCommand: Schema.optionalKey(Schema.String),
|
|
40
|
+
retryable: Schema.optionalKey(Schema.Boolean),
|
|
41
|
+
}) {}
|
|
45
42
|
|
|
46
|
-
export class GitHubTimeoutError extends Schema.
|
|
43
|
+
export class GitHubTimeoutError extends Schema.TaggedError<GitHubTimeoutError>()(
|
|
47
44
|
"GitHubTimeoutError",
|
|
48
45
|
{
|
|
49
46
|
message: Schema.String,
|
|
@@ -2,6 +2,7 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
|
|
|
2
2
|
import { Effect, Stream } from "effect";
|
|
3
3
|
|
|
4
4
|
import { GitHubCommandError } from "#gh/errors";
|
|
5
|
+
import { missingBinaryFromSpawnFailure } from "#shared/binary-preflight";
|
|
5
6
|
|
|
6
7
|
export type LocalCommandResult = {
|
|
7
8
|
stdout: string;
|
|
@@ -69,15 +70,16 @@ export const runLocalCommand = Effect.fn("pr.runLocalCommand")(function* (
|
|
|
69
70
|
return commandResult;
|
|
70
71
|
}),
|
|
71
72
|
).pipe(
|
|
72
|
-
Effect.mapError(
|
|
73
|
-
(error)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}),
|
|
80
|
-
|
|
73
|
+
Effect.mapError((error) => {
|
|
74
|
+
const missing = missingBinaryFromSpawnFailure(binary, String(error));
|
|
75
|
+
return new GitHubCommandError({
|
|
76
|
+
command: [binary, ...args].join(" "),
|
|
77
|
+
exitCode: -1,
|
|
78
|
+
stderr: String(error),
|
|
79
|
+
message: missing?.message ?? String(error),
|
|
80
|
+
...(missing ? { hint: missing.hint } : {}),
|
|
81
|
+
});
|
|
82
|
+
}),
|
|
81
83
|
);
|
|
82
84
|
|
|
83
85
|
return result;
|
package/src/index.ts
CHANGED
package/src/k8s-tool/errors.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
2
|
|
|
3
|
-
export class K8sContextError extends Schema.
|
|
3
|
+
export class K8sContextError extends Schema.TaggedError<K8sContextError>()("K8sContextError", {
|
|
4
4
|
message: Schema.String,
|
|
5
5
|
clusterId: Schema.String,
|
|
6
6
|
hint: Schema.optionalKey(Schema.String),
|
|
@@ -8,7 +8,7 @@ export class K8sContextError extends Schema.TaggedErrorClass<K8sContextError>()(
|
|
|
8
8
|
retryable: Schema.optionalKey(Schema.Boolean),
|
|
9
9
|
}) {}
|
|
10
10
|
|
|
11
|
-
export class K8sCommandError extends Schema.
|
|
11
|
+
export class K8sCommandError extends Schema.TaggedError<K8sCommandError>()("K8sCommandError", {
|
|
12
12
|
message: Schema.String,
|
|
13
13
|
command: Schema.String,
|
|
14
14
|
exitCode: Schema.optionalKey(Schema.Number),
|
|
@@ -18,7 +18,7 @@ export class K8sCommandError extends Schema.TaggedErrorClass<K8sCommandError>()(
|
|
|
18
18
|
retryable: Schema.optionalKey(Schema.Boolean),
|
|
19
19
|
}) {}
|
|
20
20
|
|
|
21
|
-
export class K8sTimeoutError extends Schema.
|
|
21
|
+
export class K8sTimeoutError extends Schema.TaggedError<K8sTimeoutError>()("K8sTimeoutError", {
|
|
22
22
|
message: Schema.String,
|
|
23
23
|
command: Schema.String,
|
|
24
24
|
timeoutMs: Schema.Number,
|
|
@@ -27,7 +27,7 @@ export class K8sTimeoutError extends Schema.TaggedErrorClass<K8sTimeoutError>()(
|
|
|
27
27
|
retryable: Schema.optionalKey(Schema.Boolean),
|
|
28
28
|
}) {}
|
|
29
29
|
|
|
30
|
-
export class K8sDangerousCommandError extends Schema.
|
|
30
|
+
export class K8sDangerousCommandError extends Schema.TaggedError<K8sDangerousCommandError>()(
|
|
31
31
|
"K8sDangerousCommandError",
|
|
32
32
|
{
|
|
33
33
|
message: Schema.String,
|
package/src/k8s-tool/service.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { ConfigService, getToolConfig } from "#config";
|
|
15
15
|
import type { K8sConfig } from "#config";
|
|
16
16
|
import { collectProcessOutput, quoteShellArg } from "#shared/exec";
|
|
17
|
+
import { missingBinaryFromSpawnFailure } from "#shared/binary-preflight";
|
|
17
18
|
import { resolveEnvTemplate } from "#shared/env-template";
|
|
18
19
|
import { isPrerequisiteRunError } from "#shared/prerequisites/errors";
|
|
19
20
|
import { normalizeProfilePrerequisites } from "#shared/prerequisites/config";
|
|
@@ -135,15 +136,16 @@ export class K8sService extends Context.Service<
|
|
|
135
136
|
}),
|
|
136
137
|
).pipe(
|
|
137
138
|
Effect.timeoutOption(timeoutMs),
|
|
138
|
-
Effect.mapError(
|
|
139
|
-
(platformError)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}),
|
|
146
|
-
|
|
139
|
+
Effect.mapError((platformError) => {
|
|
140
|
+
const missing = missingBinaryFromSpawnFailure("kubectl", String(platformError));
|
|
141
|
+
return new K8sCommandError({
|
|
142
|
+
message: `Command execution failed: ${String(platformError)}`,
|
|
143
|
+
command: commandStr,
|
|
144
|
+
exitCode: -1,
|
|
145
|
+
stderr: String(platformError),
|
|
146
|
+
...(missing ? { hint: missing.hint } : {}),
|
|
147
|
+
});
|
|
148
|
+
}),
|
|
147
149
|
);
|
|
148
150
|
|
|
149
151
|
const runPrerequisiteCommand = (command: ChildProcess.Command, label: string) =>
|
|
@@ -153,15 +155,16 @@ export class K8sService extends Context.Service<
|
|
|
153
155
|
return yield* collectProcessOutput(process);
|
|
154
156
|
}),
|
|
155
157
|
).pipe(
|
|
156
|
-
Effect.mapError(
|
|
157
|
-
(platformError)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}),
|
|
164
|
-
|
|
158
|
+
Effect.mapError((platformError) => {
|
|
159
|
+
const missing = missingBinaryFromSpawnFailure("kubectl", String(platformError));
|
|
160
|
+
return new K8sCommandError({
|
|
161
|
+
message: `Prerequisite command failed (${label}): ${String(platformError)}`,
|
|
162
|
+
command: label,
|
|
163
|
+
exitCode: -1,
|
|
164
|
+
stderr: String(platformError),
|
|
165
|
+
...(missing ? { hint: missing.hint } : {}),
|
|
166
|
+
});
|
|
167
|
+
}),
|
|
165
168
|
);
|
|
166
169
|
|
|
167
170
|
/**
|
|
@@ -276,6 +279,7 @@ export class K8sService extends Context.Service<
|
|
|
276
279
|
const k8sConfig = yield* requireK8sConfig(profile);
|
|
277
280
|
const timeoutMs = k8sConfig.timeoutMs ?? 60000;
|
|
278
281
|
const apiProbeTimeoutMs = k8sConfig.apiProbeTimeoutMs ?? 2000;
|
|
282
|
+
|
|
279
283
|
const { context, kubeconfig } = yield* resolveContext(profile, k8sConfig);
|
|
280
284
|
const reachableWithoutPrerequisites = yield* probeApiReachable(
|
|
281
285
|
context,
|
|
@@ -314,15 +318,16 @@ export class K8sService extends Context.Service<
|
|
|
314
318
|
}),
|
|
315
319
|
).pipe(
|
|
316
320
|
Effect.timeoutOption(timeoutMs),
|
|
317
|
-
Effect.mapError(
|
|
318
|
-
(platformError)
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}),
|
|
325
|
-
|
|
321
|
+
Effect.mapError((platformError) => {
|
|
322
|
+
const missing = missingBinaryFromSpawnFailure("kubectl", String(platformError));
|
|
323
|
+
return new K8sCommandError({
|
|
324
|
+
message: `Command execution failed: ${String(platformError)}`,
|
|
325
|
+
command: fullCommand,
|
|
326
|
+
exitCode: -1,
|
|
327
|
+
stderr: String(platformError),
|
|
328
|
+
...(missing ? { hint: missing.hint } : {}),
|
|
329
|
+
});
|
|
330
|
+
}),
|
|
326
331
|
);
|
|
327
332
|
|
|
328
333
|
if (Option.isNone(resultOption)) {
|
|
@@ -473,7 +478,7 @@ export class K8sService extends Context.Service<
|
|
|
473
478
|
`Remote realpath exited with code ${realpathResult.exitCode}`,
|
|
474
479
|
command: realpathResult.command,
|
|
475
480
|
exitCode: realpathResult.exitCode,
|
|
476
|
-
stderr: realpathResult.stderr
|
|
481
|
+
...(realpathResult.stderr ? { stderr: realpathResult.stderr } : {}),
|
|
477
482
|
});
|
|
478
483
|
}
|
|
479
484
|
const [canonicalBase, canonicalPath] = realpathResult.stdout.trim().split("\n");
|