Skip to content

Build on the .NET 10 SDK (product code still net8) - #2091

Draft
NickJosevski wants to merge 10 commits into
mainfrom
nj/net10-try-one
Draft

Build on the .NET 10 SDK (product code still net8)#2091
NickJosevski wants to merge 10 commits into
mainfrom
nj/net10-try-one

Conversation

@NickJosevski

@NickJosevski NickJosevski commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Background

.NET 8 reaches end of support on 10 November 2026. Calamari has to move to .NET 10 before then. Octopus Server already runs on .NET 10.

Moving the SDK and moving the product target framework are two separate risks. Doing both together makes a failure hard to attribute. This PR moves only the toolchain.

Results

The build runs on the .NET 10 SDK. Product code still targets net8.0.

The .NET 10 SDK audits transitive packages during restore. Auditing surfaced vulnerable dependencies reached through Octopus.Nuke.Common. TreatWarningsAsErrors turns those advisories into build errors. Fixed versions are pinned rather than the audit being suppressed.

Signing.cs suppresses SYSLIB0057 at one call site. The replacement API only loads certificate files. No replacement exists for reading an Authenticode signature out of a signed PE.

Reducing risk

  • No product target framework changed
  • source/Calamari.sln builds at 164 warnings and 0 errors, identical to the pre-change baseline

How to review this PR

General quality.

⚠️ Does this change require a corresponding Server Change?
⚠️ If so - please add a "Requires Server Change" label to this PR!

NickJosevski added a commit that referenced this pull request Jul 31, 2026
The bundled dotnet-script 1.6.0 is a framework-dependent application targeting
Microsoft.NETCore.App 8.0.0. A framework-dependent app does not roll forward
across a major version by default, so once the build agents were provisioned
with only .NET 10 it failed to launch:

    App: .../Binaries/dotnet-script/dotnet-script.dll
    Framework: 'Microsoft.NETCore.App', version '8.0.0' (x64)
    The following frameworks were found:
      10.0.10 at [.../shared/Microsoft.NETCore.App]

That is the cause of the DotnetScriptProxyFixture failures on the Linux
netcore-testing agents in PR #2091.

Calamari itself is published self-contained and carries no such requirement -
this affects only the separate dotnet-script process. Setting
DOTNET_ROLL_FORWARD=Major on that invocation lets it run on whatever newer
runtime is present. An explicit value already set in the environment is
respected rather than overwritten.

This is a customer-facing fix as much as a CI one: without it, a target with
.NET 10 but no .NET 8 runtime cannot run C# script steps at all. It should also
reduce the need to provision a specific runtime alongside Calamari on workers.

EnvironmentVars is applied additively by SilentProcessRunner
(process.StartInfo.EnvironmentVariables[key] = value), so passing a dictionary
where one was previously null does not discard the inherited environment.

Not done here: upgrading the bundled dotnet-script. 2.0.1 was evaluated and
rejected for now - the GitHub release zip that Calamari vendors is still
net8.0-targeted (only the NuGet package ships a net10.0 build), so it would not
have fixed this, and it changes the NuGet version dotnet-script bundles
(6.10.1.5 -> 6.14.3.1) which invalidates the premise of
UsingIsolatedAssemblyLoadContext. Worth doing as its own piece of work.

Verified: solution builds 171 warnings / 0 errors; all 10 DotnetScriptFixture
tests pass. The 6 DotnetScriptProxyFixture failures seen locally are
environmental - they need the proxy-initializer-fixture-good-proxy host, which
is NXDOMAIN off CI, and they pass on baseline PR #2087's Debian agent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
NickJosevski added a commit that referenced this pull request Aug 3, 2026
The bundled dotnet-script 1.6.0 is a framework-dependent application targeting
Microsoft.NETCore.App 8.0.0. A framework-dependent app does not roll forward
across a major version by default, so once the build agents were provisioned
with only .NET 10 it failed to launch:

    App: .../Binaries/dotnet-script/dotnet-script.dll
    Framework: 'Microsoft.NETCore.App', version '8.0.0' (x64)
    The following frameworks were found:
      10.0.10 at [.../shared/Microsoft.NETCore.App]

That is the cause of the DotnetScriptProxyFixture failures on the Linux
netcore-testing agents in PR #2091.

Calamari itself is published self-contained and carries no such requirement -
this affects only the separate dotnet-script process. Setting
DOTNET_ROLL_FORWARD=Major on that invocation lets it run on whatever newer
runtime is present. An explicit value already set in the environment is
respected rather than overwritten.

This is a customer-facing fix as much as a CI one: without it, a target with
.NET 10 but no .NET 8 runtime cannot run C# script steps at all. It should also
reduce the need to provision a specific runtime alongside Calamari on workers.

EnvironmentVars is applied additively by SilentProcessRunner
(process.StartInfo.EnvironmentVariables[key] = value), so passing a dictionary
where one was previously null does not discard the inherited environment.

Not done here: upgrading the bundled dotnet-script. 2.0.1 was evaluated and
rejected for now - the GitHub release zip that Calamari vendors is still
net8.0-targeted (only the NuGet package ships a net10.0 build), so it would not
have fixed this, and it changes the NuGet version dotnet-script bundles
(6.10.1.5 -> 6.14.3.1) which invalidates the premise of
UsingIsolatedAssemblyLoadContext. Worth doing as its own piece of work.

Verified: solution builds 171 warnings / 0 errors; all 10 DotnetScriptFixture
tests pass. The 6 DotnetScriptProxyFixture failures seen locally are
environmental - they need the proxy-initializer-fixture-good-proxy host, which
is NXDOMAIN off CI, and they pass on baseline PR #2087's Debian agent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
NickJosevski added a commit that referenced this pull request Aug 3, 2026
.NET 10 changed the behaviour of Environment.SetEnvironmentVariable(name, ""):
on net8 an empty value deleted the variable, on net10 it stores an empty string.
Verified directly against 8.0.27 and 10.0.10:

    Set(name, "") then GetEnvironmentVariable   net8: null    net10: ""
    GetEnvironmentVariables().Contains(name)    net8: False   net10: True

Set(name, null) still deletes on both.

The proxy fixtures relied on the old semantics in two ways, which together
account for 11 of the 13 test failures in PR #2091's chain 2026.3.533:

- The TearDown/reset helpers called SetEnvironmentVariable(name, string.Empty)
  meaning "unset". On net10 that instead leaves HTTP_PROXY/HTTPS_PROXY/NO_PROXY
  set to "". ProxyEnvironmentVariablesGenerator tests for *presence*, not value
  (ProxyEnvironmentVariablesGenerator.cs:30), so it took the "proxy variables
  already exist, Calamari will not overwrite" branch and propagated empty
  values into every later fixture in the run. That is the source of
  ScriptEngineFixture's `found at least one item {[http_proxy, ]}` and of
  SetupKubectlAuthenticationFixture's 3 surplus entries.
- ProxySettingsInitializerFixture passed "" for absent credentials and then
  asserted BeNull(). On net8 the write deleted the variable so the read
  returned null; on net10 it returns "". Hence
  `Expected proxy.Username to be <null>, but found ""`.

Fixes: reset helpers now pass null, which expresses "unset" and behaves the
same on both runtimes. The credential assertions use BeNullOrEmpty(), which
states the actual intent ("no credentials") rather than a representation
detail. The setter deliberately keeps "" so the empty-string case is now
genuinely covered - net8 could never exercise it in-process.

No product change, and no customer-visible behaviour change:

- Inherited environment variables are unaffected. Verified that HTTP_PROXY=""
  exported by a parent process reports Contains=true and "" on BOTH runtimes.
  Calamari in production inherits its environment, so this is identical on
  net8 and net10.
- "" and null are already equivalent downstream: ProxySettings.cs:46 and :92
  guard with string.IsNullOrWhiteSpace(Username), and
  ProxyEnvironmentVariablesGenerator uses !string.IsNullOrEmpty().
- All 30 uses of the empty-string-as-unset idiom were in test code; none in
  product code.

Verified on net10.0 locally: ProxySettingsInitializerFixture 5/5 (was 2 failed),
ScriptEngineFixture.ExecuteWithoutEnvironmentVarsPassesEmptyDictionaryToWrappers
1/1, SetupKubectlAuthenticationFixture Execute_WithGoogleCloudAccountType 3/3.

Not verified locally: the 5 BashProxyFixture / DotnetScriptProxyFixture cases,
which need bash/pwsh and the proxy fixture host. They share the leaked-teardown
mechanism and ScriptProxyFixtureBase got the same fix, so they are expected to
clear, but CI is the confirmation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
NickJosevski added a commit that referenced this pull request Aug 3, 2026
CodeGenerator.GenerateConsoleApplication hardcoded a net8-era toolchain: it
wrote a global.json pinning SDK 8.0.10 and ran `dotnet new console -f net8.0`.
With the agents providing only a .NET 10 SDK, 8.0.10 is unsatisfiable
(rollForward: latestFeature only rolls within the 8.0.x band), so the test
failed before it could exercise anything:

    "dotnet" new console -f net8.0 ... Failed with exit code: 155
    Requested SDK version: 8.0.10
    global.json file: <buildTmp>/.../node/global.json
    Installed SDKs:
    (empty)

That is the ExecuteManifestCommandFixture.WithInstructions failure on the Linux
netcore-testing configs in PR #2091's chain 2026.3.534 - the last remaining
Linux test failure once the proxy fixture fix landed.

Bumped to SDK 10.0.302 / net10.0, matching the repository's root global.json.

Verification is partial. The SDK resolution error is gone: `dotnet new console
-f net10.0` succeeds and the project is created. The test still fails on an
Apple Silicon dev machine, but for a pre-existing and unrelated reason - the RID
selection a few lines below sets rid = "osx-x64" for Mac and then
unconditionally overwrites it with "linux-arm64" whenever ProcessArchitecture is
Arm64, because that check is not nested under the IsRunningOnNix branch. The
generated app is therefore published for linux-arm64 and exec'd on macOS,
yielding "Exec format error". CI is unaffected (Windows resolves win-x64, Linux
x64 resolves linux-x64, Linux arm64 correctly resolves linux-arm64), so CI is
the confirmation for this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@NickJosevski
NickJosevski marked this pull request as draft August 4, 2026 06:57
NickJosevski and others added 9 commits August 4, 2026 17:50
.NET 8 reaches end of support on 10 November 2026. Moving the SDK and moving the
product target framework are two separate risks. Doing both together makes a
failure hard to attribute. This commit moves only the toolchain.

global.json goes to SDK 10.0.302. The Nuke build project targets net10.0.
Frameworks.cs gains Net100 constants alongside the existing ones.

The .NET 10 SDK audits transitive packages during restore. Auditing surfaced
vulnerable dependencies reached through Octopus.Nuke.Common. TreatWarningsAsErrors
turns those advisories into build errors. Fixed versions are pinned rather than the
audit being suppressed.

Signing.cs suppresses SYSLIB0057 at one call site. X509CertificateLoader only loads
certificate files. No replacement exists for reading an Authenticode signature out
of a signed PE.

Verified: source/Calamari.sln builds at 164 warnings and 0 errors, identical to the
pre-change baseline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The .NET 10 SDK raises NU1510 for direct references to packages the framework
already provides. TreatWarningsAsErrors turns NU1510 into a build error. Removing
the references now, on net8, keeps the change isolated from the framework move.

Removed System.ValueTuple, System.ComponentModel.TypeConverter,
System.Diagnostics.Tracing, System.IO.FileSystem,
System.Runtime.InteropServices.RuntimeInformation, and the
Microsoft.NETFramework.ReferenceAssemblies.net462 reference left over from #1669.

System.Threading.AccessControl stays with NoWarn="NU1510". NuGet flags the package
as likely unnecessary. That heuristic assumes a -windows target framework. These
projects target plain net8.0 and need the package for SemaphoreSecurity.

Verified: 164 warnings and 0 errors, identical to baseline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Autofac 4.8.0 dates from 2018 and is five majors behind. Octopus Server already
runs 9.3.1 on net10.

Done while Calamari still targets net8. Autofac 9.3.1 resolves on net8. The DI
upgrade is therefore verified against a known-good baseline instead of being
entangled with the framework change.

Notes for reviewers. ContainerBuilder.Update() is the headline Autofac 5 removal
and was never used here. The .Update call sites in this repo belong to LibGit2Sharp.
The API surface in use is mainstream and unchanged across the version range. The
custom RegisterPrioritisedList<T> extension builds on Meta<T> and WithMetadata.
Both APIs are stable.

Verified: clean rebuild with no new warnings. 26 tests pass covering the custom
registration ordering and flavour resolution.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Continues the dependency work on net8 before the target framework changes.

System.Linq.Async goes to 7.0.1. .NET 10 pulls System.Linq.AsyncEnumerable into the
core libraries. 7.0.1 is the version Octopus Server runs against net10.

Serilog collapses a three-way version spread onto 4.0.2. Microsoft.Extensions.Http
and Microsoft.Extensions.Logging go to 10.0.10.

System.Text.Json and System.Text.Encoding.CodePages are deliberately left alone.
Both become framework-provided on net10. Both are still required on net8. Removing
them now would downgrade System.Text.Json to the in-box 8.x version and lose
security fixes.

Verified: 165 warnings and 0 errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Flips 41 projects to net10.0 and net10.0-windows. Also updates the build scripts and
source/Directory.Build.props. Directory.Build.props compares the target framework as
a string. Calamari.AzureWebApp.NetCoreShim stays on net462. Web Deploy is
.NET Framework only.

OctoVersion.Tool 1.0.50 already ships a net10.0 folder. No tool bump was needed.

System.Text.Json and System.Text.Encoding.CodePages are removed here rather than
earlier. Both raise NU1510 on net10. Both were genuinely required on net8.

.NET 10 audits transitive packages during restore. Auditing surfaced vulnerable
dependencies that net8 never reported. The direct references that pulled them in
were upgraded rather than the audit being suppressed. WireMock.Net goes to 2.13.0.
System.DirectoryServices.AccountManagement goes to 10.0.10. RestSharp is pinned to
112.1.0. Serilog goes to 4.4.0 for Serilog.Extensions.Logging 10.0.0.

Certificate loading moves off the constructors obsoleted by SYSLIB0057. All four
call sites load .pfx files. X509CertificateLoader.LoadPkcs12FromFile and
LoadPkcs12CollectionFromFile are direct equivalents with no behaviour change.

AzureWebAppBehaviour suppresses SYSLIB0014 around its ServicePointManager callback
and carries a TODO. The callback looks like dead code. Web Deploy runs in a separate
net462 child process. A callback registered in the parent cannot affect the child.
The behaviour predates this change. Removing security-adjacent code deserves its own
reviewed commit.

Verified: 171 warnings and 0 errors on net10. build/_build.csproj clean. 24 DI and
structured-variable tests pass against net10.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
build.sh and build.ps1 hardcode the SDK channel to 8.0 in an Octopus-local
modification to the Nuke bootstrapper. The modification deliberately ignores
global.json. Builds then roll forward to the latest patch automatically. On an
agent without .NET preinstalled the bootstrapper would install an 8.0 SDK and then
fail against a global.json requiring 10.0.302.

Both bootstrappers now use the 10.0 channel.

Verified end to end. ./build.sh --target PublishCalamariProjects --target-runtime
linux-x64 succeeds. The published output is genuinely self-contained on the new
runtime. Calamari.runtimeconfig.json reports net10.0 with Microsoft.NETCore.App
10.0.10 included.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bundled dotnet-script 1.6.0 is a framework-dependent app targeting
Microsoft.NETCore.App 8.0.0. Framework-dependent apps do not roll forward across a
major version by default. On a machine with only .NET 10 installed dotnet-script
fails to launch.

Calamari itself is unaffected. Calamari ships self-contained. The failure is in the
separate dotnet-script process.

Setting DOTNET_ROLL_FORWARD=Major on that invocation lets dotnet-script run on
whatever newer runtime is present. An explicit value already set in the environment
is respected rather than overwritten.

This is a customer-facing fix as much as a CI one. Without the fix a target with
.NET 10 and no .NET 8 runtime cannot run C# script steps at all.

SilentProcessRunner applies EnvironmentVars additively. Passing a dictionary where
one was previously null does not discard the inherited environment.

Not done here: upgrading the bundled dotnet-script. The 2.0.1 release zip Calamari
vendors is still net8 targeted. Only the NuGet package ships a net10 build. The upgrade
also changes the NuGet version dotnet-script bundles. The change invalidates the
premise of UsingIsolatedAssemblyLoadContext.

Verified: 10 DotnetScriptFixture tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
.NET 10 changed Environment.SetEnvironmentVariable(name, ""). On net8 an empty
value deleted the variable. On net10 it stores an empty string. Verified directly
against 8.0.27 and 10.0.10. Passing null still deletes on both.

The proxy fixtures relied on the old behaviour. Two uses together account for 11 of
the 13 test failures in PR #2091's chain 2026.3.533.

Reset helpers passed string.Empty to mean unset. On net10 that leaves HTTP_PROXY,
HTTPS_PROXY and NO_PROXY set to "". ProxyEnvironmentVariablesGenerator tests for
presence rather than value. The generator therefore took the "proxy variables
already exist" branch and leaked empty values into every later fixture in the run.

ProxySettingsInitializerFixture passed "" for absent credentials and then asserted
BeNull(). On net10 the read returns "".

Reset helpers now pass null. Credential assertions use BeNullOrEmpty(). The assertion states the actual intent
rather than a representation detail. The setter deliberately keeps
"" so the empty-string case is now genuinely covered.

No product change and no customer-visible behaviour change. All 30 uses of the idiom
were in test code. Inherited environment variables behave identically on both
runtimes. Calamari inherits its environment in production.

Verified on net10.0 locally: ProxySettingsInitializerFixture 5/5, ScriptEngineFixture
1/1, SetupKubectlAuthenticationFixture 3/3. The five Bash and DotnetScript proxy
cases need bash, pwsh and the proxy fixture host. Those share the same teardown
mechanism and ScriptProxyFixtureBase got the same fix. CI is the confirmation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CodeGenerator.GenerateConsoleApplication hardcoded a net8-era toolchain. It wrote a
global.json pinning SDK 8.0.10 and ran dotnet new console -f net8.0. The agents
provide only a .NET 10 SDK. SDK 8.0.10 is unsatisfiable because rollForward
latestFeature only rolls in the 8.0.x band. The test failed before it could
exercise anything.

That is the ExecuteManifestCommandFixture.WithInstructions failure on the Linux
netcore-testing configs in PR #2091's chain 2026.3.534. It was the last remaining
Linux test failure after the proxy fixture fix landed.

Bumped to SDK 10.0.302 and net10.0, matching the repository's root global.json.

Verification is partial. The SDK resolution error is gone. dotnet new console -f
net10.0 succeeds and the project is created. The test still fails on an Apple
Silicon dev machine for a pre-existing and unrelated reason. RID selection a few
lines below sets osx-x64 for Mac and then overwrites it with linux-arm64 whenever
ProcessArchitecture is Arm64. That check is not nested under the IsRunningOnNix
branch. The generated app is published for linux-arm64 and exec'd on macOS. CI is
unaffected and is the confirmation for this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
No content change. Triggers the new TeamCity project
Team-Modern-Deployments / Calamari / Experiments / dotnet10, whose VCS trigger
filter is:

    -:<default>
    -:pull/*
    +:pull/2110
    +:pull/2091
    +:pull/2095
    +:pull/2094

Two things this run should establish:

1. The experiment chain picks up this PR and runs independently of vNext.
2. That the Windows test agents need the .NET 10 SDK installed. The experiment's
   "Install DotNet SDK via choco" step still runs `choco install dotnet-8.0-sdk`
   only, so Test: Windows - DotNet is expected to fail the same way it did on
   vNext chain 2026.3.534 - roughly 112 test failures with
   "Requested SDK version: 10.0.302 ... Installed SDKs:" listing only 6.0.428
   and 8.0.423, because PackageBuilder and CodeGenerator shell out to the
   machine dotnet rather than nuke's bootstrapped copy.

This commit can be dropped in a later rebase.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant