Skip to content

Commit 4b8d98c

Browse files
authored
feat(Fix): Input issues (objects including 'url'), not just issue URLs (#906)
Related to github/continuous-ai-for-accessibility#33 Follow-up to (but not blocked by) github-community-projects/continuous-ai-for-accessibility-scanner#891 This PR adds a new `issues` input to the ‘Fix’ action, and marks the existing `issue_urls` input as deprecated (to be removed in v2). By continuing to input it in the v1 series, this change is non-breaking and, additionally, it can be merged before/independently-of github-community-projects/continuous-ai-for-accessibility-scanner#891. The new input requires `url`. If `nodeId` (which is optional) is included too, it will be reused, so we don’t have to look it up (again) in ‘Fix’. This conserves request quota and makes hitting rate limits less likely (github/continuous-ai-for-accessibility#31).
2 parents 0b8772a + ed19a99 commit 4b8d98c

File tree

6 files changed

+113
-28
lines changed

6 files changed

+113
-28
lines changed

.github/actions/fix/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@ Attempts to fix issues with Copilot.
66

77
### Inputs
88

9+
#### `issues`
10+
11+
**NOTE: This input will be unconditionally required in `v2`.** **Required if `issue_urls` is not provided** List of issues to attempt to fix—including, at a minimum, their `url`s—as stringified JSON. For example: `'[{"url":"https://github.com/github/docs/issues/123"},{"nodeId":"SXNzdWU6Mg==","url":"https://github.com/github/docs/issues/124"},{"id":4,"nodeId":"SXNzdWU6NA==","url":"https://github.com/github/docs/issues/126","title":"Accessibility issue: 4"}]'`.
12+
913
#### `issue_urls`
1014

11-
**Required** List of issue URLs to attempt to fix, as stringified JSON. For example: `'["https://github.com/github/docs/issues/123","https://github.com/github/docs/issues/124","https://github.com/github/docs/issues/126","https://github.com/github/docs/issues/127"]'`.
15+
**DEPRECATED: This input will be removed in `v2`.** **Required if `issues` is not provided** List of issue URLs to attempt to fix, as stringified JSON. For example: `'["https://github.com/github/docs/issues/123","https://github.com/github/docs/issues/124","https://github.com/github/docs/issues/126","https://github.com/github/docs/issues/127"]'`. If both `issues` and `issue_urls` are provided, `issue_urls` will be ignored.
1216

1317
#### `repository`
1418

.github/actions/fix/action.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ name: "Fix"
22
description: "Attempts to fix issues with Copilot."
33

44
inputs:
5+
issues:
6+
description: "List of issues to attempt to fix, as stringified JSON"
7+
required: false
58
issue_urls:
69
description: "List of issue URLs to attempt to fix, as stringified JSON"
7-
required: true
10+
required: false
811
repository:
912
description: "Repository (with owner) containing issues"
1013
required: true

.github/actions/fix/src/Issue.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { IssueInput } from "./types.d.js";
2+
3+
interface IIssue extends IssueInput{
4+
owner: string;
5+
repository: string;
6+
issueNumber: number;
7+
}
8+
9+
export class Issue implements IIssue {
10+
/**
11+
* Extracts owner, repository, and issue number from a GitHub issue URL.
12+
* @param issueUrl A GitHub issue URL (e.g. `https://github.com/owner/repo/issues/42`).
13+
* @returns An object with `owner`, `repository`, and `issueNumber` keys.
14+
* @throws The provided URL is unparseable due to its unexpected format.
15+
*/
16+
static parseIssueUrl(issueUrl: string): { owner: string; repository: string; issueNumber: number } {
17+
const { owner, repository, issueNumber } = /\/(?<owner>[^/]+)\/(?<repository>[^/]+)\/issues\/(?<issueNumber>\d+)(?:[/?#]|$)/.exec(issueUrl)?.groups || {};
18+
if (!owner || !repository || !issueNumber) {
19+
throw new Error(`Could not parse issue URL: ${issueUrl}`);
20+
}
21+
return { owner, repository, issueNumber: Number(issueNumber) }
22+
}
23+
24+
url: string;
25+
nodeId?: string;
26+
27+
get owner(): string {
28+
return Issue.parseIssueUrl(this.url).owner;
29+
}
30+
31+
get repository(): string {
32+
return Issue.parseIssueUrl(this.url).repository;
33+
}
34+
35+
get issueNumber(): number {
36+
return Issue.parseIssueUrl(this.url).issueNumber;
37+
}
38+
39+
constructor({url, nodeId}: IssueInput) {
40+
this.url = url;
41+
this.nodeId = nodeId;
42+
}
43+
}

.github/actions/fix/src/fixIssue.ts

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import type { Octokit } from '@octokit/core';
2+
import { Issue } from './Issue.js';
23

34
// https://docs.github.com/en/enterprise-cloud@latest/copilot/how-tos/use-copilot-agents/coding-agent/assign-copilot-to-an-issue#assigning-an-existing-issue
4-
export async function fixIssue(octokit: Octokit, repoWithOwner: string, issueUrl: string) {
5-
const owner = repoWithOwner.split('/')[0];
6-
const repo = repoWithOwner.split('/')[1];
7-
const issueNumber = Number(issueUrl.split('/').pop());
5+
export async function fixIssue(octokit: Octokit, { owner, repository, issueNumber, nodeId }: Issue) {
86
// Check whether issues can be assigned to Copilot
97
const suggestedActorsResponse = await octokit.graphql<{
108
repository: {
@@ -13,8 +11,8 @@ export async function fixIssue(octokit: Octokit, repoWithOwner: string, issueUrl
1311
}
1412
}
1513
}>(
16-
`query ($owner: String!, $repo: String!) {
17-
repository(owner: $owner, name: $repo) {
14+
`query ($owner: String!, $repository: String!) {
15+
repository(owner: $owner, name: $repository) {
1816
suggestedActors(capabilities: [CAN_BE_ASSIGNED], first: 1) {
1917
nodes {
2018
login
@@ -25,30 +23,49 @@ export async function fixIssue(octokit: Octokit, repoWithOwner: string, issueUrl
2523
}
2624
}
2725
}`,
28-
{ owner, repo },
26+
{ owner, repository },
2927
);
3028
if (suggestedActorsResponse?.repository?.suggestedActors?.nodes[0]?.login !== "copilot-swe-agent") {
3129
return;
3230
}
33-
// Get GraphQL identifier for issue
34-
const issueResponse = await octokit.graphql<{
35-
repository: {
36-
issue: { id: string }
37-
}
38-
}>(
39-
`query($owner: String!, $repo: String!, $issueNumber: Int!) {
40-
repository(owner: $owner, name: $repo) {
41-
issue(number: $issueNumber) { id }
31+
// Get GraphQL identifier for issue (unless already provided)
32+
let issueId = nodeId;
33+
if (!issueId) {
34+
console.debug(`Fetching identifier for issue ${owner}/${repository}#${issueNumber}`);
35+
const issueResponse = await octokit.graphql<{
36+
repository: {
37+
issue: { id: string }
4238
}
43-
}`,
44-
{ owner, repo, issueNumber }
45-
);
46-
const issueId = issueResponse?.repository?.issue?.id;
39+
}>(
40+
`query($owner: String!, $repository: String!, $issueNumber: Int!) {
41+
repository(owner: $owner, name: $repository) {
42+
issue(number: $issueNumber) { id }
43+
}
44+
}`,
45+
{ owner, repository, issueNumber }
46+
);
47+
issueId = issueResponse?.repository?.issue?.id;
48+
console.debug(`Fetched identifier for issue ${owner}/${repository}#${issueNumber}: ${issueId}`);
49+
} else {
50+
console.debug(`Using provided identifier for issue ${owner}/${repository}#${issueNumber}: ${issueId}`);
51+
}
4752
if (!issueId) {
53+
console.warn(`Couldn’t get identifier for issue ${owner}/${repository}#${issueNumber}. Skipping assignment to Copilot.`);
4854
return;
4955
}
5056
// Assign issue to Copilot
51-
return octokit.graphql(
57+
await octokit.graphql<{
58+
replaceActorsForAssignable: {
59+
assignable: {
60+
id: string;
61+
url: string;
62+
title: string;
63+
assignees: {
64+
nodes: { login: string }[]
65+
}
66+
}
67+
}
68+
}>(
5269
`mutation($issueId: ID!, $assigneeId: ID!) {
5370
replaceActorsForAssignable(input: {assignableId: $issueId, actorIds: [$assigneeId]}) {
5471
assignable {

.github/actions/fix/src/index.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,28 @@
1+
import type { IssueInput } from "./types.d.js";
12
import process from "node:process";
23
import core from "@actions/core";
34
import { Octokit } from "@octokit/core";
45
import { throttling } from "@octokit/plugin-throttling";
56
import { fixIssue } from "./fixIssue.js";
7+
import { Issue } from "./Issue.js";
68
const OctokitWithThrottling = Octokit.plugin(throttling);
79

810
export default async function () {
911
core.info("Started 'fix' action");
10-
const issueUrls = JSON.parse(core.getInput('issue_urls', { required: true }));
12+
/** @deprecated Use `issues` instead. */
13+
const issueUrls: string[] = JSON.parse(
14+
core.getInput('issue_urls', { required: false }) || "[]"
15+
);
16+
const issues: IssueInput[] = JSON.parse(
17+
core.getInput('issues', { required: false }) || JSON.stringify(issueUrls.map(url => ({ url })))
18+
);
19+
if (issues.length === 0) {
20+
core.setFailed("Neither 'issues' nor 'issue_urls' was provided, but one is required.");
21+
process.exit(1);
22+
}
1123
const repoWithOwner = core.getInput('repository', { required: true });
1224
const token = core.getInput('token', { required: true });
25+
core.debug(`Input: 'issues: ${JSON.stringify(issues)}'`);
1326
core.debug(`Input: 'issue_urls: ${JSON.stringify(issueUrls)}'`);
1427
core.debug(`Input: 'repository: ${repoWithOwner}'`);
1528

@@ -32,12 +45,13 @@ export default async function () {
3245
},
3346
}
3447
});
35-
for (const issueUrl of issueUrls) {
48+
for (const issueInput of issues) {
3649
try {
37-
await fixIssue(octokit, repoWithOwner, issueUrl);
38-
core.info(`Assigned ${repoWithOwner}#${issueUrl.split('/').pop()} to Copilot!`);
50+
const issue = new Issue(issueInput);
51+
await fixIssue(octokit, issue);
52+
core.info(`Assigned ${issue.owner}/${issue.repository}#${issue.issueNumber} to Copilot!`);
3953
} catch (error) {
40-
core.setFailed(`Failed to assign ${repoWithOwner}#${issueUrl.split('/').pop()} to Copilot: ${error}`);
54+
core.setFailed(`Failed to assign ${issueInput.url} to Copilot: ${error}`);
4155
process.exit(1);
4256
}
4357
}

.github/actions/fix/src/types.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export type IssueInput = {
2+
url: string;
3+
nodeId?: string;
4+
};

0 commit comments

Comments
 (0)