Skip to content

Commit 684de22

Browse files
committed
feat: add workflow_dispatch trigger to accessibility scan and finops cost gate workflows
1 parent e417aa5 commit 684de22

2 files changed

Lines changed: 113 additions & 6 deletions

File tree

.github/workflows/accessibility-scan.yml

Lines changed: 112 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
name: Accessibility Scan
2020

2121
on:
22+
workflow_dispatch:
2223
pull_request:
2324
branches: [main]
2425
schedule:
@@ -53,17 +54,122 @@ jobs:
5354

5455
- name: Install scanning dependencies
5556
run: |
56-
npm install -g @axe-core/cli accessibility-checker
57+
npm install --no-save @axe-core/playwright accessibility-checker playwright
5758
npx playwright install --with-deps chromium
5859
60+
- name: Build and start sample app
61+
if: contains(matrix.scan-url.url, 'localhost')
62+
run: |
63+
cd sample-app
64+
npm ci
65+
npm run build
66+
npm start &
67+
# Wait for the server to be ready
68+
for i in $(seq 1 30); do
69+
if curl -s http://localhost:3000 > /dev/null 2>&1; then
70+
echo "Server is ready"
71+
break
72+
fi
73+
echo "Waiting for server... ($i/30)"
74+
sleep 2
75+
done
76+
5977
- name: Run accessibility scan
6078
id: scan
6179
run: |
62-
npx a11y-scan scan \
63-
--url "${{ matrix.scan-url.url }}" \
64-
--threshold ${{ env.A11Y_THRESHOLD }} \
65-
--format sarif \
66-
--output a11y-results.sarif
80+
# Run axe-core via Playwright (avoids ChromeDriver version issues)
81+
node -e '
82+
const { chromium } = require("playwright");
83+
const AxeBuilder = require("@axe-core/playwright").default;
84+
const fs = require("fs");
85+
86+
(async () => {
87+
const browser = await chromium.launch();
88+
const context = await browser.newContext();
89+
const page = await context.newPage();
90+
await page.goto("${{ matrix.scan-url.url }}", { waitUntil: "networkidle" });
91+
const axeResults = await new AxeBuilder({ page })
92+
.withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa", "best-practice"])
93+
.analyze();
94+
await browser.close();
95+
fs.writeFileSync("axe-results.json", JSON.stringify(axeResults, null, 2));
96+
console.log("Violations: " + axeResults.violations.length);
97+
})();
98+
'
99+
100+
# Convert axe JSON output to SARIF v2.1.0
101+
node -e '
102+
const fs = require("fs");
103+
if (!fs.existsSync("axe-results.json")) {
104+
console.error("No axe results found");
105+
process.exit(1);
106+
}
107+
const axeResults = JSON.parse(fs.readFileSync("axe-results.json", "utf8"));
108+
const results = Array.isArray(axeResults) ? axeResults : [axeResults];
109+
110+
const impactToLevel = { critical: "error", serious: "error", moderate: "warning", minor: "note" };
111+
const impactToSeverity = { critical: "9.0", serious: "7.0", moderate: "4.0", minor: "1.0" };
112+
const impactToWeight = { critical: 10, serious: 7, moderate: 3, minor: 1 };
113+
114+
const rulesMap = new Map();
115+
const sarifResults = [];
116+
let totalWeight = 0;
117+
let maxWeight = 0;
118+
119+
// Map URL path to a source file relative to the repo root
120+
function urlToSourceFile(pageUrl) {
121+
try {
122+
const urlPath = new URL(pageUrl).pathname;
123+
const clean = urlPath === "/" ? "" : urlPath.replace(/\/$/, "");
124+
if (clean === "") return "sample-app/src/app/page.tsx";
125+
return "sample-app/src/app" + clean + "/page.tsx";
126+
} catch { return "sample-app/src/app/page.tsx"; }
127+
}
128+
129+
for (const page of results) {
130+
const sourceFile = urlToSourceFile(page.url || "${{ matrix.scan-url.url }}");
131+
for (const violation of (page.violations || [])) {
132+
if (!rulesMap.has(violation.id)) {
133+
rulesMap.set(violation.id, {
134+
id: violation.id,
135+
shortDescription: { text: violation.help || violation.id },
136+
fullDescription: { text: violation.description || "" },
137+
help: { text: violation.help || "", markdown: violation.helpUrl ? `${violation.help || ""}. [Learn more](${violation.helpUrl})` : violation.help || "" },
138+
properties: { tags: ["accessibility"].concat(violation.tags || []) }
139+
});
140+
}
141+
const weight = impactToWeight[violation.impact] || 1;
142+
for (const node of (violation.nodes || [])) {
143+
maxWeight += 10;
144+
totalWeight += weight;
145+
const target = (node.target || []).join(" ");
146+
sarifResults.push({
147+
ruleId: violation.id,
148+
level: impactToLevel[violation.impact] || "warning",
149+
message: { text: node.failureSummary || violation.help || violation.id },
150+
locations: [{ physicalLocation: { artifactLocation: { uri: sourceFile }, region: { snippet: { text: node.html || "" } } } }],
151+
partialFingerprints: { primaryLocationLineHash: require("crypto").createHash("sha256").update(violation.id + ":" + target).digest("hex").slice(0, 16) },
152+
properties: { impact: violation.impact, target, "security-severity": impactToSeverity[violation.impact] || "1.0" }
153+
});
154+
}
155+
}
156+
}
157+
158+
const score = maxWeight > 0 ? Math.round((1 - totalWeight / maxWeight) * 100) : 100;
159+
const sarif = {
160+
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
161+
version: "2.1.0",
162+
runs: [{
163+
tool: { driver: { name: "accessibility-scanner", version: "1.0.0", rules: [...rulesMap.values()] } },
164+
automationDetails: { id: "accessibility-scan/${{ matrix.scan-url.label }}" },
165+
results: sarifResults,
166+
properties: { score }
167+
}]
168+
};
169+
fs.writeFileSync("a11y-results.sarif", JSON.stringify(sarif, null, 2));
170+
console.log("Accessibility score: " + score);
171+
console.log("Violations found: " + sarifResults.length);
172+
'
67173
continue-on-error: true
68174

69175
- name: Upload a11y SARIF

.github/workflows/finops-cost-gate.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
name: FinOps Cost Gate
2424

2525
on:
26+
workflow_dispatch:
2627
pull_request:
2728
paths:
2829
- '**/*.tf'

0 commit comments

Comments
 (0)