Bundle dotnet-script 2.0.1 and set roll-forward in its runtimeconfig - #2133
Draft
NickJosevski wants to merge 1 commit into
Draft
Bundle dotnet-script 2.0.1 and set roll-forward in its runtimeconfig#2133NickJosevski wants to merge 1 commit into
NickJosevski wants to merge 1 commit into
Conversation
An alternative to #2095. Same problem, different mechanism, plus the 2.0.1 bump on top. dotnet-script is framework-dependent on Microsoft.NETCore.App 8.0.0 with no rollForward of its own, so on a target that has only a newer runtime it fails to launch: exit 150, "You must install or update .NET to run this application". This is true of the 2.0.1 zip as well, so bumping the version does not fix it and the two changes are independent. Rather than setting DOTNET_ROLL_FORWARD in the child process environment, ship rollForward: LatestMajor in the vendored dotnet-script.runtimeconfig.json. The host reads that file on every launch path, so it covers Windows (dotnet-script.cmd) and Linux (dotnet "<path>/dotnet-script.dll") with no C# change. It is also process-scoped: an environment variable leaks into every process the customer's script starts, so a script shelling out to its own framework-dependent .NET 8 tool would silently start rolling forward too. LatestMajor rather than Major. Major is a no-op whenever an 8.x runtime is present, which is the shape of every Octopus Cloud worker, and when it does engage it picks the lowest higher major rather than the newest. LatestMajor degrades silently to 8 on a net8-only target, so it cannot regress a target that works today. Two variables, both no-ops when unset: - Octopus.Action.Script.CSharp.RollForward overrides the policy per step. It goes through DOTNET_ROLL_FORWARD, which outranks the runtimeconfig and is the only mechanism that reaches a dotnet-script the customer installed themselves, since Calamari prefers that over the bundled copy. - Octopus.Action.Script.CSharp.DisableIsolatedLoadContext restores pre-2.0 assembly load context behaviour. 2.0 makes the isolated context the default. That is what makes native NuGet assets work, but it also means a type loaded via Assembly.LoadFrom is no longer reference-equal to the same type in the script's own closure. Verified against the real build output in containers, linux/arm64: net10 SDK only, no env var -> .NET 10.0.10, generated TFM net10.0 net8 SDK only, no env var -> .NET 8.0.29, generated TFM net8.0 net10 SDK only, Minor -> exit 150 (override beats the config) 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 — an alternative to #2095, for discussion. Same problem, different delivery mechanism, plus the dotnet-script 2.0.1 bump on top. Everything below was measured in containers on linux/arm64; nothing is inferred.
The two changes are independent
dotnet-scriptis framework-dependent onMicrosoft.NETCore.App 8.0.0and declares norollForward, so the default policy isMinor— any 8.x, nothing higher. On a target that has only a newer runtime it fails to launch:The 2.0.1 zip behaves identically. Its runtimeconfig is still
"tfm": "net8.0"/"version": "8.0.0"with norollForward, and it fails with the same exit 150 and the same requested framework version. So bumping the version does not fix the launch problem, and the roll-forward fix is needed either way. (The 2.0.1 NuGet package does shiptools/net10.0, but all three tool folders are framework-dependent with norollForward— vendoring that one would hard-require .NET 10 and take C# steps away from every net8-only customer.)Roll-forward via runtimeconfig instead of an environment variable
Ship
rollForward: LatestMajorin the vendoreddotnet-script.runtimeconfig.json, copied over the zip's own copy after<Unzip>inIncludeDotNetScript.targets.Three reasons to prefer this over
DOTNET_ROLL_FORWARD:dotnet-script.cmdon Windows,dotnet "<path>/dotnet-script.dll"on Linux. The host reads the runtimeconfig on both paths.cli.EnvironmentVarsis inherited by every process the customer's script starts. A script that shells out to its own framework-dependent .NET 8 tool would silently start rolling forward too.LatestMajor, notMajorMajoronly engages when the requested major is absent, and then picks the lowest higher major. On a box with .NET 8 and 10 — the shape of every Octopus Cloud worker — it is a no-op.LatestMajordegrades silently to 8 when nothing newer is installed, so it cannot regress a target that works today.Also worth fixing in #2095 regardless
WithDotnetRollForwardintends "don't override an explicit value — the surrounding environment may have set one deliberately", but it checks only the passed dictionary, neverEnvironment.GetEnvironmentVariable.SilentProcessRunner.cs:100-102then writes that dictionary over a blockUseShellExecute = falsehas already seeded from the parent, so a machine-levelDOTNET_ROLL_FORWARDset by a target administrator is silently overridden. Moving the default into the runtimeconfig removes the class of bug, since config sits below both env and command line.What the 2.0.1 bump actually changes
Measured head-to-head against 1.6.0 on a .NET 8 SDK-only target. Launch, restore, generated TFM (
net8.0),#r "nuget:",#load,AssemblyResolve, proxy,Process.Startand argument passthrough are all unchanged. Two real deltas:1. The isolated assembly load context becomes the default (the flag inverted from
--isolated-load-contextto--disable-isolated-load-context). This is a single toggle with a genuine trade-off on both sides, and it is not new in 2.0 — 1.6.0 has the same behaviour behind the opt-in flag. 2.0 only flips the default:Microsoft.Data.SqlClient)Assembly.LoadFromthen castTypeInitializationExceptionInvalidCastExceptionVerified in all four cells, including 1.6.0 with
--isolated-load-contextand 2.0.1 with--disable-isolated-load-context. So taking the 2.0 default fixes dotnet-script#763 — native packages have never worked in C# script steps — at the cost of breaking scripts that hand-load an assembly and cast the result. Type identity for#r "nuget:"resolved by name is unaffected; it is specifically load-from-path-then-cast.This PR takes upstream's default and adds an escape hatch rather than silently picking a side. This is the main thing worth a second opinion.
2. The C# language ceiling moves 13 → 14. Roslyn is vendored inside the zip with
LanguageVersion.Preview, so this follows the bump, not the runtime.fieldkeyword:CS0103on 1.6.0, compiles and runs on 2.0.1. Not a break, but a one-way door — once a customer uses C# 14 you cannot revert the zip.Variables added
Both are no-ops when unset.
Octopus.Action.Script.CSharp.RollForwardDOTNET_ROLL_FORWARD. Deliberately the env var and not the config: it outranks the runtimeconfig, and it is the only mechanism that reaches a dotnet-script the customer installed themselves — which Calamari prefers over the bundled copy.Octopus.Action.Script.CSharp.DisableIsolatedLoadContextVerification
Against the real build output (
source/Calamari/bin/Debug/net8.0/dotnet-script/), no environment variables set:.NET 10.0.10, generated TFMnet10.0✅.NET 8.0.29, generated TFMnet8.0✅ clean no-opDOTNET_ROLL_FORWARD=MinorCalamari.Commonbuilds with no new warnings;DotnetScriptBootstrapperFixture10/10 pass.Not verified
dotnet-script.cmdis one line,@dotnet exec "%~dp0dotnet-script.dll" %*— but that is reasoning, not measurement. The main gap.LatestMajorshould not select a prerelease runtime withoutrollForwardToPrerelease, so a target with a .NET 11 preview should be unaffected. Untested, and the one scenario whereLatestMajorcould surprise us.Related
dotnet restoreon every execution, so a runtime-only target has never been able to run a C# script step. That is unchanged by anything here.🤖 Generated with Claude Code