Select the newest installed .NET runtime for C# script steps, and upgrade dotnet-script to 2.0.1 - #2132
Draft
NickJosevski wants to merge 2 commits into
Draft
Select the newest installed .NET runtime for C# script steps, and upgrade dotnet-script to 2.0.1#2132NickJosevski wants to merge 2 commits into
NickJosevski wants to merge 2 commits into
Conversation
dotnet-script is a framework-dependent application requesting
Microsoft.NETCore.App 8.0.0, and the runtimeconfig.json in the zip we vendor
sets no rollForward. The default Minor policy therefore binds to an 8.x runtime
and nothing else, which has two consequences on customer targets:
- a target with only a newer runtime cannot run C# script steps at all
(exit 150, "You must install or update .NET to run this application")
- a target with both 8.x and a newer runtime silently stays on .NET 8, so
scripts keep compiling against the .NET 8 reference set
Both are live today; neither is caused by the net10 migration. The second one
is why every Octopus Cloud dynamic worker runs C# scripts on .NET 8 even though
.NET 10 is installed alongside it.
This overwrites the extracted runtimeconfig.json with a copy carrying
"rollForward": "LatestMajor", so the host binds to the newest major installed
on the target and falls back cleanly when there is nothing newer.
Verified against the built artefact, no environment variable set:
.NET 8 SDK + .NET 10 runtime -> 10.0.10 (was 8.0.29)
.NET 10 SDK only -> 10.0.10 (was exit 150)
.NET 8 SDK only -> 8.0.29 (unchanged)
Chosen over DOTNET_ROLL_FORWARD because the host reads runtimeconfig.json on
every launch path - including the Windows dotnet-script.cmd - so no change to
the invocation is needed on either platform, and the policy cannot leak into
processes the customer's script starts. It also sits below both the environment
variable and the command line in precedence, so either can still override it.
LatestMajor rather than Major: Major selects the *lowest* higher major, so on a
target with .NET 9 and .NET 10 installed it would pick 9.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replaces the vendored release zip. Note this is the GitHub release zip, which is still net8-targeted with no rollForward - only the NuGet package ships tools/net10.0 - so this upgrade does not remove the need for the roll-forward policy in the previous commit. Verified: 2.0.1 with no policy still exits 150 on a .NET-10-only target, exactly as 1.6.0 did. Measured on a .NET 8 SDK-only target, 2.0.1 vs 1.6.0: the real Bootstrap.csx plus 17 BCL idiom probes are byte-identical apart from the machine name, and both exit 0. Two things do change, and both reach net8 customers as well as net10 ones: 1. Isolated assembly load context is now the default. A script referencing a package that dotnet-script also carries used to bind to dotnet-script's copy (#r "nuget: Newtonsoft.Json, 9.0.1" resolved to 13.0.0.0); under isolation it gets the version it asked for. This commit passes --disable-isolated-load-context to hold the existing behaviour, so the upgrade does not silently change what customer scripts run against. It is skipped when the caller already passed --isolated-load-context, which the existing FormatCommandArguments test cases show customers do: when both flags are present the disable wins, so adding ours unconditionally would override the customers who deliberately opted in. Covered by two new tests. 2. Roslyn moves 4.11 -> 5.0.0-2.final, which raises the C# language ceiling under customer scripts independent of runtime. The C# 14 `field` keyword fails to compile on 1.6.0 and compiles on 2.0.1, on the same .NET 8. That is a widening rather than a break, but it is one-way: scripts written against 2.0.1 will not compile if the bundle is rolled back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft — alternative mechanism to #2095, plus the dotnet-script version bump. Two independent commits; see "Should this be one PR?" below.
Why
dotnet-scriptis framework-dependent onMicrosoft.NETCore.App 8.0.0and the runtimeconfig in the zip we vendor sets norollForward, so the defaultMinorpolicy binds to an 8.x runtime and nothing else. Two live consequences, neither caused by the net10 migration:You must install or update .NET to run this applicationThe second is why every Octopus Cloud dynamic worker runs C# scripts on .NET 8 today even with .NET 10 installed alongside. It also means that when .NET 8 goes EOL (2026-11-10) and images stop shipping the 8.x runtime, every C# script step jumps to .NET 10 at once, at a moment nobody chose.
Calamari's own TFM has no bearing on any of this — Calamari ships self-contained with no
dotnetmuxer, so it launches dotnet-script using the target'sdotnet.Commit 1 —
rollForward: LatestMajorOverwrites the extracted
runtimeconfig.jsonwith a copy carrying"rollForward": "LatestMajor". Verified against the built artefact with no environment variable set:Why this instead of
DOTNET_ROLL_FORWARD(#2095):runtimeconfig.jsonon every launch path, including the Windowsdotnet-script.cmd, so no invocation change is needed on either platform.DOTNET_ROLL_FORWARDis inherited by every process the customer's script starts — measured: a grandchild process seesDOTNET_ROLL_FORWARD=Major. A script shelling out to its own framework-dependent .NET 8 tool would make that tool roll forward too.WithDotnetRollForwardintends not to override an explicit value, but checks only the passed dictionary, neverEnvironment.GetEnvironmentVariable— andSilentProcessRunnerwrites that dictionary over the inherited environment. A machine-levelDOTNET_ROLL_FORWARDset by a target admin is silently overridden today.LatestMajor, notMajor:Majorselects the lowest higher major. On a target with .NET 9 and .NET 10 installed it picks 9 (measured).LatestMajorpicks 10, and on a net8-only target still falls back to 8 — so it cannot regress a target that works today.Commit 2 — dotnet-script 1.6.0 → 2.0.1
Heads up: this is the GitHub release zip, which is still net8-targeted with no
rollForward. Only the NuGet package shipstools/net10.0. So the upgrade does not remove the need for commit 1 — verified, 2.0.1 with no policy still exits 150 on a .NET-10-only target.On a .NET 8 SDK-only target, 2.0.1 vs 1.6.0 running the real
Bootstrap.csxplus 17 BCL idiom probes: identical apart from the machine name, both exit 0. Two things do change, and both reach net8 customers too:#r "nuget: Newtonsoft.Json, 9.0.1"resolved to13.0.0.0— and under isolation it gets 9.0.1. This commit passes--disable-isolated-load-contextto hold existing behaviour, skipped when the caller already passed--isolated-load-context: when both flags are present the disable wins, so adding ours unconditionally would override the customers who deliberately opted in. The existing test cases show customers do pass it. Two new tests cover both branches.fieldkeyword fails to compile on 1.6.0 and compiles on 2.0.1, on the same .NET 8. A widening rather than a break, but one-way: scripts written against 2.0.1 won't compile if the bundle is rolled back.Should this be one PR?
My recommendation is no — commit 2 should ship in its own release. Commit 1 is reversible and measured-nil in script behaviour; commit 2 carries two semantic changes that land on net8 and net10 targets alike. Shipping them together makes any resulting support ticket ambiguous between "the runtime moved" and "the tool moved". They are deliberately separate commits so
git cherry-picksplits them cleanly. Combined here because that is what was asked for.Verification
Container matrix over controlled .NET install states (arm64, Linux), driving the vendored tool and the built artefact. Raw output and harness in the
CalamariDotNet10project:test-plan/harness/dotnet-script/—runmode.sh,out/runtime-policy-matrix.txt,out/ds201-on-net8.txt.Not verified: Windows end-to-end (the
runtimeconfigmechanism is platform-independent by construction, but that is reasoning, not measurement); amd64; the locally-installed-dotnet-script-on-PATH branch ofDotnetScriptExecutor, which none of these mechanisms reach. The ALC finding is one package pair, not a survey.🤖 Generated with Claude Code