From e36058e4cae57d979ad4716abc9216d7d85f1deb Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Jul 2026 12:21:17 +0000 Subject: [PATCH 1/5] test: add PackageSmokeTest consumer + enable local multi-platform packing - Runtime.csproj: Exists-guard the win-only pack entries (native + .exe apphosts) so `dotnet pack` succeeds on Linux too, producing a clean linux-x64-only package. On Windows the files are present, so the produced package is unchanged. This lets a cross-platform package be assembled by populating both artifacts/native/ folders on one (Windows) pack host, and a per-platform package be built locally on either OS for testing. - examples/PackageSmokeTest: a consumer that references CycloneDDS.NET as a real NuGet package from the local feed (nuget.config), declares a [DdsTopic] (so the packaged code generator + idlc run at build time), and performs a publish/subscribe round-trip with assertions. Runs identically on Windows and Linux; SmokePkgVersion selects the packed version. - RELEASE-GUIDE.md: document building/testing a multi-platform package locally (Windows cross-platform pack, Linux-only pack, smoke test for both the package and the ddsmonitor tool) and update prerequisites (.NET 10 SDK, per-OS native toolchain). Verified on Linux: packed 0.3.1 + 1.1.1 locally, smoke test PASS (native + codegen + round-trip via the package), and the ddsmonitor tool installed from the local feed serves HTTP 200. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01N7RoW2a2tBB2T8tDbNHB11 --- RELEASE-GUIDE.md | 74 ++++++++++++++- .../PackageSmokeTest/PackageSmokeTest.csproj | 39 ++++++++ examples/PackageSmokeTest/Program.cs | 90 +++++++++++++++++++ examples/PackageSmokeTest/nuget.config | 13 +++ .../CycloneDDS.Runtime.csproj | 16 ++-- 5 files changed, 222 insertions(+), 10 deletions(-) create mode 100644 examples/PackageSmokeTest/PackageSmokeTest.csproj create mode 100644 examples/PackageSmokeTest/Program.cs create mode 100644 examples/PackageSmokeTest/nuget.config diff --git a/RELEASE-GUIDE.md b/RELEASE-GUIDE.md index 6b9be94..cf34750 100644 --- a/RELEASE-GUIDE.md +++ b/RELEASE-GUIDE.md @@ -9,10 +9,80 @@ Before you begin, ensure you have: 1. **Maintainer Access** to the GitHub repository 2. **NuGet API Key** with push permissions for the CycloneDDS.NET package 3. **Development Environment** with: - - .NET 8.0 SDK or later - - PowerShell + - **.NET 10 SDK** — the code uses C# 13 language features, so building requires it; the produced binaries still target `net8.0`. (The .NET 8 runtime is also handy for running the `net8.0` tests.) + - **CMake 3.16+** + - **Windows:** PowerShell and Visual Studio 2022 (*C++ Desktop Development* workload) for native compilation. + - **Linux:** a C toolchain and `patchelf` — `sudo apt-get install -y cmake build-essential patchelf`. - Git command-line tools +## Building & Testing a Multi-Platform Package Locally + +The published `CycloneDDS.NET` and `CycloneDDS.NET.DdsMonitor` packages ship native +assets for **both** `win-x64` and `linux-x64`. The pack step includes whatever +native artifacts are present under `artifacts/native//` (Windows-only entries +are `Exists`-guarded), so a **cross-platform** package is produced by making *both* +platforms' natives available on one pack host. + +A single machine can only compile one platform's native libraries, so bring the +other platform's from CI — the workflow publishes a `native-linux-x64` artifact +(and you can build `win-x64` locally) — or from WSL/Docker. Because the packages +bundle the Windows apphost `.exe` launchers for the build-time tools, **the final +cross-platform pack must run on Windows**. Packing on Linux is fully supported but +yields a `linux-x64`-only package (handy for validating the Linux side). + +### A. Cross-platform package on a Windows box + +```powershell +# 1. Build the Windows native, and bring in the Linux native from a green CI run on main +.\build\native-win.ps1 +gh run download --repo pjanec/CycloneDds.NET -n native-linux-x64 -D artifacts\native\linux-x64 + +# 2. Build, test and pack -> artifacts\nuget\ (both packages, both platforms' natives) +.\build\pack.ps1 +``` + +### B. Linux-only package on a Linux box + +```bash +build/native-linux.sh Release +dotnet build CycloneDDS.NET.Core.slnf -c Release +dotnet pack src/CycloneDDS.Runtime/CycloneDDS.Runtime.csproj -c Release --no-build -o artifacts/nuget +dotnet pack tools/DdsMonitor/DdsMonitor.Blazor/DdsMonitor.csproj -c Release --no-build -o artifacts/nuget +``` + +### Smoke-testing the packages + +`examples/PackageSmokeTest` consumes `CycloneDDS.NET` as a real NuGet **package** +from the local feed (`artifacts/nuget`, set in its `nuget.config`). It declares a +`[DdsTopic]` (so the packaged code generator + `idlc` run) and does a +publish/subscribe round-trip. Run it against the exact packed version: + +```bash +# Linux / bash +VER=$(ls artifacts/nuget/CycloneDDS.NET.[0-9]*.nupkg | head -1 | sed -E 's|.*/CycloneDDS\.NET\.(.+)\.nupkg|\1|') +dotnet run --project examples/PackageSmokeTest -c Release -p:SmokePkgVersion=$VER +# Expect: [smoke] PASS: round-trip Id=42 Value=3.14159 Label=hello-cyclonedds +``` + +```powershell +# Windows / PowerShell +$pkg = Get-ChildItem artifacts\nuget\CycloneDDS.NET.[0-9]*.nupkg | Select-Object -First 1 +$ver = $pkg.Name -replace '^CycloneDDS\.NET\.(.+)\.nupkg$','$1' +dotnet run --project examples\PackageSmokeTest -c Release -p:SmokePkgVersion=$ver +``` + +Smoke-test the DdsMonitor global tool: + +```bash +dotnet tool install --global --add-source artifacts/nuget --version CycloneDDS.NET.DdsMonitor +ddsmonitor --NoBrowser true # logs "Application started" and serves http://127.0.0.1:/ +dotnet tool uninstall --global CycloneDDS.NET.DdsMonitor +``` + +Run the smoke test on **both** a Windows and a Linux box before publishing (copy a +cross-platform package across, or do a per-platform local build on each). Both were +validated during development — Linux locally + in CI, Windows in the CI `build` job. + ## Version Management This project uses **Nerdbank.GitVersioning (NBGV)** for automatic semantic versioning. diff --git a/examples/PackageSmokeTest/PackageSmokeTest.csproj b/examples/PackageSmokeTest/PackageSmokeTest.csproj new file mode 100644 index 0000000..19ec19f --- /dev/null +++ b/examples/PackageSmokeTest/PackageSmokeTest.csproj @@ -0,0 +1,39 @@ + + + + + Exe + net8.0 + enable + disable + + true + false + + false + + *-* + + + + + + + diff --git a/examples/PackageSmokeTest/Program.cs b/examples/PackageSmokeTest/Program.cs new file mode 100644 index 0000000..3f290c0 --- /dev/null +++ b/examples/PackageSmokeTest/Program.cs @@ -0,0 +1,90 @@ +using System; +using System.Threading; +using System.Runtime.InteropServices; +using CycloneDDS.Runtime; +using CycloneDDS.Schema; + +namespace PackageSmokeTest +{ + // Declaring a [DdsTopic] forces the package's code generator (idlc) to run at + // build time — so a successful build already proves the packaged tooling works + // on this OS. Main() then proves the runtime native (libddsc.so / ddsc.dll) + // works by doing a real publish -> subscribe round-trip. + [DdsTopic("PackageSmokeTest_SmokeSample")] + public partial struct SmokeSample + { + [DdsKey] public int Id; + public double Value; + public FixedString32 Label; + } + + public static class Program + { + public static int Main() + { + Console.WriteLine($"[smoke] {RuntimeInformation.OSDescription} / {RuntimeInformation.ProcessArchitecture}"); + try + { + using var participant = new DdsParticipant(); + using var writer = new DdsWriter(participant); + using var reader = new DdsReader(participant); + + // Wait for the reader/writer to discover each other so the first + // sample isn't dropped. This also exercises the matched-status + // listener callback (the ABI fix that was broken on Linux). + if (!writer.WaitForReaderAsync(TimeSpan.FromSeconds(5)).GetAwaiter().GetResult()) + { + Console.Error.WriteLine("[smoke] FAIL: reader/writer did not discover each other"); + return 1; + } + + var sent = new SmokeSample + { + Id = 42, + Value = 3.14159, + Label = new FixedString32("hello-cyclonedds") + }; + writer.Write(sent); + + SmokeSample? received = null; + for (int attempt = 0; attempt < 50 && received is null; attempt++) + { + using (var loan = reader.Take(maxSamples: 10)) + { + foreach (var sample in loan) + { + if (sample.IsValid) { received = sample.Data; break; } + } + } + if (received is null) Thread.Sleep(100); + } + + if (received is null) + { + Console.Error.WriteLine("[smoke] FAIL: no sample received within timeout"); + return 1; + } + + var got = received.Value; + bool ok = got.Id == sent.Id + && Math.Abs(got.Value - sent.Value) < 1e-9 + && got.Label.ToString() == sent.Label.ToString(); + + if (!ok) + { + Console.Error.WriteLine( + $"[smoke] FAIL: round-trip mismatch: Id={got.Id} Value={got.Value} Label={got.Label}"); + return 1; + } + + Console.WriteLine($"[smoke] PASS: round-trip Id={got.Id} Value={got.Value} Label={got.Label}"); + return 0; + } + catch (Exception ex) + { + Console.Error.WriteLine($"[smoke] FAIL: {ex}"); + return 1; + } + } + } +} diff --git a/examples/PackageSmokeTest/nuget.config b/examples/PackageSmokeTest/nuget.config new file mode 100644 index 0000000..ca4635b --- /dev/null +++ b/examples/PackageSmokeTest/nuget.config @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/src/CycloneDDS.Runtime/CycloneDDS.Runtime.csproj b/src/CycloneDDS.Runtime/CycloneDDS.Runtime.csproj index ca1a998..db43d60 100644 --- a/src/CycloneDDS.Runtime/CycloneDDS.Runtime.csproj +++ b/src/CycloneDDS.Runtime/CycloneDDS.Runtime.csproj @@ -13,7 +13,7 @@ - + - - - - - + + + + + - + @@ -63,7 +63,7 @@ - + From 1cc298138215d4f0673586a03f939f853694a4ef Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Jul 2026 13:07:23 +0000 Subject: [PATCH 2/5] build: add test-package scripts, publish native-win-x64 artifact, doc CI-download flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to make local multi-platform package build/test repeatable, and to address two rough edges found while verifying on Windows. - build/test-package.sh + build/test-package.ps1: smoke-test the packages from a feed. They pick the NEWEST package by write-time (fixes the name-sort footgun that selected an old historical build), run examples/PackageSmokeTest against it (restore -> bundled idlc code-gen -> pub/sub round-trip), then install the ddsmonitor tool, confirm HTTP 200, and uninstall. - CI: the windows build job now also uploads the win-x64 native as the 'native-win-x64' artifact. Combined with the existing 'native-linux-x64' and 'nuget-packages' artifacts, this enables assembling the package locally with no C++ toolchain (download both natives -> dotnet pack) — or just downloading the finished cross-platform 'nuget-packages' and testing it. - RELEASE-GUIDE: reorder the local build/test section easiest-first (download the finished package from CI; toolchain-free local pack; full local build), and reference the test-package scripts instead of the fragile inline version picker. Verified on Linux: build/test-package.sh -> smoke PASS + ddsmonitor HTTP 200. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01N7RoW2a2tBB2T8tDbNHB11 --- .github/workflows/ci.yml | 8 +++ RELEASE-GUIDE.md | 101 ++++++++++++++++++++--------------- build/test-package.ps1 | 96 +++++++++++++++++++++++++++++++++ build/test-package.sh | 112 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 275 insertions(+), 42 deletions(-) create mode 100644 build/test-package.ps1 create mode 100755 build/test-package.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 825bb43..6a17acb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -109,6 +109,14 @@ jobs: shell: pwsh run: .\build\pack.ps1 + - name: Upload win-x64 Native Artifacts + uses: actions/upload-artifact@v4 + if: always() + with: + name: native-win-x64 + path: artifacts/native/win-x64/* + if-no-files-found: warn + - name: Upload NuGet Packages uses: actions/upload-artifact@v4 if: always() diff --git a/RELEASE-GUIDE.md b/RELEASE-GUIDE.md index cf34750..e3156d2 100644 --- a/RELEASE-GUIDE.md +++ b/RELEASE-GUIDE.md @@ -18,70 +18,87 @@ Before you begin, ensure you have: ## Building & Testing a Multi-Platform Package Locally The published `CycloneDDS.NET` and `CycloneDDS.NET.DdsMonitor` packages ship native -assets for **both** `win-x64` and `linux-x64`. The pack step includes whatever -native artifacts are present under `artifacts/native//` (Windows-only entries -are `Exists`-guarded), so a **cross-platform** package is produced by making *both* -platforms' natives available on one pack host. +assets for **both** `win-x64` and `linux-x64`. Every green CI run already produces +these as artifacts, so **you rarely need to build them yourself** — the options +below are ordered easiest first. -A single machine can only compile one platform's native libraries, so bring the -other platform's from CI — the workflow publishes a `native-linux-x64` artifact -(and you can build `win-x64` locally) — or from WSL/Docker. Because the packages -bundle the Windows apphost `.exe` launchers for the build-time tools, **the final -cross-platform pack must run on Windows**. Packing on Linux is fully supported but -yields a `linux-x64`-only package (handy for validating the Linux side). +> **How the cross-platform package is assembled.** The pack step includes whatever +> native artifacts are present under `artifacts/native//` (Windows-only entries +> are `Exists`-guarded), so a cross-platform package needs *both* platforms' natives +> on one pack host. Because the packages bundle the Windows apphost `.exe` launchers +> for the build-time tools, the final cross-platform pack runs on **Windows**. +> Packing on Linux is fully supported but yields a `linux-x64`-only package. -### A. Cross-platform package on a Windows box +CI publishes three artifacts per run: **`nuget-packages`** (the finished +cross-platform `.nupkg` + `.snupkg`), **`native-win-x64`**, and +**`native-linux-x64`**. Download them with the GitHub CLI (`gh`, recommended — +`winget install GitHub.cli` on Windows) or from the Actions run page. + +### Option A — Download the finished package from CI (easiest, no build) + +```bash +# Grab the cross-platform packages from the latest green run on main: +gh run download --repo pjanec/CycloneDds.NET -n nuget-packages -D artifacts/nuget +build/test-package.sh # smoke-test them on this OS (PowerShell: .\build\test-package.ps1) +``` + +Then publish `artifacts/nuget/*.nupkg` manually (see *Publishing* below). Run +`test-package` on both a Windows and a Linux box against the same package before +publishing. + +### Option B — Assemble the package locally without a C++ toolchain + +Download **both** natives from CI and pack — no Visual Studio / CMake needed, just +the .NET 10 SDK: ```powershell -# 1. Build the Windows native, and bring in the Linux native from a green CI run on main -.\build\native-win.ps1 +gh run download --repo pjanec/CycloneDds.NET -n native-win-x64 -D artifacts\native\win-x64 gh run download --repo pjanec/CycloneDds.NET -n native-linux-x64 -D artifacts\native\linux-x64 +dotnet build CycloneDDS.NET.Core.slnf -c Release +dotnet pack src\CycloneDDS.Runtime\CycloneDDS.Runtime.csproj -c Release --no-build -o artifacts\nuget +dotnet pack tools\DdsMonitor\DdsMonitor.Blazor\DdsMonitor.csproj -c Release --no-build -o artifacts\nuget +.\build\test-package.ps1 +``` + +### Option C — Full local build from source -# 2. Build, test and pack -> artifacts\nuget\ (both packages, both platforms' natives) +```powershell +# Windows (needs VS 2022 C++ workload + CMake): builds win native, downloads linux native, packs both +.\build\native-win.ps1 +gh run download --repo pjanec/CycloneDds.NET -n native-linux-x64 -D artifacts\native\linux-x64 .\build\pack.ps1 +.\build\test-package.ps1 ``` -### B. Linux-only package on a Linux box - ```bash +# Linux (needs cmake/gcc/patchelf): produces a linux-x64-only package, enough to validate the Linux side build/native-linux.sh Release dotnet build CycloneDDS.NET.Core.slnf -c Release dotnet pack src/CycloneDDS.Runtime/CycloneDDS.Runtime.csproj -c Release --no-build -o artifacts/nuget dotnet pack tools/DdsMonitor/DdsMonitor.Blazor/DdsMonitor.csproj -c Release --no-build -o artifacts/nuget +build/test-package.sh ``` -### Smoke-testing the packages +### The smoke test (`build/test-package.{sh,ps1}`) -`examples/PackageSmokeTest` consumes `CycloneDDS.NET` as a real NuGet **package** -from the local feed (`artifacts/nuget`, set in its `nuget.config`). It declares a -`[DdsTopic]` (so the packaged code generator + `idlc` run) and does a -publish/subscribe round-trip. Run it against the exact packed version: +Both scripts pick the **newest** package in the feed by write-time (so a cluttered +`artifacts/nuget` with old builds is fine — do **not** name-sort), then: -```bash -# Linux / bash -VER=$(ls artifacts/nuget/CycloneDDS.NET.[0-9]*.nupkg | head -1 | sed -E 's|.*/CycloneDDS\.NET\.(.+)\.nupkg|\1|') -dotnet run --project examples/PackageSmokeTest -c Release -p:SmokePkgVersion=$VER -# Expect: [smoke] PASS: round-trip Id=42 Value=3.14159 Label=hello-cyclonedds -``` +1. Consume `CycloneDDS.NET` as a real NuGet **package** via `examples/PackageSmokeTest` + — which declares a `[DdsTopic]` (so the packaged code generator + `idlc` run at + build time) and does a publish/subscribe round-trip. Expect + `[smoke] PASS: round-trip Id=42 ...`. +2. Install the `ddsmonitor` global tool, confirm it serves HTTP 200 (native loaded), + and uninstall it. -```powershell -# Windows / PowerShell -$pkg = Get-ChildItem artifacts\nuget\CycloneDDS.NET.[0-9]*.nupkg | Select-Object -First 1 -$ver = $pkg.Name -replace '^CycloneDDS\.NET\.(.+)\.nupkg$','$1' -dotnet run --project examples\PackageSmokeTest -c Release -p:SmokePkgVersion=$ver ``` - -Smoke-test the DdsMonitor global tool: - -```bash -dotnet tool install --global --add-source artifacts/nuget --version CycloneDDS.NET.DdsMonitor -ddsmonitor --NoBrowser true # logs "Application started" and serves http://127.0.0.1:/ -dotnet tool uninstall --global CycloneDDS.NET.DdsMonitor +build/test-package.sh [FEED_DIR] # default artifacts/nuget; NO_DDSMON=1 to skip the tool +.\build\test-package.ps1 [-FeedDir ] [-NoDdsMon] ``` -Run the smoke test on **both** a Windows and a Linux box before publishing (copy a -cross-platform package across, or do a per-platform local build on each). Both were -validated during development — Linux locally + in CI, Windows in the CI `build` job. +Run the smoke test on **both** a Windows and a Linux box before publishing. Both +were validated during development — Linux locally + in CI, Windows in the CI +`build` job. ## Version Management diff --git a/build/test-package.ps1 b/build/test-package.ps1 new file mode 100644 index 0000000..45ecbf9 --- /dev/null +++ b/build/test-package.ps1 @@ -0,0 +1,96 @@ +<# +.SYNOPSIS + Smoke-test the CycloneDDS.NET NuGet package (and the DdsMonitor global tool) + from a local feed — whether built locally (build\pack.ps1) or downloaded from a + CI run (the 'nuget-packages' artifact). + +.DESCRIPTION + Picks the NEWEST matching package by LastWriteTime (so a cluttered + artifacts\nuget with many historical builds is fine), then consumes it as a real + PackageReference via examples\PackageSmokeTest: restore -> run the bundled code + generator (idlc) -> publish/subscribe round-trip. Optionally installs the + ddsmonitor tool, confirms it serves HTTP, and uninstalls it. + +.EXAMPLE + .\build\test-package.ps1 + .\build\test-package.ps1 -FeedDir C:\downloads\nuget-packages -NoDdsMon +#> +[CmdletBinding()] +param( + [string]$FeedDir = "artifacts/nuget", + [switch]$NoDdsMon +) + +$ErrorActionPreference = "Stop" +$RepoRoot = Split-Path -Parent $PSScriptRoot +$Feed = (Resolve-Path $FeedDir).Path + +function Get-Newest($pattern) { + Get-ChildItem -Path (Join-Path $Feed $pattern) -ErrorAction SilentlyContinue | + Sort-Object LastWriteTime -Descending | Select-Object -First 1 +} + +$rt = Get-Newest 'CycloneDDS.NET.[0-9]*.nupkg' +if (-not $rt) { throw "No CycloneDDS.NET..nupkg found in $Feed" } +$rtVer = $rt.Name -replace '^CycloneDDS\.NET\.(.+)\.nupkg$', '$1' + +Write-Host "============================================================" +Write-Host " Package smoke test" +Write-Host " feed: $Feed" +Write-Host " package: $($rt.Name) (version $rtVer)" +Write-Host "============================================================" + +Remove-Item -Recurse -Force ` + "$env:USERPROFILE\.nuget\packages\cyclonedds.net", ` + "$env:USERPROFILE\.nuget\packages\cyclonedds.net.ddsmonitor" -ErrorAction SilentlyContinue +Remove-Item -Recurse -Force ` + "$RepoRoot\examples\PackageSmokeTest\bin", ` + "$RepoRoot\examples\PackageSmokeTest\obj" -ErrorAction SilentlyContinue + +Write-Host "`n[1/2] Running examples/PackageSmokeTest against $rtVer ..." +dotnet run --project "$RepoRoot\examples\PackageSmokeTest" -c Release ` + -p:SmokePkgVersion=$rtVer -p:RestoreAdditionalSources=$Feed +if ($LASTEXITCODE -ne 0) { throw "runtime package smoke test FAILED" } +Write-Host " [+] runtime package smoke test PASSED" + +if ($NoDdsMon) { Write-Host "`nSkipping DdsMonitor tool check (-NoDdsMon)."; Write-Host "All good."; exit 0 } + +$mon = Get-Newest 'CycloneDDS.NET.DdsMonitor.*.nupkg' +if (-not $mon) { Write-Host "`nNo DdsMonitor package in feed — skipping tool check."; Write-Host "All good."; exit 0 } +$monVer = $mon.Name -replace '^CycloneDDS\.NET\.DdsMonitor\.(.+)\.nupkg$', '$1' + +Write-Host "`n[2/2] Testing the ddsmonitor global tool $monVer ..." +dotnet tool uninstall --global CycloneDDS.NET.DdsMonitor 2>$null | Out-Null +dotnet tool install --global --add-source $Feed --version $monVer CycloneDDS.NET.DdsMonitor | Out-Null + +$out = [System.IO.Path]::GetTempFileName() +$err = [System.IO.Path]::GetTempFileName() +$proc = Start-Process -FilePath "ddsmonitor" -ArgumentList "--NoBrowser true" ` + -RedirectStandardOutput $out -RedirectStandardError $err -PassThru -NoNewWindow + +$ok = $false; $port = $null +for ($i = 0; $i -lt 30; $i++) { + $txt = (Get-Content $out, $err -Raw -ErrorAction SilentlyContinue) -join "`n" + if ($txt -match 'Now listening on: http://127\.0\.0\.1:(\d+)') { $port = $Matches[1] } + if ($txt -match 'Application started') { $ok = $true; break } + if ($proc.HasExited) { break } + Start-Sleep -Milliseconds 500 +} + +$code = $null +if ($port) { + try { $code = (Invoke-WebRequest -Uri "http://127.0.0.1:$port/" -UseBasicParsing -TimeoutSec 5).StatusCode } catch {} +} + +if (-not $proc.HasExited) { $proc.Kill(); $proc.WaitForExit() } +dotnet tool uninstall --global CycloneDDS.NET.DdsMonitor 2>$null | Out-Null + +if ($ok -and $code -eq 200) { + Write-Host " [+] ddsmonitor started (port $port) and served HTTP 200 — native loaded OK" + Write-Host "`nAll good." + exit 0 +} + +Write-Host " [-] ddsmonitor check FAILED (started=$ok, http=$code). Log tail:" -ForegroundColor Red +Get-Content $out, $err -Tail 20 -ErrorAction SilentlyContinue +exit 1 diff --git a/build/test-package.sh b/build/test-package.sh new file mode 100755 index 0000000..d88f1bc --- /dev/null +++ b/build/test-package.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +set -euo pipefail + +# =================================================================================== +# build/test-package.sh +# +# PURPOSE: +# Smoke-test the CycloneDDS.NET NuGet package (and, optionally, the DdsMonitor +# global tool) from a local feed — whether the packages were built locally +# (build/pack.ps1 or dotnet pack) or downloaded from a CI run (the +# 'nuget-packages' artifact). It always picks the NEWEST matching package by +# modification time, so a cluttered artifacts/nuget with many historical builds +# is fine. +# +# It consumes the package as a real PackageReference via examples/PackageSmokeTest: +# restore -> run the bundled code generator (idlc) -> publish/subscribe round-trip. +# +# USAGE: +# build/test-package.sh [FEED_DIR] # default FEED_DIR = artifacts/nuget +# NO_DDSMON=1 build/test-package.sh # skip the DdsMonitor tool check +# =================================================================================== + +FEED="${1:-artifacts/nuget}" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(dirname "$SCRIPT_DIR")" + +if [ ! -d "$FEED" ]; then + echo "ERROR: feed directory not found: $FEED" >&2 + exit 1 +fi +FEED_ABS="$(cd "$FEED" && pwd)" + +# Newest file matching a glob (by mtime), or empty. +newest() { ls -t "$FEED_ABS"/$1 2>/dev/null | head -n1 || true; } + +RT_PKG="$(newest 'CycloneDDS.NET.[0-9]*.nupkg')" +if [ -z "$RT_PKG" ]; then + echo "ERROR: no CycloneDDS.NET..nupkg found in $FEED_ABS" >&2 + exit 1 +fi +RT_VER="$(basename "$RT_PKG" | sed -E 's/^CycloneDDS\.NET\.(.+)\.nupkg$/\1/')" + +echo "============================================================" +echo " Package smoke test" +echo " feed: $FEED_ABS" +echo " package: $(basename "$RT_PKG") (version $RT_VER)" +echo "============================================================" + +# Force a fresh restore of exactly this build. +rm -rf "$HOME/.nuget/packages/cyclonedds.net" "$HOME/.nuget/packages/cyclonedds.net.ddsmonitor" 2>/dev/null || true +rm -rf "$REPO_ROOT/examples/PackageSmokeTest/bin" "$REPO_ROOT/examples/PackageSmokeTest/obj" + +echo "" +echo "[1/2] Running examples/PackageSmokeTest against $RT_VER ..." +dotnet run --project "$REPO_ROOT/examples/PackageSmokeTest" -c Release \ + -p:SmokePkgVersion="$RT_VER" \ + -p:RestoreAdditionalSources="$FEED_ABS" +echo " [+] runtime package smoke test PASSED" + +if [ "${NO_DDSMON:-0}" = "1" ]; then + echo "" + echo "Skipping DdsMonitor tool check (NO_DDSMON=1)." + echo "All good." + exit 0 +fi + +MON_PKG="$(newest 'CycloneDDS.NET.DdsMonitor.*.nupkg')" +if [ -z "$MON_PKG" ]; then + echo "" + echo "No CycloneDDS.NET.DdsMonitor package in feed — skipping tool check." + echo "All good." + exit 0 +fi +MON_VER="$(basename "$MON_PKG" | sed -E 's/^CycloneDDS\.NET\.DdsMonitor\.(.+)\.nupkg$/\1/')" + +echo "" +echo "[2/2] Testing the ddsmonitor global tool $MON_VER ..." +dotnet tool uninstall --global CycloneDDS.NET.DdsMonitor >/dev/null 2>&1 || true +dotnet tool install --global --add-source "$FEED_ABS" --version "$MON_VER" CycloneDDS.NET.DdsMonitor >/dev/null + +TOOL="$HOME/.dotnet/tools/ddsmonitor" +LOG="$(mktemp)" +"$TOOL" --NoBrowser true >"$LOG" 2>&1 & +TOOL_PID=$! + +ok=0 +for _ in $(seq 1 30); do + if grep -q "Application started" "$LOG" 2>/dev/null; then ok=1; break; fi + if ! kill -0 "$TOOL_PID" 2>/dev/null; then break; fi + sleep 0.5 +done + +PORT="$(grep -oE 'Now listening on: http://127.0.0.1:[0-9]+' "$LOG" | grep -oE '[0-9]+$' | head -n1 || true)" +code="" +if [ -n "$PORT" ]; then + code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:$PORT/" 2>/dev/null || true)" +fi + +kill "$TOOL_PID" 2>/dev/null || true +wait "$TOOL_PID" 2>/dev/null || true +dotnet tool uninstall --global CycloneDDS.NET.DdsMonitor >/dev/null 2>&1 || true + +if [ "$ok" = "1" ] && [ "$code" = "200" ]; then + echo " [+] ddsmonitor started (port $PORT) and served HTTP 200 — native loaded OK" + echo "" + echo "All good." + exit 0 +fi + +echo " [-] ddsmonitor check FAILED (started=$ok, http=$code). Log:" >&2 +tail -n 20 "$LOG" >&2 +exit 1 From 5993a97e74f934217983d385ac5443813810309c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Jul 2026 13:15:21 +0000 Subject: [PATCH 3/5] ci: verify the packaged output on both platforms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Windows build job: after packing, run build/test-package.ps1 to consume the freshly-packed cross-platform package (PackageSmokeTest round-trip + ddsmonitor tool) on Windows. - New linux-verify job: downloads the finished 'nuget-packages' artifact (packed on Windows) and runs build/test-package.sh on Linux — proving the same cross-platform package restores, runs the bundled Linux idlc for code-gen, loads libddsc.so, round-trips, and serves the ddsmonitor tool. No native build. Also invoke ddsmonitor by absolute path in test-package.ps1 so it works right after `dotnet tool install --global` (tools dir may not be on PATH yet in CI). Net effect: every green run publishes a cross-platform package that has been smoke-tested on BOTH Windows and Linux before you download it. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01N7RoW2a2tBB2T8tDbNHB11 --- .github/workflows/ci.yml | 37 +++++++++++++++++++++++++++++++++++++ build/test-package.ps1 | 7 ++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a17acb..14ab895 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -109,6 +109,10 @@ jobs: shell: pwsh run: .\build\pack.ps1 + - name: Smoke-test the packed package (Windows) + shell: pwsh + run: .\build\test-package.ps1 + - name: Upload win-x64 Native Artifacts uses: actions/upload-artifact@v4 if: always() @@ -132,3 +136,36 @@ jobs: name: symbol-packages path: artifacts/nuget/*.snupkg if-no-files-found: warn + + # --------------------------------------------------------------------------- + # Linux: prove the *cross-platform* package (packed on Windows above) actually + # works on Linux. Downloads the finished 'nuget-packages' artifact and consumes + # it as a real NuGet package via build/test-package.sh: restore -> the bundled + # Linux idlc runs code generation -> DllImport resolves libddsc.so -> pub/sub + # round-trip, then the ddsmonitor tool is installed and served. No native build. + # --------------------------------------------------------------------------- + linux-verify: + runs-on: ubuntu-latest + needs: [ build ] + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Required for Nerdbank.GitVersioning (applies to the smoke-test project) + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 8.0.x + 10.0.x + + - name: Download cross-platform packages + uses: actions/download-artifact@v4 + with: + name: nuget-packages + path: artifacts/nuget/ + + - name: Smoke-test the cross-platform package (Linux) + shell: bash + run: build/test-package.sh diff --git a/build/test-package.ps1 b/build/test-package.ps1 index 45ecbf9..64a67bc 100644 --- a/build/test-package.ps1 +++ b/build/test-package.ps1 @@ -63,9 +63,14 @@ Write-Host "`n[2/2] Testing the ddsmonitor global tool $monVer ..." dotnet tool uninstall --global CycloneDDS.NET.DdsMonitor 2>$null | Out-Null dotnet tool install --global --add-source $Feed --version $monVer CycloneDDS.NET.DdsMonitor | Out-Null +# Invoke by absolute path: the global-tools dir may not be on PATH in this session +# (e.g. right after `dotnet tool install --global` in CI). +$toolExe = Join-Path $env:USERPROFILE ".dotnet\tools\ddsmonitor.exe" +if (-not (Test-Path $toolExe)) { $toolExe = "ddsmonitor" } + $out = [System.IO.Path]::GetTempFileName() $err = [System.IO.Path]::GetTempFileName() -$proc = Start-Process -FilePath "ddsmonitor" -ArgumentList "--NoBrowser true" ` +$proc = Start-Process -FilePath $toolExe -ArgumentList "--NoBrowser true" ` -RedirectStandardOutput $out -RedirectStandardError $err -PassThru -NoNewWindow $ok = $false; $port = $null From f19a416b3f7bfebc6c56485cbac6c8a79bda5b88 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Jul 2026 13:33:11 +0000 Subject: [PATCH 4/5] fix: ship SourceLink PDBs for Core and Schema so consumers can debug the whole library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SourceLink was already configured (Microsoft.SourceLink.GitHub + PublishRepositoryUrl + snupkg), and CycloneDDS.Runtime's PDB carried correct raw.githubusercontent URLs. But the package's lib/net8.0 shipped CycloneDDS.Core and CycloneDDS.Schema as DLLs only — their PDBs were never packed (they're manually-added project-reference outputs, so not auto-collected). A consumer could step into Runtime but not Core/Schema. - Runtime.csproj: pack CycloneDDS.Core.pdb and CycloneDDS.Schema.pdb next to their DLLs in lib/net8.0 (Exists-guarded), mirroring the existing Runtime.pdb. These PDBs already carry SourceLink URLs, so all three lib assemblies are now debuggable straight from the .nupkg (no symbol server needed). - Directory.Build.props: set ContinuousIntegrationBuild=true on GitHub Actions so published PDBs are deterministic with normalized source paths. - RELEASE-GUIDE: document the SourceLink/symbols verification and note to publish both .nupkg and .snupkg from a CI build on main/a tag. Verified: repacked locally; lib/net8.0 now has Runtime/Core/Schema .dll + .pdb, and sourcelink print-urls lists GitHub raw URLs for all three PDBs. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01N7RoW2a2tBB2T8tDbNHB11 --- Directory.Build.props | 3 +++ RELEASE-GUIDE.md | 12 ++++++++++++ src/CycloneDDS.Runtime/CycloneDDS.Runtime.csproj | 8 ++++++++ 3 files changed, 23 insertions(+) diff --git a/Directory.Build.props b/Directory.Build.props index c5f4167..6c3f1ce 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -10,6 +10,9 @@ snupkg true true + + true $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb false diff --git a/RELEASE-GUIDE.md b/RELEASE-GUIDE.md index e3156d2..ccc0144 100644 --- a/RELEASE-GUIDE.md +++ b/RELEASE-GUIDE.md @@ -220,8 +220,20 @@ Expand-Archive artifacts/nuget/CycloneDDS.NET.1.0.0.nupkg -DestinationPath temp_ # - ThirdPartyNotices.txt is present # - Native binaries are in runtimes/ folder # - Build tools are in build/ or buildTransitive/ folder +# - lib/net8.0/ has *.dll AND matching *.pdb for Runtime, Core and Schema +# (SourceLink debugging), and a .snupkg was produced alongside the .nupkg ``` +**Debugging support (SourceLink).** The package embeds SourceLink so consumers can +step into the library sources straight from GitHub. Symbols ship two ways: the +`lib/net8.0/*.pdb` travel inside the `.nupkg` (Visual Studio loads them directly), +and a `.snupkg` is published to the NuGet symbol server. Verify with the +[`sourcelink`](https://www.nuget.org/packages/sourcelink) tool, e.g. +`sourcelink print-urls lib\net8.0\CycloneDDS.Runtime.pdb` should list +`https://raw.githubusercontent.com/pjanec/CycloneDds.NET//...` URLs. +Publish **both** the `.nupkg` and the `.snupkg` (see below). SourceLink URLs only +resolve for commits pushed to GitHub, so publish from a CI build on `main`/a tag. + **Key Things to Check:** - [ ] Package version is correct (no `-alpha` suffix for stable) - [ ] `icon.png` is at package root diff --git a/src/CycloneDDS.Runtime/CycloneDDS.Runtime.csproj b/src/CycloneDDS.Runtime/CycloneDDS.Runtime.csproj index db43d60..7c8cce5 100644 --- a/src/CycloneDDS.Runtime/CycloneDDS.Runtime.csproj +++ b/src/CycloneDDS.Runtime/CycloneDDS.Runtime.csproj @@ -83,6 +83,14 @@ + + + From 345f0ddf9e3058f1162abaf1a1747ac8eaed02e5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Jul 2026 13:45:12 +0000 Subject: [PATCH 5/5] ci: auto-publish to NuGet on version tags; document token setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New 'publish' job in ci.yml: on a vX.Y.Z tag, after both the Windows build and the linux-verify job pass, download the packages+symbols artifacts and `dotnet nuget push` them to NuGet.org (--skip-duplicate). Fails fast with a clear message if the NUGET_API_KEY secret is missing. Publishes both CycloneDDS.NET and CycloneDDS.NET.DdsMonitor (and their .snupkg). - version.json (root + DdsMonitor): add ^refs/tags/v\d+\.\d+ to publicReleaseRefSpec so a tag build gets a clean version (no -gSHA prerelease suffix). - RELEASE-GUIDE: document the NUGET_API_KEY repository secret, the tag/release flow, and that the published version is the NBGV-computed version (name the tag to match `nbgv get-version`). - CHANGELOG: add 0.3.2 (SourceLink fix, CI package verification + auto-publish, local test tooling). Note: merging the follow-up PR advances main to 0.3.2 (NBGV patch = git height), so the first published release will be 0.3.2 — which is what carries the SourceLink fix. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01N7RoW2a2tBB2T8tDbNHB11 --- .github/workflows/ci.yml | 49 +++++++++++++++++++++++++++++++++++ CHANGELOG.md | 25 ++++++++++++++++++ RELEASE-GUIDE.md | 39 ++++++++++++++++++++++++---- tools/DdsMonitor/version.json | 3 ++- version.json | 3 ++- 5 files changed, 112 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 14ab895..4fd4047 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -169,3 +169,52 @@ jobs: - name: Smoke-test the cross-platform package (Linux) shell: bash run: build/test-package.sh + + # --------------------------------------------------------------------------- + # Publish to NuGet.org — ONLY on a version tag (vX.Y.Z), and ONLY after the + # cross-platform package has been verified on both Windows (build) and Linux + # (linux-verify). Pushes the packages produced by the build job (both + # CycloneDDS.NET and CycloneDDS.NET.DdsMonitor) plus their symbol packages. + # + # Requires a repository secret named NUGET_API_KEY: + # Repo -> Settings -> Secrets and variables -> Actions -> New repository secret. + # To release: create a GitHub Release with a tag matching the NBGV version of + # the target commit (e.g. v0.3.2 — check with `nbgv get-version` first). + # --------------------------------------------------------------------------- + publish: + runs-on: ubuntu-latest + needs: [ build, linux-verify ] + if: startsWith(github.ref, 'refs/tags/v') + + steps: + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 10.0.x + + - name: Download packages + uses: actions/download-artifact@v4 + with: + name: nuget-packages + path: artifacts/nuget/ + + - name: Download symbol packages + uses: actions/download-artifact@v4 + with: + name: symbol-packages + path: artifacts/nuget/ + + - name: Push to NuGet.org + shell: bash + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + run: | + if [ -z "$NUGET_API_KEY" ]; then + echo "::error::NUGET_API_KEY secret is not set — cannot publish." >&2 + exit 1 + fi + # dotnet expands the wildcard and auto-pushes the matching .snupkg for each .nupkg. + dotnet nuget push "artifacts/nuget/*.nupkg" \ + --api-key "$NUGET_API_KEY" \ + --source https://api.nuget.org/v3/index.json \ + --skip-duplicate diff --git a/CHANGELOG.md b/CHANGELOG.md index 73f4470..79de05a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## unreleased nothing yet +## 0.3.2 + +### Fixed +- **SourceLink symbols for the whole library.** `CycloneDDS.Core` and + `CycloneDDS.Schema` now ship their SourceLink-enabled PDBs in `lib/net8.0` + (previously only `CycloneDDS.Runtime` did), so consumers can step into all three + assemblies straight from the package. + +### Added +- **Automatic NuGet publish on a version tag.** Pushing a `vX.Y.Z` tag (e.g. via a + GitHub Release) builds and verifies the cross-platform package on Windows and + Linux, then publishes it — and the `ddsmonitor` tool — to NuGet.org. Requires the + `NUGET_API_KEY` repository secret. +- **CI now verifies the packaged output on both platforms** before publishing (a + Windows smoke test + a Linux job that consumes the finished package), and uploads + the `win-x64` native as a workflow artifact. +- **`examples/PackageSmokeTest`** and **`build/test-package.{sh,ps1}`** for + smoke-testing the package (and the `ddsmonitor` tool) locally on either OS. + +### Changed +- Packing now succeeds on Linux too (Windows-only pack entries are `Exists`-guarded), + so a per-platform package can be built locally on either OS. +- CI builds set `ContinuousIntegrationBuild` for deterministic PDBs with normalized + SourceLink source paths. + ## 0.3.1 ### Added diff --git a/RELEASE-GUIDE.md b/RELEASE-GUIDE.md index ccc0144..6041cba 100644 --- a/RELEASE-GUIDE.md +++ b/RELEASE-GUIDE.md @@ -245,13 +245,42 @@ resolve for commits pushed to GitHub, so publish from a CI build on `main`/a tag ## Publishing to NuGet.org -### Option A: Automatic Publishing (CI/CD) +### Option A: Automatic Publishing (CI/CD) — recommended -If CI is configured to auto-publish on tags: +CI publishes to NuGet.org automatically when you push a **version tag** (`vX.Y.Z`), +but only after the cross-platform package has passed the Windows and Linux +verification jobs. The `publish` job pushes both `CycloneDDS.NET` and +`CycloneDDS.NET.DdsMonitor` (plus their `.snupkg` symbols). -1. **Wait for CI to complete** after pushing the tag -2. **Verify on NuGet.org** that the new version appears -3. **Create a GitHub Release** with release notes +**One-time setup — store the NuGet API key as a repository secret:** + +1. Create an API key at with **"Push new + packages and package versions"**. Scope the *Package glob* to `CycloneDDS.NET*` + so it covers both packages. (For the very first push of a brand-new package id, + a key scoped to *all* packages / your account may be required until the id + exists.) +2. In GitHub: **Repo → Settings → Secrets and variables → Actions → New repository + secret**. Name it exactly **`NUGET_API_KEY`** and paste the key. GitHub encrypts + it and masks it in logs; the workflow reads it as `secrets.NUGET_API_KEY`. You + never commit the key. + +**Releasing:** + +1. Make sure `main` is green and at the commit you want to ship. +2. Check the version NBGV will produce for that commit: + ```bash + nbgv get-version # e.g. 0.3.2 (dotnet tool install -g nbgv) + ``` + The published version is `version.json` base + git height — the tag name does + **not** set it, so name the tag to match (e.g. `v0.3.2`). +3. **Create a GitHub Release** with tag `vX.Y.Z` targeting that commit (or + `git tag v0.3.2 && git push origin v0.3.2`). The tag push runs CI; on success + the `publish` job pushes to NuGet.org. +4. **Verify on NuGet.org** that the new version (and the DdsMonitor tool) appear. + +> The tag is added to `publicReleaseRefSpec` in `version.json`, so a tag build gets +> a clean version (no `-gSHA` prerelease suffix). SourceLink URLs resolve because +> the tagged commit is on GitHub. ### Option B: Manual Publishing diff --git a/tools/DdsMonitor/version.json b/tools/DdsMonitor/version.json index 8564c5f..cb201cd 100644 --- a/tools/DdsMonitor/version.json +++ b/tools/DdsMonitor/version.json @@ -4,7 +4,8 @@ "publicReleaseRefSpec": [ "^refs/heads/main$", "^refs/heads/master$", - "^refs/heads/v\\d+(?:\\.\\d+)?$" + "^refs/heads/v\\d+(?:\\.\\d+)?$", + "^refs/tags/v\\d+\\.\\d+" ], "cloudBuild": { "buildNumber": { diff --git a/version.json b/version.json index db804ce..c75a435 100644 --- a/version.json +++ b/version.json @@ -4,7 +4,8 @@ "publicReleaseRefSpec": [ "^refs/heads/main$", "^refs/heads/master$", - "^refs/heads/v\\d+(?:\\.\\d+)?$" + "^refs/heads/v\\d+(?:\\.\\d+)?$", + "^refs/tags/v\\d+\\.\\d+" ], "cloudBuild": { "buildNumber": {