Skip to content

Replace SonarCloud with an in-repo zizmor gate - #440

Merged
tylervick merged 3 commits into
mainfrom
remove-sonarcloud
Aug 12, 2026
Merged

tylervick merged 3 commits into
mainfrom
remove-sonarcloud

Conversation

@tylervick

Copy link
Copy Markdown
Member

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

  • Add a zizmor job to lint.yml (pinned to 1.29.0), gated at --min-severity low so the gate starts green, per the same philosophy as the SwiftLint config. The 13 informational findings (template expansions of tag-derived values in release.yml) sit deliberately below the floor.
  • Pin the official actions (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.
  • Inline artipacked ignore on the bump_version checkout — it keeps credentials on purpose because create-pull-request pushes from it (already documented in the build job's comment).
  • Remove the SonarCloud badge from the README.

Verified

  • zizmor --min-severity low .github/workflows/ exits clean in both offline and online (token-fed) modes locally.
  • With no severity floor: 0 high/medium/low, 13 informational, 1 ignored — confirming the inline ignore works and nothing real is being filtered.

Follow-ups (not in this PR)

  • Delete the project on sonarcloud.io to stop Automatic Analysis (account-level action).
  • The template accessibility issues Sonar flagged in 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

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]>
@coderabbitai

coderabbitai Bot commented Aug 12, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

@tylervick, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 13 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d6454605-3d57-4999-9feb-2cd3b9f337b6

📥 Commits

Reviewing files that changed from the base of the PR and between 013ae6c and 12f2fd4.

📒 Files selected for processing (7)
  • .github/workflows/ci.yml
  • .github/workflows/codecov.yml
  • .github/workflows/lint.yml
  • .github/workflows/release.yml
  • .github/workflows/test-artifacts.yml
  • .github/workflows/test.yml
  • README.md

Comment @coderabbitai help to get the list of available commands.

tylervick and others added 2 commits August 12, 2026 15:10
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]>
@tylervick
tylervick merged commit 7b8b93c into main Aug 12, 2026
9 checks passed
@tylervick
tylervick deleted the remove-sonarcloud branch August 12, 2026 22:14
tylervick added a commit that referenced this pull request Aug 13, 2026
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.
tylervick added a commit that referenced this pull request Aug 13, 2026
* 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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant