@blogic-cz/agent-tools 1.12.0 → 1.13.1
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 +1 -0
- package/dist/gh-tool/pr/checkbox-markdown.d.ts +22 -0
- package/dist/gh-tool/pr/checkbox-markdown.d.ts.map +1 -0
- package/dist/gh-tool/pr/checkbox.d.ts +21 -0
- package/dist/gh-tool/pr/checkbox.d.ts.map +1 -0
- package/dist/gh-tool/pr/commands.d.ts +13 -4
- package/dist/gh-tool/pr/commands.d.ts.map +1 -1
- package/dist/gh-tool/pr/core.d.ts.map +1 -1
- package/dist/gh-tool/pr/index.d.ts +1 -1
- package/dist/gh-tool/pr/index.d.ts.map +1 -1
- package/dist/gh-tool/read/porcelain.d.ts +4 -2
- package/dist/gh-tool/read/porcelain.d.ts.map +1 -1
- package/dist/gh-tool/read/pulls.d.ts +1 -0
- package/dist/gh-tool/read/pulls.d.ts.map +1 -1
- package/dist/gh-tool/read/reads.d.ts +3 -0
- package/dist/gh-tool/read/reads.d.ts.map +1 -1
- package/dist/gh-tool/read/transport.d.ts +9 -0
- package/dist/gh-tool/read/transport.d.ts.map +1 -1
- package/dist/gh-tool/read/writes.d.ts +2 -1
- package/dist/gh-tool/read/writes.d.ts.map +1 -1
- package/dist/gh-tool/types.d.ts +2 -1
- package/dist/gh-tool/types.d.ts.map +1 -1
- package/dist/gh-tool/workflow.d.ts +2 -1
- package/dist/gh-tool/workflow.d.ts.map +1 -1
- package/dist/shared/content-security.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/gh-tool/main.ts +2 -0
- package/src/gh-tool/pr/checkbox-markdown.ts +317 -0
- package/src/gh-tool/pr/checkbox.ts +113 -0
- package/src/gh-tool/pr/commands.ts +66 -1
- package/src/gh-tool/pr/core.ts +61 -28
- package/src/gh-tool/pr/index.ts +1 -0
- package/src/gh-tool/read/porcelain.ts +2 -1
- package/src/gh-tool/read/pulls.ts +19 -0
- package/src/gh-tool/read/reads.ts +2 -1
- package/src/gh-tool/read/transport.ts +85 -1
- package/src/gh-tool/read/writes.ts +19 -2
- package/src/gh-tool/types.ts +9 -1
- package/src/gh-tool/workflow.ts +63 -11
- package/src/shared/content-security.ts +1 -0
|
@@ -29,6 +29,7 @@ export const WorkflowRun = Schema.Struct({
|
|
|
29
29
|
event: Schema.String,
|
|
30
30
|
url: Schema.String,
|
|
31
31
|
workflowName: Schema.String,
|
|
32
|
+
workflowDatabaseId: Schema.optionalKey(Schema.NullOr(Schema.Number)),
|
|
32
33
|
});
|
|
33
34
|
export const WorkflowRunDetail = Schema.Struct({
|
|
34
35
|
...WorkflowRun.fields,
|
|
@@ -39,7 +40,7 @@ export type WorkflowJob = typeof WorkflowJob.Type;
|
|
|
39
40
|
export type WorkflowRun = typeof WorkflowRun.Type;
|
|
40
41
|
export type WorkflowRunDetail = typeof WorkflowRunDetail.Type;
|
|
41
42
|
export const RUN_LIST_FIELDS =
|
|
42
|
-
"databaseId,displayTitle,status,conclusion,headBranch,headSha,createdAt,event,url,workflowName";
|
|
43
|
+
"databaseId,displayTitle,status,conclusion,headBranch,headSha,createdAt,event,url,workflowName,workflowDatabaseId";
|
|
43
44
|
export const RUN_VIEW_FIELDS = `${RUN_LIST_FIELDS},jobs,attempt`;
|
|
44
45
|
|
|
45
46
|
export const parsePreserving =
|
|
@@ -101,6 +101,25 @@ export const prHead = Effect.fn("reads.prHead")(function* (
|
|
|
101
101
|
return { headSha: head.head.sha, baseSha: head.base.sha, signature: snapshotDigest(snapshot) };
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
+
const PRBody = Schema.Struct({ body: Schema.NullOr(Schema.String) });
|
|
105
|
+
|
|
106
|
+
export const prBody = Effect.fn("reads.prBody")(function* (repo: RepoRef, pr: number) {
|
|
107
|
+
const gh = yield* GitHubService;
|
|
108
|
+
const path = `repos/${repoSlug(repo)}/pulls/${pr}`;
|
|
109
|
+
const response = yield* gh.apiRequest<unknown>({ path });
|
|
110
|
+
const parsed = yield* Effect.try({
|
|
111
|
+
try: () => Schema.decodeUnknownSync(PRBody)(response.body),
|
|
112
|
+
catch: (error) =>
|
|
113
|
+
new GitHubCommandError({
|
|
114
|
+
command: `GET ${path}`,
|
|
115
|
+
exitCode: 0,
|
|
116
|
+
message: `GitHub returned an invalid PR body: ${String(error)}`,
|
|
117
|
+
stderr: `GitHub returned an invalid PR body: ${String(error)}`,
|
|
118
|
+
}),
|
|
119
|
+
});
|
|
120
|
+
return parsed.body ?? "";
|
|
121
|
+
});
|
|
122
|
+
|
|
104
123
|
export const checkSignal = Effect.fn("reads.checkSignal")(function* (repo: RepoRef, sha: string) {
|
|
105
124
|
const snapshot = yield* restSnapshot({ op: "checkSignal", repo, sha }, [
|
|
106
125
|
{
|
|
@@ -6,7 +6,7 @@ import { cachedRead, ReadCache } from "./cache";
|
|
|
6
6
|
import { OPS } from "./classes";
|
|
7
7
|
import { repoSlug } from "./keys";
|
|
8
8
|
import { issueView, issueList, issueComments } from "./issues";
|
|
9
|
-
import { prView, prList, prChecks, prHead, checkSignal } from "./pulls";
|
|
9
|
+
import { prView, prList, prChecks, prHead, prBody, checkSignal } from "./pulls";
|
|
10
10
|
import { prComments, prReviews, reviewThreads, lastHumanReviewer } from "./reviews";
|
|
11
11
|
import { reviewGuards } from "./review-guards";
|
|
12
12
|
import type { RepoRef, RunFilter } from "./keys";
|
|
@@ -376,6 +376,7 @@ export const reads = {
|
|
|
376
376
|
prList,
|
|
377
377
|
prChecks,
|
|
378
378
|
prHead,
|
|
379
|
+
prBody,
|
|
379
380
|
checkSignal,
|
|
380
381
|
runList,
|
|
381
382
|
runView,
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { ghSubprocessEvent, recordRequest } from "./telemetry";
|
|
2
2
|
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
3
|
-
import { Context, Effect, Layer, Stream } from "effect";
|
|
3
|
+
import { Context, Effect, Layer, Schema, Stream } from "effect";
|
|
4
4
|
|
|
5
5
|
import type { GitHubRepoConfig } from "#config";
|
|
6
6
|
import type { RepoInfo } from "#gh/types";
|
|
7
7
|
import type { RepoRef } from "./keys";
|
|
8
8
|
|
|
9
|
+
import {
|
|
10
|
+
bunMarkdownOutline,
|
|
11
|
+
findCheckboxLines,
|
|
12
|
+
validateCheckboxEdit,
|
|
13
|
+
} from "#gh/pr/checkbox-markdown";
|
|
9
14
|
import { GH_BINARY } from "#gh/config";
|
|
10
15
|
import { ghEnvironment } from "#gh/environment";
|
|
11
16
|
import { GitHubAuthError, GitHubCommandError, GitHubNotFoundError } from "#gh/errors";
|
|
@@ -80,11 +85,20 @@ export type GhResult = {
|
|
|
80
85
|
};
|
|
81
86
|
|
|
82
87
|
export type GhError = GitHubCommandError | GitHubAuthError | GitHubNotFoundError;
|
|
88
|
+
export type PRCheckboxWrite = {
|
|
89
|
+
readonly repo: RepoRef;
|
|
90
|
+
readonly pr: number;
|
|
91
|
+
readonly expectedBody: string;
|
|
92
|
+
readonly boxOffset: number;
|
|
93
|
+
readonly replacement: " " | "x";
|
|
94
|
+
readonly selectionText: readonly string[];
|
|
95
|
+
};
|
|
83
96
|
|
|
84
97
|
export class GitHubService extends Context.Service<
|
|
85
98
|
GitHubService,
|
|
86
99
|
{
|
|
87
100
|
readonly runGh: (args: string[]) => Effect.Effect<GhResult, GhError>;
|
|
101
|
+
readonly writePRCheckbox?: (opts: PRCheckboxWrite) => Effect.Effect<GhResult, GhError>;
|
|
88
102
|
readonly runGhJson: <T>(args: string[]) => Effect.Effect<T, GhError>;
|
|
89
103
|
readonly runGraphQL: (
|
|
90
104
|
query: string,
|
|
@@ -325,6 +339,75 @@ export class GitHubService extends Context.Service<
|
|
|
325
339
|
});
|
|
326
340
|
};
|
|
327
341
|
|
|
342
|
+
const writePRCheckbox = Effect.fn("GitHubService.writePRCheckbox")(function* (
|
|
343
|
+
opts: PRCheckboxWrite,
|
|
344
|
+
) {
|
|
345
|
+
const path = `repos/${opts.repo.owner}/${opts.repo.name}/pulls/${opts.pr}`;
|
|
346
|
+
const response = yield* githubApi<unknown>({ path });
|
|
347
|
+
const parsed = yield* Effect.try({
|
|
348
|
+
try: () =>
|
|
349
|
+
Schema.decodeUnknownSync(Schema.Struct({ body: Schema.NullOr(Schema.String) }))(
|
|
350
|
+
response.body,
|
|
351
|
+
),
|
|
352
|
+
catch: (error) =>
|
|
353
|
+
new GitHubCommandError({
|
|
354
|
+
command: `GET ${path}`,
|
|
355
|
+
exitCode: 0,
|
|
356
|
+
message: `GitHub returned an invalid PR body: ${String(error)}`,
|
|
357
|
+
stderr: `GitHub returned an invalid PR body: ${String(error)}`,
|
|
358
|
+
}),
|
|
359
|
+
});
|
|
360
|
+
const body = parsed.body ?? "";
|
|
361
|
+
if (body !== opts.expectedBody) {
|
|
362
|
+
return yield* new GitHubCommandError({
|
|
363
|
+
command: "gh-tool pr checkbox",
|
|
364
|
+
exitCode: 1,
|
|
365
|
+
stderr: `PR #${opts.pr} body changed before the checkbox update`,
|
|
366
|
+
message: `Refusing to update PR #${opts.pr}: its body changed after the first read`,
|
|
367
|
+
hint: "Re-read the pull request and retry with the current task text.",
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const checkboxes = findCheckboxLines(
|
|
372
|
+
body,
|
|
373
|
+
opts.selectionText[0] ?? "",
|
|
374
|
+
null,
|
|
375
|
+
bunMarkdownOutline,
|
|
376
|
+
);
|
|
377
|
+
if (
|
|
378
|
+
!checkboxes.some((checkbox) => checkbox.offset === opts.boxOffset) ||
|
|
379
|
+
body[opts.boxOffset] === opts.replacement
|
|
380
|
+
) {
|
|
381
|
+
return yield* new GitHubCommandError({
|
|
382
|
+
command: "gh-tool pr checkbox",
|
|
383
|
+
exitCode: 1,
|
|
384
|
+
stderr: "Refusing an invalid PR checkbox position",
|
|
385
|
+
message: "Refusing to update a character that is not a task checkbox",
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
yield* Effect.try({
|
|
390
|
+
try: () => validateCheckboxEdit(body, opts.boxOffset, opts.replacement),
|
|
391
|
+
catch: () =>
|
|
392
|
+
new GitHubCommandError({
|
|
393
|
+
command: "gh-tool pr checkbox",
|
|
394
|
+
exitCode: 1,
|
|
395
|
+
stderr: "PR body structure is ambiguous; tick this item in the GitHub UI",
|
|
396
|
+
message: "Refusing to update the PR checkbox because its source is ambiguous",
|
|
397
|
+
hint: "PR body structure is ambiguous; tick this item in the GitHub UI.",
|
|
398
|
+
}),
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const updatedBody =
|
|
402
|
+
body.slice(0, opts.boxOffset) + opts.replacement + body.slice(opts.boxOffset + 1);
|
|
403
|
+
const args = ["api", "--method", "PATCH", path, "-f", `body=${updatedBody}`];
|
|
404
|
+
const outboundText = [...args];
|
|
405
|
+
outboundText[5] = "body=[GitHub PR body]";
|
|
406
|
+
outboundText.push(...opts.selectionText);
|
|
407
|
+
yield* validateOutboundText(outboundText.join("\n"), "gh-tool pr checkbox");
|
|
408
|
+
return yield* runGhAttempt(args);
|
|
409
|
+
});
|
|
410
|
+
|
|
328
411
|
const runGhJson = <T>(args: string[]) =>
|
|
329
412
|
Effect.gen(function* () {
|
|
330
413
|
const result = yield* runGh(args);
|
|
@@ -415,6 +498,7 @@ export class GitHubService extends Context.Service<
|
|
|
415
498
|
|
|
416
499
|
return {
|
|
417
500
|
runGh,
|
|
501
|
+
writePRCheckbox,
|
|
418
502
|
runGhJson,
|
|
419
503
|
runGraphQL,
|
|
420
504
|
apiRequest: githubApi,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
|
+
import { GitHubCommandError } from "#gh/errors";
|
|
2
3
|
import { GitHubService } from "#gh/read/transport";
|
|
3
4
|
import { invalidate } from "./cache";
|
|
4
5
|
import type { Tag } from "./keys";
|
|
5
|
-
import type { GhError } from "#gh/read/transport";
|
|
6
|
+
import type { GhError, PRCheckboxWrite } from "#gh/read/transport";
|
|
6
7
|
import type { GitHubApiRequest } from "#gh/read/api";
|
|
7
8
|
|
|
8
9
|
const mutate = <A, R>(effect: Effect.Effect<A, GhError, R>, touched: readonly Tag[]) =>
|
|
@@ -15,6 +16,22 @@ const run = Effect.fn("writes.run")(function* (argv: string[], touched: readonly
|
|
|
15
16
|
const gh = yield* GitHubService;
|
|
16
17
|
return yield* mutate(gh.runGh(argv), touched);
|
|
17
18
|
});
|
|
19
|
+
const prCheckbox = Effect.fn("writes.prCheckbox")(function* (
|
|
20
|
+
opts: PRCheckboxWrite,
|
|
21
|
+
touched: readonly Tag[],
|
|
22
|
+
) {
|
|
23
|
+
const gh = yield* GitHubService;
|
|
24
|
+
if (gh.writePRCheckbox === undefined) {
|
|
25
|
+
return yield* new GitHubCommandError({
|
|
26
|
+
command: "gh-tool pr checkbox",
|
|
27
|
+
exitCode: 1,
|
|
28
|
+
stderr: "PR checkbox writes are unavailable",
|
|
29
|
+
message:
|
|
30
|
+
"Refusing to update the PR checkbox because the GitHub service cannot verify the body",
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return yield* mutate(gh.writePRCheckbox(opts), touched);
|
|
34
|
+
});
|
|
18
35
|
const json = <A>(argv: string[], touched: readonly Tag[]) =>
|
|
19
36
|
Effect.gen(function* () {
|
|
20
37
|
const gh = yield* GitHubService;
|
|
@@ -34,4 +51,4 @@ const api = <A>(request: GitHubApiRequest, touched: readonly Tag[]) =>
|
|
|
34
51
|
const gh = yield* GitHubService;
|
|
35
52
|
return yield* mutate(gh.apiRequest<A>(request), touched);
|
|
36
53
|
});
|
|
37
|
-
export const writes = { run, json, graphql, api };
|
|
54
|
+
export const writes = { run, prCheckbox, json, graphql, api };
|
package/src/gh-tool/types.ts
CHANGED
|
@@ -192,6 +192,7 @@ export type RerunCheckAttempt = {
|
|
|
192
192
|
attempt?: number | null;
|
|
193
193
|
jobs: readonly RerunCheckJob[];
|
|
194
194
|
status?: string;
|
|
195
|
+
conclusion?: string | null;
|
|
195
196
|
};
|
|
196
197
|
|
|
197
198
|
export type RerunChecksRun = {
|
|
@@ -210,7 +211,14 @@ export type RerunChecksReport = {
|
|
|
210
211
|
rerun: number;
|
|
211
212
|
failed?: number;
|
|
212
213
|
message: string;
|
|
213
|
-
status?:
|
|
214
|
+
status?:
|
|
215
|
+
| "evidence_unavailable"
|
|
216
|
+
| "escalation_required"
|
|
217
|
+
| "failed"
|
|
218
|
+
| "rerun_started"
|
|
219
|
+
| "completed"
|
|
220
|
+
| "watch_timeout"
|
|
221
|
+
| "discovery_timeout";
|
|
214
222
|
runs?: RerunChecksRun[];
|
|
215
223
|
};
|
|
216
224
|
|
package/src/gh-tool/workflow.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { Effect, Option } from "effect";
|
|
|
3
3
|
|
|
4
4
|
import { formatOption, logFormatted, logText } from "#shared";
|
|
5
5
|
import { CI_CHECK_WATCH_TIMEOUT_MS } from "#gh/config";
|
|
6
|
-
import { GitHubCommandError, GitHubNotFoundError } from "./errors";
|
|
6
|
+
import { GitHubCommandError, GitHubNotFoundError, GitHubTimeoutError } from "./errors";
|
|
7
7
|
import { GitHubService } from "./read/transport";
|
|
8
8
|
import { reads, resolveRepo, waits, writes } from "./read";
|
|
9
9
|
import { rawJobLog } from "./read/reads";
|
|
@@ -57,13 +57,54 @@ const listRuns = Effect.fn("workflow.listRuns")(function* (opts: {
|
|
|
57
57
|
event: null,
|
|
58
58
|
limit: opts.limit,
|
|
59
59
|
});
|
|
60
|
-
return runs.map(({ headSha: _headSha, ...run }) =>
|
|
60
|
+
return runs.map(({ headSha: _headSha, workflowDatabaseId, ...run }) => {
|
|
61
|
+
if (run.conclusion !== "cancelled") return run;
|
|
62
|
+
const newer = runs
|
|
63
|
+
.filter(
|
|
64
|
+
(candidate) =>
|
|
65
|
+
workflowDatabaseId !== undefined &&
|
|
66
|
+
workflowDatabaseId !== null &&
|
|
67
|
+
workflowDatabaseId > 0 &&
|
|
68
|
+
candidate.workflowDatabaseId === workflowDatabaseId &&
|
|
69
|
+
run.headBranch.length > 0 &&
|
|
70
|
+
candidate.headBranch === run.headBranch &&
|
|
71
|
+
candidate.event === run.event &&
|
|
72
|
+
candidate.databaseId !== run.databaseId &&
|
|
73
|
+
Date.parse(candidate.createdAt) > Date.parse(run.createdAt),
|
|
74
|
+
)
|
|
75
|
+
.toSorted(
|
|
76
|
+
(left, right) =>
|
|
77
|
+
Date.parse(right.createdAt) - Date.parse(left.createdAt) ||
|
|
78
|
+
right.databaseId - left.databaseId,
|
|
79
|
+
)[0];
|
|
80
|
+
return Object.assign(
|
|
81
|
+
run,
|
|
82
|
+
{ cancellationReason: "unknown" },
|
|
83
|
+
newer === undefined
|
|
84
|
+
? {}
|
|
85
|
+
: {
|
|
86
|
+
newerRun: {
|
|
87
|
+
databaseId: newer.databaseId,
|
|
88
|
+
status: newer.status,
|
|
89
|
+
conclusion: newer.conclusion,
|
|
90
|
+
url: newer.url,
|
|
91
|
+
context:
|
|
92
|
+
"Newer run in this list with the same workflow, branch and event. The cancellation reason is unknown.",
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
});
|
|
61
97
|
});
|
|
62
98
|
|
|
63
99
|
const viewRun = Effect.fn("workflow.viewRun")(function* (runId: number, repo: string | null) {
|
|
64
100
|
const target = yield* resolveRepo(repo);
|
|
65
101
|
const detail = yield* reads.runView(target, runId);
|
|
66
|
-
const {
|
|
102
|
+
const {
|
|
103
|
+
attempt: _attempt,
|
|
104
|
+
headSha: _headSha,
|
|
105
|
+
workflowDatabaseId: _workflowDatabaseId,
|
|
106
|
+
...snapshot
|
|
107
|
+
} = detail;
|
|
67
108
|
return snapshot;
|
|
68
109
|
});
|
|
69
110
|
|
|
@@ -276,13 +317,16 @@ const watchRun = Effect.fn("workflow.watchRun")(function* (
|
|
|
276
317
|
);
|
|
277
318
|
const finalState = outcome.kind === "done" ? outcome.value : outcome.last;
|
|
278
319
|
|
|
279
|
-
return
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
320
|
+
return {
|
|
321
|
+
kind: outcome.kind,
|
|
322
|
+
result: buildWatchResult(
|
|
323
|
+
runId,
|
|
324
|
+
finalState,
|
|
325
|
+
outcome.kind === "timedOut" ? null : progress.join("\n"),
|
|
326
|
+
frames,
|
|
327
|
+
timeoutSeconds,
|
|
328
|
+
),
|
|
329
|
+
};
|
|
286
330
|
});
|
|
287
331
|
|
|
288
332
|
const fetchAnnotations = Effect.fn("workflow.fetchAnnotations")(function* (opts: {
|
|
@@ -864,8 +908,16 @@ export const workflowWatchCommand = Command.make(
|
|
|
864
908
|
({ format, frames, repo, run, timeout }) =>
|
|
865
909
|
Effect.gen(function* () {
|
|
866
910
|
const resolvedRepo = yield* resolveRepoArg(repo);
|
|
867
|
-
const result = yield* watchRun(run, resolvedRepo, timeout, frames);
|
|
911
|
+
const { kind, result } = yield* watchRun(run, resolvedRepo, timeout, frames);
|
|
868
912
|
yield* logFormatted(result, format);
|
|
913
|
+
if (kind === "timedOut") {
|
|
914
|
+
return yield* new GitHubTimeoutError({
|
|
915
|
+
message: `Workflow run ${run} timed out after ${timeout}s with status ${result.status}`,
|
|
916
|
+
timeoutMs: timeout * 1000,
|
|
917
|
+
hint: "The snapshot shows an unfinished run. Run workflow watch again to keep watching.",
|
|
918
|
+
nextCommand: `agent-tools-gh workflow watch --run ${run}${resolvedRepo === null ? "" : ` --repo ${resolvedRepo}`}`,
|
|
919
|
+
});
|
|
920
|
+
}
|
|
869
921
|
}),
|
|
870
922
|
).pipe(Command.withDescription("Watch a workflow run until it completes, then show final status"));
|
|
871
923
|
|