@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.
- package/README.md +2 -1
- package/dist/db-tool/service.d.ts.map +1 -1
- package/dist/gh-tool/branch.d.ts +2 -3
- package/dist/gh-tool/branch.d.ts.map +1 -1
- package/dist/gh-tool/environment.d.ts +1 -1
- package/dist/gh-tool/environment.d.ts.map +1 -1
- package/dist/gh-tool/gist.d.ts +8 -28
- package/dist/gh-tool/gist.d.ts.map +1 -1
- package/dist/gh-tool/issue/commands.d.ts +9 -9
- package/dist/gh-tool/issue/commands.d.ts.map +1 -1
- package/dist/gh-tool/issue/core.d.ts +103 -9
- package/dist/gh-tool/issue/core.d.ts.map +1 -1
- package/dist/gh-tool/issue/triage.d.ts +6 -6
- package/dist/gh-tool/issue/triage.d.ts.map +1 -1
- package/dist/gh-tool/pr/commands.d.ts +53 -46
- package/dist/gh-tool/pr/commands.d.ts.map +1 -1
- package/dist/gh-tool/pr/core.d.ts +156 -86
- package/dist/gh-tool/pr/core.d.ts.map +1 -1
- package/dist/gh-tool/pr/review.d.ts +16 -16
- package/dist/gh-tool/pr/review.d.ts.map +1 -1
- package/dist/gh-tool/pr/snapshot.d.ts +9 -0
- package/dist/gh-tool/pr/snapshot.d.ts.map +1 -0
- package/dist/gh-tool/pr/stack-read.d.ts +1 -6
- package/dist/gh-tool/pr/stack-read.d.ts.map +1 -1
- package/dist/gh-tool/pr/stack.d.ts +3 -3
- package/dist/gh-tool/pr/stack.d.ts.map +1 -1
- package/dist/gh-tool/{api.d.ts → read/api.d.ts} +9 -3
- package/dist/gh-tool/read/api.d.ts.map +1 -0
- package/dist/gh-tool/read/cache.d.ts +44 -0
- package/dist/gh-tool/read/cache.d.ts.map +1 -0
- package/dist/gh-tool/read/catalog.d.ts +170 -0
- package/dist/gh-tool/read/catalog.d.ts.map +1 -0
- package/dist/gh-tool/read/classes.d.ts +94 -0
- package/dist/gh-tool/read/classes.d.ts.map +1 -0
- package/dist/gh-tool/read/commands.d.ts +3 -0
- package/dist/gh-tool/read/commands.d.ts.map +1 -0
- package/dist/gh-tool/read/gated.d.ts +14 -0
- package/dist/gh-tool/read/gated.d.ts.map +1 -0
- package/dist/gh-tool/read/index.d.ts +5 -0
- package/dist/gh-tool/read/index.d.ts.map +1 -0
- package/dist/gh-tool/read/issues.d.ts +68 -0
- package/dist/gh-tool/read/issues.d.ts.map +1 -0
- package/dist/gh-tool/read/keys.d.ts +61 -0
- package/dist/gh-tool/read/keys.d.ts.map +1 -0
- package/dist/gh-tool/read/porcelain.d.ts +68 -0
- package/dist/gh-tool/read/porcelain.d.ts.map +1 -0
- package/dist/gh-tool/read/pulls.d.ts +137 -0
- package/dist/gh-tool/read/pulls.d.ts.map +1 -0
- package/dist/gh-tool/read/reads.d.ts +209 -0
- package/dist/gh-tool/read/reads.d.ts.map +1 -0
- package/dist/gh-tool/read/rest.d.ts +49 -0
- package/dist/gh-tool/read/rest.d.ts.map +1 -0
- package/dist/gh-tool/read/review-guards.d.ts +20 -0
- package/dist/gh-tool/read/review-guards.d.ts.map +1 -0
- package/dist/gh-tool/read/review-model.d.ts +190 -0
- package/dist/gh-tool/read/review-model.d.ts.map +1 -0
- package/dist/gh-tool/read/reviews.d.ts +9 -0
- package/dist/gh-tool/read/reviews.d.ts.map +1 -0
- package/dist/gh-tool/read/stacks.d.ts +7 -0
- package/dist/gh-tool/read/stacks.d.ts.map +1 -0
- package/dist/gh-tool/read/store.d.ts +70 -0
- package/dist/gh-tool/read/store.d.ts.map +1 -0
- package/dist/gh-tool/{service.d.ts → read/transport.d.ts} +5 -3
- package/dist/gh-tool/read/transport.d.ts.map +1 -0
- package/dist/gh-tool/read/waits.d.ts +22 -0
- package/dist/gh-tool/read/waits.d.ts.map +1 -0
- package/dist/gh-tool/read/writes.d.ts +16 -0
- package/dist/gh-tool/read/writes.d.ts.map +1 -0
- package/dist/gh-tool/release.d.ts +10 -26
- package/dist/gh-tool/release.d.ts.map +1 -1
- package/dist/gh-tool/repo.d.ts +4 -4
- package/dist/gh-tool/repo.d.ts.map +1 -1
- package/dist/gh-tool/types.d.ts +1 -1
- package/dist/gh-tool/types.d.ts.map +1 -1
- package/dist/gh-tool/workflow.d.ts +43 -21
- package/dist/gh-tool/workflow.d.ts.map +1 -1
- package/dist/observability-tool/shared.d.ts.map +1 -1
- package/dist/shared/content-security.d.ts +2 -0
- package/dist/shared/content-security.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/api-tool/proxy-entry.ts +3 -1
- package/src/db-tool/service.ts +4 -1
- package/src/gh-tool/branch.ts +5 -8
- package/src/gh-tool/environment.ts +2 -1
- package/src/gh-tool/gist.ts +8 -35
- package/src/gh-tool/issue/commands.ts +1 -1
- package/src/gh-tool/issue/core.ts +55 -122
- package/src/gh-tool/issue/triage.ts +4 -26
- package/src/gh-tool/main.ts +5 -2
- package/src/gh-tool/pr/commands.ts +17 -15
- package/src/gh-tool/pr/core.ts +486 -582
- package/src/gh-tool/pr/review.ts +106 -454
- package/src/gh-tool/pr/snapshot.ts +23 -0
- package/src/gh-tool/pr/stack-read.ts +1 -73
- package/src/gh-tool/pr/stack.ts +37 -36
- package/src/gh-tool/{api.ts → read/api.ts} +40 -7
- package/src/gh-tool/read/cache.ts +192 -0
- package/src/gh-tool/read/catalog.ts +310 -0
- package/src/gh-tool/read/classes.ts +30 -0
- package/src/gh-tool/read/commands.ts +25 -0
- package/src/gh-tool/read/gated.ts +56 -0
- package/src/gh-tool/read/index.ts +4 -0
- package/src/gh-tool/read/issues.ts +107 -0
- package/src/gh-tool/read/keys.ts +102 -0
- package/src/gh-tool/read/porcelain.ts +58 -0
- package/src/gh-tool/read/pulls.ts +266 -0
- package/src/gh-tool/read/reads.ts +306 -0
- package/src/gh-tool/read/rest.ts +111 -0
- package/src/gh-tool/read/review-guards.ts +61 -0
- package/src/gh-tool/read/review-model.ts +286 -0
- package/src/gh-tool/read/reviews.ts +114 -0
- package/src/gh-tool/read/stacks.ts +73 -0
- package/src/gh-tool/read/store.ts +504 -0
- package/src/gh-tool/{service.ts → read/transport.ts} +26 -6
- package/src/gh-tool/read/waits.ts +52 -0
- package/src/gh-tool/read/writes.ts +37 -0
- package/src/gh-tool/release.ts +15 -168
- package/src/gh-tool/repo.ts +5 -78
- package/src/gh-tool/types.ts +1 -1
- package/src/gh-tool/workflow.ts +123 -214
- package/src/observability-tool/shared.ts +2 -1
- package/src/shared/content-security.ts +10 -1
- package/src/shared/credential-patterns.ts +1 -1
- package/dist/gh-tool/api.d.ts.map +0 -1
- package/dist/gh-tool/pr/check-status-cache.d.ts +0 -6
- package/dist/gh-tool/pr/check-status-cache.d.ts.map +0 -1
- package/dist/gh-tool/service.d.ts.map +0 -1
- package/src/gh-tool/pr/check-status-cache.ts +0 -142
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { GitHubService } from "#gh/read/transport";
|
|
3
|
+
import { GitHubCommandError } from "#gh/errors";
|
|
4
|
+
|
|
5
|
+
type OrgRepo = {
|
|
6
|
+
name: string;
|
|
7
|
+
description: string | null;
|
|
8
|
+
visibility: string;
|
|
9
|
+
updatedAt: string;
|
|
10
|
+
url: string;
|
|
11
|
+
isArchived: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type CodeSearchResponse = {
|
|
15
|
+
items: Array<{
|
|
16
|
+
repository: {
|
|
17
|
+
full_name: string;
|
|
18
|
+
};
|
|
19
|
+
path: string;
|
|
20
|
+
html_url: string;
|
|
21
|
+
}>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Internal handlers
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
export const listOrgRepos = Effect.fn("repo.listOrgRepos")(function* (opts: {
|
|
29
|
+
org: string;
|
|
30
|
+
limit: number;
|
|
31
|
+
visibility: string | null;
|
|
32
|
+
}) {
|
|
33
|
+
const gh = yield* GitHubService;
|
|
34
|
+
|
|
35
|
+
const args = [
|
|
36
|
+
"repo",
|
|
37
|
+
"list",
|
|
38
|
+
opts.org,
|
|
39
|
+
"--json",
|
|
40
|
+
"name,description,visibility,updatedAt,url,isArchived",
|
|
41
|
+
"--limit",
|
|
42
|
+
String(opts.limit),
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
if (opts.visibility !== null) {
|
|
46
|
+
args.push("--visibility", opts.visibility);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return yield* gh.runGhJson<OrgRepo[]>(args);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const searchOrgCode = Effect.fn("repo.searchOrgCode")(function* (opts: {
|
|
53
|
+
org: string;
|
|
54
|
+
query: string;
|
|
55
|
+
limit: number;
|
|
56
|
+
}) {
|
|
57
|
+
const gh = yield* GitHubService;
|
|
58
|
+
|
|
59
|
+
const trimmedQuery = opts.query.trim();
|
|
60
|
+
if (trimmedQuery.length === 0) {
|
|
61
|
+
return yield* new GitHubCommandError({
|
|
62
|
+
message: "Search query cannot be empty",
|
|
63
|
+
command: "repo search-code",
|
|
64
|
+
exitCode: 1,
|
|
65
|
+
stderr: "Search query cannot be empty",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const searchQuery = encodeURIComponent(`${trimmedQuery} org:${opts.org}`);
|
|
70
|
+
const args = ["api", `/search/code?q=${searchQuery}&per_page=${opts.limit}`];
|
|
71
|
+
|
|
72
|
+
const response = yield* gh.runGhJson<CodeSearchResponse>(args);
|
|
73
|
+
|
|
74
|
+
return response.items.map((item) => ({
|
|
75
|
+
repo: item.repository.full_name,
|
|
76
|
+
path: item.path,
|
|
77
|
+
url: item.html_url,
|
|
78
|
+
}));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export type ReleaseListItem = {
|
|
82
|
+
tagName: string;
|
|
83
|
+
name: string;
|
|
84
|
+
isDraft: boolean;
|
|
85
|
+
isPrerelease: boolean;
|
|
86
|
+
createdAt: string;
|
|
87
|
+
publishedAt: string | null;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export type ReleaseAuthor = {
|
|
91
|
+
login: string;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export type ReleaseAsset = {
|
|
95
|
+
name: string;
|
|
96
|
+
size: number;
|
|
97
|
+
downloadCount: number;
|
|
98
|
+
contentType: string;
|
|
99
|
+
createdAt: string;
|
|
100
|
+
updatedAt: string;
|
|
101
|
+
url: string;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export type ReleaseDetail = {
|
|
105
|
+
tagName: string;
|
|
106
|
+
name: string;
|
|
107
|
+
isDraft: boolean;
|
|
108
|
+
isPrerelease: boolean;
|
|
109
|
+
createdAt: string;
|
|
110
|
+
publishedAt: string | null;
|
|
111
|
+
url: string;
|
|
112
|
+
body: string;
|
|
113
|
+
targetCommitish: string;
|
|
114
|
+
author: ReleaseAuthor | null;
|
|
115
|
+
assets: ReleaseAsset[];
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export type ReleaseCreateResult = {
|
|
119
|
+
created: true;
|
|
120
|
+
tagName: string;
|
|
121
|
+
name: string;
|
|
122
|
+
url: string;
|
|
123
|
+
isDraft: boolean;
|
|
124
|
+
isPrerelease: boolean;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export type ReleaseEditResult = {
|
|
128
|
+
edited: true;
|
|
129
|
+
tagName: string;
|
|
130
|
+
name: string;
|
|
131
|
+
url: string;
|
|
132
|
+
isDraft: boolean;
|
|
133
|
+
isPrerelease: boolean;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export type ReleaseDeleteResult = {
|
|
137
|
+
deleted: boolean;
|
|
138
|
+
tagName: string;
|
|
139
|
+
tagCleaned: boolean;
|
|
140
|
+
dryRun?: true;
|
|
141
|
+
message?: string;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export type LatestRelease = {
|
|
145
|
+
tagName: string;
|
|
146
|
+
name: string;
|
|
147
|
+
createdAt: string;
|
|
148
|
+
publishedAt: string | null;
|
|
149
|
+
isLatest: boolean;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export type ReleaseStatusResult = {
|
|
153
|
+
latestRelease: LatestRelease | null;
|
|
154
|
+
repo: {
|
|
155
|
+
owner: string;
|
|
156
|
+
name: string;
|
|
157
|
+
defaultBranch: string;
|
|
158
|
+
url: string;
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export const listReleases = Effect.fn("release.listReleases")(function* (opts: {
|
|
163
|
+
limit: number;
|
|
164
|
+
repo: string | null;
|
|
165
|
+
}) {
|
|
166
|
+
const gh = yield* GitHubService;
|
|
167
|
+
|
|
168
|
+
const args = [
|
|
169
|
+
"release",
|
|
170
|
+
"list",
|
|
171
|
+
"--json",
|
|
172
|
+
"tagName,name,isDraft,isPrerelease,createdAt,publishedAt",
|
|
173
|
+
"--limit",
|
|
174
|
+
String(opts.limit),
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
if (opts.repo !== null) {
|
|
178
|
+
args.push("--repo", opts.repo);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return yield* gh.runGhJson<ReleaseListItem[]>(args);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
export const viewRelease = Effect.fn("release.viewRelease")(function* (opts: {
|
|
185
|
+
tag: string;
|
|
186
|
+
repo: string | null;
|
|
187
|
+
}) {
|
|
188
|
+
const gh = yield* GitHubService;
|
|
189
|
+
|
|
190
|
+
const args = [
|
|
191
|
+
"release",
|
|
192
|
+
"view",
|
|
193
|
+
opts.tag,
|
|
194
|
+
"--json",
|
|
195
|
+
"tagName,name,isDraft,isPrerelease,createdAt,publishedAt,url,body,targetCommitish,author,assets",
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
if (opts.repo !== null) {
|
|
199
|
+
args.push("--repo", opts.repo);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return yield* gh.runGhJson<ReleaseDetail>(args);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
export const releaseStatus = Effect.fn("release.releaseStatus")(function* (repo: string | null) {
|
|
206
|
+
const gh = yield* GitHubService;
|
|
207
|
+
|
|
208
|
+
const args = [
|
|
209
|
+
"release",
|
|
210
|
+
"list",
|
|
211
|
+
"--json",
|
|
212
|
+
"tagName,name,createdAt,publishedAt,isLatest",
|
|
213
|
+
"--limit",
|
|
214
|
+
"1",
|
|
215
|
+
];
|
|
216
|
+
if (repo !== null) {
|
|
217
|
+
args.push("--repo", repo);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const [repoInfo, releases] = yield* Effect.all([
|
|
221
|
+
gh.getRepoInfo(),
|
|
222
|
+
gh.runGhJson<LatestRelease[]>(args),
|
|
223
|
+
]);
|
|
224
|
+
|
|
225
|
+
const result: ReleaseStatusResult = {
|
|
226
|
+
latestRelease: releases.length > 0 ? releases[0] : null,
|
|
227
|
+
repo: {
|
|
228
|
+
owner: repoInfo.owner,
|
|
229
|
+
name: repoInfo.name,
|
|
230
|
+
defaultBranch: repoInfo.defaultBranch,
|
|
231
|
+
url: repoInfo.url,
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
return result;
|
|
236
|
+
});
|
|
237
|
+
export type GistApiFile = {
|
|
238
|
+
filename: string;
|
|
239
|
+
language: string | null;
|
|
240
|
+
type: string;
|
|
241
|
+
size: number;
|
|
242
|
+
truncated?: boolean;
|
|
243
|
+
content?: string;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
export type GistApi = {
|
|
247
|
+
id: string;
|
|
248
|
+
description: string | null;
|
|
249
|
+
public: boolean;
|
|
250
|
+
html_url: string;
|
|
251
|
+
created_at: string;
|
|
252
|
+
updated_at: string;
|
|
253
|
+
owner: { login: string } | null;
|
|
254
|
+
files: Record<string, GistApiFile>;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const gistList = Effect.fn("reads.gistList")(function* (limit: number) {
|
|
258
|
+
const gh = yield* GitHubService;
|
|
259
|
+
return yield* gh.runGhJson<GistApi[]>(["api", `gists?per_page=${limit}`]);
|
|
260
|
+
});
|
|
261
|
+
const gistView = Effect.fn("reads.gistView")(function* (id: string) {
|
|
262
|
+
const gh = yield* GitHubService;
|
|
263
|
+
return yield* gh.runGhJson<GistApi>(["api", `gists/${id}`]);
|
|
264
|
+
});
|
|
265
|
+
export type AsyncMergeResult = {
|
|
266
|
+
status: "pending" | "merged" | "enqueued" | "failed";
|
|
267
|
+
details?: {
|
|
268
|
+
message?: string;
|
|
269
|
+
uuid?: string;
|
|
270
|
+
sha?: string;
|
|
271
|
+
merge_method?: "squash" | "merge" | "rebase";
|
|
272
|
+
};
|
|
273
|
+
};
|
|
274
|
+
const baseSha = Effect.fn("reads.baseSha")(function* (
|
|
275
|
+
repo: { owner: string; name: string },
|
|
276
|
+
pr: number,
|
|
277
|
+
) {
|
|
278
|
+
const gh = yield* GitHubService;
|
|
279
|
+
return yield* gh.runGh([
|
|
280
|
+
"api",
|
|
281
|
+
`repos/${repo.owner}/${repo.name}/pulls/${pr}`,
|
|
282
|
+
"--jq",
|
|
283
|
+
".base.sha",
|
|
284
|
+
]);
|
|
285
|
+
});
|
|
286
|
+
const asyncMergeStatus = Effect.fn("reads.asyncMergeStatus")(function* (path: string) {
|
|
287
|
+
const gh = yield* GitHubService;
|
|
288
|
+
return yield* gh.runGhJson<AsyncMergeResult>(["api", path]);
|
|
289
|
+
});
|
|
290
|
+
const stackMergeStatus = Effect.fn("reads.stackMergeStatus")(function* (path: string) {
|
|
291
|
+
const gh = yield* GitHubService;
|
|
292
|
+
return yield* gh.apiRequest<AsyncMergeResult>({ path });
|
|
293
|
+
});
|
|
294
|
+
const repoInfo = Effect.fn("reads.repoInfo")(function* () {
|
|
295
|
+
const gh = yield* GitHubService;
|
|
296
|
+
return yield* gh.getRepoInfo();
|
|
297
|
+
});
|
|
298
|
+
export const passthrough = {
|
|
299
|
+
repoInfo,
|
|
300
|
+
listOrgRepos,
|
|
301
|
+
searchOrgCode,
|
|
302
|
+
listReleases,
|
|
303
|
+
viewRelease,
|
|
304
|
+
releaseStatus,
|
|
305
|
+
gistList,
|
|
306
|
+
gistView,
|
|
307
|
+
baseSha,
|
|
308
|
+
asyncMergeStatus,
|
|
309
|
+
stackMergeStatus,
|
|
310
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Op } from "./keys";
|
|
2
|
+
|
|
3
|
+
export type CacheClass =
|
|
4
|
+
| { readonly kind: "immutable" }
|
|
5
|
+
| { readonly kind: "ttl" | "etag"; readonly ttlMs: number }
|
|
6
|
+
| { readonly kind: "gated"; readonly ttlMs: number; readonly maxAgeMs: number }
|
|
7
|
+
| { readonly kind: "passthrough" };
|
|
8
|
+
|
|
9
|
+
export const OPS = {
|
|
10
|
+
prHead: { kind: "etag", ttlMs: 5000 },
|
|
11
|
+
checkSignal: { kind: "etag", ttlMs: 5000 },
|
|
12
|
+
prView: { kind: "gated", ttlMs: 5000, maxAgeMs: 15000 },
|
|
13
|
+
prChecks: { kind: "gated", ttlMs: 5000, maxAgeMs: 60000 },
|
|
14
|
+
prList: { kind: "ttl", ttlMs: 15000 },
|
|
15
|
+
prComments: { kind: "etag", ttlMs: 15000 },
|
|
16
|
+
prReviews: { kind: "etag", ttlMs: 15000 },
|
|
17
|
+
reviewThreads: { kind: "gated", ttlMs: 5000, maxAgeMs: 60000 },
|
|
18
|
+
lastHumanReviewer: { kind: "ttl", ttlMs: 15000 },
|
|
19
|
+
issueView: { kind: "ttl", ttlMs: 30000 },
|
|
20
|
+
issueList: { kind: "ttl", ttlMs: 15000 },
|
|
21
|
+
issueComments: { kind: "etag", ttlMs: 15000 },
|
|
22
|
+
runList: { kind: "ttl", ttlMs: 15000 },
|
|
23
|
+
runView: { kind: "ttl", ttlMs: 15000 },
|
|
24
|
+
runJobs: { kind: "ttl", ttlMs: 15000 },
|
|
25
|
+
completedAttemptJobs: { kind: "immutable" },
|
|
26
|
+
runLog: { kind: "ttl", ttlMs: 15000 },
|
|
27
|
+
jobLog: { kind: "passthrough" },
|
|
28
|
+
annotations: { kind: "passthrough" },
|
|
29
|
+
jobIdentity: { kind: "immutable" },
|
|
30
|
+
} as const satisfies Record<Op, CacheClass>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Effect, Option } from "effect";
|
|
2
|
+
import { Command, Flag } from "effect/cli";
|
|
3
|
+
import { formatOption, logFormatted } from "#shared";
|
|
4
|
+
import { cacheStats, clearCache } from "./cache";
|
|
5
|
+
import { resolveRepo } from "./reads";
|
|
6
|
+
|
|
7
|
+
const stats = Command.make("stats", { format: formatOption }, ({ format }) =>
|
|
8
|
+
Effect.gen(function* () {
|
|
9
|
+
yield* logFormatted(yield* cacheStats, format);
|
|
10
|
+
}),
|
|
11
|
+
);
|
|
12
|
+
const clear = Command.make(
|
|
13
|
+
"clear",
|
|
14
|
+
{ repo: Flag.String("repo").pipe(Flag.optional), format: formatOption },
|
|
15
|
+
({ repo, format }) =>
|
|
16
|
+
Effect.gen(function* () {
|
|
17
|
+
const target = Option.isSome(repo) ? yield* resolveRepo(repo.value) : null;
|
|
18
|
+
const cleared = yield* clearCache(target);
|
|
19
|
+
yield* logFormatted({ cleared, repo: Option.getOrNull(repo) }, format);
|
|
20
|
+
}),
|
|
21
|
+
);
|
|
22
|
+
export const cacheCommand = Command.make("cache", {}).pipe(
|
|
23
|
+
Command.withDescription("Inspect or clear the shared GitHub read cache"),
|
|
24
|
+
Command.withSubcommands([stats, clear]),
|
|
25
|
+
);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { cachedRead, ReadCache } from "./cache";
|
|
3
|
+
import { OPS } from "./classes";
|
|
4
|
+
import type { ReadKey } from "./keys";
|
|
5
|
+
import { parsePreserving } from "./porcelain";
|
|
6
|
+
|
|
7
|
+
const Envelope = Schema.Struct({
|
|
8
|
+
data: Schema.Unknown,
|
|
9
|
+
originFetchedAt: Schema.Number,
|
|
10
|
+
signal: Schema.String,
|
|
11
|
+
});
|
|
12
|
+
type Envelope<A> = { data: A; originFetchedAt: number; signal: string };
|
|
13
|
+
|
|
14
|
+
export const gatedRead = <A, E, R, E2, R2>(opts: {
|
|
15
|
+
readonly key: ReadKey & { op: "prView" | "prChecks" | "reviewThreads" };
|
|
16
|
+
readonly decode: (value: unknown) => A;
|
|
17
|
+
readonly fetch: Effect.Effect<A, E, R>;
|
|
18
|
+
readonly signal: Effect.Effect<string, E2, R2>;
|
|
19
|
+
readonly maxAgeMs: (value: A) => number;
|
|
20
|
+
readonly consistency?: "cached" | "origin";
|
|
21
|
+
readonly cacheableWhen?: (value: A) => boolean;
|
|
22
|
+
}) =>
|
|
23
|
+
Effect.gen(function* () {
|
|
24
|
+
const session = yield* ReadCache;
|
|
25
|
+
if (session === null || session.store === null || opts.consistency === "origin")
|
|
26
|
+
return yield* opts.fetch;
|
|
27
|
+
const value = yield* cachedRead({
|
|
28
|
+
key: opts.key,
|
|
29
|
+
policy: OPS[opts.key.op],
|
|
30
|
+
cacheableWhen: (envelope: Envelope<A>) => opts.cacheableWhen?.(envelope.data) !== false,
|
|
31
|
+
decode: (raw): Envelope<A> => {
|
|
32
|
+
const envelope = parsePreserving(Envelope)(raw);
|
|
33
|
+
return { ...envelope, data: opts.decode(envelope.data) };
|
|
34
|
+
},
|
|
35
|
+
fetch: (previous: { value: Envelope<A>; valid: boolean } | null) =>
|
|
36
|
+
Effect.gen(function* () {
|
|
37
|
+
const signal = yield* opts.signal.pipe(Effect.orElseSucceed(() => null));
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
if (
|
|
40
|
+
previous !== null &&
|
|
41
|
+
signal !== null &&
|
|
42
|
+
previous.valid &&
|
|
43
|
+
previous.value.signal === signal &&
|
|
44
|
+
now >= previous.value.originFetchedAt &&
|
|
45
|
+
now - previous.value.originFetchedAt < opts.maxAgeMs(previous.value.data)
|
|
46
|
+
)
|
|
47
|
+
return { kind: "fresh" as const, value: previous.value };
|
|
48
|
+
const data = yield* opts.fetch;
|
|
49
|
+
return {
|
|
50
|
+
kind: "fresh" as const,
|
|
51
|
+
value: { data, originFetchedAt: now, signal: signal ?? "" },
|
|
52
|
+
};
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
return value.data;
|
|
56
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { GitHubService } from "#gh/read/transport";
|
|
3
|
+
import { restSnapshot } from "./rest";
|
|
4
|
+
import { cachedRead } from "./cache";
|
|
5
|
+
import { OPS } from "./classes";
|
|
6
|
+
import { repoSlug } from "./keys";
|
|
7
|
+
import type { RepoRef } from "./keys";
|
|
8
|
+
import { parsePreserving } from "./porcelain";
|
|
9
|
+
|
|
10
|
+
const Label = Schema.Struct({ name: Schema.String });
|
|
11
|
+
const Login = Schema.Struct({ login: Schema.String });
|
|
12
|
+
const IssueListItem = Schema.Struct({
|
|
13
|
+
number: Schema.Number,
|
|
14
|
+
title: Schema.String,
|
|
15
|
+
state: Schema.String,
|
|
16
|
+
url: Schema.String,
|
|
17
|
+
labels: Schema.mutable(Schema.Array(Label)),
|
|
18
|
+
createdAt: Schema.String,
|
|
19
|
+
});
|
|
20
|
+
const IssueView = Schema.Struct({
|
|
21
|
+
...IssueListItem.fields,
|
|
22
|
+
assignees: Schema.mutable(Schema.Array(Login)),
|
|
23
|
+
author: Login,
|
|
24
|
+
body: Schema.String,
|
|
25
|
+
closedAt: Schema.NullOr(Schema.String),
|
|
26
|
+
});
|
|
27
|
+
const IssueComment = Schema.Struct({
|
|
28
|
+
id: Schema.Number,
|
|
29
|
+
user: Login,
|
|
30
|
+
body: Schema.String,
|
|
31
|
+
created_at: Schema.String,
|
|
32
|
+
html_url: Schema.String,
|
|
33
|
+
});
|
|
34
|
+
export type IssueView = typeof IssueView.Type;
|
|
35
|
+
export type IssueListFilter = { state: string; labels: string | null; limit: number };
|
|
36
|
+
export const ISSUE_LIST_FIELDS = "number,title,state,url,labels,createdAt";
|
|
37
|
+
export const ISSUE_VIEW_FIELDS =
|
|
38
|
+
"number,title,state,url,labels,assignees,author,body,createdAt,closedAt";
|
|
39
|
+
|
|
40
|
+
export const issueView = Effect.fn("reads.issueView")(function* (
|
|
41
|
+
repo: RepoRef,
|
|
42
|
+
issue: number,
|
|
43
|
+
consistency: "cached" | "origin" = "cached",
|
|
44
|
+
) {
|
|
45
|
+
const gh = yield* GitHubService;
|
|
46
|
+
return yield* cachedRead({
|
|
47
|
+
key: { op: "issueView", repo, issue },
|
|
48
|
+
policy: OPS.issueView,
|
|
49
|
+
consistency,
|
|
50
|
+
decode: parsePreserving(IssueView),
|
|
51
|
+
fetch: () =>
|
|
52
|
+
gh
|
|
53
|
+
.runGhJson<IssueView>([
|
|
54
|
+
"issue",
|
|
55
|
+
"view",
|
|
56
|
+
String(issue),
|
|
57
|
+
"--repo",
|
|
58
|
+
repoSlug(repo),
|
|
59
|
+
"--json",
|
|
60
|
+
ISSUE_VIEW_FIELDS,
|
|
61
|
+
])
|
|
62
|
+
.pipe(Effect.map((value) => ({ kind: "fresh" as const, value }))),
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export const issueList = Effect.fn("reads.issueList")(function* (
|
|
67
|
+
repo: RepoRef,
|
|
68
|
+
filter: IssueListFilter,
|
|
69
|
+
) {
|
|
70
|
+
const gh = yield* GitHubService;
|
|
71
|
+
const args = [
|
|
72
|
+
"issue",
|
|
73
|
+
"list",
|
|
74
|
+
"--state",
|
|
75
|
+
filter.state,
|
|
76
|
+
"--limit",
|
|
77
|
+
String(filter.limit),
|
|
78
|
+
"--json",
|
|
79
|
+
ISSUE_LIST_FIELDS,
|
|
80
|
+
"--repo",
|
|
81
|
+
repoSlug(repo),
|
|
82
|
+
];
|
|
83
|
+
if (filter.labels !== null) args.push("--label", filter.labels);
|
|
84
|
+
return yield* cachedRead({
|
|
85
|
+
key: { op: "issueList", repo, filter },
|
|
86
|
+
policy: OPS.issueList,
|
|
87
|
+
decode: parsePreserving(Schema.mutable(Schema.Array(IssueListItem))),
|
|
88
|
+
fetch: () =>
|
|
89
|
+
gh
|
|
90
|
+
.runGhJson<Array<typeof IssueListItem.Type>>(args)
|
|
91
|
+
.pipe(Effect.map((value) => ({ kind: "fresh" as const, value }))),
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export const issueComments = Effect.fn("reads.issueComments")(function* (
|
|
96
|
+
repo: RepoRef,
|
|
97
|
+
issue: number,
|
|
98
|
+
) {
|
|
99
|
+
const snapshot = yield* restSnapshot({ op: "issueComments", repo, issue }, [
|
|
100
|
+
{
|
|
101
|
+
kind: "pages",
|
|
102
|
+
path: `repos/${repoSlug(repo)}/issues/${issue}/comments`,
|
|
103
|
+
items: Schema.decodeUnknownSync(Schema.Array(Schema.Unknown)),
|
|
104
|
+
},
|
|
105
|
+
]);
|
|
106
|
+
return snapshot.pages.flatMap((page) => page.body as Array<typeof IssueComment.Type>);
|
|
107
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export type RepoRef = { readonly owner: string; readonly name: string };
|
|
4
|
+
export type RunFilter = {
|
|
5
|
+
readonly workflow: string | null;
|
|
6
|
+
readonly branch: string | null;
|
|
7
|
+
readonly status: string | null;
|
|
8
|
+
readonly event: string | null;
|
|
9
|
+
readonly limit: number;
|
|
10
|
+
};
|
|
11
|
+
export type ReadKey = { readonly repo: RepoRef } & (
|
|
12
|
+
| {
|
|
13
|
+
readonly op:
|
|
14
|
+
| "prView"
|
|
15
|
+
| "prHead"
|
|
16
|
+
| "prComments"
|
|
17
|
+
| "prReviews"
|
|
18
|
+
| "reviewThreads"
|
|
19
|
+
| "lastHumanReviewer";
|
|
20
|
+
readonly pr: number;
|
|
21
|
+
}
|
|
22
|
+
| { readonly op: "prChecks"; readonly pr: number; readonly headSha: string }
|
|
23
|
+
| { readonly op: "checkSignal"; readonly sha: string }
|
|
24
|
+
| {
|
|
25
|
+
readonly op: "prList" | "issueList";
|
|
26
|
+
readonly filter: Readonly<Record<string, string | number | boolean | null>>;
|
|
27
|
+
}
|
|
28
|
+
| { readonly op: "issueView" | "issueComments"; readonly issue: number }
|
|
29
|
+
| { readonly op: "runList"; readonly filter: RunFilter }
|
|
30
|
+
| { readonly op: "runView"; readonly runId: number; readonly attempt?: number }
|
|
31
|
+
| { readonly op: "runJobs"; readonly runId: number; readonly attempt: number }
|
|
32
|
+
| { readonly op: "completedAttemptJobs"; readonly runId: number; readonly attempt: number }
|
|
33
|
+
| {
|
|
34
|
+
readonly op: "runLog";
|
|
35
|
+
readonly runId: number;
|
|
36
|
+
readonly attempt: number;
|
|
37
|
+
readonly failedOnly: boolean;
|
|
38
|
+
readonly jobId: number | null;
|
|
39
|
+
}
|
|
40
|
+
| { readonly op: "jobLog" | "annotations" | "jobIdentity"; readonly jobId: number }
|
|
41
|
+
);
|
|
42
|
+
export type Op = ReadKey["op"];
|
|
43
|
+
export type Tag =
|
|
44
|
+
`${"pr" | "reviews" | "review-comments" | "threads" | "issue" | "issue-comments" | "run" | "checks" | "pulls" | "issues" | "runs" | "repo"}:${string}`;
|
|
45
|
+
|
|
46
|
+
const canonical = (value: unknown): unknown => {
|
|
47
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
48
|
+
if (value !== null && typeof value === "object")
|
|
49
|
+
return Object.fromEntries(
|
|
50
|
+
Object.entries(value)
|
|
51
|
+
.toSorted(([a], [b]) => a.localeCompare(b))
|
|
52
|
+
.map(([key, item]) => [key, canonical(item)]),
|
|
53
|
+
);
|
|
54
|
+
return value;
|
|
55
|
+
};
|
|
56
|
+
export const digest = (value: string): string => createHash("sha256").update(value).digest("hex");
|
|
57
|
+
export const repoSlug = (repo: RepoRef): string => `${repo.owner}/${repo.name}`.toLowerCase();
|
|
58
|
+
export const entryKey = (identity: string, key: ReadKey): string =>
|
|
59
|
+
digest(JSON.stringify(canonical({ schema: 1, identity, ...key, repo: repoSlug(key.repo) })));
|
|
60
|
+
export const tagsFor = (key: ReadKey): readonly Tag[] => {
|
|
61
|
+
const repo = repoSlug(key.repo);
|
|
62
|
+
const fallback: Tag = `repo:${repo}`;
|
|
63
|
+
switch (key.op) {
|
|
64
|
+
case "prView":
|
|
65
|
+
case "prHead":
|
|
66
|
+
case "lastHumanReviewer":
|
|
67
|
+
return [fallback, `pr:${repo}#${key.pr}`];
|
|
68
|
+
case "prChecks":
|
|
69
|
+
return [fallback, `pr:${repo}#${key.pr}`, `checks:${repo}#${key.pr}`];
|
|
70
|
+
case "checkSignal":
|
|
71
|
+
return [fallback, `checks:${repo}@${key.sha}`];
|
|
72
|
+
case "prComments":
|
|
73
|
+
return [fallback, `review-comments:${repo}#${key.pr}`];
|
|
74
|
+
case "prReviews":
|
|
75
|
+
return [fallback, `reviews:${repo}#${key.pr}`];
|
|
76
|
+
case "reviewThreads":
|
|
77
|
+
return [fallback, `threads:${repo}#${key.pr}`];
|
|
78
|
+
case "prList":
|
|
79
|
+
return [fallback, `pulls:${repo}`];
|
|
80
|
+
case "issueList":
|
|
81
|
+
return [fallback, `issues:${repo}`];
|
|
82
|
+
case "issueView":
|
|
83
|
+
return [fallback, `issue:${repo}#${key.issue}`];
|
|
84
|
+
case "issueComments":
|
|
85
|
+
return [fallback, `issue-comments:${repo}#${key.issue}`];
|
|
86
|
+
case "runView":
|
|
87
|
+
case "runJobs":
|
|
88
|
+
case "completedAttemptJobs":
|
|
89
|
+
case "runLog":
|
|
90
|
+
return [fallback, `run:${repo}/${key.runId}`];
|
|
91
|
+
case "runList":
|
|
92
|
+
return [fallback, `runs:${repo}`];
|
|
93
|
+
case "jobLog":
|
|
94
|
+
case "annotations":
|
|
95
|
+
case "jobIdentity":
|
|
96
|
+
return [fallback];
|
|
97
|
+
default: {
|
|
98
|
+
const exhaustive: never = key;
|
|
99
|
+
return exhaustive;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
const Step = Schema.Struct({
|
|
4
|
+
name: Schema.String,
|
|
5
|
+
status: Schema.String,
|
|
6
|
+
conclusion: Schema.NullOr(Schema.String),
|
|
7
|
+
number: Schema.Number,
|
|
8
|
+
startedAt: Schema.optional(Schema.NullOr(Schema.String)),
|
|
9
|
+
completedAt: Schema.optional(Schema.NullOr(Schema.String)),
|
|
10
|
+
});
|
|
11
|
+
export const WorkflowJob = Schema.Struct({
|
|
12
|
+
databaseId: Schema.Number,
|
|
13
|
+
name: Schema.String,
|
|
14
|
+
status: Schema.String,
|
|
15
|
+
conclusion: Schema.NullOr(Schema.String),
|
|
16
|
+
startedAt: Schema.String,
|
|
17
|
+
completedAt: Schema.NullOr(Schema.String),
|
|
18
|
+
url: Schema.String,
|
|
19
|
+
steps: Schema.Array(Step),
|
|
20
|
+
});
|
|
21
|
+
export const WorkflowRun = Schema.Struct({
|
|
22
|
+
databaseId: Schema.Number,
|
|
23
|
+
displayTitle: Schema.String,
|
|
24
|
+
status: Schema.String,
|
|
25
|
+
conclusion: Schema.NullOr(Schema.String),
|
|
26
|
+
headBranch: Schema.String,
|
|
27
|
+
headSha: Schema.String,
|
|
28
|
+
createdAt: Schema.String,
|
|
29
|
+
event: Schema.String,
|
|
30
|
+
url: Schema.String,
|
|
31
|
+
workflowName: Schema.String,
|
|
32
|
+
});
|
|
33
|
+
export const WorkflowRunDetail = Schema.Struct({
|
|
34
|
+
...WorkflowRun.fields,
|
|
35
|
+
jobs: Schema.Array(WorkflowJob),
|
|
36
|
+
attempt: Schema.Number,
|
|
37
|
+
});
|
|
38
|
+
export type WorkflowJob = typeof WorkflowJob.Type;
|
|
39
|
+
export type WorkflowRun = typeof WorkflowRun.Type;
|
|
40
|
+
export type WorkflowRunDetail = typeof WorkflowRunDetail.Type;
|
|
41
|
+
export const RUN_LIST_FIELDS =
|
|
42
|
+
"databaseId,displayTitle,status,conclusion,headBranch,headSha,createdAt,event,url,workflowName";
|
|
43
|
+
export const RUN_VIEW_FIELDS = `${RUN_LIST_FIELDS},jobs,attempt`;
|
|
44
|
+
|
|
45
|
+
export const parsePreserving =
|
|
46
|
+
<A>(schema: Schema.Codec<A>) =>
|
|
47
|
+
(value: unknown): A => {
|
|
48
|
+
if (Schema.is(schema)(value)) return value;
|
|
49
|
+
return Schema.decodeUnknownSync(schema)(value);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const project = <A extends object, K extends keyof A>(
|
|
53
|
+
value: A,
|
|
54
|
+
fields: readonly K[],
|
|
55
|
+
): Pick<A, K> =>
|
|
56
|
+
Object.fromEntries(
|
|
57
|
+
Object.entries(value).filter(([key]) => fields.some((field) => field === key)),
|
|
58
|
+
) as Pick<A, K>;
|