@@ -176,22 +176,18 @@ export class GitHubService {
176176 }
177177
178178 /**
179- * Create a GitHub issue
179+ * Create a GitHub issue using GitHub App installation token
180180 *
181181 * @param {string } repoFullName - Repository full name (owner/repo)
182- * @param {string | null } installationId - GitHub App installation ID (optional if using delegatedUser)
182+ * @param {string | null } installationId - GitHub App installation ID
183183 * @param {IssueData } issueData - Issue data (title, body, labels)
184- * @param {boolean } assignAgent - Whether to assign Copilot agent (creates issue via GraphQL with assigneeIds)
185- * @param {string | null } delegatedUserToken - User-to-server OAuth token (optional, preferred over installation token)
186184 * @returns {Promise<GitHubIssue> } Created issue
187185 * @throws {Error } If issue creation fails
188186 */
189187 public async createIssue (
190188 repoFullName : string ,
191189 installationId : string | null ,
192- issueData : IssueData ,
193- assignAgent : boolean = false ,
194- delegatedUserToken : string | null = null
190+ issueData : IssueData
195191 ) : Promise < GitHubIssue > {
196192 const [ owner , repo ] = repoFullName . split ( '/' ) ;
197193
@@ -200,135 +196,163 @@ export class GitHubService {
200196 }
201197
202198 /**
203- * Get authentication token (delegatedUser token preferred, then installation access token)
199+ * Get installation access token (GitHub App token)
204200 */
205- let accessToken : string ;
206-
207- if ( delegatedUserToken ) {
208- console . log ( '[GitHub API] Using delegated user-to-server token for authentication' ) ;
209- accessToken = delegatedUserToken ;
210- } else {
211- accessToken = await this . getAuthToken ( installationId ) ;
212- }
201+ const accessToken = await this . getAuthToken ( installationId ) ;
213202
214203 /**
215- * Create Octokit instance with authentication token and configured timeout
204+ * Create Octokit instance with installation token and configured timeout
216205 */
217206 const octokit = this . createOctokit ( accessToken ) ;
218207
219208 /**
220- * If assignAgent is true, create issue via GraphQL with Copilot assignment
221- * This is the recommended approach according to GitHub community discussions
209+ * Create issue via REST API using installation token
222210 */
223- if ( assignAgent ) {
224- try {
225- /**
226- * Step 1: Get repository ID and find Copilot bot ID
227- * Note: Actor is a union type, so we need to use fragments to get id
228- */
229- const repoInfoQuery = `
230- query($owner: String!, $name: String!) {
231- repository(owner: $owner, name: $name) {
211+ return this . createIssueViaRest ( octokit , owner , repo , issueData ) ;
212+ }
213+
214+ /**
215+ * Assign Copilot agent to a GitHub issue using user-to-server OAuth token
216+ *
217+ * @param {string } repoFullName - Repository full name (owner/repo)
218+ * @param {number } issueNumber - Issue number
219+ * @param {string } delegatedUserToken - User-to-server OAuth token
220+ * @returns {Promise<void> }
221+ * @throws {Error } If Copilot assignment fails
222+ */
223+ public async assignCopilot (
224+ repoFullName : string ,
225+ issueNumber : number ,
226+ delegatedUserToken : string
227+ ) : Promise < void > {
228+ const [ owner , repo ] = repoFullName . split ( '/' ) ;
229+
230+ if ( ! owner || ! repo ) {
231+ throw new Error ( `Invalid repository name format: ${ repoFullName } . Expected format: owner/repo` ) ;
232+ }
233+
234+ /**
235+ * Create Octokit instance with user-to-server OAuth token
236+ */
237+ const octokit = this . createOctokit ( delegatedUserToken ) ;
238+
239+ try {
240+ /**
241+ * Step 1: Get repository ID and find Copilot bot ID
242+ */
243+ const repoInfoQuery = `
244+ query($owner: String!, $name: String!) {
245+ repository(owner: $owner, name: $name) {
246+ id
247+ issue(number: ${ issueNumber } ) {
232248 id
233- suggestedActors(capabilities: [CAN_BE_ASSIGNED], first: 100) {
234- nodes {
235- login
236- __typename
237- ... on Bot {
238- id
239- }
240- ... on User {
241- id
242- }
249+ }
250+ suggestedActors(capabilities: [CAN_BE_ASSIGNED], first: 100) {
251+ nodes {
252+ login
253+ __typename
254+ ... on Bot {
255+ id
256+ }
257+ ... on User {
258+ id
243259 }
244260 }
245261 }
246262 }
247- ` ;
263+ }
264+ ` ;
248265
249- const repoInfo : any = await octokit . graphql ( repoInfoQuery , {
250- owner,
251- name : repo ,
252- } ) ;
266+ const repoInfo : any = await octokit . graphql ( repoInfoQuery , {
267+ owner,
268+ name : repo ,
269+ } ) ;
253270
254- console . log ( '[GitHub API] Repository info query response:' , JSON . stringify ( repoInfo , null , 2 ) ) ;
271+ console . log ( '[GitHub API] Repository info query response:' , JSON . stringify ( repoInfo , null , 2 ) ) ;
255272
256- const repositoryId = repoInfo ?. repository ?. id ;
273+ const repositoryId = repoInfo ?. repository ?. id ;
274+ const issueId = repoInfo ?. repository ?. issue ?. id ;
257275
258- if ( ! repositoryId ) {
259- throw new Error ( `Failed to get repository ID for ${ repoFullName } ` ) ;
260- }
276+ if ( ! repositoryId ) {
277+ throw new Error ( `Failed to get repository ID for ${ repoFullName } ` ) ;
278+ }
261279
262- /**
263- * Find Copilot bot in suggested actors
264- */
265- let copilotBot = repoInfo . repository . suggestedActors . nodes . find (
266- ( node : any ) => node . login === 'copilot-swe-agent'
267- ) ;
280+ if ( ! issueId ) {
281+ throw new Error ( `Failed to get issue ID for issue #${ issueNumber } ` ) ;
282+ }
268283
269- console . log ( '[GitHub API] Copilot bot found in suggestedActors:' , copilotBot ? { login : copilotBot . login , id : copilotBot . id } : 'not found' ) ;
284+ /**
285+ * Find Copilot bot in suggested actors
286+ */
287+ let copilotBot = repoInfo . repository . suggestedActors . nodes . find (
288+ ( node : any ) => node . login === 'copilot-swe-agent'
289+ ) ;
270290
271- /**
272- * If not found in suggestedActors, try to get it directly by login
273- */
274- if ( ! copilotBot || ! copilotBot . id ) {
275- console . log ( '[GitHub API] Trying to get Copilot bot directly by login...' ) ;
291+ console . log ( '[GitHub API] Copilot bot found in suggestedActors:' , copilotBot ? { login : copilotBot . login , id : copilotBot . id } : 'not found' ) ;
276292
277- try {
278- const copilotBotQuery = `
279- query($login: String!) {
280- user(login: $login) {
281- id
282- login
283- __typename
284- }
293+ /**
294+ * If not found in suggestedActors, try to get it directly by login
295+ */
296+ if ( ! copilotBot || ! copilotBot . id ) {
297+ console . log ( '[GitHub API] Trying to get Copilot bot directly by login...' ) ;
298+
299+ try {
300+ const copilotBotQuery = `
301+ query($login: String!) {
302+ user(login: $login) {
303+ id
304+ login
305+ __typename
285306 }
286- ` ;
307+ }
308+ ` ;
287309
288- const copilotUserInfo : any = await octokit . graphql ( copilotBotQuery , {
289- login : 'copilot-swe-agent' ,
290- } ) ;
310+ const copilotUserInfo : any = await octokit . graphql ( copilotBotQuery , {
311+ login : 'copilot-swe-agent' ,
312+ } ) ;
291313
292- console . log ( '[GitHub API] Direct Copilot bot query response:' , JSON . stringify ( copilotUserInfo , null , 2 ) ) ;
314+ console . log ( '[GitHub API] Direct Copilot bot query response:' , JSON . stringify ( copilotUserInfo , null , 2 ) ) ;
293315
294- if ( copilotUserInfo ?. user ?. id ) {
295- copilotBot = {
296- login : copilotUserInfo . user . login ,
297- id : copilotUserInfo . user . id ,
298- } ;
299- }
300- } catch ( directQueryError ) {
301- console . log ( '[GitHub API] Failed to get Copilot bot directly:' , directQueryError ) ;
316+ if ( copilotUserInfo ?. user ?. id ) {
317+ copilotBot = {
318+ login : copilotUserInfo . user . login ,
319+ id : copilotUserInfo . user . id ,
320+ } ;
302321 }
322+ } catch ( directQueryError ) {
323+ console . log ( '[GitHub API] Failed to get Copilot bot directly:' , directQueryError ) ;
303324 }
325+ }
304326
305- if ( ! copilotBot || ! copilotBot . id ) {
306- /**
307- * Fallback: Create issue without Copilot assignment via REST API
308- */
309- console . log ( '[GitHub API] Copilot bot not found, creating issue without assignment' ) ;
310- return this . createIssueViaRest ( octokit , owner , repo , issueData ) ;
311- }
327+ if ( ! copilotBot || ! copilotBot . id ) {
328+ throw new Error ( 'Copilot coding agent (copilot-swe-agent) is not available for this repository' ) ;
329+ }
312330
313- console . log ( '[GitHub API] Using Copilot bot:' , { login : copilotBot . login , id : copilotBot . id } ) ;
331+ console . log ( '[GitHub API] Using Copilot bot:' , { login : copilotBot . login , id : copilotBot . id } ) ;
314332
315- /**
316- * Step 2: Create issue via GraphQL with Copilot assignment
317- * This is the recommended approach from GitHub community discussions
318- */
319- const createIssueMutation = `
320- mutation($repoId: ID!, $title: String!, $body: String!, $assigneeIds: [ID!]) {
321- createIssue(input: {
322- repositoryId: $repoId
323- title: $title
324- body: $body
325- assigneeIds: $assigneeIds
326- }) {
327- issue {
333+ /**
334+ * Step 2: Assign Copilot to issue via GraphQL
335+ * Note: Assignable is a union type (Issue | PullRequest), so we need to use fragments
336+ */
337+ const assignCopilotMutation = `
338+ mutation($issueId: ID!, $assigneeIds: [ID!]!) {
339+ addAssigneesToAssignable(input: {
340+ assignableId: $issueId
341+ assigneeIds: $assigneeIds
342+ }) {
343+ assignable {
344+ ... on Issue {
345+ id
346+ number
347+ assignees(first: 10) {
348+ nodes {
349+ login
350+ }
351+ }
352+ }
353+ ... on PullRequest {
354+ id
328355 number
329- title
330- url
331- state
332356 assignees(first: 10) {
333357 nodes {
334358 login
@@ -337,42 +361,61 @@ export class GitHubService {
337361 }
338362 }
339363 }
340- ` ;
364+ }
365+ ` ;
366+
367+ const response : any = await octokit . graphql ( assignCopilotMutation , {
368+ issueId,
369+ assigneeIds : [ copilotBot . id ] ,
370+ } ) ;
341371
342- const response : any = await octokit . graphql ( createIssueMutation , {
343- repoId : repositoryId ,
344- title : issueData . title ,
345- body : issueData . body ,
346- assigneeIds : [ copilotBot . id ] ,
347- } ) ;
372+ console . log ( '[GitHub API] Assign Copilot mutation response:' , JSON . stringify ( response , null , 2 ) ) ;
348373
349- console . log ( '[GitHub API] Create issue with Copilot mutation response:' , JSON . stringify ( response , null , 2 ) ) ;
374+ const assignable = response ?. addAssigneesToAssignable ?. assignable ;
350375
351- const issue = response ?. createIssue ?. issue ;
376+ if ( ! assignable ) {
377+ throw new Error ( 'Failed to assign Copilot to issue' ) ;
378+ }
352379
353- if ( ! issue ) {
354- throw new Error ( 'Failed to create issue via GraphQL' ) ;
355- }
380+ /**
381+ * Assignable is a union type (Issue | PullRequest), so we need to check which type it is
382+ * Both Issue and PullRequest have assignees field, so we can access it directly
383+ *
384+ * Note: The assignees list might not be immediately updated in the response,
385+ * so we check if the mutation succeeded (assignable is not null) rather than
386+ * verifying the assignees list directly
387+ */
388+ const assignedLogins = assignable . assignees ?. nodes ?. map ( ( n : any ) => n . login ) || [ ] ;
389+
390+ /**
391+ * Log assignees for debugging (but don't fail if Copilot is not in the list yet)
392+ * GitHub API might not immediately reflect the assignment in the response
393+ */
394+ console . log ( `[GitHub API] Issue assignees after mutation:` , assignedLogins ) ;
356395
357- return {
358- number : issue . number ,
359- html_url : issue . url ,
360- title : issue . title ,
361- state : issue . state ,
362- } ;
363- } catch ( error ) {
396+ /**
397+ * Get issue number from assignable (works for both Issue and PullRequest)
398+ */
399+ const assignedNumber = assignable . number ;
400+
401+ /**
402+ * If Copilot is in the list, log success. Otherwise, just log a warning
403+ * but don't throw an error, as the mutation might have succeeded even if
404+ * the response doesn't show the assignee yet
405+ */
406+ if ( assignedLogins . includes ( 'copilot-swe-agent' ) ) {
407+ console . log ( `[GitHub API] Successfully assigned Copilot to issue #${ assignedNumber } ` ) ;
408+ } else {
364409 /**
365- * If GraphQL creation fails, fallback to REST API
410+ * Mutation succeeded (assignable is not null), but assignees list might not be updated yet
411+ * This is a known behavior of GitHub API - the mutation succeeds but the response
412+ * might not immediately reflect the new assignee
366413 */
367- console . log ( '[GitHub API] GraphQL issue creation failed, falling back to REST API:' , error ) ;
368- return this . createIssueViaRest ( octokit , owner , repo , issueData ) ;
414+ console . log ( `[GitHub API] Copilot assignment mutation completed for issue #${ assignedNumber } , but assignees list not yet updated in response` ) ;
369415 }
416+ } catch ( error ) {
417+ throw new Error ( `Failed to assign Copilot: ${ error instanceof Error ? error . message : String ( error ) } ` ) ;
370418 }
371-
372- /**
373- * Default: Create issue via REST API (no Copilot assignment)
374- */
375- return this . createIssueViaRest ( octokit , owner , repo , issueData ) ;
376419 }
377420
378421 /**
0 commit comments