Skip to content

build: track the public API surface with PublicApiAnalyzers - #52

Merged
rubenhensen merged 2 commits into
mainfrom
ci/49-public-api-analyzer
Jul 27, 2026
Merged

build: track the public API surface with PublicApiAnalyzers#52
rubenhensen merged 2 commits into
mainfrom
ci/49-public-api-analyzer

Conversation

@dobby-coder

@dobby-coder dobby-coder Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Adds Microsoft.CodeAnalysis.PublicApiAnalyzers with tracked PublicAPI.Shipped.txt / PublicAPI.Unshipped.txt, so any change to the public surface of E4A.PostGuard lands 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 own RS0016 output and verified against the E4A.PostGuard-v0.5.0 tag.
  • 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 two AdditionalFiles, and WarningsAsErrors for 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 .editorconfig and .globalconfig with dotnet_analyzer_diagnostic.category-ApiDesign.severity = error first. Both escalate RS0016 (reported on a .cs file) but leave RS0017 at warning, because that one is reported against PublicAPI.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 in CLAUDE.md.

Verification

Both enforcement directions were checked by hand, then reverted:

probe result
added a public property not in the API files error RS0016, build failed
made a tracked public property internal error RS0017 on PublicAPI.Shipped.txt:61, build failed
unchanged tree build succeeded, 0 warnings, 0 errors

The tests are a regression check, not a restatement of the analyzer. Removing the AdditionalFiles lines and the WarningsAsErrors entry and then adding a stray public type, which is how this silently stops working, makes EveryExportedType_IsDeclaredInThePublicApiFiles and Csproj_KeepsTheAnalyzerWiredUpAndFailingTheBuild fail. On the committed tree all 87 tests pass.

dotnet pack was run too: the .txt files are AdditionalFiles, 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.0 cannot start there; CI covers both. dotnet build was 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 to PublicAPI.Shipped.txt and the unshipped file emptied back to its #nullable enable header. That step is written down in CLAUDE.md.

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>
@dobby-coder
dobby-coder Bot requested a review from rubenhensen July 27, 2026 13:24
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>

@dobby-coder dobby-coder Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@dobby-coder
dobby-coder Bot marked this pull request as ready for review July 27, 2026 13:43
@rubenhensen
rubenhensen merged commit c7d8d62 into main Jul 27, 2026
3 checks passed
@rubenhensen
rubenhensen deleted the ci/49-public-api-analyzer branch July 27, 2026 14:41
rubenhensen added a commit that referenced this pull request Jul 27, 2026
BREAKING CHANGE: NetworkException.Body was removed in #43 (GHSA info-exposure fix) but landed as a plain fix:, so release-please computed 0.5.1. The public-API tracking added in #52 records the removal as *REMOVED*; this marker corrects the next release to reflect it.

Release-As: 0.6.0
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.

ci: PublicApiAnalyzers with tracked PublicAPI.txt

1 participant