Skip to content

Commit 14a7ae9

Browse files
committed
Adds screenshots to issues
1 parent 7c85a7e commit 14a7ae9

10 files changed

Lines changed: 109 additions & 18 deletions

File tree

.github/actions/file/src/generateIssueBody.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,29 @@ export function generateIssueBody(finding: Finding, repoWithOwner: string): stri
1111
: line
1212
)
1313
.join("\n");
14-
const acceptanceCriteria = `## Acceptance Criteria
14+
15+
let screenshotSection;
16+
if (finding.screenshotId) {
17+
const screenshotUrl = `https://raw.githubusercontent.com/${repoWithOwner}/gh-cache/.screenshots/${finding.screenshotId}.png`;
18+
screenshotSection = `\n\n
19+
20+
![Screenshot of the issue on ${finding.url}](${screenshotUrl})
21+
`;
22+
}
23+
24+
const acceptanceCriteria = `## Acceptance Criteria
1525
- [ ] The specific axe violation reported in this issue is no longer reproducible.
1626
- [ ] The fix MUST meet WCAG 2.1 guidelines OR the accessibility standards specified by the repository or organization.
1727
- [ ] A test SHOULD be added to ensure this specific axe violation does not regress.
1828
- [ ] This PR MUST NOT introduce any new accessibility issues or regressions.
1929
`;
20-
const body = `## What
30+
31+
const body = `## What
2132
An accessibility scan flagged the element \`${finding.html}\` on ${finding.url} because ${finding.problemShort}. Learn more about why this was flagged by visiting ${finding.problemUrl}.
2233
34+
${screenshotSection ?? ""}
2335
To fix this, ${finding.solutionShort}.
24-
${solutionLong ? `\nSpecifically:\n\n${solutionLong}` : ''}
36+
${solutionLong ? `\nSpecifically:\n\n${solutionLong}` : ""}
2537
2638
${acceptanceCriteria}
2739
`;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export type Finding = {
77
problemUrl: string;
88
solutionShort: string;
99
solutionLong?: string;
10+
screenshotId?: string;
1011
};
1112

1213
export type Issue = {

.github/actions/find/action.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ inputs:
99
auth_context:
1010
description: "Stringified JSON object containing 'username', 'password', 'cookies', and/or 'localStorage' from an authenticated session"
1111
required: false
12+
include_screenshots:
13+
description: "Whether to capture screenshots of scanned pages"
14+
required: false
15+
default: "true"
1216

1317
outputs:
1418
findings:

.github/actions/find/src/findForUrl.ts

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,25 @@ import type { Finding } from './types.d.js';
22
import AxeBuilder from '@axe-core/playwright'
33
import playwright from 'playwright';
44
import { AuthContext } from './AuthContext.js';
5+
import fs from "node:fs";
6+
import path from "node:path";
57

6-
export async function findForUrl(url: string, authContext?: AuthContext): Promise<Finding[]> {
7-
const browser = await playwright.chromium.launch({ headless: true, executablePath: process.env.CI ? '/usr/bin/google-chrome' : undefined });
8+
// Use GITHUB_WORKSPACE to ensure screenshots are saved in the workflow workspace root
9+
// where the artifact upload step can find them
10+
const SCREENSHOT_DIR = path.join(
11+
process.env.GITHUB_WORKSPACE || process.cwd(),
12+
".screenshots",
13+
);
14+
15+
export async function findForUrl(
16+
url: string,
17+
authContext?: AuthContext,
18+
includeScreenshots: boolean = true,
19+
): Promise<Finding[]> {
20+
const browser = await playwright.chromium.launch({
21+
headless: true,
22+
executablePath: process.env.CI ? "/usr/bin/google-chrome" : undefined,
23+
});
824
const contextOptions = authContext?.toPlaywrightBrowserContextOptions() ?? {};
925
const context = await browser.newContext(contextOptions);
1026
const page = await context.newPage();
@@ -14,15 +30,46 @@ export async function findForUrl(url: string, authContext?: AuthContext): Promis
1430
let findings: Finding[] = [];
1531
try {
1632
const rawFindings = await new AxeBuilder({ page }).analyze();
17-
findings = rawFindings.violations.map(violation => ({
18-
scannerType: 'axe',
33+
34+
let screenshotId: string | undefined;
35+
36+
if (includeScreenshots) {
37+
// Ensure screenshot directory exists
38+
if (!fs.existsSync(SCREENSHOT_DIR)) {
39+
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
40+
console.log(`Created screenshot directory: ${SCREENSHOT_DIR}`);
41+
}
42+
43+
try {
44+
const screenshotBuffer = await page.screenshot({
45+
fullPage: true,
46+
type: "png",
47+
});
48+
49+
screenshotId = crypto.randomUUID();
50+
const filename = `${screenshotId}.png`;
51+
const filepath = path.join(SCREENSHOT_DIR, filename);
52+
53+
fs.writeFileSync(filepath, screenshotBuffer);
54+
console.log(`Screenshot saved: ${filename}`);
55+
} catch (error) {
56+
console.error("Failed to capture/save screenshot:", error);
57+
screenshotId = undefined;
58+
}
59+
}
60+
61+
findings = rawFindings.violations.map((violation) => ({
62+
scannerType: "axe",
1963
url,
2064
html: violation.nodes[0].html.replace(/'/g, "&apos;"),
2165
problemShort: violation.help.toLowerCase().replace(/'/g, "&apos;"),
2266
problemUrl: violation.helpUrl.replace(/'/g, "&apos;"),
2367
ruleId: violation.id,
24-
solutionShort: violation.description.toLowerCase().replace(/'/g, "&apos;"),
25-
solutionLong: violation.nodes[0].failureSummary?.replace(/'/g, "&apos;")
68+
solutionShort: violation.description
69+
.toLowerCase()
70+
.replace(/'/g, "&apos;"),
71+
solutionLong: violation.nodes[0].failureSummary?.replace(/'/g, "&apos;"),
72+
screenshotId,
2673
}));
2774
} catch (e) {
2875
// do something with the error

.github/actions/find/src/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,17 @@ export default async function () {
1212
);
1313
const authContext = new AuthContext(authContextInput);
1414

15+
const includeScreenshots =
16+
core.getInput("include_screenshots", { required: false }) !== "false";
17+
1518
let findings = [];
1619
for (const url of urls) {
1720
core.info(`Preparing to scan ${url}`);
18-
const findingsForUrl = await findForUrl(url, authContext);
21+
const findingsForUrl = await findForUrl(
22+
url,
23+
authContext,
24+
includeScreenshots,
25+
);
1926
if (findingsForUrl.length === 0) {
2027
core.info(`No accessibility gaps were found on ${url}`);
2128
continue;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export type Finding = {
55
problemUrl: string;
66
solutionShort: string;
77
solutionLong?: string;
8+
screenshotId?: string;
89
};
910

1011
export type Cookie = {

.github/actions/gh-cache/delete/action.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,20 +60,22 @@ runs:
6060
echo "Created new orphaned 'gh-cache' branch"
6161
fi
6262
63-
- name: Copy file to repo
63+
- name: Delete file from repo
6464
shell: bash
6565
run: |
66-
if [ -f "${{ inputs.path }}" ]; then
66+
if [ -e "${{ inputs.path }}" ]; then
6767
rm -Rf "${{ inputs.path }}"
68-
rm -Rf ".gh-cache-${{ github.run_id }}/${{ inputs.path }}"
6968
echo "Deleted '${{ inputs.path }}' from 'gh-cache' branch"
69+
else
70+
echo "'${{ inputs.path }}' does not exist in 'gh-cache' branch"
71+
echo "Skipping delete"
7072
fi
7173
7274
- name: Commit and push
7375
shell: bash
7476
working-directory: .gh-cache-${{ github.run_id }}
7577
run: |
76-
git add "${{ inputs.path }}" || true
78+
git add --all -- "${{ inputs.path }}" || true
7779
git config user.name "github-actions[bot]"
7880
git config user.email "github-actions[bot]@users.noreply.github.com"
7981
git commit -m "$(printf 'Delete artifact: %s' "${{ inputs.path }}")" \

.github/actions/gh-cache/restore/action.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ runs:
7272
src=".gh-cache-${{ github.run_id }}/${{ inputs.path }}"
7373
dest="${{ inputs.path }}"
7474
if [ -e "$src" ]; then
75+
if [ -d "$src" ]; then
76+
rm -rf "$dest"
77+
fi
7578
mkdir -p "$(dirname "$dest")"
7679
cp -Rf "$src" "$dest"
7780
echo "Restored '${{ inputs.path }}' from cache"

.github/actions/gh-cache/save/action.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,21 +60,24 @@ runs:
6060
echo "Created new orphaned 'gh-cache' branch"
6161
fi
6262
63-
- name: Copy file to repo
63+
- name: Copy file or directory to repo
6464
shell: bash
6565
run: |
66-
if [ -f "${{ inputs.path }}" ]; then
66+
if [ -e "${{ inputs.path }}" ]; then
6767
mkdir -p ".gh-cache-${{ github.run_id }}/$(dirname "${{ inputs.path }}")"
6868
cp -Rf "${{ inputs.path }}" ".gh-cache-${{ github.run_id }}/${{ inputs.path }}"
6969
echo "Copied '${{ inputs.path }}' to 'gh-cache' branch"
70+
else
71+
echo "'${{ inputs.path }}' does not exist"
72+
echo "Skipping copy"
7073
fi
7174
7275
- name: Commit and push
7376
shell: bash
7477
working-directory: .gh-cache-${{ github.run_id }}
7578
run: |
76-
if [ -f "${{ inputs.path }}" ]; then
77-
git add "${{ inputs.path }}"
79+
if [ -e "${{ inputs.path }}" ]; then
80+
git add -- "${{ inputs.path }}"
7881
git config user.name "github-actions[bot]"
7982
git config user.email "github-actions[bot]@users.noreply.github.com"
8083
git commit -m "$(printf 'Save artifact: %s' "${{ inputs.path }}")" \

action.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ inputs:
3131
description: "Whether to skip assigning filed issues to Copilot"
3232
required: false
3333
default: "false"
34+
include_screenshots:
35+
description: "Whether to include screenshots of issues in the issue bodies"
36+
required: false
37+
default: "true"
3438

3539
outputs:
3640
results:
@@ -80,6 +84,7 @@ runs:
8084
with:
8185
urls: ${{ inputs.urls }}
8286
auth_context: ${{ inputs.auth_context || steps.auth.outputs.auth_context }}
87+
include_screenshots: ${{ inputs.include_screenshots }}
8388
- name: File
8489
id: file
8590
uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/file
@@ -125,6 +130,12 @@ runs:
125130
}
126131
core.setOutput('results', JSON.stringify(results));
127132
core.debug(`Results: ${JSON.stringify(results)}`);
133+
- if: ${{ inputs.include_screenshots == 'true' }}
134+
name: Save screenshots
135+
uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/gh-cache/save
136+
with:
137+
path: .screenshots
138+
token: ${{ inputs.token }}
128139
- name: Save cached results
129140
uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/gh-cache/cache
130141
with:

0 commit comments

Comments
 (0)