feat(quality): produce tests/axe/report.json so gate-33 (axe-core) can actually run - #148
Merged
Merged
Conversation
gate-33 (axe-core) consumes tests/axe/report.json and fails on serious/
critical violations. Measured 2026-08-03 across 13 repos: that file exists
in NONE of them, and the scripts/run-browser-tests.sh documented as
producing it exists in no app either. gate-33 has therefore never run
anywhere, and every "all gates green" this fleet has produced excluded
accessibility runtime checking outright. v1.1.0 made the skip loud; this
makes the report exist.
The Playwright job is the only job in this file with both a booted
Nextcloud and a browser, so axe runs there and the report is handed to the
hydra-gates job as an artifact. That requires an edge (an artifact cannot
be downloaded before its producer finishes), so hydra-gates now needs
[playwright] with !cancelled() — without !cancelled() the implicit
success() on needs would silently delete the gates job for every repo that
has the gates on and Playwright off. The workflow's own verdict does not
move: `report` already needed both jobs.
Opt-in via enable-axe, same shape and same reasoning as enable-hydra-gates,
default false. Measured: a VANILLA Nextcloud 34 with no app installed
already carries three serious/critical violations on /apps/files/ and
/settings/user, so the first run in any repo will be red and part of that
red is core's, not the app's.
The runner refuses to write a report it has not earned, because gate-33
reports PASS on a file containing exactly `{}` — a report written by a
crashed step would convert today's loud skip into a silent false pass,
which is strictly worse than never having run. Three guards, each shown
able to fire:
1. a positive control on every run — axe is pointed at a deliberate
`button-name` violation first, and the run is abandoned if axe does
not report it;
2. a route answering HTTP >= 400 is a hard failure, not an empty result;
3. zero passes AND zero violations is an absent measurement, not a clean
one.
The gates job re-validates the downloaded file against the provenance the
runner writes (testEngine, routesAnalysed, selfTest.detected) and DELETES
it if it does not hold up, restoring the loud skip rather than inventing a
pass.
Verified end to end before shipping: gate-33 SKIPPED with no report, FAIL
on a report carrying a deliberate button-name/color-contrast pair, PASS
once the violations are removed, and FAIL on 3 real violations found in a
live Nextcloud 34. The extracted-from-YAML copy of the runner was the one
tested, not just the draft. No repo is enabled by this change.
This was referenced Aug 4, 2026
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The measured problem
hydra-gatesgate-33 consumestests/axe/report.jsonand fails onserious/criticalaxe violations. Measured 2026-08-03 across 13 repos: that file exists in no repository, and thescripts/run-browser-tests.shdocumented as producing it exists in no app either. So gate-33 has never run anywhere, and every "all gates green" this fleet has produced excluded accessibility runtime checking — contrast, landmark structure, ARIA validity, live regions — entirely. v1.1.0 made the skip loud. This makes the report exist.What is wired
Where it runs, and why. The
playwrightjob is the only job in this file with both a booted Nextcloud and a browser, so axe runs there.hydra-gateshas neither — it is a checkout, a shell script and python — so the report is handed over as an artifact. An artifact cannot be downloaded before its producer finishes, sohydra-gatesnow declaresneeds: [playwright].!cancelled()on that job'sif:is load-bearing:needs:impliessuccess(), which would have silently deleted the gates job for every repo that has the gates on and Playwright off. The workflow's own verdict does not move —reportalready needed both jobs — only the position of the "Hydra Gates" check in the list.The alternative (booting a second Nextcloud inside the gates job) duplicates ~150 lines for one gate, and the second instance would be a second thing that can differ from the one the specs ran against.
Opt-in, in the same shape as
enable-hydra-gates. Three new inputs, all defaulting off/empty:enable-axefalseaxe-routes""axe-version4.12.1No repo is enabled by this change.
Default-off is not caution for its own sake: measured against a vanilla Nextcloud 34 with no Conduction app installed at all,
/apps/files/and/settings/useralready carry three serious/critical violations (label,aria-input-field-name,aria-prohibited-attr). An app renders inside that shell, so the first run in any repo will be red and part of that red is core's.Why the runner refuses to write a bad report
gate-33 reports PASS on a
tests/axe/report.jsoncontaining exactly{}. Verified. A report written by a crashed step would therefore convert today's loud_skipinto a silent false pass — strictly worse than where we started. So the runner writes nothing unless it earned it, behind three guards, and the gates job re-validates whatever arrives:button-nameviolation. If axe does not report it, axe cannot fail in this environment, so a subsequent emptyviolationsarray would be evidence about the harness. The run is abandoned.Then in the gates job, the downloaded file is checked against the provenance the runner writes —
testEngine.name, non-emptyroutesAnalysedwith 2xx/3xx statuses and non-zero results,selfTest.detected— and deleted if it does not hold up. Deleting it restores the loud skip. It never invents a pass and never invents a failure.Verification — both directions
Everything below was run against the copy extracted back out of this YAML, not the draft.
[gate-33] axe-core: SKIPPED — no tests/axe/report.json …, coverage48 of 63,33named inGATES THAT DID NOT RUNbutton-name, critical) + a 1.6:1 paragraph (color-contrast, serious)[gate-33] axe-core: FAIL — 2 serious/critical axe violation(s), exit code 2[gate-33] axe-core: PASS, coverage49 of 63,33gone from the not-run listFAIL — 3 serious/criticalon real core violations; report 4.5 KB, real rule ids, 21/30/30 passes per route{}Each guard was also shown able to fire, writing no report in every case: self-test defeated (control button given a name) → refuses; route 404 → refuses; zero routes configured → refuses; unreachable host → refuses; a stale report from a previous attempt is removed up front, so "did not write" always means "no report".
The validator's eight branches were each exercised (good report /
{}/ truncated / zero routes / no self-test / route 404 / route with no results / no artifact at all) — every rejection deletes the file, every one prints why.actionlintis clean on the file, and was positive-controlled: breakingsteps.axe.outcometo a nonexistent step makes it exit 1 with that exact complaint, which also confirms the step id resolves. The gates package's own suites still pass (25/25 entry-point, 17 helper suites, 2 pre-existing quarantines).Could not be verified here
This PR's own CI does not execute the new steps.
quality.ymlis a reusable workflow;ConductionNL/.githubdoes not call it on itself, so nothing in this PR's check set boots a Nextcloud or runs axe. The compensating evidence is the live Nextcloud 34 run above, which exercised the same path end to end (login, three routes, real violations, artifact-shaped report). First real CI exercise will be the first repo to setenable-axe: true.Scope
Workflow only. No repo enabled, no suppression baseline added or modified. The two doc touches (
hydra-gates/README.md, the gate-33 comment block inrun-hydra-gates.sh) point at the new input and record the{}-passes hazard; neither changes gate logic.