@open-cr-agent/vcs-github 0.4.0 → 0.6.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/dist/adapter.d.ts +3 -2
- package/dist/adapter.js +24 -22
- package/dist/client.d.ts +2 -3
- package/dist/client.js +33 -50
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/plugin.js +5 -12
- package/dist/target.d.ts +7 -0
- package/dist/target.js +53 -0
- package/package.json +3 -8
- package/dist/internal.d.ts +0 -3
- package/dist/internal.js +0 -5
package/dist/adapter.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CodeSource, type History, PlatformReview } from "@open-cr-agent/vcs-platform";
|
|
1
|
+
import { type CodeSource, type History, type PlatformChangeRequest, PlatformReview } from "@open-cr-agent/vcs-platform";
|
|
2
2
|
import { type GitHubApi, type PullRequest } from "./client.js";
|
|
3
3
|
export interface GitHubPullRequest {
|
|
4
4
|
owner: string;
|
|
@@ -12,10 +12,11 @@ export interface GitHubAdapterOptions {
|
|
|
12
12
|
botLogin: string;
|
|
13
13
|
requestChanges?: boolean;
|
|
14
14
|
history?: History;
|
|
15
|
-
snapshot?:
|
|
15
|
+
snapshot?: PlatformChangeRequest;
|
|
16
16
|
}
|
|
17
17
|
export declare const DEFAULT_BOT_LOGIN = "github-actions[bot]";
|
|
18
18
|
export declare class GitHubAdapter extends PlatformReview {
|
|
19
19
|
constructor(options: GitHubAdapterOptions);
|
|
20
20
|
}
|
|
21
|
+
export declare function pullRequestOf(pr: PullRequest, { owner, repo }: Pick<GitHubPullRequest, "owner" | "repo">): PlatformChangeRequest;
|
|
21
22
|
//# sourceMappingURL=adapter.d.ts.map
|
package/dist/adapter.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { OcraError } from "@open-cr-agent/core";
|
|
2
|
-
import {
|
|
3
|
-
import { PlatformReview, } from "@open-cr-agent/vcs-platform";
|
|
1
|
+
import { errorMessage, OcraError } from "@open-cr-agent/core";
|
|
2
|
+
import { githubSuggestion, PlatformReview, } from "@open-cr-agent/vcs-platform";
|
|
4
3
|
import { GitHubApiError, } from "./client.js";
|
|
5
4
|
export const DEFAULT_BOT_LOGIN = "github-actions[bot]";
|
|
6
5
|
// A GitHub pull request as ocra's review conversation (vcs-platform).
|
|
@@ -15,11 +14,12 @@ export class GitHubAdapter extends PlatformReview {
|
|
|
15
14
|
}
|
|
16
15
|
}
|
|
17
16
|
class GitHubPlatform {
|
|
18
|
-
options;
|
|
19
17
|
text = { changeRequest: "pull request", authority: "write access" };
|
|
18
|
+
suggestionFence = githubSuggestion;
|
|
20
19
|
pullRequest;
|
|
21
20
|
// The REST comments behind the platform's, for their GraphQL node ids.
|
|
22
21
|
listed = new Map();
|
|
22
|
+
options;
|
|
23
23
|
constructor(options) {
|
|
24
24
|
this.options = options;
|
|
25
25
|
}
|
|
@@ -28,17 +28,14 @@ class GitHubPlatform {
|
|
|
28
28
|
const login = this.options.botLogin;
|
|
29
29
|
return { login, is: (other) => other === login || `${other}[bot]` === login };
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
headSha: pr.head.sha,
|
|
40
|
-
...(pr.user ? { author: pr.user.login } : {}),
|
|
41
|
-
};
|
|
31
|
+
changeRequest() {
|
|
32
|
+
const { pullRequest } = this.options;
|
|
33
|
+
this.pullRequest ??= this.options.snapshot
|
|
34
|
+
? Promise.resolve(this.options.snapshot)
|
|
35
|
+
: this.options.api
|
|
36
|
+
.getPullRequest(pullRequest.number)
|
|
37
|
+
.then((pr) => pullRequestOf(pr, pullRequest));
|
|
38
|
+
return this.pullRequest;
|
|
42
39
|
}
|
|
43
40
|
async comments() {
|
|
44
41
|
const comments = await this.options.api.listIssueComments(this.options.pullRequest.number);
|
|
@@ -145,7 +142,7 @@ class GitHubPlatform {
|
|
|
145
142
|
if (fresh.length === 0 && !requestChanges)
|
|
146
143
|
return [];
|
|
147
144
|
const review = {
|
|
148
|
-
commit_id: (await this.
|
|
145
|
+
commit_id: (await this.changeRequest()).headSha,
|
|
149
146
|
event: requestChanges ? "REQUEST_CHANGES" : "COMMENT",
|
|
150
147
|
body: `ocra: ${report.findings.length} finding(s); details in the summary comment.`,
|
|
151
148
|
};
|
|
@@ -167,12 +164,17 @@ class GitHubPlatform {
|
|
|
167
164
|
return [];
|
|
168
165
|
}
|
|
169
166
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
167
|
+
}
|
|
168
|
+
// The pull request as ocra's review conversation reads it.
|
|
169
|
+
export function pullRequestOf(pr, { owner, repo }) {
|
|
170
|
+
return {
|
|
171
|
+
id: `${owner}/${repo}#${pr.number}`,
|
|
172
|
+
title: pr.title,
|
|
173
|
+
description: pr.body ?? "",
|
|
174
|
+
baseSha: pr.base.sha,
|
|
175
|
+
headSha: pr.head.sha,
|
|
176
|
+
...(pr.user ? { author: pr.user.login } : {}),
|
|
177
|
+
};
|
|
176
178
|
}
|
|
177
179
|
function reviewComment({ finding, body }) {
|
|
178
180
|
const { start, end } = finding.lineRange;
|
package/dist/client.d.ts
CHANGED
|
@@ -73,10 +73,10 @@ export interface ReviewThread {
|
|
|
73
73
|
}[];
|
|
74
74
|
}
|
|
75
75
|
export declare class GitHubApi {
|
|
76
|
+
private readonly baseUrl;
|
|
77
|
+
private readonly api;
|
|
76
78
|
private readonly repository;
|
|
77
79
|
private readonly options;
|
|
78
|
-
private readonly baseUrl;
|
|
79
|
-
private readonly fetchImpl;
|
|
80
80
|
constructor(repository: {
|
|
81
81
|
owner: string;
|
|
82
82
|
repo: string;
|
|
@@ -96,7 +96,6 @@ export declare class GitHubApi {
|
|
|
96
96
|
private graphql;
|
|
97
97
|
private graphqlUrl;
|
|
98
98
|
private request;
|
|
99
|
-
private send;
|
|
100
99
|
}
|
|
101
100
|
export {};
|
|
102
101
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { setTimeout as wait } from "node:timers/promises";
|
|
1
2
|
import { OcraError } from "@open-cr-agent/core";
|
|
2
|
-
import { MAX_ATTEMPTS
|
|
3
|
+
import { MAX_ATTEMPTS } from "@open-cr-agent/vcs-platform";
|
|
4
|
+
import { pageInfoSchema, sendWithRetry, } from "@open-cr-agent/vcs-platform/internal";
|
|
3
5
|
import { z } from "zod";
|
|
4
6
|
// Commit ids reach git as arguments; anything else is refused at the boundary.
|
|
5
7
|
const sha = z.string().regex(/^[0-9a-f]{40}([0-9a-f]{24})?$/, "not a commit id");
|
|
@@ -40,6 +42,8 @@ const MAX_REVIEW_PAGES = 10;
|
|
|
40
42
|
const MAX_FILE_PAGES = 30;
|
|
41
43
|
const pullRequestFileSchema = z.object({ filename: z.string(), patch: z.string().optional() });
|
|
42
44
|
const MAX_COMMENT_PAGES = 30;
|
|
45
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
46
|
+
const REVIEW_TIMEOUT_MS = 120_000;
|
|
43
47
|
const MAX_THREAD_PAGES = 10;
|
|
44
48
|
const REVIEW_THREADS_QUERY = `query($owner: String!, $repo: String!, $number: Int!, $after: String) {
|
|
45
49
|
repository(owner: $owner, name: $repo) {
|
|
@@ -74,7 +78,7 @@ const reviewThreadsSchema = z.object({
|
|
|
74
78
|
repository: z.object({
|
|
75
79
|
pullRequest: z.object({
|
|
76
80
|
reviewThreads: z.object({
|
|
77
|
-
pageInfo:
|
|
81
|
+
pageInfo: pageInfoSchema,
|
|
78
82
|
nodes: z.array(z.object({
|
|
79
83
|
id: z.string(),
|
|
80
84
|
isResolved: z.boolean(),
|
|
@@ -90,15 +94,28 @@ const reviewThreadsSchema = z.object({
|
|
|
90
94
|
// A thin REST client: every response is validated before it reaches the
|
|
91
95
|
// adapter, and errors never echo the token.
|
|
92
96
|
export class GitHubApi {
|
|
97
|
+
baseUrl;
|
|
98
|
+
api;
|
|
93
99
|
repository;
|
|
94
100
|
options;
|
|
95
|
-
baseUrl;
|
|
96
|
-
fetchImpl;
|
|
97
101
|
constructor(repository, options) {
|
|
98
102
|
this.repository = repository;
|
|
99
103
|
this.options = options;
|
|
100
104
|
this.baseUrl = (options.baseUrl ?? "https://api.github.com").replace(/\/$/, "");
|
|
101
|
-
this.
|
|
105
|
+
this.api = {
|
|
106
|
+
platform: "GitHub",
|
|
107
|
+
headers: {
|
|
108
|
+
Accept: "application/vnd.github+json",
|
|
109
|
+
Authorization: `Bearer ${options.token}`,
|
|
110
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
111
|
+
},
|
|
112
|
+
fetch: options.fetch ?? fetch,
|
|
113
|
+
sleep: options.sleep,
|
|
114
|
+
// Creating a review with many inline comments can take GitHub a while,
|
|
115
|
+
// and a POST that timed out after GitHub acted cannot be repeated.
|
|
116
|
+
timeoutMs: (method, path) => method === "POST" && path.endsWith("/reviews") ? REVIEW_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
|
|
117
|
+
toError: (status, message) => new GitHubApiError(status, message),
|
|
118
|
+
};
|
|
102
119
|
}
|
|
103
120
|
async listReviewThreads(number) {
|
|
104
121
|
const threads = [];
|
|
@@ -213,10 +230,18 @@ export class GitHubApi {
|
|
|
213
230
|
// GraphQL reports its rate limit as an error in a 200 response, which the
|
|
214
231
|
// HTTP retry does not see; it is retried here the same bounded way.
|
|
215
232
|
async graphql(query, variables) {
|
|
216
|
-
const sleep = this.options.sleep ?? ((ms) =>
|
|
233
|
+
const sleep = this.options.sleep ?? ((ms) => wait(ms));
|
|
217
234
|
let result;
|
|
218
235
|
for (let attempt = 1;; attempt += 1) {
|
|
219
|
-
result = graphqlResultSchema.parse(await
|
|
236
|
+
result = graphqlResultSchema.parse(await sendWithRetry(this.api, {
|
|
237
|
+
method: "POST",
|
|
238
|
+
url: this.graphqlUrl(),
|
|
239
|
+
path: "/graphql",
|
|
240
|
+
body: { query, variables },
|
|
241
|
+
// GraphQL queries and resolving a thread are safe to repeat,
|
|
242
|
+
// although they are POSTs.
|
|
243
|
+
idempotent: true,
|
|
244
|
+
}));
|
|
220
245
|
const limited = result.errors?.some((e) => e.type === "RATE_LIMITED");
|
|
221
246
|
if (!limited || attempt >= MAX_ATTEMPTS)
|
|
222
247
|
break;
|
|
@@ -237,49 +262,7 @@ export class GitHubApi {
|
|
|
237
262
|
request(method, path, body) {
|
|
238
263
|
const { owner, repo } = this.repository;
|
|
239
264
|
const url = `${this.baseUrl}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}${path}`;
|
|
240
|
-
return this.
|
|
241
|
-
}
|
|
242
|
-
// GraphQL queries and resolving a thread are safe to repeat, although they
|
|
243
|
-
// are POSTs; REST POSTs create comments and reviews, and are not.
|
|
244
|
-
async send(method, url, path, body, idempotent = method !== "POST") {
|
|
245
|
-
const sleep = this.options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
246
|
-
for (let attempt = 1;; attempt += 1) {
|
|
247
|
-
const init = {
|
|
248
|
-
method,
|
|
249
|
-
headers: {
|
|
250
|
-
Accept: "application/vnd.github+json",
|
|
251
|
-
Authorization: `Bearer ${this.options.token}`,
|
|
252
|
-
"X-GitHub-Api-Version": "2022-11-28",
|
|
253
|
-
"User-Agent": "open-cr-agent",
|
|
254
|
-
...(body === undefined ? {} : { "Content-Type": "application/json" }),
|
|
255
|
-
},
|
|
256
|
-
// Creating a review with many inline comments can take GitHub a while,
|
|
257
|
-
// and a POST that timed out after GitHub acted cannot be repeated.
|
|
258
|
-
signal: AbortSignal.timeout(method === "POST" && path.endsWith("/reviews") ? 120_000 : 30_000),
|
|
259
|
-
};
|
|
260
|
-
if (body !== undefined)
|
|
261
|
-
init.body = JSON.stringify(body);
|
|
262
|
-
let response;
|
|
263
|
-
try {
|
|
264
|
-
response = await this.fetchImpl(url, init);
|
|
265
|
-
}
|
|
266
|
-
catch (error) {
|
|
267
|
-
const next = retryDecision(method, idempotent, "network_error", attempt);
|
|
268
|
-
if (!next.retry)
|
|
269
|
-
throw error;
|
|
270
|
-
await sleep(next.waitMs);
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
if (response.ok)
|
|
274
|
-
return response.status === 204 ? undefined : response.json();
|
|
275
|
-
const detail = (await response.text().catch(() => "")).slice(0, 500);
|
|
276
|
-
const next = retryDecision(method, idempotent, { status: response.status, headers: response.headers, body: detail }, attempt);
|
|
277
|
-
if (next.retry) {
|
|
278
|
-
await sleep(next.waitMs);
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
throw new GitHubApiError(response.status, `GitHub ${method} ${path.split("?")[0]} failed with ${response.status}: ${detail}`);
|
|
282
|
-
}
|
|
265
|
+
return sendWithRetry(this.api, { method, url, path, body });
|
|
283
266
|
}
|
|
284
267
|
}
|
|
285
268
|
//# sourceMappingURL=client.js.map
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/plugin.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { changeRequestSchema, codeSourceSchema, fetchSchema, historySchema, } from "@open-cr-agent/vcs-platform";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { DEFAULT_BOT_LOGIN, GitHubAdapter } from "./adapter.js";
|
|
3
4
|
import { GitHubApi } from "./client.js";
|
|
@@ -9,18 +10,10 @@ const optionsSchema = z.object({
|
|
|
9
10
|
apiUrl: z.string().url().optional(),
|
|
10
11
|
botLogin: z.string().min(1).default(DEFAULT_BOT_LOGIN),
|
|
11
12
|
requestChanges: z.boolean().default(false),
|
|
12
|
-
fetch:
|
|
13
|
-
code:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
snapshot: z
|
|
17
|
-
.custom((value) => typeof value === "object" && value !== null && "head" in value)
|
|
18
|
-
.optional(),
|
|
19
|
-
history: z
|
|
20
|
-
.custom((value) => typeof value === "object" &&
|
|
21
|
-
value !== null &&
|
|
22
|
-
typeof value.filesChangedSince === "function", "history must provide filesChangedSince")
|
|
23
|
-
.optional(),
|
|
13
|
+
fetch: fetchSchema.optional(),
|
|
14
|
+
code: codeSourceSchema,
|
|
15
|
+
snapshot: changeRequestSchema.optional(),
|
|
16
|
+
history: historySchema.optional(),
|
|
24
17
|
});
|
|
25
18
|
export const githubPlugin = {
|
|
26
19
|
name: "vcs-github",
|
package/dist/target.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PlatformTarget, ResolveTargetOptions } from "@open-cr-agent/vcs-platform";
|
|
2
|
+
export interface GitHubSettings {
|
|
3
|
+
botLogin?: string;
|
|
4
|
+
requestChanges?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function resolveGitHubTarget(options: ResolveTargetOptions): Promise<PlatformTarget<GitHubSettings>>;
|
|
7
|
+
//# sourceMappingURL=target.d.ts.map
|
package/dist/target.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { OcraError } from "@open-cr-agent/core";
|
|
2
|
+
import { DEFAULT_BOT_LOGIN, pullRequestOf } from "./adapter.js";
|
|
3
|
+
import { GitHubApi } from "./client.js";
|
|
4
|
+
// A pull request on GitHub. GITHUB_TOKEN (or GH_TOKEN) is the credential,
|
|
5
|
+
// GITHUB_API_URL the API of GitHub Enterprise Server; the repository is
|
|
6
|
+
// --repo, else GITHUB_REPOSITORY, else origin's.
|
|
7
|
+
export async function resolveGitHubTarget(options) {
|
|
8
|
+
const { env, ref } = options;
|
|
9
|
+
const token = env.GITHUB_TOKEN ?? env.GH_TOKEN;
|
|
10
|
+
if (!token) {
|
|
11
|
+
throw new OcraError("CONFIG_CREDENTIALS_MISSING", "--pr needs a GitHub token in GITHUB_TOKEN or GH_TOKEN");
|
|
12
|
+
}
|
|
13
|
+
const [owner = "", repo = ""] = (ref.repository ?? (await repositoryName(options))).split("/");
|
|
14
|
+
const apiUrl = env.GITHUB_API_URL;
|
|
15
|
+
const fetchImpl = options.fetch;
|
|
16
|
+
const api = new GitHubApi({ owner, repo }, {
|
|
17
|
+
token,
|
|
18
|
+
...(apiUrl ? { baseUrl: apiUrl } : {}),
|
|
19
|
+
...(fetchImpl ? { fetch: fetchImpl } : {}),
|
|
20
|
+
});
|
|
21
|
+
const pull = await api.getPullRequest(ref.number);
|
|
22
|
+
return {
|
|
23
|
+
platform: "github",
|
|
24
|
+
baseSha: pull.base.sha,
|
|
25
|
+
headSha: pull.head.sha,
|
|
26
|
+
headRefs: [`pull/${ref.number}/head`],
|
|
27
|
+
webUrl: pull.html_url,
|
|
28
|
+
createVcs: (registry, local, settings) => registry.createVcs("github", {
|
|
29
|
+
owner,
|
|
30
|
+
repo,
|
|
31
|
+
number: ref.number,
|
|
32
|
+
token,
|
|
33
|
+
...(apiUrl ? { apiUrl } : {}),
|
|
34
|
+
botLogin: settings.botLogin ?? DEFAULT_BOT_LOGIN,
|
|
35
|
+
requestChanges: settings.requestChanges ?? false,
|
|
36
|
+
code: local.code,
|
|
37
|
+
snapshot: pullRequestOf(pull, { owner, repo }),
|
|
38
|
+
history: local.history,
|
|
39
|
+
...(fetchImpl ? { fetch: fetchImpl } : {}),
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async function repositoryName(options) {
|
|
44
|
+
if (options.env.GITHUB_REPOSITORY)
|
|
45
|
+
return options.env.GITHUB_REPOSITORY;
|
|
46
|
+
const url = await options.origin();
|
|
47
|
+
const match = url && /github\.com[:/]([\w.-]+\/[\w.-]+?)(?:\.git)?$/.exec(url);
|
|
48
|
+
if (!match?.[1]) {
|
|
49
|
+
throw new OcraError("CONFIG_INVALID", "Cannot tell which GitHub repository this is; pass --repo owner/name");
|
|
50
|
+
}
|
|
51
|
+
return match[1];
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=target.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-cr-agent/vcs-github",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "GitHub pull request adapter for Open-CR-Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"code-review",
|
|
@@ -26,11 +26,6 @@
|
|
|
26
26
|
"types": "./dist/index.d.ts",
|
|
27
27
|
"default": "./dist/index.js"
|
|
28
28
|
},
|
|
29
|
-
"./internal": {
|
|
30
|
-
"@open-cr-agent/source": "./src/internal.ts",
|
|
31
|
-
"types": "./dist/internal.d.ts",
|
|
32
|
-
"default": "./dist/internal.js"
|
|
33
|
-
},
|
|
34
29
|
"./package.json": "./package.json"
|
|
35
30
|
},
|
|
36
31
|
"files": [
|
|
@@ -38,8 +33,8 @@
|
|
|
38
33
|
"!dist/**/*.map"
|
|
39
34
|
],
|
|
40
35
|
"dependencies": {
|
|
41
|
-
"@open-cr-agent/core": "0.
|
|
42
|
-
"@open-cr-agent/vcs-platform": "0.
|
|
36
|
+
"@open-cr-agent/core": "0.6.0",
|
|
37
|
+
"@open-cr-agent/vcs-platform": "0.6.0",
|
|
43
38
|
"zod": "^4.6.5"
|
|
44
39
|
},
|
|
45
40
|
"publishConfig": {
|
package/dist/internal.d.ts
DELETED
package/dist/internal.js
DELETED