Skip to content

Modernize NullFX.CRC targeting, versioning, packaging, and CI - #32

Merged
nullfx merged 12 commits into
mainfrom
modernization-updates
Oct 9, 2026
Merged

nullfx merged 12 commits into
mainfrom
modernization-updates

Conversation

@nullfx

@nullfx nullfx commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

Modernize NullFX.CRC targeting, versioning, packaging, and CI

Summary

This PR modernizes the NullFX.CRC target frameworks, versioning, NuGet packaging, README and CI/CD pipeline. It does not change the library's public API, and it does not publish a package.

Target frameworks

The library now targets:

netstandard2.0;netcoreapp3.1;net40;net45;net46;net47;net48;net6.0;net7.0;net8.0;net10.0

  • Added: net8.0, net10.0
  • Confirmed (already present): netstandard2.0, netcoreapp3.1, net45, net46, net47, net48
  • Kept (already present, not in the requested list): net40, net6.0, net7.0. I kept them because the instructions said not to remove valid targets. net6.0 and net7.0 are out of support (they still build cleanly with the .NET 10 SDK). Dropping them could be a follow-up.
  • Unit tests: net7.0 → net10.0
  • Benchmarks: netstandard2.0;net47;net48;netcoreapp3.1;net6.0;net7.0 → net10.0. The netstandard2.0 Exe target could never run.

Versioning

  • New Directory.Build.props is the single, checked-in source of truth: RELEASE_MAJOR=1, RELEASE_MINOR=1, RELEASE_REVISION=10.
  • RELEASE_VERSION is computed as $(RELEASE_MAJOR).$(RELEASE_MINOR).$(RELEASE_REVISION) unless it is passed explicitly, for example -p:RELEASE_VERSION=0.0.0.
  • Version, PackageVersion, AssemblyVersion (x.y.z.0), FileVersion (x.y.z.0) and InformationalVersion are all derived from it. No version is hard-coded in any .csproj.
  • vars.RELEASE_VERSION is no longer used anywhere. After merge you can delete that repository variable.
  • Why a checked-in file and not repository variables: GITHUB_TOKEN cannot write Actions repository variables, so a workflow cannot safely increment them on its own. If a RELEASE_VARIABLES_TOKEN secret is set (a fine-grained PAT with "Variables: read and write"), the release workflow mirrors RELEASE_MAJOR/MINOR/REVISION into repository variables after each release so they stay visible. The file remains the source of truth.

Packaging / nuspec

  • nuspec decision: kept embedded in the .csproj. dotnet pack handles every TFM, the README, the license expression, SourceLink and the .snupkg natively. A standalone .nuspec would duplicate metadata.
  • Token strategy: all #{RELEASE_VERSION}# placeholders and the cschleiden/replace-tokens step are removed. The version comes only from the MSBuild property.
  • README version strategy: the version was removed from the README, so no file needs a 0.0.0 placeholder or release-time replacement.
  • GeneratePackageOnBuild is now false. Packing is an explicit dotnet pack step, so builds and test runs no longer emit packages as a side effect.
  • ContinuousIntegrationBuild=true is set under GitHub Actions for deterministic, SourceLink-friendly builds.
  • Removed the floating Microsoft.SourceLink.GitHub Version="*" reference. It had started resolving to 10.0.x, which pulls in System.IO.Hashing and caused unsupported-TFM warnings for netcoreapp3.1, net6.0 and net7.0. SourceLink is built into the .NET 8+ SDK, and the packed nuspec still records the repository commit.
  • .github/scripts/verify-package.ps1 checks every packed .nupkg:
    • id and version
    • README.md is packed and referenced
    • MIT license expression
    • lib/<tfm>/NullFX.CRC.dll for every TFM, read from the project
    • no leftover #{…}# tokens
    • no bin/obj, test, benchmark or .snk files
  • No package icon exists in the repo today, so none is verified. Adding one could be a follow-up.

README

  • dotnet add PROJECT package NullFX.CRC --version 1.1.10 → dotnet add package NullFX.CRC
  • Removed the broken SonarCloud quality-gate badge.
  • Replaced the cicd-actions.yml badge with CI (ci.yml) and CodeQL (codeql.yml) badges for nullfx/NullFX.CRC, and added a NuGet version badge.
  • Added the list of supported target frameworks.

CI/CD

ci.yml (replaces cicd-actions.yml)

  • Runs on PRs to main, pushes to main and manual dispatch.
  • Uses windows-latest, which is required for net40–net48.
  • Installs the .NET 8 and .NET 10 SDKs. The .NET 10 SDK builds every TFM, using reference packs for netcoreapp3.1, net6.0, net7.0 and .NET Framework, so a .NET Core 3.1 SDK is not needed.
  • Separate jobs:
    • Build (all target frameworks): builds every TFM, confirms that each TFM produced output, packs and verifies a 0.0.0 inspection package, and uploads the package and binlog.
    • Unit tests (net10.0): TRX results go to the job summary and are uploaded as the test-results artifact.
    • Benchmarks (net10.0, short run): BenchmarkDotNet ShortRun. The GitHub-markdown report goes to the job summary, and the JSON/markdown/CSV results are uploaded as the benchmark-results artifact. ShortRun keeps PR feedback quick, and shared runners can't give stable absolute timings anyway.
  • PR builds always use version 0.0.0. They never change version state and never publish.
  • SonarCloud removed: scanner install, caches, SONAR_TOKEN, the JDK setup that only existed for it, and the csproj SonarQube* markers. The old workflow also published to NuGet on every push to main; that now happens only in release.yml.

codeql.yml (new)

  • Runs on PRs to main, pushes to main, weekly (Monday 06:30 UTC) and manual dispatch.
  • windows-latest, github/codeql-action@v4, languages: csharp, build-mode: manual, security-and-quality queries, category: /language:csharp.
  • Builds the shipped library for all TFMs. Permissions are least-privilege.

release.yml (new)

Runs on push to main (a merged PR) or on workflow_dispatch, which takes a bump input of patch, minor or major.

  1. Reads the semver parts and increments the version just before the release build. Default is patch. Major resets minor and revision; minor resets revision.
  2. Idempotent: if the stored version has no v<ver> or <ver> tag yet (because an earlier run failed after bumping), it releases that version instead of incrementing again. It refuses to bump onto a version that is already tagged.
  3. Commits the bump locally. Then it builds all TFMs, packs and verifies with that exact version, and runs unit tests and short benchmarks.
  4. Pushes the bump to main as release: vX.Y.Z [skip ci]. If the push is rejected, the run stops before anything is published.
  5. Publishes to GitHub Packages, then to nuget.org using Trusted Publishing (see below), and finally creates the vX.Y.Z tag and GitHub release with the .nupkg/.snupkg attached.
  6. Optionally mirrors the version to repository variables (see Versioning).

Secrets are scoped only to the steps that use them. The PR validation workflow never increments the version.

nuget.org Trusted Publishing

No long-lived nuget.org API key is stored. The release job has id-token: write. Right before the push, NuGet/login@v1 exchanges the job's GitHub OIDC token for a short-lived (about 1 hour) API key, which is passed to dotnet nuget push through an environment variable. If the NUGET_USER secret is missing, the nuget.org step is skipped with a warning; GitHub Packages and the GitHub release still happen.

Setup needed before the first release:

  1. On nuget.org, add a Trusted Publishing policy for owner nullfx, repository NullFX.CRC, workflow file release.yml (no environment).
  2. Add the repo secret NUGET_USER with the nuget.org profile name (not the email address).
  3. Delete the old NUGET_ORG_API_KEY secret and revoke that key on nuget.org. Nothing uses it anymore.

Optional secrets:

Secret Purpose
NUGET_USER nuget.org profile name for Trusted Publishing. nuget.org publishing is skipped without it.
RELEASE_PUSH_TOKEN Only needed if branch protection on main blocks GITHUB_TOKEN from pushing the version commit.
RELEASE_VARIABLES_TOKEN Mirrors RELEASE_MAJOR/MINOR/REVISION into repository variables.

Validation

Run locally on Linux x64 with .NET SDK 10.0.401 and 8.0.425 and PowerShell 7.4. The Windows runners in CI are the final check, especially for the .NET Framework targets.

Check Result
dotnet build -c Release (library, all 11 TFMs) ✅ Builds with 0 errors. The only warnings are the expected SYSLIB0051 on net8.0/net10.0 from the existing serialization constructor on UnknownAlgorithmException (public API, left unchanged).
dotnet test -c Release -f net10.0 ✅ 36 passed, 0 failed, 0 skipped.
Benchmarks (--job short --exporters json github) ✅ 18 benchmarks in about 2 minutes. JSON, GitHub markdown, CSV and HTML reports produced. In this ShortRun, NullFX was roughly 1.1–12× faster than System.Data.HashFunction depending on algorithm and payload size (ShortRun numbers are noisy).
dotnet pack with 0.0.0 ✅ NullFX.CRC.0.0.0.nupkg + .snupkg. verify-package.ps1 passes: 11 TFMs, README, MIT, no dependencies, no tokens, no stray files.
Simulated release pack (RELEASE_VERSION=1.1.11, GITHUB_ACTIONS=true) ✅ NullFX.CRC.1.1.11.nupkg verifies. The nuspec carries the SourceLink repository commit. The assemblies are NullFX.CRC, Version=1.1.11.0, PublicKeyToken=090786b319d4654b. Nothing was published.
Version bump script ✅ Against the real tags: 1.1.10 → 1.1.11 (patch); a re-run with 1.1.11 untagged doesn't bump again; minor → 1.2.0; major → 2.0.0; a bump onto an existing tag is an error.
CI summary steps ✅ The TFM check, test summary and benchmark summary scripts from ci.yml were run against the real outputs above.
CodeQL Runs on this PR. Not reproducible locally.
Trusted Publishing login Not testable outside GitHub Actions; it needs the job's OIDC token and the nuget.org policy.
Workflow YAML All three files parse.
Repo hygiene git diff --check is clean. No sonar, #{RELEASE_VERSION}#, PROJECT package, vars.RELEASE or NUGET_ORG_API_KEY references remain. 1.1.10 appears nowhere; it lives only as 1/1/10 in Directory.Build.props. No bin/obj, nupkg or trx files are tracked.

Notes and follow-ups

  • ⚠️ Merging this PR triggers release.yml. Because 1.1.10 is already tagged, the first run will bump to 1.1.11, push that commit to main, publish to GitHub Packages (and to nuget.org if Trusted Publishing is set up), and create the v1.1.11 release. If you don't want a release on merge, disable the Release workflow first or add a path filter.
  • Tag format: new releases are tagged vX.Y.Z. Historical tags (1.1.10, …) have no v; the bump script recognises both.
  • Strong naming: this branch already contained PR add snk file #31, which added NullFX.CRC.snk and SignAssembly. The first release built here will be the first strong-named NullFX.CRC. That changes the assembly identity, which can affect .NET Framework consumers that bind to the unsigned assembly, so please consider it before releasing. Committing the .snk follows Microsoft's guidance for OSS strong naming.
  • AssemblyVersion: it still follows the full version (x.y.z.0), as it did before. For a strong-named library, a common alternative is to pin AssemblyVersion to x.0.0.0 to avoid binding redirects.
  • Dependencies: test packages are now pinned. Most were floating * before. FluentAssertions is held on 7.2.0 because 8.x is commercially licensed. BenchmarkDotNet moved from 0.13.5 to 0.15.8 for .NET 10 support.
  • Not added: Microsoft.NETFramework.ReferenceAssemblies. The SDK references it automatically when targeting packs are missing, and Windows runners have them installed anyway.
  • What this PR does not do: it does not merge, publish a package, force-push or rewrite history.

🤖 Generated with Claude Code

https://claude.ai/code/session_012C6aJYkYBV2GSnSZwsd9GU

jcguo and others added 12 commits March 24, 2025 18:31
Keeps the existing net40, net6.0 and net7.0 targets and adds the
requested modern targets (netstandard2.0, netcoreapp3.1, net45-net48
were already present).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012C6aJYkYBV2GSnSZwsd9GU
…VERSION

- Add Directory.Build.props holding RELEASE_MAJOR=1, RELEASE_MINOR=1,
  RELEASE_REVISION=10 as the checked-in version source of truth.
- RELEASE_VERSION is computed as MAJOR.MINOR.REVISION unless passed
  explicitly (e.g. -p:RELEASE_VERSION=0.0.0 for CI/PR builds).
- Version, PackageVersion, AssemblyVersion, FileVersion and
  InformationalVersion all derive from RELEASE_VERSION.
- Remove the #{RELEASE_VERSION}# replace-tokens placeholders from the
  library csproj; no hard-coded version remains in the project file.
- Turn off GeneratePackageOnBuild: packing is now an explicit
  'dotnet pack' step so builds and test runs no longer emit packages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012C6aJYkYBV2GSnSZwsd9GU
- Retarget NullFX.CRC.Tests from net7.0 (EOL) to net10.0, the common
  test/benchmark runtime.
- Pin previously floating (*) package versions so CI is reproducible.
  FluentAssertions is held on 7.2.0 (Apache-2.0); 8.x is commercially
  licensed.
- Drop the redundant xunit.assert reference (included by the xunit
  metapackage), the replace-tokens ReleaseVersion property and the
  SonarQubeExclude marker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012C6aJYkYBV2GSnSZwsd9GU
- Retarget NullFX.CRC.Benchmarks from netstandard2.0/net47/net48/
  netcoreapp3.1/net6.0/net7.0 to the common net10.0 runtime. The
  netstandard2.0 Exe target could never run, and BenchmarkDotNet is
  started from a single host runtime anyway.
- Update BenchmarkDotNet 0.13.5 -> 0.15.8 (0.13.x predates .NET 10).
- Forward args to BenchmarkRunner so CI can choose a short job and
  JSON/GitHub-markdown exporters. No-arg behaviour is unchanged.
- Drop the replace-tokens ReleaseVersion property and SonarQubeExclude.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012C6aJYkYBV2GSnSZwsd9GU
Replaces .github/workflows/cicd-actions.yml with .github/workflows/ci.yml:

- Runs on windows-latest (required for the net40-net48 targets).
- Installs the .NET 8 and .NET 10 SDKs. The .NET 10 SDK builds every
  target, including netcoreapp3.1/net6.0/net7.0 and the .NET Framework
  targets via reference packs, so no .NET Core 3.1 SDK is needed.
- Separate jobs: build (all TFMs + 0.0.0 inspection pack), unit tests
  (net10.0, TRX) and benchmarks (net10.0, BenchmarkDotNet ShortRun).
- Test and benchmark results are written to the job summary and
  uploaded as artifacts; the package and binlog are uploaded too.
- Never changes the version and never publishes (release.yml does).
- Removes the SonarCloud scanner, cache steps, SONAR_TOKEN usage, the
  JDK setup that only existed for SonarCloud, the replace-tokens step
  and the publish steps that ran on every push to main.
- Adds .github/scripts/verify-package.ps1, which checks the packed
  version, README, license, per-TFM assemblies and for leftover tokens
  or bin/obj content.
- Library csproj: drop the SonarQubeTestProject marker and set
  ContinuousIntegrationBuild under GitHub Actions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012C6aJYkYBV2GSnSZwsd9GU
Adds .github/workflows/codeql.yml as the replacement for SonarCloud:
- Runs on pull requests to main, pushes to main, weekly and on demand.
- windows-latest with the .NET 8 and .NET 10 SDKs, manual build mode,
  building the library for every target framework.
- security-and-quality query suite, category /language:csharp.
- Least-privilege permissions (security-events: write for upload).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012C6aJYkYBV2GSnSZwsd9GU
Adds .github/workflows/release.yml and .github/scripts/bump-version.ps1.

On every push to main (i.e. a merged PR) or manual dispatch:
1. Read RELEASE_MAJOR/MINOR/REVISION from Directory.Build.props.
2. Increment just before the release build: patch by default, or
   major/minor/patch via workflow_dispatch input. Idempotent: if the
   stored version has no v<version>/<version> tag yet, it is released
   as-is instead of being incremented again.
3. Commit the bump locally, build all TFMs, pack and verify the
   package with that version, then run tests and short benchmarks.
4. Push the bump to main with [skip ci]; abort before publishing if
   the push is rejected.
5. Publish to GitHub Packages (GITHUB_TOKEN), to nuget.org only when
   NUGET_ORG_API_KEY is set, and create the v<version> GitHub release
   with the .nupkg/.snupkg attached.
6. Optionally mirror RELEASE_MAJOR/MINOR/REVISION to repository
   variables when RELEASE_VARIABLES_TOKEN is provided (GITHUB_TOKEN
   cannot write repository variables).

Secrets are scoped to the steps that use them. Also lists the new
workflow files and Directory.Build.props under Solution Items.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012C6aJYkYBV2GSnSZwsd9GU
- Install with 'dotnet add package NullFX.CRC' (no PROJECT placeholder
  and no hard-coded --version that goes stale every release).
- Remove the broken SonarCloud quality-gate badge.
- Replace the old cicd-actions build badge with CI and CodeQL workflow
  badges for nullfx/NullFX.CRC, and add a NuGet version badge.
- List the supported target frameworks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012C6aJYkYBV2GSnSZwsd9GU
nuget.org now supports Trusted Publishing, so the release workflow no
longer needs a long-lived NUGET_ORG_API_KEY secret.

- Grant id-token: write (job-scoped; workflow default is contents: read).
- Exchange the job's GitHub OIDC token for a short-lived API key with
  NuGet/login@v1 immediately before 'dotnet nuget push'.
- Gate nuget.org publishing on the NUGET_USER secret (nuget.org profile
  name, not email); skip with a warning when it is absent.
- The temporary key is passed via env, not inlined into the script.

Requires a Trusted Publishing policy on nuget.org for owner nullfx,
repository NullFX.CRC, workflow file release.yml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012C6aJYkYBV2GSnSZwsd9GU
The library referenced Microsoft.SourceLink.GitHub with Version="*".
That now floats to 10.0.x, which pulls in System.IO.Hashing and emits
'doesn't support netcoreapp3.1/net6.0/net7.0' build warnings, and it
made release builds non-reproducible.

SourceLink for GitHub ships in the .NET 8+ SDK (required for the net8.0
and net10.0 targets), so the package reference is removed. Verified the
packed nuspec still carries the repository commit and that the package
has no dependencies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012C6aJYkYBV2GSnSZwsd9GU
@nullfx nullfx self-assigned this Oct 9, 2026
@nullfx

nullfx commented Oct 9, 2026

Copy link
Copy Markdown
Owner Author

🥳

@nullfx nullfx closed this Oct 9, 2026
@github-project-automation github-project-automation Bot moved this from In review to Done in NullFX.CRC Devopment Oct 9, 2026
@nullfx nullfx reopened this Oct 9, 2026
@nullfx
nullfx merged commit d439d8c into main Oct 9, 2026
11 checks passed
@nullfx
nullfx deleted the modernization-updates branch October 9, 2026 09:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

2 participants