@blogic-cz/agent-tools 1.7.5 → 1.8.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.
Files changed (128) hide show
  1. package/README.md +2 -1
  2. package/dist/db-tool/service.d.ts.map +1 -1
  3. package/dist/gh-tool/branch.d.ts +2 -3
  4. package/dist/gh-tool/branch.d.ts.map +1 -1
  5. package/dist/gh-tool/environment.d.ts +1 -1
  6. package/dist/gh-tool/environment.d.ts.map +1 -1
  7. package/dist/gh-tool/gist.d.ts +8 -28
  8. package/dist/gh-tool/gist.d.ts.map +1 -1
  9. package/dist/gh-tool/issue/commands.d.ts +9 -9
  10. package/dist/gh-tool/issue/commands.d.ts.map +1 -1
  11. package/dist/gh-tool/issue/core.d.ts +103 -9
  12. package/dist/gh-tool/issue/core.d.ts.map +1 -1
  13. package/dist/gh-tool/issue/triage.d.ts +6 -6
  14. package/dist/gh-tool/issue/triage.d.ts.map +1 -1
  15. package/dist/gh-tool/pr/commands.d.ts +53 -46
  16. package/dist/gh-tool/pr/commands.d.ts.map +1 -1
  17. package/dist/gh-tool/pr/core.d.ts +156 -86
  18. package/dist/gh-tool/pr/core.d.ts.map +1 -1
  19. package/dist/gh-tool/pr/review.d.ts +16 -16
  20. package/dist/gh-tool/pr/review.d.ts.map +1 -1
  21. package/dist/gh-tool/pr/snapshot.d.ts +9 -0
  22. package/dist/gh-tool/pr/snapshot.d.ts.map +1 -0
  23. package/dist/gh-tool/pr/stack-read.d.ts +1 -6
  24. package/dist/gh-tool/pr/stack-read.d.ts.map +1 -1
  25. package/dist/gh-tool/pr/stack.d.ts +3 -3
  26. package/dist/gh-tool/pr/stack.d.ts.map +1 -1
  27. package/dist/gh-tool/{api.d.ts → read/api.d.ts} +9 -3
  28. package/dist/gh-tool/read/api.d.ts.map +1 -0
  29. package/dist/gh-tool/read/cache.d.ts +44 -0
  30. package/dist/gh-tool/read/cache.d.ts.map +1 -0
  31. package/dist/gh-tool/read/catalog.d.ts +170 -0
  32. package/dist/gh-tool/read/catalog.d.ts.map +1 -0
  33. package/dist/gh-tool/read/classes.d.ts +94 -0
  34. package/dist/gh-tool/read/classes.d.ts.map +1 -0
  35. package/dist/gh-tool/read/commands.d.ts +3 -0
  36. package/dist/gh-tool/read/commands.d.ts.map +1 -0
  37. package/dist/gh-tool/read/gated.d.ts +14 -0
  38. package/dist/gh-tool/read/gated.d.ts.map +1 -0
  39. package/dist/gh-tool/read/index.d.ts +5 -0
  40. package/dist/gh-tool/read/index.d.ts.map +1 -0
  41. package/dist/gh-tool/read/issues.d.ts +68 -0
  42. package/dist/gh-tool/read/issues.d.ts.map +1 -0
  43. package/dist/gh-tool/read/keys.d.ts +61 -0
  44. package/dist/gh-tool/read/keys.d.ts.map +1 -0
  45. package/dist/gh-tool/read/porcelain.d.ts +68 -0
  46. package/dist/gh-tool/read/porcelain.d.ts.map +1 -0
  47. package/dist/gh-tool/read/pulls.d.ts +137 -0
  48. package/dist/gh-tool/read/pulls.d.ts.map +1 -0
  49. package/dist/gh-tool/read/reads.d.ts +209 -0
  50. package/dist/gh-tool/read/reads.d.ts.map +1 -0
  51. package/dist/gh-tool/read/rest.d.ts +49 -0
  52. package/dist/gh-tool/read/rest.d.ts.map +1 -0
  53. package/dist/gh-tool/read/review-guards.d.ts +20 -0
  54. package/dist/gh-tool/read/review-guards.d.ts.map +1 -0
  55. package/dist/gh-tool/read/review-model.d.ts +190 -0
  56. package/dist/gh-tool/read/review-model.d.ts.map +1 -0
  57. package/dist/gh-tool/read/reviews.d.ts +9 -0
  58. package/dist/gh-tool/read/reviews.d.ts.map +1 -0
  59. package/dist/gh-tool/read/stacks.d.ts +7 -0
  60. package/dist/gh-tool/read/stacks.d.ts.map +1 -0
  61. package/dist/gh-tool/read/store.d.ts +70 -0
  62. package/dist/gh-tool/read/store.d.ts.map +1 -0
  63. package/dist/gh-tool/{service.d.ts → read/transport.d.ts} +5 -3
  64. package/dist/gh-tool/read/transport.d.ts.map +1 -0
  65. package/dist/gh-tool/read/waits.d.ts +22 -0
  66. package/dist/gh-tool/read/waits.d.ts.map +1 -0
  67. package/dist/gh-tool/read/writes.d.ts +16 -0
  68. package/dist/gh-tool/read/writes.d.ts.map +1 -0
  69. package/dist/gh-tool/release.d.ts +10 -26
  70. package/dist/gh-tool/release.d.ts.map +1 -1
  71. package/dist/gh-tool/repo.d.ts +4 -4
  72. package/dist/gh-tool/repo.d.ts.map +1 -1
  73. package/dist/gh-tool/types.d.ts +1 -1
  74. package/dist/gh-tool/types.d.ts.map +1 -1
  75. package/dist/gh-tool/workflow.d.ts +43 -21
  76. package/dist/gh-tool/workflow.d.ts.map +1 -1
  77. package/dist/observability-tool/shared.d.ts.map +1 -1
  78. package/dist/shared/content-security.d.ts +2 -0
  79. package/dist/shared/content-security.d.ts.map +1 -1
  80. package/package.json +6 -6
  81. package/src/api-tool/proxy-entry.ts +3 -1
  82. package/src/db-tool/service.ts +4 -1
  83. package/src/gh-tool/branch.ts +5 -8
  84. package/src/gh-tool/environment.ts +2 -1
  85. package/src/gh-tool/gist.ts +8 -35
  86. package/src/gh-tool/issue/commands.ts +1 -1
  87. package/src/gh-tool/issue/core.ts +55 -122
  88. package/src/gh-tool/issue/triage.ts +4 -26
  89. package/src/gh-tool/main.ts +5 -2
  90. package/src/gh-tool/pr/commands.ts +17 -15
  91. package/src/gh-tool/pr/core.ts +486 -582
  92. package/src/gh-tool/pr/review.ts +106 -454
  93. package/src/gh-tool/pr/snapshot.ts +23 -0
  94. package/src/gh-tool/pr/stack-read.ts +1 -73
  95. package/src/gh-tool/pr/stack.ts +37 -36
  96. package/src/gh-tool/{api.ts → read/api.ts} +40 -7
  97. package/src/gh-tool/read/cache.ts +192 -0
  98. package/src/gh-tool/read/catalog.ts +310 -0
  99. package/src/gh-tool/read/classes.ts +30 -0
  100. package/src/gh-tool/read/commands.ts +25 -0
  101. package/src/gh-tool/read/gated.ts +56 -0
  102. package/src/gh-tool/read/index.ts +4 -0
  103. package/src/gh-tool/read/issues.ts +107 -0
  104. package/src/gh-tool/read/keys.ts +102 -0
  105. package/src/gh-tool/read/porcelain.ts +58 -0
  106. package/src/gh-tool/read/pulls.ts +266 -0
  107. package/src/gh-tool/read/reads.ts +306 -0
  108. package/src/gh-tool/read/rest.ts +111 -0
  109. package/src/gh-tool/read/review-guards.ts +61 -0
  110. package/src/gh-tool/read/review-model.ts +286 -0
  111. package/src/gh-tool/read/reviews.ts +114 -0
  112. package/src/gh-tool/read/stacks.ts +73 -0
  113. package/src/gh-tool/read/store.ts +504 -0
  114. package/src/gh-tool/{service.ts → read/transport.ts} +26 -6
  115. package/src/gh-tool/read/waits.ts +52 -0
  116. package/src/gh-tool/read/writes.ts +37 -0
  117. package/src/gh-tool/release.ts +15 -168
  118. package/src/gh-tool/repo.ts +5 -78
  119. package/src/gh-tool/types.ts +1 -1
  120. package/src/gh-tool/workflow.ts +123 -214
  121. package/src/observability-tool/shared.ts +2 -1
  122. package/src/shared/content-security.ts +10 -1
  123. package/src/shared/credential-patterns.ts +1 -1
  124. package/dist/gh-tool/api.d.ts.map +0 -1
  125. package/dist/gh-tool/pr/check-status-cache.d.ts +0 -6
  126. package/dist/gh-tool/pr/check-status-cache.d.ts.map +0 -1
  127. package/dist/gh-tool/service.d.ts.map +0 -1
  128. package/src/gh-tool/pr/check-status-cache.ts +0 -142
@@ -0,0 +1,266 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { GitHubService } from "#gh/read/transport";
3
+ import { GitHubCommandError } from "#gh/errors";
4
+ import type { CheckResult } from "#gh/types";
5
+ import { cachedRead, readFence, ReadCache, seedRead } from "./cache";
6
+ import { resolveTarget } from "#gh/pr/snapshot";
7
+ import { gatedRead } from "./gated";
8
+ import { restSnapshot, snapshotDigest } from "./rest";
9
+ import { OPS } from "./classes";
10
+ import { repoSlug } from "./keys";
11
+ import type { RepoRef } from "./keys";
12
+ import { parsePreserving } from "./porcelain";
13
+
14
+ export const PR_VIEW_FIELDS =
15
+ "number,url,title,headRefName,baseRefName,headRefOid,state,isDraft,mergeable,body,author,reviewDecision,reviewRequests,createdAt,headRepositoryOwner,mergeCommit";
16
+ export const PR_LIST_FIELDS =
17
+ "number,url,title,headRefName,baseRefName,state,isDraft,mergeable,author,createdAt,reviewDecision,headRepositoryOwner";
18
+ export type PrListFilter = {
19
+ state: string;
20
+ limit: number;
21
+ author: string | null;
22
+ base: string | null;
23
+ head: string | null;
24
+ search: string | null;
25
+ };
26
+ export type PrSelector = { by: "number"; number: number } | { by: "currentBranch" };
27
+ const Pull = Schema.Struct({
28
+ number: Schema.Number,
29
+ url: Schema.String,
30
+ title: Schema.String,
31
+ headRefName: Schema.String,
32
+ baseRefName: Schema.String,
33
+ state: Schema.String,
34
+ isDraft: Schema.Boolean,
35
+ mergeable: Schema.Literals(["MERGEABLE", "CONFLICTING", "UNKNOWN"]),
36
+ author: Schema.optional(
37
+ Schema.Struct({ login: Schema.String, is_bot: Schema.optional(Schema.Boolean) }),
38
+ ),
39
+ createdAt: Schema.optional(Schema.String),
40
+ reviewDecision: Schema.optional(Schema.String),
41
+ headRepositoryOwner: Schema.optional(Schema.Struct({ login: Schema.String })),
42
+ });
43
+
44
+ const PullView = Schema.Struct({
45
+ ...Pull.fields,
46
+ headRefOid: Schema.optional(Schema.String),
47
+ headSha: Schema.optional(Schema.NullOr(Schema.String)),
48
+ baseSha: Schema.optional(Schema.NullOr(Schema.String)),
49
+ body: Schema.String,
50
+ author: Schema.Struct({ login: Schema.String, is_bot: Schema.Boolean }),
51
+ reviewDecision: Schema.Literals(["APPROVED", "CHANGES_REQUESTED", "REVIEW_REQUIRED", ""]),
52
+ reviewRequests: Schema.mutable(
53
+ Schema.Array(
54
+ Schema.Union([
55
+ Schema.Struct({ __typename: Schema.Literal("User"), login: Schema.String }),
56
+ Schema.Struct({
57
+ __typename: Schema.Literal("Team"),
58
+ name: Schema.String,
59
+ slug: Schema.String,
60
+ }),
61
+ ]),
62
+ ),
63
+ ),
64
+ mergeCommit: Schema.optional(Schema.NullOr(Schema.Struct({ oid: Schema.String }))),
65
+ });
66
+ export type PrView = typeof PullView.Type;
67
+ export type PrListItem = typeof Pull.Type;
68
+ const Check = Schema.Struct({
69
+ name: Schema.String,
70
+ state: Schema.String,
71
+ bucket: Schema.String,
72
+ link: Schema.String,
73
+ });
74
+
75
+ export const prHead = Effect.fn("reads.prHead")(function* (
76
+ repo: RepoRef,
77
+ pr: number,
78
+ consistency: "cached" | "origin" = "cached",
79
+ ) {
80
+ const snapshot = yield* restSnapshot(
81
+ { op: "prHead", repo, pr },
82
+ [{ kind: "single", path: `repos/${repoSlug(repo)}/pulls/${pr}` }],
83
+ consistency,
84
+ );
85
+ const head = yield* Effect.try({
86
+ try: () =>
87
+ Schema.decodeUnknownSync(
88
+ Schema.Struct({
89
+ head: Schema.Struct({ sha: Schema.String }),
90
+ base: Schema.Struct({ sha: Schema.String }),
91
+ }),
92
+ )(snapshot.pages[0]?.body),
93
+ catch: (error) =>
94
+ new GitHubCommandError({
95
+ command: "pr head",
96
+ exitCode: 0,
97
+ message: String(error),
98
+ stderr: String(error),
99
+ }),
100
+ });
101
+ return { headSha: head.head.sha, baseSha: head.base.sha, signature: snapshotDigest(snapshot) };
102
+ });
103
+
104
+ export const checkSignal = Effect.fn("reads.checkSignal")(function* (repo: RepoRef, sha: string) {
105
+ const snapshot = yield* restSnapshot({ op: "checkSignal", repo, sha }, [
106
+ {
107
+ kind: "pages",
108
+ path: `repos/${repoSlug(repo)}/commits/${sha}/check-runs`,
109
+ items: (body) =>
110
+ Schema.decodeUnknownSync(Schema.Struct({ check_runs: Schema.Array(Schema.Unknown) }))(body)
111
+ .check_runs,
112
+ },
113
+ {
114
+ kind: "pages",
115
+ path: `repos/${repoSlug(repo)}/commits/${sha}/status`,
116
+ items: (body) =>
117
+ Schema.decodeUnknownSync(Schema.Struct({ statuses: Schema.Array(Schema.Unknown) }))(body)
118
+ .statuses,
119
+ },
120
+ ]);
121
+ return snapshotDigest(snapshot);
122
+ });
123
+
124
+ export const prView = Effect.fn("reads.prView")(function* (
125
+ repo: RepoRef | null,
126
+ selector: PrSelector,
127
+ consistency: "cached" | "origin" = "cached",
128
+ ) {
129
+ const gh = yield* GitHubService;
130
+ const args = [
131
+ "pr",
132
+ "view",
133
+ ...(selector.by === "number" ? [String(selector.number)] : []),
134
+ ...(repo === null ? [] : ["--repo", repoSlug(repo)]),
135
+ "--json",
136
+ PR_VIEW_FIELDS,
137
+ ];
138
+ const fetch = () =>
139
+ gh.runGhJson<PrView>(args).pipe(Effect.map((value) => ({ kind: "fresh" as const, value })));
140
+ if (selector.by === "currentBranch" || repo === null) {
141
+ const fence = yield* readFence;
142
+ const data = (yield* fetch()).value;
143
+ if (repo !== null && consistency !== "origin" && fence !== null) {
144
+ const head = yield* prHead(repo, data.number).pipe(Effect.orElseSucceed(() => null));
145
+ if (head !== null && head.headSha === data.headRefOid)
146
+ yield* seedRead(
147
+ { op: "prView", repo, pr: data.number },
148
+ { data, originFetchedAt: Date.now(), signal: head.signature },
149
+ fence,
150
+ );
151
+ }
152
+ return data;
153
+ }
154
+ return yield* gatedRead({
155
+ key: { op: "prView", repo, pr: selector.number },
156
+ consistency,
157
+ decode: parsePreserving(PullView),
158
+ fetch: Effect.suspend(() => gh.runGhJson<PrView>(args)),
159
+ signal: prHead(repo, selector.number).pipe(Effect.map((head) => head.signature)),
160
+ maxAgeMs: () => OPS.prView.maxAgeMs,
161
+ });
162
+ });
163
+
164
+ export const prList = Effect.fn("reads.prList")(function* (
165
+ repo: RepoRef,
166
+ filter: PrListFilter,
167
+ consistency: "cached" | "origin" = "cached",
168
+ ) {
169
+ const gh = yield* GitHubService;
170
+ const args = [
171
+ "pr",
172
+ "list",
173
+ "--state",
174
+ filter.state,
175
+ "--limit",
176
+ String(filter.limit),
177
+ "--repo",
178
+ repoSlug(repo),
179
+ "--json",
180
+ PR_LIST_FIELDS,
181
+ ];
182
+ for (const [flag, value] of [
183
+ ["--author", filter.author],
184
+ ["--base", filter.base],
185
+ ["--head", filter.head],
186
+ ["--search", filter.search],
187
+ ] as const)
188
+ if (value !== null) args.push(flag, value);
189
+ return yield* cachedRead({
190
+ key: { op: "prList", repo, filter },
191
+ policy: OPS.prList,
192
+ consistency,
193
+ decode: parsePreserving(Schema.mutable(Schema.Array(Pull))),
194
+ fetch: () =>
195
+ gh
196
+ .runGhJson<PrListItem[]>(args)
197
+ .pipe(Effect.map((value) => ({ kind: "fresh" as const, value }))),
198
+ });
199
+ });
200
+
201
+ export const prChecks = Effect.fn("reads.prChecks")(function* (
202
+ repo: RepoRef,
203
+ pr: number | null,
204
+ headSha: string | null,
205
+ ) {
206
+ const gh = yield* GitHubService;
207
+ const fetch = () =>
208
+ gh
209
+ .runGhJson<CheckResult[]>([
210
+ "pr",
211
+ "checks",
212
+ ...(pr === null ? [] : [String(pr)]),
213
+ "--repo",
214
+ repoSlug(repo),
215
+ "--json",
216
+ "name,state,bucket,link",
217
+ ])
218
+ .pipe(Effect.map((value) => ({ kind: "fresh" as const, value })));
219
+ const session = yield* ReadCache;
220
+ if (session === null || session.store === null) return (yield* fetch()).value;
221
+ const number = yield* resolveTarget(pr, prView(repo, { by: "currentBranch" }));
222
+ const head =
223
+ headSha === null
224
+ ? yield* prHead(repo, number, "origin").pipe(Effect.orElseSucceed(() => null))
225
+ : { headSha };
226
+ if (head === null) return (yield* fetch()).value;
227
+ const sha = head.headSha;
228
+ let verified = true;
229
+ const stableFetch = Effect.gen(function* () {
230
+ const before = yield* prHead(repo, number, "origin").pipe(Effect.orElseSucceed(() => null));
231
+ if (before !== null && before.headSha !== sha) return yield* movedHead(number);
232
+ const value = yield* gh.runGhJson<CheckResult[]>([
233
+ "pr",
234
+ "checks",
235
+ String(number),
236
+ "--repo",
237
+ repoSlug(repo),
238
+ "--json",
239
+ "name,state,bucket,link",
240
+ ]);
241
+ const after = yield* prHead(repo, number, "origin").pipe(Effect.orElseSucceed(() => null));
242
+ if (after !== null && after.headSha !== sha) return yield* movedHead(number);
243
+ verified = before !== null && after !== null;
244
+ return value;
245
+ });
246
+ return yield* gatedRead({
247
+ key: { op: "prChecks", repo, pr: number, headSha: sha },
248
+ decode: parsePreserving(Schema.mutable(Schema.Array(Check))),
249
+ fetch: stableFetch,
250
+ cacheableWhen: () => verified,
251
+ signal: checkSignal(repo, sha),
252
+ maxAgeMs: (value) =>
253
+ value.length === 0 || value.some((check) => check.bucket === "pending")
254
+ ? 15000
255
+ : OPS.prChecks.maxAgeMs,
256
+ });
257
+ });
258
+
259
+ const movedHead = (pr: number) =>
260
+ new GitHubCommandError({
261
+ command: `gh-tool pr checks --pr ${pr}`,
262
+ exitCode: 1,
263
+ message: "PR head changed while collecting checks",
264
+ stderr: "PR head changed while collecting checks",
265
+ retryable: true,
266
+ });
@@ -0,0 +1,306 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { GitHubService } from "#gh/read/transport";
3
+ import { GitHubCommandError } from "#gh/errors";
4
+ import type { CheckRunAnnotation } from "#gh/types";
5
+ import { cachedRead, ReadCache } from "./cache";
6
+ import { OPS } from "./classes";
7
+ import { repoSlug } from "./keys";
8
+ import { issueView, issueList, issueComments } from "./issues";
9
+ import { prView, prList, prChecks, prHead, checkSignal } from "./pulls";
10
+ import { prComments, prReviews, reviewThreads, lastHumanReviewer } from "./reviews";
11
+ import { reviewGuards } from "./review-guards";
12
+ import type { RepoRef, RunFilter } from "./keys";
13
+ import {
14
+ parsePreserving,
15
+ RUN_LIST_FIELDS,
16
+ RUN_VIEW_FIELDS,
17
+ WorkflowRun,
18
+ WorkflowRunDetail,
19
+ WorkflowJob,
20
+ } from "./porcelain";
21
+
22
+ export type JobRef = { readonly id: number; readonly status: string };
23
+
24
+ const completedAttemptJobs = Effect.fn("reads.completedAttemptJobs")(function* (
25
+ repo: RepoRef,
26
+ runId: number,
27
+ attempt: number,
28
+ ) {
29
+ const gh = yield* GitHubService;
30
+ const Job = Schema.Struct({ id: Schema.Number, name: Schema.String });
31
+ const result = yield* cachedRead({
32
+ key: { op: "completedAttemptJobs", repo, runId, attempt },
33
+ policy: OPS.completedAttemptJobs,
34
+ decode: parsePreserving(
35
+ Schema.Struct({ jobs: Schema.mutable(Schema.Array(Job)), completed: Schema.Boolean }),
36
+ ),
37
+ cacheableWhen: (value) => value.completed,
38
+ fetch: () =>
39
+ Effect.gen(function* () {
40
+ const run = yield* gh.runGhJson<{ status?: string }>([
41
+ "api",
42
+ `repos/${repoSlug(repo)}/actions/runs/${runId}/attempts/${attempt}`,
43
+ ]);
44
+ const jobs: Array<typeof Job.Type> = [];
45
+ for (let page = 1; ; page += 1) {
46
+ const response = yield* gh.runGhJson<{ jobs?: Array<typeof Job.Type> }>([
47
+ "api",
48
+ `repos/${repoSlug(repo)}/actions/runs/${runId}/attempts/${attempt}/jobs?per_page=100&page=${page}`,
49
+ ]);
50
+ if (response.jobs === undefined)
51
+ return yield* new GitHubCommandError({
52
+ command: "attempt jobs",
53
+ exitCode: 0,
54
+ message: "Attempt jobs unavailable",
55
+ stderr: "Attempt jobs unavailable",
56
+ });
57
+ jobs.push(...response.jobs);
58
+ if (response.jobs.length < 100)
59
+ return {
60
+ kind: "fresh" as const,
61
+ value: { jobs, completed: run.status === "completed" },
62
+ };
63
+ }
64
+ }),
65
+ });
66
+ return result.jobs;
67
+ });
68
+
69
+ export const resolveRepo = Effect.fn("reads.resolveRepo")(function* (repo: string | null) {
70
+ if (repo !== null) {
71
+ const [owner, name, extra] = repo.split("/");
72
+ if (owner && name && extra === undefined) return { owner, name };
73
+ return yield* new GitHubCommandError({
74
+ message: `Invalid repository: ${repo}`,
75
+ command: "gh-tool --repo",
76
+ exitCode: 1,
77
+ stderr: "Expected owner/name",
78
+ });
79
+ }
80
+ const gh = yield* GitHubService;
81
+ return yield* gh.getRepoRef();
82
+ });
83
+
84
+ const runList = Effect.fn("reads.runList")(function* (
85
+ repo: RepoRef,
86
+ filter: RunFilter,
87
+ consistency: "cached" | "origin" = "cached",
88
+ ) {
89
+ const gh = yield* GitHubService;
90
+ const args = [
91
+ "run",
92
+ "list",
93
+ "--repo",
94
+ repoSlug(repo),
95
+ "--json",
96
+ RUN_LIST_FIELDS,
97
+ "--limit",
98
+ String(filter.limit),
99
+ ];
100
+ for (const [flag, value] of [
101
+ ["--workflow", filter.workflow],
102
+ ["--branch", filter.branch],
103
+ ["--status", filter.status],
104
+ ["--event", filter.event],
105
+ ] as const)
106
+ if (value !== null && value !== undefined) args.push(flag, value);
107
+ return yield* cachedRead({
108
+ key: { op: "runList", repo, filter },
109
+ policy: OPS.runList,
110
+ consistency,
111
+ decode: parsePreserving(Schema.Array(WorkflowRun)),
112
+ fetch: () =>
113
+ gh
114
+ .runGhJson<ReadonlyArray<WorkflowRun>>(args)
115
+ .pipe(Effect.map((value) => ({ kind: "fresh" as const, value }))),
116
+ });
117
+ });
118
+
119
+ const runView = Effect.fn("reads.runView")(function* (
120
+ repo: RepoRef,
121
+ runId: number,
122
+ consistency: "cached" | "origin" = "cached",
123
+ ) {
124
+ const gh = yield* GitHubService;
125
+ const run = yield* cachedRead({
126
+ key: { op: "runView", repo, runId },
127
+ policy: OPS.runView,
128
+ consistency,
129
+ decode: parsePreserving(WorkflowRunDetail),
130
+ policyForValue: (value) =>
131
+ value.status === "completed" ? { kind: "ttl", ttlMs: 300000 } : OPS.runView,
132
+ fetch: () =>
133
+ gh
134
+ .runGhJson<WorkflowRunDetail>([
135
+ "run",
136
+ "view",
137
+ String(runId),
138
+ "--repo",
139
+ repoSlug(repo),
140
+ "--json",
141
+ RUN_VIEW_FIELDS,
142
+ ])
143
+ .pipe(Effect.map((value) => ({ kind: "fresh" as const, value }))),
144
+ });
145
+ if (run.status !== "completed" || consistency === "origin") return run;
146
+ return yield* cachedRead({
147
+ key: { op: "runView", repo, runId, attempt: run.attempt },
148
+ policy: { kind: "immutable" },
149
+ decode: parsePreserving(WorkflowRunDetail),
150
+ fetch: () => Effect.succeed({ kind: "fresh" as const, value: run }),
151
+ });
152
+ });
153
+
154
+ const runJobs = Effect.fn("reads.runJobs")(function* (repo: RepoRef, runId: number) {
155
+ const run = yield* runView(repo, runId);
156
+ return yield* cachedRead({
157
+ key: { op: "runJobs", repo, runId, attempt: run.attempt },
158
+ policy: run.status === "completed" ? { kind: "immutable" } : OPS.runJobs,
159
+ decode: parsePreserving(Schema.Array(WorkflowJob)),
160
+ fetch: () => Effect.succeed({ kind: "fresh" as const, value: run.jobs }),
161
+ });
162
+ });
163
+
164
+ const runLog = Effect.fn("reads.runLog")(function* (
165
+ repo: RepoRef,
166
+ runId: number,
167
+ failedOnly: boolean,
168
+ jobId: number | null,
169
+ ) {
170
+ const gh = yield* GitHubService;
171
+ const run = yield* runView(repo, runId);
172
+ const args = [
173
+ "run",
174
+ "view",
175
+ String(runId),
176
+ "--repo",
177
+ repoSlug(repo),
178
+ failedOnly ? "--log-failed" : "--log",
179
+ "--attempt",
180
+ String(run.attempt),
181
+ ];
182
+ if (jobId !== null) args.push("--job", String(jobId));
183
+ return yield* cachedRead({
184
+ key: { op: "runLog", repo, runId, attempt: run.attempt, failedOnly, jobId },
185
+ policy: run.status === "completed" ? { kind: "immutable" } : OPS.runLog,
186
+ decode: parsePreserving(Schema.String),
187
+ fetch: () =>
188
+ gh
189
+ .runGh(args)
190
+ .pipe(Effect.map((result) => ({ kind: "fresh" as const, value: result.stdout }))),
191
+ });
192
+ });
193
+
194
+ export const rawJobLog = Effect.fn("reads.rawJobLog")(function* (path: string) {
195
+ const gh = yield* GitHubService;
196
+ return yield* gh
197
+ .runGh(["api", path, "--allow-escape-sequences"])
198
+ .pipe(
199
+ Effect.catchTag("GitHubCommandError", (error) =>
200
+ `${error.stderr}${error.message}`.includes("unknown flag: --allow-escape-sequences")
201
+ ? gh.runGh(["api", path])
202
+ : Effect.fail(error),
203
+ ),
204
+ );
205
+ });
206
+ const jobLog = Effect.fn("reads.jobLog")(function* (repo: RepoRef, job: JobRef) {
207
+ return yield* cachedRead({
208
+ key: { op: "jobLog", repo, jobId: job.id },
209
+ policy: job.status === "completed" ? { kind: "immutable" } : OPS.jobLog,
210
+ decode: parsePreserving(Schema.String),
211
+ fetch: () =>
212
+ rawJobLog(`repos/${repoSlug(repo)}/actions/jobs/${job.id}/logs`).pipe(
213
+ Effect.map((result) => ({ kind: "fresh" as const, value: result.stdout })),
214
+ ),
215
+ });
216
+ });
217
+
218
+ const Annotation = Schema.Struct({
219
+ path: Schema.String,
220
+ start_line: Schema.Number,
221
+ end_line: Schema.Number,
222
+ start_column: Schema.NullOr(Schema.Number),
223
+ end_column: Schema.NullOr(Schema.Number),
224
+ annotation_level: Schema.Literals(["notice", "warning", "failure"]),
225
+ message: Schema.String,
226
+ title: Schema.NullOr(Schema.String),
227
+ raw_details: Schema.NullOr(Schema.String),
228
+ });
229
+ const annotations = Effect.fn("reads.annotations")(function* (repo: RepoRef, job: JobRef) {
230
+ const gh = yield* GitHubService;
231
+ return yield* cachedRead({
232
+ key: { op: "annotations", repo, jobId: job.id },
233
+ policy: job.status === "completed" ? { kind: "immutable" } : OPS.annotations,
234
+ decode: parsePreserving(Schema.Array(Annotation)),
235
+ fetch: () =>
236
+ gh
237
+ .runGhJson<ReadonlyArray<CheckRunAnnotation>>([
238
+ "api",
239
+ `repos/${repoSlug(repo)}/check-runs/${job.id}/annotations`,
240
+ "--paginate",
241
+ ])
242
+ .pipe(Effect.map((value) => ({ kind: "fresh" as const, value }))),
243
+ });
244
+ });
245
+
246
+ const JobIdentity = Schema.NullOr(Schema.Struct({ runId: Schema.Number, attempt: Schema.Number }));
247
+ const jobIdentity = Effect.fn("reads.jobIdentity")(function* (repo: RepoRef, jobId: number) {
248
+ const gh = yield* GitHubService;
249
+ const session = yield* ReadCache;
250
+ const path = `repos/${repoSlug(repo)}/actions/jobs/${jobId}`;
251
+ return yield* cachedRead({
252
+ key: { op: "jobIdentity", repo, jobId },
253
+ policy: OPS.jobIdentity,
254
+ decode: parsePreserving(JobIdentity),
255
+ fetch: () =>
256
+ (session === null || session.store === null
257
+ ? gh.runGhJson<{ run_id: number; run_attempt: number }>(["api", path])
258
+ : gh
259
+ .apiRequest<{ run_id: number; run_attempt: number }>({ path })
260
+ .pipe(Effect.map((response) => response.body))
261
+ ).pipe(
262
+ Effect.flatMap((job) =>
263
+ Effect.try({
264
+ try: () =>
265
+ Schema.decodeUnknownSync(JobIdentity)({
266
+ runId: job.run_id,
267
+ attempt: job.run_attempt,
268
+ }),
269
+ catch: (error) =>
270
+ new GitHubCommandError({
271
+ command: `GET ${path}`,
272
+ exitCode: 0,
273
+ message: String(error),
274
+ stderr: String(error),
275
+ }),
276
+ }),
277
+ ),
278
+ Effect.catchTag("GitHubNotFoundError", () => Effect.succeed(null)),
279
+ Effect.map((value) => ({ kind: "fresh" as const, value })),
280
+ ),
281
+ });
282
+ });
283
+
284
+ export const reads = {
285
+ completedAttemptJobs,
286
+ jobIdentity,
287
+ reviewGuards,
288
+ prComments,
289
+ prReviews,
290
+ reviewThreads,
291
+ lastHumanReviewer,
292
+ prView,
293
+ prList,
294
+ prChecks,
295
+ prHead,
296
+ checkSignal,
297
+ runList,
298
+ runView,
299
+ runJobs,
300
+ runLog,
301
+ jobLog,
302
+ annotations,
303
+ issueView,
304
+ issueList,
305
+ issueComments,
306
+ };
@@ -0,0 +1,111 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { GitHubService } from "#gh/read/transport";
3
+ import { GitHubCommandError } from "#gh/errors";
4
+ import { cachedRead, ReadCache } from "./cache";
5
+ import { OPS } from "./classes";
6
+ import { digest } from "./keys";
7
+ import type { ReadKey } from "./keys";
8
+ import { parsePreserving } from "./porcelain";
9
+
10
+ const Page = Schema.Struct({
11
+ path: Schema.String,
12
+ etag: Schema.NullOr(Schema.String),
13
+ body: Schema.Unknown,
14
+ });
15
+ const Snapshot = Schema.Struct({ pages: Schema.Array(Page) });
16
+ export type RestSnapshot = typeof Snapshot.Type;
17
+ export type RestSource =
18
+ | { readonly path: string; readonly kind: "single" }
19
+ | {
20
+ readonly path: string;
21
+ readonly kind: "pages";
22
+ readonly items: (body: unknown) => readonly unknown[];
23
+ };
24
+
25
+ export const restSnapshot = Effect.fn("reads.restSnapshot")(function* (
26
+ key: ReadKey & { op: "prHead" | "checkSignal" | "issueComments" | "prComments" | "prReviews" },
27
+ sources: readonly RestSource[],
28
+ consistency: "cached" | "origin" = "cached",
29
+ ) {
30
+ const gh = yield* GitHubService;
31
+ const session = yield* ReadCache;
32
+ return yield* cachedRead({
33
+ key,
34
+ policy: OPS[key.op],
35
+ consistency,
36
+ validateOrigin: true,
37
+ decode: parsePreserving(Snapshot),
38
+ fetch: (previous: { value: RestSnapshot } | null) =>
39
+ Effect.gen(function* () {
40
+ const pages: Array<typeof Page.Type> = [];
41
+ let unchanged = previous !== null;
42
+ for (const source of sources) {
43
+ let page = 1;
44
+ while (true) {
45
+ const path =
46
+ source.kind === "single"
47
+ ? source.path
48
+ : `${source.path}${source.path.includes("?") ? "&" : "?"}per_page=100&page=${page}`;
49
+ const prior = previous?.value.pages.find((item) => item.path === path);
50
+ const request = {
51
+ path,
52
+ ...(prior?.etag === null || prior?.etag === undefined
53
+ ? {}
54
+ : { ifNoneMatch: prior.etag }),
55
+ };
56
+ const response = yield* session?.store === null || session === null
57
+ ? gh.runGh(["api", path]).pipe(
58
+ Effect.flatMap((result) =>
59
+ Effect.try({
60
+ try: () => ({
61
+ status: 200,
62
+ body: JSON.parse(result.stdout) as unknown,
63
+ etag: undefined,
64
+ }),
65
+ catch: (error) =>
66
+ new GitHubCommandError({
67
+ command: `GET ${path}`,
68
+ exitCode: 0,
69
+ message: `Failed to parse response: ${String(error)}`,
70
+ stderr: `Failed to parse response: ${String(error)}`,
71
+ }),
72
+ }),
73
+ ),
74
+ )
75
+ : gh.apiRequest<unknown>(request);
76
+ if (response.status === 304 && prior === undefined)
77
+ return yield* new GitHubCommandError({
78
+ command: `GET ${path}`,
79
+ exitCode: 304,
80
+ message: "304 without a cached page",
81
+ stderr: "304 without a cached page",
82
+ });
83
+ const body =
84
+ response.status === 304 && prior !== undefined ? prior.body : response.body;
85
+ const etag = response.etag ?? (response.status === 304 ? (prior?.etag ?? null) : null);
86
+ unchanged = unchanged && response.status === 304;
87
+ pages.push({ path, etag, body });
88
+ if (source.kind === "single") break;
89
+ const count = yield* Effect.try({
90
+ try: () => source.items(body).length,
91
+ catch: (error) =>
92
+ new GitHubCommandError({
93
+ command: `GET ${path}`,
94
+ exitCode: 0,
95
+ message: `Invalid paginated response: ${String(error)}`,
96
+ stderr: `Invalid paginated response: ${String(error)}`,
97
+ }),
98
+ });
99
+ if (count < 100) break;
100
+ page += 1;
101
+ }
102
+ }
103
+ return unchanged
104
+ ? { kind: "unchanged" as const }
105
+ : { kind: "fresh" as const, value: { pages } };
106
+ }),
107
+ });
108
+ });
109
+
110
+ export const snapshotDigest = (snapshot: RestSnapshot): string =>
111
+ digest(JSON.stringify(snapshot.pages.map((page) => page.body)));