Skip to content

Commit bd32c1a

Browse files
authored
Merge pull request #1340 from makeabilitylab/1278-pa11y-ci
Wire Pa11y accessibility sweep into CI (report-only) (#1278 item 6)
2 parents 8459c62 + 4f22c0a commit bd32c1a

3 files changed

Lines changed: 178 additions & 0 deletions

File tree

.github/workflows/test.yml

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,99 @@ jobs:
142142

143143
- name: Run end-to-end tests
144144
run: python manage.py test website.tests.test_member_e2e --settings=makeabilitylab.settings_test --verbosity=2
145+
146+
# Accessibility sweep (Pa11y + Axe, WCAG 2.0 AA). CONTRIBUTING requires Pa11y
147+
# on UI changes but nothing enforced it (#1278 item 6). This wires it in.
148+
#
149+
# Like `test` and `e2e`, it is REPORT-ONLY — it surfaces violations in the run
150+
# Summary but never fails the build (so a pre-existing violation doesn't sit
151+
# red next to every master deploy). Tighten to blocking later once the current
152+
# findings are triaged.
153+
#
154+
# The local `.pa11yci.json` targets the docker-compose host with the
155+
# maintainer's real DB snapshot; CI has no snapshot, so we build a fresh DB
156+
# from models, seed deterministic demo content (seed_demo_projects +
157+
# seed_demo_news), serve it with a native runserver, and scan the localhost
158+
# URLs in `.pa11yci.ci.json`.
159+
a11y:
160+
runs-on: ubuntu-latest
161+
162+
services:
163+
postgres:
164+
image: postgres:16
165+
env:
166+
POSTGRES_DB: makeability
167+
POSTGRES_USER: admin
168+
POSTGRES_PASSWORD: password
169+
ports:
170+
- 5432:5432
171+
options: >-
172+
--health-cmd "pg_isready -U admin -d makeability"
173+
--health-interval 10s
174+
--health-timeout 5s
175+
--health-retries 5
176+
177+
env:
178+
DATABASE_HOST: localhost
179+
DATABASE_PORT: 5432
180+
# DJANGO_ENV=DEBUG -> DEBUG=True, so the dev runserver serves media/static
181+
# and renders real error pages while pa11y scans.
182+
DJANGO_ENV: DEBUG
183+
DJANGO_SETTINGS_MODULE: makeabilitylab.settings_test
184+
185+
steps:
186+
- uses: actions/checkout@v4
187+
188+
# ImageMagick + Ghostscript power the PDF->thumbnail path the demo
189+
# publications hit on save; libpq-dev builds psycopg2.
190+
- name: Install system dependencies
191+
run: |
192+
sudo apt-get update
193+
sudo apt-get install -y --no-install-recommends imagemagick ghostscript libpq-dev
194+
sudo cp imagemagick-policy.xml /etc/ImageMagick-6/policy.xml
195+
196+
- name: Set up Python
197+
uses: actions/setup-python@v5
198+
with:
199+
python-version: "3.13"
200+
cache: pip
201+
202+
- name: Install Python dependencies
203+
run: pip install -r requirements.txt
204+
205+
# Build the website schema from models (migrations are gitignored;
206+
# settings_test sets MIGRATION_MODULES={'website': None}, so --run-syncdb
207+
# creates the tables directly) and seed deterministic demo content.
208+
- name: Build schema and seed demo data
209+
run: |
210+
python manage.py migrate --run-syncdb
211+
python manage.py seed_demo_projects
212+
python manage.py seed_demo_news
213+
214+
- name: Start dev server
215+
run: |
216+
python manage.py runserver 0.0.0.0:8000 --noreload &
217+
echo "Waiting for the server to come up…"
218+
for i in $(seq 1 30); do
219+
if curl -sf -o /dev/null http://localhost:8000/; then
220+
echo "Server is up."; exit 0
221+
fi
222+
sleep 1
223+
done
224+
echo "Server did not start in time"; exit 1
225+
226+
- name: Install pa11y-ci
227+
run: npm install -g pa11y-ci
228+
229+
# Report-only: `|| true` so violations never fail the job. Output is teed
230+
# to the log and a trimmed tail posted to the run Summary.
231+
- name: Run Pa11y accessibility sweep
232+
run: |
233+
pa11y-ci --config .pa11yci.ci.json 2>&1 | tee pa11y-output.txt || true
234+
{
235+
echo "## Accessibility (Pa11y + Axe, WCAG2AA) — report-only"
236+
echo
237+
echo '```'
238+
tail -n 60 pa11y-output.txt
239+
echo '```'
240+
} >> "$GITHUB_STEP_SUMMARY"

.pa11yci.ci.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"defaults": {
3+
"standard": "WCAG2AA",
4+
"runners": ["axe", "htmlcs"],
5+
"timeout": 60000,
6+
"chromeLaunchConfig": {
7+
"args": ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"]
8+
},
9+
"hideElements": "#js-toc",
10+
"ignore": []
11+
},
12+
"_comment": "CI variant of .pa11yci.json (#1278 item 6). The local-dev config targets the docker-compose 'website:8000' host with the maintainer's real DB snapshot (jonfroehlich, sidewalk, ...). CI has no snapshot, so this config targets a native runserver on localhost:8000 seeded with deterministic demo content via `manage.py seed_demo_projects` + `seed_demo_news`. Keep the two URL lists in sync with what those seed commands create.",
13+
"urls": [
14+
"http://localhost:8000/",
15+
"http://localhost:8000/people/",
16+
"http://localhost:8000/publications/",
17+
"http://localhost:8000/projects/",
18+
"http://localhost:8000/news/",
19+
"http://localhost:8000/awards/",
20+
"http://localhost:8000/project/demo-active-tall/",
21+
"http://localhost:8000/member/demolovelace/",
22+
"http://localhost:8000/news/demo-news-one/"
23+
]
24+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""
2+
Local-dev / CI helper: seed a few demo news items so the news listing
3+
(``/news/``) and a news detail page render with real content.
4+
5+
Run inside the website container (or in CI):
6+
7+
python manage.py seed_demo_news
8+
9+
Idempotent — deletes any prior demo news (title starting with "Demo News")
10+
and recreates from scratch, so it's safe to re-run. Pairs with
11+
``seed_demo_projects``: together they populate enough content for the Pa11y
12+
accessibility sweep (#1278 item 6) to scan real pages rather than empty ones.
13+
If demo people exist (from ``seed_demo_projects``), the first one is set as the
14+
author so the byline path renders too.
15+
16+
News.save() derives the slug from the title, so the detail URLs are stable:
17+
"Demo News One" -> /news/demo-news-one/.
18+
19+
This file lives in management/commands/ so Django auto-discovers it, but it's
20+
explicitly a dev/test tool — don't wire it into docker-entrypoint.sh.
21+
"""
22+
23+
from datetime import date
24+
25+
from django.core.management.base import BaseCommand
26+
27+
28+
class Command(BaseCommand):
29+
help = "Seed a few demo news items (for local visual testing and the Pa11y CI sweep)."
30+
31+
_TITLES = ["Demo News One", "Demo News Two", "Demo News Three"]
32+
33+
def handle(self, *args, **opts):
34+
from website.models import News, Person
35+
36+
wiped = News.objects.filter(title__startswith="Demo News").count()
37+
if wiped:
38+
self.stdout.write(self.style.WARNING(f"Removing {wiped} prior demo news item(s)."))
39+
News.objects.filter(title__startswith="Demo News").delete()
40+
41+
# Reuse a demo author if seed_demo_projects has run; otherwise authorless
42+
# (News.author is nullable) — both paths should render.
43+
author = Person.objects.filter(first_name="Demo").order_by("last_name").first()
44+
45+
self.stdout.write(self.style.NOTICE("Creating demo news items…"))
46+
for i, title in enumerate(self._TITLES):
47+
news = News.objects.create(
48+
title=title,
49+
content=(
50+
f"<p>This is demo news item #{i + 1}, created for local visual "
51+
f"testing and the automated accessibility (Pa11y) sweep.</p>"
52+
),
53+
date=date(2024, 1, i + 1),
54+
author=author,
55+
)
56+
self.stdout.write(f" ✓ /news/{news.slug}/")
57+
58+
self.stdout.write(self.style.SUCCESS("Done."))

0 commit comments

Comments
 (0)