Skip to content

Commit d34995a

Browse files
authored
Adds an option for grouping related issues (#118)
Adds `open_grouped_issues` option that will group a single violation's issues in one issue (in addition to the individual issues filed). Note: in it's current state, grouped issues will only open when new issues are found. If there are existing issues in the repo (I.e. the scans have run multiple times before), grouped issues will not be opened for those. This is a known limitation at this point that we plan to address in the future if it becomes troublesome. Example: <img width="935" height="425" alt="Screen shot of a new grouped issue, with the title 'heading levels should only increase by one issues' and the body of the issue is a checklist of 3 linked issues with this violation" src="https://github.com/user-attachments/assets/1629aff2-3161-42a5-a680-5022ebcdcc06" /> Also fixes the `site-with-errors.test.ts` which was failing if Copilot failed to create PRs (which can happen). The new logic will only check the PR author, state, and assignee if the PRs exist.
2 parents 9c3f4a5 + a9c33ce commit d34995a

File tree

5 files changed

+71
-4
lines changed

5 files changed

+71
-4
lines changed

.github/actions/file/action.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ inputs:
1717
screenshot_repository:
1818
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."
1919
required: false
20+
open_grouped_issues:
21+
description: "In the 'file' step, also open grouped issues which link to all issues with the same root cause"
22+
required: false
23+
default: "false"
2024

2125
outputs:
2226
filings:

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

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {Finding, ResolvedFiling, RepeatedFiling} from './types.d.js'
1+
import type {Finding, ResolvedFiling, RepeatedFiling, FindingGroupIssue, Filing, IssueResponse} from './types.d.js'
22
import process from 'node:process'
33
import * as core from '@actions/core'
44
import {Octokit} from '@octokit/core'
@@ -11,6 +11,7 @@ import {isResolvedFiling} from './isResolvedFiling.js'
1111
import {openIssue} from './openIssue.js'
1212
import {reopenIssue} from './reopenIssue.js'
1313
import {updateFilingsWithNewFindings} from './updateFilingsWithNewFindings.js'
14+
import {OctokitResponse} from '@octokit/types'
1415
const OctokitWithThrottling = Octokit.plugin(throttling)
1516

1617
export default async function () {
@@ -22,10 +23,12 @@ export default async function () {
2223
const cachedFilings: (ResolvedFiling | RepeatedFiling)[] = JSON.parse(
2324
core.getInput('cached_filings', {required: false}) || '[]',
2425
)
26+
const shouldOpenGroupedIssues = core.getBooleanInput('open_grouped_issues')
2527
core.debug(`Input: 'findings: ${JSON.stringify(findings)}'`)
2628
core.debug(`Input: 'repository: ${repoWithOwner}'`)
2729
core.debug(`Input: 'screenshot_repository: ${screenshotRepo}'`)
2830
core.debug(`Input: 'cached_filings: ${JSON.stringify(cachedFilings)}'`)
31+
core.debug(`Input: 'open_grouped_issues: ${shouldOpenGroupedIssues}'`)
2932

3033
const octokit = new OctokitWithThrottling({
3134
auth: token,
@@ -48,8 +51,12 @@ export default async function () {
4851
})
4952
const filings = updateFilingsWithNewFindings(cachedFilings, findings)
5053

54+
// Track new issues for grouping
55+
const newIssuesByProblemShort: Record<string, FindingGroupIssue[]> = {}
56+
const trackingIssueUrls: Record<string, string> = {}
57+
5158
for (const filing of filings) {
52-
let response
59+
let response: OctokitResponse<IssueResponse> | undefined
5360
try {
5461
if (isResolvedFiling(filing)) {
5562
// Close the filing’s issue (if necessary)
@@ -58,8 +65,19 @@ export default async function () {
5865
} else if (isNewFiling(filing)) {
5966
// Open a new issue for the filing
6067
response = await openIssue(octokit, repoWithOwner, filing.findings[0], screenshotRepo)
61-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
62-
;(filing as any).issue = {state: 'open'} as Issue
68+
;(filing as Filing).issue = {state: 'open'} as Issue
69+
70+
// Track for grouping
71+
if (shouldOpenGroupedIssues) {
72+
const problemShort: string = filing.findings[0].problemShort
73+
if (!newIssuesByProblemShort[problemShort]) {
74+
newIssuesByProblemShort[problemShort] = []
75+
}
76+
newIssuesByProblemShort[problemShort].push({
77+
url: response.data.html_url,
78+
id: response.data.number,
79+
})
80+
}
6381
} else if (isRepeatedFiling(filing)) {
6482
// Reopen the filing's issue (if necessary) and update the body with the latest finding
6583
response = await reopenIssue(
@@ -87,6 +105,32 @@ export default async function () {
87105
}
88106
}
89107

108+
// Open tracking issues for groups with >1 new issue and link back from each
109+
// new issue
110+
if (shouldOpenGroupedIssues) {
111+
for (const [problemShort, issues] of Object.entries(newIssuesByProblemShort)) {
112+
if (issues.length > 1) {
113+
const capitalizedProblemShort = problemShort[0].toUpperCase() + problemShort.slice(1)
114+
const title: string = `${capitalizedProblemShort} issues`
115+
const body: string =
116+
`# ${capitalizedProblemShort} issues\n\n` + issues.map(issue => `- [ ] ${issue.url}`).join('\n')
117+
try {
118+
const trackingResponse = await octokit.request(`POST /repos/${repoWithOwner}/issues`, {
119+
owner: repoWithOwner.split('/')[0],
120+
repo: repoWithOwner.split('/')[1],
121+
title,
122+
body,
123+
})
124+
const trackingUrl: string = trackingResponse.data.html_url
125+
trackingIssueUrls[problemShort] = trackingUrl
126+
core.info(`Opened tracking issue for '${capitalizedProblemShort}' with ${issues.length} issues.`)
127+
} catch (error) {
128+
core.warning(`Failed to open tracking issue for '${capitalizedProblemShort}': ${error}`)
129+
}
130+
}
131+
}
132+
}
133+
90134
core.setOutput('filings', JSON.stringify(filings))
91135
core.debug(`Output: 'filings: ${JSON.stringify(filings)}'`)
92136
core.info("Finished 'file' action")

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ export type Issue = {
1818
state?: 'open' | 'reopened' | 'closed'
1919
}
2020

21+
export type IssueResponse = {
22+
id: number
23+
node_id: string
24+
number: number
25+
html_url: string
26+
title: string
27+
}
28+
2129
export type ResolvedFiling = {
2230
findings: never[]
2331
issue: Issue
@@ -34,3 +42,8 @@ export type RepeatedFiling = {
3442
}
3543

3644
export type Filing = ResolvedFiling | NewFiling | RepeatedFiling
45+
46+
export type FindingGroupIssue = {
47+
url: string
48+
id: number
49+
}

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ jobs:
5353
# auth_context: # Optional: Stringified JSON object for complex authentication
5454
# skip_copilot_assignment: false # Optional: Set to true to skip assigning issues to GitHub Copilot (or if you don't have GitHub Copilot)
5555
# include_screenshots: false # Optional: Set to true to capture screenshots and include links to them in filed issues
56+
# open_grouped_issues: false # Optional: Set to true to open an issue grouping individual issues per violation
5657
# reduced_motion: no-preference # Optional: Playwright reduced motion configuration option
5758
# color_scheme: light # Optional: Playwright color scheme configuration option
5859
```

action.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ inputs:
3535
description: "Whether to capture screenshots and include links to them in the issue"
3636
required: false
3737
default: "false"
38+
open_grouped_issues:
39+
description: "In the 'file' step, also open grouped issues which link to all issues with the same problem"
40+
required: false
41+
default: "false"
3842
reduced_motion:
3943
description: "Playwright reducedMotion setting: https://playwright.dev/docs/api/class-browser#browser-new-page-option-reduced-motion"
4044
required: false
@@ -102,6 +106,7 @@ runs:
102106
token: ${{ inputs.token }}
103107
cached_filings: ${{ steps.normalize_cache.outputs.value }}
104108
screenshot_repository: ${{ github.repository }}
109+
open_grouped_issues: ${{ inputs.open_grouped_issues }}
105110
- if: ${{ steps.file.outputs.filings }}
106111
name: Get issues from filings
107112
id: get_issues_from_filings

0 commit comments

Comments
 (0)