@in-the-loop-labs/pair-review 3.5.2 → 3.7.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 +4 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/css/analysis-config.css +1807 -0
- package/public/css/pr.css +1029 -2169
- package/public/index.html +11 -0
- package/public/js/components/AIPanel.js +39 -23
- package/public/js/components/AdvancedConfigTab.js +56 -4
- package/public/js/components/AnalysisConfigModal.js +41 -25
- package/public/js/components/ChatPanel.js +163 -3
- package/public/js/components/KeyboardShortcuts.js +10 -26
- package/public/js/components/ReviewModal.js +135 -13
- package/public/js/components/TourBar.js +248 -0
- package/public/js/components/VoiceCentricConfigTab.js +36 -0
- package/public/js/index.js +175 -16
- package/public/js/local.js +64 -8
- package/public/js/modules/cancel-background-job.js +183 -0
- package/public/js/modules/hunk-summary-renderer.js +116 -0
- package/public/js/modules/storage-cleanup.js +16 -0
- package/public/js/modules/suggestion-manager.js +25 -1
- package/public/js/modules/tour-renderer.js +755 -0
- package/public/js/pr.js +1826 -56
- package/public/js/repo-links.js +328 -0
- package/public/js/utils/modal-detection.js +77 -0
- package/public/js/utils/provider-model.js +88 -0
- package/public/js/utils/storage-keys.js +50 -0
- package/public/local.html +24 -0
- package/public/pr.html +24 -0
- package/public/repo-settings.html +1 -0
- package/public/setup.html +2 -0
- package/src/ai/abort-signal-wiring.js +130 -0
- package/src/ai/analyzer.js +125 -18
- package/src/ai/background-queue.js +290 -0
- package/src/ai/claude-cli.js +1 -1
- package/src/ai/claude-provider.js +50 -7
- package/src/ai/codex-provider.js +28 -5
- package/src/ai/copilot-provider.js +22 -3
- package/src/ai/cursor-agent-provider.js +22 -6
- package/src/ai/executable-provider.js +4 -19
- package/src/ai/gemini-provider.js +22 -5
- package/src/ai/hunk-hashing.js +161 -0
- package/src/ai/index.js +2 -0
- package/src/ai/opencode-provider.js +21 -5
- package/src/ai/pi-provider.js +21 -5
- package/src/ai/prompts/hunk-summary.js +199 -0
- package/src/ai/prompts/tour.js +232 -0
- package/src/ai/provider.js +21 -1
- package/src/ai/summary-generator.js +469 -0
- package/src/ai/tour-generator.js +568 -0
- package/src/config.js +778 -10
- package/src/database.js +282 -1
- package/src/external/github-adapter.js +114 -25
- package/src/git/base-branch.js +11 -4
- package/src/github/client.js +482 -588
- package/src/github/errors.js +55 -0
- package/src/github/impl/graphql/pending-review-comments.js +230 -0
- package/src/github/impl/graphql/pending-review.js +153 -0
- package/src/github/impl/graphql/review-lifecycle.js +161 -0
- package/src/github/impl/graphql/stack-walker.js +210 -0
- package/src/github/impl/host/pending-review-comments.js +338 -0
- package/src/github/impl/rest/pending-review.js +251 -0
- package/src/github/impl/rest/review-lifecycle.js +226 -0
- package/src/github/impl/rest/stack-walker.js +309 -0
- package/src/github/operations/pending-review-comments.js +79 -0
- package/src/github/operations/pending-review.js +89 -0
- package/src/github/operations/review-lifecycle.js +126 -0
- package/src/github/operations/stack-walker.js +87 -0
- package/src/github/parser.js +230 -4
- package/src/github/stack-walker.js +14 -189
- package/src/links/repo-links.js +230 -0
- package/src/local-review.js +201 -172
- package/src/main.js +133 -30
- package/src/routes/analyses.js +30 -7
- package/src/routes/bulk-analysis-configs.js +295 -0
- package/src/routes/config.js +118 -3
- package/src/routes/context-files.js +2 -29
- package/src/routes/external-comments.js +20 -10
- package/src/routes/github-collections.js +3 -1
- package/src/routes/local.js +410 -13
- package/src/routes/mcp.js +47 -4
- package/src/routes/middleware/validate-review-id.js +53 -0
- package/src/routes/pr.js +556 -71
- package/src/routes/reviews.js +145 -29
- package/src/routes/setup.js +8 -3
- package/src/routes/stack-analysis.js +33 -9
- package/src/routes/worktrees.js +3 -2
- package/src/server.js +2 -0
- package/src/setup/pr-setup.js +37 -11
- package/src/setup/stack-setup.js +13 -3
- package/src/single-port.js +6 -3
- package/src/utils/diff-hunks.js +65 -0
- package/src/utils/json-extractor.js +5 -2
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
// Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
const logger = require('../../../utils/logger');
|
|
3
|
+
const { GitHubApiError } = require('../../errors');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Derive an identity for a REST review payload. Falls back to the
|
|
7
|
+
* stringified numeric id when `node_id` is absent so callers always
|
|
8
|
+
* have a non-null id to work with. Returns null only when the payload
|
|
9
|
+
* has neither field.
|
|
10
|
+
*/
|
|
11
|
+
function deriveRestReviewId(data) {
|
|
12
|
+
if (!data || typeof data !== 'object') return null;
|
|
13
|
+
if (data.node_id) return data.node_id;
|
|
14
|
+
if (data.id !== undefined && data.id !== null) return String(data.id);
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* REST implementation of the pending-review-check area.
|
|
20
|
+
*
|
|
21
|
+
* Provides REST-backed equivalents of the GraphQL implementations in
|
|
22
|
+
* `impl/graphql/pending-review.js`. Each function returns the same shape
|
|
23
|
+
* that the GraphQL implementation does so the dispatcher's callers do
|
|
24
|
+
* not see any difference in behaviour.
|
|
25
|
+
*
|
|
26
|
+
* - getPendingReviewForUser(octokit, owner, repo, prNumber)
|
|
27
|
+
* - getReviewById(octokit, nodeId, prContext)
|
|
28
|
+
*
|
|
29
|
+
* Note: `getReviewById` requires a `prContext = { owner, repo, prNumber }`
|
|
30
|
+
* because the GitHub REST API's `pulls.getReview` endpoint identifies a
|
|
31
|
+
* review by `(owner, repo, pull_number, review_id)` — the node ID alone
|
|
32
|
+
* is not enough. The GraphQL form accepts only the node ID, so the
|
|
33
|
+
* dispatcher signature accepts an optional `prContext` and the REST path
|
|
34
|
+
* requires it.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Cache of authenticated-user lookups, keyed by Octokit instance. The
|
|
39
|
+
* cache survives across calls within a single Octokit instance, which
|
|
40
|
+
* matches how `GitHubClient` uses a single Octokit per host.
|
|
41
|
+
*
|
|
42
|
+
* Using a WeakMap so disposed Octokit instances don't keep entries
|
|
43
|
+
* alive. The cache value is a Promise so concurrent callers share one
|
|
44
|
+
* request.
|
|
45
|
+
*
|
|
46
|
+
* @type {WeakMap<Object, Promise<{id: number, login: string}>>}
|
|
47
|
+
*/
|
|
48
|
+
const authenticatedUserCache = new WeakMap();
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the authenticated user for the given Octokit instance,
|
|
52
|
+
* caching the result on the instance via a WeakMap so subsequent
|
|
53
|
+
* lookups in the same process don't re-call the API.
|
|
54
|
+
*
|
|
55
|
+
* @param {Object} octokit
|
|
56
|
+
* @returns {Promise<{id: number, login: string}>}
|
|
57
|
+
*/
|
|
58
|
+
async function getAuthenticatedUserCached(octokit) {
|
|
59
|
+
const cached = authenticatedUserCache.get(octokit);
|
|
60
|
+
if (cached) return cached;
|
|
61
|
+
|
|
62
|
+
const pending = (async () => {
|
|
63
|
+
const { data } = await octokit.rest.users.getAuthenticated();
|
|
64
|
+
return { id: data.id, login: data.login };
|
|
65
|
+
})();
|
|
66
|
+
|
|
67
|
+
authenticatedUserCache.set(octokit, pending);
|
|
68
|
+
try {
|
|
69
|
+
return await pending;
|
|
70
|
+
} catch (err) {
|
|
71
|
+
// On failure, drop the cache entry so the next caller retries.
|
|
72
|
+
authenticatedUserCache.delete(octokit);
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Fetch the pending review (if any) authored by the authenticated user
|
|
79
|
+
* via the REST API.
|
|
80
|
+
*
|
|
81
|
+
* GitHub allows only ONE pending review per user per PR. We list all
|
|
82
|
+
* reviews, filter to `state === 'PENDING'` and the authenticated user's
|
|
83
|
+
* id, then shape the result to match the GraphQL implementation:
|
|
84
|
+
*
|
|
85
|
+
* { id (node_id), databaseId (numeric id), body, url (html_url),
|
|
86
|
+
* state, createdAt (submitted_at || created), comments: { totalCount } }
|
|
87
|
+
*
|
|
88
|
+
* The `comments.totalCount` field is computed by a follow-up call to
|
|
89
|
+
* `pulls.listCommentsForReview({ review_id })` so the caller can rely
|
|
90
|
+
* on the same field being populated regardless of transport.
|
|
91
|
+
*
|
|
92
|
+
* @param {Object} octokit - Octokit instance bound to the host's baseUrl
|
|
93
|
+
* @param {string} owner
|
|
94
|
+
* @param {string} repo
|
|
95
|
+
* @param {number} prNumber
|
|
96
|
+
* @returns {Promise<Object|null>} Pending review or null
|
|
97
|
+
*/
|
|
98
|
+
async function getPendingReviewForUser(octokit, owner, repo, prNumber) {
|
|
99
|
+
try {
|
|
100
|
+
logger.debug(`Checking for pending review on PR #${prNumber} in ${owner}/${repo} (REST)`);
|
|
101
|
+
|
|
102
|
+
const user = await getAuthenticatedUserCached(octokit);
|
|
103
|
+
|
|
104
|
+
const reviews = await octokit.paginate(octokit.rest.pulls.listReviews, {
|
|
105
|
+
owner,
|
|
106
|
+
repo,
|
|
107
|
+
pull_number: prNumber,
|
|
108
|
+
per_page: 100
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const pending = reviews.find(r =>
|
|
112
|
+
r.state === 'PENDING' && r.user && r.user.id === user.id
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (!pending) {
|
|
116
|
+
logger.debug('No pending review found for user (REST)');
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// GraphQL exposes the review's createdAt directly. REST does not
|
|
121
|
+
// expose a created-at timestamp on the review payload at all —
|
|
122
|
+
// `submitted_at` is always null for a PENDING review by definition.
|
|
123
|
+
// Return null explicitly so callers do not mistake a missing-submit
|
|
124
|
+
// timestamp for a missing-created timestamp; the GraphQL shape is
|
|
125
|
+
// nullable for never-submitted reviews so this is consistent.
|
|
126
|
+
const createdAt = null;
|
|
127
|
+
|
|
128
|
+
// Count comments attached to the pending review. We use
|
|
129
|
+
// `pulls.listCommentsForReview` (GET
|
|
130
|
+
// /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/comments)
|
|
131
|
+
// which returns comments scoped to a single review — no filtering
|
|
132
|
+
// required and far less wasteful than listing every comment on the
|
|
133
|
+
// PR and discarding non-matching ones. Alt-hosts that do not
|
|
134
|
+
// surface `pull_request_review_id` consistently also benefit from
|
|
135
|
+
// this scoped endpoint. Paginated to match GraphQL's totalCount
|
|
136
|
+
// semantics.
|
|
137
|
+
let totalCount = 0;
|
|
138
|
+
try {
|
|
139
|
+
const reviewComments = await octokit.paginate(octokit.rest.pulls.listCommentsForReview, {
|
|
140
|
+
owner,
|
|
141
|
+
repo,
|
|
142
|
+
pull_number: prNumber,
|
|
143
|
+
review_id: pending.id,
|
|
144
|
+
per_page: 100
|
|
145
|
+
});
|
|
146
|
+
totalCount = reviewComments.length;
|
|
147
|
+
} catch (commentErr) {
|
|
148
|
+
logger.warn(`Could not count comments on pending review ${pending.id}: ${commentErr.message}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
logger.debug(`Found pending review for user: ${pending.node_id} with ${totalCount} comments`);
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
id: deriveRestReviewId(pending),
|
|
155
|
+
databaseId: pending.id,
|
|
156
|
+
body: pending.body || '',
|
|
157
|
+
url: pending.html_url,
|
|
158
|
+
state: pending.state,
|
|
159
|
+
createdAt,
|
|
160
|
+
comments: { totalCount }
|
|
161
|
+
};
|
|
162
|
+
} catch (error) {
|
|
163
|
+
logger.error(`Error checking for pending review (REST): ${error.message}`);
|
|
164
|
+
|
|
165
|
+
if (error.status === 401) {
|
|
166
|
+
throw new GitHubApiError('GitHub authentication failed. Check your token in ~/.pair-review/config.json', 401);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (error.status === 404) {
|
|
170
|
+
throw new GitHubApiError(`Pull request #${prNumber} not found in repository ${owner}/${repo}`, 404);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
throw new Error(`Failed to check for pending review: ${error.message}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Fetch a review by id via REST.
|
|
179
|
+
*
|
|
180
|
+
* Unlike GraphQL, the REST endpoint requires `(owner, repo, pull_number,
|
|
181
|
+
* review_id)`. Callers must therefore supply a `prContext` argument when
|
|
182
|
+
* the dispatcher is in REST mode. The `nodeId` parameter is accepted for
|
|
183
|
+
* signature parity with the GraphQL impl but is not used by REST — the
|
|
184
|
+
* `prContext.reviewId` (numeric) field is consulted first, then we fall
|
|
185
|
+
* back to the `nodeId` argument when it is a numeric string.
|
|
186
|
+
*
|
|
187
|
+
* @param {Object} octokit
|
|
188
|
+
* @param {string|number} nodeId - GraphQL node id OR numeric review id
|
|
189
|
+
* @param {Object} prContext - { owner, repo, prNumber, reviewId? }
|
|
190
|
+
* @returns {Promise<Object|null>}
|
|
191
|
+
*/
|
|
192
|
+
async function getReviewById(octokit, nodeId, prContext) {
|
|
193
|
+
if (!prContext || !prContext.owner || !prContext.repo || !prContext.prNumber) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
'REST getReviewById requires prContext={owner, repo, prNumber}. ' +
|
|
196
|
+
'The REST API identifies a review by (owner, repo, pull_number, review_id); ' +
|
|
197
|
+
'a node id alone is not sufficient.'
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Prefer an explicit numeric review id when present; otherwise expect
|
|
202
|
+
// `nodeId` to be a numeric REST id (callers that have only a GraphQL
|
|
203
|
+
// node id should resolve it upstream).
|
|
204
|
+
let reviewId = prContext.reviewId;
|
|
205
|
+
if (reviewId === undefined || reviewId === null) {
|
|
206
|
+
if (typeof nodeId === 'number') {
|
|
207
|
+
reviewId = nodeId;
|
|
208
|
+
} else if (typeof nodeId === 'string' && /^\d+$/.test(nodeId)) {
|
|
209
|
+
reviewId = Number(nodeId);
|
|
210
|
+
} else {
|
|
211
|
+
// No usable numeric id — surface a clear error rather than calling
|
|
212
|
+
// the REST API with a GraphQL node id (which would 404).
|
|
213
|
+
logger.warn(`REST getReviewById called with non-numeric id "${nodeId}" and no prContext.reviewId`);
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
logger.debug(`Fetching review ${reviewId} on PR #${prContext.prNumber} (REST)`);
|
|
220
|
+
|
|
221
|
+
const { data } = await octokit.rest.pulls.getReview({
|
|
222
|
+
owner: prContext.owner,
|
|
223
|
+
repo: prContext.repo,
|
|
224
|
+
pull_number: prContext.prNumber,
|
|
225
|
+
review_id: reviewId
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
id: deriveRestReviewId(data),
|
|
230
|
+
state: data.state,
|
|
231
|
+
submittedAt: data.submitted_at || null,
|
|
232
|
+
url: data.html_url
|
|
233
|
+
};
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (error.status === 404) {
|
|
236
|
+
logger.debug(`Review ${reviewId} not found via REST`);
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
logger.warn(`Error fetching review ${reviewId} via REST: ${error.message}`);
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
module.exports = {
|
|
245
|
+
getPendingReviewForUser,
|
|
246
|
+
getReviewById,
|
|
247
|
+
// Exported for tests so they can reset the per-instance cache.
|
|
248
|
+
_resetAuthenticatedUserCache(octokit) {
|
|
249
|
+
if (octokit) authenticatedUserCache.delete(octokit);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
const logger = require('../../../utils/logger');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Derive an identity for a REST review payload. GraphQL identifies
|
|
6
|
+
* reviews by their `node_id` string; on alt-hosts that don't surface
|
|
7
|
+
* `node_id` we fall back to the numeric `id` stringified, so callers
|
|
8
|
+
* always receive a non-null id when the review was created. Returns
|
|
9
|
+
* null only when neither field is present.
|
|
10
|
+
*
|
|
11
|
+
* @param {Object|null|undefined} data - The REST API review payload
|
|
12
|
+
* @returns {string|null}
|
|
13
|
+
*/
|
|
14
|
+
function deriveRestReviewId(data) {
|
|
15
|
+
if (!data || typeof data !== 'object') return null;
|
|
16
|
+
if (data.node_id) return data.node_id;
|
|
17
|
+
if (data.id !== undefined && data.id !== null) return String(data.id);
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* REST implementation of the review-lifecycle area:
|
|
23
|
+
* - addPullRequestReview -> `pulls.createReview({ body: '' })` (no event, empty body)
|
|
24
|
+
* - addPullRequestReviewWithBody -> `pulls.createReview({ body })`
|
|
25
|
+
* - submitPullRequestReview -> `pulls.submitReview({ event, body })`
|
|
26
|
+
* - deletePullRequestReview -> `pulls.deletePendingReview()`
|
|
27
|
+
*
|
|
28
|
+
* Each function takes an Octokit-like client as its first parameter and
|
|
29
|
+
* a `prContext = { owner, repo, prNumber }` since REST endpoints
|
|
30
|
+
* identify the review by `(owner, repo, pull_number, review_id)` rather
|
|
31
|
+
* than by a GraphQL node id.
|
|
32
|
+
*
|
|
33
|
+
* Return shapes are byte-identical to the GraphQL impl in
|
|
34
|
+
* `impl/graphql/review-lifecycle.js` so the orchestration in
|
|
35
|
+
* `client.js` does not have to branch on transport.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Ensure prContext has the fields REST endpoints require. Throws a
|
|
40
|
+
* descriptive error pointing at the dispatcher/client wiring when
|
|
41
|
+
* called without enough context.
|
|
42
|
+
*
|
|
43
|
+
* @param {Object} prContext
|
|
44
|
+
* @param {string} fnName - Function name for the error message
|
|
45
|
+
*/
|
|
46
|
+
function requirePRContext(prContext, fnName) {
|
|
47
|
+
if (!prContext || !prContext.owner || !prContext.repo || !prContext.prNumber) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`REST ${fnName} requires prContext={owner, repo, prNumber}. ` +
|
|
50
|
+
'The REST API needs the PR coordinates to identify the review; ' +
|
|
51
|
+
'pass them via the dispatcher / GitHubClient.'
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create a pending review with no body. Used by the submit-review flow
|
|
58
|
+
* that supplies the body at submission time.
|
|
59
|
+
*
|
|
60
|
+
* Returns both the GraphQL node id (`id`) and the numeric REST database
|
|
61
|
+
* id (`databaseId`). Downstream callers (e.g.
|
|
62
|
+
* `submitPullRequestReview`, `deletePullRequestReview`, the host
|
|
63
|
+
* `addCommentsInBatches` extension) require the numeric id to address
|
|
64
|
+
* the review via REST/host endpoints — they read it from
|
|
65
|
+
* `prContext.reviewId` populated by the orchestration in `client.js`.
|
|
66
|
+
*
|
|
67
|
+
* @param {Object} octokit
|
|
68
|
+
* @param {string} prNodeId - Accepted for signature parity; REST does not need it
|
|
69
|
+
* @param {Object} prContext - { owner, repo, prNumber }
|
|
70
|
+
* @returns {Promise<{id: string, databaseId: number|null}>}
|
|
71
|
+
*/
|
|
72
|
+
async function addPullRequestReview(octokit, prNodeId, prContext) {
|
|
73
|
+
requirePRContext(prContext, 'addPullRequestReview');
|
|
74
|
+
|
|
75
|
+
const { data } = await octokit.rest.pulls.createReview({
|
|
76
|
+
owner: prContext.owner,
|
|
77
|
+
repo: prContext.repo,
|
|
78
|
+
pull_number: prContext.prNumber,
|
|
79
|
+
// No `event` -> review is created in PENDING state.
|
|
80
|
+
//
|
|
81
|
+
// The explicit empty `body` is REQUIRED, not cosmetic. Without any
|
|
82
|
+
// body params Octokit serializes a POST with an empty HTTP body.
|
|
83
|
+
// github.com tolerates that and creates an empty pending review, but
|
|
84
|
+
// strict GitHub-compatible alt-hosts (those with an `api_host`)
|
|
85
|
+
// reject it with HTTP 400 `{ message: "request body is empty" }`.
|
|
86
|
+
// Sending `body: ''` makes the serialized body non-empty (`{"body":""}`)
|
|
87
|
+
// while keeping the review PENDING. This is observationally identical
|
|
88
|
+
// on github.com to an empty-summary pending review and mirrors the
|
|
89
|
+
// sibling `addPullRequestReviewWithBody` (`body: body || ''`).
|
|
90
|
+
// Do NOT "simplify" this away.
|
|
91
|
+
body: ''
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
id: deriveRestReviewId(data),
|
|
96
|
+
databaseId: typeof data.id === 'number' ? data.id : null
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Create a pending review with a body. Used by the draft-review flow
|
|
102
|
+
* which persists the summary on the pending review itself.
|
|
103
|
+
*
|
|
104
|
+
* @param {Object} octokit
|
|
105
|
+
* @param {string} prNodeId
|
|
106
|
+
* @param {string|null} body
|
|
107
|
+
* @param {Object} prContext
|
|
108
|
+
* @returns {Promise<{id: string, databaseId: number|null, url: string}>}
|
|
109
|
+
*/
|
|
110
|
+
async function addPullRequestReviewWithBody(octokit, prNodeId, body, prContext) {
|
|
111
|
+
requirePRContext(prContext, 'addPullRequestReviewWithBody');
|
|
112
|
+
|
|
113
|
+
const { data } = await octokit.rest.pulls.createReview({
|
|
114
|
+
owner: prContext.owner,
|
|
115
|
+
repo: prContext.repo,
|
|
116
|
+
pull_number: prContext.prNumber,
|
|
117
|
+
body: body || ''
|
|
118
|
+
// No `event` -> PENDING.
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
id: deriveRestReviewId(data),
|
|
123
|
+
databaseId: typeof data.id === 'number' ? data.id : null,
|
|
124
|
+
url: data.html_url
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Submit a pending review. Maps the GraphQL `event` enum values
|
|
130
|
+
* (APPROVE/REQUEST_CHANGES/COMMENT) directly to REST's `event` field,
|
|
131
|
+
* which accepts the same uppercase values.
|
|
132
|
+
*
|
|
133
|
+
* @param {Object} octokit
|
|
134
|
+
* @param {string} reviewId - GraphQL node id OR numeric review id
|
|
135
|
+
* @param {string} event - APPROVE | REQUEST_CHANGES | COMMENT
|
|
136
|
+
* @param {string|null} body
|
|
137
|
+
* @param {Object} prContext - { owner, repo, prNumber, reviewId? } - numeric id required for REST
|
|
138
|
+
* @returns {Promise<{id: string, databaseId: number|null, url: string, state: string}>}
|
|
139
|
+
*/
|
|
140
|
+
async function submitPullRequestReview(octokit, reviewId, event, body, prContext) {
|
|
141
|
+
requirePRContext(prContext, 'submitPullRequestReview');
|
|
142
|
+
|
|
143
|
+
const numericId = resolveNumericReviewId(reviewId, prContext);
|
|
144
|
+
if (numericId === null) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
'REST submitPullRequestReview needs a numeric review id. Pass prContext.reviewId ' +
|
|
147
|
+
'when the reviewId argument is a GraphQL node id.'
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const { data } = await octokit.rest.pulls.submitReview({
|
|
152
|
+
owner: prContext.owner,
|
|
153
|
+
repo: prContext.repo,
|
|
154
|
+
pull_number: prContext.prNumber,
|
|
155
|
+
review_id: numericId,
|
|
156
|
+
event,
|
|
157
|
+
body: body || ''
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
id: deriveRestReviewId(data),
|
|
162
|
+
databaseId: typeof data.id === 'number' ? data.id : null,
|
|
163
|
+
url: data.html_url,
|
|
164
|
+
state: data.state
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Delete a pending review (cleanup on failure). Never throws — mirrors
|
|
170
|
+
* the GraphQL impl, which logs and returns false on failure.
|
|
171
|
+
*
|
|
172
|
+
* @param {Object} octokit
|
|
173
|
+
* @param {string} reviewId
|
|
174
|
+
* @param {Object} prContext
|
|
175
|
+
* @returns {Promise<boolean>}
|
|
176
|
+
*/
|
|
177
|
+
async function deletePullRequestReview(octokit, reviewId, prContext) {
|
|
178
|
+
try {
|
|
179
|
+
requirePRContext(prContext, 'deletePullRequestReview');
|
|
180
|
+
|
|
181
|
+
const numericId = resolveNumericReviewId(reviewId, prContext);
|
|
182
|
+
if (numericId === null) {
|
|
183
|
+
logger.warn('REST deletePullRequestReview called without a numeric review id; skipping');
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await octokit.rest.pulls.deletePendingReview({
|
|
188
|
+
owner: prContext.owner,
|
|
189
|
+
repo: prContext.repo,
|
|
190
|
+
pull_number: prContext.prNumber,
|
|
191
|
+
review_id: numericId
|
|
192
|
+
});
|
|
193
|
+
logger.info('Cleaned up pending review after failure (REST)');
|
|
194
|
+
return true;
|
|
195
|
+
} catch (cleanupError) {
|
|
196
|
+
logger.warn(`Failed to clean up pending review (REST): ${cleanupError.message}`);
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Resolve a numeric REST review id from the dispatcher-supplied
|
|
203
|
+
* `reviewId` (which may be either a numeric REST id or a GraphQL node
|
|
204
|
+
* id) plus an optional override on `prContext.reviewId`.
|
|
205
|
+
*
|
|
206
|
+
* @param {string|number} reviewId
|
|
207
|
+
* @param {Object} prContext
|
|
208
|
+
* @returns {number|null}
|
|
209
|
+
*/
|
|
210
|
+
function resolveNumericReviewId(reviewId, prContext) {
|
|
211
|
+
if (prContext && (typeof prContext.reviewId === 'number' || typeof prContext.reviewId === 'string')) {
|
|
212
|
+
const fromCtx = Number(prContext.reviewId);
|
|
213
|
+
if (Number.isFinite(fromCtx)) return fromCtx;
|
|
214
|
+
}
|
|
215
|
+
if (typeof reviewId === 'number') return reviewId;
|
|
216
|
+
if (typeof reviewId === 'string' && /^\d+$/.test(reviewId)) return Number(reviewId);
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = {
|
|
221
|
+
addPullRequestReview,
|
|
222
|
+
addPullRequestReviewWithBody,
|
|
223
|
+
submitPullRequestReview,
|
|
224
|
+
deletePullRequestReview,
|
|
225
|
+
_internals: { resolveNumericReviewId, requirePRContext }
|
|
226
|
+
};
|