@@ -280,6 +280,10 @@ export class GitHubService {
280280 /**
281281 * Get installation access token
282282 */
283+ if ( ! installationId ) {
284+ throw new Error ( 'installationId is required for getting repositories' ) ;
285+ }
286+
283287 const accessToken = await this . createInstallationToken ( installationId ) ;
284288
285289 /**
@@ -351,17 +355,17 @@ export class GitHubService {
351355 }
352356
353357 /**
354- * Create a GitHub issue
358+ * Create a GitHub issue using GitHub App installation token
355359 *
356360 * @param {string } repoFullName - Repository full name (owner/repo)
357- * @param {string } installationId - GitHub App installation ID
361+ * @param {string | null } installationId - GitHub App installation ID
358362 * @param {IssueData } issueData - Issue data (title, body, labels)
359363 * @returns {Promise<GitHubIssue> } Created issue
360364 * @throws {Error } If issue creation fails
361365 */
362366 public async createIssue (
363367 repoFullName : string ,
364- installationId : string ,
368+ installationId : string | null ,
365369 issueData : IssueData
366370 ) : Promise < GitHubIssue > {
367371 const [ owner , repo ] = repoFullName . split ( '/' ) ;
@@ -371,15 +375,22 @@ export class GitHubService {
371375 }
372376
373377 /**
374- * Get installation access token
378+ * Get installation access token (GitHub App token)
375379 */
380+ if ( ! installationId ) {
381+ throw new Error ( 'installationId is required for creating GitHub issues' ) ;
382+ }
383+
376384 const accessToken = await this . createInstallationToken ( installationId ) ;
377385
378386 /**
379- * Create Octokit instance with installation access token and configured timeout
387+ * Create Octokit instance with installation token and configured timeout
380388 */
381389 const octokit = this . createOctokit ( accessToken ) ;
382390
391+ /**
392+ * Create issue via REST API using installation token
393+ */
383394 try {
384395 const { data } = await octokit . rest . issues . create ( {
385396 owner,
@@ -401,44 +412,207 @@ export class GitHubService {
401412 }
402413
403414 /**
404- * Assign GitHub Copilot to an issue
415+ * Assign Copilot agent to a GitHub issue using user-to-server OAuth token
405416 *
406- * @param {string } owner - Repository owner
407- * @param {string } repo - Repository name
417+ * @param {string } repoFullName - Repository full name (owner/repo)
408418 * @param {number } issueNumber - Issue number
409- * @param {string } installationId - GitHub App installation ID
410- * @returns {Promise<boolean> } True if assignment was successful
411- * @throws {Error } If assignment fails
419+ * @param {string } delegatedUserToken - User-to-server OAuth token
420+ * @returns {Promise<void> }
421+ * @throws {Error } If Copilot assignment fails
412422 */
413423 public async assignCopilot (
414- owner : string ,
415- repo : string ,
424+ repoFullName : string ,
416425 issueNumber : number ,
417- installationId : string
418- ) : Promise < boolean > {
419- /**
420- * Get installation access token
421- */
422- const accessToken = await this . createInstallationToken ( installationId ) ;
426+ delegatedUserToken : string
427+ ) : Promise < void > {
428+ const [ owner , repo ] = repoFullName . split ( '/' ) ;
429+
430+ if ( ! owner || ! repo ) {
431+ throw new Error ( `Invalid repository name format: ${ repoFullName } . Expected format: owner/repo` ) ;
432+ }
423433
424434 /**
425- * Create Octokit instance with installation access token and configured timeout
435+ * Create Octokit instance with user-to-server OAuth token
426436 */
427- const octokit = this . createOctokit ( accessToken ) ;
437+ const octokit = this . createOctokit ( delegatedUserToken ) ;
428438
429439 try {
430440 /**
431- * Assign GitHub Copilot coding agent (copilot-swe-agent[bot]) as assignee
432- * According to GitHub docs: https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/create-a-pr
441+ * Step 1: Get repository ID and find Copilot bot ID
433442 */
434- await octokit . rest . issues . addAssignees ( {
443+ const repoInfoQuery = `
444+ query($owner: String!, $name: String!) {
445+ repository(owner: $owner, name: $name) {
446+ id
447+ issue(number: ${ issueNumber } ) {
448+ id
449+ }
450+ suggestedActors(capabilities: [CAN_BE_ASSIGNED], first: 100) {
451+ nodes {
452+ login
453+ __typename
454+ ... on Bot {
455+ id
456+ }
457+ ... on User {
458+ id
459+ }
460+ }
461+ }
462+ }
463+ }
464+ ` ;
465+
466+ const repoInfo : any = await octokit . graphql ( repoInfoQuery , {
435467 owner,
436- repo,
437- issue_number : issueNumber ,
438- assignees : [ 'copilot-swe-agent[bot]' ] ,
468+ name : repo ,
439469 } ) ;
440470
441- return true ;
471+ console . log ( '[GitHub API] Repository info query response:' , JSON . stringify ( repoInfo , null , 2 ) ) ;
472+
473+ const repositoryId = repoInfo ?. repository ?. id ;
474+ const issueId = repoInfo ?. repository ?. issue ?. id ;
475+
476+ if ( ! repositoryId ) {
477+ throw new Error ( `Failed to get repository ID for ${ repoFullName } ` ) ;
478+ }
479+
480+ if ( ! issueId ) {
481+ throw new Error ( `Failed to get issue ID for issue #${ issueNumber } ` ) ;
482+ }
483+
484+ /**
485+ * Find Copilot bot in suggested actors
486+ */
487+ let copilotBot = repoInfo . repository . suggestedActors . nodes . find (
488+ ( node : any ) => node . login === 'copilot-swe-agent'
489+ ) ;
490+
491+ console . log ( '[GitHub API] Copilot bot found in suggestedActors:' , copilotBot ? { login : copilotBot . login , id : copilotBot . id } : 'not found' ) ;
492+
493+ /**
494+ * If not found in suggestedActors, try to get it directly by login
495+ */
496+ if ( ! copilotBot || ! copilotBot . id ) {
497+ console . log ( '[GitHub API] Trying to get Copilot bot directly by login...' ) ;
498+
499+ try {
500+ const copilotBotQuery = `
501+ query($login: String!) {
502+ user(login: $login) {
503+ id
504+ login
505+ __typename
506+ }
507+ }
508+ ` ;
509+
510+ const copilotUserInfo : any = await octokit . graphql ( copilotBotQuery , {
511+ login : 'copilot-swe-agent' ,
512+ } ) ;
513+
514+ console . log ( '[GitHub API] Direct Copilot bot query response:' , JSON . stringify ( copilotUserInfo , null , 2 ) ) ;
515+
516+ if ( copilotUserInfo ?. user ?. id ) {
517+ copilotBot = {
518+ login : copilotUserInfo . user . login ,
519+ id : copilotUserInfo . user . id ,
520+ } ;
521+ }
522+ } catch ( directQueryError ) {
523+ console . log ( '[GitHub API] Failed to get Copilot bot directly:' , directQueryError ) ;
524+ }
525+ }
526+
527+ if ( ! copilotBot || ! copilotBot . id ) {
528+ throw new Error ( 'Copilot coding agent (copilot-swe-agent) is not available for this repository' ) ;
529+ }
530+
531+ console . log ( '[GitHub API] Using Copilot bot:' , { login : copilotBot . login , id : copilotBot . id } ) ;
532+
533+ /**
534+ * Step 2: Assign Copilot to issue via GraphQL
535+ * Note: Assignable is a union type (Issue | PullRequest), so we need to use fragments
536+ */
537+ const assignCopilotMutation = `
538+ mutation($issueId: ID!, $assigneeIds: [ID!]!) {
539+ addAssigneesToAssignable(input: {
540+ assignableId: $issueId
541+ assigneeIds: $assigneeIds
542+ }) {
543+ assignable {
544+ ... on Issue {
545+ id
546+ number
547+ assignees(first: 10) {
548+ nodes {
549+ login
550+ }
551+ }
552+ }
553+ ... on PullRequest {
554+ id
555+ number
556+ assignees(first: 10) {
557+ nodes {
558+ login
559+ }
560+ }
561+ }
562+ }
563+ }
564+ }
565+ ` ;
566+
567+ const response : any = await octokit . graphql ( assignCopilotMutation , {
568+ issueId,
569+ assigneeIds : [ copilotBot . id ] ,
570+ } ) ;
571+
572+ console . log ( '[GitHub API] Assign Copilot mutation response:' , JSON . stringify ( response , null , 2 ) ) ;
573+
574+ const assignable = response ?. addAssigneesToAssignable ?. assignable ;
575+
576+ if ( ! assignable ) {
577+ throw new Error ( 'Failed to assign Copilot to issue' ) ;
578+ }
579+
580+ /**
581+ * Assignable is a union type (Issue | PullRequest), so we need to check which type it is
582+ * Both Issue and PullRequest have assignees field, so we can access it directly
583+ *
584+ * Note: The assignees list might not be immediately updated in the response,
585+ * so we check if the mutation succeeded (assignable is not null) rather than
586+ * verifying the assignees list directly
587+ */
588+ const assignedLogins = assignable . assignees ?. nodes ?. map ( ( n : any ) => n . login ) || [ ] ;
589+
590+ /**
591+ * Log assignees for debugging (but don't fail if Copilot is not in the list yet)
592+ * GitHub API might not immediately reflect the assignment in the response
593+ */
594+ console . log ( `[GitHub API] Issue assignees after mutation:` , assignedLogins ) ;
595+
596+ /**
597+ * Get issue number from assignable (works for both Issue and PullRequest)
598+ */
599+ const assignedNumber = assignable . number ;
600+
601+ /**
602+ * If Copilot is in the list, log success. Otherwise, just log a warning
603+ * but don't throw an error, as the mutation might have succeeded even if
604+ * the response doesn't show the assignee yet
605+ */
606+ if ( assignedLogins . includes ( 'copilot-swe-agent' ) ) {
607+ console . log ( `[GitHub API] Successfully assigned Copilot to issue #${ assignedNumber } ` ) ;
608+ } else {
609+ /**
610+ * Mutation succeeded (assignable is not null), but assignees list might not be updated yet
611+ * This is a known behavior of GitHub API - the mutation succeeds but the response
612+ * might not immediately reflect the new assignee
613+ */
614+ console . log ( `[GitHub API] Copilot assignment mutation completed for issue #${ assignedNumber } , but assignees list not yet updated in response` ) ;
615+ }
442616 } catch ( error ) {
443617 throw new Error ( `Failed to assign Copilot: ${ error instanceof Error ? error . message : String ( error ) } ` ) ;
444618 }
0 commit comments