Skip to content

chore: write each package's changelog section into its NuGet release notes - #347

Open
CaffeinatedCoder wants to merge 1 commit into
mainfrom
chore/346-release-notes-from-changelog
Open

CaffeinatedCoder wants to merge 1 commit into
mainfrom
chore/346-release-notes-from-changelog

Conversation

@CaffeinatedCoder

Copy link
Copy Markdown
Owner

Closes #346

Problem

PackageReleaseNotes was a link to blob/main/src/<package>/CHANGELOG.md. nuget.org's Release Notes tab and Visual Studio's Updates tab show that field, frozen per version, so a reader of any version saw a link to today's file instead of the notes for the version in front of them.

How nuget.org renders release notes (measured)

The documentation does not say, so I fetched real package pages and read NuGetGallery's renderer:

  • bUnit 2.12.5, whose nuspec notes are Markdown (### Fixed, - bullets, backticks, [@user](https://github.com/user)), renders on https://www.nuget.org/packages/bunit/2.12.5 as <p>Changes in bUnit 2.12.5<br /><br /><br />### Fixed<br /><br />- \WrapperBase.WrappedElement` … Reported by [@psu-prog](https://github.com/psu-prog) …

    . Headings, bullets, backticks and link brackets appear exactly as typed. Only the bare URL is linked, and the link stops before the closing )`.
  • Bogus 35.6.5 (## v35.6.5, * PR 622: …) renders the same way: ## and * shown literally, newlines turned into <br />.
  • Polly 8.8.0 (See https://… for details): the bare URL becomes <a href="…" rel="nofollow">.
  • The source, NuGetGallery/Helpers/HtmlExtensions.cs PreFormattedText (used by Views/Packages/DisplayPackage.cshtml for Model.ReleaseNotes), HTML-encodes the text, turns \n into <br />, turns runs of spaces into &nbsp;, and links only matches of ((http|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&amp;:/~\+#]*[\w\-\@?^=%&amp;/~\+#])?). # fragments are part of a link. (, ) and a trailing . or , are not.

So release notes on nuget.org are plain text with auto-linked bare URLs. The chosen form: a link [text](url) is written text (url) (the URL still links, and the ) stays outside it), backticks and ** are dropped, and a ### Added heading becomes Added. The - bullets and blank lines stay, since they read as plain text. I could not measure Visual Studio's Updates tab here. Plain text is the safe form there too.

Change

  • Directory.Build.targets (new): the target SetPackageReleaseNotesFromChangelog runs BeforeTargets="GenerateNuspec" on every packable project. An inline task (RoslynCodeTaskFactory) reads the section under ## [$(Version)], previews included, up to the next ##, from $(MSBuildProjectDirectory)/CHANGELOG.md. It trims the blank lines around the section, writes it in the plain form above, and adds one footer line: Full changelog: https://github.com/CaffeinatedCoder/CodoMetis.ValueRanges/blob/v$(Version)/src/<package>/CHANGELOG.md. The footer links the version's tag, not main: release tags are immutable and are pushed before release.yml publishes, so the link resolves for every published version on either line and shows the changelog as it was at that version. The changelog is added to NuGetPackInput, so an edit to the changelog alone repacks (checked: an edited section shows up in the next dotnet pack --no-build).
  • Fail closed: pack fails with an error naming the file when the section is missing, empty or doubled, or when the notes exceed nuget.org's 35,000 characters. "No change in this package." is a valid section.
  • Directory.Build.props: the link is gone. Nothing sets PackageReleaseNotes statically any more.
  • PackagingConventionTests: EveryPackage_LinksItsReleaseNotesToItsOwnChangelog is replaced by two tests:
    • EveryPackage_ShipsItsVersionsChangelogSectionAsReleaseNotes runs NuGet's own pack up to the nuspec for every package (dotnet msbuild -t:_GetRestoreProjectStyle;GenerateNuspec -p:NoBuild=true -p:IncludeBuildOutput=false, about 0.3 s each). It reads releaseNotes from the generated nuspec and compares it with the section, transformed by the test's own code rather than the target's. It also checks that no ](, backtick, ** or # heading is left.
    • Pack_FailsNamingTheChangelog_WhenTheSectionCannotBeTheReleaseNotes covers four rows (missing, empty, doubled, too long). Each points the target at a temporary changelog through -p:ReleaseNotesChangelog=… and requires a failed pack whose error names that file.
  • test/consumer-smoke-test.sh: before the consumers, the script reads each .nupkg's nuspec. The release notes must open with the plain form of the section's first line, end with the tag-pinned footer, and hold no backtick or ](. In the release workflow's mode, those are the packages about to be pushed.
  • Docs: AGENTS.md (the packaging rule under Commands, plus the conventions and smoke-test bullets), CONTRIBUTING.md#changelog (a package's section is its NuGet release notes; Packaging added to the area order) and docs/testing.md.
  • Changelogs: a Packaging: line under ### Changed in ## [11.0.0-preview.1] of the root and all four package changelogs.
  • Oracles: no claim in test/oracles/claims.json or waiver in surface-waivers.json covers release notes or packaging metadata, so nothing changes there (dotnet run tools/oracles.cs status lists none).

Tests

Unit and convention suites as CI runs them, locally on SDK 11.0.100-rc.2: 2,512 passed, 0 failed (Conventions 411, core 1,397, EF 291, NodaTime 238, AspNetCore 92, EF NodaTime 83). The smoke test's new block ran locally against a fresh dotnet pack feed (16 ok); the full smoke test (AOT and the consumers) runs in CI.

Each guard proven by seeding (apply, run, git reset --hard):

Seed Fails
A. The old wiring back: the link in props, no targets file EveryPackage_… ("does not carry the '## [11.0.0-preview.1]' section", 133 characters where 885 were expected), all four Pack_Fails… rows ("Pack succeeded…"), and the smoke check: all 8 "opens with" and "ends with" assertions
B. Wrong section: the target reads 10.1.0's EveryPackage_… (885 characters expected, 385 found, first difference at index 0), and the smoke check's 4 "opens with its '## [11.0.0-preview.1]' section"
C. The section missing from src/CodoMetis.ValueRanges.NodaTime/CHANGELOG.md EveryPackage_…: "Packing CodoMetis.ValueRanges.NodaTime's nuspec failed: … CHANGELOG.md has no '## [11.0.0-preview.1]' section"
D. Backticks kept EveryPackage_… (first difference at index 156), and the smoke check's 4 "hold no Markdown backticks"
E. A missing section does not fail the pack Pack_Fails…("missing"): "Pack succeeded with a changelog whose section is missing"
F. The 35,000 limit dropped Pack_Fails…("too long")
G. Footer links blob/main EveryPackage_… (first difference at index 802, the footer)
H. The target runs after the nuspec (AfterTargets) EveryPackage_…: "nuspec has no release notes; nothing set PackageReleaseNotes before it was written". The first run hit a Single() exception, so I added the null assertion that now reports it

The cherry-pick to release/10.x follows in its own PR, which links this one.

🤖 Generated with Claude Code

…notes

PackageReleaseNotes linked blob/main/src/<package>/CHANGELOG.md, so nuget.org's
Release Notes tab and Visual Studio's Updates tab sent a reader of any version
to today's file. Pack now writes the version's section of the package's own
changelog (Directory.Build.targets, before GenerateNuspec), in plain text
because nuget.org renders release notes as typed and links only bare URLs,
with a footer linking the changelog at the version's tag. Pack fails naming
the file on a missing, empty, doubled or over-35,000-character section.

PackagingConventionTests generates every package's nuspec through NuGet's
pack task and compares releaseNotes with the section; the smoke test checks
the .nupkg itself.

Refs #346

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@CaffeinatedCoder CaffeinatedCoder added this to the 11.0.0 milestone Oct 20, 2026
@CaffeinatedCoder CaffeinatedCoder added chore Build, CI, packaging, refactoring with no behaviour change package/nodatime Affects CodoMetis.ValueRanges.NodaTime package/efcore Affects CodoMetis.ValueRanges.EFCore.PostgreSQL area/release Versioning, changelogs, release wiring package/core Affects CodoMetis.ValueRanges package/efcore-nodatime Affects CodoMetis.ValueRanges.EFCore.PostgreSQL.NodaTime labels Oct 20, 2026
@CaffeinatedCoder

Copy link
Copy Markdown
Owner Author

10.x cherry-pick: #348

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/release Versioning, changelogs, release wiring chore Build, CI, packaging, refactoring with no behaviour change package/core Affects CodoMetis.ValueRanges package/efcore Affects CodoMetis.ValueRanges.EFCore.PostgreSQL package/efcore-nodatime Affects CodoMetis.ValueRanges.EFCore.PostgreSQL.NodaTime package/nodatime Affects CodoMetis.ValueRanges.NodaTime

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Packages: show the version's changelog section as NuGet release notes

1 participant