opencode-sdlc-plugin 1.0.0-alpha.0 → 1.1.1
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/config/presets/event-modeling.json +13 -2
- package/config/presets/minimal.json +29 -16
- package/config/presets/standard.json +13 -2
- package/dist/cli/index.js +1473 -1530
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +703 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin/index.js +703 -0
- package/dist/plugin/index.js.map +1 -1
- package/package.json +2 -1
- package/config/presets/copilot-only.json +0 -76
- package/config/presets/enterprise.json +0 -79
- package/config/presets/solo-quick.json +0 -70
- package/config/presets/strict-tdd.json +0 -79
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { tmpdir, homedir, platform } from 'os';
|
|
2
2
|
import { existsSync, readFileSync, appendFileSync, mkdirSync, writeFileSync, statSync, truncateSync } from 'fs';
|
|
3
3
|
import { join, dirname } from 'path';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import { Octokit } from '@octokit/rest';
|
|
4
6
|
import { minimatch } from 'minimatch';
|
|
5
7
|
import { z } from 'zod';
|
|
6
8
|
import { readFile, mkdir, writeFile, readdir } from 'fs/promises';
|
|
@@ -173,14 +175,692 @@ function handleSessionError(event, tracker) {
|
|
|
173
175
|
});
|
|
174
176
|
}
|
|
175
177
|
}
|
|
178
|
+
function resolveGitHubToken(explicitToken) {
|
|
179
|
+
if (explicitToken) {
|
|
180
|
+
return explicitToken;
|
|
181
|
+
}
|
|
182
|
+
const envToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
183
|
+
if (envToken) {
|
|
184
|
+
return envToken;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const ghToken = execSync("gh auth token", {
|
|
188
|
+
encoding: "utf-8",
|
|
189
|
+
timeout: 5e3,
|
|
190
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
191
|
+
}).trim();
|
|
192
|
+
if (ghToken) {
|
|
193
|
+
return ghToken;
|
|
194
|
+
}
|
|
195
|
+
} catch {
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
function isGitHubAuthenticated(token) {
|
|
200
|
+
return resolveGitHubToken(token) !== null;
|
|
201
|
+
}
|
|
202
|
+
var GitHubClient = class {
|
|
203
|
+
octokit;
|
|
204
|
+
owner;
|
|
205
|
+
repo;
|
|
206
|
+
constructor(options = {}) {
|
|
207
|
+
const token = resolveGitHubToken(options.token);
|
|
208
|
+
if (!token) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
"GitHub authentication not found. Set GITHUB_TOKEN environment variable or run 'gh auth login'."
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
this.octokit = new Octokit({ auth: token });
|
|
214
|
+
this.owner = options.owner;
|
|
215
|
+
this.repo = options.repo;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Set the default owner/repo for operations
|
|
219
|
+
*/
|
|
220
|
+
setRepo(owner, repo) {
|
|
221
|
+
this.owner = owner;
|
|
222
|
+
this.repo = repo;
|
|
223
|
+
}
|
|
224
|
+
getRepoParams(owner, repo) {
|
|
225
|
+
const o = owner || this.owner;
|
|
226
|
+
const r = repo || this.repo;
|
|
227
|
+
if (!o || !r) {
|
|
228
|
+
throw new Error("Repository owner and name are required");
|
|
229
|
+
}
|
|
230
|
+
return { owner: o, repo: r };
|
|
231
|
+
}
|
|
232
|
+
// ==========================================================================
|
|
233
|
+
// Issues
|
|
234
|
+
// ==========================================================================
|
|
235
|
+
/**
|
|
236
|
+
* Get a single issue by number
|
|
237
|
+
*/
|
|
238
|
+
async getIssue(issueNumber, owner, repo) {
|
|
239
|
+
const params = this.getRepoParams(owner, repo);
|
|
240
|
+
const { data } = await this.octokit.issues.get({
|
|
241
|
+
...params,
|
|
242
|
+
issue_number: issueNumber
|
|
243
|
+
});
|
|
244
|
+
return {
|
|
245
|
+
number: data.number,
|
|
246
|
+
title: data.title,
|
|
247
|
+
body: data.body ?? null,
|
|
248
|
+
url: data.html_url,
|
|
249
|
+
state: data.state,
|
|
250
|
+
labels: (data.labels || []).map(
|
|
251
|
+
(label) => typeof label === "string" ? { name: label } : { name: label.name || "" }
|
|
252
|
+
)
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* List issues in a repository
|
|
257
|
+
*/
|
|
258
|
+
async listIssues(options = {}, owner, repo) {
|
|
259
|
+
const params = this.getRepoParams(owner, repo);
|
|
260
|
+
const { data } = await this.octokit.issues.listForRepo({
|
|
261
|
+
...params,
|
|
262
|
+
state: options.state || "open",
|
|
263
|
+
labels: options.labels?.join(","),
|
|
264
|
+
per_page: options.limit || 30
|
|
265
|
+
});
|
|
266
|
+
return data.filter((issue) => !issue.pull_request).map((issue) => ({
|
|
267
|
+
number: issue.number,
|
|
268
|
+
title: issue.title,
|
|
269
|
+
url: issue.html_url,
|
|
270
|
+
state: issue.state
|
|
271
|
+
}));
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Update an issue's body
|
|
275
|
+
*/
|
|
276
|
+
async updateIssueBody(issueNumber, body, owner, repo) {
|
|
277
|
+
const params = this.getRepoParams(owner, repo);
|
|
278
|
+
await this.octokit.issues.update({
|
|
279
|
+
...params,
|
|
280
|
+
issue_number: issueNumber,
|
|
281
|
+
body
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Add labels to an issue
|
|
286
|
+
*/
|
|
287
|
+
async addLabels(issueNumber, labels, owner, repo) {
|
|
288
|
+
const params = this.getRepoParams(owner, repo);
|
|
289
|
+
await this.octokit.issues.addLabels({
|
|
290
|
+
...params,
|
|
291
|
+
issue_number: issueNumber,
|
|
292
|
+
labels
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
// ==========================================================================
|
|
296
|
+
// Pull Requests
|
|
297
|
+
// ==========================================================================
|
|
298
|
+
/**
|
|
299
|
+
* Create a pull request
|
|
300
|
+
*/
|
|
301
|
+
async createPullRequest(options, owner, repo) {
|
|
302
|
+
const params = this.getRepoParams(owner, repo);
|
|
303
|
+
const { data } = await this.octokit.pulls.create({
|
|
304
|
+
...params,
|
|
305
|
+
title: options.title,
|
|
306
|
+
body: options.body,
|
|
307
|
+
head: options.head,
|
|
308
|
+
base: options.base || "main",
|
|
309
|
+
draft: options.draft
|
|
310
|
+
});
|
|
311
|
+
return {
|
|
312
|
+
number: data.number,
|
|
313
|
+
url: data.html_url
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Get pull request details
|
|
318
|
+
*/
|
|
319
|
+
async getPullRequest(prNumber, owner, repo) {
|
|
320
|
+
const params = this.getRepoParams(owner, repo);
|
|
321
|
+
const { data } = await this.octokit.pulls.get({
|
|
322
|
+
...params,
|
|
323
|
+
pull_number: prNumber
|
|
324
|
+
});
|
|
325
|
+
return {
|
|
326
|
+
number: data.number,
|
|
327
|
+
title: data.title,
|
|
328
|
+
body: data.body,
|
|
329
|
+
url: data.html_url,
|
|
330
|
+
state: data.merged ? "merged" : data.state,
|
|
331
|
+
mergeable: data.mergeable,
|
|
332
|
+
mergeableState: data.mergeable_state,
|
|
333
|
+
draft: data.draft || false
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Get pull request status including checks and reviews
|
|
338
|
+
*/
|
|
339
|
+
async getPullRequestStatus(prNumber, owner, repo) {
|
|
340
|
+
const params = this.getRepoParams(owner, repo);
|
|
341
|
+
const pr = await this.getPullRequest(prNumber, owner, repo);
|
|
342
|
+
const { data: prData } = await this.octokit.pulls.get({
|
|
343
|
+
...params,
|
|
344
|
+
pull_number: prNumber
|
|
345
|
+
});
|
|
346
|
+
let checks = [];
|
|
347
|
+
try {
|
|
348
|
+
const { data: checksData } = await this.octokit.checks.listForRef({
|
|
349
|
+
...params,
|
|
350
|
+
ref: prData.head.sha
|
|
351
|
+
});
|
|
352
|
+
checks = checksData.check_runs.map((check) => ({
|
|
353
|
+
name: check.name,
|
|
354
|
+
status: check.status === "completed" ? check.conclusion === "success" ? "pass" : check.conclusion === "skipped" ? "skipped" : "fail" : "pending",
|
|
355
|
+
conclusion: check.conclusion
|
|
356
|
+
}));
|
|
357
|
+
} catch {
|
|
358
|
+
}
|
|
359
|
+
const { data: reviewsData } = await this.octokit.pulls.listReviews({
|
|
360
|
+
...params,
|
|
361
|
+
pull_number: prNumber
|
|
362
|
+
});
|
|
363
|
+
const reviews = reviewsData.map((review) => ({
|
|
364
|
+
reviewer: review.user?.login || "unknown",
|
|
365
|
+
state: review.state
|
|
366
|
+
}));
|
|
367
|
+
return {
|
|
368
|
+
number: pr.number,
|
|
369
|
+
url: pr.url,
|
|
370
|
+
state: pr.state,
|
|
371
|
+
mergeable: pr.mergeable,
|
|
372
|
+
checks,
|
|
373
|
+
reviews
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Get pull request diff
|
|
378
|
+
*/
|
|
379
|
+
async getPullRequestDiff(prNumber, owner, repo) {
|
|
380
|
+
const params = this.getRepoParams(owner, repo);
|
|
381
|
+
const { data } = await this.octokit.pulls.get({
|
|
382
|
+
...params,
|
|
383
|
+
pull_number: prNumber,
|
|
384
|
+
mediaType: { format: "diff" }
|
|
385
|
+
});
|
|
386
|
+
return data;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Merge a pull request
|
|
390
|
+
*/
|
|
391
|
+
async mergePullRequest(options, owner, repo) {
|
|
392
|
+
const params = this.getRepoParams(owner, repo);
|
|
393
|
+
const { data } = await this.octokit.pulls.merge({
|
|
394
|
+
...params,
|
|
395
|
+
pull_number: options.prNumber,
|
|
396
|
+
merge_method: options.mergeMethod || "squash",
|
|
397
|
+
commit_title: options.commitTitle,
|
|
398
|
+
commit_message: options.commitMessage
|
|
399
|
+
});
|
|
400
|
+
return {
|
|
401
|
+
sha: data.sha,
|
|
402
|
+
merged: data.merged
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
// ==========================================================================
|
|
406
|
+
// Repositories
|
|
407
|
+
// ==========================================================================
|
|
408
|
+
/**
|
|
409
|
+
* Create a new repository (personal or in an organization)
|
|
410
|
+
*/
|
|
411
|
+
async createRepository(options) {
|
|
412
|
+
let data;
|
|
413
|
+
if (options.org) {
|
|
414
|
+
const response = await this.octokit.repos.createInOrg({
|
|
415
|
+
org: options.org,
|
|
416
|
+
name: options.name,
|
|
417
|
+
description: options.description,
|
|
418
|
+
private: options.private ?? false,
|
|
419
|
+
auto_init: options.autoInit ?? false
|
|
420
|
+
});
|
|
421
|
+
data = response.data;
|
|
422
|
+
} else {
|
|
423
|
+
const response = await this.octokit.repos.createForAuthenticatedUser({
|
|
424
|
+
name: options.name,
|
|
425
|
+
description: options.description,
|
|
426
|
+
private: options.private ?? false,
|
|
427
|
+
auto_init: options.autoInit ?? false
|
|
428
|
+
});
|
|
429
|
+
data = response.data;
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
id: data.id,
|
|
433
|
+
name: data.name,
|
|
434
|
+
fullName: data.full_name,
|
|
435
|
+
url: data.html_url,
|
|
436
|
+
private: data.private,
|
|
437
|
+
defaultBranch: data.default_branch || "main"
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* List organizations the authenticated user belongs to
|
|
442
|
+
*/
|
|
443
|
+
async listOrganizations() {
|
|
444
|
+
const query = `
|
|
445
|
+
query {
|
|
446
|
+
viewer {
|
|
447
|
+
organizations(first: 100) {
|
|
448
|
+
nodes {
|
|
449
|
+
id
|
|
450
|
+
databaseId
|
|
451
|
+
login
|
|
452
|
+
name
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
`;
|
|
458
|
+
const result = await this.octokit.graphql(query);
|
|
459
|
+
return result.viewer.organizations.nodes.map((org) => ({
|
|
460
|
+
id: String(org.databaseId),
|
|
461
|
+
nodeId: org.id,
|
|
462
|
+
login: org.login,
|
|
463
|
+
name: org.name
|
|
464
|
+
}));
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Get repository details
|
|
468
|
+
*/
|
|
469
|
+
async getRepository(owner, repo) {
|
|
470
|
+
const params = this.getRepoParams(owner, repo);
|
|
471
|
+
const { data } = await this.octokit.repos.get(params);
|
|
472
|
+
return {
|
|
473
|
+
id: data.id,
|
|
474
|
+
name: data.name,
|
|
475
|
+
fullName: data.full_name,
|
|
476
|
+
url: data.html_url,
|
|
477
|
+
private: data.private,
|
|
478
|
+
defaultBranch: data.default_branch || "main"
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* List repositories for the authenticated user
|
|
483
|
+
*/
|
|
484
|
+
async listUserRepos(options = {}) {
|
|
485
|
+
const { data } = await this.octokit.repos.listForAuthenticatedUser({
|
|
486
|
+
type: options.type || "owner",
|
|
487
|
+
per_page: options.limit || 30,
|
|
488
|
+
sort: "updated"
|
|
489
|
+
});
|
|
490
|
+
return data.map((repo) => ({
|
|
491
|
+
id: repo.id,
|
|
492
|
+
name: repo.name,
|
|
493
|
+
fullName: repo.full_name,
|
|
494
|
+
url: repo.html_url,
|
|
495
|
+
private: repo.private,
|
|
496
|
+
defaultBranch: repo.default_branch || "main"
|
|
497
|
+
}));
|
|
498
|
+
}
|
|
499
|
+
// ==========================================================================
|
|
500
|
+
// Projects (GraphQL required for Projects V2)
|
|
501
|
+
// ==========================================================================
|
|
502
|
+
/**
|
|
503
|
+
* List projects for the authenticated user
|
|
504
|
+
* Note: Projects V2 requires GraphQL API
|
|
505
|
+
*/
|
|
506
|
+
async listUserProjects(limit = 20) {
|
|
507
|
+
const query = `
|
|
508
|
+
query($first: Int!) {
|
|
509
|
+
viewer {
|
|
510
|
+
projectsV2(first: $first) {
|
|
511
|
+
nodes {
|
|
512
|
+
id
|
|
513
|
+
number
|
|
514
|
+
title
|
|
515
|
+
url
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
`;
|
|
521
|
+
const result = await this.octokit.graphql(query, { first: limit });
|
|
522
|
+
return result.viewer.projectsV2.nodes;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Get project by number for a user
|
|
526
|
+
*/
|
|
527
|
+
async getUserProject(userLogin, projectNumber) {
|
|
528
|
+
const query = `
|
|
529
|
+
query($login: String!, $number: Int!) {
|
|
530
|
+
user(login: $login) {
|
|
531
|
+
projectV2(number: $number) {
|
|
532
|
+
id
|
|
533
|
+
number
|
|
534
|
+
title
|
|
535
|
+
url
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
`;
|
|
540
|
+
try {
|
|
541
|
+
const result = await this.octokit.graphql(query, { login: userLogin, number: projectNumber });
|
|
542
|
+
return result.user.projectV2;
|
|
543
|
+
} catch {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Get project by number for an organization
|
|
549
|
+
*/
|
|
550
|
+
async getOrgProject(orgLogin, projectNumber) {
|
|
551
|
+
const query = `
|
|
552
|
+
query($login: String!, $number: Int!) {
|
|
553
|
+
organization(login: $login) {
|
|
554
|
+
projectV2(number: $number) {
|
|
555
|
+
id
|
|
556
|
+
number
|
|
557
|
+
title
|
|
558
|
+
url
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
`;
|
|
563
|
+
try {
|
|
564
|
+
const result = await this.octokit.graphql(query, { login: orgLogin, number: projectNumber });
|
|
565
|
+
return result.organization.projectV2;
|
|
566
|
+
} catch {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* List projects for an organization
|
|
572
|
+
*/
|
|
573
|
+
async listOrgProjects(orgLogin, limit = 20) {
|
|
574
|
+
const query = `
|
|
575
|
+
query($login: String!, $first: Int!) {
|
|
576
|
+
organization(login: $login) {
|
|
577
|
+
projectsV2(first: $first) {
|
|
578
|
+
nodes {
|
|
579
|
+
id
|
|
580
|
+
number
|
|
581
|
+
title
|
|
582
|
+
url
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
`;
|
|
588
|
+
try {
|
|
589
|
+
const result = await this.octokit.graphql(query, { login: orgLogin, first: limit });
|
|
590
|
+
return result.organization.projectsV2.nodes;
|
|
591
|
+
} catch {
|
|
592
|
+
return [];
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Create a new project
|
|
597
|
+
*/
|
|
598
|
+
async createProject(options) {
|
|
599
|
+
let ownerId;
|
|
600
|
+
if (options.isOrg) {
|
|
601
|
+
const query = `
|
|
602
|
+
query($login: String!) {
|
|
603
|
+
organization(login: $login) {
|
|
604
|
+
id
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
`;
|
|
608
|
+
const result2 = await this.octokit.graphql(query, { login: options.ownerLogin });
|
|
609
|
+
ownerId = result2.organization.id;
|
|
610
|
+
} else {
|
|
611
|
+
const query = `
|
|
612
|
+
query($login: String!) {
|
|
613
|
+
user(login: $login) {
|
|
614
|
+
id
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
`;
|
|
618
|
+
const result2 = await this.octokit.graphql(query, { login: options.ownerLogin });
|
|
619
|
+
ownerId = result2.user.id;
|
|
620
|
+
}
|
|
621
|
+
const mutation = `
|
|
622
|
+
mutation($ownerId: ID!, $title: String!) {
|
|
623
|
+
createProjectV2(input: { ownerId: $ownerId, title: $title }) {
|
|
624
|
+
projectV2 {
|
|
625
|
+
id
|
|
626
|
+
number
|
|
627
|
+
title
|
|
628
|
+
url
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
`;
|
|
633
|
+
const result = await this.octokit.graphql(mutation, { ownerId, title: options.title });
|
|
634
|
+
return result.createProjectV2.projectV2;
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Copy an existing project
|
|
638
|
+
*/
|
|
639
|
+
async copyProject(options) {
|
|
640
|
+
let sourceProject;
|
|
641
|
+
if (options.sourceIsOrg) {
|
|
642
|
+
sourceProject = await this.getOrgProject(
|
|
643
|
+
options.sourceOwnerLogin,
|
|
644
|
+
options.sourceProjectNumber
|
|
645
|
+
);
|
|
646
|
+
} else {
|
|
647
|
+
sourceProject = await this.getUserProject(
|
|
648
|
+
options.sourceOwnerLogin,
|
|
649
|
+
options.sourceProjectNumber
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
if (!sourceProject) {
|
|
653
|
+
throw new Error(
|
|
654
|
+
`Source project #${options.sourceProjectNumber} not found for ${options.sourceOwnerLogin}`
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
let targetOwnerId;
|
|
658
|
+
if (options.targetIsOrg) {
|
|
659
|
+
const query = `
|
|
660
|
+
query($login: String!) {
|
|
661
|
+
organization(login: $login) {
|
|
662
|
+
id
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
`;
|
|
666
|
+
const result2 = await this.octokit.graphql(query, { login: options.targetOwnerLogin });
|
|
667
|
+
targetOwnerId = result2.organization.id;
|
|
668
|
+
} else {
|
|
669
|
+
const query = `
|
|
670
|
+
query($login: String!) {
|
|
671
|
+
user(login: $login) {
|
|
672
|
+
id
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
`;
|
|
676
|
+
const result2 = await this.octokit.graphql(query, { login: options.targetOwnerLogin });
|
|
677
|
+
targetOwnerId = result2.user.id;
|
|
678
|
+
}
|
|
679
|
+
const mutation = `
|
|
680
|
+
mutation($projectId: ID!, $ownerId: ID!, $title: String!, $includeDraftIssues: Boolean) {
|
|
681
|
+
copyProjectV2(input: {
|
|
682
|
+
projectId: $projectId,
|
|
683
|
+
ownerId: $ownerId,
|
|
684
|
+
title: $title,
|
|
685
|
+
includeDraftIssues: $includeDraftIssues
|
|
686
|
+
}) {
|
|
687
|
+
projectV2 {
|
|
688
|
+
id
|
|
689
|
+
number
|
|
690
|
+
title
|
|
691
|
+
url
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
`;
|
|
696
|
+
const result = await this.octokit.graphql(mutation, {
|
|
697
|
+
projectId: sourceProject.id,
|
|
698
|
+
ownerId: targetOwnerId,
|
|
699
|
+
title: options.title,
|
|
700
|
+
includeDraftIssues: options.includeDraftIssues ?? false
|
|
701
|
+
});
|
|
702
|
+
return result.copyProjectV2.projectV2;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Link a repository to a project
|
|
706
|
+
* Note: This creates a linked repository in the project
|
|
707
|
+
*/
|
|
708
|
+
async linkRepoToProject(projectId, repoOwner, repoName) {
|
|
709
|
+
const { data: repoData } = await this.octokit.repos.get({
|
|
710
|
+
owner: repoOwner,
|
|
711
|
+
repo: repoName
|
|
712
|
+
});
|
|
713
|
+
const mutation = `
|
|
714
|
+
mutation($projectId: ID!, $repositoryId: ID!) {
|
|
715
|
+
linkProjectV2ToRepository(input: { projectId: $projectId, repositoryId: $repositoryId }) {
|
|
716
|
+
repository {
|
|
717
|
+
id
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
`;
|
|
722
|
+
await this.octokit.graphql(mutation, {
|
|
723
|
+
projectId,
|
|
724
|
+
repositoryId: repoData.node_id
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Add an issue to a project
|
|
729
|
+
*/
|
|
730
|
+
async addIssueToProject(projectId, issueNodeId) {
|
|
731
|
+
const mutation = `
|
|
732
|
+
mutation($projectId: ID!, $contentId: ID!) {
|
|
733
|
+
addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) {
|
|
734
|
+
item {
|
|
735
|
+
id
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
`;
|
|
740
|
+
const result = await this.octokit.graphql(mutation, {
|
|
741
|
+
projectId,
|
|
742
|
+
contentId: issueNodeId
|
|
743
|
+
});
|
|
744
|
+
return result.addProjectV2ItemById.item.id;
|
|
745
|
+
}
|
|
746
|
+
// ==========================================================================
|
|
747
|
+
// Branch Protection / Rulesets
|
|
748
|
+
// ==========================================================================
|
|
749
|
+
/**
|
|
750
|
+
* Create a branch ruleset with common protection rules
|
|
751
|
+
*/
|
|
752
|
+
async createBranchRuleset(options, owner, repo) {
|
|
753
|
+
const params = this.getRepoParams(owner, repo);
|
|
754
|
+
const rules = [];
|
|
755
|
+
if (options.requirePullRequest) {
|
|
756
|
+
rules.push({
|
|
757
|
+
type: "pull_request",
|
|
758
|
+
parameters: {
|
|
759
|
+
dismiss_stale_reviews_on_push: options.dismissStaleReviews ?? false,
|
|
760
|
+
require_code_owner_review: options.requireCodeOwnerReview ?? false,
|
|
761
|
+
require_last_push_approval: false,
|
|
762
|
+
required_approving_review_count: options.requiredApprovals ?? 1,
|
|
763
|
+
required_review_thread_resolution: false
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
if (options.requireSignedCommits) {
|
|
768
|
+
rules.push({ type: "required_signatures" });
|
|
769
|
+
}
|
|
770
|
+
if (options.requireLinearHistory) {
|
|
771
|
+
rules.push({ type: "required_linear_history" });
|
|
772
|
+
}
|
|
773
|
+
if (options.preventDeletion) {
|
|
774
|
+
rules.push({ type: "deletion" });
|
|
775
|
+
}
|
|
776
|
+
if (options.preventForcePush) {
|
|
777
|
+
rules.push({ type: "non_fast_forward" });
|
|
778
|
+
}
|
|
779
|
+
const { data } = await this.octokit.repos.createRepoRuleset({
|
|
780
|
+
...params,
|
|
781
|
+
name: options.name,
|
|
782
|
+
enforcement: options.enforcement,
|
|
783
|
+
conditions: {
|
|
784
|
+
ref_name: {
|
|
785
|
+
include: options.targetBranches,
|
|
786
|
+
exclude: []
|
|
787
|
+
}
|
|
788
|
+
},
|
|
789
|
+
rules
|
|
790
|
+
});
|
|
791
|
+
return { id: data.id };
|
|
792
|
+
}
|
|
793
|
+
// ==========================================================================
|
|
794
|
+
// Utility Methods
|
|
795
|
+
// ==========================================================================
|
|
796
|
+
/**
|
|
797
|
+
* Get the authenticated user's login
|
|
798
|
+
*/
|
|
799
|
+
async getAuthenticatedUser() {
|
|
800
|
+
const { data } = await this.octokit.users.getAuthenticated();
|
|
801
|
+
return { login: data.login, id: data.id };
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Check if a repository exists
|
|
805
|
+
*/
|
|
806
|
+
async repoExists(owner, repo) {
|
|
807
|
+
try {
|
|
808
|
+
await this.octokit.repos.get({ owner, repo });
|
|
809
|
+
return true;
|
|
810
|
+
} catch {
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
function createGitHubClientIfAuthenticated(options = {}) {
|
|
816
|
+
if (!isGitHubAuthenticated(options.token)) {
|
|
817
|
+
return null;
|
|
818
|
+
}
|
|
819
|
+
try {
|
|
820
|
+
return new GitHubClient(options);
|
|
821
|
+
} catch {
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
176
825
|
|
|
177
826
|
// src/plugin/utils/github-issues.ts
|
|
178
827
|
var log3 = createPluginLogger("github-issues");
|
|
828
|
+
var cachedClient = null;
|
|
829
|
+
var cachedClientRepo = null;
|
|
830
|
+
function getClient(config) {
|
|
831
|
+
if (!config.github?.owner || !config.github?.repo) return null;
|
|
832
|
+
const repoKey = `${config.github.owner}/${config.github.repo}`;
|
|
833
|
+
if (cachedClient && cachedClientRepo === repoKey) {
|
|
834
|
+
return cachedClient;
|
|
835
|
+
}
|
|
836
|
+
cachedClient = createGitHubClientIfAuthenticated({
|
|
837
|
+
owner: config.github.owner,
|
|
838
|
+
repo: config.github.repo
|
|
839
|
+
});
|
|
840
|
+
cachedClientRepo = cachedClient ? repoKey : null;
|
|
841
|
+
return cachedClient;
|
|
842
|
+
}
|
|
179
843
|
function getRepo(config) {
|
|
180
844
|
if (!config.github?.owner || !config.github?.repo) return null;
|
|
181
845
|
return { owner: config.github.owner, repo: config.github.repo };
|
|
182
846
|
}
|
|
183
847
|
async function fetchIssue(ctx, config, issueNumber) {
|
|
848
|
+
const client = getClient(config);
|
|
849
|
+
if (client) {
|
|
850
|
+
try {
|
|
851
|
+
const issue = await client.getIssue(issueNumber);
|
|
852
|
+
return {
|
|
853
|
+
number: issue.number,
|
|
854
|
+
title: issue.title,
|
|
855
|
+
body: issue.body,
|
|
856
|
+
url: issue.url,
|
|
857
|
+
state: issue.state,
|
|
858
|
+
labels: issue.labels
|
|
859
|
+
};
|
|
860
|
+
} catch (error) {
|
|
861
|
+
log3.warn("Failed to fetch issue via Octokit", { issueNumber, error: String(error) });
|
|
862
|
+
}
|
|
863
|
+
}
|
|
184
864
|
const repo = getRepo(config);
|
|
185
865
|
if (!repo) return null;
|
|
186
866
|
try {
|
|
@@ -194,6 +874,20 @@ async function fetchIssue(ctx, config, issueNumber) {
|
|
|
194
874
|
}
|
|
195
875
|
}
|
|
196
876
|
async function listIssues(ctx, config, status, limit = 20) {
|
|
877
|
+
const client = getClient(config);
|
|
878
|
+
if (client && !status) {
|
|
879
|
+
try {
|
|
880
|
+
const issues = await client.listIssues({ limit });
|
|
881
|
+
return issues.map((issue) => ({
|
|
882
|
+
number: issue.number,
|
|
883
|
+
title: issue.title,
|
|
884
|
+
url: issue.url,
|
|
885
|
+
state: issue.state
|
|
886
|
+
}));
|
|
887
|
+
} catch (error) {
|
|
888
|
+
log3.warn("Failed to list issues via Octokit", { error: String(error) });
|
|
889
|
+
}
|
|
890
|
+
}
|
|
197
891
|
const repo = getRepo(config);
|
|
198
892
|
if (!repo) return [];
|
|
199
893
|
try {
|
|
@@ -219,6 +913,15 @@ async function moveIssueToStatus(ctx, config, issueNumber, status) {
|
|
|
219
913
|
}
|
|
220
914
|
}
|
|
221
915
|
async function updateIssueBody(ctx, config, issueNumber, body) {
|
|
916
|
+
const client = getClient(config);
|
|
917
|
+
if (client) {
|
|
918
|
+
try {
|
|
919
|
+
await client.updateIssueBody(issueNumber, body);
|
|
920
|
+
return true;
|
|
921
|
+
} catch (error) {
|
|
922
|
+
log3.warn("Failed to update issue body via Octokit", { issueNumber, error: String(error) });
|
|
923
|
+
}
|
|
924
|
+
}
|
|
222
925
|
const repo = getRepo(config);
|
|
223
926
|
if (!repo) return false;
|
|
224
927
|
try {
|