Skip to content

Bundle dotnet-script 2.0.1 and roll forward on targets without .NET 8 - #2135

Draft
NickJosevski wants to merge 2 commits into
mainfrom
nj/dotnet-script-201-rollforward-consolidated
Draft

Bundle dotnet-script 2.0.1 and roll forward on targets without .NET 8#2135
NickJosevski wants to merge 2 commits into
mainfrom
nj/dotnet-script-201-rollforward-consolidated

Conversation

@NickJosevski

@NickJosevski NickJosevski commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Supersedes #2132, #2133 and #2134. They will close and were to help explore the space.

Problem

  1. dotnet-script is framework-dependent.
  2. It requests Microsoft.NETCore.App 8.0.0 with no rollForward.
  3. The default policy binds to 8.x and nothing higher.
  4. A target with no 8.x runtime fails with exit 150 before any script runs.
  5. Calamari itself is self-contained and unaffected.

Upgrading to 2.0.1 does not fix the launch failure.

The 2.0.1 release zip carries a runtimeconfig byte-identical to 1.6.0's.

Commit 1: rollForward: Major

The build overwrites the extracted runtimeconfig with a copy held in source control. The vendored zip stays byte-for-byte upstream. The policy stays reviewable as text. Octopus.Action.Script.CSharp.RollForward overrides the policy per step via DOTNET_ROLL_FORWARD.

Runtimeconfig instead of a default env var (#2095) for four reasons:

  • The host reads the runtimeconfig on every launch path. That includes Windows' dotnet-script.cmd.
  • Config sits below env var and command line in precedence. The per-step override still wins.
  • An env var leaks into every process a customer's script starts. Measured on a real Windows worker (see Verification).
  • Let dotnet-script roll forward to a newer .NET runtime #2095's "don't override an explicit value" guard checks only the passed dictionary. SilentProcessRunner then writes the dictionary over the inherited environment. A machine-level DOTNET_ROLL_FORWARD is silently overridden.

Why Major and not LatestMajor

dotnet-script derives its generated csproj TFM from the loaded runtime. Measured on a container with SDK 8 only and runtimes 8 + 10:

policy result
none (today) ✅ .NET 8.0.29
Major ✅ .NET 8.0.29, no-op
LatestMajor NETSDK1045, even with zero package refs

LatestMajor rolls the host to 10. The generated project becomes net10.0. SDK 8 cannot restore a net10.0 project. LatestMajor takes C# script steps away from a target that works today.

Major fixes the reported failure. On an SDK-10-only target: exit 150 before, runs on 10.0.10 after. Everywhere 8.x exists Major is a no-op.

Commit 2: dotnet-script 2.0.1

Vendored byte-for-byte from upstream, sha256 667535727b0660223cfb9dcfc240831d8ca2a2af8f2e98214ee556334e0e1e13.

Isolation default. 2.0 makes the isolated assembly load context the default. Isolation makes packages with native binaries work (dotnet-script#763). A script also gets the package version it asked for. The cost: a type loaded via Assembly.LoadFrom is no longer reference-equal to the same type in the script's closure. This commit keeps upstream's default. Octopus.Action.Script.CSharp.DisableIsolatedLoadContext restores pre-2.0 behaviour for a step that needs the old behaviour. This decision is the one thing worth a second opinion. Dropping the commit leaves the roll-forward fix intact.

The legacy flag is stripped. 2.0 dropped --isolated-load-context. dotnet-script forwards unrecognised options into the script's argument list with exit 0. Left in place, the flag becomes Env.ScriptArgs[0] and shifts every real argument along by one. Existing test cases show customers pass the flag today. Calamari strips the flag and logs a verbose line.

Rewritten isolation tests. The old UsingIsolatedAssemblyLoadContext(False) asserted a load failure. The failure only held while the host bundled an older NuGet.Protocol than the script pinned. 2.0.1 bundles a newer copy. The default context accepts an upgrade and refuses a downgrade. The replacement tests assert the version binding in each direction: 6.10.1.5 isolated, 6.14.3.1 not.

Verification

  • DotnetScriptFixture 11/11. Bootstrapper and runtimeconfig fixtures 26/26. The proxy fixture fails the same 4 of 11 as clean main.
  • Windows, Hosted worker, through the real dotnet-script.cmd: the runtimeconfig is honoured. A 6.0.0 request with Major resolved to 8.0.27. The same request with no policy failed to launch.
  • Bonus finding on that worker: an inherited DOTNET_ROLL_FORWARD=Major outranked a LatestMajor runtimeconfig. An env-var default does leak into grandchild processes of a customer's script.

Not addressed

🤖 Generated with Claude Code

NickJosevski and others added 2 commits August 18, 2026 17:48
dotnet-script is framework-dependent and requests Microsoft.NETCore.App 8.0.0
with no rollForward, so the default Minor policy binds it to an 8.x runtime and
nothing higher. On a target with no 8.x runtime it fails to launch with exit 150
before any script runs. Calamari itself is published self-contained and is
unaffected - this is only the separate dotnet-script process.

Applies "rollForward": "Major" by overwriting the extracted runtimeconfig with a
copy held in source control, so the vendored zip stays byte-for-byte upstream and
the policy stays reviewable as text that a future re-vendor cannot silently drop.

Major rather than LatestMajor, which the decision doc recommended. LatestMajor
regresses a target that works today: dotnet-script derives its generated csproj
TFM from the loaded corelib, so rolling the host to 10 makes it emit net10.0, and
on a box with the .NET 10 runtime but only the .NET 8 SDK the restore then fails
with NETSDK1045. Measured in a container with runtimes 10.0.11 + 8.0.29 and SDK
8.0.423 only: no policy and Major both work on 8.0.29, LatestMajor exits 1.
Major fixes the reported failure and moves nobody else.

Also adds Octopus.Action.Script.CSharp.RollForward for a per-step override, set
as DOTNET_ROLL_FORWARD only when the variable is non-empty. That outranks the
runtimeconfig and is the only mechanism that reaches a dotnet-script the customer
installed themselves, which Calamari prefers over the bundled copy.

Verified end to end on a Hosted Windows worker through the real dotnet-script.cmd
launch path: a runtimeconfig requesting framework 6.0.0 with no policy fails to
launch, and the same config with rollForward Major starts on 8.0.27.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The zip is vendored byte-for-byte from the upstream GitHub release
(sha256 6675357...e0e1e13, verifiable against the release asset). Its
runtimeconfig is byte-identical to 1.6.0's, so the roll-forward policy in the
previous commit is still needed and is unaffected by the bump.

Takes upstream's 2.0 default of an isolated assembly load context, which fixes
dotnet-script #763 - native NuGet assets (SQLite, SkiaSharp,
Microsoft.Data.SqlClient) have never worked in C# script steps - and gives a
script the package version it asked for instead of whichever version
dotnet-script itself carries. The cost is that a type loaded via
Assembly.LoadFrom is no longer reference-equal to the same type in the script's
own closure, so Octopus.Action.Script.CSharp.DisableIsolatedLoadContext restores
the old behaviour for a step that needs it.

Also strips the 1.6.0 --isolated-load-context flag from a step's script
parameters. 2.0 dropped that option, and dotnet-script forwards options it does
not recognise into the *script's* argument list rather than rejecting them -
measured on 1.6.0 and 2.0.1 alike, silently and with exit 0. Left in place the
flag becomes Env.ScriptArgs[0] and shifts every real argument along by one, which
would silently break the existing steps that pass it. Isolation is the default
now, so dropping the flag preserves what those steps asked for.

The two UsingIsolatedAssemblyLoadContext cases are replaced rather than updated.
The old test asserted that isolation *off* fails outright, which held only
because 1.6.0 bundled NuGet 6.10.0.107 - lower than the 6.10.1.5 the script pins,
and the default load context refuses a downgrade while accepting an upgrade.
2.0.1 bundles 6.14.3.1, so the same collision resolves silently to the wrong
assembly. The replacements assert the version binding in each direction, so they
test the load context rather than an accident of which version is vendored.

Separately shippable: dropping this commit leaves the roll-forward fix intact.
Roslyn 4.11 -> 5.0.0-2.final also raises the C# ceiling from 13 to 14, which is a
widening but a one-way door - a script using C# 14 will not compile if the bundle
is ever rolled back.

DotnetScriptFixture is 11/11 and DotnetScriptBootstrapperFixture plus
DotnetScriptRuntimeConfigFixture are 26/26. DotnetScriptProxyFixture fails the
same 4 of 11 as clean main, verified side by side.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@NickJosevski
NickJosevski force-pushed the nj/dotnet-script-201-rollforward-consolidated branch from 6fd826a to a7a86e4 Compare August 18, 2026 07:48
@NickJosevski NickJosevski changed the title Bundle dotnet-script 2.0.1 and roll forward to the newest supported runtime Bundle dotnet-script 2.0.1 and roll forward on targets without .NET 8 Aug 19, 2026
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