Adopt a C# lint/format/static-analysis stack (the ruff equivalent) and enforce in CI #15
Closed
opened 2026-06-27 00:06:42 +02:00 by timothy
·
7 comments
No Branch/Tag Specified
main
renovate/meziantou.analyzer-3.x
renovate/nuget-sixlabors.imagesharp-vulnerability
913-scripted-playout-401
929-vuln-pkcs-imagesharp-v4
renovate/dotnet-monorepo
release/v26.15.0-notes
renovate/lucene.net
renovate/cliwrap-3.x
issue-806-guard-populations
scratch/767b-poisoned
scratch/767b-control
release/v26.14.0-notes
release/v26.14.0
renovate/sqlitepclraw.bundle_e_sqlite3-3.x
docs/510-skill-logo-bug-policy
fix/510-watermark-resolution-policy
fix/629-verdict-classifier-falseopens
fix/609-decisions-edit-token-scope
issue-135-clear-to-none
release/v26.12.0-notes
fix/409b-lastscan-api-parity
fix/401-updatechannel-mirror-422
fix/327-playlist-rename-validation
fix/410-scancancel-log-level
fix/409-447-librariesscreen-neverscanned
fix/338-zap-exit-code
fix/367-plex-budget-message
fix/310-debom-legacy-cs
ci/604-lane-rebalance
feat/388-design-mirror
feat/247-test-ownership
feat/247-primary-action
feat/357-player-owned-playback
feat/357-jellyfin-plugin-poc
fix/289-mcp-hardening
issue58-mcp
feat/244-channels-extract
ci/auto-bump-prod-compose
feat/multi-rerun-collections-api
feat/collections-api
feat/quick-wins
feat/185-docs-part2
feat/140-collections-screen
feat/146-channel-edit
feat/147-classic-ui-link
issue22-renovate-dashboard
feat/91-cutover
feat/63-composite-create
feat/65-library-browse
feat/85-epg
feat/86-schedule-editor
feat/109-dashboard-data
feat/99-session-tracking
fix/dockerfile-node-tag
feat/59-spa-foundation
docs/59-ui-redesign-brief
feat/102-json-guide
feat/111-schedule-durations
feat/104-artwork-upload
feat/103-media-sources-api
feat/playouts-read-api
feat/108-health-api
feat/105-picker-list-endpoints
issue-97-channel-state-api
issue42-jellyfin-musicvideos
issue46-rest-api-error-contract
dependabot/nuget/ErsatzTV.FFmpeg.Tests/multi-d307a2e06f
qsv-improvements
hdr-vulkan-cuda-test
v26.16.0
v26.15.0
v26.14.0
v26.13.0
v26.12.0
v26.11.0
v26.10.0
v26.9.0
v26.8.0
v26.7.0
blazor-final
v26.6.0
v26.5.0
v26.4.0
v26.3.1
v26.3.0
v26.2.0
v26.1.1
v26.1.0
v25.9.0
v25.8.0
v25.7.1
v25.7.0
v25.6.0
v25.5.0
v25.4.0
v25.3.1
v25.3.0
v25.2.0
v25.1.0
v0.8.8-beta
v0.8.7-beta
v0.8.6-beta
v0.8.5-beta
v0.8.4-beta
v0.8.3-beta
v0.8.2-beta
v0.8.1-beta
v0.8.0-beta
v0.7.9-beta
v0.7.8-beta
v0.7.7-beta
v0.7.6-beta
v0.7.5-beta
v0.7.4-beta
v0.7.3-beta
v0.7.2-beta
v0.7.1-beta
v0.7.0-beta
v0.6.9-beta
v0.6.8-beta
v0.6.7-beta
v0.6.6-beta
v0.6.5-beta
v0.6.4-beta
v0.6.3-beta
v0.6.2-beta
v0.6.1-beta
v0.6.0-beta
v0.5.8-beta
v0.5.7-beta
v0.5.6-beta
v0.5.5-beta
v0.5.4-beta
v0.5.3-beta
v0.5.2-beta
v0.5.1-beta
v0.5.0-beta
v0.4.5-alpha
v0.4.4-alpha
v0.4.3-alpha
v0.4.2-alpha
v0.4.1-alpha
v0.4.0-alpha
v0.3.8-alpha
v0.3.7-alpha
develop
v0.3.6-alpha
v0.3.5-alpha
v0.3.4-alpha
v0.3.3-alpha
v0.3.2-alpha
v0.3.1-alpha
v0.3.0-alpha
v0.2.5-alpha
v0.2.4-alpha
v0.2.3-alpha
v0.2.2-alpha
v0.2.1-alpha
v0.2.0-alpha
v0.1.5-alpha
v0.1.4-alpha
v0.1.3-alpha
v0.1.2-alpha
v0.1.1-alpha
v0.1.0-alpha
v0.0.62-alpha
v0.0.61-alpha
v0.0.60-alpha
v0.0.59-alpha
v0.0.58-alpha
v0.0.57-alpha
v0.0.56-alpha
v0.0.55-alpha
v0.0.54-alpha
v0.0.53-alpha
v0.0.52-alpha
v0.0.51-alpha
v0.0.50-alpha
v0.0.49-prealpha
v0.0.48-prealpha
v0.0.47-prealpha
v0.0.46-prealpha
v0.0.45-prealpha
v0.0.44-prealpha
v0.0.43-prealpha
v0.0.42-prealpha
v0.0.41-prealpha
v0.0.40-prealpha
v0.0.39-prealpha
v0.0.38-prealpha
v0.0.37-prealpha
v0.0.36-prealpha
v0.0.35-prealpha
v0.0.34-prealpha
v0.0.33-prealpha
v0.0.32-prealpha
v0.0.31-prealpha
v0.0.30-prealpha
v0.0.29-prealpha
v0.0.28-prealpha
v0.0.27-prealpha
v0.0.26-prealpha
v0.0.25-prealpha
v0.0.24-prealpha
v0.0.23-prealpha
v0.0.22-prealpha
v0.0.21-prealpha
v0.0.20-prealpha
v0.0.19-prealpha
v0.0.18-prealpha
v0.0.17-prealpha
v0.0.16-prealpha
v0.0.15-prealpha
v0.0.14-prealpha
v0.0.13-prealpha
v0.0.12-prealpha
v0.0.11-prealpha
v0.0.10-prealpha
v0.0.9-prealpha
v0.0.8-prealpha
v0.0.7-prealpha
v0.0.6-prealpha
v0.0.5-prealpha
v0.0.4-prealpha
v0.0.3-prealpha
v0.0.2-prealpha
v0.0.1-prealpha
Labels
Clear labels
ad-hoc
api
bug
ci-cd
content
dependencies
enhancement
frontend
in-progress
jellyfin
parked
priority: high
priority: low
priority: medium
review
security
One-off / ad-hoc work not tracked by a dedicated issue
REST API / HTTP endpoints
Something isn't working
Build, test, deploy pipeline
Channel content / schedules / playlists
Dependency updates (Renovate)
New feature or improvement
ChicoryTV React SPA frontend
Claimed by an active session — do not pick up
Jellyfin tuner / IPTV integration
Excluded from automatic queue pickup; work only when explicitly selected
Adversarial review finding
Security / vulnerability fix
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: timothy/ersatztv#15
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Goal
Stand up the C#/.NET equivalent of Python's
ruff— consistent formatting + static analysis — and actually enforce it in CI. The pieces exist; they're just not turned up or applied uniformly.What's already in place
.editorconfig— the config (formatting + rule severities), analogous toruff.toml.NoWarnforCA1873proves CAxxxx rules run).Microsoft.VisualStudio.Threading.Analyzers— wired inDirectory.Build.targets, but only whenEnableThreadingAnalyzers=true.TreatWarningsAsErrors— on in the app projects (so findings already block their build).What to add / enforce
dotnet format --verify-no-changes(whitespace + style + analyzers) so.editorconfigis enforced, not aspirational. (The Docker build now copies.editorconfigtoo, so local/CI/image agree.) Optionally evaluate CSharpier for opinionated, zero-config formatting.Directory.Build.props(so every project gets them, pinned once — pairs with Central Package Management in #14):<EnableNETAnalyzers>true</EnableNETAnalyzers>, raise<AnalysisMode>(Recommended→All), enable threading analyzers repo-wide..editorconfig.Approach (important)
Introduce incrementally — turning every rule to
errorat once would bury us in a wall of pre-existing findings. Start new rules atsuggestion/warning, fix or baseline, then promote toerror. Keep changes consistent with the established style (the "follow established patterns" principle / contributors guide #10) rather than letting a tool reformat the whole tree at once.Payoff
Style and a broad class of bugs/smells/security issues get caught automatically and uniformly — the regression/consistency net Python gets from ruff. Backlog. Related: #10 (contributors guide), #14 (Central Package Management).
Done-when
latest-Alland the threading analyzer is enabled repo-wide.CI code-quality gate: dotnet format (+ coverage report)to Adopt a C# lint/format/static-analysis stack (the ruff equivalent) and enforce in CIClaiming for a bounded, single increment — this issue stays open as an increments tracker.
Live state of #15's scope:
Directory.Build.targets(StyleCop deliberately omitted, rationale in-file).dotnet format --verify-no-changes, fix-as-you-touch scoped to changed.cs(#311).AnalysisLevel=latest-Recommendedset per project.coverlet.collectoris referenced by the test projects but the CITeststep never passes--collect, and nothing publishes a summary.This increment: wire coverage collection into the CI test step (Cobertura via coverlet) and publish a human-readable summary (no floor yet, per "decide on a floor later"). CI-only, low risk. Will land as a follow-up-friendly PR referencing (not closing) #15.
Increment landed: coverage collection + summary (PR #370, merged
3654fad1)Scope item 4 ("collect with coverlet and publish a summary; decide on a floor later") is done:
Teststep now runs--collect:"XPlat Code Coverage" --settings coverlet.runsettings; a best-effortCoverage summarystep publishes a ReportGenerator summary (TextSummary → log, MarkdownSummaryGithub → job step summary). No floor enforced yet,continue-on-errorso it never blocks a build.coverlet.collectortoErsatzTV.Core.Tests+ErsatzTV.Tests(now uniform across all six*.Tests).Architecture.Testsprocess instruments both EF migration folders (~2.59M generated lines vs ~200k authored), which OOM-killed the shared job (exit 137).coverlet.runsettingsexcludes**/Migrations/*.cs+ generated code → instrumented surface dropped 2,510,185 → 19,963 coverable lines (~126×). Any future coverage tuning must keep that exclusion.docs/ci-cd.md(test job).Still open on this tracker (leaving #15 open):
dotnet formatnormalization (deliberately deferred; fix-as-you-touch via #311 in the meantime).Removing my
in-progressclaim.Claiming #15 for this session. I’ll first reconcile the original checklist against the analyzers, formatting hooks, CI jobs, and coverage support that have landed since the issue was filed, then implement and verify only the remaining enforceable gap(s) in a worktree off
origin/main.Reconciliation found one clean final increment: Sonar
S3981reports the production conditionWorkers.Count >= 0, which is always true. Root cause: the non-empty check used>=instead of>, so scheduled non-forced memory releases always classified the process as busy and never selected the intended aggressive idle-GC branch. Scope is now fixed: change that predicate, add idle/active-worker regression coverage, promoteS3981towarningrepo-wide (there is exactly one current occurrence), and update the static-analysis/formatting docs. Async lifetime findings were reviewed and excluded as behaviorally broader. Once this increment passes full verification and adversarial review, the original four #15 outcomes are complete; full-tree normalization and a coverage floor remain explicitly deferred, so #15 can close rather than remain a perpetual automatic pickup.Scope correction after inspecting the evaluated build configuration: the earlier reconciliation missed an explicit original checklist gap.
EnableThreadingAnalyzersstill defaulted tofalse, andlatest-Recommendedwas duplicated only in six production csproj files. This increment now also moves the analyzer posture toDirectory.Build.props(EnableNETAnalyzers=true,AnalysisLevel=latest-All,EnableThreadingAnalyzers=true) for every project, removes the six narrower duplicates, and verifies the evaluated properties/build. Findings still default tosuggestion; only the independently reviewedS3981promotion blocks CI.Analyzer-baseline validation found and resolved an SDK precedence trap before commit:
latest-Allbuild injected the SDK's exact per-ID severities, which outrank the bulk.editorconfigsuggestion setting, and activated 455 errors in TWAE projects (1,651 analyzer warnings solution-wide).AnalysisLevel=latest-Alland checks in the .NET 10Allinventory as an explicit higher-priority suggestion baseline: 281 SDK IDs, with the SDK's future-ruleCA1516=nonepreserved. Findings remain visible; none are suppressed..editorconfigper-ID promotions override it.S3981is also in the centralWarningsAsErrorslist, so the promotion blocks in test projects as well as TWAE app projects.Evidence so far: evaluated properties are
latest-All/ SDK analyzers enabled / threading analyzers enabled on a test project; the full solution build succeeds with only 20 pre-existing compiler nullability warnings; and a negative-control build that temporarily restoredWorkers.Count >= 0failed exactly onS3981(0 warnings, 1 error). The source fix was immediately restored. Full tests and final adversarial review are still pending.Closed by PR #375 (merged to
mainasdbd4bb43,Fixes #15). Completing the close ritual that was interrupted when the Codex session dropped mid-finish.Merge-gate state at merge (all derived, not asserted):
10ae885b— all 8 checks (Build & test 3,688 pass, Functional E2E, EF migration integrity SQLite+MySql, Formatting, API-docs-in-sync, decisions.md, docs-reminder; image-push skipped as normal for a PR).Review-verdict: MERGEABLE @ 10ae885b— a fresh exact-head adversarial re-review (the fourth pass, after rebasing onto merged PR #374; the reviewer confirmed the base-to-head patch was byte-for-byte identical to the previously-reviewed diff and that #374's IPTV code compiles cleanly under the promoted analyzer posture).## Done-whenboxes ticked.What shipped (per PR #375): SDK analyzers centralized at
latest-All+ threading analyzers enabled repo-wide, a checked-in 281-rule SDK globalconfig baseline (exact match to .NET SDK 10.0.301) kept at suggestion severity so All-only rules stay visible without a wall of errors, SonarS3981promoted to a repo-wide CI blocker, and the real bug it caught —ReleaseMemoryHandler's always-trueWorkers.Count >= 0predicate (which made the aggressive idle-GC branch unreachable) fixed with idle/active regression tests. Docker restore/publish layers now copy the analyzer baseline; the gitignored non-CPM.mcptool is preserved via aManagePackageVersionsCentrallyguard (fixing anNU1015). Static-analysis + formatting docs updated.Deferred (documented in the PR): full-tree formatting normalization (format-as-you-touch stays enforced), a coverage floor (until CI summaries establish a baseline), SDK-globalconfig regeneration on a future .NET SDK major, and the active-process/forced-override memory-handler branches (unchanged, low-severity coverage debt). No local Docker image build claimed (Docker unavailable on the workstation; the Dockerfile now supplies the exact previously-missing build input).
Note: this completes the analyzer-foundation work (#15 increment 1). Housekeeping: the orphaned Codex worktree at
/private/tmp/ersatztv-15(branchfeat/15-analyzer-promotion, now merged + remote-deleted) can be pruned at leisure — left untouched since it's another session's checkout.