Accessibility regression gating
This document is the operator-facing spec for the a11y regression gate on Vetrix. It covers three moving parts:
- The branch-protection configuration that makes a11y jobs required
checks on
developandmaster. - The Playwright a11y suite gate that is required on MRs touching
web/. - The nightly full-route axe run against
gitvetrix.testand the auto-filed bug-ticket path when it fails.
The plan it implements is plans/accessibility.md, §6 (verification
checklist) and the "How to read this plan" preamble that lists
axe / pa11y and keyboard-only verification as continuous-run
expectations rather than one-shot checks.
Dependency note. The required-check contexts
ci/a11y/axe-pa11yandci/a11y/playwrightare produced by the axe/pa11y gate job and the Playwright a11y job respectively. Until both jobs are reporting successfully on the protected branch, those contexts will be "never reported"; an MR adding this scaffolding must NOT be followed by a branch-protection apply until at least the axe/pa11y gate is green — otherwise no MR can merge. See §5 "Rollout" below.
1. Required checks on protected branches (AC 1)
The canonical configuration lives at
deployments/branch-protection/vetrix-a11y.yml. Summary:
| Branch | Required reviewers | Required status checks | Restrict pushes |
|---|---|---|---|
develop |
1 | ci/test, ci/web-lint, ci/a11y/axe-pa11y, ci/a11y/playwright |
no |
master |
2 | ci/test, ci/web-lint, ci/a11y/axe-pa11y, ci/a11y/playwright |
yes |
Apply with:
ISSUE_TRACKER_USER=vetrix-admin \
ISSUE_TRACKER_PASS=**** \
./scripts/a11y/apply-branch-protection.sh
The apply script is idempotent: it diffs desired state against
GET /branches/protection and PUTs anything that drifted. Rerunning
is safe.
A companion lint — ./scripts/a11y/lint-branch-protection.sh — walks
the YAML and verifies every require_status_checks entry has an
emitting job somewhere in the repo (vetrix-ci.yml + automations).
Vetrix's API silently accepts unknown contexts, so the lint is the
canonical check for typo drift.
2. Playwright a11y suite — required on web/ MRs
The Playwright a11y suite lives at
web/playwright.config.ts (project a11y). Its CI counterpart runs
inside the .vetrix/automations/web-ci.yml workflow. The job
MUST emit the ci/a11y/playwright context for every MR, including
MRs whose diff does not touch web/ — otherwise Vetrix's required-
check evaluator treats the skip as a missing check and refuses to
merge.
The workflow therefore declares two sibling jobs:
jobs:
playwright-a11y:
runs-on: linux
if: ${{ contains(vetrix.changed_paths, 'web/') }}
# real run
playwright-a11y-skip:
runs-on: linux
if: ${{ !contains(vetrix.changed_paths, 'web/') }}
steps:
- name: Finalise context
# Emits the same status context with conclusion=success so the
# branch-protection evaluator treats the MR as having satisfied
# the check.
run: echo "ci/a11y/playwright skipped for non-web MR — reporting success"
Both jobs report the same context name; only one runs on any given MR.
3. Nightly full-route axe run (AC 2)
3.1 Schedule + trigger
- Workflow:
.vetrix/automations/a11y-nightly.yml - Cron:
0 3 * * *(03:00 UTC daily). Spaced from mirror/backup schedulers by a safe margin; tight enough to catch same-day content regressions. - Manual re-run:
workflow_dispatch— the "Run workflow" button on the automation's UI page. An optionaltarget_urlinput points the runner at a release-candidate deployment instead of the default staging host.
3.2 What the runner does
- Installs
@axe-core/cli,pa11y, and Playwright with Chromium. - Reads the authoritative route list from
web/a11y-routes.json(authenticated + unauthenticated routes). - For each route, requests the page via a headless browser, runs
axe-core, records any
seriousorcriticalviolation. - Writes
web/reports/a11y-nightly.jsonandweb/reports/a11y-nightly.xml(JUnit for the artifacts UI). - Exits non-zero if any serious/critical violation is present.
3.3 What happens on failure
The workflow's last step runs scripts/a11y/file-bug-ticket.sh when
the axe step failed. The script:
- Authenticates to
https://api.gitvetrix.comwith the repository-scopedISSUE_TRACKER_USER/ISSUE_TRACKER_PASSsecrets. - POSTs an issue titled
[a11y][nightly] Regression on https://www.gitvetrix.test/ (YYYY-MM-DD)againstrts2271/Vetrixcontaining the JSON violation summary, a link back to the failing run, and a pointer to §4 of this doc. - Surfaces the full API response body on any non-2xx — both the login call and the issue POST. This is required: a silently-successful bug-ticket creator hides regressions worse than the axe failure itself.
3.4 Configuring the secrets
In the repo that owns the upstream mirror, add two CI secrets:
ISSUE_TRACKER_USER— Vetrix service account username (must haverepo:issues:writeon the target repo).ISSUE_TRACKER_PASS— its password. Password-login is used (not a token) so the same credential pair works against bothgitvetrix.comandgitvetrix.testwithout per-environment token rotation.
These secrets are wired into the workflow via
secrets.ISSUE_TRACKER_USER / secrets.ISSUE_TRACKER_PASS — they
never touch the nightly axe step itself, only the bug-ticket step.
4. Triage runbook
When an auto-filed ticket appears:
- Open the pipeline run (link in the ticket body).
- Inspect
a11y-nightly-reportartifact →a11y-nightly.json. - Re-run the workflow once via
workflow_dispatchto confirm the regression is reproducible, not a network/content flake. - If reproducible:
- File a child task against the owning component (UI primitive,
shared shell, or page) and set its
parent_numberviaPUT /issues/{n}/parent(PATCH is a silent no-op on this endpoint). - Link the root-cause fix MR to the ticket.
- File a child task against the owning component (UI primitive,
shared shell, or page) and set its
- If the regression is a known-flake class (third-party embed,
iframe from a vendor whose markup we don't control), add the rule
id + selector to
web/a11y-routes.json#ignoreand close the ticketwontfix. Never close silently without updating the allow-list — the next nightly will refile the same ticket.
Closing the ticket uses
POST /api/v1/repos/{owner}/{repo}/issues/{n}/close (not PATCH).
5. Rollout
Ordered so a failure at any step is recoverable:
- Land the axe/pa11y gate workflow so
ci/a11y/axe-pa11ystarts reporting on the protected branch. Verify the context name matchesdeployments/branch-protection/vetrix-a11y.ymlexactly. - Land the Playwright a11y suite so
ci/a11y/playwrightreports (including the non-web-MR skip-finaliser described in §2). - Land the regression-gating scaffolding — the nightly cron, bug-ticket script, branch-protection YAML, and lint script. No required checks change yet because the config hasn't been applied.
- Apply the branch-protection config. Run
scripts/a11y/apply-branch-protection.shagainstapi.gitvetrix.com. This is the point where merges begin requiring the new contexts. - Verify. Open a throwaway MR touching README; confirm all four contexts report and merge is gated accordingly.
If step 4 happens before step 1 or 2, no MR can merge to the protected branch — fix by temporarily removing the offending context from the live protection rule via the API, land the dependency, then reapply.
6. Related artifacts
.vetrix/automations/a11y-nightly.yml— the nightly workflow.scripts/a11y/file-bug-ticket.sh— bug-ticket helper.scripts/a11y/apply-branch-protection.sh— apply the YAML.scripts/a11y/lint-branch-protection.sh— catch context-name drift.deployments/branch-protection/vetrix-a11y.yml— source of truth.../../user-docs/protected-branches.md— Vetrix API reference for branch rules.plans/accessibility.md— the plan this gating enforces.