@blogic-cz/agent-tools 1.5.7 → 1.5.9
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 +21 -8
- package/dist/az-tool/service.d.ts.map +1 -1
- package/dist/azdo-tool/service.d.ts.map +1 -1
- package/dist/credential-guard/index.d.ts +2 -0
- package/dist/credential-guard/index.d.ts.map +1 -1
- package/dist/db-tool/service.d.ts.map +1 -1
- package/dist/gh-tool/gist.d.ts +13 -0
- package/dist/gh-tool/gist.d.ts.map +1 -1
- package/dist/gh-tool/pr/core.d.ts.map +1 -1
- package/dist/gh-tool/pr/review.d.ts.map +1 -1
- package/dist/gh-tool/release.d.ts +46 -1
- package/dist/gh-tool/release.d.ts.map +1 -1
- package/dist/gh-tool/text-input.d.ts +5 -3
- package/dist/gh-tool/text-input.d.ts.map +1 -1
- package/dist/k8s-tool/service.d.ts.map +1 -1
- package/dist/logs-tool/main.d.ts +1 -2
- package/dist/logs-tool/main.d.ts.map +1 -1
- package/dist/observability-tool/shared.d.ts.map +1 -1
- package/dist/session-tool/main.d.ts +1 -2
- package/dist/session-tool/main.d.ts.map +1 -1
- package/dist/shared/audit.d.ts.map +1 -1
- package/dist/shared/content-security.d.ts +6 -0
- package/dist/shared/content-security.d.ts.map +1 -0
- package/dist/shared/credential-patterns.d.ts +11 -0
- package/dist/shared/credential-patterns.d.ts.map +1 -0
- package/dist/shared/effect-version.d.ts.map +1 -1
- package/dist/shared/error-renderer.d.ts.map +1 -1
- package/dist/shared/format.d.ts.map +1 -1
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/output-boundary.d.ts +3 -0
- package/dist/shared/output-boundary.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/audit-tool/main.ts +3 -1
- package/src/az-tool/main.ts +3 -1
- package/src/az-tool/service.ts +9 -0
- package/src/azdo-tool/main.ts +3 -1
- package/src/azdo-tool/service.ts +17 -0
- package/src/credential-guard/claude-hook.ts +2 -1
- package/src/credential-guard/index.ts +936 -87
- package/src/db-tool/main.ts +3 -1
- package/src/db-tool/service.ts +25 -2
- package/src/gh-tool/gist.ts +63 -64
- package/src/gh-tool/main.ts +3 -1
- package/src/gh-tool/pr/core.ts +10 -3
- package/src/gh-tool/pr/review.ts +199 -12
- package/src/gh-tool/release.ts +104 -29
- package/src/gh-tool/text-input.ts +19 -38
- package/src/k8s-tool/main.ts +3 -1
- package/src/k8s-tool/service.ts +25 -0
- package/src/logs-tool/main.ts +7 -2
- package/src/observability-tool/main.ts +5 -7
- package/src/observability-tool/shared.ts +41 -1
- package/src/session-tool/main.ts +18 -3
- package/src/shared/audit.ts +11 -10
- package/src/shared/content-security.ts +91 -0
- package/src/shared/credential-patterns.ts +62 -0
- package/src/shared/effect-version.ts +2 -1
- package/src/shared/error-renderer.ts +15 -2
- package/src/shared/format.ts +33 -6
- package/src/shared/index.ts +5 -0
- package/src/shared/output-boundary.ts +29 -0
package/src/db-tool/main.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
VERSION,
|
|
15
15
|
} from "#shared";
|
|
16
16
|
import { AuditServiceLayer, withAudit } from "#shared/audit";
|
|
17
|
+
import { withRedactedOutput } from "#shared/output-boundary";
|
|
17
18
|
import { ConfigService, ConfigServiceLayer, getDefaultEnvironment } from "#config";
|
|
18
19
|
import { DbConfigService, makeDbConfigLayer } from "./config-service";
|
|
19
20
|
import { DbConnectionError } from "./errors";
|
|
@@ -238,6 +239,7 @@ const mainCommand = Command.make("db-tool", {}).pipe(
|
|
|
238
239
|
|
|
239
240
|
const cli = Command.run(mainCommand, {
|
|
240
241
|
version: VERSION,
|
|
242
|
+
renderErrors: false,
|
|
241
243
|
});
|
|
242
244
|
|
|
243
245
|
const dbConfigLayer = makeDbConfigLayer(profileArg);
|
|
@@ -252,7 +254,7 @@ const MainLayer = DbService.layer.pipe(
|
|
|
252
254
|
Layer.provideMerge(AuditServiceLayer),
|
|
253
255
|
);
|
|
254
256
|
|
|
255
|
-
const program = withAudit("db", cli).pipe(
|
|
257
|
+
const program = withRedactedOutput(withAudit("db", cli)).pipe(
|
|
256
258
|
Effect.provide(MainLayer),
|
|
257
259
|
Effect.tapCause(renderCauseToStderr),
|
|
258
260
|
);
|
package/src/db-tool/service.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { DbConfig, DbMutationOperation, QueryResult, SchemaMode } from "./t
|
|
|
6
6
|
import { ConfigService } from "#config";
|
|
7
7
|
import { isPrerequisiteRunError } from "#shared/prerequisites/errors";
|
|
8
8
|
import { resolveEnvTemplate } from "#shared/env-template";
|
|
9
|
+
import { unsafeOutboundTextReason } from "#shared/content-security";
|
|
9
10
|
import { resolveEnvironmentScopedPrerequisites } from "#shared/prerequisites/config";
|
|
10
11
|
import { runWithProfilePrerequisites } from "#shared/prerequisites/runtime";
|
|
11
12
|
import { buildApiProbeArgs } from "#shared/k8s-probe";
|
|
@@ -501,8 +502,18 @@ export class DbService extends Context.Service<
|
|
|
501
502
|
readOnly: isFullyReadOnly(config),
|
|
502
503
|
});
|
|
503
504
|
|
|
504
|
-
const
|
|
505
|
-
|
|
505
|
+
const unsafeSqlError = (reason: string) =>
|
|
506
|
+
new DbQueryError({
|
|
507
|
+
message: `Refusing to send SQL containing ${reason}.`,
|
|
508
|
+
sql: "[redacted]",
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
const runSql = (config: DbConfig, password: string, sql: string) => {
|
|
512
|
+
const unsafeReason = unsafeOutboundTextReason(sql);
|
|
513
|
+
return unsafeReason === null
|
|
514
|
+
? sqlClient.run(toConnection(config, password), sql)
|
|
515
|
+
: Effect.fail(unsafeSqlError(unsafeReason));
|
|
516
|
+
};
|
|
506
517
|
|
|
507
518
|
const fetchSingleColumn = (config: DbConfig, password: string, sql: string) =>
|
|
508
519
|
runSql(config, password, sql).pipe(
|
|
@@ -772,6 +783,11 @@ export class DbService extends Context.Service<
|
|
|
772
783
|
sql: string,
|
|
773
784
|
limit?: number,
|
|
774
785
|
) {
|
|
786
|
+
const unsafeReason = unsafeOutboundTextReason(sql);
|
|
787
|
+
if (unsafeReason !== null) {
|
|
788
|
+
return yield* unsafeSqlError(unsafeReason);
|
|
789
|
+
}
|
|
790
|
+
|
|
775
791
|
const config = yield* getConfigForEnv(env);
|
|
776
792
|
const startTimeMs = yield* Clock.currentTimeMillis;
|
|
777
793
|
const resolvedConfig = yield* resolveDbConfig(config, env);
|
|
@@ -827,6 +843,13 @@ export class DbService extends Context.Service<
|
|
|
827
843
|
mode: SchemaMode,
|
|
828
844
|
table?: string,
|
|
829
845
|
) {
|
|
846
|
+
if (table !== undefined) {
|
|
847
|
+
const unsafeReason = unsafeOutboundTextReason(table);
|
|
848
|
+
if (unsafeReason !== null) {
|
|
849
|
+
return yield* unsafeSqlError(unsafeReason);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
830
853
|
const config = yield* getConfigForEnv(env);
|
|
831
854
|
const startTimeMs = yield* Clock.currentTimeMillis;
|
|
832
855
|
const resolvedConfig = yield* resolveDbConfig(config, env);
|
package/src/gh-tool/gist.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { Command, Flag } from "effect/unstable/cli";
|
|
2
2
|
import { Effect, Option } from "effect";
|
|
3
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
4
|
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
4
6
|
|
|
5
7
|
import { formatOption, logFormatted } from "#shared";
|
|
6
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
isSensitivePath,
|
|
10
|
+
readValidatedOutboundFile,
|
|
11
|
+
resolveOptionalTextInput,
|
|
12
|
+
} from "#gh/text-input";
|
|
7
13
|
import { GitHubCommandError } from "./errors";
|
|
8
14
|
import { GitHubService } from "./service";
|
|
9
15
|
|
|
@@ -118,29 +124,13 @@ const validateFilePath = (path: string, command: string) =>
|
|
|
118
124
|
// `gh gist create/edit` read content from files only; inline --body is staged in a temp file
|
|
119
125
|
// whose basename becomes the gist filename.
|
|
120
126
|
const stageBody = Effect.fn("gist.stageBody")(function* (opts: {
|
|
121
|
-
body: string;
|
|
127
|
+
body: string | Uint8Array;
|
|
122
128
|
filename: string;
|
|
123
129
|
command: string;
|
|
124
130
|
}) {
|
|
125
131
|
const directory = yield* Effect.acquireRelease(
|
|
126
132
|
Effect.tryPromise({
|
|
127
|
-
try:
|
|
128
|
-
const proc = Bun.spawn(
|
|
129
|
-
["mktemp", "-d", join(process.env.TMPDIR ?? "/tmp", "gh-tool-gist-XXXXXX")],
|
|
130
|
-
{ stdout: "pipe", stderr: "pipe" },
|
|
131
|
-
);
|
|
132
|
-
const [exitCode, stdout, stderr] = await Promise.all([
|
|
133
|
-
proc.exited,
|
|
134
|
-
new Response(proc.stdout).text(),
|
|
135
|
-
new Response(proc.stderr).text(),
|
|
136
|
-
]);
|
|
137
|
-
|
|
138
|
-
if (exitCode !== 0) {
|
|
139
|
-
throw new Error(stderr.trim() || `mktemp exited with code ${exitCode}`);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return stdout.trim();
|
|
143
|
-
},
|
|
133
|
+
try: () => mkdtemp(join(process.env.TMPDIR ?? tmpdir(), "gh-tool-gist-")),
|
|
144
134
|
catch: (error) =>
|
|
145
135
|
inputError(
|
|
146
136
|
`Failed to stage gist content: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -149,20 +139,14 @@ const stageBody = Effect.fn("gist.stageBody")(function* (opts: {
|
|
|
149
139
|
}),
|
|
150
140
|
(tempDirectory) =>
|
|
151
141
|
Effect.tryPromise({
|
|
152
|
-
try:
|
|
153
|
-
const proc = Bun.spawn(["rm", "-rf", tempDirectory], {
|
|
154
|
-
stdout: "ignore",
|
|
155
|
-
stderr: "ignore",
|
|
156
|
-
});
|
|
157
|
-
await proc.exited;
|
|
158
|
-
},
|
|
142
|
+
try: () => rm(tempDirectory, { recursive: true, force: true }),
|
|
159
143
|
catch: () => undefined,
|
|
160
144
|
}).pipe(Effect.ignore),
|
|
161
145
|
);
|
|
162
146
|
const path = join(directory, opts.filename);
|
|
163
147
|
|
|
164
148
|
yield* Effect.tryPromise({
|
|
165
|
-
try: () =>
|
|
149
|
+
try: () => writeFile(path, opts.body),
|
|
166
150
|
catch: (error) =>
|
|
167
151
|
inputError(
|
|
168
152
|
`Failed to stage gist content: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -293,45 +277,55 @@ export const createGist = Effect.fn("gist.createGist")(function* (opts: {
|
|
|
293
277
|
public: boolean;
|
|
294
278
|
}) {
|
|
295
279
|
const gh = yield* GitHubService;
|
|
280
|
+
return yield* Effect.scoped(
|
|
281
|
+
Effect.gen(function* () {
|
|
282
|
+
const snapshots: string[] = [];
|
|
283
|
+
for (const path of opts.paths) {
|
|
284
|
+
const validated = yield* readValidatedOutboundFile(path, "gh-tool gist create");
|
|
285
|
+
const filename = path.split(/[\\/]/).at(-1) ?? path;
|
|
286
|
+
snapshots.push(
|
|
287
|
+
yield* stageBody({ body: validated.bytes, filename, command: "gh-tool gist create" }),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
296
290
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const args = ["gist", "create", ...opts.paths];
|
|
291
|
+
const args = ["gist", "create", ...snapshots];
|
|
300
292
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
293
|
+
if (opts.description !== null) {
|
|
294
|
+
args.push("--desc", opts.description);
|
|
295
|
+
}
|
|
304
296
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
297
|
+
if (opts.public) {
|
|
298
|
+
args.push("--public");
|
|
299
|
+
}
|
|
308
300
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
301
|
+
const result = yield* gh.runGh(args);
|
|
302
|
+
const url =
|
|
303
|
+
result.stdout
|
|
304
|
+
.trim()
|
|
305
|
+
.split("\n")
|
|
306
|
+
.map((line) => line.trim())
|
|
307
|
+
.findLast((line) => line.startsWith("https://")) ?? "";
|
|
308
|
+
|
|
309
|
+
if (url === "") {
|
|
310
|
+
return yield* Effect.fail(
|
|
311
|
+
inputError("gh gist create did not return a gist URL", "gh-tool gist create"),
|
|
312
|
+
);
|
|
313
|
+
}
|
|
322
314
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
315
|
+
const created: GistCreateResult = {
|
|
316
|
+
created: true,
|
|
317
|
+
id: url.split("/").at(-1) ?? "",
|
|
318
|
+
url,
|
|
319
|
+
public: opts.public,
|
|
320
|
+
files: opts.paths.map((path) => path.split(/[\\/]/).at(-1) ?? path),
|
|
321
|
+
};
|
|
330
322
|
|
|
331
|
-
|
|
323
|
+
return created;
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
332
326
|
});
|
|
333
327
|
|
|
334
|
-
const editGist = Effect.fn("gist.editGist")(function* (opts: {
|
|
328
|
+
export const editGist = Effect.fn("gist.editGist")(function* (opts: {
|
|
335
329
|
id: string;
|
|
336
330
|
description: string | null;
|
|
337
331
|
add: string | null;
|
|
@@ -341,15 +335,13 @@ const editGist = Effect.fn("gist.editGist")(function* (opts: {
|
|
|
341
335
|
}) {
|
|
342
336
|
const gh = yield* GitHubService;
|
|
343
337
|
|
|
344
|
-
for (const path of [opts.sourcePath, opts.add]) {
|
|
345
|
-
if (path !== null) yield* validateOutboundFile(path, "gh-tool gist edit");
|
|
346
|
-
}
|
|
347
|
-
|
|
348
338
|
const args = ["gist", "edit", opts.id];
|
|
349
339
|
const changes: string[] = [];
|
|
350
340
|
|
|
351
341
|
if (opts.sourcePath !== null) {
|
|
352
|
-
|
|
342
|
+
const validated = yield* readValidatedOutboundFile(opts.sourcePath, "gh-tool gist edit");
|
|
343
|
+
const filename = opts.filename ?? opts.sourcePath.split(/[\\/]/).at(-1) ?? "";
|
|
344
|
+
args.push(yield* stageBody({ body: validated.bytes, filename, command: "gh-tool gist edit" }));
|
|
353
345
|
changes.push(`content:${opts.filename ?? opts.sourcePath.split("/").at(-1)}`);
|
|
354
346
|
}
|
|
355
347
|
|
|
@@ -359,7 +351,14 @@ const editGist = Effect.fn("gist.editGist")(function* (opts: {
|
|
|
359
351
|
}
|
|
360
352
|
|
|
361
353
|
if (opts.add !== null) {
|
|
362
|
-
|
|
354
|
+
const validated = yield* readValidatedOutboundFile(opts.add, "gh-tool gist edit");
|
|
355
|
+
const filename = opts.add.split(/[\\/]/).at(-1) ?? opts.add;
|
|
356
|
+
const snapshot = yield* stageBody({
|
|
357
|
+
body: validated.bytes,
|
|
358
|
+
filename,
|
|
359
|
+
command: "gh-tool gist edit",
|
|
360
|
+
});
|
|
361
|
+
args.push("--add", snapshot);
|
|
363
362
|
changes.push(`add:${opts.add.split("/").at(-1)}`);
|
|
364
363
|
}
|
|
365
364
|
|
package/src/gh-tool/main.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { Effect, Layer } from "effect";
|
|
|
5
5
|
|
|
6
6
|
import { makeSchemaCommand, renderCauseToStderr, VERSION } from "#shared";
|
|
7
7
|
import { AuditServiceLayer, withAudit } from "#shared/audit";
|
|
8
|
+
import { withRedactedOutput } from "#shared/output-boundary";
|
|
8
9
|
import { ConfigServiceLayer } from "#config";
|
|
9
10
|
import {
|
|
10
11
|
issueCloseCommand,
|
|
@@ -240,6 +241,7 @@ WORKFLOW FOR AI AGENTS:
|
|
|
240
241
|
|
|
241
242
|
const cli = Command.run(mainCommand, {
|
|
242
243
|
version: VERSION,
|
|
244
|
+
renderErrors: false,
|
|
243
245
|
});
|
|
244
246
|
|
|
245
247
|
const MainLayer = GitHubService.layer.pipe(
|
|
@@ -248,7 +250,7 @@ const MainLayer = GitHubService.layer.pipe(
|
|
|
248
250
|
Layer.provideMerge(ConfigServiceLayer),
|
|
249
251
|
);
|
|
250
252
|
|
|
251
|
-
const program = withAudit("gh", cli).pipe(
|
|
253
|
+
const program = withRedactedOutput(withAudit("gh", cli)).pipe(
|
|
252
254
|
Effect.provide(MainLayer),
|
|
253
255
|
Effect.tapCause(renderCauseToStderr),
|
|
254
256
|
);
|
package/src/gh-tool/pr/core.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { GitHubCommandError, GitHubMergeError } from "#gh/errors";
|
|
|
21
21
|
import { GitHubService } from "#gh/service";
|
|
22
22
|
import type { GhResult } from "#gh/service";
|
|
23
23
|
import { logText } from "#shared";
|
|
24
|
+
import { redactSensitiveText } from "#shared/content-security";
|
|
24
25
|
|
|
25
26
|
import type { ButStatusJson, PRViewJsonResult } from "./helpers";
|
|
26
27
|
import { pollUntilResolved } from "#shared/poll-until-resolved";
|
|
@@ -114,14 +115,18 @@ const resolveMergeSha = Effect.fn("pr.resolveMergeSha")(function* (
|
|
|
114
115
|
Effect.map((sha) => ({ sha, failed: false as const })),
|
|
115
116
|
Effect.catch((error) =>
|
|
116
117
|
Console.error(
|
|
117
|
-
|
|
118
|
+
redactSensitiveText(
|
|
119
|
+
`Warning: PR #${pr} was merged, but its merge commit SHA could not be read: ${error.message}`,
|
|
120
|
+
),
|
|
118
121
|
).pipe(Effect.as({ sha: null, failed: true as const })),
|
|
119
122
|
),
|
|
120
123
|
);
|
|
121
124
|
|
|
122
125
|
if (result.sha === null && !result.failed) {
|
|
123
126
|
yield* Console.error(
|
|
124
|
-
|
|
127
|
+
redactSensitiveText(
|
|
128
|
+
`Warning: PR #${pr} was merged, but its merge commit SHA is still unknown after ${MERGE_SHA_WAIT_SECONDS}s.`,
|
|
129
|
+
),
|
|
125
130
|
);
|
|
126
131
|
}
|
|
127
132
|
|
|
@@ -1088,7 +1093,9 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
|
|
|
1088
1093
|
if (error._tag !== "GitHubCommandError") {
|
|
1089
1094
|
return rollbackNote === ""
|
|
1090
1095
|
? Effect.fail(error)
|
|
1091
|
-
: Console.error(rollbackNote.trim()).pipe(
|
|
1096
|
+
: Console.error(redactSensitiveText(rollbackNote.trim())).pipe(
|
|
1097
|
+
Effect.andThen(Effect.fail(error)),
|
|
1098
|
+
);
|
|
1092
1099
|
}
|
|
1093
1100
|
|
|
1094
1101
|
const stderr = error.stderr.toLowerCase();
|
package/src/gh-tool/pr/review.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { REVIEW_EVENTS } from "#gh/config";
|
|
|
15
15
|
|
|
16
16
|
import { GitHubCommandError } from "#gh/errors";
|
|
17
17
|
import { GitHubService } from "#gh/service";
|
|
18
|
+
import { validateOutboundText } from "#gh/text-input";
|
|
18
19
|
|
|
19
20
|
import { viewPR } from "./core";
|
|
20
21
|
|
|
@@ -83,16 +84,39 @@ const RESOLVE_THREAD_MUTATION = `
|
|
|
83
84
|
`;
|
|
84
85
|
|
|
85
86
|
const PENDING_REVIEWS_QUERY = `
|
|
86
|
-
query($owner: String!, $name: String!, $pr: Int
|
|
87
|
+
query($owner: String!, $name: String!, $pr: Int!, $after: String) {
|
|
87
88
|
viewer { login }
|
|
88
89
|
repository(owner: $owner, name: $name) {
|
|
89
90
|
pullRequest(number: $pr) {
|
|
90
|
-
reviews(
|
|
91
|
+
reviews(first: 100, after: $after, states: [PENDING]) {
|
|
91
92
|
nodes {
|
|
92
93
|
id
|
|
93
94
|
state
|
|
94
95
|
author { login }
|
|
95
96
|
}
|
|
97
|
+
pageInfo { hasNextPage endCursor }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
`;
|
|
103
|
+
|
|
104
|
+
const PENDING_REVIEW_CONTENT_QUERY = `
|
|
105
|
+
query($reviewId: ID!, $after: String) {
|
|
106
|
+
viewer { login }
|
|
107
|
+
node(id: $reviewId) {
|
|
108
|
+
... on PullRequestReview {
|
|
109
|
+
id
|
|
110
|
+
state
|
|
111
|
+
body
|
|
112
|
+
author { login }
|
|
113
|
+
pullRequest {
|
|
114
|
+
number
|
|
115
|
+
repository { nameWithOwner }
|
|
116
|
+
}
|
|
117
|
+
comments(first: 100, after: $after) {
|
|
118
|
+
nodes { body }
|
|
119
|
+
pageInfo { hasNextPage endCursor }
|
|
96
120
|
}
|
|
97
121
|
}
|
|
98
122
|
}
|
|
@@ -188,11 +212,27 @@ type PendingReviewsQueryResult = {
|
|
|
188
212
|
state: string;
|
|
189
213
|
author: { login: string };
|
|
190
214
|
}>;
|
|
215
|
+
pageInfo: { hasNextPage: boolean; endCursor: string | null };
|
|
191
216
|
};
|
|
192
217
|
};
|
|
193
218
|
};
|
|
194
219
|
};
|
|
195
220
|
|
|
221
|
+
type PendingReviewContentQueryResult = {
|
|
222
|
+
viewer: { login: string };
|
|
223
|
+
node: {
|
|
224
|
+
id: string;
|
|
225
|
+
state: string;
|
|
226
|
+
body: string | null;
|
|
227
|
+
author: { login: string };
|
|
228
|
+
pullRequest: { number: number; repository: { nameWithOwner: string } };
|
|
229
|
+
comments: {
|
|
230
|
+
nodes: Array<{ body: string }>;
|
|
231
|
+
pageInfo: { hasNextPage: boolean; endCursor: string | null };
|
|
232
|
+
};
|
|
233
|
+
} | null;
|
|
234
|
+
};
|
|
235
|
+
|
|
196
236
|
type SubmitReviewResult = {
|
|
197
237
|
submitPullRequestReview: {
|
|
198
238
|
pullRequestReview: { id: string; state: string };
|
|
@@ -1080,18 +1120,76 @@ export const submitPendingReview = Effect.fn("pr.submitPendingReview")(function*
|
|
|
1080
1120
|
let targetReviewId = reviewId;
|
|
1081
1121
|
|
|
1082
1122
|
if (targetReviewId === null) {
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1123
|
+
let after: string | null = null;
|
|
1124
|
+
let hasNextPage = true;
|
|
1125
|
+
let viewerLogin: string | null = null;
|
|
1126
|
+
let ownPendingReviewId: string | null = null;
|
|
1127
|
+
const seenPendingCursors = new Set<string>();
|
|
1128
|
+
|
|
1129
|
+
while (hasNextPage) {
|
|
1130
|
+
const pending = (yield* service.runGraphQL(PENDING_REVIEWS_QUERY, {
|
|
1131
|
+
owner: repoInfo.owner,
|
|
1132
|
+
name: repoInfo.name,
|
|
1133
|
+
pr: resolvedPr,
|
|
1134
|
+
after,
|
|
1135
|
+
})) as PendingReviewsQueryResult;
|
|
1136
|
+
|
|
1137
|
+
if (
|
|
1138
|
+
typeof pending.viewer?.login !== "string" ||
|
|
1139
|
+
!Array.isArray(pending.repository?.pullRequest?.reviews?.nodes) ||
|
|
1140
|
+
typeof pending.repository.pullRequest.reviews?.pageInfo?.hasNextPage !== "boolean" ||
|
|
1141
|
+
(pending.repository.pullRequest.reviews.pageInfo.endCursor !== null &&
|
|
1142
|
+
typeof pending.repository.pullRequest.reviews.pageInfo.endCursor !== "string") ||
|
|
1143
|
+
pending.repository.pullRequest.reviews.nodes.some(
|
|
1144
|
+
(review) =>
|
|
1145
|
+
typeof review?.id !== "string" ||
|
|
1146
|
+
typeof review.state !== "string" ||
|
|
1147
|
+
typeof review.author?.login !== "string",
|
|
1148
|
+
)
|
|
1149
|
+
) {
|
|
1150
|
+
return yield* Effect.fail(
|
|
1151
|
+
new GitHubCommandError({
|
|
1152
|
+
command: "gh-tool pr submit-review",
|
|
1153
|
+
exitCode: 1,
|
|
1154
|
+
message: "Could not verify pending reviews; refusing to submit",
|
|
1155
|
+
stderr: "Pending review lookup returned incomplete data",
|
|
1156
|
+
}),
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1088
1159
|
|
|
1089
|
-
|
|
1090
|
-
|
|
1160
|
+
viewerLogin ??= pending.viewer.login;
|
|
1161
|
+
const own = pending.repository.pullRequest.reviews.nodes.find(
|
|
1162
|
+
(review) => review.author?.login === viewerLogin && review.state === "PENDING",
|
|
1163
|
+
);
|
|
1164
|
+
ownPendingReviewId ??= own?.id ?? null;
|
|
1091
1165
|
|
|
1092
|
-
|
|
1166
|
+
const pageInfo = pending.repository.pullRequest.reviews.pageInfo;
|
|
1167
|
+
hasNextPage = pageInfo.hasNextPage;
|
|
1168
|
+
after = pageInfo.endCursor;
|
|
1169
|
+
if (hasNextPage && (after === null || after.length === 0)) {
|
|
1170
|
+
return yield* Effect.fail(
|
|
1171
|
+
new GitHubCommandError({
|
|
1172
|
+
command: "gh-tool pr submit-review",
|
|
1173
|
+
exitCode: 1,
|
|
1174
|
+
message: "Could not verify all pending reviews; refusing to submit",
|
|
1175
|
+
stderr: "Pending review lookup returned an incomplete page cursor",
|
|
1176
|
+
}),
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
if (hasNextPage && after !== null && seenPendingCursors.has(after)) {
|
|
1180
|
+
return yield* Effect.fail(
|
|
1181
|
+
new GitHubCommandError({
|
|
1182
|
+
command: "gh-tool pr submit-review",
|
|
1183
|
+
exitCode: 1,
|
|
1184
|
+
message: "Could not verify all pending reviews; refusing to submit",
|
|
1185
|
+
stderr: "Pending review lookup repeated a page cursor",
|
|
1186
|
+
}),
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
if (hasNextPage && after !== null) seenPendingCursors.add(after);
|
|
1190
|
+
}
|
|
1093
1191
|
|
|
1094
|
-
if (
|
|
1192
|
+
if (ownPendingReviewId === null) {
|
|
1095
1193
|
return yield* Effect.fail(
|
|
1096
1194
|
new GitHubCommandError({
|
|
1097
1195
|
command: "gh-tool pr submit-review",
|
|
@@ -1102,7 +1200,96 @@ export const submitPendingReview = Effect.fn("pr.submitPendingReview")(function*
|
|
|
1102
1200
|
);
|
|
1103
1201
|
}
|
|
1104
1202
|
|
|
1105
|
-
targetReviewId =
|
|
1203
|
+
targetReviewId = ownPendingReviewId;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
if (targetReviewId === null) {
|
|
1207
|
+
return yield* Effect.fail(
|
|
1208
|
+
new GitHubCommandError({
|
|
1209
|
+
command: "gh-tool pr submit-review",
|
|
1210
|
+
exitCode: 1,
|
|
1211
|
+
message: "Could not identify pending review; refusing to submit",
|
|
1212
|
+
stderr: "Pending review lookup returned no review ID",
|
|
1213
|
+
}),
|
|
1214
|
+
);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
let afterComment: string | null = null;
|
|
1218
|
+
let commentsHaveNextPage = true;
|
|
1219
|
+
const seenCommentCursors = new Set<string>();
|
|
1220
|
+
while (commentsHaveNextPage) {
|
|
1221
|
+
const pendingReview = (yield* service.runGraphQL(PENDING_REVIEW_CONTENT_QUERY, {
|
|
1222
|
+
reviewId: targetReviewId,
|
|
1223
|
+
after: afterComment,
|
|
1224
|
+
})) as PendingReviewContentQueryResult;
|
|
1225
|
+
|
|
1226
|
+
const review = pendingReview?.node;
|
|
1227
|
+
if (
|
|
1228
|
+
typeof pendingReview?.viewer?.login !== "string" ||
|
|
1229
|
+
review === null ||
|
|
1230
|
+
review === undefined ||
|
|
1231
|
+
review.id !== targetReviewId ||
|
|
1232
|
+
review.state !== "PENDING" ||
|
|
1233
|
+
review.author?.login !== pendingReview.viewer.login ||
|
|
1234
|
+
review.pullRequest?.number !== resolvedPr ||
|
|
1235
|
+
typeof review.pullRequest.repository?.nameWithOwner !== "string" ||
|
|
1236
|
+
review.pullRequest.repository.nameWithOwner.toLowerCase() !==
|
|
1237
|
+
`${repoInfo.owner}/${repoInfo.name}`.toLowerCase() ||
|
|
1238
|
+
typeof review.body !== "string" ||
|
|
1239
|
+
!Array.isArray(review.comments?.nodes) ||
|
|
1240
|
+
typeof review.comments.pageInfo?.hasNextPage !== "boolean" ||
|
|
1241
|
+
(review.comments.pageInfo.endCursor !== null &&
|
|
1242
|
+
typeof review.comments.pageInfo.endCursor !== "string")
|
|
1243
|
+
) {
|
|
1244
|
+
return yield* Effect.fail(
|
|
1245
|
+
new GitHubCommandError({
|
|
1246
|
+
command: "gh-tool pr submit-review",
|
|
1247
|
+
exitCode: 1,
|
|
1248
|
+
message: "Could not verify pending review contents; refusing to submit",
|
|
1249
|
+
stderr: "Pending review lookup returned incomplete or non-owned review data",
|
|
1250
|
+
}),
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
yield* validateOutboundText(review.body, "gh-tool pr submit-review");
|
|
1255
|
+
for (const comment of review.comments.nodes) {
|
|
1256
|
+
if (typeof comment?.body !== "string") {
|
|
1257
|
+
return yield* Effect.fail(
|
|
1258
|
+
new GitHubCommandError({
|
|
1259
|
+
command: "gh-tool pr submit-review",
|
|
1260
|
+
exitCode: 1,
|
|
1261
|
+
message: "Could not verify pending review comments; refusing to submit",
|
|
1262
|
+
stderr: "Pending review lookup returned a comment without a body",
|
|
1263
|
+
}),
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
yield* validateOutboundText(comment.body, "gh-tool pr submit-review");
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
const pageInfo = review.comments.pageInfo;
|
|
1270
|
+
commentsHaveNextPage = pageInfo.hasNextPage;
|
|
1271
|
+
afterComment = pageInfo.endCursor;
|
|
1272
|
+
if (commentsHaveNextPage && (afterComment === null || afterComment.length === 0)) {
|
|
1273
|
+
return yield* Effect.fail(
|
|
1274
|
+
new GitHubCommandError({
|
|
1275
|
+
command: "gh-tool pr submit-review",
|
|
1276
|
+
exitCode: 1,
|
|
1277
|
+
message: "Could not verify all pending review comments; refusing to submit",
|
|
1278
|
+
stderr: "Pending review lookup returned an incomplete comment cursor",
|
|
1279
|
+
}),
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
if (commentsHaveNextPage && afterComment !== null && seenCommentCursors.has(afterComment)) {
|
|
1283
|
+
return yield* Effect.fail(
|
|
1284
|
+
new GitHubCommandError({
|
|
1285
|
+
command: "gh-tool pr submit-review",
|
|
1286
|
+
exitCode: 1,
|
|
1287
|
+
message: "Could not verify all pending review comments; refusing to submit",
|
|
1288
|
+
stderr: "Pending review lookup repeated a comment cursor",
|
|
1289
|
+
}),
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
if (commentsHaveNextPage && afterComment !== null) seenCommentCursors.add(afterComment);
|
|
1106
1293
|
}
|
|
1107
1294
|
|
|
1108
1295
|
const result = (yield* service.runGraphQL(SUBMIT_REVIEW_MUTATION, {
|