Replace SonarCloud with an in-repo zizmor gate - #440
Conversation
SonarCloud had degraded to an untriaged dashboard: a red quality gate nobody was watching, a violations badge advertising it, and no PR integration. Its only findings that SwiftLint/SwiftFormat/ShellCheck could not have made were about the workflows themselves, so cover that gap in-repo instead: - Add a zizmor job to lint.yml, gated at low severity and above so it starts green; informational findings (tag-derived template expansions in release.yml) are deliberately below the floor. - Pin the official actions (checkout, upload-artifact, download-artifact) to commit SHAs, which was SonarCloud's one open gate-worthy finding and is required by zizmor's blanket pinning policy. - Drop the SonarCloud badge from the README. The bump_version checkout keeps its credentials on purpose, so it carries an inline artipacked ignore rather than a fix. Co-Authored-By: Claude Fable 5 <[email protected]>
|
Warning Review limit reached
Next review available in: 13 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
Comment |
The fixture-cache step merged in #436 landed after this branch was cut and tripped the new zizmor gate on the PR's merge commit. Co-Authored-By: Claude Fable 5 <[email protected]>
GATING_IMPACTS = [] (correctly, per #440) makes the assertion expect([]).toEqual([]) — it cannot fail regardless of what axe finds, but the old name ("report has no critical or serious accessibility violations") claimed a guarantee the test does not provide. Renamed to describe the gate's actual state and expanded the comment to record all six known findings (image-alt critical x6, frame-title serious, heading-order/landmark-one-main/region moderate, empty-heading minor) so the debt is legible without re-running the suite.
* Design: test coverage for the rendered report Nothing here has ever asserted anything about how the report looks. The three existing comparisons -- DifferentialTests, ReproducibilityTests, BaselineCaptureTests -- all compare one render to another render, and none of them knows what any of it means. A token that resolves to nothing, a dark-mode palette that fails contrast, a filter tab that stops filtering: all pass silently. That became load-bearing when #439 started redesigning the UI. #455 tokenized the stylesheet and claimed "zero visual change" with no test able to confirm it; #456 adds dark mode and asserts WCAG floors. Those are claims a browser checks mechanically and a reviewer cannot. Three layers over one synthetic ParsedResult fixture: unit tests on model logic, per-template HTML goldens, and Playwright for the facts only a browser knows. Layers 1 and 2 add no toolchain and need neither simulator nor .xcresult. The design turns on a constraint BaselineCaptureTests already documents: fixtures regenerate, so a golden keyed to one cannot be checked in. The way out is to stop feeding renders from generated fixtures -- hence the synthetic fixture, and hence the one production change, a Summary initialiser taking pre-parsed runs. Also records a defect found while writing this: TestScreenshotFlow discards its tailCount parameter and hardcodes suffix(3). Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> * Plan: report visual test coverage, 13 tasks in three phases Phase 1 builds the Swift foundation -- a StubPayloadProvider, a synthetic ParsedResult covering every rendered status, and a Summary initialiser taking pre-parsed runs. Phase 2 adds golden snapshots keyed to that fixture rather than to a generated .xcresult, which is what makes them committable at all. Phase 3 adds the browser layer. Task 1 opens RED on the tailCount defect the spec recorded. Task 12 hands the finished suite to #456 and unskips the dark-mode assertion there, which is the sequencing argument's payoff: the first claim about how this report looks that a machine, not a reviewer, checks. Three assertions in the plan are verified by deliberately breaking something and confirming the failure -- an undeclared token reference, a contrast floor, and a changed golden -- because an assertion that has never failed is one nobody should trust. Selectors are taken from HTMLTemplates.swift rather than guessed: `.selected` is the live selection class, group rows sit under `.run.active`, and the filter tabs are `<li>` elements carrying counts. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> * TestScreenshotFlow honours tailCount instead of hardcoding 3 * Synthetic ParsedResult fixture covering every rendered state * Fix PayloadProviding member count in the visual-test-coverage design spec The spec called PayloadProviding a three-member protocol, but it declares five: url, exportPayload, exportPayloadData, exportLogs, exportLogsData. Task 2's SyntheticResultTests fixture is what surfaced the discrepancy. * Deduplicate SyntheticResultTests' ParsedTestCase extraction, widen filename assertion Review fix round 1: the three tests repeated the same 11-line runs→testables→groups→ParsedNode extraction chain verbatim. Extracted a private allTestCases() helper. Also widened testCoversHostileAttachmentFilename to require all five hostile characters the fixture filename actually carries ("'<>&), not just two of them, so the assertion pins what the fixture provides. * Summary initialiser taking pre-parsed runs, for fixture-free rendering Adds an internal Summary.init(parsedRuns:payloads:renderingMode: downsizeImagesEnabled:downsizeScaleFactor:faultCollector:bundleNames:), alongside the existing public path-based initialiser, so tests can render a full report page from SyntheticResult's fixture with no .xcresult and no simulator. This is the injection point #391 made possible: ParsedRun is now the boundary between reading and rendering, so tests can construct one directly instead of reading it out of a bundle. Internal, not public, because PayloadProviding is internal; @testable import reaches it from tests while library consumers keep the resultPaths-based initialiser untouched. SummarySeamTests exercises the new seam: a full page renders (<!doctype html>, the fixture's SyntheticSuite group, the :root token layer), the complete synthetic tree produces zero faults, and two renders of the same fixture are byte-identical. * Give the synthetic fixture a real log reference, so Run.init? stops degrading SyntheticResult.parsedRun passed logReference: nil, which sent Run.init? down the else branch: a warning to the console, and logContent = .none — no logs section at all in the rendered page. Every golden HTML file from Task 4 onward would have pinned a report missing that pane, and StubPayloadProvider.exportLogs/exportLogsData would have stayed dead code no test ever reached. Fixing this now, before any goldens exist, is the cheapest point to do it. StubPayloadProvider gains a logText constant and real exportLogs/ exportLogsData implementations that mirror exportPayload/exportPayloadData: resolve from the same exports map, touch no filesystem. SyntheticResult registers a logReference in payloads and wires it into parsedRun. SummarySeamTests.testRendersAFullPageWithoutAnXcresult now asserts the logs iframe doesn't degrade to an empty src. A new test, testInlineRenderingEmbedsTheActualLogBytes, proves the fixture's log bytes reach the rendered page for real: under .inline rendering the iframe src is a data: URI, so the exact base64 encoding of logText is a verifiable substring of the output. .linking mode (used elsewhere in this file) only yields a content-free relative file name, so it cannot by itself prove real bytes made the trip — the .inline mode assertion is required to confirm the fix's actual purpose, not just that the warning went away. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> * Snapshot harness and the index golden, keyed to the synthetic fixture * Inline-mode golden alongside the linking-mode one * Correct the no-fixtures verification claim: park only the .xcresult bundles Parking the entire Resources directory also removes differential-allowlist.json, the fourth Package.swift-declared resource. With zero resolvable resources SwiftPM synthesizes no Bundle.module, breaking the whole test target's build — a packaging artifact, not evidence the synthetic-fixture suites need .xcresult fixtures. Parking only the three .xcresult bundles isolates the actual claim. * Dump the synthetic render for the browser suite * Playwright scaffold and token-resolution assertions Introduces the browser test layer: a pinned Playwright + axe-core devDependency set, chromium-only config with retries disabled, and two assertions over the report's computed CSS custom properties — every declared :root token resolves to a non-empty value, and no rule references a token that was never declared. Verified the second assertion actually bites: temporarily broke a var(--color-text-primary) reference in HTMLTemplates.swift to var(--color-text-nonexistent), re-dumped the fixture, and watched `no rule references an undeclared token` fail naming the bogus token. The template edit was reverted before committing. * Recurse into grouping rules so dark-mode :root is not blind spot Both CSS scanners in tokens.spec.ts filtered on `rule instanceof CSSStyleRule`, which is false for CSSMediaRule — so the @media (prefers-color-scheme: dark) :root block PR #456 added was invisible to both assertions. A stylesheet with a token declared or referenced only inside that block would pass "no rule references an undeclared token" vacuously; the name was inaccurate. Walk CSSGroupingRule.cssRules recursively (covers @media, @supports, @layer alike) in both evaluate bodies. Verified the walk now visits 2 :root rules instead of 1, and that a var() reference broken *inside* the dark block is caught post-fix but was silently missed by the pre-fix scanner against the same fixture. * WCAG contrast and dark-mode assertions, dark mode unskipped (#456 landed) * axe-core runs without gating; all findings filed on #440 * Behavioural assertions for filters and keyboard navigation * Run the browser assertions in CI Adds a two-job workflow: a macOS job dumps the synthetic render fixtures via VisualFixtureDumpTests (no simulator, no .xcresult, no prepareTestResults.sh needed — Package.swift's differential-allowlist.json resource is enough to synthesize Bundle.module), then an Ubuntu job downloads those fixtures and runs the Playwright suite against them. setup-node and download-artifact are pinned to their actual latest releases (v7.0.0 and v8.0.1) rather than the older SHAs from the initial draft; download-artifact's SHA matches the one already pinned in release.yml. * Fix effectiveBackground() to skip masked icon fills The WCAG contrast test's effectiveBackground() walk treated any element's non-transparent computed background-color as a real painted surface. The #439 icon refresh (merged to main via #459, which this branch predates) switched .preview-icon and friends to `mask-image` + `background-color: currentColor` — the standard single-colour tintable-icon technique. That background-color is clipped to the icon's silhouette by the mask; it is never a rectangle behind text. For any such element, currentColor makes the "background" trivially equal the "foreground" by construction, so the walk found a spurious 1.00:1 wherever a masked icon happened to pick up sampleable text. That happened here because of a real (separate, out of scope for this fix) HTML-escaping bug: the synthetic fixture's edge-case filename containing a raw `"` breaks out of `data="[[FILENAME]]"`, leaking markup as text children of the otherwise-empty .preview-icon span and an unrecognized <greaterthan> element the broken parse produces. That leak is real and made both elements eligible for sampling; skipping masked elements in the background walk makes the walk find the same true ancestor surface every other passing text element already clears against. Diagnosed by downloading the exact fixture CI's dump job produced (GH Actions run 31745292239) and reproducing the 1.00:1 locally against that fixture with plain diagnostic instrumentation: this branch's own fixture predates #459's icon refresh entirely, which is why "8/8 green locally" and "2/8 red in CI" were both true — pull_request's default checkout tests the PR merged with current main, not the branch alone. * Fix undeclared-token scan to see shorthand var() references rule.style iterated longhand-by-longhand: Chromium stores a shorthand containing var() (e.g. border: 1px solid var(--color-border-strong)) as a pending-substitution value, so every expanded longhand serialises to "". Scanning rule.style.cssText instead sees the literal var(...) text regardless of shorthand expansion. Confirmed by mutation: before the fix the scan found 28 of 34 var() references; after, all 34. Deleting --color-border-strong from HTMLTemplates.swift's :root (both light and dark blocks) now makes the test fail, naming the token; previously it stayed green. * Fail a snapshot refresh run instead of reporting it green The XCHR_UPDATE_SNAPSHOTS=1 branch wrote the golden and returned, so a refresh run was unconditionally green — writing bytes proves nothing about whether the new content is correct. XCTFail after a successful write so CI can never pass while the env var is set, and a developer refreshing locally is prompted to re-run without it for a real verdict. * Rename a11y test to state what it actually checks GATING_IMPACTS = [] (correctly, per #440) makes the assertion expect([]).toEqual([]) — it cannot fail regardless of what axe finds, but the old name ("report has no critical or serious accessibility violations") claimed a guarantee the test does not provide. Renamed to describe the gate's actual state and expanded the comment to record all six known findings (image-alt critical x6, frame-title serious, heading-order/landmark-one-main/region moderate, empty-heading minor) so the debt is legible without re-running the suite. * Add expected-failure regression test for hostile-filename escaping SyntheticResultTests.testCoversHostileAttachmentFilename asserts the fixture contains a filename with " ' < > & — but nothing asserted the render escapes it, and it does not: HTMLTemplates.swift interpolates the raw filename into onclick="showText('[[SOURCE]]')" unescaped, so the embedded double quote breaks out of the attribute. The committed goldens already contain this broken markup. Adds a regression test wrapped in XCTExpectFailure recording the defect (HTMLTemplates.swift / Attachment.swift, out of scope here) without turning the suite red. The moment the escaping is fixed, XCTExpectFailure flips this into a hard failure — an unexpectedly-passing expected failure — which is the loud signal that the test should be promoted out of the wrapper. * Amend the design spec to match what actually shipped Whole-branch review found five claims in the spec the delivered work does not satisfy. Recorded as a dated Amendments section rather than editing history: fixture attachment coverage (PNG/text only, not video/HTML), behavioural assertions (2 of 3 shipped — no attachment-click-populates- preview test), contrast pairing discovery (sampled from one static DOM state, not the cascade; --color-text-secondary and --color-accent-soft are never exercised as text colour in the fixture's default render), axe moderate/minor reporting (console.log, not $GITHUB_STEP_SUMMARY), and Sequencing (PR #456 merged first and is this branch's merge-base, so the suite did not land before #456 as planned — #456's claims were verified retroactively instead, and confirmed sound). * Document the visual test layer in CONTRIBUTING.md "CI runs exactly these two commands, so a green run locally means a green run on your pull request" stopped being true once the Visual workflow landed. Corrected the claim and documented the two layers CONTRIBUTING.md was silent on: template snapshots (XCHR_UPDATE_SNAPSHOTS=1 to refresh, and why a refresh run always fails) and the Playwright browser layer (dump via XCHR_VISUAL_DIR swift test --filter VisualFixtureDumpTests, then npm ci + npx playwright test in visual/). * Drop the test-fixture default from Summary's seam initialiser bundleNames: [String] = ["Synthetic"] put a test fixture's name into production source as a default value. Made it a required parameter and updated the three callers (SummarySeamTests, TemplateSnapshotTests, VisualFixtureDumpTests) to pass "Synthetic" explicitly — the goldens' <title> is unchanged because the value itself didn't change, only where it's supplied from. Also mirrored the path-based initialiser's identifier shape — .appending("bundle\(index)").appending("action0") — instead of the unrelated run-\(index), so the goldens pin an identifier set a real report could actually produce. This does change the goldens: every derived id/toggle/activities hash shifts with the path string. Regenerated via XCHR_UPDATE_SNAPSHOTS=1 and diffed to confirm every changed line is only a 32-hex-char identifier, nothing else. * Exclude committed Snapshots goldens from the test target's resources swift build --build-tests warned "found 2 file(s) which are unhandled" for Tests/XCTestHTMLReportTests/Snapshots/*.html once those goldens existed: SwiftPM saw them under the target's source directory but they are read directly by path (SnapshotSupport.swift), not vended as Bundle.module resources. exclude: ["Snapshots"] tells SwiftPM to leave the directory alone. * Regenerate goldens against PR #459's icon/structure refresh Rebased onto origin/main (f442e8e), which merged #459 mid-execution of this branch's plan: an icon refresh adding status-glyph and icon tokens (SVG masks replacing base64 PNGs), a responsive @media (max-width: 700px) breakpoint, and type-derived attachment labels. TemplateSnapshotTests correctly caught the drift (Snapshot index/index-inline changed). Refreshed with XCHR_UPDATE_SNAPSHOTS=1 swift test --filter TemplateSnapshotTests (which itself fails on purpose per the FIX-2 change), then re-ran clean and read both goldens in full before committing: well-formed, properly closed, the hostile-filename markup this branch's expected-failure regression test depends on is still present and unescaped. --------- Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
Why
SonarCloud has degraded to an untriaged dashboard: the quality gate went red on Aug 7 with nobody watching, the README badge advertises 78 open violations, and none of it gates PRs. Everything it flags in Swift overlaps with SwiftLint/SwiftFormat, which already run as real PR gates. Its only non-redundant, gate-worthy coverage was of the GitHub Actions workflows themselves — this PR moves that coverage in-repo and drops SonarCloud.
What
lint.yml(pinned to 1.29.0), gated at--min-severity lowso the gate starts green, per the same philosophy as the SwiftLint config. The 13 informational findings (template expansions of tag-derived values inrelease.yml) sit deliberately below the floor.checkout@v7→ v7.0.1,upload-artifact@v7→ v7.0.1,download-artifact@v8→ v8.0.1) to commit SHAs across all workflows. This was SonarCloud's one open gate-worthy finding (githubactions:S7694) and is required by zizmor's blanket pinning policy. All third-party actions were already SHA-pinned.artipackedignore on thebump_versioncheckout — it keeps credentials on purpose because create-pull-request pushes from it (already documented in the build job's comment).Verified
zizmor --min-severity low .github/workflows/exits clean in both offline and online (token-fed) modes locally.Follow-ups (not in this PR)
gif.html/iteration.html/testGroup.html(non-native interactive elements) are real and survive Sonar's removal — worth a separate pass.🤖 Generated with Claude Code