build: track the public API surface with PublicApiAnalyzers - #52
Conversation
Adds Microsoft.CodeAnalysis.PublicApiAnalyzers with PublicAPI.Shipped.txt and PublicAPI.Unshipped.txt, so any change to the public surface of E4A.PostGuard shows up as an explicit diff in those files instead of slipping into a release. The shipped file is the surface of the released 0.5.0. The one public change made since then, dropping NetworkException.Body (#41), is recorded as a *REMOVED* entry in the unshipped file. The analyzer runs inside dotnet build, so CI needs no new step. Its rules are raised to errors through WarningsAsErrors rather than .editorconfig, because RS0017 is reported against PublicAPI.Shipped.txt and path-based .editorconfig severity does not reach additional files. Closes #49 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two review nits on the tracking tests, both verified by probe: - ShippedFile_TracksNullability only checked PublicAPI.Shipped.txt, but the hazard applies to PublicAPI.Unshipped.txt too, and that is the file contributors edit on every API change. Dropping its #nullable enable header builds green with 0 warnings, so nothing caught it. Now a [Theory] over both files; the unshipped case fails on that probe. - Csproj_KeepsTheAnalyzerWiredUpAndFailingTheBuild matched raw csproj text, so it red-lit on edits that left the wiring intact: reformatting an element to the "<X ... />" form that dotnet format and the IDEs produce, or adding a second WarningsAsErrors element, which made .Single() throw. Parses with XDocument instead. Same guarantee; unwiring AdditionalFiles still fails it. Also drop the one bold span added to CLAUDE.md, which has none elsewhere. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
VERDICT: approve
Rule sweep plus the carried-forward review findings. No blocking issues. Nit-only, and the fixable nits are already fixed on this branch in b2e1dd9, so this is a sign-off rather than a bounce.
Fixed in b2e1dd9
Two brittleness nits in PublicApiTrackingTests.cs, each verified by probe rather than argued:
ShippedFile_TracksNullability asserted the #nullable enable header on the shipped file only, but the hazard its own comment describes applies to PublicAPI.Unshipped.txt too, and that is the file contributors touch on every API change. Deleting line 1 of the unshipped file builds green, 0 warnings, 0 errors, and RS0037 does not fire for it despite being in WarningsAsErrors. Now a [Theory] over both files; the unshipped case fails on that probe.
Csproj_KeepsTheAnalyzerWiredUpAndFailingTheBuild matched raw csproj text, so it red-lit on edits that left the analyzer fully wired. Reformatting <AdditionalFiles Include="PublicAPI.Shipped.txt"/> to the " /> form that dotnet format and the IDEs emit broke the substring assertion, and a second <WarningsAsErrors> element in a TFM-conditional PropertyGroup made .Single() throw a bare InvalidOperationException. It parses with XDocument now. Both edits pass; deleting the AdditionalFiles lines still fails it.
88 tests pass on net10.0, dotnet build clean on both TFMs, CI green on this HEAD.
Also dropped one bold span from the CLAUDE.md addition and two from the PR body. CLAUDE.md on main has zero bold and zero em dashes, so those were newly introduced rather than house style.
Left for you
One inline note below on the semver consequence of the *REMOVED* entry. It is a release decision, not a change to this PR.
Rule sweep
Checked against the applicable rules for a .NET build/test diff: conventional-commit PR title (pr-title.yml present, check green), closing keyword (closingIssuesReferences resolves to #49), reviewer assigned, cross-repo link format, writing rules, workflows permission (no workflow files touched, correctly), test coverage of the change, and the CRLF/.gitattributes trap. The last one does not apply: the tests read lines and substrings, not byte-exact hashes, and File.ReadAllLines splits on \r\n and \n alike. Everything else clean apart from the bold count noted above.
| @@ -0,0 +1,2 @@ | |||
| #nullable enable | |||
| *REMOVED*E4A.PostGuard.Exceptions.NetworkException.Body.get -> string! | |||
There was a problem hiding this comment.
Not introduced by this PR, and no change needed here, but this file is what makes it visible.
The *REMOVED* entry formally records that the pending release drops a public property. Release PR #38 is open and cuts E4A.PostGuard 0.5.1, a patch bump, because #43 landed as fix:. Publishing a patch that removes a public member breaks consumers on dotnet restore with nothing in the version number to signal it.
Worth deciding before #38 merges: re-cut as 0.6.0, or accept it as pre-1.0 latitude.
Adds
Microsoft.CodeAnalysis.PublicApiAnalyzerswith trackedPublicAPI.Shipped.txt/PublicAPI.Unshipped.txt, so any change to the public surface ofE4A.PostGuardlands as an explicit diff in those files instead of slipping into a release unnoticed.Closes #49
Part of encryption4all/postguard#247 (workstream D).
What is in here
src/PublicAPI.Shipped.txt: the 100 public entries of the released 0.5.0, generated from the analyzer's ownRS0016output and verified against theE4A.PostGuard-v0.5.0tag.src/PublicAPI.Unshipped.txt: one entry,*REMOVED*E4A.PostGuard.Exceptions.NetworkException.Body.get -> string!. That property was dropped after 0.5.0 in fix: stop leaking raw upstream body in NetworkException (#41) #43, so the pending release is a breaking change and the file now says so.src/E4A.PostGuard.csproj: the analyzer package reference plus the twoAdditionalFiles, andWarningsAsErrorsfor the tracking rules.tests/E4A.PostGuard.Tests/PublicApiTrackingTests.cs: three tests covering what the analyzer cannot cover itself.CLAUDE.md: how to update the files, and the release-time promotion step.No workflow changes: the analyzer runs inside
dotnet build, which CI already does.Two decisions to check
Severity is set in the csproj, not
.editorconfig. I tried.editorconfigand.globalconfigwithdotnet_analyzer_diagnostic.category-ApiDesign.severity = errorfirst. Both escalateRS0016(reported on a.csfile) but leaveRS0017at warning, because that one is reported againstPublicAPI.Shipped.txt, and path-based editorconfig severity does not reach additional files. A removed public member would then have gone green.<WarningsAsErrors>applies regardless of where the diagnostic sits, so that is what the PR uses. Rules listed: RS0016, RS0017, RS0024, RS0025, RS0036, RS0037, RS0041, RS0048, RS0050. The overload-design rules (RS0022, RS0026, RS0027) are left at their default warning level, since they are API advice rather than part of the tracking contract.One pair of files covers both TFMs.
src/has no#if, and I generated the surface separately for net8.0 and net10.0 and diffed them: identical, 99 entries each. If a member ever becomes TFM-conditional the files have to be split per TFM. That is noted inCLAUDE.md.Verification
Both enforcement directions were checked by hand, then reverted:
error RS0016, build failedinternalerror RS0017onPublicAPI.Shipped.txt:61, build failedThe tests are a regression check, not a restatement of the analyzer. Removing the
AdditionalFileslines and theWarningsAsErrorsentry and then adding a stray public type, which is how this silently stops working, makesEveryExportedType_IsDeclaredInThePublicApiFilesandCsproj_KeepsTheAnalyzerWiredUpAndFailingTheBuildfail. On the committed tree all 87 tests pass.dotnet packwas run too: the.txtfiles areAdditionalFiles, so they do not end up in the nupkg.Tests ran on net10.0 only. The workspace has the net10.0 SDK but not the net8.0 runtime, so
dotnet test --framework net8.0cannot start there; CI covers both.dotnet buildwas run for both TFMs.After a release
release-please does not move unshipped entries. When 0.5.1 is cut, the
*REMOVED*line has to be applied toPublicAPI.Shipped.txtand the unshipped file emptied back to its#nullable enableheader. That step is written down inCLAUDE.md.