Skip to content

Commit eed5e49

Browse files
ci: add daily rgaa review (#2874)
1 parent 7894638 commit eed5e49

8 files changed

Lines changed: 366 additions & 1 deletion

File tree

.github/workflows/rgaa-audit.yaml

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
name: "♿ RGAA Daily Audit"
2+
3+
on:
4+
schedule:
5+
# Every weekday at 06:00 UTC (08:00 Paris time)
6+
- cron: "0 6 * * 1-5"
7+
workflow_dispatch:
8+
inputs:
9+
site_url:
10+
description: "URL to audit (leave empty for alpha deployment)"
11+
required: false
12+
type: string
13+
14+
concurrency:
15+
group: rgaa-audit
16+
cancel-in-progress: true
17+
18+
jobs:
19+
scan:
20+
name: "axe-core scan"
21+
runs-on: ubuntu-latest
22+
outputs:
23+
site_url: ${{ steps.url.outputs.site_url }}
24+
audit_date: ${{ steps.date.outputs.today }}
25+
steps:
26+
- name: Checkout repository
27+
uses: actions/checkout@v4
28+
with:
29+
ref: alpha
30+
31+
- name: Get current date
32+
id: date
33+
run: echo "today=$(date -u +%Y-%m-%d)" >> "$GITHUB_OUTPUT"
34+
35+
- name: Setup pnpm
36+
uses: pnpm/action-setup@v4
37+
38+
- name: Setup Node
39+
uses: actions/setup-node@v4
40+
with:
41+
node-version-file: ".nvmrc"
42+
cache: pnpm
43+
44+
- name: Install dependencies
45+
run: pnpm install --frozen-lockfile
46+
47+
- name: Install Playwright browsers
48+
run: pnpm playwright:install
49+
50+
- name: Compute alpha deployment URL
51+
if: ${{ !inputs.site_url }}
52+
id: env
53+
uses: socialgouv/kontinuous/.github/actions/env@v1
54+
with:
55+
branch: alpha
56+
57+
- name: Set site URL
58+
id: url
59+
env:
60+
INPUT_SITE_URL: ${{ inputs.site_url }}
61+
SUBDOMAIN: ${{ steps.env.outputs.subdomain }}
62+
run: |
63+
if [ -n "$INPUT_SITE_URL" ]; then
64+
echo "site_url=$INPUT_SITE_URL" >> "$GITHUB_OUTPUT"
65+
else
66+
echo "site_url=https://${SUBDOMAIN}.ovh.fabrique.social.gouv.fr" >> "$GITHUB_OUTPUT"
67+
fi
68+
69+
- name: Wait for app to be ready
70+
env:
71+
SITE_URL: ${{ steps.url.outputs.site_url }}
72+
run: |
73+
for i in $(seq 1 10); do
74+
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$SITE_URL" 2>/dev/null || echo "000")
75+
if [ "$HTTP_CODE" = "200" ]; then
76+
echo "App is ready!"
77+
exit 0
78+
fi
79+
echo "Health check returned $HTTP_CODE (attempt $i/10). Waiting 10s..."
80+
sleep 10
81+
done
82+
echo "::error::App not ready after 100 seconds"
83+
exit 1
84+
85+
- name: Run RGAA audit
86+
run: pnpm test:rgaa
87+
env:
88+
SITE_URL: ${{ steps.url.outputs.site_url }}
89+
PLAYWRIGHT_BASE_URL: ${{ steps.url.outputs.site_url }}
90+
CI: "true"
91+
92+
- name: Upload axe-core results
93+
uses: actions/upload-artifact@v4
94+
if: always()
95+
with:
96+
name: rgaa-results
97+
path: packages/app/rgaa-results.json
98+
retention-days: 30
99+
100+
report:
101+
name: "Claude report & wiki publish"
102+
needs: scan
103+
if: always() && needs.scan.result != 'cancelled'
104+
runs-on: ubuntu-latest
105+
permissions:
106+
contents: write
107+
id-token: write
108+
steps:
109+
- name: Checkout repository
110+
uses: actions/checkout@v4
111+
with:
112+
ref: alpha
113+
114+
- name: Download axe-core results
115+
uses: actions/download-artifact@v4
116+
with:
117+
name: rgaa-results
118+
path: .
119+
120+
- name: Run Claude Code — Generate report & publish to wiki
121+
id: claude
122+
uses: anthropics/claude-code-action@9d86c9b0c946914e9c71ac5ee1c008959cbfa9af # v1
123+
with:
124+
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
125+
show_full_output: true
126+
direct_prompt: |
127+
Tu es un expert accessibilité RGAA. Tu dois rédiger un rapport d'audit RGAA à partir des résultats axe-core ci-dessous, puis le publier sur le wiki GitHub.
128+
129+
## Données
130+
131+
- URL auditée : ${{ needs.scan.outputs.site_url }}
132+
- Commit : ${{ github.sha }}
133+
- Date : ${{ needs.scan.outputs.audit_date }}
134+
- Fichier de résultats : `rgaa-results.json` (à la racine du repo)
135+
136+
## Instructions
137+
138+
1. **Lis le fichier `rgaa-results.json`** qui contient les résultats axe-core pour chaque page auditée.
139+
140+
2. **Rédige un rapport Markdown** avec cette structure :
141+
142+
```
143+
# Rapport RGAA — Egapro
144+
145+
> Dernier audit : YYYY-MM-DD | Commit : SHA | URL : ... | Pages : N
146+
147+
## Résumé
148+
149+
| Sévérité | Violations |
150+
|----------|-----------|
151+
| 🔴 Critique | N |
152+
| 🟠 Sérieuse | N |
153+
| 🟡 Modérée | N |
154+
| 🔵 Mineure | N |
155+
| **Total** | **N** |
156+
157+
## Actions prioritaires
158+
159+
Liste numérotée des violations les plus impactantes, regroupées quand la même violation apparaît sur plusieurs pages. Pour chaque action :
160+
- Sévérité et règle axe-core
161+
- Pages concernées
162+
- Éléments CSS ciblés
163+
- **Suggestion de correction concrète** en utilisant les composants et classes DSFR (utilise le MCP dsfr pour vérifier)
164+
165+
## Détail par page
166+
167+
### Nom de la page (chemin)
168+
✅ N règles passées | ⚠️ N incomplètes | ❌ N violations
169+
170+
| Règle | Sévérité | WCAG | Élément | Description | Correction suggérée |
171+
|-------|----------|------|---------|-------------|-------------------|
172+
```
173+
174+
3. Pour le mapping WCAG → RGAA, utilise ces correspondances principales :
175+
- WCAG 1.1.1 → RGAA 1 (Images)
176+
- WCAG 1.3.x → RGAA 9 (Structure)
177+
- WCAG 1.4.x → RGAA 3 (Couleurs) et 10 (Présentation)
178+
- WCAG 2.1.x → RGAA 7 (Scripts) et 12 (Navigation)
179+
- WCAG 2.4.x → RGAA 12 (Navigation)
180+
- WCAG 3.1.x → RGAA 8 (Éléments obligatoires)
181+
- WCAG 4.1.x → RGAA 7 (Scripts)
182+
183+
4. **Utilise le MCP `dsfr`** pour chercher les composants et classes appropriés dans tes suggestions de correction.
184+
185+
5. **Publie le rapport sur le wiki GitHub** :
186+
```bash
187+
git config --global user.name "github-actions[bot]"
188+
git config --global user.email "github-actions[bot]@users.noreply.github.com"
189+
git clone "https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.wiki.git" /tmp/wiki
190+
# Écris le rapport dans /tmp/wiki/RGAA-Audit-Report.md
191+
cd /tmp/wiki
192+
git add RGAA-Audit-Report.md
193+
git diff --cached --quiet || git commit -m "Update RGAA audit report — ${{ needs.scan.outputs.audit_date }}"
194+
git push
195+
```
196+
197+
6. Ta réponse finale doit confirmer la publication et inclure un résumé des violations trouvées.
198+
claude_args: >-
199+
--max-turns 15
200+
--mcp-config '{"mcpServers":{"dsfr":{"command":"npx","args":["-y","dsfr-mcp"]}}}'
201+
--allowedTools "Read,Bash(git *),Bash(cat *),mcp__dsfr__list_components,mcp__dsfr__get_component_doc,mcp__dsfr__search_components,mcp__dsfr__search_icons,mcp__dsfr__get_color_tokens"
202+
env:
203+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"db:push": "pnpm --filter app run db:push",
3333
"db:studio": "pnpm --filter app run db:studio",
3434
"test:e2e": "PLAYWRIGHT_BASE_URL=${SITE_URL:-http://localhost:3000} pnpm --filter app run test:e2e",
35+
"test:rgaa": "PLAYWRIGHT_BASE_URL=${SITE_URL:-http://localhost:3000} pnpm --filter app run test:rgaa",
3536
"playwright:install": "pnpm --filter app exec playwright install --with-deps chromium",
3637
"test:lighthouse": "pnpm --filter app run test:lighthouse",
3738
"release": "semantic-release"

packages/app/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"test:coverage": "vitest run --coverage",
2525
"test:e2e": "playwright test",
2626
"test:e2e:ui": "playwright test --ui",
27+
"test:rgaa": "playwright test --config playwright.rgaa.config.ts",
2728
"test:lighthouse": "lhci collect --url=${LIGHTHOUSE_URL:-http://localhost:3000} && lhci upload && lhci assert",
2829
"typecheck": "tsc --noEmit"
2930
},
@@ -48,6 +49,7 @@
4849
"zod": "^4.3.6"
4950
},
5051
"devDependencies": {
52+
"@axe-core/playwright": "^4.11.1",
5153
"@biomejs/biome": "^2.4.2",
5254
"@lhci/cli": "^0.15.1",
5355
"@playwright/test": "^1.58.2",
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { defineConfig, devices } from "@playwright/test";
2+
3+
const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? "http://localhost:3000";
4+
const isRemote = !!process.env.SITE_URL;
5+
6+
export default defineConfig({
7+
testDir: "./src/e2e",
8+
testMatch: "**/rgaa-audit.spec.ts",
9+
fullyParallel: false,
10+
forbidOnly: !!process.env.CI,
11+
retries: 0,
12+
workers: 1,
13+
reporter: [["json", { outputFile: "rgaa-results.json" }], ["list"]],
14+
use: {
15+
baseURL,
16+
trace: "off",
17+
},
18+
projects: [
19+
{
20+
name: "rgaa-audit",
21+
use: { ...devices["Desktop Chrome"] },
22+
},
23+
],
24+
...(isRemote
25+
? {}
26+
: {
27+
webServer: {
28+
command: "pnpm dev",
29+
url: baseURL,
30+
reuseExistingServer: true,
31+
timeout: 120_000,
32+
},
33+
}),
34+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import AxeBuilder from "@axe-core/playwright";
2+
import type { Page } from "@playwright/test";
3+
4+
/** Axe-core violation result for a single page. */
5+
export type PageAuditResult = {
6+
url: string;
7+
label: string;
8+
violations: Array<{
9+
id: string;
10+
impact: string;
11+
description: string;
12+
helpUrl: string;
13+
wcagTags: string[];
14+
nodes: Array<{
15+
target: string[];
16+
failureSummary: string;
17+
}>;
18+
}>;
19+
passesCount: number;
20+
incompleteCount: number;
21+
};
22+
23+
/** Run axe-core WCAG 2.1 AA audit on the current page. */
24+
export async function scanPage(
25+
page: Page,
26+
label: string,
27+
): Promise<PageAuditResult> {
28+
const results = await new AxeBuilder({ page })
29+
.withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "best-practice"])
30+
.analyze();
31+
32+
return {
33+
url: page.url(),
34+
label,
35+
violations: results.violations.map((v) => ({
36+
id: v.id,
37+
impact: v.impact ?? "unknown",
38+
description: v.description,
39+
helpUrl: v.helpUrl,
40+
wcagTags: v.tags.filter((t) => t.startsWith("wcag")),
41+
nodes: v.nodes.map((n) => ({
42+
target: n.target.map(String),
43+
failureSummary: n.failureSummary ?? "",
44+
})),
45+
})),
46+
passesCount: results.passes.length,
47+
incompleteCount: results.incomplete.length,
48+
};
49+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { type Page, test } from "@playwright/test";
2+
3+
import { scanPage } from "./helpers/axe-scan";
4+
import { loginWithProConnect } from "./helpers/login";
5+
6+
/** Scan a page with axe-core and attach results to the test report (no assertion). */
7+
async function auditPage(page: Page, path: string, label: string) {
8+
await page.goto(path);
9+
await page.waitForLoadState("networkidle");
10+
const result = await scanPage(page, label);
11+
12+
await test.info().attach("axe-results", {
13+
body: JSON.stringify(result, null, 2),
14+
contentType: "application/json",
15+
});
16+
}
17+
18+
// -- Public pages --
19+
20+
test.describe("RGAA audit — public pages", () => {
21+
test("Accueil (/)", async ({ page }) => {
22+
await auditPage(page, "/", "Accueil");
23+
});
24+
25+
test("Connexion (/login)", async ({ page }) => {
26+
await auditPage(page, "/login", "Connexion");
27+
});
28+
29+
test("404", async ({ page }) => {
30+
await auditPage(page, "/page-inexistante", "404 Not Found");
31+
});
32+
});
33+
34+
// -- Authenticated pages --
35+
36+
test.describe("RGAA audit — authenticated pages", () => {
37+
test.beforeEach(async ({ page }) => {
38+
await loginWithProConnect(page);
39+
});
40+
41+
test("Déclaration — Introduction", async ({ page }) => {
42+
await auditPage(
43+
page,
44+
"/declaration-remuneration",
45+
"Déclaration — Introduction",
46+
);
47+
});
48+
49+
for (const step of [1, 2, 3, 4, 5, 6]) {
50+
test(`Déclaration — Étape ${step}`, async ({ page }) => {
51+
await auditPage(
52+
page,
53+
`/declaration-remuneration/etape/${step}`,
54+
`Déclaration — Étape ${step}`,
55+
);
56+
});
57+
}
58+
59+
test("Mes entreprises", async ({ page }) => {
60+
await auditPage(page, "/mon-espace/mes-entreprises", "Mes entreprises");
61+
});
62+
});

packages/app/vitest.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,15 @@ export default defineConfig({
3636
globals: true,
3737
setupFiles: ["./src/test/setup.ts"],
3838
include: ["src/**/*.{test,spec}.{ts,tsx}"],
39-
exclude: ["src/**/*.e2e.{ts,tsx}", "node_modules", ".next"],
39+
exclude: ["src/**/*.e2e.{ts,tsx}", "src/e2e/**", "node_modules", ".next"],
4040
coverage: {
4141
provider: "v8",
4242
reporter: ["text", "lcov", "html"],
4343
exclude: [
4444
"node_modules/**",
4545
".next/**",
4646
"src/test/**",
47+
"src/e2e/**",
4748
"**/*.config.*",
4849
"**/*.d.ts",
4950
],

0 commit comments

Comments
 (0)