pr-shepherd 0.2.0 → 0.4.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/marketplace.json +18 -0
- package/.claude-plugin/plugin.json +8 -2
- package/README.md +128 -83
- package/bin/cache/file-cache.mjs +79 -0
- package/bin/cache/fix-attempts.mjs +67 -0
- package/bin/checks/classify.mjs +53 -0
- package/bin/checks/triage.mjs +77 -0
- package/bin/cli/args.mjs +173 -0
- package/bin/cli.mjs +204 -0
- package/bin/commands/check.mjs +140 -0
- package/bin/commands/iterate.mjs +301 -0
- package/bin/commands/ready-delay.mjs +87 -0
- package/bin/commands/resolve.mjs +64 -0
- package/bin/commands/status.mjs +107 -0
- package/{src/comments/outdated.mts → bin/comments/outdated.mjs} +2 -5
- package/bin/comments/resolve.mjs +111 -0
- package/bin/config/load.mjs +158 -0
- package/bin/github/batch.mjs +208 -0
- package/bin/github/client.mjs +152 -0
- package/{src/github/pagination.mts → bin/github/pagination.mjs} +26 -52
- package/{src/github/queries.mts → bin/github/queries.mjs} +1 -10
- package/{src/index.mts → bin/index.mjs} +3 -5
- package/bin/merge-status/derive.mjs +72 -0
- package/bin/pr-shepherd +2 -0
- package/bin/reporters/agent.mjs +41 -0
- package/{src/reporters/json.mts → bin/reporters/json.mjs} +2 -5
- package/bin/reporters/text.mjs +111 -0
- package/bin/types.mjs +2 -0
- package/package.json +9 -9
- package/skills/check/SKILL.md +12 -14
- package/skills/monitor/SKILL.md +9 -5
- package/src/cache/file-cache.mts +0 -101
- package/src/cache/file-cache.test.mts +0 -91
- package/src/cache/fix-attempts.mts +0 -86
- package/src/checks/classify.mts +0 -80
- package/src/checks/classify.test.mts +0 -164
- package/src/checks/triage.mock.test.mts +0 -202
- package/src/checks/triage.mts +0 -88
- package/src/cli.mts +0 -423
- package/src/commands/check.mts +0 -188
- package/src/commands/iterate.mock.test.mts +0 -1111
- package/src/commands/iterate.mts +0 -371
- package/src/commands/ready-delay.mts +0 -117
- package/src/commands/ready-delay.test.mts +0 -116
- package/src/commands/resolve.mts +0 -92
- package/src/commands/status.mts +0 -173
- package/src/comments/resolve.mts +0 -179
- package/src/config/load.mts +0 -240
- package/src/github/batch.mts +0 -351
- package/src/github/client.mts +0 -207
- package/src/github/client.test.mts +0 -19
- package/src/github/pagination.test.mts +0 -140
- package/src/merge-status/derive.mts +0 -74
- package/src/merge-status/derive.test.mts +0 -130
- package/src/reporters/text.mts +0 -140
- package/src/types.mts +0 -309
- /package/{src → bin}/config.json +0 -0
- /package/{src → bin}/github/gql/batch-pr.gql +0 -0
- /package/{src → bin}/github/gql/dismiss-review.gql +0 -0
- /package/{src → bin}/github/gql/minimize-comment.gql +0 -0
- /package/{src → bin}/github/gql/multi-pr-status-paged.gql +0 -0
- /package/{src → bin}/github/gql/multi-pr-status.gql +0 -0
- /package/{src → bin}/github/gql/resolve-thread.gql +0 -0
- /package/{src/util/path-segment.mts → bin/util/path-segment.mjs} +0 -0
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { paginateForward, paginateBackward, type Connection } from "./pagination.mts";
|
|
3
|
-
|
|
4
|
-
// ---------------------------------------------------------------------------
|
|
5
|
-
// paginateForward
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
|
|
8
|
-
describe("paginateForward", () => {
|
|
9
|
-
it("collects all nodes across three pages", async () => {
|
|
10
|
-
const pages: Connection<string>[] = [
|
|
11
|
-
{ pageInfo: { hasNextPage: true, endCursor: "cursor1" }, nodes: ["a", "b"] },
|
|
12
|
-
{ pageInfo: { hasNextPage: true, endCursor: "cursor2" }, nodes: ["c"] },
|
|
13
|
-
{ pageInfo: { hasNextPage: false, endCursor: null }, nodes: ["d", "e"] },
|
|
14
|
-
];
|
|
15
|
-
const cursors: Array<string | null> = [];
|
|
16
|
-
let i = 0;
|
|
17
|
-
|
|
18
|
-
const result = await paginateForward((cursor) => {
|
|
19
|
-
cursors.push(cursor);
|
|
20
|
-
return Promise.resolve(pages[i++]!);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
expect(result).toEqual(["a", "b", "c", "d", "e"]);
|
|
24
|
-
expect(cursors).toEqual([null, "cursor1", "cursor2"]);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("stops after a single page when hasNextPage is false", async () => {
|
|
28
|
-
let calls = 0;
|
|
29
|
-
const result = await paginateForward(() => {
|
|
30
|
-
calls++;
|
|
31
|
-
return Promise.resolve({
|
|
32
|
-
pageInfo: { hasNextPage: false, endCursor: null },
|
|
33
|
-
nodes: ["x", "y"],
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
expect(result).toEqual(["x", "y"]);
|
|
38
|
-
expect(calls).toBe(1);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("returns an empty array for an empty first page", async () => {
|
|
42
|
-
const result = await paginateForward(() =>
|
|
43
|
-
Promise.resolve({
|
|
44
|
-
pageInfo: { hasNextPage: false, endCursor: null },
|
|
45
|
-
nodes: [] as string[],
|
|
46
|
-
}),
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
expect(result).toEqual([]);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("starts from initialCursor to avoid re-fetching the already-known page", async () => {
|
|
53
|
-
// Simulates the batch.mts use case: the initial query already returned page
|
|
54
|
-
// ending at 'cur-first'. paginateForward should start from that endCursor
|
|
55
|
-
// so it only fetches pages *after* it.
|
|
56
|
-
const pages: Record<string, Connection<string>> = {
|
|
57
|
-
"cur-first": {
|
|
58
|
-
pageInfo: { hasNextPage: true, endCursor: "cur-second" },
|
|
59
|
-
nodes: ["c", "d"],
|
|
60
|
-
},
|
|
61
|
-
"cur-second": {
|
|
62
|
-
pageInfo: { hasNextPage: false, endCursor: null },
|
|
63
|
-
nodes: ["e"],
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
const cursors: Array<string | null> = [];
|
|
67
|
-
|
|
68
|
-
const result = await paginateForward((cursor) => {
|
|
69
|
-
cursors.push(cursor);
|
|
70
|
-
return Promise.resolve(pages[cursor ?? ""]!);
|
|
71
|
-
}, "cur-first");
|
|
72
|
-
|
|
73
|
-
// Should fetch pages after 'cur-first', not re-fetch it.
|
|
74
|
-
expect(cursors).toEqual(["cur-first", "cur-second"]);
|
|
75
|
-
expect(result).toEqual(["c", "d", "e"]);
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// ---------------------------------------------------------------------------
|
|
80
|
-
// paginateBackward
|
|
81
|
-
// ---------------------------------------------------------------------------
|
|
82
|
-
|
|
83
|
-
describe("paginateBackward", () => {
|
|
84
|
-
it("collects nodes across pages and returns oldest-first", async () => {
|
|
85
|
-
// Backward pagination: newest page first, oldest last.
|
|
86
|
-
const pages: Connection<string>[] = [
|
|
87
|
-
{ pageInfo: { hasPreviousPage: true, startCursor: "cur1" }, nodes: ["newer", "newest"] },
|
|
88
|
-
{ pageInfo: { hasPreviousPage: true, startCursor: "cur2" }, nodes: ["older"] },
|
|
89
|
-
{ pageInfo: { hasPreviousPage: false, startCursor: null }, nodes: ["oldest"] },
|
|
90
|
-
];
|
|
91
|
-
const cursors: Array<string | null> = [];
|
|
92
|
-
let i = 0;
|
|
93
|
-
|
|
94
|
-
const result = await paginateBackward((cursor) => {
|
|
95
|
-
cursors.push(cursor);
|
|
96
|
-
return Promise.resolve(pages[i++]!);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// unshift inserts older pages at the front.
|
|
100
|
-
expect(result).toEqual(["oldest", "older", "newer", "newest"]);
|
|
101
|
-
expect(cursors).toEqual([null, "cur1", "cur2"]);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("returns single-page nodes unchanged", async () => {
|
|
105
|
-
const result = await paginateBackward(() =>
|
|
106
|
-
Promise.resolve({
|
|
107
|
-
pageInfo: { hasPreviousPage: false, startCursor: null },
|
|
108
|
-
nodes: ["a", "b"],
|
|
109
|
-
}),
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
expect(result).toEqual(["a", "b"]);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("starts from initialCursor to avoid re-fetching the already-known page", async () => {
|
|
116
|
-
// Simulates the batch.mts use case: the initial query already returned the
|
|
117
|
-
// "newest" page (cur-newest). paginateBackward should start from that
|
|
118
|
-
// startCursor so it only fetches pages *before* it.
|
|
119
|
-
const pages: Record<string, Connection<string>> = {
|
|
120
|
-
"cur-newest": {
|
|
121
|
-
pageInfo: { hasPreviousPage: true, startCursor: "cur-middle" },
|
|
122
|
-
nodes: ["middle"],
|
|
123
|
-
},
|
|
124
|
-
"cur-middle": {
|
|
125
|
-
pageInfo: { hasPreviousPage: false, startCursor: null },
|
|
126
|
-
nodes: ["oldest"],
|
|
127
|
-
},
|
|
128
|
-
};
|
|
129
|
-
const cursors: Array<string | null> = [];
|
|
130
|
-
|
|
131
|
-
const result = await paginateBackward((cursor) => {
|
|
132
|
-
cursors.push(cursor);
|
|
133
|
-
return Promise.resolve(pages[cursor ?? ""]!);
|
|
134
|
-
}, "cur-newest");
|
|
135
|
-
|
|
136
|
-
// Should fetch pages before 'cur-newest', not re-fetch 'cur-newest' itself.
|
|
137
|
-
expect(cursors).toEqual(["cur-newest", "cur-middle"]);
|
|
138
|
-
expect(result).toEqual(["oldest", "middle"]);
|
|
139
|
-
});
|
|
140
|
-
});
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Derives a shepherd `MergeStatusResult` from raw PR data.
|
|
3
|
-
*
|
|
4
|
-
* `pr.state` is passed through unchanged; `iterate` handles the cancel action
|
|
5
|
-
* for non-OPEN (merged/closed) PRs — this function does not branch on it.
|
|
6
|
-
*
|
|
7
|
-
* Interpretation order for `status` — first match wins:
|
|
8
|
-
* 1. mergeable == CONFLICTING → CONFLICTS
|
|
9
|
-
* 2. mergeStateStatus DIRTY → CONFLICTS (GitHub merge conflicts)
|
|
10
|
-
* 3. copilotReviewInProgress → BLOCKED
|
|
11
|
-
* 4. mergeStateStatus BEHIND → BEHIND
|
|
12
|
-
* 5. mergeStateStatus BLOCKED / HAS_HOOKS → BLOCKED
|
|
13
|
-
* 6. mergeStateStatus UNSTABLE → UNSTABLE
|
|
14
|
-
* 7. isDraft → DRAFT
|
|
15
|
-
* 8. mergeStateStatus UNKNOWN → UNKNOWN
|
|
16
|
-
* 9. mergeStateStatus CLEAN → CLEAN
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import type { BatchPrData, MergeStatusResult } from "../types.mts";
|
|
20
|
-
import { loadConfig } from "../config/load.mts";
|
|
21
|
-
|
|
22
|
-
export function deriveMergeStatus(pr: BatchPrData): MergeStatusResult {
|
|
23
|
-
const copilotReviewInProgress = detectCopilotReview(pr);
|
|
24
|
-
|
|
25
|
-
let status: MergeStatusResult["status"];
|
|
26
|
-
|
|
27
|
-
if (pr.mergeable === "CONFLICTING") {
|
|
28
|
-
status = "CONFLICTS";
|
|
29
|
-
} else if (copilotReviewInProgress) {
|
|
30
|
-
status = "BLOCKED";
|
|
31
|
-
} else if (pr.mergeStateStatus === "DIRTY") {
|
|
32
|
-
// DIRTY means GitHub detected merge conflicts in the branch.
|
|
33
|
-
status = "CONFLICTS";
|
|
34
|
-
} else if (pr.mergeStateStatus === "BEHIND") {
|
|
35
|
-
status = "BEHIND";
|
|
36
|
-
} else if (pr.mergeStateStatus === "BLOCKED" || pr.mergeStateStatus === "HAS_HOOKS") {
|
|
37
|
-
status = "BLOCKED";
|
|
38
|
-
} else if (pr.mergeStateStatus === "UNSTABLE") {
|
|
39
|
-
status = "UNSTABLE";
|
|
40
|
-
} else if (pr.isDraft || pr.mergeStateStatus === "DRAFT") {
|
|
41
|
-
status = "DRAFT";
|
|
42
|
-
} else if (pr.mergeStateStatus === "UNKNOWN") {
|
|
43
|
-
status = "UNKNOWN";
|
|
44
|
-
} else {
|
|
45
|
-
status = "CLEAN";
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return {
|
|
49
|
-
status,
|
|
50
|
-
state: pr.state,
|
|
51
|
-
isDraft: pr.isDraft,
|
|
52
|
-
mergeable: pr.mergeable,
|
|
53
|
-
reviewDecision: pr.reviewDecision,
|
|
54
|
-
copilotReviewInProgress,
|
|
55
|
-
mergeStateStatus: pr.mergeStateStatus,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
// Copilot review detection
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
|
|
63
|
-
function detectCopilotReview(pr: BatchPrData): boolean {
|
|
64
|
-
// A blocking bot review is "in progress" when:
|
|
65
|
-
// 1. Any reviewRequest has a login starting with one of the configured prefixes, OR
|
|
66
|
-
// 2. Any latestReview has such a login AND state == "PENDING"
|
|
67
|
-
const prefixes = loadConfig().mergeStatus.blockingReviewerLogins.map((l) => l.toLowerCase());
|
|
68
|
-
const isBlocking = (login: string) => prefixes.some((p) => login.toLowerCase().startsWith(p));
|
|
69
|
-
|
|
70
|
-
const requested = pr.reviewRequests.some((r) => isBlocking(r.login));
|
|
71
|
-
const pendingReview = pr.latestReviews.some((r) => isBlocking(r.login) && r.state === "PENDING");
|
|
72
|
-
|
|
73
|
-
return requested || pendingReview;
|
|
74
|
-
}
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { deriveMergeStatus } from "./derive.mts";
|
|
3
|
-
import type { BatchPrData } from "../types.mts";
|
|
4
|
-
|
|
5
|
-
function makePr(overrides: Partial<BatchPrData>): BatchPrData {
|
|
6
|
-
return {
|
|
7
|
-
number: 42,
|
|
8
|
-
state: "OPEN",
|
|
9
|
-
isDraft: false,
|
|
10
|
-
mergeable: "MERGEABLE",
|
|
11
|
-
mergeStateStatus: "CLEAN",
|
|
12
|
-
reviewDecision: null,
|
|
13
|
-
headRefOid: "abc123",
|
|
14
|
-
reviewRequests: [],
|
|
15
|
-
latestReviews: [],
|
|
16
|
-
reviewThreads: [],
|
|
17
|
-
comments: [],
|
|
18
|
-
changesRequestedReviews: [],
|
|
19
|
-
checks: [],
|
|
20
|
-
...overrides,
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
// Interpretation order (first match wins)
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
|
|
28
|
-
describe("deriveMergeStatus", () => {
|
|
29
|
-
it("CONFLICTING mergeable → CONFLICTS", () => {
|
|
30
|
-
const result = deriveMergeStatus(makePr({ mergeable: "CONFLICTING" }));
|
|
31
|
-
expect(result.status).toBe("CONFLICTS");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("Copilot review requested → BLOCKED", () => {
|
|
35
|
-
const result = deriveMergeStatus(
|
|
36
|
-
makePr({ reviewRequests: [{ login: "copilot-pull-request-reviewer[bot]" }] }),
|
|
37
|
-
);
|
|
38
|
-
expect(result.status).toBe("BLOCKED");
|
|
39
|
-
expect(result.copilotReviewInProgress).toBe(true);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("Copilot review PENDING in latestReviews → BLOCKED", () => {
|
|
43
|
-
const result = deriveMergeStatus(
|
|
44
|
-
makePr({
|
|
45
|
-
latestReviews: [{ login: "copilot[bot]", state: "PENDING" }],
|
|
46
|
-
}),
|
|
47
|
-
);
|
|
48
|
-
expect(result.status).toBe("BLOCKED");
|
|
49
|
-
expect(result.copilotReviewInProgress).toBe(true);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("Copilot review APPROVED in latestReviews → not copilotInProgress", () => {
|
|
53
|
-
const result = deriveMergeStatus(
|
|
54
|
-
makePr({
|
|
55
|
-
latestReviews: [{ login: "copilot[bot]", state: "APPROVED" }],
|
|
56
|
-
}),
|
|
57
|
-
);
|
|
58
|
-
expect(result.copilotReviewInProgress).toBe(false);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("CONFLICTING takes priority over copilot blocked", () => {
|
|
62
|
-
const result = deriveMergeStatus(
|
|
63
|
-
makePr({
|
|
64
|
-
mergeable: "CONFLICTING",
|
|
65
|
-
reviewRequests: [{ login: "copilot[bot]" }],
|
|
66
|
-
}),
|
|
67
|
-
);
|
|
68
|
-
expect(result.status).toBe("CONFLICTS");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("BEHIND mergeStateStatus → BEHIND", () => {
|
|
72
|
-
const result = deriveMergeStatus(makePr({ mergeStateStatus: "BEHIND" }));
|
|
73
|
-
expect(result.status).toBe("BEHIND");
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("BLOCKED mergeStateStatus → BLOCKED", () => {
|
|
77
|
-
const result = deriveMergeStatus(makePr({ mergeStateStatus: "BLOCKED" }));
|
|
78
|
-
expect(result.status).toBe("BLOCKED");
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("UNSTABLE mergeStateStatus → UNSTABLE", () => {
|
|
82
|
-
const result = deriveMergeStatus(makePr({ mergeStateStatus: "UNSTABLE" }));
|
|
83
|
-
expect(result.status).toBe("UNSTABLE");
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("isDraft → DRAFT", () => {
|
|
87
|
-
const result = deriveMergeStatus(makePr({ isDraft: true }));
|
|
88
|
-
expect(result.status).toBe("DRAFT");
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("UNKNOWN mergeStateStatus → UNKNOWN", () => {
|
|
92
|
-
const result = deriveMergeStatus(makePr({ mergeStateStatus: "UNKNOWN" }));
|
|
93
|
-
expect(result.status).toBe("UNKNOWN");
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("CLEAN mergeStateStatus → CLEAN", () => {
|
|
97
|
-
const result = deriveMergeStatus(makePr({ mergeStateStatus: "CLEAN" }));
|
|
98
|
-
expect(result.status).toBe("CLEAN");
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("includes full detail fields in result", () => {
|
|
102
|
-
const result = deriveMergeStatus(
|
|
103
|
-
makePr({ reviewDecision: "CHANGES_REQUESTED", isDraft: false }),
|
|
104
|
-
);
|
|
105
|
-
expect(result.reviewDecision).toBe("CHANGES_REQUESTED");
|
|
106
|
-
expect(result.isDraft).toBe(false);
|
|
107
|
-
expect(result.mergeable).toBe("MERGEABLE");
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
describe("deriveMergeStatus — state pass-through", () => {
|
|
112
|
-
it("passes OPEN state through", () => {
|
|
113
|
-
const result = deriveMergeStatus(makePr({ state: "OPEN" }));
|
|
114
|
-
expect(result.state).toBe("OPEN");
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("passes MERGED state through", () => {
|
|
118
|
-
const result = deriveMergeStatus(
|
|
119
|
-
makePr({ state: "MERGED", mergeable: "UNKNOWN", mergeStateStatus: "UNKNOWN" }),
|
|
120
|
-
);
|
|
121
|
-
expect(result.state).toBe("MERGED");
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("passes CLOSED state through", () => {
|
|
125
|
-
const result = deriveMergeStatus(
|
|
126
|
-
makePr({ state: "CLOSED", mergeable: "UNKNOWN", mergeStateStatus: "UNKNOWN" }),
|
|
127
|
-
);
|
|
128
|
-
expect(result.state).toBe("CLOSED");
|
|
129
|
-
});
|
|
130
|
-
});
|
package/src/reporters/text.mts
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Human-readable text reporter for shepherd check output.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { ShepherdReport, TriagedCheck } from "../types.mts";
|
|
6
|
-
|
|
7
|
-
export function formatText(report: ShepherdReport): string {
|
|
8
|
-
const lines: string[] = [];
|
|
9
|
-
|
|
10
|
-
// Header
|
|
11
|
-
lines.push(`\nPR #${report.pr} — ${report.repo}`);
|
|
12
|
-
lines.push(`Status: ${report.status}`);
|
|
13
|
-
lines.push("");
|
|
14
|
-
|
|
15
|
-
// Merge status
|
|
16
|
-
const ms = report.mergeStatus;
|
|
17
|
-
lines.push(`Merge Status: ${ms.status}`);
|
|
18
|
-
lines.push(` mergeStateStatus: ${ms.mergeStateStatus}`);
|
|
19
|
-
lines.push(` mergeable: ${ms.mergeable}`);
|
|
20
|
-
lines.push(` reviewDecision: ${ms.reviewDecision ?? "(none)"}`);
|
|
21
|
-
lines.push(` isDraft: ${ms.isDraft}`);
|
|
22
|
-
lines.push(` copilotReviewInProgress:${ms.copilotReviewInProgress}`);
|
|
23
|
-
lines.push("");
|
|
24
|
-
|
|
25
|
-
// CI checks
|
|
26
|
-
const { passing, failing, inProgress, skipped } = report.checks;
|
|
27
|
-
const total = passing.length + failing.length + inProgress.length + skipped.length;
|
|
28
|
-
lines.push(`CI Checks: ${passing.length}/${total} passed`);
|
|
29
|
-
|
|
30
|
-
if (failing.length > 0) {
|
|
31
|
-
lines.push(`\nFailed Checks (${failing.length}):`);
|
|
32
|
-
for (const c of failing) {
|
|
33
|
-
const triaged = c as TriagedCheck;
|
|
34
|
-
const kind = triaged.failureKind ? ` [${triaged.failureKind}]` : "";
|
|
35
|
-
lines.push(` - ${c.name}${kind}: ${c.conclusion ?? c.status}`);
|
|
36
|
-
if (triaged.logExcerpt) {
|
|
37
|
-
lines.push(indent(triaged.logExcerpt.split("\n").slice(-10).join("\n"), " "));
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (inProgress.length > 0) {
|
|
43
|
-
lines.push(`\nIn Progress (${inProgress.length}):`);
|
|
44
|
-
for (const c of inProgress) {
|
|
45
|
-
lines.push(` - ${c.name}: ${c.status}`);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (skipped.length > 0) {
|
|
50
|
-
lines.push(`\nSkipped (${skipped.length}): ${skipped.map((c) => c.name).join(", ")}`);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (report.checks.filtered.length > 0) {
|
|
54
|
-
lines.push(
|
|
55
|
-
`\nFiltered (non-PR-trigger) (${report.checks.filtered.length}): ${report.checks.filtered.map((c) => c.name).join(", ")}`,
|
|
56
|
-
);
|
|
57
|
-
if (report.checks.blockedByFilteredCheck) {
|
|
58
|
-
lines.push(
|
|
59
|
-
" Note: PR is BLOCKED and all filtered checks are non-PR-trigger — one of these filtered checks may be a required status check blocking merge.",
|
|
60
|
-
);
|
|
61
|
-
} else if (report.mergeStatus.status === "BLOCKED") {
|
|
62
|
-
lines.push(
|
|
63
|
-
" Note: one or more of these filtered checks may be a required status check blocking merge.",
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
lines.push("");
|
|
69
|
-
|
|
70
|
-
// Review threads
|
|
71
|
-
const { actionable: actionableThreads, autoResolved, autoResolveErrors } = report.threads;
|
|
72
|
-
if (autoResolved.length > 0) {
|
|
73
|
-
lines.push(`Auto-resolved outdated threads (${autoResolved.length}):`);
|
|
74
|
-
for (const t of autoResolved) {
|
|
75
|
-
lines.push(` - threadId=${t.id} ${t.path ?? ""}:${t.line ?? "?"} (@${t.author})`);
|
|
76
|
-
}
|
|
77
|
-
lines.push("");
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (autoResolveErrors.length > 0) {
|
|
81
|
-
lines.push(`Auto-resolve errors (${autoResolveErrors.length}):`);
|
|
82
|
-
for (const e of autoResolveErrors) {
|
|
83
|
-
lines.push(` - ${e}`);
|
|
84
|
-
}
|
|
85
|
-
lines.push("");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (actionableThreads.length > 0) {
|
|
89
|
-
lines.push(`Actionable Review Threads (${actionableThreads.length}):`);
|
|
90
|
-
for (const t of actionableThreads) {
|
|
91
|
-
const label = t.path ? `${t.path}:${t.line ?? "?"}` : "(general)";
|
|
92
|
-
lines.push(` - threadId=${t.id} ${label} (@${t.author})`);
|
|
93
|
-
lines.push(` ${firstLine(t.body)}`);
|
|
94
|
-
}
|
|
95
|
-
lines.push("");
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// PR comments
|
|
99
|
-
const { actionable: actionableComments } = report.comments;
|
|
100
|
-
if (actionableComments.length > 0) {
|
|
101
|
-
lines.push(`Actionable PR Comments (${actionableComments.length}):`);
|
|
102
|
-
for (const c of actionableComments) {
|
|
103
|
-
lines.push(` - commentId=${c.id} (@${c.author}): ${firstLine(c.body)}`);
|
|
104
|
-
}
|
|
105
|
-
lines.push("");
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// CHANGES_REQUESTED reviews
|
|
109
|
-
if (report.changesRequestedReviews.length > 0) {
|
|
110
|
-
lines.push(`Pending CHANGES_REQUESTED reviews (${report.changesRequestedReviews.length}):`);
|
|
111
|
-
for (const r of report.changesRequestedReviews) {
|
|
112
|
-
lines.push(` - reviewId=${r.id} (@${r.author}): ${firstLine(r.body)}`);
|
|
113
|
-
}
|
|
114
|
-
lines.push("");
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Summary
|
|
118
|
-
const totalActionable =
|
|
119
|
-
actionableThreads.length + actionableComments.length + report.changesRequestedReviews.length;
|
|
120
|
-
lines.push(
|
|
121
|
-
`Summary: ${totalActionable === 0 ? "0 actionable — all threads resolved/minimized" : `${totalActionable} actionable item(s) remaining`}`,
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
return lines.join("\n");
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// ---------------------------------------------------------------------------
|
|
128
|
-
// Helpers
|
|
129
|
-
// ---------------------------------------------------------------------------
|
|
130
|
-
|
|
131
|
-
function firstLine(text: string): string {
|
|
132
|
-
return (text.split("\n")[0] ?? "").trim().slice(0, 120);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function indent(text: string, prefix: string): string {
|
|
136
|
-
return text
|
|
137
|
-
.split("\n")
|
|
138
|
-
.map((l) => prefix + l)
|
|
139
|
-
.join("\n");
|
|
140
|
-
}
|