Skip to content

Commit 0567657

Browse files
committed
feat(Fix): Input issues (objects including 'url'), not just issue URLs
1 parent 0b8772a commit 0567657

File tree

6 files changed

+108
-28
lines changed

6 files changed

+108
-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: 33 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,44 @@ 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+
const issueResponse = await octokit.graphql<{
35+
repository: {
36+
issue: { id: string }
4237
}
43-
}`,
44-
{ owner, repo, issueNumber }
45-
);
46-
const issueId = issueResponse?.repository?.issue?.id;
38+
}>(
39+
`query($owner: String!, $repository: String!, $issueNumber: Int!) {
40+
repository(owner: $owner, name: $repository) {
41+
issue(number: $issueNumber) { id }
42+
}
43+
}`,
44+
{ owner, repository, issueNumber }
45+
);
46+
issueId = issueResponse?.repository?.issue?.id;
47+
}
4748
if (!issueId) {
4849
return;
4950
}
5051
// Assign issue to Copilot
51-
return octokit.graphql(
52+
await octokit.graphql<{
53+
replaceActorsForAssignable: {
54+
assignable: {
55+
id: string;
56+
url: string;
57+
title: string;
58+
assignees: {
59+
nodes: { login: string }[]
60+
}
61+
}
62+
}
63+
}>(
5264
`mutation($issueId: ID!, $assigneeId: ID!) {
5365
replaceActorsForAssignable(input: {assignableId: $issueId, actorIds: [$assigneeId]}) {
5466
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)