[Release 0.70.0] ~85% NumPy 2.x API coverage: 100+ new np.* APIs, new nuget packages (OpenBLAS & pythonnet), 3 living dashboards #226
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build and Release | |
| on: | |
| push: | |
| branches: [ "master", "main" ] | |
| tags: [ "v*" ] | |
| pull_request: | |
| branches: [ "master", "main" ] | |
| workflow_dispatch: | |
| permissions: write-all | |
| env: | |
| # Suppress noisy build warnings that clutter CI annotations. | |
| # Uses %3B (URL-encoded semicolon) because MSBuild CLI treats raw ; and , as property separators. | |
| # XML doc warnings (CS15xx, CS17xx): ~7,200 of ~8,700 total — malformed/missing doc comments | |
| # CS8981: 'np' only contains lowercase chars — intentional for NumPy API compat | |
| # NU5048: PackageIconUrl deprecated — cosmetic NuGet warning | |
| DOTNET_NOWARN: CS1570%3BCS1571%3BCS1572%3BCS1573%3BCS1574%3BCS1587%3BCS1591%3BCS1711%3BCS1734%3BCS8981%3BNU5048 | |
| jobs: | |
| test: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ windows-latest, ubuntu-latest, macos-latest ] | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: | | |
| 8.0.x | |
| 10.0.x | |
| dotnet-quality: 'preview' | |
| - name: Build | |
| run: dotnet build test/NumSharp.Tests/NumSharp.Tests.csproj --configuration Release -p:NoWarn=${{ env.DOTNET_NOWARN }} | |
| # The differential-fuzz ORACLE is its own project (isolated so the optional OpenBLAS engine can be | |
| # managed apart from the managed-kernel tiers). It carries the FuzzMatrix gate plus the host-pinned | |
| # matmul_parity / BlasBackendDelta tiers, so it must build and test on the release-blocking path. | |
| - name: Build Oracle (differential-fuzz gate) | |
| run: dotnet build test/NumSharp.Tests.Oracle/NumSharp.Tests.Oracle.csproj --configuration Release -p:NoWarn=${{ env.DOTNET_NOWARN }} | |
| # Test filtering: | |
| # - OpenBugs: excluded (known-failing bug reproductions) | |
| # - HighMemory: excluded (requires 8GB+ RAM, too much for CI runners) | |
| # - WindowsOnly: excluded on non-Windows runners | |
| # - FuzzMatrix: INCLUDED — the NumPy differential gate (cast/binary/comparison/unary/reduce/ | |
| # where/place matrices + the seeded FuzzRandom corpus + FuzzRegression). These replay the | |
| # committed offline corpora under test/.../Fuzz/corpus/ and need no Python. The "millions of | |
| # cases" tail (Python-generated) runs nightly in fuzz-soak.yml. | |
| - name: Test (net8.0) | |
| shell: bash | |
| timeout-minutes: 10 | |
| run: | | |
| echo "Starting test run (net8.0)..." | |
| echo "dotnet version: $(dotnet --version)" | |
| echo "Available memory: $(free -h 2>/dev/null || echo 'N/A')" | |
| FILTER="TestCategory!=OpenBugs&TestCategory!=HighMemory" | |
| if [[ "$RUNNER_OS" != "Windows" ]]; then | |
| FILTER="$FILTER&TestCategory!=WindowsOnly" | |
| fi | |
| dotnet test test/NumSharp.Tests/NumSharp.Tests.csproj \ | |
| --configuration Release --no-build --framework net8.0 \ | |
| --filter "$FILTER" --logger "trx" | |
| - name: Test (net10.0) | |
| shell: bash | |
| timeout-minutes: 10 | |
| run: | | |
| echo "Starting test run (net10.0)..." | |
| echo "dotnet version: $(dotnet --version)" | |
| echo "Available memory: $(free -h 2>/dev/null || echo 'N/A')" | |
| FILTER="TestCategory!=OpenBugs&TestCategory!=HighMemory" | |
| if [[ "$RUNNER_OS" != "Windows" ]]; then | |
| FILTER="$FILTER&TestCategory!=WindowsOnly" | |
| fi | |
| dotnet test test/NumSharp.Tests/NumSharp.Tests.csproj \ | |
| --configuration Release --no-build --framework net10.0 \ | |
| --filter "$FILTER" --logger "trx" | |
| # The differential-fuzz gate (FuzzMatrix) now lives in NumSharp.Tests.Oracle. Same category | |
| # filter — the Oracle project carries no OpenBugs/HighMemory tiers, but the filter is harmless and | |
| # keeps the two suites symmetric. | |
| - name: Test Oracle (net8.0) | |
| shell: bash | |
| timeout-minutes: 10 | |
| run: | | |
| FILTER="TestCategory!=OpenBugs&TestCategory!=HighMemory" | |
| if [[ "$RUNNER_OS" != "Windows" ]]; then | |
| FILTER="$FILTER&TestCategory!=WindowsOnly" | |
| fi | |
| dotnet test test/NumSharp.Tests.Oracle/NumSharp.Tests.Oracle.csproj \ | |
| --configuration Release --no-build --framework net8.0 \ | |
| --filter "$FILTER" --logger "trx" | |
| - name: Test Oracle (net10.0) | |
| shell: bash | |
| timeout-minutes: 10 | |
| run: | | |
| FILTER="TestCategory!=OpenBugs&TestCategory!=HighMemory" | |
| if [[ "$RUNNER_OS" != "Windows" ]]; then | |
| FILTER="$FILTER&TestCategory!=WindowsOnly" | |
| fi | |
| dotnet test test/NumSharp.Tests.Oracle/NumSharp.Tests.Oracle.csproj \ | |
| --configuration Release --no-build --framework net10.0 \ | |
| --filter "$FILTER" --logger "trx" | |
| - name: Upload Test Results | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: test-results-${{ matrix.os }} | |
| path: ${{ github.workspace }}/**/TestResults/**/*.trx | |
| retention-days: 5 | |
| # Strong-naming gate, on EVERY push and PR. | |
| # | |
| # The identical check also runs in build-nuget below, but that job only exists on a tag push — and | |
| # a release-only gate is how the original defect survived six years: signing was configured in a | |
| # `Publish` configuration nothing built, so nothing ever evaluated it until someone looked at a | |
| # shipped DLL. Packing here costs ~1 min and means a regression is caught by the PR that causes it | |
| # rather than on release day, when the fix is a re-tag. | |
| # | |
| # This packs a THROWAWAY version into a scratch directory; it publishes nothing. | |
| verify-signing: | |
| name: Verify strong naming | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: | | |
| 8.0.x | |
| 10.0.x | |
| dotnet-quality: 'preview' | |
| # The key is committed and marked `binary` in .gitattributes (without which `* text=auto eol=lf` | |
| # would rewrite CRLF bytes inside it and corrupt the key). Assert both facts before building, so | |
| # a checkout/attribute problem reports as itself instead of as a confusing signing failure. | |
| - name: Key is present and intact | |
| run: | | |
| test -f Open.snk || { echo "::error::Open.snk missing from checkout — signing cannot work."; exit 1; } | |
| SIZE=$(stat -c%s Open.snk) | |
| SHA=$(sha256sum Open.snk | cut -d' ' -f1) | |
| echo "Open.snk: $SIZE bytes, sha256 $SHA" | |
| [ "$SIZE" = "596" ] || { echo "::error::Open.snk is $SIZE bytes, expected 596 — likely line-ending corruption; check the '*.snk binary' rule in .gitattributes."; exit 1; } | |
| [ "$SHA" = "b897629e0b20090c9219ac80392c037fa4cfcc2bdc21d3c45d9bf74b8df0f671" ] || { echo "::error::Open.snk content changed."; exit 1; } | |
| - name: Pack (throwaway) and verify every shipped assembly is signed | |
| run: | | |
| mkdir -p artifacts/signing-check | |
| for proj in src/NumSharp.Core/NumSharp.Core.csproj \ | |
| src/NumSharp.Bitmap/NumSharp.Bitmap.csproj \ | |
| src/NumSharp.Interop.pythonnet/NumSharp.Interop.pythonnet.csproj \ | |
| src/NumSharp.Interop.OpenBLAS/NumSharp.Interop.OpenBLAS.csproj; do | |
| # Tolerate absence: the interop packages do not exist on every branch. | |
| [ -f "$proj" ] || { echo "skip (absent): $proj"; continue; } | |
| echo "::group::pack $proj" | |
| # GeneratePackageOnBuild=true (set in each csproj) + `dotnet pack` + multi-targeting | |
| # yields NU5026 — the outer Pack runs before the per-TFM DLLs stage. Overriding it to | |
| # false here restores the normal restore→build→pack ordering. | |
| dotnet pack "$proj" --configuration Release --output artifacts/signing-check \ | |
| -p:GeneratePackageOnBuild=false \ | |
| -p:NoWarn=${{ env.DOTNET_NOWARN }} | |
| echo "::endgroup::" | |
| done | |
| dotnet run .github/scripts/verify_strong_name.cs -- artifacts/signing-check | |
| # Python.NET interop. Kept as its OWN job rather than extra steps on `test`: | |
| # the embedded-CPython setup must not slow down or destabilise the main gate, | |
| # and a red here is immediately attributable to interop. | |
| # | |
| # Building is itself a gate — this is the ONLY place CI compiles | |
| # src/NumSharp.Interop.pythonnet, whose sources reach into NumSharp.Core | |
| # internals and would otherwise break unnoticed. | |
| # | |
| # The suite needs a Python that can host pythonnet AND import numpy. Discovery | |
| # is left to PythonSession's own probe (the shipped code path, so CI exercises | |
| # what users hit); when it finds nothing, every interop test reports | |
| # Inconclusive — a runner without a usable libpython goes SKIPPED, never red. | |
| # The "Report Python host" step below prints exactly the values that probe | |
| # reads, so a silent skip is diagnosable straight from the log. | |
| interop-test: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ windows-latest, ubuntu-latest, macos-latest ] | |
| runs-on: ${{ matrix.os }} | |
| # This job installs Python + numpy below, so the engine MUST start here. Requiring the engine is | |
| # now PythonSession's DEFAULT (an absent engine hard-fails instead of skipping), so this line is | |
| # explicit belt-and-suspenders — it also guards against an ambient NUMSHARP_PYTHONNET_REQUIRE_ENGINE=0 | |
| # from the runner environment silently re-enabling the green-by-skipping failure mode. | |
| env: | |
| NUMSHARP_PYTHONNET_REQUIRE_ENGINE: '1' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: | | |
| 8.0.x | |
| 10.0.x | |
| dotnet-quality: 'preview' | |
| # 3.12 sits inside pythonnet 3.0.5's supported range (PythonSession rejects | |
| # anything outside 3.7-3.13) and has numpy 2.x wheels on all three OSes. | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.12' | |
| # The interop suite's byte-exact parity tests compare NumSharp's OpenBLAS-backed LAPACK/BLAS | |
| # against THIS live numpy, so both MUST call the identical binary. Two things pin that: | |
| # | |
| # (1) VERSION — numpy==2.4.2. NumSharp bundles scipy-openblas 0.3.31.22.0, the exact build | |
| # numpy 2.4.2 pins (numpy.libs is byte-identical). A newer numpy (e.g. 2.5.x) ships a | |
| # DIFFERENT openblas build, so its results diverge ~1 ULP and every *_ByteExact test reds. | |
| # Keep in lockstep with tools/openblas-manifest.json's numpy_version. | |
| # | |
| # (2) BLAS FLAVOUR on macOS — numpy 2.4.2 ships TWO Apple-silicon wheels: macosx_14_0_arm64 | |
| # links Apple **Accelerate**, macosx_11_0_arm64 bundles **scipy-openblas**. On a macOS 14+ | |
| # runner (ours is macos-26) pip prefers the newer tag, so a plain `pip install numpy` links | |
| # Accelerate — a WHOLLY DIFFERENT BLAS than NumSharp's bundle, which no version pin or | |
| # OpenBLAS bump can reconcile (Accelerate is not OpenBLAS). Force the OpenBLAS wheel so both | |
| # sides call scipy-openblas 0.3.31.22.0, exactly as Windows/Linux already do. (Windows and | |
| # Linux numpy wheels are OpenBLAS by default, so they just take the version pin.) | |
| - name: Install numpy | |
| shell: bash | |
| run: | | |
| python -m pip install --upgrade pip | |
| if [ "$RUNNER_OS" = "macOS" ] && [ "$(uname -m)" = "arm64" ]; then | |
| python -m pip download --only-binary=:all: --platform macosx_11_0_arm64 \ | |
| --python-version 312 --implementation cp --abi cp312 --no-deps \ | |
| -d "$RUNNER_TEMP/np" "numpy==2.4.2" | |
| python -m pip install "$RUNNER_TEMP"/np/numpy-*.whl | |
| else | |
| python -m pip install "numpy==2.4.2" | |
| fi | |
| python -c "import numpy; print('numpy', numpy.__version__); numpy.show_config()" | |
| # Prints precisely what PythonSession.Probe consumes, so if the interop tests | |
| # report Inconclusive the reason is visible without reproducing locally. | |
| - name: Report Python host | |
| shell: bash | |
| run: | | |
| python -c "import sys, sysconfig, numpy; \ | |
| print('python ', sys.version.replace('\n', ' ')); \ | |
| print('numpy ', numpy.__version__); \ | |
| print('base_prefix ', sys.base_prefix); \ | |
| print('INSTSONAME ', sysconfig.get_config_var('INSTSONAME')); \ | |
| print('LIBDIR ', sysconfig.get_config_var('LIBDIR'))" | |
| # The bundled OpenBLAS binaries are gitignored, so a clean checkout has none. This suite runs | |
| # with OpenBLAS as its DEFAULT backend (PythonSession.Start calls OpenBlasEngine.TryEnable), and | |
| # the Backend_IsOpenBlas* tests hard-assert OpenBlasEngine.Enabled. Stage the pinned per-RID | |
| # assets before the build so the current RID's native library is copied into the test output. | |
| # (fetch_openblas.py is stdlib-only; Python is already set up above.) | |
| - name: Stage OpenBLAS native assets | |
| run: python src/NumSharp.Interop.OpenBLAS/tools/fetch_openblas.py | |
| - name: Build | |
| run: dotnet build test/NumSharp.Tests.Interop/NumSharp.Tests.Interop.csproj --configuration Release -p:NoWarn=${{ env.DOTNET_NOWARN }} | |
| # The current-RID OpenBLAS native flows to the test output as CopyToOutputDirectory content | |
| # across the ProjectReference — but on a clean runner it can be missing (RID-specific runtime | |
| # assets don't always flow to a portable, no-RID build). Copy the staged runtimes into the test | |
| # output as a self-heal, and log both what landed AND the .so's dynamic deps, so a genuine | |
| # load failure (a missing libgfortran/libquadmath) is visible rather than a silent Enabled=false. | |
| - name: Ensure + diagnose OpenBLAS native in test output | |
| shell: bash | |
| run: | | |
| for tfm in net8.0 net10.0; do | |
| out="test/NumSharp.Tests.Interop/bin/Release/$tfm" | |
| mkdir -p "$out/runtimes" | |
| cp -r src/NumSharp.Interop.OpenBLAS/runtimes/* "$out/runtimes/" 2>/dev/null || true | |
| echo "=== $out native assets ===" | |
| find "$out" -path "*native*" \( -name "*openblas*" -o -name "*scipy*" \) 2>/dev/null || echo " (none — content did not flow)" | |
| done | |
| so=$(find src/NumSharp.Interop.OpenBLAS/runtimes -name "*openblas*.so" 2>/dev/null | head -1) | |
| if [ -n "$so" ] && command -v ldd >/dev/null 2>&1; then | |
| echo "=== ldd $so ==="; ldd "$so" 2>&1 | grep -i "not found" || echo " (all deps resolved)" | |
| fi | |
| # No category filter: the interop suite carries no OpenBugs/HighMemory tests, | |
| # and every test already self-asserts a LiveExports/LiveImports leak baseline. | |
| - name: Test (net8.0) | |
| timeout-minutes: 10 | |
| run: | | |
| dotnet test test/NumSharp.Tests.Interop/NumSharp.Tests.Interop.csproj \ | |
| --configuration Release --no-build --framework net8.0 --logger "trx" | |
| shell: bash | |
| - name: Test (net10.0) | |
| timeout-minutes: 10 | |
| run: | | |
| dotnet test test/NumSharp.Tests.Interop/NumSharp.Tests.Interop.csproj \ | |
| --configuration Release --no-build --framework net10.0 --logger "trx" | |
| shell: bash | |
| - name: Upload Test Results | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: interop-test-results-${{ matrix.os }} | |
| path: ${{ github.workspace }}/**/TestResults/**/*.trx | |
| retention-days: 5 | |
| validate-release: | |
| # Interop now ships as a package, so a broken interop must block the release. | |
| needs: [test, interop-test] | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| runs-on: ubuntu-latest | |
| outputs: | |
| is_valid: ${{ steps.check.outputs.is_valid }} | |
| version: ${{ steps.check.outputs.version }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Verify tag is on master branch | |
| id: check | |
| run: | | |
| TAG_COMMIT=$(git rev-parse HEAD) | |
| VERSION="${GITHUB_REF#refs/tags/v}" | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| # Check if the tagged commit is reachable from origin/master | |
| if git merge-base --is-ancestor "$TAG_COMMIT" origin/master; then | |
| echo "Tag v$VERSION is on master branch" | |
| echo "is_valid=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "::error::Tag v$VERSION is NOT on master branch. Releases must be tagged from master." | |
| echo "is_valid=false" >> $GITHUB_OUTPUT | |
| exit 1 | |
| fi | |
| build-nuget: | |
| needs: validate-release | |
| if: needs.validate-release.outputs.is_valid == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: | | |
| 8.0.x | |
| 10.0.x | |
| dotnet-quality: 'preview' | |
| - name: Setup Python (for OpenBLAS native-asset staging) | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.12' | |
| # NumSharp.Interop.OpenBLAS bundles the prebuilt OpenBLAS binaries NumPy pins as per-RID runtime | |
| # assets. They are gitignored, so a clean release checkout has none — fetch_openblas.py downloads | |
| # all 8 RIDs from PyPI and verifies two sha256s each against the checked-in manifest (pure stdlib, | |
| # no pip needed). RequireOpenBlasAssets=true at build/pack (below) then makes a missing asset a | |
| # hard error, so the package can never publish as an empty managed shell. | |
| - name: Stage OpenBLAS native assets | |
| run: python src/NumSharp.Interop.OpenBLAS/tools/fetch_openblas.py | |
| - name: Get version info | |
| id: version | |
| shell: bash | |
| run: | | |
| VERSION="${GITHUB_REF#refs/tags/v}" | |
| ASSEMBLY_VERSION="${VERSION%%-*}" | |
| COMMIT_SHA="${GITHUB_SHA:0:7}" | |
| echo "VERSION=$VERSION" >> $GITHUB_OUTPUT | |
| echo "ASSEMBLY_VERSION=$ASSEMBLY_VERSION" >> $GITHUB_OUTPUT | |
| echo "COMMIT_SHA=$COMMIT_SHA" >> $GITHUB_OUTPUT | |
| echo "Building version $VERSION (assembly: $ASSEMBLY_VERSION) +$COMMIT_SHA" | |
| - name: Build | |
| run: | | |
| # -t:Rebuild (NOT incremental): every shipping project now emits XML docs | |
| # (GenerateDocumentationFile), and the Pack steps below run --no-build. An incremental | |
| # build can declare a stale target-framework up-to-date and skip CoreCompile, leaving no | |
| # .xml on disk — pack then fails NU5026, or (worse) packs stale output. Rebuild forces a | |
| # clean compile of every TFM so --no-build always packs a complete, current tree. This is | |
| # the repo convention for anything that gets packed (see .claude/CLAUDE.md / OpenBLAS design). | |
| dotnet build src/NumSharp.Core/NumSharp.Core.csproj \ | |
| --configuration Release \ | |
| -t:Rebuild \ | |
| -p:NoWarn=${{ env.DOTNET_NOWARN }} \ | |
| -p:Version=${{ steps.version.outputs.VERSION }} \ | |
| -p:AssemblyVersion=${{ steps.version.outputs.ASSEMBLY_VERSION }} \ | |
| -p:FileVersion=${{ steps.version.outputs.ASSEMBLY_VERSION }} \ | |
| -p:PackageVersion=${{ steps.version.outputs.VERSION }} \ | |
| -p:SourceRevisionId=${{ steps.version.outputs.COMMIT_SHA }} | |
| dotnet build src/NumSharp.Bitmap/NumSharp.Bitmap.csproj \ | |
| --configuration Release \ | |
| -t:Rebuild \ | |
| -p:NoWarn=${{ env.DOTNET_NOWARN }} \ | |
| -p:Version=${{ steps.version.outputs.VERSION }} \ | |
| -p:AssemblyVersion=${{ steps.version.outputs.ASSEMBLY_VERSION }} \ | |
| -p:FileVersion=${{ steps.version.outputs.ASSEMBLY_VERSION }} \ | |
| -p:PackageVersion=${{ steps.version.outputs.VERSION }} \ | |
| -p:SourceRevisionId=${{ steps.version.outputs.COMMIT_SHA }} | |
| # Interop packs its NumSharp.Core ProjectReference as a NuGet dependency | |
| # on "NumSharp" $(Version), so the same -p:Version keeps the two in lockstep | |
| # (the csproj's hardcoded 0.60.0 is only the local-dev default). | |
| dotnet build src/NumSharp.Interop.pythonnet/NumSharp.Interop.pythonnet.csproj \ | |
| --configuration Release \ | |
| -t:Rebuild \ | |
| -p:NoWarn=${{ env.DOTNET_NOWARN }} \ | |
| -p:Version=${{ steps.version.outputs.VERSION }} \ | |
| -p:AssemblyVersion=${{ steps.version.outputs.ASSEMBLY_VERSION }} \ | |
| -p:FileVersion=${{ steps.version.outputs.ASSEMBLY_VERSION }} \ | |
| -p:PackageVersion=${{ steps.version.outputs.VERSION }} \ | |
| -p:SourceRevisionId=${{ steps.version.outputs.COMMIT_SHA }} | |
| # OpenBLAS: native assets were staged above; RequireOpenBlasAssets=true makes a missing | |
| # bundle a hard error (GeneratePackageOnBuild packs during build, so it is enforced here too). | |
| dotnet build src/NumSharp.Interop.OpenBLAS/NumSharp.Interop.OpenBLAS.csproj \ | |
| --configuration Release \ | |
| -t:Rebuild \ | |
| -p:NoWarn=${{ env.DOTNET_NOWARN }} \ | |
| -p:RequireOpenBlasAssets=true \ | |
| -p:Version=${{ steps.version.outputs.VERSION }} \ | |
| -p:AssemblyVersion=${{ steps.version.outputs.ASSEMBLY_VERSION }} \ | |
| -p:FileVersion=${{ steps.version.outputs.ASSEMBLY_VERSION }} \ | |
| -p:PackageVersion=${{ steps.version.outputs.VERSION }} \ | |
| -p:SourceRevisionId=${{ steps.version.outputs.COMMIT_SHA }} | |
| - name: Pack | |
| run: | | |
| mkdir -p artifacts/nuget | |
| dotnet pack src/NumSharp.Core/NumSharp.Core.csproj \ | |
| --configuration Release \ | |
| --no-build \ | |
| --output artifacts/nuget \ | |
| -p:NoWarn=${{ env.DOTNET_NOWARN }} \ | |
| -p:Version=${{ steps.version.outputs.VERSION }} \ | |
| -p:AssemblyVersion=${{ steps.version.outputs.ASSEMBLY_VERSION }} \ | |
| -p:FileVersion=${{ steps.version.outputs.ASSEMBLY_VERSION }} \ | |
| -p:PackageVersion=${{ steps.version.outputs.VERSION }} | |
| dotnet pack src/NumSharp.Bitmap/NumSharp.Bitmap.csproj \ | |
| --configuration Release \ | |
| --no-build \ | |
| --output artifacts/nuget \ | |
| -p:NoWarn=${{ env.DOTNET_NOWARN }} \ | |
| -p:Version=${{ steps.version.outputs.VERSION }} \ | |
| -p:AssemblyVersion=${{ steps.version.outputs.ASSEMBLY_VERSION }} \ | |
| -p:FileVersion=${{ steps.version.outputs.ASSEMBLY_VERSION }} \ | |
| -p:PackageVersion=${{ steps.version.outputs.VERSION }} | |
| dotnet pack src/NumSharp.Interop.pythonnet/NumSharp.Interop.pythonnet.csproj \ | |
| --configuration Release \ | |
| --no-build \ | |
| --output artifacts/nuget \ | |
| -p:NoWarn=${{ env.DOTNET_NOWARN }} \ | |
| -p:Version=${{ steps.version.outputs.VERSION }} \ | |
| -p:AssemblyVersion=${{ steps.version.outputs.ASSEMBLY_VERSION }} \ | |
| -p:FileVersion=${{ steps.version.outputs.ASSEMBLY_VERSION }} \ | |
| -p:PackageVersion=${{ steps.version.outputs.VERSION }} | |
| dotnet pack src/NumSharp.Interop.OpenBLAS/NumSharp.Interop.OpenBLAS.csproj \ | |
| --configuration Release \ | |
| --no-build \ | |
| --output artifacts/nuget \ | |
| -p:NoWarn=${{ env.DOTNET_NOWARN }} \ | |
| -p:RequireOpenBlasAssets=true \ | |
| -p:Version=${{ steps.version.outputs.VERSION }} \ | |
| -p:AssemblyVersion=${{ steps.version.outputs.ASSEMBLY_VERSION }} \ | |
| -p:FileVersion=${{ steps.version.outputs.ASSEMBLY_VERSION }} \ | |
| -p:PackageVersion=${{ steps.version.outputs.VERSION }} | |
| echo "Packages built:" | |
| ls -la artifacts/nuget/ | |
| # Last gate before the artifact leaves the machine. Asserts every managed assembly inside every | |
| # packed .nupkg carries NumSharp's public key AND a real signature (not delay-signed), and fails | |
| # rather than passing vacuously if pack produced nothing. This is the check whose absence let six | |
| # years of releases ship with PublicKeyToken=null. | |
| - name: Verify strong naming of packed assemblies | |
| run: dotnet run .github/scripts/verify_strong_name.cs -- artifacts/nuget | |
| - name: Upload NuGet Packages | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: nuget-packages | |
| path: artifacts/nuget/*.nupkg | |
| retention-days: 5 | |
| create-release: | |
| needs: [validate-release, build-nuget] | |
| if: needs.validate-release.outputs.is_valid == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Extract version | |
| id: version | |
| run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT | |
| - name: Download NuGet Packages | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: nuget-packages | |
| path: artifacts | |
| - name: Generate checksums | |
| run: | | |
| cd artifacts | |
| for f in *.nupkg; do | |
| sha256sum "$f" | cut -d' ' -f1 > "${f}.sha256" | |
| echo "${f}: $(cat ${f}.sha256)" | |
| done | |
| - name: Check if prerelease | |
| id: prerelease | |
| run: | | |
| if [[ "${{ steps.version.outputs.VERSION }}" == *"-"* ]]; then | |
| echo "IS_PRERELEASE=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "IS_PRERELEASE=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Compose release notes | |
| id: notes | |
| run: | | |
| VERSION="${{ steps.version.outputs.VERSION }}" | |
| NOTES_FILE="docs/releases/RELEASE_${VERSION}.md" | |
| { | |
| echo "## NumSharp v${VERSION}" | |
| echo "" | |
| echo "### Install via NuGet" | |
| echo "" | |
| echo '```' | |
| echo "dotnet add package NumSharp --version ${VERSION}" | |
| echo "dotnet add package NumSharp.Bitmap --version ${VERSION}" | |
| echo "dotnet add package NumSharp.Interop.pythonnet --version ${VERSION}" | |
| echo "dotnet add package NumSharp.Interop.OpenBLAS --version ${VERSION}" | |
| echo '```' | |
| echo "" | |
| echo "### Packages" | |
| echo "" | |
| echo "| Package | NuGet |" | |
| echo "|---------|-------|" | |
| echo "| NumSharp | [](https://www.nuget.org/packages/NumSharp/${VERSION}) |" | |
| echo "| NumSharp.Bitmap | [](https://www.nuget.org/packages/NumSharp.Bitmap/${VERSION}) |" | |
| echo "| NumSharp.Interop.pythonnet | [](https://www.nuget.org/packages/NumSharp.Interop.pythonnet/${VERSION}) |" | |
| echo "| NumSharp.Interop.OpenBLAS | [](https://www.nuget.org/packages/NumSharp.Interop.OpenBLAS/${VERSION}) |" | |
| } > RELEASE_BODY.md | |
| if [ -f "$NOTES_FILE" ]; then | |
| { echo ""; echo "---"; echo ""; cat "$NOTES_FILE"; } >> RELEASE_BODY.md | |
| echo "HAS_NOTES=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "HAS_NOTES=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| echo "Composed RELEASE_BODY.md ($(wc -l < RELEASE_BODY.md) lines); notes file present: $([ -f "$NOTES_FILE" ] && echo yes || echo no)" | |
| - name: Create Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| files: | | |
| artifacts/*.nupkg | |
| artifacts/*.sha256 | |
| draft: false | |
| prerelease: ${{ steps.prerelease.outputs.IS_PRERELEASE }} | |
| generate_release_notes: ${{ steps.notes.outputs.HAS_NOTES != 'true' }} | |
| body_path: RELEASE_BODY.md | |
| publish-nuget: | |
| needs: [validate-release, build-nuget] | |
| if: needs.validate-release.outputs.is_valid == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download NuGet Packages | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: nuget-packages | |
| path: artifacts | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: '8.0.x' | |
| - name: Push to NuGet | |
| run: | | |
| for package in artifacts/*.nupkg; do | |
| echo "Pushing $package..." | |
| dotnet nuget push "$package" \ | |
| --api-key ${{ secrets.NUGETAPIKEY }} \ | |
| --source https://api.nuget.org/v3/index.json \ | |
| --skip-duplicate | |
| done |