@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 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?: PullRequest;
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 { errorMessage } from "@open-cr-agent/core/internal";
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
- async changeRequest() {
32
- const pr = await this.pr();
33
- const { owner, repo } = this.options.pullRequest;
34
- return {
35
- id: `${owner}/${repo}#${pr.number}`,
36
- title: pr.title,
37
- description: pr.body ?? "",
38
- baseSha: pr.base.sha,
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.pr()).head.sha,
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
- pr() {
171
- this.pullRequest ??= this.options.snapshot
172
- ? Promise.resolve(this.options.snapshot)
173
- : this.options.api.getPullRequest(this.options.pullRequest.number);
174
- return this.pullRequest;
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, retryDecision } from "@open-cr-agent/vcs-platform";
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: z.object({ hasNextPage: z.boolean(), endCursor: z.string().nullable() }),
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.fetchImpl = options.fetch ?? fetch;
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) => new Promise((r) => setTimeout(r, 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 this.send("POST", this.graphqlUrl(), "/graphql", { query, variables }, true));
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.send(method, url, path, body);
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
@@ -1,3 +1,4 @@
1
1
  export { GitHubApiError } from "./client.js";
2
2
  export { githubPlugin } from "./plugin.js";
3
+ export { type GitHubSettings, resolveGitHubTarget } from "./target.js";
3
4
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { GitHubApiError } from "./client.js";
2
2
  export { githubPlugin } from "./plugin.js";
3
+ export { resolveGitHubTarget } from "./target.js";
3
4
  //# sourceMappingURL=index.js.map
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: z.custom((value) => typeof value === "function").optional(),
13
- code: z.custom((value) => typeof value === "object" &&
14
- value !== null &&
15
- ["getDiff", "readFile", "searchCode"].every((m) => typeof value[m] === "function"), "code must provide getDiff, readFile and searchCode"),
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",
@@ -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.4.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.4.0",
42
- "@open-cr-agent/vcs-platform": "0.4.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": {
@@ -1,3 +0,0 @@
1
- export { DEFAULT_BOT_LOGIN } from "./adapter.js";
2
- export { GitHubApi } from "./client.js";
3
- //# sourceMappingURL=internal.d.ts.map
package/dist/internal.js DELETED
@@ -1,5 +0,0 @@
1
- // "@open-cr-agent/vcs-github/internal": what the CLI uses beyond the public
2
- // entry. Not a contract: any release may change it.
3
- export { DEFAULT_BOT_LOGIN } from "./adapter.js";
4
- export { GitHubApi } from "./client.js";
5
- //# sourceMappingURL=internal.js.map