Skip to content

Port to C# and ship as a native-AOT tool - #28

Merged
Mpdreamz merged 1 commit into
masterfrom
port-csharp-argh-aot
Sep 2, 2026
Merged

Mpdreamz merged 1 commit into
masterfrom
port-csharp-argh-aot

Conversation

@Mpdreamz

@Mpdreamz Mpdreamz commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

release-notes ships as a self-contained native-AOT executable on five platforms, ported from F#/Argu to C#/Nullean.Argh.

Prompt summary: Modernize this repo's build tooling the way curb and mermaider already do — package the tool as native-AOT dotnet tools per-RID with a framework-dependent fallback, moving CLI parsing onto Nullean.Argh where reflection-based parsers block AOT.

Why

Argu's argument parsing leans on reflection over F# union case metadata, which native AOT trims away. A manual AOT publish of the old F# tool confirmed this: every flag came back as unrecognized argument. Porting to C#/Nullean.Argh, a Roslyn source generator with no reflection at parse time, unblocks AOT the same way it did for assembly-differ#41 and nupkg-validator#19.

What

C# port

ReleaseNotesCommands.cs, ReleaseNotesRunner.cs, GithubScanner.cs, Labeler.cs, GitHubItem.cs, and OutputWriter.cs replace their F# counterparts (Program.fs, Arguments.fs, GithubScanner.fs, Labeler.fs, GithubItem.fs, OutputWriter.fs). Each subcommand (generate, apply-labels, find-previous, current-version, create-release) keeps its own explicit command name — MapAndRootAlias isn't used here because --version collides with Argh's own global --version flag and silently prints the CLI version instead of running the command.

Native-AOT packaging

release-notes.csproj gains linux-x64, linux-arm64, win-x64, win-arm64, and osx-arm64 as RuntimeIdentifiers, selected transparently through the root package's DotnetToolSettings.xml (v2), plus a framework-dependent any fallback. PublishAot, PublishSelfContained, and StripSymbols apply only when a specific RID is targeted.

Dependencies

Fake.Core.SemVer is replaced with NuGet.Versioning, and Octokit moves from 0.43.0 to 14.0.0 — both reflection-free and AOT-friendly, where the F# tool's originals were not.

Package staging and signing

generatePackages stages a full dotnet pack and keeps only the root and any packages, since the real per-RID packages come from CI. A signing fix replaces the unsigned obj/ copy of this project's own assembly that dotnet pack puts inside the any package with the correctly signed bin/ one. validatePackages confirms strong-name signing (PublicKeyToken=96c599bbe3e70f5d) on the any package for both TFMs.

CI

.github/workflows/ci.yml adds an aot-pack matrix job building each RID on a matching runner, ahead of a build job that downloads and merges the artifacts before packages get published.

Breaking

Argh's dispatch requires an explicit command name for every invocation — there's no bare release-notes <owner> <repo> ... shorthand for generate anymore:

release-notes generate nullean release-notes --version 1.0.0
release-notes apply-labels nullean release-notes --version 1.0.0
release-notes find-previous nullean release-notes --version 1.0.0
release-notes current-version nullean release-notes --version 1.0.0 --query master
release-notes create-release nullean release-notes --version 1.0.0 --body notes.md

--label moves from two tokens to one combined token (--label bug "Bug Fixes" becomes --label bug=Bug Fixes), and every other flag moves from a single word to kebab-case (--oldversion becomes --old-version, --releaselabel becomes --release-label-format). README updated with the full flag reference and examples for every command.

Verify

./build.sh build generatepackages validatepackages generatereleasenotes -s true
dotnet tool install --add-source build/output release-notes
release-notes generate nullean release-notes --version 1.0.0

Argu's argument parsing leans on reflection over F# union case metadata,
which NativeAOT trims away, so a published AOT build of the old F# tool
silently failed to recognize any of its own arguments. Porting to C# and
Nullean.Argh (a source generator, so no reflection at parse time) unblocks
AOT packaging the same way curb and assembly-differ already ship it: one
native-AOT package per RID plus a framework-dependent 'any' fallback,
selected transparently by the root package's DotnetToolSettings.xml.

SemVer parsing/comparison moves from Fake.Core.SemVer to NuGet.Versioning,
and Octokit is bumped from 0.43.0 to 14.0.0, both reflection-free and
AOT-friendly.

Breaking change: Argh's dispatch model requires an explicit command name
for every invocation (generate/apply-labels/find-previous/current-version/
create-release) - there is no bare `release-notes <owner> <repo>` shorthand
for `generate` anymore. --label also moves from two tokens to one combined
`<label>=<description>` token, and other flags move to kebab-case.

Also fixes dotnet pack silently copying the unsigned obj/ build of this
project's own assembly into the portable 'any' package instead of the
correctly signed bin/ one (same fix as applied to assembly-differ and
nupkg-validator).

Co-authored-by: Cursor <cursoragent@cursor.com>
@Mpdreamz Mpdreamz added the enhancement New feature or request label Sep 2, 2026
@Mpdreamz Mpdreamz changed the title Port from F#/Argu to C#/Nullean.Argh, add native-AOT tool packaging Port to C# and ship as a native-AOT tool Sep 2, 2026
@Mpdreamz
Mpdreamz merged commit e93d1a4 into master Sep 2, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant