From fe7ce78e59136636274f09cb5c85f1943d88e1e1 Mon Sep 17 00:00:00 2001 From: Joyce Zhu Date: Thu, 12 Feb 2026 07:03:56 +0000 Subject: [PATCH 1/4] Initial attempt at grouping related issues --- .github/actions/file/action.yml | 3 ++ .github/actions/file/src/index.ts | 58 +++++++++++++++++++++++++++---- action.yml | 5 +++ 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/.github/actions/file/action.yml b/.github/actions/file/action.yml index 005d3b0..188a763 100644 --- a/.github/actions/file/action.yml +++ b/.github/actions/file/action.yml @@ -14,6 +14,9 @@ inputs: cached_filings: description: "Cached filings from previous runs, as stringified JSON. Without this, duplicate issues may be filed." required: false + open_tracking_issues: + description: "In the 'file' step, also open tracking issues which link to all issues with the same problem" + required: false outputs: filings: diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index 0c1304b..618bcf0 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -11,17 +11,18 @@ import { isResolvedFiling } from "./isResolvedFiling.js"; import { openIssue } from "./openIssue.js"; import { reopenIssue } from "./reopenIssue.js"; import { updateFilingsWithNewFindings } from "./updateFilingsWithNewFindings.js"; +import { OctokitResponse } from "@octokit/types"; const OctokitWithThrottling = Octokit.plugin(throttling); export default async function () { core.info("Started 'file' action"); const findings: Finding[] = JSON.parse( - core.getInput("findings", { required: true }) + core.getInput("findings", { required: true }), ); const repoWithOwner = core.getInput("repository", { required: true }); const token = core.getInput("token", { required: true }); const cachedFilings: (ResolvedFiling | RepeatedFiling)[] = JSON.parse( - core.getInput("cached_filings", { required: false }) || "[]" + core.getInput("cached_filings", { required: false }) || "[]", ); core.debug(`Input: 'findings: ${JSON.stringify(findings)}'`); core.debug(`Input: 'repository: ${repoWithOwner}'`); @@ -32,7 +33,7 @@ export default async function () { throttle: { onRateLimit: (retryAfter, options, octokit, retryCount) => { octokit.log.warn( - `Request quota exhausted for request ${options.method} ${options.url}` + `Request quota exhausted for request ${options.method} ${options.url}`, ); if (retryCount < 3) { octokit.log.info(`Retrying after ${retryAfter} seconds!`); @@ -41,7 +42,7 @@ export default async function () { }, onSecondaryRateLimit: (retryAfter, options, octokit, retryCount) => { octokit.log.warn( - `Secondary rate limit hit for request ${options.method} ${options.url}` + `Secondary rate limit hit for request ${options.method} ${options.url}`, ); if (retryCount < 3) { octokit.log.info(`Retrying after ${retryAfter} seconds!`); @@ -52,8 +53,13 @@ export default async function () { }); const filings = updateFilingsWithNewFindings(cachedFilings, findings); + // Track new issues for grouping + const newIssuesByProblemShort: Record = + {}; + const trackingIssueUrls: Record = {}; + for (const filing of filings) { - let response; + let response: OctokitResponse | undefined; try { if (isResolvedFiling(filing)) { // Close the filing’s issue (if necessary) @@ -63,6 +69,14 @@ export default async function () { // Open a new issue for the filing response = await openIssue(octokit, repoWithOwner, filing.findings[0]); (filing as any).issue = { state: "open" } as Issue; + // Track for grouping + const problemShort: string = filing.findings[0].problemShort; + if (!newIssuesByProblemShort[problemShort]) + newIssuesByProblemShort[problemShort] = []; + newIssuesByProblemShort[problemShort].push({ + url: response.data.html_url, + id: response.data.number, + }); } else if (isRepeatedFiling(filing)) { // Reopen the filing’s issue (if necessary) response = await reopenIssue(octokit, new Issue(filing.issue)); @@ -75,7 +89,7 @@ export default async function () { filing.issue.url = response.data.html_url; filing.issue.title = response.data.title; core.info( - `Set issue ${response.data.title} (${repoWithOwner}#${response.data.number}) state to ${filing.issue.state}` + `Set issue ${response.data.title} (${repoWithOwner}#${response.data.number}) state to ${filing.issue.state}`, ); } } catch (error) { @@ -84,6 +98,38 @@ export default async function () { } } + // Open tracking issues for groups with >1 new issue and link back from each new issue + for (const [problemShort, issues] of Object.entries( + newIssuesByProblemShort, + ) as [string, { url: string; id: number }[]][]) { + if (issues.length > 1) { + const title: string = `${problemShort} issues`; + const body: string = + `# ${problemShort} issues\n\n` + + issues.map((issue) => `- [ ] ${issue.url}`).join("\n"); + try { + const trackingResponse = await octokit.request( + `POST /repos/${repoWithOwner}/issues`, + { + owner: repoWithOwner.split("/")[0], + repo: repoWithOwner.split("/")[1], + title, + body, + }, + ); + const trackingUrl: string = trackingResponse.data.html_url; + trackingIssueUrls[problemShort] = trackingUrl; + core.info( + `Opened tracking issue for '${problemShort}' with ${issues.length} issues.`, + ); + } catch (error) { + core.warning( + `Failed to open tracking issue for '${problemShort}': ${error}`, + ); + } + } + } + core.setOutput("filings", JSON.stringify(filings)); core.debug(`Output: 'filings: ${JSON.stringify(filings)}'`); core.info("Finished 'file' action"); diff --git a/action.yml b/action.yml index 3e3ee60..5b2391f 100644 --- a/action.yml +++ b/action.yml @@ -31,6 +31,10 @@ inputs: description: "Whether to skip assigning filed issues to Copilot" required: false default: "false" + open_tracking_issues: + description: "In the 'file' step, also open tracking issues which link to all issues with the same problem" + required: false + default: "false" outputs: results: @@ -88,6 +92,7 @@ runs: repository: ${{ inputs.repository }} token: ${{ inputs.token }} cached_filings: ${{ steps.normalize_cache.outputs.value }} + open_tracking_issues: ${{ inputs.open_tracking_issues }} - if: ${{ steps.file.outputs.filings }} name: Get issues from filings id: get_issues_from_filings From fd048ca2ce61e432de82e9195df33fa134f054b4 Mon Sep 17 00:00:00 2001 From: Joyce Zhu Date: Thu, 12 Feb 2026 18:39:07 +0000 Subject: [PATCH 2/4] Actually respect `open_tracking_issues` --- .github/actions/file/src/index.ts | 83 ++++++++++++++++--------------- 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index 618bcf0..1f377bc 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -24,6 +24,7 @@ export default async function () { const cachedFilings: (ResolvedFiling | RepeatedFiling)[] = JSON.parse( core.getInput("cached_filings", { required: false }) || "[]", ); + const shouldOpenTrackingIssues = core.getBooleanInput("open_tracking_issues"); core.debug(`Input: 'findings: ${JSON.stringify(findings)}'`); core.debug(`Input: 'repository: ${repoWithOwner}'`); core.debug(`Input: 'cached_filings: ${JSON.stringify(cachedFilings)}'`); @@ -70,13 +71,15 @@ export default async function () { response = await openIssue(octokit, repoWithOwner, filing.findings[0]); (filing as any).issue = { state: "open" } as Issue; // Track for grouping - const problemShort: string = filing.findings[0].problemShort; - if (!newIssuesByProblemShort[problemShort]) - newIssuesByProblemShort[problemShort] = []; - newIssuesByProblemShort[problemShort].push({ - url: response.data.html_url, - id: response.data.number, - }); + if (shouldOpenTrackingIssues) { + const problemShort: string = filing.findings[0].problemShort; + if (!newIssuesByProblemShort[problemShort]) + newIssuesByProblemShort[problemShort] = []; + newIssuesByProblemShort[problemShort].push({ + url: response.data.html_url, + id: response.data.number, + }); + } } else if (isRepeatedFiling(filing)) { // Reopen the filing’s issue (if necessary) response = await reopenIssue(octokit, new Issue(filing.issue)); @@ -98,39 +101,41 @@ export default async function () { } } - // Open tracking issues for groups with >1 new issue and link back from each new issue - for (const [problemShort, issues] of Object.entries( - newIssuesByProblemShort, - ) as [string, { url: string; id: number }[]][]) { - if (issues.length > 1) { - const title: string = `${problemShort} issues`; - const body: string = - `# ${problemShort} issues\n\n` + - issues.map((issue) => `- [ ] ${issue.url}`).join("\n"); - try { - const trackingResponse = await octokit.request( - `POST /repos/${repoWithOwner}/issues`, - { - owner: repoWithOwner.split("/")[0], - repo: repoWithOwner.split("/")[1], - title, - body, - }, - ); - const trackingUrl: string = trackingResponse.data.html_url; - trackingIssueUrls[problemShort] = trackingUrl; - core.info( - `Opened tracking issue for '${problemShort}' with ${issues.length} issues.`, - ); - } catch (error) { - core.warning( - `Failed to open tracking issue for '${problemShort}': ${error}`, - ); + // Open tracking issues for each root cause and link back from each newly-created issue + if (shouldOpenTrackingIssues) { + for (const [problemShort, issues] of Object.entries( + newIssuesByProblemShort, + ) as [string, { url: string; id: number }[]][]) { + if (issues.length > 1) { + const title: string = `${problemShort} issues`; + const body: string = + `# ${problemShort} issues\n\n` + + issues.map((issue) => `- [ ] ${issue.url}`).join("\n"); + try { + const trackingResponse = await octokit.request( + `POST /repos/${repoWithOwner}/issues`, + { + owner: repoWithOwner.split("/")[0], + repo: repoWithOwner.split("/")[1], + title, + body, + }, + ); + const trackingUrl: string = trackingResponse.data.html_url; + trackingIssueUrls[problemShort] = trackingUrl; + core.info( + `Opened tracking issue for '${problemShort}': ${issues.length} issues.`, + ); + } catch (error) { + core.warning( + `Failed to open tracking issue for '${problemShort}': ${error}`, + ); + } } } - } - core.setOutput("filings", JSON.stringify(filings)); - core.debug(`Output: 'filings: ${JSON.stringify(filings)}'`); - core.info("Finished 'file' action"); + core.setOutput("filings", JSON.stringify(filings)); + core.debug(`Output: 'filings: ${JSON.stringify(filings)}'`); + core.info("Finished 'file' action"); + } } From 3348e9d60c262b441fb1337a7f761126df576744 Mon Sep 17 00:00:00 2001 From: Joyce Zhu Date: Fri, 13 Feb 2026 20:15:53 +0000 Subject: [PATCH 3/4] Feedback from code review --- .github/actions/file/action.yml | 5 ++-- .github/actions/file/src/index.ts | 36 +++++++++++++++++------------ .github/actions/file/src/types.d.ts | 5 ++++ action.yml | 4 ++-- 4 files changed, 31 insertions(+), 19 deletions(-) diff --git a/.github/actions/file/action.yml b/.github/actions/file/action.yml index 188a763..7a80e5b 100644 --- a/.github/actions/file/action.yml +++ b/.github/actions/file/action.yml @@ -14,9 +14,10 @@ inputs: cached_filings: description: "Cached filings from previous runs, as stringified JSON. Without this, duplicate issues may be filed." required: false - open_tracking_issues: - description: "In the 'file' step, also open tracking issues which link to all issues with the same problem" + open_grouped_issues: + description: "In the 'file' step, also open tracking issues which link to all issues with the same root cause" required: false + default: "false" outputs: filings: diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index 1f377bc..f57b461 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -1,4 +1,10 @@ -import type { Finding, ResolvedFiling, RepeatedFiling } from "./types.d.js"; +import type { + Finding, + ResolvedFiling, + RepeatedFiling, + FindingGroupIssue, + Filing, +} from "./types.d.js"; import process from "node:process"; import core from "@actions/core"; import { Octokit } from "@octokit/core"; @@ -11,7 +17,7 @@ import { isResolvedFiling } from "./isResolvedFiling.js"; import { openIssue } from "./openIssue.js"; import { reopenIssue } from "./reopenIssue.js"; import { updateFilingsWithNewFindings } from "./updateFilingsWithNewFindings.js"; -import { OctokitResponse } from "@octokit/types"; +import type { OctokitResponse } from "@octokit/types"; const OctokitWithThrottling = Octokit.plugin(throttling); export default async function () { @@ -24,7 +30,7 @@ export default async function () { const cachedFilings: (ResolvedFiling | RepeatedFiling)[] = JSON.parse( core.getInput("cached_filings", { required: false }) || "[]", ); - const shouldOpenTrackingIssues = core.getBooleanInput("open_tracking_issues"); + const shouldOpenGroupedIssues = core.getBooleanInput("open_grouped_issues"); core.debug(`Input: 'findings: ${JSON.stringify(findings)}'`); core.debug(`Input: 'repository: ${repoWithOwner}'`); core.debug(`Input: 'cached_filings: ${JSON.stringify(cachedFilings)}'`); @@ -55,8 +61,7 @@ export default async function () { const filings = updateFilingsWithNewFindings(cachedFilings, findings); // Track new issues for grouping - const newIssuesByProblemShort: Record = - {}; + const newIssuesByProblemShort: Record = {}; const trackingIssueUrls: Record = {}; for (const filing of filings) { @@ -69,12 +74,13 @@ export default async function () { } else if (isNewFiling(filing)) { // Open a new issue for the filing response = await openIssue(octokit, repoWithOwner, filing.findings[0]); - (filing as any).issue = { state: "open" } as Issue; + (filing as Filing).issue = { state: "open" } as Issue; // Track for grouping - if (shouldOpenTrackingIssues) { + if (shouldOpenGroupedIssues) { const problemShort: string = filing.findings[0].problemShort; - if (!newIssuesByProblemShort[problemShort]) + if (!newIssuesByProblemShort[problemShort]) { newIssuesByProblemShort[problemShort] = []; + } newIssuesByProblemShort[problemShort].push({ url: response.data.html_url, id: response.data.number, @@ -102,12 +108,12 @@ export default async function () { } // Open tracking issues for each root cause and link back from each newly-created issue - if (shouldOpenTrackingIssues) { + if (shouldOpenGroupedIssues) { for (const [problemShort, issues] of Object.entries( newIssuesByProblemShort, - ) as [string, { url: string; id: number }[]][]) { + )) { if (issues.length > 1) { - const title: string = `${problemShort} issues`; + const title: string = `Accessibility tracking issue for all ${problemShort} issues`; const body: string = `# ${problemShort} issues\n\n` + issues.map((issue) => `- [ ] ${issue.url}`).join("\n"); @@ -133,9 +139,9 @@ export default async function () { } } } - - core.setOutput("filings", JSON.stringify(filings)); - core.debug(`Output: 'filings: ${JSON.stringify(filings)}'`); - core.info("Finished 'file' action"); } + + core.setOutput("filings", JSON.stringify(filings)); + core.debug(`Output: 'filings: ${JSON.stringify(filings)}'`); + core.info("Finished 'file' action"); } diff --git a/.github/actions/file/src/types.d.ts b/.github/actions/file/src/types.d.ts index bcc52ea..a42463a 100644 --- a/.github/actions/file/src/types.d.ts +++ b/.github/actions/file/src/types.d.ts @@ -33,3 +33,8 @@ export type RepeatedFiling = { }; export type Filing = ResolvedFiling | NewFiling | RepeatedFiling; + +export type FindingGroupIssue = { + url: string; + id: number; +}; diff --git a/action.yml b/action.yml index 5b2391f..27670c4 100644 --- a/action.yml +++ b/action.yml @@ -31,7 +31,7 @@ inputs: description: "Whether to skip assigning filed issues to Copilot" required: false default: "false" - open_tracking_issues: + open_grouped_issues: description: "In the 'file' step, also open tracking issues which link to all issues with the same problem" required: false default: "false" @@ -92,7 +92,7 @@ runs: repository: ${{ inputs.repository }} token: ${{ inputs.token }} cached_filings: ${{ steps.normalize_cache.outputs.value }} - open_tracking_issues: ${{ inputs.open_tracking_issues }} + open_grouped_issues: ${{ inputs.open_grouped_issues }} - if: ${{ steps.file.outputs.filings }} name: Get issues from filings id: get_issues_from_filings From e9f281d50edff24f68414bb950b64bc942f272ba Mon Sep 17 00:00:00 2001 From: Joyce Zhu Date: Fri, 13 Feb 2026 15:29:01 -0500 Subject: [PATCH 4/4] Apply suggestions from Lindsey Co-authored-by: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> --- .github/actions/file/action.yml | 2 +- .github/actions/file/src/index.ts | 2 +- action.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/file/action.yml b/.github/actions/file/action.yml index 7a80e5b..4bc8654 100644 --- a/.github/actions/file/action.yml +++ b/.github/actions/file/action.yml @@ -15,7 +15,7 @@ inputs: description: "Cached filings from previous runs, as stringified JSON. Without this, duplicate issues may be filed." required: false open_grouped_issues: - description: "In the 'file' step, also open tracking issues which link to all issues with the same root cause" + description: "In the 'file' step, also open grouped issues which link to all issues with the same root cause" required: false default: "false" diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index f57b461..d6362ec 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -134,7 +134,7 @@ export default async function () { ); } catch (error) { core.warning( - `Failed to open tracking issue for '${problemShort}': ${error}`, + `Failed to open grouped issue for '${problemShort}': ${error}`, ); } } diff --git a/action.yml b/action.yml index 27670c4..2bc88dd 100644 --- a/action.yml +++ b/action.yml @@ -32,7 +32,7 @@ inputs: required: false default: "false" open_grouped_issues: - description: "In the 'file' step, also open tracking issues which link to all issues with the same problem" + description: "In the 'file' step, also open grouped issues which link to all issues with the same problem" required: false default: "false"