Skip to content

Commit 3f0fac3

Browse files
committed
Refactor GitHub issue creation and Copilot assignment
Separated GitHub issue creation and Copilot agent assignment into distinct steps. The issue is now always created using the GitHub App installation token, and Copilot is assigned afterward using a user-to-server OAuth token if enabled. Updated the TaskManagerWorker logic to reflect this change, improved error handling, and updated the event saving logic to accurately reflect Copilot assignment status.
1 parent 637f162 commit 3f0fac3

2 files changed

Lines changed: 231 additions & 154 deletions

File tree

workers/task-manager/src/GithubService.ts

Lines changed: 175 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)