From 6ffa740b14a07c7640328919d822202ea5a2011d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Tue, 17 Feb 2026 15:33:38 +0100 Subject: [PATCH 01/18] feat(dotnet): Add initial .NET project structure with Native AOT Set up .NET 10 project with console application and test framework. Includes Native AOT compilation support for improved performance and central package management with TUnit for testing. Co-Authored-By: Claude --- src/dotnet/.gitignore | 482 ++++++++++++++++++ src/dotnet/Directory.Build.props | 13 + src/dotnet/Directory.Build.targets | 2 + src/dotnet/Directory.Packages.props | 13 + src/dotnet/Sentry.Cli.Tests/MyTests.cs | 10 + .../Sentry.Cli.Tests/Sentry.Cli.Tests.csproj | 17 + src/dotnet/Sentry.Cli.slnx | 4 + src/dotnet/Sentry.Cli/Program.cs | 1 + src/dotnet/Sentry.Cli/Sentry.Cli.csproj | 13 + src/dotnet/global.json | 9 + 10 files changed, 564 insertions(+) create mode 100644 src/dotnet/.gitignore create mode 100644 src/dotnet/Directory.Build.props create mode 100644 src/dotnet/Directory.Build.targets create mode 100644 src/dotnet/Directory.Packages.props create mode 100644 src/dotnet/Sentry.Cli.Tests/MyTests.cs create mode 100644 src/dotnet/Sentry.Cli.Tests/Sentry.Cli.Tests.csproj create mode 100644 src/dotnet/Sentry.Cli.slnx create mode 100644 src/dotnet/Sentry.Cli/Program.cs create mode 100644 src/dotnet/Sentry.Cli/Sentry.Cli.csproj create mode 100644 src/dotnet/global.json diff --git a/src/dotnet/.gitignore b/src/dotnet/.gitignore new file mode 100644 index 00000000..0808c4ad --- /dev/null +++ b/src/dotnet/.gitignore @@ -0,0 +1,482 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from `dotnet new gitignore` + +# dotenv files +.env + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +# but not Directory.Build.rsp, as it configures directory-level build defaults +!Directory.Build.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +.idea/ + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Vim temporary swap files +*.swp diff --git a/src/dotnet/Directory.Build.props b/src/dotnet/Directory.Build.props new file mode 100644 index 00000000..b2546821 --- /dev/null +++ b/src/dotnet/Directory.Build.props @@ -0,0 +1,13 @@ + + + + enable + enable + + + + true + $(MSBuildThisFileDirectory)artifacts + + + diff --git a/src/dotnet/Directory.Build.targets b/src/dotnet/Directory.Build.targets new file mode 100644 index 00000000..8c119d54 --- /dev/null +++ b/src/dotnet/Directory.Build.targets @@ -0,0 +1,2 @@ + + diff --git a/src/dotnet/Directory.Packages.props b/src/dotnet/Directory.Packages.props new file mode 100644 index 00000000..19be8937 --- /dev/null +++ b/src/dotnet/Directory.Packages.props @@ -0,0 +1,13 @@ + + + + true + false + false + + + + + + + diff --git a/src/dotnet/Sentry.Cli.Tests/MyTests.cs b/src/dotnet/Sentry.Cli.Tests/MyTests.cs new file mode 100644 index 00000000..5c85e224 --- /dev/null +++ b/src/dotnet/Sentry.Cli.Tests/MyTests.cs @@ -0,0 +1,10 @@ +namespace Sentry.Cli.Tests; + +public class MyTests +{ + [Test] + public async Task MyTest() + { + await Assert.That(true).IsTrue(); + } +} diff --git a/src/dotnet/Sentry.Cli.Tests/Sentry.Cli.Tests.csproj b/src/dotnet/Sentry.Cli.Tests/Sentry.Cli.Tests.csproj new file mode 100644 index 00000000..09c5022e --- /dev/null +++ b/src/dotnet/Sentry.Cli.Tests/Sentry.Cli.Tests.csproj @@ -0,0 +1,17 @@ + + + + Exe + net10.0 + + + + true + true + + + + + + + diff --git a/src/dotnet/Sentry.Cli.slnx b/src/dotnet/Sentry.Cli.slnx new file mode 100644 index 00000000..2a7974d9 --- /dev/null +++ b/src/dotnet/Sentry.Cli.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/src/dotnet/Sentry.Cli/Program.cs b/src/dotnet/Sentry.Cli/Program.cs new file mode 100644 index 00000000..1bc52a60 --- /dev/null +++ b/src/dotnet/Sentry.Cli/Program.cs @@ -0,0 +1 @@ +Console.WriteLine("Hello, World!"); diff --git a/src/dotnet/Sentry.Cli/Sentry.Cli.csproj b/src/dotnet/Sentry.Cli/Sentry.Cli.csproj new file mode 100644 index 00000000..f87a89ba --- /dev/null +++ b/src/dotnet/Sentry.Cli/Sentry.Cli.csproj @@ -0,0 +1,13 @@ + + + + Exe + net10.0 + + + + true + true + + + diff --git a/src/dotnet/global.json b/src/dotnet/global.json new file mode 100644 index 00000000..1d364c6a --- /dev/null +++ b/src/dotnet/global.json @@ -0,0 +1,9 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature" + }, + "test": { + "runner": "Microsoft.Testing.Platform" + } +} From 385cff7835b5174bd5bc5cf77fa9e7798a596f01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Tue, 17 Feb 2026 15:43:46 +0100 Subject: [PATCH 02/18] build: add nuget.config --- src/dotnet/nuget.config | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/dotnet/nuget.config diff --git a/src/dotnet/nuget.config b/src/dotnet/nuget.config new file mode 100644 index 00000000..fbcef101 --- /dev/null +++ b/src/dotnet/nuget.config @@ -0,0 +1,7 @@ + + + + + + + From 5ced23bf0544e45f1bc252ec2568cf2d639b63ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:32:28 +0100 Subject: [PATCH 03/18] feat(dotnet): Add NuGet package properties for global tool Configure Sentry.Cli as a .NET global tool with all required and recommended NuGet package metadata. The package will be published as "dotnet-sentry" and installed via "dotnet tool install -g dotnet-sentry". Changes include: - Set IsPackable to false by default in Directory.Build.props - Override IsPackable to true for Sentry.Cli project - Configure PackAsTool with command name "sentry" - Add package metadata (ID, version, authors, description, etc.) - Include LICENSE.md, README.md, and package icon in NuGet package Fixes GH-255 Co-Authored-By: Claude Sonnet 4.5 --- src/dotnet/Directory.Build.props | 4 ++++ src/dotnet/Sentry.Cli/Sentry.Cli.csproj | 28 ++++++++++++++++++++++++ src/dotnet/sentry-nuget.png | Bin 0 -> 71837 bytes 3 files changed, 32 insertions(+) create mode 100644 src/dotnet/sentry-nuget.png diff --git a/src/dotnet/Directory.Build.props b/src/dotnet/Directory.Build.props index b2546821..44d52745 100644 --- a/src/dotnet/Directory.Build.props +++ b/src/dotnet/Directory.Build.props @@ -5,6 +5,10 @@ enable + + false + + true $(MSBuildThisFileDirectory)artifacts diff --git a/src/dotnet/Sentry.Cli/Sentry.Cli.csproj b/src/dotnet/Sentry.Cli/Sentry.Cli.csproj index f87a89ba..05265f70 100644 --- a/src/dotnet/Sentry.Cli/Sentry.Cli.csproj +++ b/src/dotnet/Sentry.Cli/Sentry.Cli.csproj @@ -10,4 +10,32 @@ true + + true + true + sentry + + + + dotnet-sentry + 0.1.0 + Sentry Team and Contributors + The command-line interface for Sentry. Built for developers and AI agents. + Copyright 2025 Functional Software, Inc. dba Sentry + LICENSE.md + https://cli.sentry.dev/ + icon.png + README.md + sentry;cli + https://github.com/getsentry/cli/blob/main/CHANGELOG.md + https://github.com/getsentry/cli + git + + + + + + + + diff --git a/src/dotnet/sentry-nuget.png b/src/dotnet/sentry-nuget.png new file mode 100644 index 0000000000000000000000000000000000000000..6a4f4415a2149a4388a019b549cc641c6f24766b GIT binary patch literal 71837 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 q3y2>SGJ~SsQ#f^EiB=M*};>_$$x8W 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

lC2sk?KK!nS1?8UQ^JU-q%yj)N=apb;4!8QH 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 D3YuuR literal 0 HcmV?d00001 From 6f8447339d644077c7b01ddfc65c16bc4030da7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:45:54 +0100 Subject: [PATCH 04/18] feat(dotnet): Add platform-specific RIDs for .NET tool packaging Add ToolPackageRuntimeIdentifiers to support platform-specific .NET tool packages for osx-arm64, osx-x64, linux-arm64, linux-x64, win-x64, and any. This enables Native AOT compilation for each supported platform using .NET 10's DotNetCliTool Version 2. Also add Deterministic and CI build properties to Directory.Build.props for reproducible builds in GitHub Actions. Refs #255 Co-Authored-By: Claude Sonnet 4.5 --- src/dotnet/Directory.Build.props | 6 ++++++ src/dotnet/Sentry.Cli/Sentry.Cli.csproj | 1 + 2 files changed, 7 insertions(+) diff --git a/src/dotnet/Directory.Build.props b/src/dotnet/Directory.Build.props index 44d52745..1a0f8e43 100644 --- a/src/dotnet/Directory.Build.props +++ b/src/dotnet/Directory.Build.props @@ -7,6 +7,7 @@ false + true @@ -14,4 +15,9 @@ $(MSBuildThisFileDirectory)artifacts + + true + $(DefineConstants);CI_BUILD + + diff --git a/src/dotnet/Sentry.Cli/Sentry.Cli.csproj b/src/dotnet/Sentry.Cli/Sentry.Cli.csproj index 05265f70..4dffa5c9 100644 --- a/src/dotnet/Sentry.Cli/Sentry.Cli.csproj +++ b/src/dotnet/Sentry.Cli/Sentry.Cli.csproj @@ -14,6 +14,7 @@ true true sentry + osx-arm64;osx-x64;linux-arm64;linux-x64;win-x64;any From db9bf55e0be1ae7d16fabb71afc632d7fae7bc02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 19 Feb 2026 00:24:58 +0100 Subject: [PATCH 05/18] feat(dotnet): Implement platform-specific NuGet tool launcher Program.cs now dispatches to the correct native Sentry CLI binary based on the target platform/architecture using compile-time constants. The .csproj is updated to define per-RID constants and pack the native binary alongside the .NET tool for each supported RID. Also adds a pack.ts script for building NuGet packages and updates .gitignore and package.json accordingly. Co-Authored-By: Claude --- .gitignore | 4 + package.json | 2 + script/pack.ts | 241 ++++++++++++++++++++++++ src/dotnet/Directory.Build.props | 5 + src/dotnet/Sentry.Cli/Program.cs | 67 ++++++- src/dotnet/Sentry.Cli/Sentry.Cli.csproj | 37 +++- 6 files changed, 354 insertions(+), 2 deletions(-) create mode 100644 script/pack.ts diff --git a/.gitignore b/.gitignore index e9259aae..6e0927d5 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,7 @@ docs/.astro # Finder (MacOS) folder config .DS_Store + +# .NET & NuGet +dist-pkg +dotnet-tools.json diff --git a/package.json b/package.json index 17be1a23..e0a916e9 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,8 @@ "build": "bun run script/build.ts --single", "build:all": "bun run script/build.ts", "bundle": "bun run script/bundle.ts", + "pack": "bun run script/pack.ts --single", + "pack:all": "bun run script/pack.ts", "typecheck": "tsc --noEmit", "lint": "bunx ultracite check", "lint:fix": "bunx ultracite fix", diff --git a/script/pack.ts b/script/pack.ts new file mode 100644 index 00000000..f562871b --- /dev/null +++ b/script/pack.ts @@ -0,0 +1,241 @@ +#!/usr/bin/env bun + +/** + * Pack script for Sentry CLI NuGet packages + * + * Creates platform-specific NuGet packages with embedded native binaries, + * as well as the required top-level pointer package and the RID-agnostic package as fallback. + * The .NET equivalent of build.ts — same target model, same CLI flags, but dependent on it's output at "dist-bin/". + * + * Usage: + * bun run script/pack.ts # Pack for all platforms + * bun run script/pack.ts --single # Pack for current platform only + * bun run script/pack.ts --target darwin-x64 # Pack for specific target (cross-compile) + * + * Output: + * dist-pkg/ + * dotnet-sentry..nupkg # Root package (pointer, no RID) + * dotnet-sentry.any..nupkg # Framework-dependent, RID-agnostic package as fallback + * dotnet-sentry.osx-arm64..nupkg # RID-specific package for macOS ARM64 + * dotnet-sentry.osx-x64..nupkg # RID-specific package for macOS x64 + * dotnet-sentry.linux-arm64..nupkg # RID-specific package for Linux ARM64 + * dotnet-sentry.linux-x64..nupkg # RID-specific package for Linux x64 + * dotnet-sentry.win-x64..nupkg # RID-specific package for Windows x64 + */ + +import { $ } from "bun"; +import pkg from "../package.json"; + +const PROJECT_DIR = "src/dotnet/Sentry.Cli"; +const DIST_BIN_DIR = "dist-bin"; +const DIST_PKG_DIR = "dist-pkg"; +const PACKAGE_ID = "dotnet-sentry"; + +/** Compute the expected .nupkg output path for a given RID (omit for root package) */ +function getNupkgPath(version: string, rid?: string): string { + const prefix = rid ? `.${rid}` : ""; + return `${DIST_PKG_DIR}/${PACKAGE_ID}${prefix}.${version}.nupkg`; +} + +/** Pack targets configuration */ +type PackTarget = { + os: "darwin" | "linux" | "win32"; + arch: "arm64" | "x64"; +}; + +const ALL_TARGETS: PackTarget[] = [ + { os: "darwin", arch: "arm64" }, + { os: "darwin", arch: "x64" }, + { os: "linux", arch: "arm64" }, + { os: "linux", arch: "x64" }, + { os: "win32", arch: "x64" }, +]; + +/** Get package name for a target (uses "windows" instead of "win32") */ +function getPackageName(target: PackTarget): string { + const platformName = target.os === "win32" ? "windows" : target.os; + return `sentry-${platformName}-${target.arch}`; +} + +/** Get binary file name for a target */ +function getBinaryName(target: PackTarget): string { + const extension = target.os === "win32" ? ".exe" : ""; + return `${getPackageName(target)}${extension}`; +} + +/** Get .NET Runtime Identifier for a target */ +function getDotnetRid(target: PackTarget): string { + if (target.os === "darwin") return `osx-${target.arch}`; + if (target.os === "win32") return `win-${target.arch}`; + return `${target.os}-${target.arch}`; +} + +/** Parse target string (e.g., "darwin-x64" or "linux-arm64") into PackTarget */ +function parseTarget(targetStr: string): PackTarget | null { + // Handle "windows" alias for "win32" + const normalized = targetStr.replace("windows-", "win32-"); + const [os, arch] = normalized.split("-") as [ + PackTarget["os"], + PackTarget["arch"], + ]; + + const target = ALL_TARGETS.find((t) => t.os === os && t.arch === arch); + return target ?? null; +} + +/** Pack a platform-specific NuGet package with embedded native binary */ +async function packTarget(target: PackTarget, version: string): Promise { + const rid = getDotnetRid(target); + const packageName = getPackageName(target); + const outfile = getNupkgPath(version, rid); + console.log(` Packing ${packageName} (${rid})...`); + + try { + await $`dotnet pack ${PROJECT_DIR} -c Release -r ${rid} -p:PackageVersion=${version} -p:PublishAot=true`.quiet(); + console.log(` -> ${outfile}`); + return true; + } catch (error) { + console.error(` Failed to pack ${packageName}:`); + console.error(error); + return false; + } +} + +/** Pack the "any" (framework-dependent, CoreCLR) package */ +async function packAny(version: string): Promise { + const outfile = getNupkgPath(version, "any"); + console.log(` Packing any (framework-dependent)...`); + + try { + await $`dotnet pack ${PROJECT_DIR} -c Release -r any -p:PackageVersion=${version} -p:PublishAot=false`.quiet(); + console.log(` -> ${outfile}`); + return true; + } catch (error) { + console.error(` Failed to pack any:`); + console.error(error); + return false; + } +} + +/** Pack the root package (no RID — pointer/manifest package) */ +async function packRoot(version: string): Promise { + const outfile = getNupkgPath(version); + console.log(` Packing root (no RID)...`); + + try { + await $`dotnet pack ${PROJECT_DIR} -c Release -p:PackageVersion=${version} -p:PublishAot=true`.quiet(); + console.log(` -> ${outfile}`); + return true; + } catch (error) { + console.error(` Failed to pack root:`); + console.error(error); + return false; + } +} + +/** Main pack function */ +async function pack(): Promise { + const args = process.argv.slice(2); + const singlePack = args.includes("--single"); + const targetIndex = args.indexOf("--target"); + const targetArg = targetIndex !== -1 ? args[targetIndex + 1] : null; + + console.log(`\nSentry CLI NuGet Pack v${pkg.version}`); + console.log("=".repeat(40)); + + // Determine targets + let targets: PackTarget[]; + + if (targetArg) { + // Explicit target specified (for cross-compilation) + const target = parseTarget(targetArg); + if (!target) { + console.error(`Invalid target: ${targetArg}`); + console.error( + `Valid targets: ${ALL_TARGETS.map((t) => `${t.os === "win32" ? "windows" : t.os}-${t.arch}`).join(", ")}` + ); + process.exit(1); + } + targets = [target]; + console.log(`\nPacking for target: ${getPackageName(target)}`); + } else if (singlePack) { + const currentTarget = ALL_TARGETS.find( + (t) => t.os === process.platform && t.arch === process.arch + ); + if (!currentTarget) { + console.error( + `Unsupported platform: ${process.platform}-${process.arch}` + ); + process.exit(1); + } + targets = [currentTarget]; + console.log( + `\nPacking for current platform: ${getPackageName(currentTarget)}` + ); + } else { + targets = ALL_TARGETS; + console.log(`\nPacking for ${targets.length} targets`); + } + + // Verify native binaries for selected targets + console.log("\nVerifying native binaries..."); + let binaryMissing = false; + for (const target of targets) { + const binaryName = getBinaryName(target); + const binaryPath = `${DIST_BIN_DIR}/${binaryName}`; + const exists = await Bun.file(binaryPath).exists(); + if (exists) { + console.log(` ✓ ${binaryPath}`); + } else { + console.error(` ✗ ${binaryPath} not found`); + binaryMissing = true; + } + } + if (binaryMissing) { + console.error("\nError: Some native binaries are missing."); + console.error("Run 'bun run build:all' first to generate all binaries."); + process.exit(1); + } + + // Clean output directory + await $`rm -rf ${DIST_PKG_DIR}`.quiet(); + + console.log(""); + + // Build all packages + let successCount = 0; + let failCount = 0; + + // Root package (no RID) — always included, even for --single / --target + if (await packRoot(pkg.version)) { + successCount += 1; + } else { + failCount += 1; + } + + // "any" package (framework-dependent fallback) — always included, even for --single / --target + if (await packAny(pkg.version)) { + successCount += 1; + } else { + failCount += 1; + } + + // Platform-specific packages + for (const target of targets) { + if (await packTarget(target, pkg.version)) { + successCount += 1; + } else { + failCount += 1; + } + } + + // Summary + console.log(`\n${"=".repeat(40)}`); + console.log(`Pack complete: ${successCount} succeeded, ${failCount} failed`); + + if (failCount > 0) { + process.exit(1); + } +} + +await pack(); diff --git a/src/dotnet/Directory.Build.props b/src/dotnet/Directory.Build.props index 1a0f8e43..abcf8e4e 100644 --- a/src/dotnet/Directory.Build.props +++ b/src/dotnet/Directory.Build.props @@ -20,4 +20,9 @@ $(DefineConstants);CI_BUILD + + + + + diff --git a/src/dotnet/Sentry.Cli/Program.cs b/src/dotnet/Sentry.Cli/Program.cs index 1bc52a60..8b6bd6f8 100644 --- a/src/dotnet/Sentry.Cli/Program.cs +++ b/src/dotnet/Sentry.Cli/Program.cs @@ -1 +1,66 @@ -Console.WriteLine("Hello, World!"); +#if _PLATFORM_SPECIFIC +return RunNativeExecutable(args); +#else +return RunPlatformAgnostic(args); +#endif + +#if _PLATFORM_SPECIFIC +static int RunNativeExecutable(string[] args) +{ + string exeName = GetNativeExecutableName(); + string exePath = Path.Combine(AppContext.BaseDirectory, exeName); + return StartExecutable(exePath, args); +} + +static string GetNativeExecutableName() +{ +#if _PLATFORM_LINUX_ARM64 + Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)); + Debug.Assert(RuntimeInformation.OSArchitecture == Architecture.Arm64); + return "sentry-linux-arm64"; +#elif _PLATFORM_LINUX_X64 + Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)); + Debug.Assert(RuntimeInformation.OSArchitecture == Architecture.X64); + return "sentry-linux-x64"; +#elif _PLATFORM_OSX_ARM64 + Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.OSX)); + Debug.Assert(RuntimeInformation.OSArchitecture == Architecture.Arm64); + return "sentry-darwin-arm64"; +#elif _PLATFORM_OSX_X64 + Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.OSX)); + Debug.Assert(RuntimeInformation.OSArchitecture == Architecture.X64); + return "sentry-darwin-x64"; +#elif _PLATFORM_WIN_X64 + Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + Debug.Assert(RuntimeInformation.OSArchitecture == Architecture.X64); + return "sentry-windows-x64.exe"; +#else + throw new PlatformNotSupportedException($"Unsupported platform: {RuntimeInformation.OSDescription} ({RuntimeInformation.OSArchitecture})"); +#error Platform not defined. +#endif +} + +static int StartExecutable(string fileName, string[] args) +{ + ProcessStartInfo startInfo = new(fileName, args) + { + CreateNoWindow = true, + UseShellExecute = false, + }; + + var process = Process.Start(startInfo); + if (process is null) + { + throw new InvalidOperationException("Sentry CLI could not be started."); + } + + process.WaitForExit(); + return process.ExitCode; +} +#else +static int RunPlatformAgnostic(string[] args) +{ + Console.Error.WriteLine($"Unsupported platform: {RuntimeInformation.OSDescription} ({RuntimeInformation.OSArchitecture})"); + return 1; +} +#endif diff --git a/src/dotnet/Sentry.Cli/Sentry.Cli.csproj b/src/dotnet/Sentry.Cli/Sentry.Cli.csproj index 4dffa5c9..672daa24 100644 --- a/src/dotnet/Sentry.Cli/Sentry.Cli.csproj +++ b/src/dotnet/Sentry.Cli/Sentry.Cli.csproj @@ -14,7 +14,8 @@ true true sentry - osx-arm64;osx-x64;linux-arm64;linux-x64;win-x64;any + any;linux-arm64;linux-x64;osx-arm64;osx-x64;win-x64 + $(MSBuildThisFileDirectory)../../../dist-pkg @@ -33,10 +34,44 @@ git + + $(MSBuildThisFileDirectory)../../../dist-bin/ + + + + $(DefineConstants);_PLATFORM_SPECIFIC;_PLATFORM_LINUX_ARM64 + $(SelfContainedPlatformSpecificSentryCliDirectory)sentry-linux-arm64 + + + + $(DefineConstants);_PLATFORM_SPECIFIC;_PLATFORM_LINUX_X64 + $(SelfContainedPlatformSpecificSentryCliDirectory)sentry-linux-x64 + + + + $(DefineConstants);_PLATFORM_SPECIFIC;_PLATFORM_OSX_ARM64 + $(SelfContainedPlatformSpecificSentryCliDirectory)sentry-darwin-arm64 + + + + $(DefineConstants);_PLATFORM_SPECIFIC;_PLATFORM_OSX_X64 + $(SelfContainedPlatformSpecificSentryCliDirectory)sentry-darwin-x64 + + + + $(DefineConstants);_PLATFORM_SPECIFIC;_PLATFORM_WIN_X64 + $(SelfContainedPlatformSpecificSentryCliDirectory)sentry-windows-x64.exe + + + + + + + From a2c4cf07aa7723db80939ec970aae4302fead8a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 19 Feb 2026 00:43:50 +0100 Subject: [PATCH 06/18] feat(dotnet): more optimizations for NativeAOT builds --- src/dotnet/Sentry.Cli/Sentry.Cli.csproj | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/dotnet/Sentry.Cli/Sentry.Cli.csproj b/src/dotnet/Sentry.Cli/Sentry.Cli.csproj index 672daa24..ffc66a37 100644 --- a/src/dotnet/Sentry.Cli/Sentry.Cli.csproj +++ b/src/dotnet/Sentry.Cli/Sentry.Cli.csproj @@ -7,7 +7,6 @@ true - true @@ -34,6 +33,13 @@ git + + + true + Speed + true + + $(MSBuildThisFileDirectory)../../../dist-bin/ From a374ae413629f8c0dcbd93897b6b3cb50cde751a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 19 Feb 2026 03:38:05 +0100 Subject: [PATCH 07/18] feat(dotnet): Add TUnit integration tests for the NuGet launcher Tests cover both the framework-dependent fallback (expects exit code 1 and an unsupported-platform error) and the platform-specific happy path (publishes with the current RID, copies the native binary, and asserts the version output). Includes test helpers: DotnetProject, PathUtilities, PlatformUtilities, ProcessResult, and JsonUtilities. Co-Authored-By: Claude Sonnet 4.6 --- src/dotnet/Sentry.Cli.Tests/DotnetProject.cs | 67 +++++++++++++++ src/dotnet/Sentry.Cli.Tests/JsonUtilities.cs | 15 ++++ src/dotnet/Sentry.Cli.Tests/LauncherTests.cs | 39 +++++++++ src/dotnet/Sentry.Cli.Tests/MyTests.cs | 10 --- src/dotnet/Sentry.Cli.Tests/PathUtilities.cs | 85 +++++++++++++++++++ .../Sentry.Cli.Tests/PlatformUtilities.cs | 23 +++++ src/dotnet/Sentry.Cli.Tests/ProcessResult.cs | 46 ++++++++++ 7 files changed, 275 insertions(+), 10 deletions(-) create mode 100644 src/dotnet/Sentry.Cli.Tests/DotnetProject.cs create mode 100644 src/dotnet/Sentry.Cli.Tests/JsonUtilities.cs create mode 100644 src/dotnet/Sentry.Cli.Tests/LauncherTests.cs delete mode 100644 src/dotnet/Sentry.Cli.Tests/MyTests.cs create mode 100644 src/dotnet/Sentry.Cli.Tests/PathUtilities.cs create mode 100644 src/dotnet/Sentry.Cli.Tests/PlatformUtilities.cs create mode 100644 src/dotnet/Sentry.Cli.Tests/ProcessResult.cs diff --git a/src/dotnet/Sentry.Cli.Tests/DotnetProject.cs b/src/dotnet/Sentry.Cli.Tests/DotnetProject.cs new file mode 100644 index 00000000..56126021 --- /dev/null +++ b/src/dotnet/Sentry.Cli.Tests/DotnetProject.cs @@ -0,0 +1,67 @@ +namespace Sentry.Cli.Tests; + +internal sealed class DotnetProject +{ + private readonly FileInfo _project; + private readonly string _configuration; + + public DotnetProject(string projectPath) + : this(new FileInfo(projectPath)) + { + } + + public DotnetProject(FileInfo project) + { + _project = project; + +#if DEBUG + _configuration = "Debug"; +#else + _configuration = "Release"; +#endif + } + + public Task RunAsync() + { + return ExecAsync("dotnet", ["run", + "--project", _project.FullName, + "--configuration", _configuration]); + } + + public Task PublishAsync(string rid, string outputDirectory) + { + return ExecAsync("dotnet", ["publish", _project.FullName, + "--configuration", _configuration, + "--runtime", rid, + "--output", outputDirectory, + "--property:PublishAot=true"]); + } + + public static Task ExecAsync(string fileName) + { + return ExecAsync(fileName, []); + } + + public static async Task ExecAsync(string fileName, ICollection arguments) + { + ProcessStartInfo startInfo = new(fileName, arguments) + { + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + + using var process = Process.Start(startInfo); + + await Assert.That(process).IsNotNull(); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + await process.WaitForExitAsync(cts.Token); + + var stdout = await process.StandardOutput.ReadToEndAsync(CancellationToken.None); + var stderr = await process.StandardError.ReadToEndAsync(CancellationToken.None); + + return new ProcessResult(process.ExitCode, stdout.Trim(), stderr.Trim()); + } +} diff --git a/src/dotnet/Sentry.Cli.Tests/JsonUtilities.cs b/src/dotnet/Sentry.Cli.Tests/JsonUtilities.cs new file mode 100644 index 00000000..71204af7 --- /dev/null +++ b/src/dotnet/Sentry.Cli.Tests/JsonUtilities.cs @@ -0,0 +1,15 @@ +using System.Text.Json; + +namespace Sentry.Cli.Tests; + +internal static class JsonUtilities +{ + internal static async Task GetVersionAsync(FileInfo packageJson) + { + await using var stream = File.OpenRead(packageJson.FullName); + using var document = await JsonDocument.ParseAsync(stream); + + var version = document.RootElement.GetProperty("version"); + return version.GetString()!; + } +} diff --git a/src/dotnet/Sentry.Cli.Tests/LauncherTests.cs b/src/dotnet/Sentry.Cli.Tests/LauncherTests.cs new file mode 100644 index 00000000..1f602fcf --- /dev/null +++ b/src/dotnet/Sentry.Cli.Tests/LauncherTests.cs @@ -0,0 +1,39 @@ +namespace Sentry.Cli.Tests; + +[NotInParallel] +public class LauncherTests +{ + [Test] + public async Task Launch_FrameworkDependent_HasNoSentryCli() + { + var project = PathUtilities.LauncherProject; + + var result = await project.RunAsync(); + + await result.AssertFailureAsync(); + await result.AssertErrorAsync($"Unsupported platform: {RuntimeInformation.OSDescription} ({RuntimeInformation.OSArchitecture})"); + } + + [Test] + public async Task Launch_PlatformSpecific_HasSentryCli() + { + var project = PathUtilities.LauncherProject; + var artifacts = PathUtilities.ArtifactsDirectory; + + var output = Path.Combine(artifacts.FullName, "test"); + var result = await project.PublishAsync(RuntimeInformation.RuntimeIdentifier, output); + await result.AssertSuccessAsync(); + + // copy from dist-bin to test artifacts + var sourceFileName = Path.Combine(PathUtilities.BinaryDirectory.FullName, PlatformUtilities.GetNativeExecutableName()); + var destFileName = Path.Combine(output, PlatformUtilities.GetNativeExecutableName()); + File.Copy(sourceFileName, destFileName, true); + + var executable = Path.Combine(output, "Sentry.Cli"); + var exec = await DotnetProject.ExecAsync(executable, ["--version"]); + + var version = await JsonUtilities.GetVersionAsync(PathUtilities.PackageFile); + await exec.AssertSuccessAsync(); + await exec.AssertOutputAsync(version); + } +} diff --git a/src/dotnet/Sentry.Cli.Tests/MyTests.cs b/src/dotnet/Sentry.Cli.Tests/MyTests.cs deleted file mode 100644 index 5c85e224..00000000 --- a/src/dotnet/Sentry.Cli.Tests/MyTests.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Sentry.Cli.Tests; - -public class MyTests -{ - [Test] - public async Task MyTest() - { - await Assert.That(true).IsTrue(); - } -} diff --git a/src/dotnet/Sentry.Cli.Tests/PathUtilities.cs b/src/dotnet/Sentry.Cli.Tests/PathUtilities.cs new file mode 100644 index 00000000..fbf9971f --- /dev/null +++ b/src/dotnet/Sentry.Cli.Tests/PathUtilities.cs @@ -0,0 +1,85 @@ +using System.Runtime.CompilerServices; + +namespace Sentry.Cli.Tests; + +internal static class PathUtilities +{ + private static readonly Lazy s_testProjectDirectory = new(() => GetTestProjectDirectory()); + private static readonly Lazy s_launcherProject = new(GetLauncherProject); + private static readonly Lazy s_artifactsDirectory = new(GetArtifactsDirectory); + private static readonly Lazy s_binaryDirectory = new(GetBinaryDirectory); + private static readonly Lazy s_packageFile = new(GetPackageFile); + + internal static DotnetProject LauncherProject => s_launcherProject.Value; + internal static DirectoryInfo ArtifactsDirectory => s_artifactsDirectory.Value; + internal static DirectoryInfo BinaryDirectory => s_binaryDirectory.Value; + internal static FileInfo PackageFile => s_packageFile.Value; + + private static DirectoryInfo GetTestProjectDirectory([CallerFilePath] string? sourceFilePath = null) + { + var testProjectPath = Path.GetDirectoryName(sourceFilePath); + Assert.NotNull(testProjectPath); + + FileInfo testProject = new(Path.Combine(testProjectPath, "Sentry.Cli.Tests.csproj")); + + if (!testProject.Exists) + { + Assert.Fail("Test project not found."); + } + + Assert.NotNull(testProject.Directory); + return testProject.Directory; + } + + private static DotnetProject GetLauncherProject() + { + var testProjectDirectory = s_testProjectDirectory.Value; + FileInfo project = new(Path.Combine(testProjectDirectory.FullName, "../Sentry.Cli/Sentry.Cli.csproj")); + + if (!project.Exists) + { + Assert.Fail("Launcher project not found."); + } + + return new DotnetProject(project); + } + + private static DirectoryInfo GetArtifactsDirectory() + { + var testProjectDirectory = s_testProjectDirectory.Value; + DirectoryInfo artifacts = new(Path.Combine(testProjectDirectory.FullName, "../artifacts")); + + if (!artifacts.Exists) + { + Assert.Fail("Artifacts path not found."); + } + + return artifacts; + } + + private static DirectoryInfo GetBinaryDirectory() + { + var testProjectDirectory = s_testProjectDirectory.Value; + DirectoryInfo binary = new(Path.Combine(testProjectDirectory.FullName, "../../../dist-bin")); + + if (!binary.Exists) + { + Assert.Fail("Binary path not found."); + } + + return binary; + } + + private static FileInfo GetPackageFile() + { + var testProjectDirectory = s_testProjectDirectory.Value; + FileInfo package = new(Path.Combine(testProjectDirectory.FullName, "../../../package.json")); + + if (!package.Exists) + { + Assert.Fail("Package JSON not found."); + } + + return package; + } +} diff --git a/src/dotnet/Sentry.Cli.Tests/PlatformUtilities.cs b/src/dotnet/Sentry.Cli.Tests/PlatformUtilities.cs new file mode 100644 index 00000000..9ffcd655 --- /dev/null +++ b/src/dotnet/Sentry.Cli.Tests/PlatformUtilities.cs @@ -0,0 +1,23 @@ +namespace Sentry.Cli.Tests; + +internal static class PlatformUtilities +{ + internal static string GetNativeExecutableName() + { + var platform = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "linux" : + RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "darwin" : + RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "windows" : + throw new PlatformNotSupportedException($"Unsupported platform: {RuntimeInformation.OSDescription} ({RuntimeInformation.OSArchitecture})"); + + var architecture = RuntimeInformation.OSArchitecture switch + { + Architecture.Arm64 => "arm64", + Architecture.X64 => "x64", + _ => throw new PlatformNotSupportedException($"Unsupported platform: {RuntimeInformation.OSDescription} ({RuntimeInformation.OSArchitecture})"), + }; + + return OperatingSystem.IsWindows() + ? $"sentry-{platform}-{architecture}.exe" + : $"sentry-{platform}-{architecture}"; + } +} diff --git a/src/dotnet/Sentry.Cli.Tests/ProcessResult.cs b/src/dotnet/Sentry.Cli.Tests/ProcessResult.cs new file mode 100644 index 00000000..991a993a --- /dev/null +++ b/src/dotnet/Sentry.Cli.Tests/ProcessResult.cs @@ -0,0 +1,46 @@ +namespace Sentry.Cli.Tests; + +internal sealed class ProcessResult +{ + private readonly int _exitCode; + private readonly string _output; + private readonly string _error; + + public ProcessResult(int exitCode, string output, string error) + { + _exitCode = exitCode; + _output = output; + _error = error; + } + + public int ExitCode => _exitCode; + public string Output => _output; + public string Error => _error; + + public async Task AssertSuccessAsync() + { + await Assert.That(_exitCode).IsZero(); + } + + public async Task AssertFailureAsync() + { + await Assert.That(_exitCode).IsNotZero(); + } + + public async Task AssertFailureAsync(int exitCode) + { + await Assert.That(_exitCode).IsEqualTo(exitCode); + } + + public async Task AssertOutputAsync(string output) + { + await Assert.That(_output).IsEqualTo(output); + await Assert.That(_error).IsEmpty(); + } + + public async Task AssertErrorAsync(string error) + { + await Assert.That(_output).IsEmpty(); + await Assert.That(_error).IsEqualTo(error); + } +} From 851936635b6f25d09f3f50a4bc85a6b17453dc77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 19 Feb 2026 04:21:13 +0100 Subject: [PATCH 08/18] ci: Add .NET integration test job Runs dotnet test on all 5 targets after build-binary, downloading the platform-specific native binary artifact into dist-bin so both TUnit tests can run: the framework-dependent fallback test and the platform-specific launcher test. Also wires test-dotnet into ci-status checks and skipped-detection, and adds a test:dotnet script to package.json for local use. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci.yml | 39 +++++++++++++++++++++++-- src/dotnet/global.json => global.json | 0 src/dotnet/nuget.config => nuget.config | 0 package.json | 1 + 4 files changed, 38 insertions(+), 2 deletions(-) rename src/dotnet/global.json => global.json (100%) rename src/dotnet/nuget.config => nuget.config (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d90fd7db..b670723c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -224,6 +224,37 @@ jobs: SENTRY_CLI_BINARY: ${{ github.workspace }}/dist-bin/sentry-linux-x64 run: bun run test:e2e + test-dotnet: + name: .NET Tests (${{ matrix.target }}) + needs: [changes, build-binary] + if: needs.changes.outputs.code == 'true' + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - target: darwin-arm64 + os: macos-latest + - target: linux-x64 + os: ubuntu-latest + - target: windows-x64 + os: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + - name: Download binary + uses: actions/download-artifact@v4 + with: + name: sentry-${{ matrix.target }} + path: dist-bin + - name: Make binary executable + if: runner.os != 'Windows' + run: chmod +x dist-bin/sentry-${{ matrix.target }} + - name: .NET Tests + run: dotnet test --project src/dotnet/Sentry.Cli.Tests/Sentry.Cli.Tests.csproj + build-npm: name: Build npm Package (Node ${{ matrix.node }}) needs: [lint, test-unit] @@ -286,14 +317,14 @@ jobs: ci-status: name: CI Status if: always() - needs: [changes, check-skill, build-binary, build-npm, build-docs, test-e2e] + needs: [changes, check-skill, build-binary, build-npm, build-docs, test-e2e, test-dotnet] runs-on: ubuntu-latest permissions: {} steps: - name: Check CI status run: | # Check for explicit failures or cancellations in all jobs - results="${{ needs.check-skill.result }} ${{ needs.build-binary.result }} ${{ needs.build-npm.result }} ${{ needs.build-docs.result }} ${{ needs.test-e2e.result }}" + results="${{ needs.check-skill.result }} ${{ needs.build-binary.result }} ${{ needs.build-npm.result }} ${{ needs.build-docs.result }} ${{ needs.test-e2e.result }} ${{ needs.test-dotnet.result }}" for result in $results; do if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then echo "::error::CI failed" @@ -307,6 +338,10 @@ jobs: echo "::error::CI failed - upstream job failed causing test-e2e to be skipped" exit 1 fi + if [[ "${{ needs.changes.outputs.code }}" == "true" && "${{ needs.test-dotnet.result }}" == "skipped" ]]; then + echo "::error::CI failed - upstream job failed causing test-dotnet to be skipped" + exit 1 + fi if [[ "${{ needs.changes.outputs.skill }}" == "true" && "${{ needs.check-skill.result }}" == "skipped" ]]; then echo "::error::CI failed - upstream job failed causing check-skill to be skipped" exit 1 diff --git a/src/dotnet/global.json b/global.json similarity index 100% rename from src/dotnet/global.json rename to global.json diff --git a/src/dotnet/nuget.config b/nuget.config similarity index 100% rename from src/dotnet/nuget.config rename to nuget.config diff --git a/package.json b/package.json index e0a916e9..67bc0157 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "test:unit": "bun test test/lib test/commands test/types --coverage --coverage-reporter=lcov", "test:isolated": "bun test test/isolated", "test:e2e": "bun test test/e2e", + "test:dotnet": "dotnet test --project src/dotnet/Sentry.Cli.Tests/Sentry.Cli.Tests.csproj", "generate:skill": "bun run script/generate-skill.ts", "check:skill": "bun run script/check-skill.ts", "check:deps": "bun run script/check-no-deps.ts" From 591ead476eaf18ab8c629336cf2b7d2bc487c846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 19 Feb 2026 04:34:29 +0100 Subject: [PATCH 09/18] refactor(dotnet): reduce cognitive complexity in pack script Extract resolveTargets() and verifyBinaries() helpers from pack() to bring its complexity score below the linter's max of 15. Co-Authored-By: Claude Sonnet 4.6 --- script/pack.ts | 70 +++++++++++++++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/script/pack.ts b/script/pack.ts index f562871b..8bc7a7c1 100644 --- a/script/pack.ts +++ b/script/pack.ts @@ -65,8 +65,12 @@ function getBinaryName(target: PackTarget): string { /** Get .NET Runtime Identifier for a target */ function getDotnetRid(target: PackTarget): string { - if (target.os === "darwin") return `osx-${target.arch}`; - if (target.os === "win32") return `win-${target.arch}`; + if (target.os === "darwin") { + return `osx-${target.arch}`; + } + if (target.os === "win32") { + return `win-${target.arch}`; + } return `${target.os}-${target.arch}`; } @@ -84,7 +88,10 @@ function parseTarget(targetStr: string): PackTarget | null { } /** Pack a platform-specific NuGet package with embedded native binary */ -async function packTarget(target: PackTarget, version: string): Promise { +async function packTarget( + target: PackTarget, + version: string +): Promise { const rid = getDotnetRid(target); const packageName = getPackageName(target); const outfile = getNupkgPath(version, rid); @@ -104,14 +111,14 @@ async function packTarget(target: PackTarget, version: string): Promise /** Pack the "any" (framework-dependent, CoreCLR) package */ async function packAny(version: string): Promise { const outfile = getNupkgPath(version, "any"); - console.log(` Packing any (framework-dependent)...`); + console.log(" Packing any (framework-dependent)..."); try { await $`dotnet pack ${PROJECT_DIR} -c Release -r any -p:PackageVersion=${version} -p:PublishAot=false`.quiet(); console.log(` -> ${outfile}`); return true; } catch (error) { - console.error(` Failed to pack any:`); + console.error(" Failed to pack any:"); console.error(error); return false; } @@ -120,34 +127,25 @@ async function packAny(version: string): Promise { /** Pack the root package (no RID — pointer/manifest package) */ async function packRoot(version: string): Promise { const outfile = getNupkgPath(version); - console.log(` Packing root (no RID)...`); + console.log(" Packing root (no RID)..."); try { await $`dotnet pack ${PROJECT_DIR} -c Release -p:PackageVersion=${version} -p:PublishAot=true`.quiet(); console.log(` -> ${outfile}`); return true; } catch (error) { - console.error(` Failed to pack root:`); + console.error(" Failed to pack root:"); console.error(error); return false; } } -/** Main pack function */ -async function pack(): Promise { - const args = process.argv.slice(2); - const singlePack = args.includes("--single"); +/** Resolve pack targets from CLI args, printing a status line and exiting on error */ +function resolveTargets(args: string[]): PackTarget[] { const targetIndex = args.indexOf("--target"); const targetArg = targetIndex !== -1 ? args[targetIndex + 1] : null; - console.log(`\nSentry CLI NuGet Pack v${pkg.version}`); - console.log("=".repeat(40)); - - // Determine targets - let targets: PackTarget[]; - if (targetArg) { - // Explicit target specified (for cross-compilation) const target = parseTarget(targetArg); if (!target) { console.error(`Invalid target: ${targetArg}`); @@ -156,9 +154,11 @@ async function pack(): Promise { ); process.exit(1); } - targets = [target]; console.log(`\nPacking for target: ${getPackageName(target)}`); - } else if (singlePack) { + return [target]; + } + + if (args.includes("--single")) { const currentTarget = ALL_TARGETS.find( (t) => t.os === process.platform && t.arch === process.arch ); @@ -168,23 +168,23 @@ async function pack(): Promise { ); process.exit(1); } - targets = [currentTarget]; console.log( `\nPacking for current platform: ${getPackageName(currentTarget)}` ); - } else { - targets = ALL_TARGETS; - console.log(`\nPacking for ${targets.length} targets`); + return [currentTarget]; } - // Verify native binaries for selected targets + console.log(`\nPacking for ${ALL_TARGETS.length} targets`); + return ALL_TARGETS; +} + +/** Verify that native binaries exist for all targets, exiting on missing files */ +async function verifyBinaries(targets: PackTarget[]): Promise { console.log("\nVerifying native binaries..."); let binaryMissing = false; for (const target of targets) { - const binaryName = getBinaryName(target); - const binaryPath = `${DIST_BIN_DIR}/${binaryName}`; - const exists = await Bun.file(binaryPath).exists(); - if (exists) { + const binaryPath = `${DIST_BIN_DIR}/${getBinaryName(target)}`; + if (await Bun.file(binaryPath).exists()) { console.log(` ✓ ${binaryPath}`); } else { console.error(` ✗ ${binaryPath} not found`); @@ -196,6 +196,18 @@ async function pack(): Promise { console.error("Run 'bun run build:all' first to generate all binaries."); process.exit(1); } +} + +/** Main pack function */ +async function pack(): Promise { + const args = process.argv.slice(2); + + console.log(`\nSentry CLI NuGet Pack v${pkg.version}`); + console.log("=".repeat(40)); + + const targets = resolveTargets(args); + + await verifyBinaries(targets); // Clean output directory await $`rm -rf ${DIST_PKG_DIR}`.quiet(); From b47d64adec750dead1d0dcad6cc8fa2837841c42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 19 Feb 2026 04:50:53 +0100 Subject: [PATCH 10/18] debug --- src/dotnet/Sentry.Cli.Tests/PathUtilities.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/dotnet/Sentry.Cli.Tests/PathUtilities.cs b/src/dotnet/Sentry.Cli.Tests/PathUtilities.cs index fbf9971f..8825447e 100644 --- a/src/dotnet/Sentry.Cli.Tests/PathUtilities.cs +++ b/src/dotnet/Sentry.Cli.Tests/PathUtilities.cs @@ -24,7 +24,7 @@ private static DirectoryInfo GetTestProjectDirectory([CallerFilePath] string? so if (!testProject.Exists) { - Assert.Fail("Test project not found."); + Assert.Fail($"Test project not found: {testProject}"); } Assert.NotNull(testProject.Directory); @@ -38,7 +38,7 @@ private static DotnetProject GetLauncherProject() if (!project.Exists) { - Assert.Fail("Launcher project not found."); + Assert.Fail($"Launcher project not found: {project}"); } return new DotnetProject(project); @@ -51,7 +51,7 @@ private static DirectoryInfo GetArtifactsDirectory() if (!artifacts.Exists) { - Assert.Fail("Artifacts path not found."); + Assert.Fail($"Artifacts path not found: {artifacts}"); } return artifacts; @@ -64,7 +64,7 @@ private static DirectoryInfo GetBinaryDirectory() if (!binary.Exists) { - Assert.Fail("Binary path not found."); + Assert.Fail($"Binary path not found: {binary}"); } return binary; @@ -77,7 +77,7 @@ private static FileInfo GetPackageFile() if (!package.Exists) { - Assert.Fail("Package JSON not found."); + Assert.Fail($"Package JSON not found: {package}"); } return package; From 97a47a5a4677b343f12b6aab6de5a0f96fb29f8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 19 Feb 2026 05:21:31 +0100 Subject: [PATCH 11/18] recursive path search fallback --- src/dotnet/Sentry.Cli.Tests/PathUtilities.cs | 33 +++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/dotnet/Sentry.Cli.Tests/PathUtilities.cs b/src/dotnet/Sentry.Cli.Tests/PathUtilities.cs index 8825447e..39fd2f0c 100644 --- a/src/dotnet/Sentry.Cli.Tests/PathUtilities.cs +++ b/src/dotnet/Sentry.Cli.Tests/PathUtilities.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace Sentry.Cli.Tests; @@ -24,13 +25,43 @@ private static DirectoryInfo GetTestProjectDirectory([CallerFilePath] string? so if (!testProject.Exists) { - Assert.Fail($"Test project not found: {testProject}"); + if (TryFindSolutionDirectory(out var solution)) + { + testProject = new FileInfo(Path.Combine(solution.FullName, "Sentry.Cli.Tests", "Sentry.Cli.Tests.csproj")); + if (!testProject.Exists) + { + Assert.Fail($"Test project not found: {testProject}"); + } + } + else + { + Assert.Fail($"Test project not found: {testProject}"); + } } Assert.NotNull(testProject.Directory); return testProject.Directory; } + private static bool TryFindSolutionDirectory([NotNullWhen(true)] out DirectoryInfo? solution) + { + var directory = new DirectoryInfo(Directory.GetCurrentDirectory()); + + while (directory.Parent is { } parent) + { + directory = parent; + + if (Directory.EnumerateFiles(directory.FullName, "Sentry.Cli.slnx", SearchOption.TopDirectoryOnly).Any()) + { + solution = directory; + return true; + } + } + + solution = null; + return false; + } + private static DotnetProject GetLauncherProject() { var testProjectDirectory = s_testProjectDirectory.Value; From c5801a9d9fd69d710c47d854fde7ea6140620470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 19 Feb 2026 05:33:18 +0100 Subject: [PATCH 12/18] suppress a failing test on WIndows --- src/dotnet/Sentry.Cli.Tests/LauncherTests.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/dotnet/Sentry.Cli.Tests/LauncherTests.cs b/src/dotnet/Sentry.Cli.Tests/LauncherTests.cs index 1f602fcf..22df0a98 100644 --- a/src/dotnet/Sentry.Cli.Tests/LauncherTests.cs +++ b/src/dotnet/Sentry.Cli.Tests/LauncherTests.cs @@ -32,6 +32,10 @@ public async Task Launch_PlatformSpecific_HasSentryCli() var executable = Path.Combine(output, "Sentry.Cli"); var exec = await DotnetProject.ExecAsync(executable, ["--version"]); + // there is an issue on Windows in CI + if (OperatingSystem.IsWindows()) + return; + var version = await JsonUtilities.GetVersionAsync(PathUtilities.PackageFile); await exec.AssertSuccessAsync(); await exec.AssertOutputAsync(version); From 21f79745f7c824515aacb0001bf1344e0b64fa93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 19 Feb 2026 05:35:41 +0100 Subject: [PATCH 13/18] feat(dotnet): Add dotnet format lint script and CI job Adds script/dotnet-lint.ts wrapping `dotnet format` with a --check flag that passes --verify-no-changes (non-zero exit if any file would change). Wires up lint:dotnet / lint:dotnet:fix npm scripts and a lint-dotnet CI job that gates test-dotnet. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci.yml | 14 +++++++++++--- package.json | 2 ++ script/dotnet-lint.ts | 28 ++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 script/dotnet-lint.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b670723c..ac70f0d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -104,6 +104,16 @@ jobs: - run: bun run typecheck - run: bun run check:deps + lint-dotnet: + name: .NET Lint + needs: [changes] + if: needs.changes.outputs.code == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + - run: dotnet format src/dotnet/Sentry.Cli.slnx --verify-no-changes + test-unit: name: Unit Tests needs: [changes] @@ -226,7 +236,7 @@ jobs: test-dotnet: name: .NET Tests (${{ matrix.target }}) - needs: [changes, build-binary] + needs: [changes, lint-dotnet, build-binary] if: needs.changes.outputs.code == 'true' runs-on: ${{ matrix.os }} strategy: @@ -242,8 +252,6 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '10.0.x' - name: Download binary uses: actions/download-artifact@v4 with: diff --git a/package.json b/package.json index 67bc0157..18b0a573 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "typecheck": "tsc --noEmit", "lint": "bunx ultracite check", "lint:fix": "bunx ultracite fix", + "lint:dotnet": "bun run script/dotnet-lint.ts --check", + "lint:dotnet:fix": "bun run script/dotnet-lint.ts", "test": "bun test", "test:unit": "bun test test/lib test/commands test/types --coverage --coverage-reporter=lcov", "test:isolated": "bun test test/isolated", diff --git a/script/dotnet-lint.ts b/script/dotnet-lint.ts new file mode 100644 index 00000000..4acdaf3b --- /dev/null +++ b/script/dotnet-lint.ts @@ -0,0 +1,28 @@ +#!/usr/bin/env bun + +/** + * Lint script for the .NET source code + * + * Runs `dotnet format` against the solution and optionally verifies that no + * changes are needed (useful in CI to enforce formatting without auto-fixing). + * + * Usage: + * bun run script/dotnet-lint.ts # Format in place + * bun run script/dotnet-lint.ts --check # Exit non-zero if any changes would be made + */ + +import { $ } from "bun"; + +const SOLUTION = "src/dotnet/Sentry.Cli.slnx"; + +const check = process.argv.includes("--check"); + +try { + if (check) { + await $`dotnet format ${SOLUTION} --verify-no-changes`; + } else { + await $`dotnet format ${SOLUTION}`; + } +} catch { + process.exit(1); +} From 3c2e610df99f0239f4c121f6e3de7d9c5094bfa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 19 Feb 2026 06:07:50 +0100 Subject: [PATCH 14/18] feat(dotnet): add --agnostic flag and CI NuGet pack job MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Redesigns pack.ts mode semantics so CI can assemble all 7 packages across three runners without duplicating the root/any packages: - `--agnostic`: packs only root (pointer) + any packages; no binary needed - `--single`: packs current platform only (no root/any) - `--target `: packs one specific target only (no root/any) - no args: packs current platform + root + any (replaces "all platforms") - `--no-clean`: skips dist-pkg cleanup, allowing accumulation across calls Adds a new `build-nuget` CI job (linux/macos/windows matrix) that downloads the pre-built native binaries, calls pack.ts with --no-clean for each target + agnostic (linux only), and uploads a per-runner nuget-* artifact. Together the three runners produce all 7 packages. Updates package.json: `pack` drops --single, `pack:all` → `pack:agnostic`. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci.yml | 69 ++++++++++++++++++++++++++++- package.json | 4 +- script/pack.ts | 95 +++++++++++++++++++++++++--------------- 3 files changed, 128 insertions(+), 40 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac70f0d2..33a787b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -208,6 +208,67 @@ jobs: name: sentry-${{ matrix.target }} path: dist-bin/sentry-* + build-nuget: + name: Pack NuGet (${{ matrix.name }}) + needs: [changes, lint-dotnet, build-binary] + if: needs.changes.outputs.code == 'true' + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - name: linux + os: ubuntu-latest + artifact-pattern: sentry-linux-* + pack-targets: linux-x64 linux-arm64 + pack-agnostic: true + - name: macos + os: macos-latest + artifact-pattern: sentry-darwin-* + pack-targets: darwin-arm64 darwin-x64 + pack-agnostic: false + - name: windows + os: windows-latest + artifact-pattern: sentry-windows-* + pack-targets: windows-x64 + pack-agnostic: false + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - uses: actions/cache@v4 + id: cache + with: + path: node_modules + key: node-modules-${{ matrix.os }}-${{ hashFiles('bun.lock', 'patches/**') }} + - name: Install dependencies + if: steps.cache.outputs.cache-hit != 'true' + shell: bash + run: bun install --frozen-lockfile + - uses: actions/setup-dotnet@v4 + - name: Download binaries + uses: actions/download-artifact@v4 + with: + pattern: ${{ matrix.artifact-pattern }} + path: dist-bin + merge-multiple: true + - name: Make binaries executable + if: runner.os != 'Windows' + run: chmod +x dist-bin/sentry-* + - name: Pack NuGet packages + shell: bash + run: | + for target in ${{ matrix.pack-targets }}; do + bun run script/pack.ts --no-clean --target $target + done + if [[ "${{ matrix.pack-agnostic }}" == "true" ]]; then + bun run script/pack.ts --no-clean --agnostic + fi + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: nuget-${{ matrix.name }} + path: dist-pkg/*.nupkg + test-e2e: name: E2E Tests needs: [build-binary] @@ -325,14 +386,14 @@ jobs: ci-status: name: CI Status if: always() - needs: [changes, check-skill, build-binary, build-npm, build-docs, test-e2e, test-dotnet] + needs: [changes, check-skill, build-binary, build-npm, build-docs, test-e2e, test-dotnet, build-nuget] runs-on: ubuntu-latest permissions: {} steps: - name: Check CI status run: | # Check for explicit failures or cancellations in all jobs - results="${{ needs.check-skill.result }} ${{ needs.build-binary.result }} ${{ needs.build-npm.result }} ${{ needs.build-docs.result }} ${{ needs.test-e2e.result }} ${{ needs.test-dotnet.result }}" + results="${{ needs.check-skill.result }} ${{ needs.build-binary.result }} ${{ needs.build-npm.result }} ${{ needs.build-docs.result }} ${{ needs.test-e2e.result }} ${{ needs.test-dotnet.result }} ${{ needs.build-nuget.result }}" for result in $results; do if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then echo "::error::CI failed" @@ -350,6 +411,10 @@ jobs: echo "::error::CI failed - upstream job failed causing test-dotnet to be skipped" exit 1 fi + if [[ "${{ needs.changes.outputs.code }}" == "true" && "${{ needs.build-nuget.result }}" == "skipped" ]]; then + echo "::error::CI failed - upstream job failed causing build-nuget to be skipped" + exit 1 + fi if [[ "${{ needs.changes.outputs.skill }}" == "true" && "${{ needs.check-skill.result }}" == "skipped" ]]; then echo "::error::CI failed - upstream job failed causing check-skill to be skipped" exit 1 diff --git a/package.json b/package.json index 18b0a573..20471e97 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ "build": "bun run script/build.ts --single", "build:all": "bun run script/build.ts", "bundle": "bun run script/bundle.ts", - "pack": "bun run script/pack.ts --single", - "pack:all": "bun run script/pack.ts", + "pack": "bun run script/pack.ts", + "pack:agnostic": "bun run script/pack.ts --agnostic", "typecheck": "tsc --noEmit", "lint": "bunx ultracite check", "lint:fix": "bunx ultracite fix", diff --git a/script/pack.ts b/script/pack.ts index 8bc7a7c1..9ec58291 100644 --- a/script/pack.ts +++ b/script/pack.ts @@ -5,12 +5,16 @@ * * Creates platform-specific NuGet packages with embedded native binaries, * as well as the required top-level pointer package and the RID-agnostic package as fallback. - * The .NET equivalent of build.ts — same target model, same CLI flags, but dependent on it's output at "dist-bin/". + * The .NET equivalent of build.ts — same target model, same CLI flags, but dependent on its output at "dist-bin/". * * Usage: - * bun run script/pack.ts # Pack for all platforms - * bun run script/pack.ts --single # Pack for current platform only - * bun run script/pack.ts --target darwin-x64 # Pack for specific target (cross-compile) + * bun run script/pack.ts # Pack for current platform + root + any packages + * bun run script/pack.ts --agnostic # Pack only root + any packages (no platform-specific) + * bun run script/pack.ts --single # Pack for current platform only (no root/any) + * bun run script/pack.ts --target darwin-x64 # Pack for a specific target only (no root/any) + * + * Flags: + * --no-clean Skip cleaning the dist-pkg directory before packing * * Output: * dist-pkg/ @@ -140,8 +144,20 @@ async function packRoot(version: string): Promise { } } -/** Resolve pack targets from CLI args, printing a status line and exiting on error */ -function resolveTargets(args: string[]): PackTarget[] { +type PackMode = { + /** Platform-specific targets to pack */ + targets: PackTarget[]; + /** Whether to also pack the root (pointer) and any (agnostic) packages */ + includeAgnostic: boolean; +}; + +/** Resolve pack mode from CLI args, printing a status line and exiting on error */ +function resolveMode(args: string[]): PackMode { + if (args.includes("--agnostic")) { + console.log("\nPacking agnostic packages (root + any)"); + return { targets: [], includeAgnostic: true }; + } + const targetIndex = args.indexOf("--target"); const targetArg = targetIndex !== -1 ? args[targetIndex + 1] : null; @@ -155,27 +171,29 @@ function resolveTargets(args: string[]): PackTarget[] { process.exit(1); } console.log(`\nPacking for target: ${getPackageName(target)}`); - return [target]; + return { targets: [target], includeAgnostic: false }; + } + + const currentTarget = ALL_TARGETS.find( + (t) => t.os === process.platform && t.arch === process.arch + ); + if (!currentTarget) { + console.error(`Unsupported platform: ${process.platform}-${process.arch}`); + process.exit(1); } if (args.includes("--single")) { - const currentTarget = ALL_TARGETS.find( - (t) => t.os === process.platform && t.arch === process.arch - ); - if (!currentTarget) { - console.error( - `Unsupported platform: ${process.platform}-${process.arch}` - ); - process.exit(1); - } console.log( `\nPacking for current platform: ${getPackageName(currentTarget)}` ); - return [currentTarget]; + return { targets: [currentTarget], includeAgnostic: false }; } - console.log(`\nPacking for ${ALL_TARGETS.length} targets`); - return ALL_TARGETS; + // Default: current platform + agnostic packages + console.log( + `\nPacking for current platform + agnostic packages: ${getPackageName(currentTarget)}` + ); + return { targets: [currentTarget], includeAgnostic: true }; } /** Verify that native binaries exist for all targets, exiting on missing files */ @@ -201,39 +219,44 @@ async function verifyBinaries(targets: PackTarget[]): Promise { /** Main pack function */ async function pack(): Promise { const args = process.argv.slice(2); + const noClean = args.includes("--no-clean"); console.log(`\nSentry CLI NuGet Pack v${pkg.version}`); console.log("=".repeat(40)); - const targets = resolveTargets(args); + const mode = resolveMode(args); - await verifyBinaries(targets); + if (mode.targets.length > 0) { + await verifyBinaries(mode.targets); + } - // Clean output directory - await $`rm -rf ${DIST_PKG_DIR}`.quiet(); + // Clean output directory (unless --no-clean is specified) + if (!noClean) { + await $`rm -rf ${DIST_PKG_DIR}`.quiet(); + } console.log(""); - // Build all packages let successCount = 0; let failCount = 0; - // Root package (no RID) — always included, even for --single / --target - if (await packRoot(pkg.version)) { - successCount += 1; - } else { - failCount += 1; - } + // Root package (no RID) and any package — only when includeAgnostic is set + if (mode.includeAgnostic) { + if (await packRoot(pkg.version)) { + successCount += 1; + } else { + failCount += 1; + } - // "any" package (framework-dependent fallback) — always included, even for --single / --target - if (await packAny(pkg.version)) { - successCount += 1; - } else { - failCount += 1; + if (await packAny(pkg.version)) { + successCount += 1; + } else { + failCount += 1; + } } // Platform-specific packages - for (const target of targets) { + for (const target of mode.targets) { if (await packTarget(target, pkg.version)) { successCount += 1; } else { From 057984205ee471bedb57def61d1a8fe8854184a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 19 Feb 2026 06:24:40 +0100 Subject: [PATCH 15/18] refactor(dotnet): split build-nuget matrix into 5 individual target entries Replaces the 3-runner matrix (linux/macos/windows) with 5 entries, one per binary target, so each runner packs and uploads exactly one package. - Downloads a single named artifact instead of a wildcard pattern - Replaces the pack loop with a direct --target call per entry - Moves the agnostic condition from a bash if to a step-level if Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci.yml | 42 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33a787b0..0dadff9e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -217,20 +217,25 @@ jobs: fail-fast: false matrix: include: - - name: linux + - name: linux-x64 os: ubuntu-latest - artifact-pattern: sentry-linux-* - pack-targets: linux-x64 linux-arm64 + pack-target: linux-x64 pack-agnostic: true - - name: macos + - name: linux-arm64 + os: ubuntu-latest + pack-target: linux-arm64 + pack-agnostic: false + - name: macos-arm64 os: macos-latest - artifact-pattern: sentry-darwin-* - pack-targets: darwin-arm64 darwin-x64 + pack-target: darwin-arm64 pack-agnostic: false - - name: windows + - name: macos-x64 + os: macos-latest + pack-target: darwin-x64 + pack-agnostic: false + - name: windows-x64 os: windows-latest - artifact-pattern: sentry-windows-* - pack-targets: windows-x64 + pack-target: windows-x64 pack-agnostic: false steps: - uses: actions/checkout@v4 @@ -245,24 +250,19 @@ jobs: shell: bash run: bun install --frozen-lockfile - uses: actions/setup-dotnet@v4 - - name: Download binaries + - name: Download binary uses: actions/download-artifact@v4 with: - pattern: ${{ matrix.artifact-pattern }} + name: sentry-${{ matrix.pack-target }} path: dist-bin - merge-multiple: true - - name: Make binaries executable + - name: Make binary executable if: runner.os != 'Windows' run: chmod +x dist-bin/sentry-* - name: Pack NuGet packages - shell: bash - run: | - for target in ${{ matrix.pack-targets }}; do - bun run script/pack.ts --no-clean --target $target - done - if [[ "${{ matrix.pack-agnostic }}" == "true" ]]; then - bun run script/pack.ts --no-clean --agnostic - fi + run: bun run script/pack.ts --target ${{ matrix.pack-target }} + - name: Pack agnostic NuGet packages + if: matrix.pack-agnostic == 'true' + run: bun run script/pack.ts --no-clean --agnostic - name: Upload artifact uses: actions/upload-artifact@v4 with: From 8d3c52fb5de9900c248a01c34820ec3d2f221e3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 19 Feb 2026 06:38:39 +0100 Subject: [PATCH 16/18] fix packing on Linux ARM 64 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0dadff9e..d55bf953 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -222,7 +222,7 @@ jobs: pack-target: linux-x64 pack-agnostic: true - name: linux-arm64 - os: ubuntu-latest + os: ubuntu-24.04-arm pack-target: linux-arm64 pack-agnostic: false - name: macos-arm64 From bfc3dc4ba6152b5b0a3d6d3f14f5b636cdee6590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:30:34 +0100 Subject: [PATCH 17/18] ci: fix agnostic packages built on linux-x64 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d55bf953..c969d444 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -261,7 +261,7 @@ jobs: - name: Pack NuGet packages run: bun run script/pack.ts --target ${{ matrix.pack-target }} - name: Pack agnostic NuGet packages - if: matrix.pack-agnostic == 'true' + if: matrix.pack-agnostic run: bun run script/pack.ts --no-clean --agnostic - name: Upload artifact uses: actions/upload-artifact@v4 From ec4460a99fe153970f378160c29a1ea5e34e1d1b Mon Sep 17 00:00:00 2001 From: Flash0ver <38893694+Flash0ver@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:33:42 +0100 Subject: [PATCH 18/18] fix: no console output on Windows --- src/dotnet/Sentry.Cli.Tests/LauncherTests.cs | 8 ++++---- src/dotnet/Sentry.Cli/Program.cs | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/dotnet/Sentry.Cli.Tests/LauncherTests.cs b/src/dotnet/Sentry.Cli.Tests/LauncherTests.cs index 22df0a98..6ee1ac27 100644 --- a/src/dotnet/Sentry.Cli.Tests/LauncherTests.cs +++ b/src/dotnet/Sentry.Cli.Tests/LauncherTests.cs @@ -28,14 +28,14 @@ public async Task Launch_PlatformSpecific_HasSentryCli() var sourceFileName = Path.Combine(PathUtilities.BinaryDirectory.FullName, PlatformUtilities.GetNativeExecutableName()); var destFileName = Path.Combine(output, PlatformUtilities.GetNativeExecutableName()); File.Copy(sourceFileName, destFileName, true); + if (OperatingSystem.IsWindows()) + { + File.Copy($"{sourceFileName}.gz", $"{destFileName}.gz", true); + } var executable = Path.Combine(output, "Sentry.Cli"); var exec = await DotnetProject.ExecAsync(executable, ["--version"]); - // there is an issue on Windows in CI - if (OperatingSystem.IsWindows()) - return; - var version = await JsonUtilities.GetVersionAsync(PathUtilities.PackageFile); await exec.AssertSuccessAsync(); await exec.AssertOutputAsync(version); diff --git a/src/dotnet/Sentry.Cli/Program.cs b/src/dotnet/Sentry.Cli/Program.cs index 8b6bd6f8..42bba7db 100644 --- a/src/dotnet/Sentry.Cli/Program.cs +++ b/src/dotnet/Sentry.Cli/Program.cs @@ -44,7 +44,11 @@ static int StartExecutable(string fileName, string[] args) { ProcessStartInfo startInfo = new(fileName, args) { +#if _PLATFORM_WIN_X64 + CreateNoWindow = false, +#else CreateNoWindow = true, +#endif UseShellExecute = false, };