From 17257b2f16509f0294adeffdad2023ffc93d07bd Mon Sep 17 00:00:00 2001 From: Joyce Zhu Date: Thu, 12 Feb 2026 07:03:56 +0000 Subject: [PATCH 1/2] Initial attempt at grouping related issues Actually respect `open_tracking_issues` Feedback from code review Apply suggestions from Lindsey Co-authored-by: Lindsey Wild <35239154+lindseywild@users.noreply.github.com> --- .github/actions/file/action.yml | 4 ++ .github/actions/file/src/index.ts | 67 ++++++++++++++++++++++++++--- .github/actions/file/src/types.d.ts | 5 +++ action.yml | 3 ++ 4 files changed, 72 insertions(+), 7 deletions(-) diff --git a/.github/actions/file/action.yml b/.github/actions/file/action.yml index c5db9d5..40c6394 100644 --- a/.github/actions/file/action.yml +++ b/.github/actions/file/action.yml @@ -17,6 +17,10 @@ inputs: screenshot_repository: description: "Repository (with owner) where screenshots are stored on the gh-cache branch. Defaults to the 'repository' input if not set. Required if issues are open in a different repo to construct proper screenshot URLs." required: false + open_grouped_issues: + description: "In the 'file' step, also open grouped 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 e95f6d4..249c61d 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -1,4 +1,4 @@ -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,6 +11,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' const OctokitWithThrottling = Octokit.plugin(throttling) export default async function () { @@ -22,10 +23,12 @@ export default async function () { const cachedFilings: (ResolvedFiling | RepeatedFiling)[] = JSON.parse( core.getInput('cached_filings', {required: false}) || '[]', ) + const shouldOpenGroupedIssues = core.getBooleanInput("open_grouped_issues") core.debug(`Input: 'findings: ${JSON.stringify(findings)}'`) core.debug(`Input: 'repository: ${repoWithOwner}'`) core.debug(`Input: 'screenshot_repository: ${screenshotRepo}'`) core.debug(`Input: 'cached_filings: ${JSON.stringify(cachedFilings)}'`) + core.debug(`Input: 'open_grouped_issues: ${shouldOpenGroupedIssues}'`) const octokit = new OctokitWithThrottling({ auth: token, @@ -48,8 +51,12 @@ 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) @@ -58,8 +65,19 @@ export default async function () { } else if (isNewFiling(filing)) { // Open a new issue for the filing response = await openIssue(octokit, repoWithOwner, filing.findings[0], screenshotRepo) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ;(filing as any).issue = {state: 'open'} as Issue + ;(filing as Filing).issue = {state: 'open'} as Issue + + // Track for grouping + if (shouldOpenGroupedIssues) { + 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) and update the body with the latest finding response = await reopenIssue( @@ -87,7 +105,42 @@ export default async function () { } } - core.setOutput('filings', JSON.stringify(filings)) - core.debug(`Output: 'filings: ${JSON.stringify(filings)}'`) - core.info("Finished 'file' action") + // Open tracking issues for groups with >1 new issue and link back from each + // new issue + if (shouldOpenGroupedIssues) { + for (const [problemShort, issues] of Object.entries( + newIssuesByProblemShort, + )) { + 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/.github/actions/file/src/types.d.ts b/.github/actions/file/src/types.d.ts index 36069a5..fc669de 100644 --- a/.github/actions/file/src/types.d.ts +++ b/.github/actions/file/src/types.d.ts @@ -34,3 +34,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 932bc27..149851e 100644 --- a/action.yml +++ b/action.yml @@ -33,6 +33,8 @@ inputs: default: "false" include_screenshots: description: "Whether to capture screenshots and include links to them in the issue" + open_grouped_issues: + description: "In the 'file' step, also open grouped issues which link to all issues with the same problem" required: false default: "false" @@ -94,6 +96,7 @@ runs: token: ${{ inputs.token }} cached_filings: ${{ steps.normalize_cache.outputs.value }} screenshot_repository: ${{ github.repository }} + open_grouped_issues: ${{ inputs.open_grouped_issues }} - if: ${{ steps.file.outputs.filings }} name: Get issues from filings id: get_issues_from_filings From b403a464459d0551547dced548fb2506b56d0a36 Mon Sep 17 00:00:00 2001 From: Joyce Zhu Date: Sat, 21 Feb 2026 06:24:27 +0000 Subject: [PATCH 2/2] Add OctokitResponse type --- .github/actions/file/src/index.ts | 51 +++++++++++------------------ .github/actions/file/src/types.d.ts | 10 +++++- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index 249c61d..36f2279 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -1,4 +1,4 @@ -import type {Finding, ResolvedFiling, RepeatedFiling, FindingGroupIssue, Filing} from './types.d.js' +import type {Finding, ResolvedFiling, RepeatedFiling, FindingGroupIssue, Filing, IssueResponse} from './types.d.js' import process from 'node:process' import core from '@actions/core' import {Octokit} from '@octokit/core' @@ -11,7 +11,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 {OctokitResponse} from '@octokit/types' const OctokitWithThrottling = Octokit.plugin(throttling) export default async function () { @@ -23,7 +23,7 @@ export default async function () { const cachedFilings: (ResolvedFiling | RepeatedFiling)[] = JSON.parse( core.getInput('cached_filings', {required: false}) || '[]', ) - const shouldOpenGroupedIssues = core.getBooleanInput("open_grouped_issues") + const shouldOpenGroupedIssues = core.getBooleanInput('open_grouped_issues') core.debug(`Input: 'findings: ${JSON.stringify(findings)}'`) core.debug(`Input: 'repository: ${repoWithOwner}'`) core.debug(`Input: 'screenshot_repository: ${screenshotRepo}'`) @@ -56,7 +56,7 @@ export default async function () { const trackingIssueUrls: Record = {} for (const filing of filings) { - let response: OctokitResponse | undefined; + let response: OctokitResponse | undefined try { if (isResolvedFiling(filing)) { // Close the filing’s issue (if necessary) @@ -108,39 +108,28 @@ export default async function () { // Open tracking issues for groups with >1 new issue and link back from each // new issue if (shouldOpenGroupedIssues) { - for (const [problemShort, issues] of Object.entries( - newIssuesByProblemShort, - )) { + for (const [problemShort, issues] of Object.entries(newIssuesByProblemShort)) { if (issues.length > 1) { - const title: string = `${problemShort} issues`; - const body: string = - `# ${problemShort} issues\n\n` + - issues.map((issue) => `- [ ] ${issue.url}`).join("\n"); + 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.`, - ); + 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.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") } diff --git a/.github/actions/file/src/types.d.ts b/.github/actions/file/src/types.d.ts index fc669de..2c0c8ac 100644 --- a/.github/actions/file/src/types.d.ts +++ b/.github/actions/file/src/types.d.ts @@ -18,6 +18,14 @@ export type Issue = { state?: 'open' | 'reopened' | 'closed' } +export type IssueResponse = { + id: number + node_id: string + number: number + html_url: string + title: string +} + export type ResolvedFiling = { findings: never[] issue: Issue @@ -38,4 +46,4 @@ export type Filing = ResolvedFiling | NewFiling | RepeatedFiling export type FindingGroupIssue = { url: string id: number -}; +}