pr-shepherd 0.35.0 → 0.37.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/.claude-plugin/plugin.json +1 -1
- package/README.md +32 -12
- package/bin/checks/conclusions.d.mts +6 -0
- package/bin/checks/conclusions.mjs +9 -0
- package/bin/checks/triage.mjs +11 -3
- package/bin/cli/args.mjs +1 -0
- package/bin/cli/default-poll.mjs +1 -0
- package/bin/cli/fix-formatter.mjs +4 -2
- package/bin/cli/help-command-pages.d.mts +1 -1
- package/bin/cli/help-iterate-poll-pages.d.mts +1 -1
- package/bin/cli/help-iterate-poll-pages.mjs +10 -5
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +2 -1
- package/bin/cli/help.d.mts +2 -2
- package/bin/cli/iterate-formatter.mjs +11 -2
- package/bin/cli/iterate-lean.mjs +1 -0
- package/bin/cli/poll-handler.mjs +7 -0
- package/bin/commands/check-annotations.d.mts +16 -3
- package/bin/commands/check-annotations.mjs +37 -1
- package/bin/commands/check.d.mts +1 -0
- package/bin/commands/check.mjs +35 -28
- package/bin/commands/commit-suggestion.mjs +11 -5
- package/bin/commands/iterate/fix-code.mjs +13 -6
- package/bin/commands/iterate/helpers.d.mts +1 -0
- package/bin/commands/iterate/helpers.mjs +15 -6
- package/bin/commands/iterate/index.mjs +5 -10
- package/bin/commands/iterate/render.mjs +9 -5
- package/bin/commands/iterate/stall.mjs +5 -0
- package/bin/commands/poll.d.mts +2 -0
- package/bin/commands/poll.mjs +51 -17
- package/bin/commands/ready-delay.mjs +3 -13
- package/bin/config/load.mjs +67 -25
- package/bin/github/batch-page-helpers.d.mts +45 -0
- package/bin/github/batch-page-helpers.mjs +63 -0
- package/bin/github/batch-page.d.mts +14 -0
- package/bin/github/batch-page.mjs +62 -0
- package/bin/github/batch-parse-suites.d.mts +4 -0
- package/bin/github/batch-parse-suites.mjs +25 -0
- package/bin/github/batch-parser-helpers.mjs +1 -0
- package/bin/github/batch-parsers-rules.d.mts +6 -0
- package/bin/github/batch-parsers-rules.mjs +122 -0
- package/bin/github/batch-parsers.mjs +6 -0
- package/bin/github/batch-raw-rules.d.mts +59 -0
- package/bin/github/batch-raw-rules.mjs +1 -0
- package/bin/github/batch-raw-types.d.mts +25 -11
- package/bin/github/batch.d.mts +2 -0
- package/bin/github/batch.mjs +14 -120
- package/bin/github/gql/batch-pr-page.gql +194 -0
- package/bin/github/gql/batch-pr.gql +79 -21
- package/bin/github/gql/commit-suggestion-thread.gql +40 -0
- package/bin/github/http.d.mts +2 -1
- package/bin/github/http.mjs +2 -1
- package/bin/github/pagination.d.mts +3 -2
- package/bin/github/pagination.mjs +3 -2
- package/bin/github/queries.d.mts +4 -0
- package/bin/github/queries.mjs +4 -0
- package/bin/github/rest-http.d.mts +6 -1
- package/bin/github/rest-http.mjs +25 -86
- package/bin/github/rest-text.d.mts +1 -0
- package/bin/github/rest-text.mjs +88 -0
- package/bin/github/suggestion-thread.d.mts +9 -0
- package/bin/github/suggestion-thread.mjs +45 -0
- package/bin/github/thread-comments.mjs +12 -8
- package/bin/merge-status/derive.mjs +2 -0
- package/bin/merge-status/requirements-format.d.mts +3 -0
- package/bin/merge-status/requirements-format.mjs +88 -0
- package/bin/merge-status/requirements.d.mts +2 -0
- package/bin/merge-status/requirements.mjs +51 -0
- package/bin/reporters/agent.mjs +0 -3
- package/bin/state/base.d.mts +9 -0
- package/bin/state/base.mjs +23 -0
- package/bin/state/bot-cr-seen.mjs +3 -13
- package/bin/state/fix-attempts.mjs +3 -13
- package/bin/state/iterate-stall.mjs +3 -13
- package/bin/state/seen-comments.mjs +6 -13
- package/bin/types/check-classification.d.mts +2 -2
- package/bin/types/github.d.mts +6 -7
- package/bin/types/iterate.d.mts +6 -7
- package/bin/types/merge-requirements.d.mts +82 -0
- package/bin/types/merge-requirements.mjs +2 -0
- package/bin/types/report.d.mts +5 -5
- package/bin/types.d.mts +1 -0
- package/bin/types.mjs +1 -0
- package/bin/util/path-segment.d.mts +2 -0
- package/bin/util/path-segment.mjs +2 -0
- package/bin/util/pool.d.mts +2 -0
- package/bin/util/pool.mjs +18 -0
- package/package.json +1 -1
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/.codex.mcp.json +1 -1
- package/plugins/pr-shepherd/.mcp.json +1 -1
package/bin/config/load.mjs
CHANGED
|
@@ -1,24 +1,40 @@
|
|
|
1
1
|
/* eslint-disable max-lines */
|
|
2
2
|
import { readFileSync, statSync } from "node:fs";
|
|
3
|
-
import { join,
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { parse } from "yaml";
|
|
6
6
|
import builtins from "../config.json" with { type: "json" };
|
|
7
7
|
import { getEffectiveCwd } from "../execution-context.mjs";
|
|
8
8
|
const MINIMIZE_COMMENTS_POLICIES = ["all", "bots", "users", "none"];
|
|
9
9
|
const RC_FILENAME = ".pr-shepherdrc.yml";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Collect `.pr-shepherdrc.yml` files from `startDir` toward `$HOME` (closest first).
|
|
12
|
+
* `$HOME/.pr-shepherdrc.yml` is always included when it exists, even if cwd is
|
|
13
|
+
* outside the home directory. The walk never includes the filesystem root, so a
|
|
14
|
+
* `/.pr-shepherdrc.yml` cannot override the user-level file.
|
|
15
|
+
*/
|
|
16
|
+
function collectRcFiles(startDir) {
|
|
17
|
+
const home = resolve(homedir());
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
const files = [];
|
|
20
|
+
const add = (dir) => {
|
|
21
|
+
const candidate = resolve(join(dir, RC_FILENAME));
|
|
22
|
+
if (seen.has(candidate))
|
|
23
|
+
return;
|
|
15
24
|
if (statSync(candidate, { throwIfNoEntry: false })?.isFile()) {
|
|
16
|
-
|
|
25
|
+
seen.add(candidate);
|
|
26
|
+
files.push(candidate);
|
|
17
27
|
}
|
|
18
|
-
|
|
19
|
-
|
|
28
|
+
};
|
|
29
|
+
let current = resolve(startDir);
|
|
30
|
+
while (current !== dirname(current)) {
|
|
31
|
+
add(current);
|
|
32
|
+
if (current === home)
|
|
33
|
+
break;
|
|
20
34
|
current = dirname(current);
|
|
21
35
|
}
|
|
36
|
+
add(home);
|
|
37
|
+
return files;
|
|
22
38
|
}
|
|
23
39
|
function deepMerge(base, override) {
|
|
24
40
|
const result = { ...base };
|
|
@@ -125,30 +141,56 @@ function warnUnknownConfigKeys(config) {
|
|
|
125
141
|
}
|
|
126
142
|
const defaults = builtins;
|
|
127
143
|
const configCache = new Map();
|
|
144
|
+
function stripDeprecatedActionKeys(parsed) {
|
|
145
|
+
const rawActions = parsed.actions;
|
|
146
|
+
if (rawActions === null || typeof rawActions !== "object" || Array.isArray(rawActions))
|
|
147
|
+
return;
|
|
148
|
+
const actions = rawActions;
|
|
149
|
+
for (const key of ["autoResolveOutdated", "commitSuggestions"]) {
|
|
150
|
+
if (key in actions) {
|
|
151
|
+
process.stderr.write(`pr-shepherd: config: actions.${key} is deprecated and ignored.\n`);
|
|
152
|
+
delete actions[key];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function readRcFile(rcPath) {
|
|
157
|
+
try {
|
|
158
|
+
const raw = readFileSync(rcPath, "utf8");
|
|
159
|
+
const parsed = (parse(raw) ?? {});
|
|
160
|
+
stripDeprecatedActionKeys(parsed);
|
|
161
|
+
warnUnknownConfigKeys(parsed);
|
|
162
|
+
return parsed;
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
process.stderr.write(`pr-shepherd: failed to parse ${rcPath}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
128
169
|
export function loadConfig() {
|
|
129
170
|
const cwd = getEffectiveCwd();
|
|
130
171
|
if (configCache.has(cwd))
|
|
131
172
|
return configCache.get(cwd);
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
173
|
+
const rcPaths = collectRcFiles(cwd);
|
|
174
|
+
if (rcPaths.length === 0) {
|
|
134
175
|
configCache.set(cwd, defaults);
|
|
135
176
|
return defaults;
|
|
136
177
|
}
|
|
137
178
|
try {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
}
|
|
179
|
+
// Farthest file first so closer directories override, ESLint-style.
|
|
180
|
+
let overlay = {};
|
|
181
|
+
let loaded = false;
|
|
182
|
+
for (const rcPath of [...rcPaths].reverse()) {
|
|
183
|
+
const parsed = readRcFile(rcPath);
|
|
184
|
+
if (parsed === null)
|
|
185
|
+
continue;
|
|
186
|
+
overlay = deepMerge(overlay, parsed);
|
|
187
|
+
loaded = true;
|
|
149
188
|
}
|
|
150
|
-
|
|
151
|
-
|
|
189
|
+
if (!loaded) {
|
|
190
|
+
configCache.set(cwd, defaults);
|
|
191
|
+
return defaults;
|
|
192
|
+
}
|
|
193
|
+
const config = deepMerge(structuredClone(defaults), overlay);
|
|
152
194
|
config.botUsernames = parseBotUsernames(config.botUsernames);
|
|
153
195
|
config.ignoreChecks = parseIgnoreChecks(config.ignoreChecks);
|
|
154
196
|
config.actions.neverCancelRuns = parseNeverCancelRuns(config.actions.neverCancelRuns);
|
|
@@ -157,7 +199,7 @@ export function loadConfig() {
|
|
|
157
199
|
return config;
|
|
158
200
|
}
|
|
159
201
|
catch (err) {
|
|
160
|
-
process.stderr.write(`pr-shepherd: failed to parse ${
|
|
202
|
+
process.stderr.write(`pr-shepherd: failed to parse ${rcPaths[0]}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
161
203
|
const fallback = { ...defaults };
|
|
162
204
|
configCache.set(cwd, fallback);
|
|
163
205
|
return fallback;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { RawContextNode, RawComment, RawPr, RawReview, RawReviewSummary, RawThread } from "./batch-raw-types.mts";
|
|
2
|
+
export interface RawPageResponse {
|
|
3
|
+
repository: {
|
|
4
|
+
pullRequest: {
|
|
5
|
+
reviewThreads?: RawPr["reviewThreads"];
|
|
6
|
+
comments?: RawPr["comments"];
|
|
7
|
+
changesRequestedReviews?: RawPr["changesRequestedReviews"];
|
|
8
|
+
reviewSummaries?: RawPr["reviewSummaries"];
|
|
9
|
+
approvedReviews?: RawPr["approvedReviews"];
|
|
10
|
+
commits?: RawPr["commits"];
|
|
11
|
+
} | null;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export declare function backwardCursor(conn: {
|
|
15
|
+
pageInfo: {
|
|
16
|
+
hasPreviousPage: boolean;
|
|
17
|
+
startCursor: string | null;
|
|
18
|
+
};
|
|
19
|
+
} | undefined): string | undefined;
|
|
20
|
+
export declare function forwardCursor(conn: {
|
|
21
|
+
pageInfo: {
|
|
22
|
+
hasNextPage: boolean;
|
|
23
|
+
endCursor: string | null;
|
|
24
|
+
};
|
|
25
|
+
} | undefined): string | undefined;
|
|
26
|
+
export interface PageCursors {
|
|
27
|
+
threads?: string;
|
|
28
|
+
comments?: string;
|
|
29
|
+
changesRequested?: string;
|
|
30
|
+
reviewSummaries?: string;
|
|
31
|
+
approvedReviews?: string;
|
|
32
|
+
checks?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface PageAccumulator {
|
|
35
|
+
threads: RawThread[];
|
|
36
|
+
comments: RawComment[];
|
|
37
|
+
changesRequested: RawReview[];
|
|
38
|
+
reviewSummaries: RawReviewSummary[];
|
|
39
|
+
approvedReviews: RawReviewSummary[];
|
|
40
|
+
checks: RawContextNode[];
|
|
41
|
+
}
|
|
42
|
+
export declare function applyIncludedPage(dest: PageAccumulator, pr2: NonNullable<RawPageResponse["repository"]["pullRequest"]>, cursors: PageCursors, firstOid: string | undefined, pageCount: number, pr: number): {
|
|
43
|
+
cursors: PageCursors;
|
|
44
|
+
pageCount: number;
|
|
45
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { requireContextNodes } from "./batch-response.mjs";
|
|
2
|
+
export function backwardCursor(conn) {
|
|
3
|
+
if (conn?.pageInfo.hasPreviousPage === true && conn.pageInfo.startCursor) {
|
|
4
|
+
return conn.pageInfo.startCursor;
|
|
5
|
+
}
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
export function forwardCursor(conn) {
|
|
9
|
+
if (conn?.pageInfo.hasNextPage === true && conn.pageInfo.endCursor) {
|
|
10
|
+
return conn.pageInfo.endCursor;
|
|
11
|
+
}
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
function prependConnection(dest, conn, pr) {
|
|
15
|
+
if (!conn)
|
|
16
|
+
throw new Error(`PR #${pr} not found`);
|
|
17
|
+
dest.unshift(...conn.nodes);
|
|
18
|
+
}
|
|
19
|
+
function takeCheckPage(pr2, firstOid, pageCount) {
|
|
20
|
+
if (!pr2.commits?.nodes[0]?.commit.statusCheckRollup) {
|
|
21
|
+
throw new Error(`Check-context pagination interrupted: statusCheckRollup disappeared on page ${pageCount + 1} (possible force-push race). Retry after the push stabilizes.`);
|
|
22
|
+
}
|
|
23
|
+
const currentOid = pr2.commits.nodes[0]?.commit.oid;
|
|
24
|
+
if (firstOid !== undefined && currentOid !== undefined && currentOid !== firstOid) {
|
|
25
|
+
throw new Error(`Check-context pagination interrupted: head commit changed from ${firstOid} to ${currentOid} between pages (force-push race). Retry.`);
|
|
26
|
+
}
|
|
27
|
+
const ctxs = pr2.commits.nodes[0]?.commit.statusCheckRollup?.contexts;
|
|
28
|
+
if (!ctxs) {
|
|
29
|
+
return { pageInfo: { hasNextPage: false, endCursor: null }, nodes: [] };
|
|
30
|
+
}
|
|
31
|
+
return { ...ctxs, nodes: requireContextNodes(ctxs.nodes) };
|
|
32
|
+
}
|
|
33
|
+
export function applyIncludedPage(dest, pr2, cursors, firstOid, pageCount, pr) {
|
|
34
|
+
const next = {};
|
|
35
|
+
let nextPageCount = pageCount;
|
|
36
|
+
if (cursors.threads !== undefined) {
|
|
37
|
+
prependConnection(dest.threads, pr2.reviewThreads, pr);
|
|
38
|
+
next.threads = backwardCursor(pr2.reviewThreads);
|
|
39
|
+
}
|
|
40
|
+
if (cursors.comments !== undefined) {
|
|
41
|
+
prependConnection(dest.comments, pr2.comments, pr);
|
|
42
|
+
next.comments = backwardCursor(pr2.comments);
|
|
43
|
+
}
|
|
44
|
+
if (cursors.changesRequested !== undefined) {
|
|
45
|
+
prependConnection(dest.changesRequested, pr2.changesRequestedReviews, pr);
|
|
46
|
+
next.changesRequested = backwardCursor(pr2.changesRequestedReviews);
|
|
47
|
+
}
|
|
48
|
+
if (cursors.reviewSummaries !== undefined) {
|
|
49
|
+
prependConnection(dest.reviewSummaries, pr2.reviewSummaries, pr);
|
|
50
|
+
next.reviewSummaries = backwardCursor(pr2.reviewSummaries);
|
|
51
|
+
}
|
|
52
|
+
if (cursors.approvedReviews !== undefined) {
|
|
53
|
+
prependConnection(dest.approvedReviews, pr2.approvedReviews, pr);
|
|
54
|
+
next.approvedReviews = backwardCursor(pr2.approvedReviews);
|
|
55
|
+
}
|
|
56
|
+
if (cursors.checks !== undefined) {
|
|
57
|
+
nextPageCount++;
|
|
58
|
+
const extra = takeCheckPage(pr2, firstOid, nextPageCount);
|
|
59
|
+
dest.checks.push(...extra.nodes);
|
|
60
|
+
next.checks = forwardCursor(extra);
|
|
61
|
+
}
|
|
62
|
+
return { cursors: next, pageCount: nextPageCount };
|
|
63
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type RateLimitInfo, type RepoInfo } from "./client.mts";
|
|
2
|
+
import { type PageAccumulator } from "./batch-page-helpers.mts";
|
|
3
|
+
import type { RawPr } from "./batch-raw-types.mts";
|
|
4
|
+
export interface PaginatedBatchNodes extends PageAccumulator {
|
|
5
|
+
rateLimit?: RateLimitInfo;
|
|
6
|
+
}
|
|
7
|
+
export interface PaginateBatchOptions {
|
|
8
|
+
paginateApprovedReviews?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Fetch remaining connection pages with a slim `@include` query. Independent
|
|
12
|
+
* connections that still have cursors are combined into one request per round.
|
|
13
|
+
*/
|
|
14
|
+
export declare function paginateBatchConnections(pr: number, repo: RepoInfo, raw: RawPr, opts: PaginateBatchOptions, initialRateLimit?: RateLimitInfo): Promise<PaginatedBatchNodes>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { graphqlWithRateLimit } from "./client.mjs";
|
|
2
|
+
import { GitHubRequestError } from "./errors.mjs";
|
|
3
|
+
import { BATCH_PR_PAGE_QUERY } from "./queries.mjs";
|
|
4
|
+
import { requireContextNodes } from "./batch-response.mjs";
|
|
5
|
+
import { applyIncludedPage, backwardCursor, forwardCursor, } from "./batch-page-helpers.mjs";
|
|
6
|
+
/**
|
|
7
|
+
* Fetch remaining connection pages with a slim `@include` query. Independent
|
|
8
|
+
* connections that still have cursors are combined into one request per round.
|
|
9
|
+
*/
|
|
10
|
+
export async function paginateBatchConnections(pr, repo, raw, opts, initialRateLimit) {
|
|
11
|
+
const dest = {
|
|
12
|
+
threads: [...raw.reviewThreads.nodes],
|
|
13
|
+
comments: [...raw.comments.nodes],
|
|
14
|
+
changesRequested: [...raw.changesRequestedReviews.nodes],
|
|
15
|
+
reviewSummaries: [...raw.reviewSummaries.nodes],
|
|
16
|
+
approvedReviews: [...raw.approvedReviews.nodes],
|
|
17
|
+
checks: [
|
|
18
|
+
...requireContextNodes(raw.commits.nodes[0]?.commit.statusCheckRollup?.contexts.nodes ?? []),
|
|
19
|
+
],
|
|
20
|
+
};
|
|
21
|
+
const firstOid = raw.commits.nodes[0]?.commit.oid;
|
|
22
|
+
let cursors = {
|
|
23
|
+
threads: backwardCursor(raw.reviewThreads),
|
|
24
|
+
comments: backwardCursor(raw.comments),
|
|
25
|
+
changesRequested: backwardCursor(raw.changesRequestedReviews),
|
|
26
|
+
reviewSummaries: backwardCursor(raw.reviewSummaries),
|
|
27
|
+
approvedReviews: opts.paginateApprovedReviews === true ? backwardCursor(raw.approvedReviews) : undefined,
|
|
28
|
+
checks: forwardCursor(raw.commits.nodes[0]?.commit.statusCheckRollup?.contexts),
|
|
29
|
+
};
|
|
30
|
+
let rateLimit = initialRateLimit;
|
|
31
|
+
let pageCount = 0;
|
|
32
|
+
while (Object.values(cursors).some((cursor) => cursor !== undefined)) {
|
|
33
|
+
if (rateLimit?.remaining === 0) {
|
|
34
|
+
throw new GitHubRequestError("GitHub GraphQL rate limit remaining is 0; pagination incomplete", { status: 403, rateLimit });
|
|
35
|
+
}
|
|
36
|
+
const res = await graphqlWithRateLimit(BATCH_PR_PAGE_QUERY, {
|
|
37
|
+
owner: repo.owner,
|
|
38
|
+
repo: repo.name,
|
|
39
|
+
pr,
|
|
40
|
+
includeThreads: cursors.threads !== undefined,
|
|
41
|
+
threadsCursor: cursors.threads ?? null,
|
|
42
|
+
includeComments: cursors.comments !== undefined,
|
|
43
|
+
commentsCursor: cursors.comments ?? null,
|
|
44
|
+
includeChangesRequested: cursors.changesRequested !== undefined,
|
|
45
|
+
changesRequestedCursor: cursors.changesRequested ?? null,
|
|
46
|
+
includeReviewSummaries: cursors.reviewSummaries !== undefined,
|
|
47
|
+
reviewSummariesCursor: cursors.reviewSummaries ?? null,
|
|
48
|
+
includeApprovedReviews: cursors.approvedReviews !== undefined,
|
|
49
|
+
approvedReviewsCursor: cursors.approvedReviews ?? null,
|
|
50
|
+
includeChecks: cursors.checks !== undefined,
|
|
51
|
+
checksCursor: cursors.checks ?? null,
|
|
52
|
+
});
|
|
53
|
+
rateLimit = res.rateLimit ?? rateLimit;
|
|
54
|
+
const pr2 = res.data.repository.pullRequest;
|
|
55
|
+
if (!pr2)
|
|
56
|
+
throw new Error(`PR #${pr} not found`);
|
|
57
|
+
const applied = applyIncludedPage(dest, pr2, cursors, firstOid, pageCount, pr);
|
|
58
|
+
cursors = applied.cursors;
|
|
59
|
+
pageCount = applied.pageCount;
|
|
60
|
+
}
|
|
61
|
+
return { ...dest, rateLimit };
|
|
62
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function parseCheckSuitesComplete(raw) {
|
|
2
|
+
return raw.commits.nodes[0]?.commit.checkSuites?.pageInfo.hasNextPage === false;
|
|
3
|
+
}
|
|
4
|
+
export function parseSuiteStartupFailures(raw) {
|
|
5
|
+
const suites = raw.commits.nodes[0]?.commit.checkSuites;
|
|
6
|
+
if (!suites)
|
|
7
|
+
return [];
|
|
8
|
+
return suites.nodes.flatMap((node) => {
|
|
9
|
+
if (node.conclusion !== "STARTUP_FAILURE" || node.workflowRun == null)
|
|
10
|
+
return [];
|
|
11
|
+
const runId = node.workflowRun.databaseId != null ? String(node.workflowRun.databaseId) : null;
|
|
12
|
+
const name = node.workflowRun.workflow?.name?.trim() || (runId ? `workflow run ${runId}` : "workflow run");
|
|
13
|
+
return [
|
|
14
|
+
{
|
|
15
|
+
name,
|
|
16
|
+
status: "COMPLETED",
|
|
17
|
+
conclusion: "STARTUP_FAILURE",
|
|
18
|
+
source: "startup_failure",
|
|
19
|
+
detailsUrl: node.workflowRun.url ?? "",
|
|
20
|
+
event: node.workflowRun.event,
|
|
21
|
+
runId,
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -45,6 +45,7 @@ export function mapCheckRunNode(node) {
|
|
|
45
45
|
...(completedAtUnix !== undefined && { completedAtUnix }),
|
|
46
46
|
...(updatedAtUnix !== undefined && { updatedAtUnix }),
|
|
47
47
|
...(summary !== undefined && { summary }),
|
|
48
|
+
...((node.annotations?.nodes.length ?? 0) > 0 && { hasAnnotations: true }),
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
51
|
function extractCheckRunSummary(title, summary) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { BranchRules, MergeQueueEntryStatus, StackStatus } from "../types.mts";
|
|
2
|
+
import type { RawBaseRef, RawPrMergeFields } from "./batch-raw-rules.mts";
|
|
3
|
+
export declare const EMPTY_BRANCH_RULES: BranchRules;
|
|
4
|
+
export declare function parseBranchRules(baseRef: RawBaseRef | null | undefined): BranchRules;
|
|
5
|
+
export declare function parseMergeQueueEntry(raw: RawPrMergeFields["mergeQueueEntry"]): MergeQueueEntryStatus | null;
|
|
6
|
+
export declare function parseStack(raw: RawPrMergeFields): StackStatus | null;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
export const EMPTY_BRANCH_RULES = {
|
|
2
|
+
requiredApprovingReviewCount: 0,
|
|
3
|
+
requiresConversationResolution: false,
|
|
4
|
+
requiresCodeOwnerReviews: false,
|
|
5
|
+
requiresLastPushApproval: false,
|
|
6
|
+
requiresCommitSignatures: false,
|
|
7
|
+
requiresLinearHistory: false,
|
|
8
|
+
requiresStrictStatusChecks: false,
|
|
9
|
+
requiredStatusCheckContexts: [],
|
|
10
|
+
requiredDeploymentEnvironments: [],
|
|
11
|
+
requiresMergeQueue: false,
|
|
12
|
+
requiresWorkflows: false,
|
|
13
|
+
requiresCodeScanning: false,
|
|
14
|
+
};
|
|
15
|
+
export function parseBranchRules(baseRef) {
|
|
16
|
+
const rules = {
|
|
17
|
+
...EMPTY_BRANCH_RULES,
|
|
18
|
+
requiredStatusCheckContexts: [],
|
|
19
|
+
requiredDeploymentEnvironments: [],
|
|
20
|
+
};
|
|
21
|
+
if (!baseRef)
|
|
22
|
+
return rules;
|
|
23
|
+
foldClassicProtection(rules, baseRef.branchProtectionRule);
|
|
24
|
+
for (const node of baseRef.rules?.nodes ?? [])
|
|
25
|
+
foldRulesetRule(rules, node);
|
|
26
|
+
return rules;
|
|
27
|
+
}
|
|
28
|
+
export function parseMergeQueueEntry(raw) {
|
|
29
|
+
if (!raw)
|
|
30
|
+
return null;
|
|
31
|
+
return {
|
|
32
|
+
position: raw.position,
|
|
33
|
+
state: raw.state,
|
|
34
|
+
estimatedTimeToMerge: raw.estimatedTimeToMerge,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function parseStack(raw) {
|
|
38
|
+
if (!raw.stack)
|
|
39
|
+
return null;
|
|
40
|
+
return {
|
|
41
|
+
number: raw.stack.number,
|
|
42
|
+
size: raw.stack.size,
|
|
43
|
+
position: raw.stackEntry?.position ?? 0,
|
|
44
|
+
baseRefName: raw.stack.baseRefName,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function foldClassicProtection(rules, bp) {
|
|
48
|
+
if (!bp)
|
|
49
|
+
return;
|
|
50
|
+
if (bp.requiresApprovingReviews) {
|
|
51
|
+
rules.requiredApprovingReviewCount = Math.max(rules.requiredApprovingReviewCount, bp.requiredApprovingReviewCount ?? 0);
|
|
52
|
+
}
|
|
53
|
+
if (bp.requiresConversationResolution)
|
|
54
|
+
rules.requiresConversationResolution = true;
|
|
55
|
+
if (bp.requiresCodeOwnerReviews)
|
|
56
|
+
rules.requiresCodeOwnerReviews = true;
|
|
57
|
+
if (bp.requireLastPushApproval)
|
|
58
|
+
rules.requiresLastPushApproval = true;
|
|
59
|
+
if (bp.requiresCommitSignatures)
|
|
60
|
+
rules.requiresCommitSignatures = true;
|
|
61
|
+
if (bp.requiresLinearHistory)
|
|
62
|
+
rules.requiresLinearHistory = true;
|
|
63
|
+
if (bp.requiresStrictStatusChecks)
|
|
64
|
+
rules.requiresStrictStatusChecks = true;
|
|
65
|
+
if (bp.requiresStatusChecks) {
|
|
66
|
+
mergeUnique(rules.requiredStatusCheckContexts, bp.requiredStatusCheckContexts ?? []);
|
|
67
|
+
}
|
|
68
|
+
if (bp.requiresDeployments) {
|
|
69
|
+
mergeUnique(rules.requiredDeploymentEnvironments, bp.requiredDeploymentEnvironments ?? []);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function foldRulesetRule(rules, node) {
|
|
73
|
+
const p = node.parameters;
|
|
74
|
+
switch (node.type) {
|
|
75
|
+
case "PULL_REQUEST":
|
|
76
|
+
if (p?.requiredApprovingReviewCount != null) {
|
|
77
|
+
rules.requiredApprovingReviewCount = Math.max(rules.requiredApprovingReviewCount, p.requiredApprovingReviewCount);
|
|
78
|
+
}
|
|
79
|
+
if (p?.requiredReviewThreadResolution)
|
|
80
|
+
rules.requiresConversationResolution = true;
|
|
81
|
+
if (p?.requireCodeOwnerReview)
|
|
82
|
+
rules.requiresCodeOwnerReviews = true;
|
|
83
|
+
if (p?.requireLastPushApproval)
|
|
84
|
+
rules.requiresLastPushApproval = true;
|
|
85
|
+
break;
|
|
86
|
+
case "REQUIRED_REVIEW_THREAD_RESOLUTION":
|
|
87
|
+
rules.requiresConversationResolution = true;
|
|
88
|
+
break;
|
|
89
|
+
case "REQUIRED_STATUS_CHECKS":
|
|
90
|
+
if (p?.strictRequiredStatusChecksPolicy)
|
|
91
|
+
rules.requiresStrictStatusChecks = true;
|
|
92
|
+
mergeUnique(rules.requiredStatusCheckContexts, (p?.requiredStatusChecks ?? []).map((c) => c.context));
|
|
93
|
+
break;
|
|
94
|
+
case "REQUIRED_SIGNATURES":
|
|
95
|
+
rules.requiresCommitSignatures = true;
|
|
96
|
+
break;
|
|
97
|
+
case "REQUIRED_LINEAR_HISTORY":
|
|
98
|
+
rules.requiresLinearHistory = true;
|
|
99
|
+
break;
|
|
100
|
+
case "REQUIRED_DEPLOYMENTS":
|
|
101
|
+
mergeUnique(rules.requiredDeploymentEnvironments, p?.requiredDeploymentEnvironments ?? []);
|
|
102
|
+
break;
|
|
103
|
+
case "MERGE_QUEUE":
|
|
104
|
+
rules.requiresMergeQueue = true;
|
|
105
|
+
break;
|
|
106
|
+
case "WORKFLOWS":
|
|
107
|
+
case "REQUIRED_WORKFLOW_STATUS_CHECKS":
|
|
108
|
+
rules.requiresWorkflows = true;
|
|
109
|
+
break;
|
|
110
|
+
case "CODE_SCANNING":
|
|
111
|
+
rules.requiresCodeScanning = true;
|
|
112
|
+
break;
|
|
113
|
+
default:
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function mergeUnique(target, values) {
|
|
118
|
+
for (const value of values) {
|
|
119
|
+
if (value && !target.includes(value))
|
|
120
|
+
target.push(value);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mapAuthorType, parseCreatedAt, mapStatusContextState, latestApprovedLogins, isReviewStale, mapCheckRunNode, } from "./batch-parser-helpers.mjs";
|
|
2
2
|
import { buildPrActivitySummary } from "./activity.mjs";
|
|
3
3
|
import { parseBranchProtection } from "./branch-protection.mjs";
|
|
4
|
+
import { parseBranchRules, parseMergeQueueEntry, parseStack } from "./batch-parsers-rules.mjs";
|
|
4
5
|
function parseReviewNode(r) {
|
|
5
6
|
const base = {
|
|
6
7
|
id: r.id,
|
|
@@ -130,6 +131,11 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
130
131
|
approvedReviews,
|
|
131
132
|
checks,
|
|
132
133
|
branchProtection: parseBranchProtection(raw),
|
|
134
|
+
branchRules: parseBranchRules(raw.baseRef),
|
|
135
|
+
isInMergeQueue: raw.isInMergeQueue ?? false,
|
|
136
|
+
isMergeQueueEnabled: raw.isMergeQueueEnabled ?? false,
|
|
137
|
+
mergeQueueEntry: parseMergeQueueEntry(raw.mergeQueueEntry),
|
|
138
|
+
stack: parseStack(raw),
|
|
133
139
|
activity: buildPrActivitySummary(raw, comments, reviewThreads, reviewSummaries, allChangesRequestedReviews, approvedReviews),
|
|
134
140
|
};
|
|
135
141
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export interface RawBranchProtectionRule {
|
|
2
|
+
requiresApprovingReviews: boolean;
|
|
3
|
+
requiredApprovingReviewCount: number;
|
|
4
|
+
requiresConversationResolution: boolean;
|
|
5
|
+
requiresStatusChecks: boolean;
|
|
6
|
+
requiredStatusCheckContexts: string[] | null;
|
|
7
|
+
requiresCodeOwnerReviews?: boolean;
|
|
8
|
+
requireLastPushApproval?: boolean;
|
|
9
|
+
requiresCommitSignatures?: boolean;
|
|
10
|
+
requiresLinearHistory?: boolean;
|
|
11
|
+
requiresStrictStatusChecks?: boolean;
|
|
12
|
+
requiresDeployments?: boolean;
|
|
13
|
+
requiredDeploymentEnvironments?: string[] | null;
|
|
14
|
+
}
|
|
15
|
+
interface RawMergeQueueEntry {
|
|
16
|
+
position: number;
|
|
17
|
+
state: string;
|
|
18
|
+
estimatedTimeToMerge: number | null;
|
|
19
|
+
}
|
|
20
|
+
interface RawStack {
|
|
21
|
+
number: number;
|
|
22
|
+
size: number;
|
|
23
|
+
baseRefName: string;
|
|
24
|
+
}
|
|
25
|
+
export interface RawRepositoryRule {
|
|
26
|
+
type: string;
|
|
27
|
+
parameters: RawRuleParameters | null;
|
|
28
|
+
}
|
|
29
|
+
interface RawRuleParameters {
|
|
30
|
+
requiredApprovingReviewCount?: number;
|
|
31
|
+
requiredReviewThreadResolution?: boolean;
|
|
32
|
+
requireCodeOwnerReview?: boolean;
|
|
33
|
+
requireLastPushApproval?: boolean;
|
|
34
|
+
strictRequiredStatusChecksPolicy?: boolean;
|
|
35
|
+
requiredStatusChecks?: Array<{
|
|
36
|
+
context: string;
|
|
37
|
+
}>;
|
|
38
|
+
requiredDeploymentEnvironments?: string[];
|
|
39
|
+
codeScanningTools?: Array<{
|
|
40
|
+
tool: string;
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
export interface RawBaseRef {
|
|
44
|
+
branchProtectionRule: RawBranchProtectionRule | null;
|
|
45
|
+
rules: {
|
|
46
|
+
nodes: RawRepositoryRule[];
|
|
47
|
+
} | null;
|
|
48
|
+
}
|
|
49
|
+
export interface RawPrMergeFields {
|
|
50
|
+
isInMergeQueue?: boolean;
|
|
51
|
+
isMergeQueueEnabled?: boolean;
|
|
52
|
+
mergeQueueEntry?: RawMergeQueueEntry | null;
|
|
53
|
+
stack?: RawStack | null;
|
|
54
|
+
stackEntry?: {
|
|
55
|
+
position: number;
|
|
56
|
+
} | null;
|
|
57
|
+
baseRef?: RawBaseRef | null;
|
|
58
|
+
}
|
|
59
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { CommentAuthorAssociation } from "../types/github.mts";
|
|
2
|
+
import type { RawPrMergeFields } from "./batch-raw-rules.mts";
|
|
2
3
|
export interface RawBatchResponse {
|
|
3
4
|
repository: {
|
|
4
5
|
pullRequest: RawPr | null;
|
|
5
6
|
} | null;
|
|
6
7
|
}
|
|
7
|
-
export interface RawPr {
|
|
8
|
+
export interface RawPr extends RawPrMergeFields {
|
|
8
9
|
id: string;
|
|
9
10
|
number: number;
|
|
10
11
|
state: string;
|
|
@@ -18,9 +19,7 @@ export interface RawPr {
|
|
|
18
19
|
nameWithOwner: string;
|
|
19
20
|
} | null;
|
|
20
21
|
baseRefName: string;
|
|
21
|
-
baseRef:
|
|
22
|
-
branchProtectionRule: RawBranchProtectionRule | null;
|
|
23
|
-
} | null;
|
|
22
|
+
baseRef: import("./batch-raw-rules.mts").RawBaseRef | null;
|
|
24
23
|
reviewRequests: {
|
|
25
24
|
nodes: Array<{
|
|
26
25
|
requestedReviewer: {
|
|
@@ -79,6 +78,7 @@ export interface RawPr {
|
|
|
79
78
|
commit: {
|
|
80
79
|
oid: string;
|
|
81
80
|
committedDate?: string;
|
|
81
|
+
checkSuites?: RawCheckSuites;
|
|
82
82
|
statusCheckRollup: {
|
|
83
83
|
contexts: {
|
|
84
84
|
pageInfo: {
|
|
@@ -92,6 +92,22 @@ export interface RawPr {
|
|
|
92
92
|
}>;
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
|
+
interface RawCheckSuites {
|
|
96
|
+
pageInfo: {
|
|
97
|
+
hasNextPage: boolean;
|
|
98
|
+
};
|
|
99
|
+
nodes: Array<{
|
|
100
|
+
conclusion: string | null;
|
|
101
|
+
workflowRun: {
|
|
102
|
+
databaseId: number | null;
|
|
103
|
+
event: string | null;
|
|
104
|
+
url: string | null;
|
|
105
|
+
workflow: {
|
|
106
|
+
name: string | null;
|
|
107
|
+
} | null;
|
|
108
|
+
} | null;
|
|
109
|
+
}>;
|
|
110
|
+
}
|
|
95
111
|
interface RawAuthor {
|
|
96
112
|
__typename?: string;
|
|
97
113
|
login: string;
|
|
@@ -165,13 +181,6 @@ export interface RawReviewSummary {
|
|
|
165
181
|
body: string;
|
|
166
182
|
createdAt?: string;
|
|
167
183
|
}
|
|
168
|
-
interface RawBranchProtectionRule {
|
|
169
|
-
requiresApprovingReviews: boolean;
|
|
170
|
-
requiredApprovingReviewCount: number;
|
|
171
|
-
requiresConversationResolution: boolean;
|
|
172
|
-
requiresStatusChecks: boolean;
|
|
173
|
-
requiredStatusCheckContexts: string[] | null;
|
|
174
|
-
}
|
|
175
184
|
export type RawContextNode = {
|
|
176
185
|
__typename: "CheckRun";
|
|
177
186
|
id: string;
|
|
@@ -183,6 +192,11 @@ export type RawContextNode = {
|
|
|
183
192
|
startedAt?: string | null;
|
|
184
193
|
title: string | null;
|
|
185
194
|
summary: string | null;
|
|
195
|
+
annotations?: {
|
|
196
|
+
nodes: Array<{
|
|
197
|
+
message: string;
|
|
198
|
+
}>;
|
|
199
|
+
};
|
|
186
200
|
checkSuite: {
|
|
187
201
|
createdAt?: string;
|
|
188
202
|
updatedAt?: string;
|
package/bin/github/batch.d.mts
CHANGED
|
@@ -3,6 +3,8 @@ import type { BatchPrData } from "../types.mts";
|
|
|
3
3
|
interface BatchResult {
|
|
4
4
|
data: BatchPrData;
|
|
5
5
|
rateLimit?: RateLimitInfo;
|
|
6
|
+
/** True when GraphQL returned a complete CheckSuite page; skip REST startup-failure fetch. */
|
|
7
|
+
checkSuitesComplete?: boolean;
|
|
6
8
|
}
|
|
7
9
|
interface FetchPrBatchOptions {
|
|
8
10
|
/**
|