v0.2.0 — installs and actually works
First release since the dashboard was audited end to end. v0.1.0 installed and ran; this one is the first that installs and actually works — four separate wires were connected to the wrong terminal, and every one of them failed silently.
⚠️ Breaking changes
Read these before upgrading.
| Change | Action |
|---|---|
GET /api/github/org-repos?org=&token= is now POST with a JSON body |
The PAT was landing in access logs and browser history. Update any caller; the bundled UI is already updated. |
helm install now fails if runner.enabled: true |
It could never work — the image ships neither pipeline_runner.py nor the claude CLI, and the runner's cwd doesn't match QF_OUTPUTS_DIR. Remove the value; run pipeline phases from the CLI. |
patch_coverage_pct removed from GET /api/metrics/{project} |
It was declared, never assigned, always null, and read by nothing. The UI's patch % comes from /api/coverage/test/{project}. |
?commit= on the coverage read endpoints rejects non-SHA input with 400 |
Closes an arbitrary-file read (below). Only real commit SHAs were ever valid. |
Security
Unauthenticated arbitrary .yaml read. GET /api/coverage/test/{project}/raw?commit=../../../../../config/routing returned the file's contents. With OIDC unset — the default — this was anonymous, and .yaml on a cluster includes mounted config and any secret projected as YAML. The upload path had always required a strict commit SHA; only the read path was missing it. Both ends now share one gate.
Two related holes closed at the same time: the previous sanitizer mapped separators to _ but passed a bare .. through unchanged, and five call sites sanitized a component segment while leaving project_id raw beside it.
GitHub PAT in a URL. A repo-scoped token travelled in the query string of org-repos, where it was written to the uvicorn access log, the ingress log and browser history. Now a POST body, matching the four other call sites that already did that.
Deployment — four silent failures
- Git sync wrote to a directory nothing reads. It targeted
ROOT/outputswhile every reader usesOUTPUTS(/data/outputson the PVC), so the clone succeeded,_last_syncupdated, the endpoint returnedok, and nothing pulled was ever displayed. It now also carriesconfig/, whichGIT_REPO_URLwas documented as delivering but never did. - The config volume was mounted empty with nothing to seed it, so a fresh install had no
routing.yaml: project resolution silently fell back to a lowercased Jira prefix and/api/projectsreturned[]. Aseed-configinitContainer now seeds it once withcp -Rn, never clobbering later edits. - The chart set a GitHub token variable the main code path never read.
GITHUB_TOKENvsGITHUB_PERSONAL_ACCESS_TOKEN— push-PR, close-PR, org scan and bulk onboard all reported "no token" while other GitHub calls worked, which is why it passed casual testing. - No LLM credential existed anywhere in the chart, so every AI feature returned 503 on a stock install. New
claude:block covers Vertex and the direct API.
Metrics that were telling the wrong story
- The coverage delta subtracted the current value from itself. History is newest-first; the reader took index 0 as the previous entry, so every delta on the dashboard read 0.
- The trend chart plotted the oldest 30 entries backwards —
history[-30:]on a newest-first list. - Phase durations could be negative (
-1.28hagainst real data). They were derived from file mtimes, which don't survive a git sync, container rebuild or volume restore. Now uses timestamps recorded by the run, with mtimes as fallback and non-positive values dropped. - Project coverage was whichever repo the loop visited last; now line-weighted across repos. Go test functions were never counted, so Go-primary projects under-reported "time saved" by their entire Go contribution.
The dashboard looked broken because it was
#metricsHero was laid out with grid-cols-6 — a class absent from the vendored Tailwind build. It fell back to one column and stacked nine hero cards as full-width blocks. The same gap silently killed tracking-wide at all 29 of its uses, so every uppercase label rendered un-spaced, plus a dozen other utilities.
Also fixed: the default landing view hid the sidebar while leaving its tab lit active; six loaders caught errors and hid their section, so a transient 500 rendered as "Coverage isn't configured — edit coverage.yaml" and sent people to fix a correct file; the settings gear was a dead button on failure; a failed feature-toggle save left the switch showing a state the server never accepted; and light mode turned skill cards near-black on hover.
Also in this release
Waves 1–4b (#21, #22, #23, #24, #25) — generated tests now actually reach the pull request, real traceability IDs flow through the pipeline, plus triage, trends, navigation, provenance and a run-history view. A non-blocking AI code reviewer (#9, #19, #20). Full QE-pipeline hardening (#26): injection defense, orphaned gates wired, dead validator rules, and an LLM-fabricated STP checksum replaced with a real shasum.
Upgrading
helm upgrade qualityflow-dashboard deploy/helm/qualityflow-dashboard \
--set image.tag=0.2.0 \
--set auth.apiKey=<your-key> \
--set claude.vertexProjectId=<project> # or --set claude.apiKey=<key>Drop runner.enabled from your values first — the chart now refuses to render with it set.
Known gaps
Two paths this release changes have not been exercised against real infrastructure, because they can't be from a laptop: a live cluster install, and a real git-sync against a remote. Verify both on a pilot cluster before rolling this out widely.
Two mutation-tested self-checks now run in CI (check_value_metrics.py, check_path_safety.py) guarding the specific defects above — every one of them returned a plausible-looking value while broken, so no syntax or lint gate would have caught any of them.