@forge-glance/sdk 0.1.0 → 0.2.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/GitHubProvider.d.ts +19 -3
- package/dist/GitHubProvider.js +217 -1
- package/dist/GitLabProvider.d.ts +25 -3
- package/dist/GitLabProvider.js +285 -1
- package/dist/GitProvider.d.ts +89 -1
- package/dist/index.d.ts +14 -14
- package/dist/index.js +502 -2
- package/dist/providers.js +502 -2
- package/dist/types.d.ts +103 -0
- package/package.json +1 -1
- package/src/GitHubProvider.ts +534 -115
- package/src/GitLabProvider.ts +519 -56
- package/src/GitProvider.ts +155 -13
- package/src/index.ts +25 -15
- package/src/types.ts +111 -0
package/src/GitHubProvider.ts
CHANGED
|
@@ -12,20 +12,25 @@
|
|
|
12
12
|
* provides the instance URL and we append "/api/v3".
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import type { GitProvider } from
|
|
15
|
+
import type { GitProvider } from './GitProvider.ts';
|
|
16
16
|
import type {
|
|
17
|
+
BranchProtectionRule,
|
|
18
|
+
CreatePullRequestInput,
|
|
17
19
|
DiffStats,
|
|
18
20
|
Discussion,
|
|
21
|
+
MergePullRequestInput,
|
|
19
22
|
MRDetail,
|
|
20
23
|
Note,
|
|
21
24
|
NoteAuthor,
|
|
22
25
|
NotePosition,
|
|
23
26
|
Pipeline,
|
|
24
27
|
PipelineJob,
|
|
28
|
+
ProviderCapabilities,
|
|
25
29
|
PullRequest,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
UpdatePullRequestInput,
|
|
31
|
+
UserRef
|
|
32
|
+
} from './types.ts';
|
|
33
|
+
import { type ForgeLogger, noopLogger } from './logger.ts';
|
|
29
34
|
|
|
30
35
|
// ---------------------------------------------------------------------------
|
|
31
36
|
// GitHub REST API response shapes (only fields we consume)
|
|
@@ -119,7 +124,7 @@ function toUserRef(u: GHUser): UserRef {
|
|
|
119
124
|
id: `github:user:${u.id}`,
|
|
120
125
|
username: u.login,
|
|
121
126
|
name: u.name ?? u.login,
|
|
122
|
-
avatarUrl: u.avatar_url
|
|
127
|
+
avatarUrl: u.avatar_url
|
|
123
128
|
};
|
|
124
129
|
}
|
|
125
130
|
|
|
@@ -128,9 +133,9 @@ function toUserRef(u: GHUser): UserRef {
|
|
|
128
133
|
* GitHub only has "open" and "closed"; we check `merged_at` to distinguish merges.
|
|
129
134
|
*/
|
|
130
135
|
function normalizePRState(pr: GHPullRequest): string {
|
|
131
|
-
if (pr.merged_at) return
|
|
132
|
-
if (pr.state ===
|
|
133
|
-
return
|
|
136
|
+
if (pr.merged_at) return 'merged';
|
|
137
|
+
if (pr.state === 'open') return 'opened';
|
|
138
|
+
return 'closed';
|
|
134
139
|
}
|
|
135
140
|
|
|
136
141
|
/**
|
|
@@ -139,32 +144,32 @@ function normalizePRState(pr: GHPullRequest): string {
|
|
|
139
144
|
*/
|
|
140
145
|
function toPipeline(
|
|
141
146
|
checkRuns: GHCheckRun[],
|
|
142
|
-
prHtmlUrl: string
|
|
147
|
+
prHtmlUrl: string
|
|
143
148
|
): Pipeline | null {
|
|
144
149
|
if (checkRuns.length === 0) return null;
|
|
145
150
|
|
|
146
|
-
const jobs: PipelineJob[] = checkRuns.map(
|
|
151
|
+
const jobs: PipelineJob[] = checkRuns.map(cr => ({
|
|
147
152
|
id: `github:check:${cr.id}`,
|
|
148
153
|
name: cr.name,
|
|
149
|
-
stage:
|
|
154
|
+
stage: 'checks', // GitHub doesn't have stages; use a flat stage name
|
|
150
155
|
status: normalizeCheckStatus(cr),
|
|
151
156
|
allowFailure: false,
|
|
152
|
-
webUrl: cr.html_url
|
|
157
|
+
webUrl: cr.html_url
|
|
153
158
|
}));
|
|
154
159
|
|
|
155
160
|
// Derive overall pipeline status from individual check runs
|
|
156
|
-
const statuses = jobs.map(
|
|
161
|
+
const statuses = jobs.map(j => j.status);
|
|
157
162
|
let overallStatus: string;
|
|
158
|
-
if (statuses.some(
|
|
159
|
-
overallStatus =
|
|
160
|
-
} else if (statuses.some(
|
|
161
|
-
overallStatus =
|
|
162
|
-
} else if (statuses.some(
|
|
163
|
-
overallStatus =
|
|
164
|
-
} else if (statuses.every(
|
|
165
|
-
overallStatus =
|
|
163
|
+
if (statuses.some(s => s === 'failed')) {
|
|
164
|
+
overallStatus = 'failed';
|
|
165
|
+
} else if (statuses.some(s => s === 'running')) {
|
|
166
|
+
overallStatus = 'running';
|
|
167
|
+
} else if (statuses.some(s => s === 'pending')) {
|
|
168
|
+
overallStatus = 'pending';
|
|
169
|
+
} else if (statuses.every(s => s === 'success' || s === 'skipped')) {
|
|
170
|
+
overallStatus = 'success';
|
|
166
171
|
} else {
|
|
167
|
-
overallStatus =
|
|
172
|
+
overallStatus = 'pending';
|
|
168
173
|
}
|
|
169
174
|
|
|
170
175
|
return {
|
|
@@ -172,32 +177,32 @@ function toPipeline(
|
|
|
172
177
|
status: overallStatus,
|
|
173
178
|
createdAt: null,
|
|
174
179
|
webUrl: `${prHtmlUrl}/checks`,
|
|
175
|
-
jobs
|
|
180
|
+
jobs
|
|
176
181
|
};
|
|
177
182
|
}
|
|
178
183
|
|
|
179
184
|
function normalizeCheckStatus(cr: GHCheckRun): string {
|
|
180
|
-
if (cr.status ===
|
|
185
|
+
if (cr.status === 'completed') {
|
|
181
186
|
switch (cr.conclusion) {
|
|
182
|
-
case
|
|
183
|
-
return
|
|
184
|
-
case
|
|
185
|
-
case
|
|
186
|
-
return
|
|
187
|
-
case
|
|
188
|
-
return
|
|
189
|
-
case
|
|
190
|
-
return
|
|
191
|
-
case
|
|
192
|
-
return
|
|
193
|
-
case
|
|
194
|
-
return
|
|
187
|
+
case 'success':
|
|
188
|
+
return 'success';
|
|
189
|
+
case 'failure':
|
|
190
|
+
case 'timed_out':
|
|
191
|
+
return 'failed';
|
|
192
|
+
case 'cancelled':
|
|
193
|
+
return 'canceled';
|
|
194
|
+
case 'skipped':
|
|
195
|
+
return 'skipped';
|
|
196
|
+
case 'neutral':
|
|
197
|
+
return 'success';
|
|
198
|
+
case 'action_required':
|
|
199
|
+
return 'manual';
|
|
195
200
|
default:
|
|
196
|
-
return
|
|
201
|
+
return 'pending';
|
|
197
202
|
}
|
|
198
203
|
}
|
|
199
|
-
if (cr.status ===
|
|
200
|
-
return
|
|
204
|
+
if (cr.status === 'in_progress') return 'running';
|
|
205
|
+
return 'pending'; // "queued"
|
|
201
206
|
}
|
|
202
207
|
|
|
203
208
|
// ---------------------------------------------------------------------------
|
|
@@ -205,7 +210,7 @@ function normalizeCheckStatus(cr: GHCheckRun): string {
|
|
|
205
210
|
// ---------------------------------------------------------------------------
|
|
206
211
|
|
|
207
212
|
export class GitHubProvider implements GitProvider {
|
|
208
|
-
readonly providerName =
|
|
213
|
+
readonly providerName = 'github' as const;
|
|
209
214
|
readonly baseURL: string;
|
|
210
215
|
private readonly apiBase: string;
|
|
211
216
|
private readonly token: string;
|
|
@@ -217,29 +222,46 @@ export class GitHubProvider implements GitProvider {
|
|
|
217
222
|
* @param token — A GitHub PAT (classic or fine-grained) with `repo` scope.
|
|
218
223
|
* @param options.logger — Optional logger; defaults to noop.
|
|
219
224
|
*/
|
|
220
|
-
constructor(
|
|
221
|
-
|
|
225
|
+
constructor(
|
|
226
|
+
baseURL: string,
|
|
227
|
+
token: string,
|
|
228
|
+
options: { logger?: ForgeLogger } = {}
|
|
229
|
+
) {
|
|
230
|
+
this.baseURL = baseURL.replace(/\/$/, '');
|
|
222
231
|
this.token = token;
|
|
223
232
|
this.log = options.logger ?? noopLogger;
|
|
224
233
|
|
|
225
234
|
// API base: github.com uses api.github.com; GHES uses <host>/api/v3
|
|
226
235
|
if (
|
|
227
|
-
this.baseURL ===
|
|
228
|
-
this.baseURL ===
|
|
236
|
+
this.baseURL === 'https://github.com' ||
|
|
237
|
+
this.baseURL === 'https://www.github.com'
|
|
229
238
|
) {
|
|
230
|
-
this.apiBase =
|
|
239
|
+
this.apiBase = 'https://api.github.com';
|
|
231
240
|
} else {
|
|
232
241
|
this.apiBase = `${this.baseURL}/api/v3`;
|
|
233
242
|
}
|
|
234
243
|
}
|
|
235
244
|
|
|
245
|
+
// ── Capabilities ──────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
readonly capabilities: ProviderCapabilities = {
|
|
248
|
+
canMerge: true,
|
|
249
|
+
canApprove: true,
|
|
250
|
+
canUnapprove: false,
|
|
251
|
+
canRebase: false,
|
|
252
|
+
canAutoMerge: false,
|
|
253
|
+
canResolveDiscussions: false,
|
|
254
|
+
canRetryPipeline: true,
|
|
255
|
+
canRequestReReview: true
|
|
256
|
+
};
|
|
257
|
+
|
|
236
258
|
// ── GitProvider interface ─────────────────────────────────────────────────
|
|
237
259
|
|
|
238
260
|
async validateToken(): Promise<UserRef> {
|
|
239
|
-
const res = await this.api(
|
|
261
|
+
const res = await this.api('GET', '/user');
|
|
240
262
|
if (!res.ok) {
|
|
241
263
|
throw new Error(
|
|
242
|
-
`GitHub token validation failed: ${res.status} ${res.statusText}
|
|
264
|
+
`GitHub token validation failed: ${res.status} ${res.statusText}`
|
|
243
265
|
);
|
|
244
266
|
}
|
|
245
267
|
const user = (await res.json()) as GHUser;
|
|
@@ -252,9 +274,9 @@ export class GitHubProvider implements GitProvider {
|
|
|
252
274
|
// review requests. We merge the results.
|
|
253
275
|
|
|
254
276
|
const [authored, reviewRequested, assigned] = await Promise.all([
|
|
255
|
-
this.searchPRs(
|
|
256
|
-
this.searchPRs(
|
|
257
|
-
this.searchPRs(
|
|
277
|
+
this.searchPRs('is:open is:pr author:@me'),
|
|
278
|
+
this.searchPRs('is:open is:pr review-requested:@me'),
|
|
279
|
+
this.searchPRs('is:open is:pr assignee:@me')
|
|
258
280
|
]);
|
|
259
281
|
|
|
260
282
|
// Deduplicate by PR number+repo
|
|
@@ -274,65 +296,67 @@ export class GitHubProvider implements GitProvider {
|
|
|
274
296
|
}
|
|
275
297
|
};
|
|
276
298
|
|
|
277
|
-
addAll(authored,
|
|
278
|
-
addAll(reviewRequested,
|
|
279
|
-
addAll(assigned,
|
|
299
|
+
addAll(authored, 'author');
|
|
300
|
+
addAll(reviewRequested, 'reviewer');
|
|
301
|
+
addAll(assigned, 'assignee');
|
|
280
302
|
|
|
281
303
|
// For each unique PR, fetch check runs and reviews in parallel
|
|
282
304
|
const entries = [...byKey.entries()];
|
|
283
305
|
const results = await Promise.all(
|
|
284
306
|
entries.map(async ([key, pr]) => {
|
|
285
|
-
const prRoles = roles.get(key) ?? [
|
|
307
|
+
const prRoles = roles.get(key) ?? ['author'];
|
|
286
308
|
const [reviews, checkRuns] = await Promise.all([
|
|
287
309
|
this.fetchReviews(pr.base.repo.full_name, pr.number),
|
|
288
|
-
this.fetchCheckRuns(pr.base.repo.full_name, pr.head.sha)
|
|
310
|
+
this.fetchCheckRuns(pr.base.repo.full_name, pr.head.sha)
|
|
289
311
|
]);
|
|
290
312
|
return this.toPullRequest(pr, prRoles, reviews, checkRuns);
|
|
291
|
-
})
|
|
313
|
+
})
|
|
292
314
|
);
|
|
293
315
|
|
|
294
|
-
this.log.debug(
|
|
316
|
+
this.log.debug('GitHubProvider.fetchPullRequests', {
|
|
317
|
+
count: results.length
|
|
318
|
+
});
|
|
295
319
|
return results;
|
|
296
320
|
}
|
|
297
321
|
|
|
298
322
|
async fetchSingleMR(
|
|
299
323
|
projectPath: string,
|
|
300
324
|
mrIid: number,
|
|
301
|
-
_currentUserNumericId: number | null
|
|
325
|
+
_currentUserNumericId: number | null
|
|
302
326
|
): Promise<PullRequest | null> {
|
|
303
327
|
// projectPath for GitHub is "owner/repo"
|
|
304
328
|
try {
|
|
305
|
-
const res = await this.api(
|
|
329
|
+
const res = await this.api('GET', `/repos/${projectPath}/pulls/${mrIid}`);
|
|
306
330
|
if (!res.ok) return null;
|
|
307
331
|
|
|
308
332
|
const pr = (await res.json()) as GHPullRequest;
|
|
309
333
|
const [reviews, checkRuns] = await Promise.all([
|
|
310
334
|
this.fetchReviews(projectPath, mrIid),
|
|
311
|
-
this.fetchCheckRuns(projectPath, pr.head.sha)
|
|
335
|
+
this.fetchCheckRuns(projectPath, pr.head.sha)
|
|
312
336
|
]);
|
|
313
337
|
|
|
314
338
|
// Determine roles from the current user
|
|
315
|
-
const currentUser = await this.api(
|
|
339
|
+
const currentUser = await this.api('GET', '/user');
|
|
316
340
|
const currentUserData = (await currentUser.json()) as GHUser;
|
|
317
341
|
const prRoles: string[] = [];
|
|
318
|
-
if (pr.user.id === currentUserData.id) prRoles.push(
|
|
319
|
-
if (pr.assignees.some(
|
|
320
|
-
prRoles.push(
|
|
321
|
-
if (pr.requested_reviewers.some(
|
|
322
|
-
prRoles.push(
|
|
342
|
+
if (pr.user.id === currentUserData.id) prRoles.push('author');
|
|
343
|
+
if (pr.assignees.some(a => a.id === currentUserData.id))
|
|
344
|
+
prRoles.push('assignee');
|
|
345
|
+
if (pr.requested_reviewers.some(r => r.id === currentUserData.id))
|
|
346
|
+
prRoles.push('reviewer');
|
|
323
347
|
|
|
324
348
|
return this.toPullRequest(
|
|
325
349
|
pr,
|
|
326
|
-
prRoles.length > 0 ? prRoles : [
|
|
350
|
+
prRoles.length > 0 ? prRoles : ['author'],
|
|
327
351
|
reviews,
|
|
328
|
-
checkRuns
|
|
352
|
+
checkRuns
|
|
329
353
|
);
|
|
330
354
|
} catch (err) {
|
|
331
355
|
const message = err instanceof Error ? err.message : String(err);
|
|
332
|
-
this.log.warn(
|
|
356
|
+
this.log.warn('GitHubProvider.fetchSingleMR failed', {
|
|
333
357
|
projectPath,
|
|
334
358
|
mrIid,
|
|
335
|
-
message
|
|
359
|
+
message
|
|
336
360
|
});
|
|
337
361
|
return null;
|
|
338
362
|
}
|
|
@@ -340,11 +364,11 @@ export class GitHubProvider implements GitProvider {
|
|
|
340
364
|
|
|
341
365
|
async fetchMRDiscussions(
|
|
342
366
|
repositoryId: string,
|
|
343
|
-
mrIid: number
|
|
367
|
+
mrIid: number
|
|
344
368
|
): Promise<MRDetail> {
|
|
345
|
-
const repoId = parseInt(repositoryId.split(
|
|
369
|
+
const repoId = parseInt(repositoryId.split(':').pop() ?? '0', 10);
|
|
346
370
|
// We need the repo full_name. For now, look it up from the API.
|
|
347
|
-
const repoRes = await this.api(
|
|
371
|
+
const repoRes = await this.api('GET', `/repositories/${repoId}`);
|
|
348
372
|
if (!repoRes.ok) {
|
|
349
373
|
throw new Error(`Failed to fetch repo: ${repoRes.status}`);
|
|
350
374
|
}
|
|
@@ -353,11 +377,11 @@ export class GitHubProvider implements GitProvider {
|
|
|
353
377
|
// Fetch review comments (diff-level) and issue comments (PR-level)
|
|
354
378
|
const [reviewComments, issueComments] = await Promise.all([
|
|
355
379
|
this.fetchAllPages<GHComment>(
|
|
356
|
-
`/repos/${repo.full_name}/pulls/${mrIid}/comments?per_page=100
|
|
380
|
+
`/repos/${repo.full_name}/pulls/${mrIid}/comments?per_page=100`
|
|
357
381
|
),
|
|
358
382
|
this.fetchAllPages<GHComment>(
|
|
359
|
-
`/repos/${repo.full_name}/issues/${mrIid}/comments?per_page=100
|
|
360
|
-
)
|
|
383
|
+
`/repos/${repo.full_name}/issues/${mrIid}/comments?per_page=100`
|
|
384
|
+
)
|
|
361
385
|
]);
|
|
362
386
|
|
|
363
387
|
// Group review comments into threads (by pull_request_review_id and in_reply_to_id)
|
|
@@ -369,7 +393,7 @@ export class GitHubProvider implements GitProvider {
|
|
|
369
393
|
id: `gh-issue-comment-${c.id}`,
|
|
370
394
|
resolvable: null,
|
|
371
395
|
resolved: null,
|
|
372
|
-
notes: [toNote(c)]
|
|
396
|
+
notes: [toNote(c)]
|
|
373
397
|
});
|
|
374
398
|
}
|
|
375
399
|
|
|
@@ -385,47 +409,444 @@ export class GitHubProvider implements GitProvider {
|
|
|
385
409
|
for (const [rootId, comments] of threadMap) {
|
|
386
410
|
comments.sort(
|
|
387
411
|
(a, b) =>
|
|
388
|
-
new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
|
|
412
|
+
new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
|
|
389
413
|
);
|
|
390
414
|
discussions.push({
|
|
391
415
|
id: `gh-review-thread-${rootId}`,
|
|
392
416
|
resolvable: true,
|
|
393
417
|
resolved: null, // GitHub doesn't have a native "resolved" state on review threads
|
|
394
|
-
notes: comments.map(toNote)
|
|
418
|
+
notes: comments.map(toNote)
|
|
395
419
|
});
|
|
396
420
|
}
|
|
397
421
|
|
|
398
422
|
return { mrIid, repositoryId, discussions };
|
|
399
423
|
}
|
|
400
424
|
|
|
425
|
+
async fetchBranchProtectionRules(
|
|
426
|
+
projectPath: string
|
|
427
|
+
): Promise<BranchProtectionRule[]> {
|
|
428
|
+
const res = await this.api(
|
|
429
|
+
'GET',
|
|
430
|
+
`/repos/${projectPath}/branches?protected=true&per_page=100`
|
|
431
|
+
);
|
|
432
|
+
if (!res.ok) {
|
|
433
|
+
throw new Error(
|
|
434
|
+
`fetchBranchProtectionRules failed: ${res.status} ${await res.text()}`
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
const branches = (await res.json()) as Array<{
|
|
438
|
+
name: string;
|
|
439
|
+
protected: boolean;
|
|
440
|
+
protection?: {
|
|
441
|
+
enabled: boolean;
|
|
442
|
+
required_status_checks?: {
|
|
443
|
+
enforcement_level: string;
|
|
444
|
+
contexts: string[];
|
|
445
|
+
} | null;
|
|
446
|
+
};
|
|
447
|
+
}>;
|
|
448
|
+
|
|
449
|
+
const rules: BranchProtectionRule[] = [];
|
|
450
|
+
for (const b of branches) {
|
|
451
|
+
if (!b.protected) continue;
|
|
452
|
+
// Fetch detailed protection for each protected branch
|
|
453
|
+
const detailRes = await this.api(
|
|
454
|
+
'GET',
|
|
455
|
+
`/repos/${projectPath}/branches/${encodeURIComponent(b.name)}/protection`
|
|
456
|
+
);
|
|
457
|
+
if (!detailRes.ok) {
|
|
458
|
+
rules.push({
|
|
459
|
+
pattern: b.name,
|
|
460
|
+
allowForcePush: false,
|
|
461
|
+
allowDeletion: false,
|
|
462
|
+
requiredApprovals: 0,
|
|
463
|
+
requireStatusChecks: false
|
|
464
|
+
});
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
const detail = (await detailRes.json()) as {
|
|
468
|
+
allow_force_pushes?: { enabled: boolean };
|
|
469
|
+
allow_deletions?: { enabled: boolean };
|
|
470
|
+
required_pull_request_reviews?: {
|
|
471
|
+
required_approving_review_count?: number;
|
|
472
|
+
} | null;
|
|
473
|
+
required_status_checks?: { strict: boolean; contexts: string[] } | null;
|
|
474
|
+
};
|
|
475
|
+
rules.push({
|
|
476
|
+
pattern: b.name,
|
|
477
|
+
allowForcePush: detail.allow_force_pushes?.enabled ?? false,
|
|
478
|
+
allowDeletion: detail.allow_deletions?.enabled ?? false,
|
|
479
|
+
requiredApprovals:
|
|
480
|
+
detail.required_pull_request_reviews
|
|
481
|
+
?.required_approving_review_count ?? 0,
|
|
482
|
+
requireStatusChecks:
|
|
483
|
+
detail.required_status_checks !== null &&
|
|
484
|
+
detail.required_status_checks !== undefined,
|
|
485
|
+
raw: detail as unknown as Record<string, unknown>
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
return rules;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async deleteBranch(projectPath: string, branch: string): Promise<void> {
|
|
492
|
+
const res = await this.api(
|
|
493
|
+
'DELETE',
|
|
494
|
+
`/repos/${projectPath}/git/refs/heads/${encodeURIComponent(branch)}`
|
|
495
|
+
);
|
|
496
|
+
if (!res.ok) {
|
|
497
|
+
throw new Error(`deleteBranch failed: ${res.status} ${await res.text()}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async fetchPullRequestByBranch(
|
|
502
|
+
projectPath: string,
|
|
503
|
+
sourceBranch: string
|
|
504
|
+
): Promise<PullRequest | null> {
|
|
505
|
+
const res = await this.api(
|
|
506
|
+
'GET',
|
|
507
|
+
`/repos/${projectPath}/pulls?head=${projectPath.split('/')[0]}:${encodeURIComponent(sourceBranch)}&state=open&per_page=1`
|
|
508
|
+
);
|
|
509
|
+
if (!res.ok) {
|
|
510
|
+
this.log.warn('fetchPullRequestByBranch failed', {
|
|
511
|
+
projectPath,
|
|
512
|
+
sourceBranch,
|
|
513
|
+
status: res.status
|
|
514
|
+
});
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
const prs = (await res.json()) as GHPullRequest[];
|
|
518
|
+
if (!prs[0]) return null;
|
|
519
|
+
return this.fetchSingleMR(projectPath, prs[0].number, null);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async createPullRequest(input: CreatePullRequestInput): Promise<PullRequest> {
|
|
523
|
+
const body: Record<string, unknown> = {
|
|
524
|
+
head: input.sourceBranch,
|
|
525
|
+
base: input.targetBranch,
|
|
526
|
+
title: input.title
|
|
527
|
+
};
|
|
528
|
+
if (input.description != null) body.body = input.description;
|
|
529
|
+
if (input.draft != null) body.draft = input.draft;
|
|
530
|
+
|
|
531
|
+
const res = await this.api(
|
|
532
|
+
'POST',
|
|
533
|
+
`/repos/${input.projectPath}/pulls`,
|
|
534
|
+
body
|
|
535
|
+
);
|
|
536
|
+
if (!res.ok) {
|
|
537
|
+
const text = await res.text();
|
|
538
|
+
throw new Error(`createPullRequest failed: ${res.status} ${text}`);
|
|
539
|
+
}
|
|
540
|
+
const created = (await res.json()) as GHPullRequest;
|
|
541
|
+
|
|
542
|
+
// GitHub doesn't support reviewers/assignees/labels on create — add them separately
|
|
543
|
+
if (input.reviewers?.length) {
|
|
544
|
+
await this.api(
|
|
545
|
+
'POST',
|
|
546
|
+
`/repos/${input.projectPath}/pulls/${created.number}/requested_reviewers`,
|
|
547
|
+
{
|
|
548
|
+
reviewers: input.reviewers
|
|
549
|
+
}
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
if (input.assignees?.length) {
|
|
553
|
+
await this.api(
|
|
554
|
+
'POST',
|
|
555
|
+
`/repos/${input.projectPath}/issues/${created.number}/assignees`,
|
|
556
|
+
{
|
|
557
|
+
assignees: input.assignees
|
|
558
|
+
}
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
if (input.labels?.length) {
|
|
562
|
+
await this.api(
|
|
563
|
+
'POST',
|
|
564
|
+
`/repos/${input.projectPath}/issues/${created.number}/labels`,
|
|
565
|
+
{
|
|
566
|
+
labels: input.labels
|
|
567
|
+
}
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const pr = await this.fetchSingleMR(
|
|
572
|
+
input.projectPath,
|
|
573
|
+
created.number,
|
|
574
|
+
null
|
|
575
|
+
);
|
|
576
|
+
if (!pr) throw new Error('Created PR but failed to fetch it back');
|
|
577
|
+
return pr;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async updatePullRequest(
|
|
581
|
+
projectPath: string,
|
|
582
|
+
mrIid: number,
|
|
583
|
+
input: UpdatePullRequestInput
|
|
584
|
+
): Promise<PullRequest> {
|
|
585
|
+
const body: Record<string, unknown> = {};
|
|
586
|
+
if (input.title != null) body.title = input.title;
|
|
587
|
+
if (input.description != null) body.body = input.description;
|
|
588
|
+
if (input.draft != null) body.draft = input.draft;
|
|
589
|
+
if (input.targetBranch != null) body.base = input.targetBranch;
|
|
590
|
+
if (input.stateEvent)
|
|
591
|
+
body.state = input.stateEvent === 'close' ? 'closed' : 'open';
|
|
592
|
+
|
|
593
|
+
const res = await this.api(
|
|
594
|
+
'PATCH',
|
|
595
|
+
`/repos/${projectPath}/pulls/${mrIid}`,
|
|
596
|
+
body
|
|
597
|
+
);
|
|
598
|
+
if (!res.ok) {
|
|
599
|
+
const text = await res.text();
|
|
600
|
+
throw new Error(`updatePullRequest failed: ${res.status} ${text}`);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Handle reviewers/assignees/labels replacement if provided
|
|
604
|
+
if (input.reviewers) {
|
|
605
|
+
await this.api(
|
|
606
|
+
'POST',
|
|
607
|
+
`/repos/${projectPath}/pulls/${mrIid}/requested_reviewers`,
|
|
608
|
+
{
|
|
609
|
+
reviewers: input.reviewers
|
|
610
|
+
}
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
if (input.assignees) {
|
|
614
|
+
await this.api(
|
|
615
|
+
'POST',
|
|
616
|
+
`/repos/${projectPath}/issues/${mrIid}/assignees`,
|
|
617
|
+
{
|
|
618
|
+
assignees: input.assignees
|
|
619
|
+
}
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
if (input.labels) {
|
|
623
|
+
await this.api('PUT', `/repos/${projectPath}/issues/${mrIid}/labels`, {
|
|
624
|
+
labels: input.labels
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const pr = await this.fetchSingleMR(projectPath, mrIid, null);
|
|
629
|
+
if (!pr) throw new Error('Updated PR but failed to fetch it back');
|
|
630
|
+
return pr;
|
|
631
|
+
}
|
|
632
|
+
|
|
401
633
|
async restRequest(
|
|
402
634
|
method: string,
|
|
403
635
|
path: string,
|
|
404
|
-
body?: unknown
|
|
636
|
+
body?: unknown
|
|
405
637
|
): Promise<Response> {
|
|
406
638
|
return this.api(method, path, body);
|
|
407
639
|
}
|
|
408
640
|
|
|
641
|
+
// ── MR lifecycle mutations ──────────────────────────────────────────────
|
|
642
|
+
|
|
643
|
+
async mergePullRequest(
|
|
644
|
+
projectPath: string,
|
|
645
|
+
mrIid: number,
|
|
646
|
+
input?: MergePullRequestInput
|
|
647
|
+
): Promise<PullRequest> {
|
|
648
|
+
const body: Record<string, unknown> = {};
|
|
649
|
+
if (input?.commitMessage != null) body.commit_title = input.commitMessage;
|
|
650
|
+
if (input?.squashCommitMessage != null)
|
|
651
|
+
body.commit_title = input.squashCommitMessage;
|
|
652
|
+
if (input?.shouldRemoveSourceBranch != null)
|
|
653
|
+
body.delete_branch = input.shouldRemoveSourceBranch;
|
|
654
|
+
if (input?.sha != null) body.sha = input.sha;
|
|
655
|
+
|
|
656
|
+
// Map mergeMethod / squash to GitHub's merge_method parameter.
|
|
657
|
+
// GitHub accepts: "merge", "squash", "rebase".
|
|
658
|
+
if (input?.mergeMethod) {
|
|
659
|
+
body.merge_method = input.mergeMethod;
|
|
660
|
+
} else if (input?.squash) {
|
|
661
|
+
body.merge_method = 'squash';
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const res = await this.api(
|
|
665
|
+
'PUT',
|
|
666
|
+
`/repos/${projectPath}/pulls/${mrIid}/merge`,
|
|
667
|
+
body
|
|
668
|
+
);
|
|
669
|
+
if (!res.ok) {
|
|
670
|
+
const text = await res.text();
|
|
671
|
+
throw new Error(`mergePullRequest failed: ${res.status} ${text}`);
|
|
672
|
+
}
|
|
673
|
+
const pr = await this.fetchSingleMR(projectPath, mrIid, null);
|
|
674
|
+
if (!pr) throw new Error('Merged PR but failed to fetch it back');
|
|
675
|
+
return pr;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
async approvePullRequest(projectPath: string, mrIid: number): Promise<void> {
|
|
679
|
+
const res = await this.api(
|
|
680
|
+
'POST',
|
|
681
|
+
`/repos/${projectPath}/pulls/${mrIid}/reviews`,
|
|
682
|
+
{
|
|
683
|
+
event: 'APPROVE'
|
|
684
|
+
}
|
|
685
|
+
);
|
|
686
|
+
if (!res.ok) {
|
|
687
|
+
const text = await res.text().catch(() => '');
|
|
688
|
+
throw new Error(
|
|
689
|
+
`approvePullRequest failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ''}`
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
async unapprovePullRequest(
|
|
695
|
+
_projectPath: string,
|
|
696
|
+
_mrIid: number
|
|
697
|
+
): Promise<void> {
|
|
698
|
+
// TODO: GitHub does not support unapproving via REST API.
|
|
699
|
+
// A possible workaround is to dismiss the review via
|
|
700
|
+
// PUT /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/dismissals
|
|
701
|
+
// but that requires knowing the review ID and is semantically different
|
|
702
|
+
// (dismissal vs. unapproval). Leave as stub until a use case emerges.
|
|
703
|
+
throw new Error(
|
|
704
|
+
'unapprovePullRequest is not supported by GitHub. ' +
|
|
705
|
+
'Check provider.capabilities.canUnapprove before calling.'
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
async rebasePullRequest(_projectPath: string, _mrIid: number): Promise<void> {
|
|
710
|
+
// TODO: GitHub has no native "rebase" API for pull requests.
|
|
711
|
+
// The closest equivalent is the update-branch API:
|
|
712
|
+
// PUT /repos/{owner}/{repo}/pulls/{pull_number}/update-branch
|
|
713
|
+
// which updates the PR branch with the latest from the base branch,
|
|
714
|
+
// but it's a merge (not a rebase). True rebase requires pushing
|
|
715
|
+
// locally rebased commits.
|
|
716
|
+
throw new Error(
|
|
717
|
+
'rebasePullRequest is not supported by GitHub. ' +
|
|
718
|
+
'Check provider.capabilities.canRebase before calling.'
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
async setAutoMerge(_projectPath: string, _mrIid: number): Promise<void> {
|
|
723
|
+
// TODO: GitHub supports auto-merge via GraphQL mutation:
|
|
724
|
+
// mutation { enablePullRequestAutoMerge(input: { pullRequestId: "..." }) { ... } }
|
|
725
|
+
// Requires the repository to have "Allow auto-merge" enabled in settings.
|
|
726
|
+
// The REST API does not support this — GraphQL only.
|
|
727
|
+
throw new Error(
|
|
728
|
+
'setAutoMerge is not supported by the GitHub REST API. ' +
|
|
729
|
+
'Check provider.capabilities.canAutoMerge before calling.'
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
async cancelAutoMerge(_projectPath: string, _mrIid: number): Promise<void> {
|
|
734
|
+
// TODO: GitHub GraphQL mutation:
|
|
735
|
+
// mutation { disablePullRequestAutoMerge(input: { pullRequestId: "..." }) { ... } }
|
|
736
|
+
// Same pre-requisites as setAutoMerge.
|
|
737
|
+
throw new Error(
|
|
738
|
+
'cancelAutoMerge is not supported by the GitHub REST API. ' +
|
|
739
|
+
'Check provider.capabilities.canAutoMerge before calling.'
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// ── Discussion mutations ────────────────────────────────────────────────
|
|
744
|
+
|
|
745
|
+
async resolveDiscussion(
|
|
746
|
+
_projectPath: string,
|
|
747
|
+
_mrIid: number,
|
|
748
|
+
_discussionId: string
|
|
749
|
+
): Promise<void> {
|
|
750
|
+
// TODO: GitHub GraphQL mutation:
|
|
751
|
+
// mutation { resolveReviewThread(input: { threadId: "..." }) { ... } }
|
|
752
|
+
// REST API does not support resolving review threads.
|
|
753
|
+
throw new Error(
|
|
754
|
+
'resolveDiscussion is not supported by the GitHub REST API. ' +
|
|
755
|
+
'Check provider.capabilities.canResolveDiscussions before calling.'
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
async unresolveDiscussion(
|
|
760
|
+
_projectPath: string,
|
|
761
|
+
_mrIid: number,
|
|
762
|
+
_discussionId: string
|
|
763
|
+
): Promise<void> {
|
|
764
|
+
// TODO: GitHub GraphQL mutation:
|
|
765
|
+
// mutation { unresolveReviewThread(input: { threadId: "..." }) { ... } }
|
|
766
|
+
// REST API does not support unresolving review threads.
|
|
767
|
+
throw new Error(
|
|
768
|
+
'unresolveDiscussion is not supported by the GitHub REST API. ' +
|
|
769
|
+
'Check provider.capabilities.canResolveDiscussions before calling.'
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// ── Pipeline mutations ──────────────────────────────────────────────────
|
|
774
|
+
|
|
775
|
+
async retryPipeline(projectPath: string, pipelineId: number): Promise<void> {
|
|
776
|
+
// GitHub Actions: re-run a workflow run.
|
|
777
|
+
// pipelineId maps to the workflow run ID.
|
|
778
|
+
const res = await this.api(
|
|
779
|
+
'POST',
|
|
780
|
+
`/repos/${projectPath}/actions/runs/${pipelineId}/rerun`
|
|
781
|
+
);
|
|
782
|
+
if (!res.ok) {
|
|
783
|
+
const text = await res.text().catch(() => '');
|
|
784
|
+
throw new Error(
|
|
785
|
+
`retryPipeline failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ''}`
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// ── Review mutations ────────────────────────────────────────────────────
|
|
791
|
+
|
|
792
|
+
async requestReReview(
|
|
793
|
+
projectPath: string,
|
|
794
|
+
mrIid: number,
|
|
795
|
+
reviewerUsernames?: string[]
|
|
796
|
+
): Promise<void> {
|
|
797
|
+
// GitHub: POST /repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers
|
|
798
|
+
// If no usernames provided, we'd need to fetch the current PR to get
|
|
799
|
+
// the existing reviewer list. For now, require explicit usernames.
|
|
800
|
+
if (!reviewerUsernames?.length) {
|
|
801
|
+
// Fetch current reviewers from the PR
|
|
802
|
+
const prRes = await this.api(
|
|
803
|
+
'GET',
|
|
804
|
+
`/repos/${projectPath}/pulls/${mrIid}`
|
|
805
|
+
);
|
|
806
|
+
if (!prRes.ok) {
|
|
807
|
+
throw new Error(`requestReReview: failed to fetch PR: ${prRes.status}`);
|
|
808
|
+
}
|
|
809
|
+
const pr = (await prRes.json()) as GHPullRequest;
|
|
810
|
+
reviewerUsernames = pr.requested_reviewers.map(r => r.login);
|
|
811
|
+
if (!reviewerUsernames.length) {
|
|
812
|
+
// Nothing to re-request
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const res = await this.api(
|
|
818
|
+
'POST',
|
|
819
|
+
`/repos/${projectPath}/pulls/${mrIid}/requested_reviewers`,
|
|
820
|
+
{ reviewers: reviewerUsernames }
|
|
821
|
+
);
|
|
822
|
+
if (!res.ok) {
|
|
823
|
+
const text = await res.text().catch(() => '');
|
|
824
|
+
throw new Error(
|
|
825
|
+
`requestReReview failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ''}`
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
409
830
|
// ── Private helpers ─────────────────────────────────────────────────────
|
|
410
831
|
|
|
411
832
|
private async api(
|
|
412
833
|
method: string,
|
|
413
834
|
path: string,
|
|
414
|
-
body?: unknown
|
|
835
|
+
body?: unknown
|
|
415
836
|
): Promise<Response> {
|
|
416
837
|
const url = `${this.apiBase}${path}`;
|
|
417
838
|
const headers: Record<string, string> = {
|
|
418
839
|
Authorization: `Bearer ${this.token}`,
|
|
419
|
-
Accept:
|
|
420
|
-
|
|
840
|
+
Accept: 'application/vnd.github+json',
|
|
841
|
+
'X-GitHub-Api-Version': '2022-11-28'
|
|
421
842
|
};
|
|
422
843
|
if (body !== undefined) {
|
|
423
|
-
headers[
|
|
844
|
+
headers['Content-Type'] = 'application/json';
|
|
424
845
|
}
|
|
425
846
|
return fetch(url, {
|
|
426
847
|
method,
|
|
427
848
|
headers,
|
|
428
|
-
body: body !== undefined ? JSON.stringify(body) : undefined
|
|
849
|
+
body: body !== undefined ? JSON.stringify(body) : undefined
|
|
429
850
|
});
|
|
430
851
|
}
|
|
431
852
|
|
|
@@ -436,11 +857,11 @@ export class GitHubProvider implements GitProvider {
|
|
|
436
857
|
private async searchPRs(qualifiers: string): Promise<GHPullRequest[]> {
|
|
437
858
|
const q = encodeURIComponent(qualifiers);
|
|
438
859
|
const res = await this.api(
|
|
439
|
-
|
|
440
|
-
`/search/issues?q=${q}&per_page=100&sort=updated
|
|
860
|
+
'GET',
|
|
861
|
+
`/search/issues?q=${q}&per_page=100&sort=updated`
|
|
441
862
|
);
|
|
442
863
|
if (!res.ok) {
|
|
443
|
-
this.log.warn(
|
|
864
|
+
this.log.warn('GitHub search failed', { status: res.status, qualifiers });
|
|
444
865
|
return [];
|
|
445
866
|
}
|
|
446
867
|
|
|
@@ -454,16 +875,16 @@ export class GitHubProvider implements GitProvider {
|
|
|
454
875
|
|
|
455
876
|
// The search API returns issue-shaped results; fetch full PR details
|
|
456
877
|
const prPromises = data.items
|
|
457
|
-
.filter(
|
|
458
|
-
.map(async
|
|
878
|
+
.filter(item => item.pull_request) // Only PRs
|
|
879
|
+
.map(async item => {
|
|
459
880
|
// Extract owner/repo from repository_url
|
|
460
881
|
const repoPath = item.repository_url.replace(
|
|
461
882
|
`${this.apiBase}/repos/`,
|
|
462
|
-
|
|
883
|
+
''
|
|
463
884
|
);
|
|
464
885
|
const res = await this.api(
|
|
465
|
-
|
|
466
|
-
`/repos/${repoPath}/pulls/${item.number}
|
|
886
|
+
'GET',
|
|
887
|
+
`/repos/${repoPath}/pulls/${item.number}`
|
|
467
888
|
);
|
|
468
889
|
if (!res.ok) return null;
|
|
469
890
|
return (await res.json()) as GHPullRequest;
|
|
@@ -475,21 +896,21 @@ export class GitHubProvider implements GitProvider {
|
|
|
475
896
|
|
|
476
897
|
private async fetchReviews(
|
|
477
898
|
repoPath: string,
|
|
478
|
-
prNumber: number
|
|
899
|
+
prNumber: number
|
|
479
900
|
): Promise<GHReview[]> {
|
|
480
901
|
return this.fetchAllPages<GHReview>(
|
|
481
|
-
`/repos/${repoPath}/pulls/${prNumber}/reviews?per_page=100
|
|
902
|
+
`/repos/${repoPath}/pulls/${prNumber}/reviews?per_page=100`
|
|
482
903
|
);
|
|
483
904
|
}
|
|
484
905
|
|
|
485
906
|
private async fetchCheckRuns(
|
|
486
907
|
repoPath: string,
|
|
487
|
-
sha: string
|
|
908
|
+
sha: string
|
|
488
909
|
): Promise<GHCheckRun[]> {
|
|
489
910
|
try {
|
|
490
911
|
const res = await this.api(
|
|
491
|
-
|
|
492
|
-
`/repos/${repoPath}/commits/${sha}/check-runs?per_page=100
|
|
912
|
+
'GET',
|
|
913
|
+
`/repos/${repoPath}/commits/${sha}/check-runs?per_page=100`
|
|
493
914
|
);
|
|
494
915
|
if (!res.ok) return [];
|
|
495
916
|
const data = (await res.json()) as GHCheckSuite;
|
|
@@ -504,17 +925,17 @@ export class GitHubProvider implements GitProvider {
|
|
|
504
925
|
let url: string | null = path;
|
|
505
926
|
|
|
506
927
|
while (url) {
|
|
507
|
-
const res = await this.api(
|
|
928
|
+
const res = await this.api('GET', url);
|
|
508
929
|
if (!res.ok) break;
|
|
509
930
|
const items = (await res.json()) as T[];
|
|
510
931
|
results.push(...items);
|
|
511
932
|
|
|
512
933
|
// Parse Link header for pagination
|
|
513
|
-
const linkHeader = res.headers.get(
|
|
934
|
+
const linkHeader = res.headers.get('Link');
|
|
514
935
|
const nextMatch = linkHeader?.match(/<([^>]+)>;\s*rel="next"/);
|
|
515
936
|
if (nextMatch) {
|
|
516
937
|
// Strip apiBase prefix — `api()` will re-add it
|
|
517
|
-
url = nextMatch[1]!.replace(this.apiBase,
|
|
938
|
+
url = nextMatch[1]!.replace(this.apiBase, '');
|
|
518
939
|
} else {
|
|
519
940
|
url = null;
|
|
520
941
|
}
|
|
@@ -530,14 +951,13 @@ export class GitHubProvider implements GitProvider {
|
|
|
530
951
|
pr: GHPullRequest,
|
|
531
952
|
roles: string[],
|
|
532
953
|
reviews: GHReview[],
|
|
533
|
-
checkRuns: GHCheckRun[]
|
|
954
|
+
checkRuns: GHCheckRun[]
|
|
534
955
|
): PullRequest {
|
|
535
956
|
// Compute approvals: latest review per user, count "APPROVED" ones
|
|
536
957
|
const latestReviewByUser = new Map<number, GHReview>();
|
|
537
958
|
for (const r of reviews.sort(
|
|
538
959
|
(a, b) =>
|
|
539
|
-
new Date(a.submitted_at).getTime() -
|
|
540
|
-
new Date(b.submitted_at).getTime(),
|
|
960
|
+
new Date(a.submitted_at).getTime() - new Date(b.submitted_at).getTime()
|
|
541
961
|
)) {
|
|
542
962
|
latestReviewByUser.set(r.user.id, r);
|
|
543
963
|
}
|
|
@@ -545,9 +965,9 @@ export class GitHubProvider implements GitProvider {
|
|
|
545
965
|
const approvedBy: UserRef[] = [];
|
|
546
966
|
let changesRequested = 0;
|
|
547
967
|
for (const r of latestReviewByUser.values()) {
|
|
548
|
-
if (r.state ===
|
|
968
|
+
if (r.state === 'APPROVED') {
|
|
549
969
|
approvedBy.push(toUserRef(r.user));
|
|
550
|
-
} else if (r.state ===
|
|
970
|
+
} else if (r.state === 'CHANGES_REQUESTED') {
|
|
551
971
|
changesRequested++;
|
|
552
972
|
}
|
|
553
973
|
}
|
|
@@ -561,13 +981,12 @@ export class GitHubProvider implements GitProvider {
|
|
|
561
981
|
? {
|
|
562
982
|
additions: pr.additions!,
|
|
563
983
|
deletions: pr.deletions ?? 0,
|
|
564
|
-
filesChanged: pr.changed_files ?? 0
|
|
984
|
+
filesChanged: pr.changed_files ?? 0
|
|
565
985
|
}
|
|
566
986
|
: null;
|
|
567
987
|
|
|
568
988
|
// Conflicts: GitHub's mergeable_state "dirty" indicates conflicts
|
|
569
|
-
const conflicts =
|
|
570
|
-
pr.mergeable === false || pr.mergeable_state === "dirty";
|
|
989
|
+
const conflicts = pr.mergeable === false || pr.mergeable_state === 'dirty';
|
|
571
990
|
|
|
572
991
|
const pipeline = toPipeline(checkRuns, pr.html_url);
|
|
573
992
|
|
|
@@ -596,7 +1015,7 @@ export class GitHubProvider implements GitProvider {
|
|
|
596
1015
|
approved: approvedBy.length > 0 && changesRequested === 0,
|
|
597
1016
|
approvedBy,
|
|
598
1017
|
diffStats,
|
|
599
|
-
detailedMergeStatus: null
|
|
1018
|
+
detailedMergeStatus: null // GitHub-specific status not applicable
|
|
600
1019
|
};
|
|
601
1020
|
}
|
|
602
1021
|
}
|
|
@@ -612,7 +1031,7 @@ function toNote(c: GHComment): Note {
|
|
|
612
1031
|
oldPath: c.path,
|
|
613
1032
|
newLine: c.line ?? null,
|
|
614
1033
|
oldLine: c.original_line ?? null,
|
|
615
|
-
positionType: c.path ?
|
|
1034
|
+
positionType: c.path ? 'text' : null
|
|
616
1035
|
}
|
|
617
1036
|
: null;
|
|
618
1037
|
|
|
@@ -622,10 +1041,10 @@ function toNote(c: GHComment): Note {
|
|
|
622
1041
|
author: toNoteAuthor(c.user),
|
|
623
1042
|
createdAt: c.created_at,
|
|
624
1043
|
system: false,
|
|
625
|
-
type: c.path ?
|
|
1044
|
+
type: c.path ? 'DiffNote' : 'DiscussionNote',
|
|
626
1045
|
resolvable: c.path ? true : null,
|
|
627
1046
|
resolved: null,
|
|
628
|
-
position
|
|
1047
|
+
position
|
|
629
1048
|
};
|
|
630
1049
|
}
|
|
631
1050
|
|
|
@@ -634,6 +1053,6 @@ function toNoteAuthor(u: GHUser): NoteAuthor {
|
|
|
634
1053
|
id: `github:user:${u.id}`,
|
|
635
1054
|
username: u.login,
|
|
636
1055
|
name: u.name ?? u.login,
|
|
637
|
-
avatarUrl: u.avatar_url
|
|
1056
|
+
avatarUrl: u.avatar_url
|
|
638
1057
|
};
|
|
639
1058
|
}
|