From 161112d7f3a6b140ef10fde520ac07a7c2fd33c2 Mon Sep 17 00:00:00 2001 From: Nick Josevski Date: Tue, 11 Aug 2026 15:39:59 +1000 Subject: [PATCH] Let dotnet-script roll forward to a newer .NET runtime The bundled dotnet-script is framework-dependent and targets Microsoft.NETCore.App 8.0.0. Framework-dependent apps do not cross a major version boundary by default, so on a machine that has .NET 10 but no .NET 8 it fails to launch with "You must install or update .NET to run this application". Setting DOTNET_ROLL_FORWARD=Major on the dotnet-script invocation lets it run on whatever newer runtime is present. The variable only engages when the requested major is absent, so where a .NET 8 runtime exists the resolved runtime is unchanged. An explicit value already set in the environment is respected rather than overwritten. Every configuration this alters is one where dotnet-script fails to launch today, so nothing can go from working to broken. Calamari itself is unaffected either way - it ships self-contained and carries its own runtime. This is only about the separate dotnet-script process. Fixes a live bug rather than only preparing for .NET 10: worker-tools:ubuntu.24.04 ships SDK 10 with no .NET 8 runtime, so C# script steps cannot run there today. Scope is launch only. A script using #r "nuget:" on a framework-provided package still fails under the .NET 10 SDK; that needs the bundled dotnet-script upgraded to 2.0.x and is tracked separately. Verified: 10 DotnetScriptFixture tests pass. Co-Authored-By: Claude Opus 5 (1M context) --- .../DotnetScript/DotnetScriptExecutor.cs | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/source/Calamari.Common/Features/Scripting/DotnetScript/DotnetScriptExecutor.cs b/source/Calamari.Common/Features/Scripting/DotnetScript/DotnetScriptExecutor.cs index 1df9532fc5..9e783acafd 100644 --- a/source/Calamari.Common/Features/Scripting/DotnetScript/DotnetScriptExecutor.cs +++ b/source/Calamari.Common/Features/Scripting/DotnetScript/DotnetScriptExecutor.cs @@ -11,6 +11,8 @@ namespace Calamari.Common.Features.Scripting.DotnetScript { public class DotnetScriptExecutor : ScriptExecutor { + const string DotnetRollForwardVariableName = "DOTNET_ROLL_FORWARD"; + readonly ICommandLineRunner commandLineRunner; public DotnetScriptExecutor(ICommandLineRunner commandLineRunner, ILog log): base(log) @@ -37,13 +39,37 @@ protected override IEnumerable PrepareExecution(Script script, bool.TryParse(variables.Get("Octopus.Action.Script.CSharp.BypassIsolation", "false"), out var bypassDotnetScriptIsolation); var cli = CreateCommandLineInvocation(executable, arguments, !string.IsNullOrWhiteSpace(localDotnetScriptPath)); - cli.EnvironmentVars = environmentVars; + cli.EnvironmentVars = WithDotnetRollForward(environmentVars); cli.WorkingDirectory = workingDirectory; cli.Isolate = !bypassDotnetScriptIsolation; yield return new ScriptExecution(cli, otherTemporaryFiles.Concat(new[] { bootstrapFile, configurationFile })); } + /// + /// dotnet-script is a framework-dependent application - the bundled copy targets + /// Microsoft.NETCore.App 8.0.0. By default a framework-dependent app will not roll forward + /// across a major version, so on a machine that only has a newer runtime installed it fails + /// to launch with "You must install or update .NET to run this application". + /// + /// Calamari itself is published self-contained and carries no such requirement; this affects + /// only the separate dotnet-script process. Setting DOTNET_ROLL_FORWARD=Major lets it run on + /// whatever newer runtime is present, so C# script steps don't additionally require the exact + /// runtime dotnet-script was built against. + /// + static Dictionary WithDotnetRollForward(Dictionary? environmentVars) + { + var vars = environmentVars == null + ? new Dictionary() + : new Dictionary(environmentVars); + + // Don't override an explicit value - the surrounding environment may have set one deliberately. + if (!vars.ContainsKey(DotnetRollForwardVariableName)) + vars[DotnetRollForwardVariableName] = "Major"; + + return vars; + } + private string GetExecutable(string? localDotnetScriptPath, string bundledExecutable) { return string.IsNullOrWhiteSpace(localDotnetScriptPath)