Skip to content

Commit 3beb05d

Browse files
authored
BREAKING CHANGE(File): Add filings output and remove deprecated outputs (#1924)
Fixes github/continuous-ai-for-accessibility#72 (Hubber access only) Fixes github/continuous-ai-for-accessibility#23 (Hubber access only) Follow-up to github-community-projects/continuous-ai-for-accessibility-scanner#1772 This PR: - Removes File’s `closed_issues`, `opened_issues`, `repeated_issues`, and `findings` (which were deprecated in github-community-projects/continuous-ai-for-accessibility-scanner#1772) - Renames File’s `cached_findings` input to `cached_filings` - Adds a `filings` output to `File` (in preparation for github/continuous-ai-for-accessibility#68 (Hubber access only)). Previously, findings and their associated issues were split among multiple separate outputs. Now, they are joined in a single output. This makes it much easier to answer questions like _‘Which findings does this issue cover?’_
2 parents 8639bc7 + d393d4d commit 3beb05d

20 files changed

+364
-232
lines changed

.github/actions/file/README.md

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,28 +22,12 @@ Files GitHub issues to track potential accessibility gaps.
2222

2323
**Required** Token with fine-grained permission 'issues: write'.
2424

25-
#### `cached_findings`
25+
#### `cached_filings`
2626

27-
**Optional** Cached findings from previous runs, as stringified JSON. Without this, duplicate issues may be filed. For example: `'[]'`.
27+
**Optional** Cached filings from previous runs, as stringified JSON. Without this, duplicate issues may be filed. For example: `'[{"findings":[],"issue":{"id":1,"nodeId":"SXNzdWU6MQ==","url":"https://github.com/github/docs/issues/123","title":"Accessibility issue: 1"}}]'`
2828

2929
### Outputs
3030

31-
#### `findings`
32-
33-
**DEPRECATED: This output will be removed in `v2`.** List of potential accessibility gaps (plus issue URLs), as stringified JSON. For example:
34-
35-
```JS
36-
'[]'
37-
```
38-
39-
#### `closed_issues`
40-
41-
**DEPRECATED: This output will be removed in `v2`.** List of closed issues’ `id`, `nodeId`, `url`, and `title`, as stringified JSON. For example: `'[{"id":1,"nodeId":"SXNzdWU6MQ==","url":"https://github.com/github/docs/issues/123","title":"Accessibility issue: 1"},{"id":2,"nodeId":"SXNzdWU6Mg==","url":"https://github.com/github/docs/issues/124","title":"Accessibility issue: 2"},{"id":4,"nodeId":"SXNzdWU6NA==","url":"https://github.com/github/docs/issues/126","title":"Accessibility issue: 4"}]'`.
42-
43-
#### `opened_issues`
44-
45-
**DEPRECATED: This output will be removed in `v2`.** List of newly-opened issues’ `id`, `nodeId`, `url`, and `title`, as stringified JSON. For example: `'[{"id":1,"nodeId":"SXNzdWU6MQ==","url":"https://github.com/github/docs/issues/123","title":"Accessibility issue: 1"},{"id":2,"nodeId":"SXNzdWU6Mg==","url":"https://github.com/github/docs/issues/124","title":"Accessibility issue: 2"},{"id":4,"nodeId":"SXNzdWU6NA==","url":"https://github.com/github/docs/issues/126","title":"Accessibility issue: 4"}]'`.
46-
47-
#### `repeated_issues`
31+
#### `filings`
4832

49-
**DEPRECATED: This output will be removed in `v2`.** List of repeated issues`id`, `nodeId`, `url`, and `title`, as stringified JSON. For example: `'[{"id":1,"nodeId":"SXNzdWU6MQ==","url":"https://github.com/github/docs/issues/123","title":"Accessibility issue: 1"},{"id":2,"nodeId":"SXNzdWU6Mg==","url":"https://github.com/github/docs/issues/124","title":"Accessibility issue: 2"},{"id":4,"nodeId":"SXNzdWU6NA==","url":"https://github.com/github/docs/issues/126","title":"Accessibility issue: 4"}]'`.
33+
List of issues filed (and their associated finding(s)), as stringified JSON. For example: `'[{"findings":[],"issue":{"id":1,"nodeId":"SXNzdWU6MQ==","url":"https://github.com/github/docs/issues/123","title":"Accessibility issue: 1"}}]'`

.github/actions/file/action.yml

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,13 @@ inputs:
1111
token:
1212
description: "Token with fine-grained permission 'issues: write'"
1313
required: true
14-
cached_findings:
15-
description: "Cached findings from previous runs, as stringified JSON. Without this, duplicate issues may be filed."
14+
cached_filings:
15+
description: "Cached filings from previous runs, as stringified JSON. Without this, duplicate issues may be filed."
1616
required: false
1717

1818
outputs:
19-
findings:
20-
description: "DEPRECATED: List of potential accessibility gaps (plus issue URLs), as stringified JSON"
21-
closed_issues:
22-
description: "DEPRECATED: List of closed issues, as stringified JSON"
23-
opened_issues:
24-
description: "DEPRECATED: List of newly-opened issues, as stringified JSON"
25-
repeated_issues:
26-
description: "DEPRECATED: List of repeated issues, as stringified JSON"
19+
filings:
20+
description: "List of issues filed (and their associated finding(s)), as stringified JSON"
2721

2822
runs:
2923
using: "node24"

.github/actions/file/src/Issue.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type { Issue as IssueInput } from "./types.d.js";
2+
3+
export class Issue implements IssueInput {
4+
#url!: string;
5+
#parsedUrl!: {
6+
owner: string;
7+
repository: string;
8+
issueNumber: number;
9+
};
10+
nodeId: string;
11+
id: number;
12+
title: string;
13+
state?: "open" | "reopened" | "closed";
14+
15+
constructor({ url, nodeId, id, title, state }: IssueInput) {
16+
this.url = url;
17+
this.nodeId = nodeId;
18+
this.id = id;
19+
this.title = title;
20+
this.state = state;
21+
}
22+
23+
set url(newUrl: string) {
24+
this.#url = newUrl;
25+
this.#parsedUrl = this.#parseUrl();
26+
}
27+
28+
get url(): string {
29+
return this.#url;
30+
}
31+
32+
get owner(): string {
33+
return this.#parsedUrl.owner;
34+
}
35+
36+
get repository(): string {
37+
return this.#parsedUrl.repository;
38+
}
39+
40+
get issueNumber(): number {
41+
return this.#parsedUrl.issueNumber;
42+
}
43+
44+
/**
45+
* Extracts owner, repository, and issue number from the Issue instance’s GitHub issue URL.
46+
* @returns An object with `owner`, `repository`, and `issueNumber` keys.
47+
* @throws The provided URL is unparseable due to its unexpected format.
48+
*/
49+
#parseUrl(): {
50+
owner: string;
51+
repository: string;
52+
issueNumber: number;
53+
} {
54+
const { owner, repository, issueNumber } =
55+
/\/(?<owner>[^/]+)\/(?<repository>[^/]+)\/issues\/(?<issueNumber>\d+)(?:[/?#]|$)/.exec(
56+
this.#url
57+
)?.groups || {};
58+
if (!owner || !repository || !issueNumber) {
59+
throw new Error(`Could not parse issue URL: ${this.#url}`);
60+
}
61+
return { owner, repository, issueNumber: Number(issueNumber) };
62+
}
63+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { Octokit } from '@octokit/core';
2+
import { Issue } from './Issue.js';
3+
4+
export async function closeIssue(octokit: Octokit, { owner, repository, issueNumber }: Issue) {
5+
return octokit.request(`PATCH /repos/${owner}/${repository}/issues/${issueNumber}`, {
6+
owner,
7+
repository,
8+
issue_number: issueNumber,
9+
state: 'closed'
10+
});
11+
}

.github/actions/file/src/closeIssueForFinding.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.

.github/actions/file/src/index.ts

Lines changed: 53 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,90 @@
1-
import type { Finding, Issue } from "./types.d.js";
1+
import type { Finding, ResolvedFiling, RepeatedFiling } from "./types.d.js";
22
import process from "node:process";
33
import core from "@actions/core";
44
import { Octokit } from "@octokit/core";
55
import { throttling } from "@octokit/plugin-throttling";
6-
import { toFindingsMap } from "./toFindingsMap.js"
7-
import { closeIssueForFinding } from "./closeIssueForFinding.js";
8-
import { openIssueForFinding } from "./openIssueForFinding.js";
6+
import { Issue } from "./Issue.js";
7+
import { closeIssue } from "./closeIssue.js";
8+
import { isNewFiling } from "./isNewFiling.js";
9+
import { isRepeatedFiling } from "./isRepeatedFiling.js";
10+
import { isResolvedFiling } from "./isResolvedFiling.js";
11+
import { openIssue } from "./openIssue.js";
12+
import { reopenIssue } from "./reopenIssue.js";
13+
import { updateFilingsWithNewFindings } from "./updateFilingsWithNewFindings.js";
914
const OctokitWithThrottling = Octokit.plugin(throttling);
1015

1116
export default async function () {
1217
core.info("Started 'file' action");
13-
const findings: Finding[] = JSON.parse(core.getInput('findings', { required: true }));
14-
const repoWithOwner = core.getInput('repository', { required: true });
15-
const token = core.getInput('token', { required: true });
16-
const cachedFindings: Finding[] = JSON.parse(core.getInput('cached_findings', { required: false }) || "[]");
18+
const findings: Finding[] = JSON.parse(
19+
core.getInput("findings", { required: true })
20+
);
21+
const repoWithOwner = core.getInput("repository", { required: true });
22+
const token = core.getInput("token", { required: true });
23+
const cachedFilings: (ResolvedFiling | RepeatedFiling)[] = JSON.parse(
24+
core.getInput("cached_results", { required: false }) || "[]"
25+
);
1726
core.debug(`Input: 'findings: ${JSON.stringify(findings)}'`);
1827
core.debug(`Input: 'repository: ${repoWithOwner}'`);
19-
core.debug(`Input: 'cached_findings: ${JSON.stringify(cachedFindings)}'`);
20-
21-
const findingsMap = toFindingsMap(findings);
22-
const cachedFindingsMap = toFindingsMap(cachedFindings);
28+
core.debug(`Input: 'cached_filings: ${JSON.stringify(cachedFilings)}'`);
2329

2430
const octokit = new OctokitWithThrottling({
2531
auth: token,
2632
throttle: {
2733
onRateLimit: (retryAfter, options, octokit, retryCount) => {
28-
octokit.log.warn(`Request quota exhausted for request ${options.method} ${options.url}`);
34+
octokit.log.warn(
35+
`Request quota exhausted for request ${options.method} ${options.url}`
36+
);
2937
if (retryCount < 3) {
3038
octokit.log.info(`Retrying after ${retryAfter} seconds!`);
3139
return true;
3240
}
3341
},
3442
onSecondaryRateLimit: (retryAfter, options, octokit, retryCount) => {
35-
octokit.log.warn(`Secondary rate limit hit for request ${options.method} ${options.url}`);
43+
octokit.log.warn(
44+
`Secondary rate limit hit for request ${options.method} ${options.url}`
45+
);
3646
if (retryCount < 3) {
3747
octokit.log.info(`Retrying after ${retryAfter} seconds!`);
3848
return true;
3949
}
4050
},
41-
}
51+
},
4252
});
43-
/** @deprecated */
44-
const closedIssues: Issue[] = [];
45-
/** @deprecated */
46-
const openedIssues: Issue[] = [];
47-
/** @deprecated */
48-
const repeatedIssues: Issue[] = [];
49-
50-
for (const cachedFinding of cachedFindings) {
51-
if (!findingsMap.has(`${cachedFinding.url};${cachedFinding.problemShort};${cachedFinding.html}`)) {
52-
try {
53-
// Finding was not found in the latest run, so close its issue (if necessary)
54-
const response = await closeIssueForFinding(octokit, repoWithOwner, cachedFinding);
55-
closedIssues.push({
56-
id: response.data.id,
57-
nodeId: response.data.node_id,
58-
url: response.data.html_url,
59-
title: response.data.title,
60-
});
61-
core.info(`Closed issue: ${response.data.title} (${repoWithOwner}#${response.data.number})`);
62-
} catch (error) {
63-
core.setFailed(`Failed to close issue for finding: ${error}`);
64-
process.exit(1);
65-
}
66-
}
67-
}
53+
const filings = updateFilingsWithNewFindings(cachedFilings, findings);
6854

69-
for (const finding of findings) {
70-
const cachedIssueUrl = cachedFindingsMap.get(`${finding.url};${finding.problemShort};${finding.html}`)?.issueUrl
71-
finding.issueUrl = cachedIssueUrl;
55+
for (const filing of filings) {
56+
let response;
7257
try {
73-
const response = await openIssueForFinding(octokit, repoWithOwner, finding);
74-
finding.issueUrl = response.data.html_url;
75-
if (response.data.html_url === cachedIssueUrl) {
76-
// Finding was found in previous and latest runs, so reopen its issue (if necessary)
77-
repeatedIssues.push({
78-
id: response.data.id,
79-
nodeId: response.data.node_id,
80-
url: response.data.html_url,
81-
title: response.data.title,
82-
});
83-
core.info(`Repeated issue: ${response.data.title} (${repoWithOwner}#${response.data.number})`);
84-
} else {
85-
// New finding was found in the latest run, so create its issue
86-
openedIssues.push({
87-
id: response.data.id,
88-
nodeId: response.data.node_id,
89-
url: response.data.html_url,
90-
title: response.data.title,
91-
});
92-
core.info(`Created issue: ${response.data.title} (${repoWithOwner}#${response.data.number})`);
58+
if (isResolvedFiling(filing)) {
59+
// Close the filing’s issue (if necessary)
60+
response = await closeIssue(octokit, new Issue(filing.issue));
61+
filing.issue.state = "closed";
62+
} else if (isNewFiling(filing)) {
63+
// Open a new issue for the filing
64+
response = await openIssue(octokit, repoWithOwner, filing.findings[0]);
65+
(filing as any).issue = { state: "open" } as Issue;
66+
} else if (isRepeatedFiling(filing)) {
67+
// Reopen the filing’s issue (if necessary)
68+
response = await reopenIssue(octokit, new Issue(filing.issue));
69+
filing.issue.state = "reopened";
70+
}
71+
if (response?.data && filing.issue) {
72+
// Update the filing with the latest issue data
73+
filing.issue.id = response.data.id;
74+
filing.issue.nodeId = response.data.node_id;
75+
filing.issue.url = response.data.html_url;
76+
filing.issue.title = response.data.title;
77+
core.info(
78+
`Set issue ${response.data.title} (${repoWithOwner}#${response.data.number}) state to ${filing.issue.state}`
79+
);
9380
}
9481
} catch (error) {
95-
core.setFailed(`Failed to open/reopen issue for finding: ${error}`);
82+
core.setFailed(`Failed on filing: ${filing}\n${error}`);
9683
process.exit(1);
9784
}
9885
}
9986

100-
// Deprecated outputs
101-
core.setOutput("closed_issues", JSON.stringify(closedIssues));
102-
core.setOutput("opened_issues", JSON.stringify(openedIssues));
103-
core.setOutput("repeated_issues", JSON.stringify(repeatedIssues));
104-
core.setOutput("findings", JSON.stringify(findings));
105-
core.debug(`Output: 'closed_issues: ${JSON.stringify(closedIssues)}'`);
106-
core.debug(`Output: 'opened_issues: ${JSON.stringify(openedIssues)}'`);
107-
core.debug(`Output: 'repeated_issues: ${JSON.stringify(repeatedIssues)}'`);
108-
core.debug(`Output: 'findings: ${JSON.stringify(findings)}'`);
109-
core.warning("The 'closed_issues' output is deprecated and will be removed in v2.");
110-
core.warning("The 'opened_issues' output is deprecated and will be removed in v2.");
111-
core.warning("The 'repeated_issues' output is deprecated and will be removed in v2.");
112-
core.warning("The 'findings' output is deprecated and will be removed in v2.");
113-
87+
core.setOutput("filings", JSON.stringify(filings));
88+
core.debug(`Output: 'filings: ${JSON.stringify(filings)}'`);
11489
core.info("Finished 'file' action");
11590
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { Filing, NewFiling } from "./types.d.js";
2+
3+
export function isNewFiling(filing: Filing): filing is NewFiling {
4+
// A Filing without an issue is new
5+
return (
6+
(!("issue" in filing) || !filing.issue?.url) &&
7+
"findings" in filing &&
8+
filing.findings.length > 0
9+
);
10+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { Filing, RepeatedFiling } from "./types.d.js";
2+
3+
export function isRepeatedFiling(filing: Filing): filing is RepeatedFiling {
4+
// A Filing with an issue and findings is a repeated filing
5+
return (
6+
"findings" in filing &&
7+
filing.findings.length > 0 &&
8+
"issue" in filing &&
9+
!!filing.issue?.url
10+
);
11+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { Filing, ResolvedFiling } from "./types.d.js";
2+
3+
export function isResolvedFiling(filing: Filing): filing is ResolvedFiling {
4+
// A Filing without findings is resolved
5+
return (
6+
(!("findings" in filing) || filing.findings.length === 0) &&
7+
"issue" in filing &&
8+
!!filing.issue?.url
9+
);
10+
}

0 commit comments

Comments
 (0)