From 4c5eac9dc3134c4d704ce277392f6ef4d09308e0 Mon Sep 17 00:00:00 2001 From: Salman Muin Kayser Chishti Date: Mon, 16 Mar 2026 14:03:17 +0000 Subject: [PATCH 1/6] Node 24 enforcement + Linux ARM32 deprecation support --- src/Runner.Common/Constants.cs | 8 ++ src/Runner.Common/Util/NodeUtil.cs | 44 ++++++- src/Runner.Worker/ExecutionContext.cs | 3 + src/Runner.Worker/GlobalContext.cs | 1 + src/Runner.Worker/Handlers/HandlerFactory.cs | 52 ++++++-- src/Runner.Worker/JobExtension.cs | 11 +- src/Test/L0/Worker/HandlerFactoryL0.cs | 119 +++++++++++++++++++ src/Test/L0/Worker/StepHostNodeVersionL0.cs | 102 ++++++++++++++++ 8 files changed, 321 insertions(+), 19 deletions(-) diff --git a/src/Runner.Common/Constants.cs b/src/Runner.Common/Constants.cs index c3d56ec3c41..4b0cbc15780 100644 --- a/src/Runner.Common/Constants.cs +++ b/src/Runner.Common/Constants.cs @@ -195,8 +195,16 @@ public static class NodeMigration public static readonly string RequireNode24Flag = "actions.runner.requirenode24"; public static readonly string WarnOnNode20Flag = "actions.runner.warnonnode20"; + // Feature flags for Linux ARM32 deprecation + public static readonly string DeprecateLinuxArm32Flag = "actions_runner_deprecate_linux_arm32"; + public static readonly string KillLinuxArm32Flag = "actions_runner_kill_linux_arm32"; + // Blog post URL for Node 20 deprecation public static readonly string Node20DeprecationUrl = "https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/"; + + // Linux ARM32 deprecation date (TBD - placeholder for October 2026) + public static readonly string LinuxArm32DeprecationDate = "October 2026"; + public static readonly string LinuxArm32DeprecationMessage = "Linux ARM32 runners are deprecated and will no longer be supported after {0}. Please migrate to a supported platform."; } public static readonly string InternalTelemetryIssueDataKey = "_internal_telemetry"; diff --git a/src/Runner.Common/Util/NodeUtil.cs b/src/Runner.Common/Util/NodeUtil.cs index ff1a7a0af53..682092a92ed 100644 --- a/src/Runner.Common/Util/NodeUtil.cs +++ b/src/Runner.Common/Util/NodeUtil.cs @@ -58,7 +58,7 @@ public static (string nodeVersion, string warningMessage) DetermineActionsNodeVe { return (Constants.Runner.NodeMigration.Node24, null); } - + // Get environment variable details with source information var forceNode24Details = GetEnvironmentVariableDetails( Constants.Runner.NodeMigration.ForceNode24Variable, workflowEnvironment); @@ -108,14 +108,48 @@ public static (string nodeVersion, string warningMessage) DetermineActionsNodeVe /// /// Checks if Node24 is requested but running on ARM32 Linux, and determines if fallback is needed. + /// Also handles ARM32 deprecation and kill switch phases. /// /// The preferred Node version + /// Feature flag indicating ARM32 Linux is deprecated + /// Feature flag indicating ARM32 Linux should no longer work /// A tuple containing the adjusted node version and an optional warning message - public static (string nodeVersion, string warningMessage) CheckNodeVersionForLinuxArm32(string preferredVersion) + public static (string nodeVersion, string warningMessage) CheckNodeVersionForLinuxArm32( + string preferredVersion, + bool deprecateArm32 = false, + bool killArm32 = false) { - if (string.Equals(preferredVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase) && - Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.Arm) && - Constants.Runner.Platform.Equals(Constants.OSPlatform.Linux)) + bool isArm32Linux = Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.Arm) && + Constants.Runner.Platform.Equals(Constants.OSPlatform.Linux); + + if (!isArm32Linux) + { + return (preferredVersion, null); + } + + // ARM32 kill switch: runner should no longer work on this platform + if (killArm32) + { + return (null, "Linux ARM32 runners are no longer supported. Please migrate to a supported platform."); + } + + // ARM32 deprecation warning: continue using node20 but warn about upcoming end of support + if (deprecateArm32) + { + string deprecationWarning = string.Format( + Constants.Runner.NodeMigration.LinuxArm32DeprecationMessage, + Constants.Runner.NodeMigration.LinuxArm32DeprecationDate); + + if (string.Equals(preferredVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase)) + { + return (Constants.Runner.NodeMigration.Node20, deprecationWarning); + } + + return (preferredVersion, deprecationWarning); + } + + // Legacy behavior: fall back to node20 if node24 was requested on ARM32 + if (string.Equals(preferredVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase)) { return (Constants.Runner.NodeMigration.Node20, "Node 24 is not supported on Linux ARM32 platforms. Falling back to Node 20."); } diff --git a/src/Runner.Worker/ExecutionContext.cs b/src/Runner.Worker/ExecutionContext.cs index f4a020c475e..6a7dd7c674b 100644 --- a/src/Runner.Worker/ExecutionContext.cs +++ b/src/Runner.Worker/ExecutionContext.cs @@ -854,6 +854,9 @@ public void InitializeJob(Pipelines.AgentJobRequestMessage message, Cancellation // Track Node.js 20 actions for deprecation warning Global.DeprecatedNode20Actions = new HashSet(StringComparer.OrdinalIgnoreCase); + // Track actions upgraded from Node.js 20 to Node.js 24 + Global.UpgradedToNode24Actions = new HashSet(StringComparer.OrdinalIgnoreCase); + // Job Outputs JobOutputs = new Dictionary(StringComparer.OrdinalIgnoreCase); diff --git a/src/Runner.Worker/GlobalContext.cs b/src/Runner.Worker/GlobalContext.cs index 27c326d68f9..6981da19517 100644 --- a/src/Runner.Worker/GlobalContext.cs +++ b/src/Runner.Worker/GlobalContext.cs @@ -34,5 +34,6 @@ public sealed class GlobalContext public bool HasDeprecatedSetOutput { get; set; } public bool HasDeprecatedSaveState { get; set; } public HashSet DeprecatedNode20Actions { get; set; } + public HashSet UpgradedToNode24Actions { get; set; } } } diff --git a/src/Runner.Worker/Handlers/HandlerFactory.cs b/src/Runner.Worker/Handlers/HandlerFactory.cs index e9e2a5a6011..6ddec2627bf 100644 --- a/src/Runner.Worker/Handlers/HandlerFactory.cs +++ b/src/Runner.Worker/Handlers/HandlerFactory.cs @@ -66,18 +66,10 @@ public IHandler Create( } // Track Node.js 20 actions for deprecation annotation - if (string.Equals(nodeData.NodeVersion, Constants.Runner.NodeMigration.Node20, StringComparison.InvariantCultureIgnoreCase)) - { - bool warnOnNode20 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.WarnOnNode20Flag) ?? false; - if (warnOnNode20) - { - string actionName = GetActionName(action); - if (!string.IsNullOrEmpty(actionName)) - { - executionContext.Global.DeprecatedNode20Actions?.Add(actionName); - } - } - } + // Note: tracking happens before potential upgrade to node24 + // Actions that get upgraded will be moved to UpgradedToNode24Actions below + bool warnOnNode20 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.WarnOnNode20Flag) ?? false; + string actionName = GetActionName(action); // Check if node20 was explicitly specified in the action // We don't modify if node24 was explicitly specified @@ -85,9 +77,19 @@ public IHandler Create( { bool useNode24ByDefault = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.UseNode24ByDefaultFlag) ?? false; bool requireNode24 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.RequireNode24Flag) ?? false; + bool deprecateArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.DeprecateLinuxArm32Flag) ?? false; + bool killArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.KillLinuxArm32Flag) ?? false; var (nodeVersion, configWarningMessage) = NodeUtil.DetermineActionsNodeVersion(environment, useNode24ByDefault, requireNode24); - var (finalNodeVersion, platformWarningMessage) = NodeUtil.CheckNodeVersionForLinuxArm32(nodeVersion); + var (finalNodeVersion, platformWarningMessage) = NodeUtil.CheckNodeVersionForLinuxArm32(nodeVersion, deprecateArm32, killArm32); + + // ARM32 kill switch: fail the step + if (finalNodeVersion == null) + { + executionContext.Error(platformWarningMessage); + throw new InvalidOperationException(platformWarningMessage); + } + nodeData.NodeVersion = finalNodeVersion; if (!string.IsNullOrEmpty(configWarningMessage)) @@ -100,6 +102,21 @@ public IHandler Create( executionContext.Warning(platformWarningMessage); } + // Track actions based on their final node version + if (!string.IsNullOrEmpty(actionName)) + { + if (string.Equals(finalNodeVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase)) + { + // Action was upgraded from node20 to node24 + executionContext.Global.UpgradedToNode24Actions?.Add(actionName); + } + else if (warnOnNode20) + { + // Action is still running on node20 (e.g., ARM32 fallback) + executionContext.Global.DeprecatedNode20Actions?.Add(actionName); + } + } + // Show information about Node 24 migration in Phase 2 if (useNode24ByDefault && !requireNode24 && string.Equals(finalNodeVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase)) { @@ -109,6 +126,15 @@ public IHandler Create( executionContext.Output(infoMessage); } } + else if (warnOnNode20 && string.Equals(nodeData.NodeVersion, Constants.Runner.NodeMigration.Node20, StringComparison.InvariantCultureIgnoreCase)) + { + // This handles the case where nodeData.NodeVersion is still node20 but wasn't caught above + // (shouldn't normally happen, but kept for safety) + if (!string.IsNullOrEmpty(actionName)) + { + executionContext.Global.DeprecatedNode20Actions?.Add(actionName); + } + } (handler as INodeScriptActionHandler).Data = nodeData; } diff --git a/src/Runner.Worker/JobExtension.cs b/src/Runner.Worker/JobExtension.cs index c210ebeb80a..f981e750eef 100644 --- a/src/Runner.Worker/JobExtension.cs +++ b/src/Runner.Worker/JobExtension.cs @@ -736,7 +736,7 @@ public async Task FinalizeJob(IExecutionContext jobContext, Pipelines.AgentJobRe } } - // Add deprecation warning annotation for Node.js 20 actions + // Add deprecation warning annotation for Node.js 20 actions (Phase 1 - actions still running on node20) if (context.Global.DeprecatedNode20Actions?.Count > 0) { var sortedActions = context.Global.DeprecatedNode20Actions.OrderBy(a => a, StringComparer.OrdinalIgnoreCase); @@ -744,6 +744,15 @@ public async Task FinalizeJob(IExecutionContext jobContext, Pipelines.AgentJobRe var deprecationMessage = $"Node.js 20 actions are deprecated. The following actions are running on Node.js 20 and may not work as expected: {actionsList}. Actions will be forced to run with Node.js 24 by default starting June 2nd, 2026. Please check if updated versions of these actions are available that support Node.js 24. To opt into Node.js 24 now, set the FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true environment variable on the runner or in your workflow file. Once Node.js 24 becomes the default, you can temporarily opt out by setting ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true. For more information see: {Constants.Runner.NodeMigration.Node20DeprecationUrl}"; context.Warning(deprecationMessage); } + + // Add annotation for actions upgraded from Node.js 20 to Node.js 24 (Phase 2/3) + if (context.Global.UpgradedToNode24Actions?.Count > 0) + { + var sortedActions = context.Global.UpgradedToNode24Actions.OrderBy(a => a, StringComparer.OrdinalIgnoreCase); + var actionsList = string.Join(", ", sortedActions); + var upgradeMessage = $"Node.js 20 is deprecated. The following actions target Node.js 20 but are being forced to run on Node.js 24: {actionsList}. For more information see: {Constants.Runner.NodeMigration.Node20DeprecationUrl}"; + context.Warning(upgradeMessage); + } } catch (Exception ex) { diff --git a/src/Test/L0/Worker/HandlerFactoryL0.cs b/src/Test/L0/Worker/HandlerFactoryL0.cs index 85a70ff5284..4bd86eee385 100644 --- a/src/Test/L0/Worker/HandlerFactoryL0.cs +++ b/src/Test/L0/Worker/HandlerFactoryL0.cs @@ -370,5 +370,124 @@ public void LocalNode20Action_TrackedWhenWarnFlagEnabled() Assert.Contains("./.github/actions/my-action", deprecatedActions); } } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Node20Action_TrackedAsUpgradedWhenUseNode24ByDefaultEnabled() + { + using (TestHostContext hc = CreateTestContext()) + { + // Arrange. + var hf = new HandlerFactory(); + hf.Initialize(hc); + + var variables = new Dictionary + { + { Constants.Runner.NodeMigration.WarnOnNode20Flag, new VariableValue("true") }, + { Constants.Runner.NodeMigration.UseNode24ByDefaultFlag, new VariableValue("true") } + }; + Variables serverVariables = new(hc, variables); + var deprecatedActions = new HashSet(StringComparer.OrdinalIgnoreCase); + var upgradedActions = new HashSet(StringComparer.OrdinalIgnoreCase); + + _ec.Setup(x => x.Global).Returns(new GlobalContext() + { + Variables = serverVariables, + EnvironmentVariables = new Dictionary(), + DeprecatedNode20Actions = deprecatedActions, + UpgradedToNode24Actions = upgradedActions + }); + + var actionRef = new RepositoryPathReference + { + Name = "actions/checkout", + Ref = "v4" + }; + + // Act. + var data = new NodeJSActionExecutionData(); + data.NodeVersion = "node20"; + var handler = hf.Create( + _ec.Object, + actionRef, + new Mock().Object, + data, + new Dictionary(), + new Dictionary(), + new Variables(hc, new Dictionary()), + "", + new List() + ) as INodeScriptActionHandler; + + // On non-ARM32 platforms, action should be upgraded to node24 + // and tracked in UpgradedToNode24Actions, NOT in DeprecatedNode20Actions + bool isArm32Linux = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.Arm && + System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux); + + if (!isArm32Linux) + { + Assert.Equal("node24", handler.Data.NodeVersion); + Assert.Contains("actions/checkout@v4", upgradedActions); + Assert.DoesNotContain("actions/checkout@v4", deprecatedActions); + } + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Node20Action_NotUpgradedWhenPhase1Only() + { + using (TestHostContext hc = CreateTestContext()) + { + // Arrange. + var hf = new HandlerFactory(); + hf.Initialize(hc); + + var variables = new Dictionary + { + { Constants.Runner.NodeMigration.WarnOnNode20Flag, new VariableValue("true") } + }; + Variables serverVariables = new(hc, variables); + var deprecatedActions = new HashSet(StringComparer.OrdinalIgnoreCase); + var upgradedActions = new HashSet(StringComparer.OrdinalIgnoreCase); + + _ec.Setup(x => x.Global).Returns(new GlobalContext() + { + Variables = serverVariables, + EnvironmentVariables = new Dictionary(), + DeprecatedNode20Actions = deprecatedActions, + UpgradedToNode24Actions = upgradedActions + }); + + var actionRef = new RepositoryPathReference + { + Name = "actions/checkout", + Ref = "v4" + }; + + // Act. + var data = new NodeJSActionExecutionData(); + data.NodeVersion = "node20"; + var handler = hf.Create( + _ec.Object, + actionRef, + new Mock().Object, + data, + new Dictionary(), + new Dictionary(), + new Variables(hc, new Dictionary()), + "", + new List() + ) as INodeScriptActionHandler; + + // In Phase 1 (no UseNode24ByDefault), action stays on node20 + // and should be in DeprecatedNode20Actions + Assert.Equal("node20", handler.Data.NodeVersion); + Assert.Contains("actions/checkout@v4", deprecatedActions); + Assert.Empty(upgradedActions); + } + } } } diff --git a/src/Test/L0/Worker/StepHostNodeVersionL0.cs b/src/Test/L0/Worker/StepHostNodeVersionL0.cs index 6ba8c9fa4e7..83a3aa93ec3 100644 --- a/src/Test/L0/Worker/StepHostNodeVersionL0.cs +++ b/src/Test/L0/Worker/StepHostNodeVersionL0.cs @@ -59,5 +59,107 @@ public void CheckNodeVersionForArm32_PassThroughNonNode24Versions() Assert.Equal("node20", nodeVersion); Assert.Null(warningMessage); } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void CheckNodeVersionForArm32_DeprecationFlagShowsWarning() + { + string preferredVersion = "node24"; + var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion, deprecateArm32: true); + + bool isArm32 = RuntimeInformation.ProcessArchitecture == Architecture.Arm || + Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE")?.Contains("ARM") == true; + bool isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + + if (isArm32 && isLinux) + { + Assert.Equal("node20", nodeVersion); + Assert.NotNull(warningMessage); + Assert.Contains("deprecated", warningMessage); + Assert.Contains("no longer be supported", warningMessage); + } + else + { + Assert.Equal("node24", nodeVersion); + Assert.Null(warningMessage); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void CheckNodeVersionForArm32_DeprecationFlagWithNode20PassesThrough() + { + // Even with deprecation flag, node20 should pass through (not downgraded further) + string preferredVersion = "node20"; + var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion, deprecateArm32: true); + + bool isArm32 = RuntimeInformation.ProcessArchitecture == Architecture.Arm || + Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE")?.Contains("ARM") == true; + bool isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + + if (isArm32 && isLinux) + { + Assert.Equal("node20", nodeVersion); + Assert.NotNull(warningMessage); + Assert.Contains("deprecated", warningMessage); + } + else + { + Assert.Equal("node20", nodeVersion); + Assert.Null(warningMessage); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void CheckNodeVersionForArm32_KillFlagReturnsNull() + { + string preferredVersion = "node24"; + var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion, killArm32: true); + + bool isArm32 = RuntimeInformation.ProcessArchitecture == Architecture.Arm || + Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE")?.Contains("ARM") == true; + bool isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + + if (isArm32 && isLinux) + { + Assert.Null(nodeVersion); + Assert.NotNull(warningMessage); + Assert.Contains("no longer supported", warningMessage); + } + else + { + Assert.Equal("node24", nodeVersion); + Assert.Null(warningMessage); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void CheckNodeVersionForArm32_KillTakesPrecedenceOverDeprecation() + { + string preferredVersion = "node20"; + var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion, deprecateArm32: true, killArm32: true); + + bool isArm32 = RuntimeInformation.ProcessArchitecture == Architecture.Arm || + Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE")?.Contains("ARM") == true; + bool isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + + if (isArm32 && isLinux) + { + Assert.Null(nodeVersion); + Assert.NotNull(warningMessage); + Assert.Contains("no longer supported", warningMessage); + } + else + { + Assert.Equal("node20", nodeVersion); + Assert.Null(warningMessage); + } + } } } From 3bd3bff38d55cdd3a40f07ec74805178fce2e8be Mon Sep 17 00:00:00 2001 From: Salman Muin Kayser Chishti Date: Mon, 16 Mar 2026 14:14:19 +0000 Subject: [PATCH 2/6] Add support for tracking actions on Node.js 20 due to ARM32 limitations --- src/Runner.Common/Util/NodeUtil.cs | 2 +- src/Runner.Worker/ExecutionContext.cs | 3 ++ src/Runner.Worker/GlobalContext.cs | 1 + src/Runner.Worker/Handlers/HandlerFactory.cs | 11 ++++-- src/Runner.Worker/Handlers/StepHost.cs | 39 +++++++++++++++++--- src/Runner.Worker/JobExtension.cs | 9 +++++ 6 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/Runner.Common/Util/NodeUtil.cs b/src/Runner.Common/Util/NodeUtil.cs index 682092a92ed..62df6b09adf 100644 --- a/src/Runner.Common/Util/NodeUtil.cs +++ b/src/Runner.Common/Util/NodeUtil.cs @@ -58,7 +58,7 @@ public static (string nodeVersion, string warningMessage) DetermineActionsNodeVe { return (Constants.Runner.NodeMigration.Node24, null); } - + // Get environment variable details with source information var forceNode24Details = GetEnvironmentVariableDetails( Constants.Runner.NodeMigration.ForceNode24Variable, workflowEnvironment); diff --git a/src/Runner.Worker/ExecutionContext.cs b/src/Runner.Worker/ExecutionContext.cs index 6a7dd7c674b..d835682a47f 100644 --- a/src/Runner.Worker/ExecutionContext.cs +++ b/src/Runner.Worker/ExecutionContext.cs @@ -857,6 +857,9 @@ public void InitializeJob(Pipelines.AgentJobRequestMessage message, Cancellation // Track actions upgraded from Node.js 20 to Node.js 24 Global.UpgradedToNode24Actions = new HashSet(StringComparer.OrdinalIgnoreCase); + // Track actions stuck on Node.js 20 due to ARM32 (separate from general deprecation) + Global.Arm32Node20Actions = new HashSet(StringComparer.OrdinalIgnoreCase); + // Job Outputs JobOutputs = new Dictionary(StringComparer.OrdinalIgnoreCase); diff --git a/src/Runner.Worker/GlobalContext.cs b/src/Runner.Worker/GlobalContext.cs index 6981da19517..64b33c755d4 100644 --- a/src/Runner.Worker/GlobalContext.cs +++ b/src/Runner.Worker/GlobalContext.cs @@ -35,5 +35,6 @@ public sealed class GlobalContext public bool HasDeprecatedSaveState { get; set; } public HashSet DeprecatedNode20Actions { get; set; } public HashSet UpgradedToNode24Actions { get; set; } + public HashSet Arm32Node20Actions { get; set; } } } diff --git a/src/Runner.Worker/Handlers/HandlerFactory.cs b/src/Runner.Worker/Handlers/HandlerFactory.cs index 6ddec2627bf..0cc2facaf7e 100644 --- a/src/Runner.Worker/Handlers/HandlerFactory.cs +++ b/src/Runner.Worker/Handlers/HandlerFactory.cs @@ -65,9 +65,7 @@ public IHandler Create( nodeData.NodeVersion = Common.Constants.Runner.NodeMigration.Node20; } - // Track Node.js 20 actions for deprecation annotation - // Note: tracking happens before potential upgrade to node24 - // Actions that get upgraded will be moved to UpgradedToNode24Actions below + // Read flags early; actionName is also resolved up front for tracking after version is determined bool warnOnNode20 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.WarnOnNode20Flag) ?? false; string actionName = GetActionName(action); @@ -110,9 +108,14 @@ public IHandler Create( // Action was upgraded from node20 to node24 executionContext.Global.UpgradedToNode24Actions?.Add(actionName); } + else if (deprecateArm32 && string.Equals(finalNodeVersion, Constants.Runner.NodeMigration.Node20, StringComparison.OrdinalIgnoreCase)) + { + // Action is on node20 because ARM32 can't run node24 + executionContext.Global.Arm32Node20Actions?.Add(actionName); + } else if (warnOnNode20) { - // Action is still running on node20 (e.g., ARM32 fallback) + // Action is still running on node20 (general case) executionContext.Global.DeprecatedNode20Actions?.Add(actionName); } } diff --git a/src/Runner.Worker/Handlers/StepHost.cs b/src/Runner.Worker/Handlers/StepHost.cs index 211009658e4..ab711ad23a5 100644 --- a/src/Runner.Worker/Handlers/StepHost.cs +++ b/src/Runner.Worker/Handlers/StepHost.cs @@ -58,8 +58,17 @@ public string ResolvePathForStepHost(IExecutionContext executionContext, string public Task DetermineNodeRuntimeVersion(IExecutionContext executionContext, string preferredVersion) { - // Use NodeUtil to check if Node24 is requested but we're on ARM32 Linux - var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion); + bool deprecateArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.DeprecateLinuxArm32Flag) ?? false; + bool killArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.KillLinuxArm32Flag) ?? false; + + var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion, deprecateArm32, killArm32); + + if (nodeVersion == null) + { + executionContext.Error(warningMessage); + throw new InvalidOperationException(warningMessage); + } + if (!string.IsNullOrEmpty(warningMessage)) { executionContext.Warning(warningMessage); @@ -142,8 +151,17 @@ public string ResolvePathForStepHost(IExecutionContext executionContext, string public async Task DetermineNodeRuntimeVersion(IExecutionContext executionContext, string preferredVersion) { - // Use NodeUtil to check if Node24 is requested but we're on ARM32 Linux - var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion); + bool deprecateArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.DeprecateLinuxArm32Flag) ?? false; + bool killArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.KillLinuxArm32Flag) ?? false; + + var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion, deprecateArm32, killArm32); + + if (nodeExternal == null) + { + executionContext.Error(warningMessage); + throw new InvalidOperationException(warningMessage); + } + if (!string.IsNullOrEmpty(warningMessage)) { executionContext.Warning(warningMessage); @@ -273,8 +291,17 @@ await containerHookManager.RunScriptStepAsync(context, private string CheckPlatformForAlpineContainer(IExecutionContext executionContext, string preferredVersion) { - // Use NodeUtil to check if Node24 is requested but we're on ARM32 Linux - var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion); + bool deprecateArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.DeprecateLinuxArm32Flag) ?? false; + bool killArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.KillLinuxArm32Flag) ?? false; + + var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion, deprecateArm32, killArm32); + + if (nodeExternal == null) + { + executionContext.Error(warningMessage); + throw new InvalidOperationException(warningMessage); + } + if (!string.IsNullOrEmpty(warningMessage)) { executionContext.Warning(warningMessage); diff --git a/src/Runner.Worker/JobExtension.cs b/src/Runner.Worker/JobExtension.cs index f981e750eef..de518114574 100644 --- a/src/Runner.Worker/JobExtension.cs +++ b/src/Runner.Worker/JobExtension.cs @@ -753,6 +753,15 @@ public async Task FinalizeJob(IExecutionContext jobContext, Pipelines.AgentJobRe var upgradeMessage = $"Node.js 20 is deprecated. The following actions target Node.js 20 but are being forced to run on Node.js 24: {actionsList}. For more information see: {Constants.Runner.NodeMigration.Node20DeprecationUrl}"; context.Warning(upgradeMessage); } + + // Add annotation for ARM32 actions stuck on Node.js 20 (ARM32 can't run node24) + if (context.Global.Arm32Node20Actions?.Count > 0) + { + var sortedActions = context.Global.Arm32Node20Actions.OrderBy(a => a, StringComparer.OrdinalIgnoreCase); + var actionsList = string.Join(", ", sortedActions); + var arm32Message = $"The following actions are running on Node.js 20 because Node.js 24 is not available on Linux ARM32: {actionsList}. Linux ARM32 runners are deprecated and will no longer be supported after {Constants.Runner.NodeMigration.LinuxArm32DeprecationDate}. Please migrate to a supported platform. For more information see: {Constants.Runner.NodeMigration.Node20DeprecationUrl}"; + context.Warning(arm32Message); + } } catch (Exception ex) { From e2e0705d81e7bfbd440a75dae0db7eec9360d53e Mon Sep 17 00:00:00 2001 From: Salman Muin Kayser Chishti Date: Tue, 17 Mar 2026 12:04:02 +0000 Subject: [PATCH 3/6] Add migration dates, consolidate ARM32 to use Node20RemovalDate --- src/Runner.Common/Constants.cs | 6 ++++-- src/Runner.Common/Util/NodeUtil.cs | 4 ++-- src/Runner.Worker/JobExtension.cs | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Runner.Common/Constants.cs b/src/Runner.Common/Constants.cs index 4b0cbc15780..a9b5dbd2cf9 100644 --- a/src/Runner.Common/Constants.cs +++ b/src/Runner.Common/Constants.cs @@ -202,8 +202,10 @@ public static class NodeMigration // Blog post URL for Node 20 deprecation public static readonly string Node20DeprecationUrl = "https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/"; - // Linux ARM32 deprecation date (TBD - placeholder for October 2026) - public static readonly string LinuxArm32DeprecationDate = "October 2026"; + // Node 20 migration dates + public static readonly string Node24DefaultDate = "June 2nd, 2026"; + public static readonly string Node20RemovalDate = "September 16th, 2026"; + public static readonly string LinuxArm32DeprecationMessage = "Linux ARM32 runners are deprecated and will no longer be supported after {0}. Please migrate to a supported platform."; } diff --git a/src/Runner.Common/Util/NodeUtil.cs b/src/Runner.Common/Util/NodeUtil.cs index 62df6b09adf..3b76d2b611a 100644 --- a/src/Runner.Common/Util/NodeUtil.cs +++ b/src/Runner.Common/Util/NodeUtil.cs @@ -58,7 +58,7 @@ public static (string nodeVersion, string warningMessage) DetermineActionsNodeVe { return (Constants.Runner.NodeMigration.Node24, null); } - + // Get environment variable details with source information var forceNode24Details = GetEnvironmentVariableDetails( Constants.Runner.NodeMigration.ForceNode24Variable, workflowEnvironment); @@ -138,7 +138,7 @@ public static (string nodeVersion, string warningMessage) CheckNodeVersionForLin { string deprecationWarning = string.Format( Constants.Runner.NodeMigration.LinuxArm32DeprecationMessage, - Constants.Runner.NodeMigration.LinuxArm32DeprecationDate); + Constants.Runner.NodeMigration.Node20RemovalDate); if (string.Equals(preferredVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase)) { diff --git a/src/Runner.Worker/JobExtension.cs b/src/Runner.Worker/JobExtension.cs index de518114574..dedea0211ca 100644 --- a/src/Runner.Worker/JobExtension.cs +++ b/src/Runner.Worker/JobExtension.cs @@ -741,7 +741,7 @@ public async Task FinalizeJob(IExecutionContext jobContext, Pipelines.AgentJobRe { var sortedActions = context.Global.DeprecatedNode20Actions.OrderBy(a => a, StringComparer.OrdinalIgnoreCase); var actionsList = string.Join(", ", sortedActions); - var deprecationMessage = $"Node.js 20 actions are deprecated. The following actions are running on Node.js 20 and may not work as expected: {actionsList}. Actions will be forced to run with Node.js 24 by default starting June 2nd, 2026. Please check if updated versions of these actions are available that support Node.js 24. To opt into Node.js 24 now, set the FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true environment variable on the runner or in your workflow file. Once Node.js 24 becomes the default, you can temporarily opt out by setting ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true. For more information see: {Constants.Runner.NodeMigration.Node20DeprecationUrl}"; + var deprecationMessage = $"Node.js 20 actions are deprecated. The following actions are running on Node.js 20 and may not work as expected: {actionsList}. Actions will be forced to run with Node.js 24 by default starting {Constants.Runner.NodeMigration.Node24DefaultDate}. Node.js 20 will be removed from the runner on {Constants.Runner.NodeMigration.Node20RemovalDate}. Please check if updated versions of these actions are available that support Node.js 24. To opt into Node.js 24 now, set the FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true environment variable on the runner or in your workflow file. Once Node.js 24 becomes the default, you can temporarily opt out by setting ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true. For more information see: {Constants.Runner.NodeMigration.Node20DeprecationUrl}"; context.Warning(deprecationMessage); } @@ -759,7 +759,7 @@ public async Task FinalizeJob(IExecutionContext jobContext, Pipelines.AgentJobRe { var sortedActions = context.Global.Arm32Node20Actions.OrderBy(a => a, StringComparer.OrdinalIgnoreCase); var actionsList = string.Join(", ", sortedActions); - var arm32Message = $"The following actions are running on Node.js 20 because Node.js 24 is not available on Linux ARM32: {actionsList}. Linux ARM32 runners are deprecated and will no longer be supported after {Constants.Runner.NodeMigration.LinuxArm32DeprecationDate}. Please migrate to a supported platform. For more information see: {Constants.Runner.NodeMigration.Node20DeprecationUrl}"; + var arm32Message = $"The following actions are running on Node.js 20 because Node.js 24 is not available on Linux ARM32: {actionsList}. Linux ARM32 runners are deprecated and will no longer be supported after {Constants.Runner.NodeMigration.Node20RemovalDate}. Please migrate to a supported platform. For more information see: {Constants.Runner.NodeMigration.Node20DeprecationUrl}"; context.Warning(arm32Message); } } From 3f201cb010ef36b22baea9f327feb818c9c7f03b Mon Sep 17 00:00:00 2001 From: Salman Muin Kayser Chishti Date: Tue, 17 Mar 2026 14:43:12 +0000 Subject: [PATCH 4/6] Read migration dates from job variables with hardcoded fallbacks --- src/Runner.Common/Constants.cs | 6 +++++- src/Runner.Common/Util/NodeUtil.cs | 5 +++-- src/Runner.Worker/Handlers/HandlerFactory.cs | 3 ++- src/Runner.Worker/Handlers/StepHost.cs | 11 +++++++---- src/Runner.Worker/JobExtension.cs | 8 ++++++-- 5 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/Runner.Common/Constants.cs b/src/Runner.Common/Constants.cs index a9b5dbd2cf9..3326e947d73 100644 --- a/src/Runner.Common/Constants.cs +++ b/src/Runner.Common/Constants.cs @@ -202,10 +202,14 @@ public static class NodeMigration // Blog post URL for Node 20 deprecation public static readonly string Node20DeprecationUrl = "https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/"; - // Node 20 migration dates + // Node 20 migration dates (hardcoded fallbacks, can be overridden via job variables) public static readonly string Node24DefaultDate = "June 2nd, 2026"; public static readonly string Node20RemovalDate = "September 16th, 2026"; + // Variable keys for server-overridable dates + public static readonly string Node24DefaultDateVariable = "actions_runner_node24_default_date"; + public static readonly string Node20RemovalDateVariable = "actions_runner_node20_removal_date"; + public static readonly string LinuxArm32DeprecationMessage = "Linux ARM32 runners are deprecated and will no longer be supported after {0}. Please migrate to a supported platform."; } diff --git a/src/Runner.Common/Util/NodeUtil.cs b/src/Runner.Common/Util/NodeUtil.cs index 3b76d2b611a..59e27a598a4 100644 --- a/src/Runner.Common/Util/NodeUtil.cs +++ b/src/Runner.Common/Util/NodeUtil.cs @@ -117,7 +117,8 @@ public static (string nodeVersion, string warningMessage) DetermineActionsNodeVe public static (string nodeVersion, string warningMessage) CheckNodeVersionForLinuxArm32( string preferredVersion, bool deprecateArm32 = false, - bool killArm32 = false) + bool killArm32 = false, + string node20RemovalDate = null) { bool isArm32Linux = Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.Arm) && Constants.Runner.Platform.Equals(Constants.OSPlatform.Linux); @@ -138,7 +139,7 @@ public static (string nodeVersion, string warningMessage) CheckNodeVersionForLin { string deprecationWarning = string.Format( Constants.Runner.NodeMigration.LinuxArm32DeprecationMessage, - Constants.Runner.NodeMigration.Node20RemovalDate); + node20RemovalDate ?? Constants.Runner.NodeMigration.Node20RemovalDate); if (string.Equals(preferredVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase)) { diff --git a/src/Runner.Worker/Handlers/HandlerFactory.cs b/src/Runner.Worker/Handlers/HandlerFactory.cs index 0cc2facaf7e..14e97fbfdfd 100644 --- a/src/Runner.Worker/Handlers/HandlerFactory.cs +++ b/src/Runner.Worker/Handlers/HandlerFactory.cs @@ -77,9 +77,10 @@ public IHandler Create( bool requireNode24 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.RequireNode24Flag) ?? false; bool deprecateArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.DeprecateLinuxArm32Flag) ?? false; bool killArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.KillLinuxArm32Flag) ?? false; + string node20RemovalDate = executionContext.Global.Variables?.Get(Constants.Runner.NodeMigration.Node20RemovalDateVariable); var (nodeVersion, configWarningMessage) = NodeUtil.DetermineActionsNodeVersion(environment, useNode24ByDefault, requireNode24); - var (finalNodeVersion, platformWarningMessage) = NodeUtil.CheckNodeVersionForLinuxArm32(nodeVersion, deprecateArm32, killArm32); + var (finalNodeVersion, platformWarningMessage) = NodeUtil.CheckNodeVersionForLinuxArm32(nodeVersion, deprecateArm32, killArm32, node20RemovalDate); // ARM32 kill switch: fail the step if (finalNodeVersion == null) diff --git a/src/Runner.Worker/Handlers/StepHost.cs b/src/Runner.Worker/Handlers/StepHost.cs index ab711ad23a5..91f3154626c 100644 --- a/src/Runner.Worker/Handlers/StepHost.cs +++ b/src/Runner.Worker/Handlers/StepHost.cs @@ -60,8 +60,9 @@ public Task DetermineNodeRuntimeVersion(IExecutionContext executionConte { bool deprecateArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.DeprecateLinuxArm32Flag) ?? false; bool killArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.KillLinuxArm32Flag) ?? false; + string node20RemovalDate = executionContext.Global.Variables?.Get(Constants.Runner.NodeMigration.Node20RemovalDateVariable); - var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion, deprecateArm32, killArm32); + var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion, deprecateArm32, killArm32, node20RemovalDate); if (nodeVersion == null) { @@ -73,7 +74,7 @@ public Task DetermineNodeRuntimeVersion(IExecutionContext executionConte { executionContext.Warning(warningMessage); } - + return Task.FromResult(nodeVersion); } @@ -153,8 +154,9 @@ public async Task DetermineNodeRuntimeVersion(IExecutionContext executio { bool deprecateArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.DeprecateLinuxArm32Flag) ?? false; bool killArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.KillLinuxArm32Flag) ?? false; + string node20RemovalDate = executionContext.Global.Variables?.Get(Constants.Runner.NodeMigration.Node20RemovalDateVariable); - var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion, deprecateArm32, killArm32); + var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion, deprecateArm32, killArm32, node20RemovalDate); if (nodeExternal == null) { @@ -293,8 +295,9 @@ private string CheckPlatformForAlpineContainer(IExecutionContext executionContex { bool deprecateArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.DeprecateLinuxArm32Flag) ?? false; bool killArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.KillLinuxArm32Flag) ?? false; + string node20RemovalDate = executionContext.Global.Variables?.Get(Constants.Runner.NodeMigration.Node20RemovalDateVariable); - var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion, deprecateArm32, killArm32); + var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion, deprecateArm32, killArm32, node20RemovalDate); if (nodeExternal == null) { diff --git a/src/Runner.Worker/JobExtension.cs b/src/Runner.Worker/JobExtension.cs index dedea0211ca..594ade59d36 100644 --- a/src/Runner.Worker/JobExtension.cs +++ b/src/Runner.Worker/JobExtension.cs @@ -736,12 +736,16 @@ public async Task FinalizeJob(IExecutionContext jobContext, Pipelines.AgentJobRe } } + // Read dates from server variables with hardcoded fallbacks + var node24DefaultDate = context.Global.Variables?.Get(Constants.Runner.NodeMigration.Node24DefaultDateVariable) ?? Constants.Runner.NodeMigration.Node24DefaultDate; + var node20RemovalDate = context.Global.Variables?.Get(Constants.Runner.NodeMigration.Node20RemovalDateVariable) ?? Constants.Runner.NodeMigration.Node20RemovalDate; + // Add deprecation warning annotation for Node.js 20 actions (Phase 1 - actions still running on node20) if (context.Global.DeprecatedNode20Actions?.Count > 0) { var sortedActions = context.Global.DeprecatedNode20Actions.OrderBy(a => a, StringComparer.OrdinalIgnoreCase); var actionsList = string.Join(", ", sortedActions); - var deprecationMessage = $"Node.js 20 actions are deprecated. The following actions are running on Node.js 20 and may not work as expected: {actionsList}. Actions will be forced to run with Node.js 24 by default starting {Constants.Runner.NodeMigration.Node24DefaultDate}. Node.js 20 will be removed from the runner on {Constants.Runner.NodeMigration.Node20RemovalDate}. Please check if updated versions of these actions are available that support Node.js 24. To opt into Node.js 24 now, set the FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true environment variable on the runner or in your workflow file. Once Node.js 24 becomes the default, you can temporarily opt out by setting ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true. For more information see: {Constants.Runner.NodeMigration.Node20DeprecationUrl}"; + var deprecationMessage = $"Node.js 20 actions are deprecated. The following actions are running on Node.js 20 and may not work as expected: {actionsList}. Actions will be forced to run with Node.js 24 by default starting {node24DefaultDate}. Node.js 20 will be removed from the runner on {node20RemovalDate}. Please check if updated versions of these actions are available that support Node.js 24. To opt into Node.js 24 now, set the FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true environment variable on the runner or in your workflow file. Once Node.js 24 becomes the default, you can temporarily opt out by setting ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true. For more information see: {Constants.Runner.NodeMigration.Node20DeprecationUrl}"; context.Warning(deprecationMessage); } @@ -759,7 +763,7 @@ public async Task FinalizeJob(IExecutionContext jobContext, Pipelines.AgentJobRe { var sortedActions = context.Global.Arm32Node20Actions.OrderBy(a => a, StringComparer.OrdinalIgnoreCase); var actionsList = string.Join(", ", sortedActions); - var arm32Message = $"The following actions are running on Node.js 20 because Node.js 24 is not available on Linux ARM32: {actionsList}. Linux ARM32 runners are deprecated and will no longer be supported after {Constants.Runner.NodeMigration.Node20RemovalDate}. Please migrate to a supported platform. For more information see: {Constants.Runner.NodeMigration.Node20DeprecationUrl}"; + var arm32Message = $"The following actions are running on Node.js 20 because Node.js 24 is not available on Linux ARM32: {actionsList}. Linux ARM32 runners are deprecated and will no longer be supported after {node20RemovalDate}. Please migrate to a supported platform. For more information see: {Constants.Runner.NodeMigration.Node20DeprecationUrl}"; context.Warning(arm32Message); } } From 35c28312577647917afcb5abb91577438dd6fa7f Mon Sep 17 00:00:00 2001 From: Salman Muin Kayser Chishti Date: Tue, 17 Mar 2026 16:14:21 +0000 Subject: [PATCH 5/6] Fix formatting and minor cleanup in HandlerFactory and tests --- src/Runner.Worker/Handlers/HandlerFactory.cs | 41 +- src/Test/L0/Worker/HandlerFactoryL0.cs | 380 +++++++++++++++++++ src/Test/L0/Worker/StepHostNodeVersionL0.cs | 54 +++ 3 files changed, 466 insertions(+), 9 deletions(-) diff --git a/src/Runner.Worker/Handlers/HandlerFactory.cs b/src/Runner.Worker/Handlers/HandlerFactory.cs index 14e97fbfdfd..8044f091da2 100644 --- a/src/Runner.Worker/Handlers/HandlerFactory.cs +++ b/src/Runner.Worker/Handlers/HandlerFactory.cs @@ -25,6 +25,14 @@ IHandler Create( public sealed class HandlerFactory : RunnerService, IHandlerFactory { + internal static bool ShouldTrackAsArm32Node20(bool deprecateArm32, string preferredNodeVersion, string finalNodeVersion, string platformWarningMessage) + { + return deprecateArm32 && + !string.IsNullOrEmpty(platformWarningMessage) && + string.Equals(preferredNodeVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase) && + string.Equals(finalNodeVersion, Constants.Runner.NodeMigration.Node20, StringComparison.OrdinalIgnoreCase); + } + public IHandler Create( IExecutionContext executionContext, Pipelines.ActionStepDefinitionReference action, @@ -67,6 +75,9 @@ public IHandler Create( // Read flags early; actionName is also resolved up front for tracking after version is determined bool warnOnNode20 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.WarnOnNode20Flag) ?? false; + bool deprecateArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.DeprecateLinuxArm32Flag) ?? false; + bool killArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.KillLinuxArm32Flag) ?? false; + string node20RemovalDate = executionContext.Global.Variables?.Get(Constants.Runner.NodeMigration.Node20RemovalDateVariable); string actionName = GetActionName(action); // Check if node20 was explicitly specified in the action @@ -75,9 +86,6 @@ public IHandler Create( { bool useNode24ByDefault = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.UseNode24ByDefaultFlag) ?? false; bool requireNode24 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.RequireNode24Flag) ?? false; - bool deprecateArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.DeprecateLinuxArm32Flag) ?? false; - bool killArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.KillLinuxArm32Flag) ?? false; - string node20RemovalDate = executionContext.Global.Variables?.Get(Constants.Runner.NodeMigration.Node20RemovalDateVariable); var (nodeVersion, configWarningMessage) = NodeUtil.DetermineActionsNodeVersion(environment, useNode24ByDefault, requireNode24); var (finalNodeVersion, platformWarningMessage) = NodeUtil.CheckNodeVersionForLinuxArm32(nodeVersion, deprecateArm32, killArm32, node20RemovalDate); @@ -109,7 +117,7 @@ public IHandler Create( // Action was upgraded from node20 to node24 executionContext.Global.UpgradedToNode24Actions?.Add(actionName); } - else if (deprecateArm32 && string.Equals(finalNodeVersion, Constants.Runner.NodeMigration.Node20, StringComparison.OrdinalIgnoreCase)) + else if (ShouldTrackAsArm32Node20(deprecateArm32, nodeVersion, finalNodeVersion, platformWarningMessage)) { // Action is on node20 because ARM32 can't run node24 executionContext.Global.Arm32Node20Actions?.Add(actionName); @@ -130,13 +138,28 @@ public IHandler Create( executionContext.Output(infoMessage); } } - else if (warnOnNode20 && string.Equals(nodeData.NodeVersion, Constants.Runner.NodeMigration.Node20, StringComparison.InvariantCultureIgnoreCase)) + else if (string.Equals(nodeData.NodeVersion, Constants.Runner.NodeMigration.Node24, StringComparison.InvariantCultureIgnoreCase)) { - // This handles the case where nodeData.NodeVersion is still node20 but wasn't caught above - // (shouldn't normally happen, but kept for safety) - if (!string.IsNullOrEmpty(actionName)) + var (finalNodeVersion, platformWarningMessage) = NodeUtil.CheckNodeVersionForLinuxArm32(nodeData.NodeVersion, deprecateArm32, killArm32, node20RemovalDate); + + // ARM32 kill switch: fail the step + if (finalNodeVersion == null) + { + executionContext.Error(platformWarningMessage); + throw new InvalidOperationException(platformWarningMessage); + } + + var preferredVersion = nodeData.NodeVersion; + nodeData.NodeVersion = finalNodeVersion; + + if (!string.IsNullOrEmpty(platformWarningMessage)) + { + executionContext.Warning(platformWarningMessage); + } + + if (!string.IsNullOrEmpty(actionName) && ShouldTrackAsArm32Node20(deprecateArm32, preferredVersion, finalNodeVersion, platformWarningMessage)) { - executionContext.Global.DeprecatedNode20Actions?.Add(actionName); + executionContext.Global.Arm32Node20Actions?.Add(actionName); } } diff --git a/src/Test/L0/Worker/HandlerFactoryL0.cs b/src/Test/L0/Worker/HandlerFactoryL0.cs index 4bd86eee385..421c9211c43 100644 --- a/src/Test/L0/Worker/HandlerFactoryL0.cs +++ b/src/Test/L0/Worker/HandlerFactoryL0.cs @@ -489,5 +489,385 @@ public void Node20Action_NotUpgradedWhenPhase1Only() Assert.Empty(upgradedActions); } } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void ExplicitNode24Action_KillArm32Flag_ThrowsOnArm32() + { + using (TestHostContext hc = CreateTestContext()) + { + // Arrange. + var hf = new HandlerFactory(); + hf.Initialize(hc); + + var variables = new Dictionary + { + { Constants.Runner.NodeMigration.KillLinuxArm32Flag, new VariableValue("true") } + }; + Variables serverVariables = new(hc, variables); + + _ec.Setup(x => x.Global).Returns(new GlobalContext() + { + Variables = serverVariables, + EnvironmentVariables = new Dictionary() + }); + + var actionRef = new RepositoryPathReference + { + Name = "actions/checkout", + Ref = "v5" + }; + + // Act - action explicitly declares node24 + var data = new NodeJSActionExecutionData(); + data.NodeVersion = "node24"; + + bool isArm32Linux = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.Arm && + System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux); + + if (isArm32Linux) + { + // On ARM32 Linux, kill flag should cause the handler to throw + Assert.Throws(() => hf.Create( + _ec.Object, + actionRef, + new Mock().Object, + data, + new Dictionary(), + new Dictionary(), + new Variables(hc, new Dictionary()), + "", + new List() + )); + } + else + { + // On other platforms, should proceed normally + var handler = hf.Create( + _ec.Object, + actionRef, + new Mock().Object, + data, + new Dictionary(), + new Dictionary(), + new Variables(hc, new Dictionary()), + "", + new List() + ) as INodeScriptActionHandler; + + Assert.Equal("node24", handler.Data.NodeVersion); + } + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void ExplicitNode24Action_DeprecateArm32Flag_DowngradesToNode20OnArm32() + { + using (TestHostContext hc = CreateTestContext()) + { + // Arrange. + var hf = new HandlerFactory(); + hf.Initialize(hc); + + var variables = new Dictionary + { + { Constants.Runner.NodeMigration.DeprecateLinuxArm32Flag, new VariableValue("true") } + }; + Variables serverVariables = new(hc, variables); + var arm32Actions = new HashSet(StringComparer.OrdinalIgnoreCase); + + _ec.Setup(x => x.Global).Returns(new GlobalContext() + { + Variables = serverVariables, + EnvironmentVariables = new Dictionary(), + Arm32Node20Actions = arm32Actions + }); + + var actionRef = new RepositoryPathReference + { + Name = "actions/checkout", + Ref = "v5" + }; + + // Act - action explicitly declares node24 + var data = new NodeJSActionExecutionData(); + data.NodeVersion = "node24"; + var handler = hf.Create( + _ec.Object, + actionRef, + new Mock().Object, + data, + new Dictionary(), + new Dictionary(), + new Variables(hc, new Dictionary()), + "", + new List() + ) as INodeScriptActionHandler; + + bool isArm32Linux = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.Arm && + System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux); + + if (isArm32Linux) + { + // On ARM32 Linux, should downgrade to node20 and track + Assert.Equal("node20", handler.Data.NodeVersion); + Assert.Contains("actions/checkout@v5", arm32Actions); + } + else + { + // On other platforms, should remain node24 + Assert.Equal("node24", handler.Data.NodeVersion); + Assert.Empty(arm32Actions); + } + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void ExplicitNode24Action_NoArm32Flags_StaysNode24() + { + using (TestHostContext hc = CreateTestContext()) + { + // Arrange. + var hf = new HandlerFactory(); + hf.Initialize(hc); + + var variables = new Dictionary(); + Variables serverVariables = new(hc, variables); + var arm32Actions = new HashSet(StringComparer.OrdinalIgnoreCase); + var deprecatedActions = new HashSet(StringComparer.OrdinalIgnoreCase); + + _ec.Setup(x => x.Global).Returns(new GlobalContext() + { + Variables = serverVariables, + EnvironmentVariables = new Dictionary(), + Arm32Node20Actions = arm32Actions, + DeprecatedNode20Actions = deprecatedActions + }); + + var actionRef = new RepositoryPathReference + { + Name = "actions/checkout", + Ref = "v5" + }; + + // Act - action explicitly declares node24, no ARM32 flags + var data = new NodeJSActionExecutionData(); + data.NodeVersion = "node24"; + var handler = hf.Create( + _ec.Object, + actionRef, + new Mock().Object, + data, + new Dictionary(), + new Dictionary(), + new Variables(hc, new Dictionary()), + "", + new List() + ) as INodeScriptActionHandler; + + // On non-ARM32 platforms, should stay node24 and not be tracked in any list + bool isArm32Linux = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.Arm && + System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux); + + if (!isArm32Linux) + { + Assert.Equal("node24", handler.Data.NodeVersion); + Assert.Empty(arm32Actions); + Assert.Empty(deprecatedActions); + } + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Node20Action_RequireNode24_ForcesNode24() + { + using (TestHostContext hc = CreateTestContext()) + { + // Arrange. + var hf = new HandlerFactory(); + hf.Initialize(hc); + + var variables = new Dictionary + { + { Constants.Runner.NodeMigration.RequireNode24Flag, new VariableValue("true") }, + { Constants.Runner.NodeMigration.WarnOnNode20Flag, new VariableValue("true") } + }; + Variables serverVariables = new(hc, variables); + var upgradedActions = new HashSet(StringComparer.OrdinalIgnoreCase); + var deprecatedActions = new HashSet(StringComparer.OrdinalIgnoreCase); + + _ec.Setup(x => x.Global).Returns(new GlobalContext() + { + Variables = serverVariables, + EnvironmentVariables = new Dictionary(), + UpgradedToNode24Actions = upgradedActions, + DeprecatedNode20Actions = deprecatedActions + }); + + var actionRef = new RepositoryPathReference + { + Name = "actions/checkout", + Ref = "v4" + }; + + // Act. + var data = new NodeJSActionExecutionData(); + data.NodeVersion = "node20"; + + bool isArm32Linux = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.Arm && + System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux); + + if (!isArm32Linux) + { + var handler = hf.Create( + _ec.Object, + actionRef, + new Mock().Object, + data, + new Dictionary(), + new Dictionary(), + new Variables(hc, new Dictionary()), + "", + new List() + ) as INodeScriptActionHandler; + + // Phase 3: RequireNode24 forces node24, ignoring env vars + Assert.Equal("node24", handler.Data.NodeVersion); + Assert.Contains("actions/checkout@v4", upgradedActions); + Assert.Empty(deprecatedActions); + } + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Node20Action_KillArm32Flag_ThrowsOnArm32() + { + using (TestHostContext hc = CreateTestContext()) + { + // Arrange. + var hf = new HandlerFactory(); + hf.Initialize(hc); + + var variables = new Dictionary + { + { Constants.Runner.NodeMigration.KillLinuxArm32Flag, new VariableValue("true") } + }; + Variables serverVariables = new(hc, variables); + + _ec.Setup(x => x.Global).Returns(new GlobalContext() + { + Variables = serverVariables, + EnvironmentVariables = new Dictionary() + }); + + var actionRef = new RepositoryPathReference + { + Name = "actions/checkout", + Ref = "v4" + }; + + var data = new NodeJSActionExecutionData(); + data.NodeVersion = "node20"; + + bool isArm32Linux = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.Arm && + System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux); + + if (isArm32Linux) + { + Assert.Throws(() => hf.Create( + _ec.Object, + actionRef, + new Mock().Object, + data, + new Dictionary(), + new Dictionary(), + new Variables(hc, new Dictionary()), + "", + new List() + )); + } + else + { + // On non-ARM32, should proceed normally (node20 stays) + var handler = hf.Create( + _ec.Object, + actionRef, + new Mock().Object, + data, + new Dictionary(), + new Dictionary(), + new Variables(hc, new Dictionary()), + "", + new List() + ) as INodeScriptActionHandler; + + Assert.Equal("node20", handler.Data.NodeVersion); + } + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void ExplicitNode24Action_DeprecateArm32_UsesOriginalVersionForTracking() + { + // Regression test: verifies that when an action explicitly declares node24 + // and ARM32 deprecation downgrades it to node20, the tracking call uses + // the original preferred version ("node24"), not the already-overwritten + // nodeData.NodeVersion ("node20"). Without this fix, ShouldTrackAsArm32Node20 + // would receive (preferred="node20", final="node20") and never return true. + string originalPreferred = "node24"; + string finalAfterArm32Downgrade = "node20"; + string deprecationWarning = "Linux ARM32 runners are deprecated and will no longer be supported after September 16th, 2026. Please migrate to a supported platform."; + + // Correct: use the original preferred version before assignment + bool correctTracking = HandlerFactory.ShouldTrackAsArm32Node20( + deprecateArm32: true, + preferredNodeVersion: originalPreferred, + finalNodeVersion: finalAfterArm32Downgrade, + platformWarningMessage: deprecationWarning); + Assert.True(correctTracking); + + // Bug scenario: if nodeData.NodeVersion was already overwritten to finalNodeVersion + bool buggyTracking = HandlerFactory.ShouldTrackAsArm32Node20( + deprecateArm32: true, + preferredNodeVersion: finalAfterArm32Downgrade, + finalNodeVersion: finalAfterArm32Downgrade, + platformWarningMessage: deprecationWarning); + Assert.False(buggyTracking); + } + + [Theory] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + [InlineData(true, "node24", "node20", "Linux ARM32 runners are deprecated", true)] + [InlineData(true, "node20", "node20", "Linux ARM32 runners are deprecated", false)] + [InlineData(true, "node24", "node24", "Linux ARM32 runners are deprecated", false)] + [InlineData(true, "node24", "node20", null, false)] + [InlineData(false, "node24", "node20", "Linux ARM32 runners are deprecated", false)] + public void ShouldTrackAsArm32Node20_ClassifiesOnlyPlatformDowngrades( + bool deprecateArm32, + string preferredNodeVersion, + string finalNodeVersion, + string platformWarningMessage, + bool expected) + { + bool actual = HandlerFactory.ShouldTrackAsArm32Node20( + deprecateArm32, + preferredNodeVersion, + finalNodeVersion, + platformWarningMessage); + + Assert.Equal(expected, actual); + } } } diff --git a/src/Test/L0/Worker/StepHostNodeVersionL0.cs b/src/Test/L0/Worker/StepHostNodeVersionL0.cs index 83a3aa93ec3..1c626c5d6ee 100644 --- a/src/Test/L0/Worker/StepHostNodeVersionL0.cs +++ b/src/Test/L0/Worker/StepHostNodeVersionL0.cs @@ -161,5 +161,59 @@ public void CheckNodeVersionForArm32_KillTakesPrecedenceOverDeprecation() Assert.Null(warningMessage); } } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void CheckNodeVersionForArm32_ServerOverridableDateUsedInDeprecationWarning() + { + string preferredVersion = "node24"; + string customDate = "December 1st, 2027"; + var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32( + preferredVersion, deprecateArm32: true, node20RemovalDate: customDate); + + bool isArm32 = RuntimeInformation.ProcessArchitecture == Architecture.Arm || + Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE")?.Contains("ARM") == true; + bool isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + + if (isArm32 && isLinux) + { + Assert.Equal("node20", nodeVersion); + Assert.NotNull(warningMessage); + Assert.Contains(customDate, warningMessage); + Assert.DoesNotContain(Constants.Runner.NodeMigration.Node20RemovalDate, warningMessage); + } + else + { + Assert.Equal("node24", nodeVersion); + Assert.Null(warningMessage); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void CheckNodeVersionForArm32_FallbackDateUsedWhenNoOverride() + { + string preferredVersion = "node24"; + var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32( + preferredVersion, deprecateArm32: true); + + bool isArm32 = RuntimeInformation.ProcessArchitecture == Architecture.Arm || + Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE")?.Contains("ARM") == true; + bool isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + + if (isArm32 && isLinux) + { + Assert.Equal("node20", nodeVersion); + Assert.NotNull(warningMessage); + Assert.Contains(Constants.Runner.NodeMigration.Node20RemovalDate, warningMessage); + } + else + { + Assert.Equal("node24", nodeVersion); + Assert.Null(warningMessage); + } + } } } From 9a236888bd9b782d3574182c2276b4e116370bb3 Mon Sep 17 00:00:00 2001 From: Salman Muin Kayser Chishti Date: Tue, 17 Mar 2026 16:20:39 +0000 Subject: [PATCH 6/6] Guard against empty string date overrides from server --- src/Runner.Common/Util/NodeUtil.cs | 3 ++- src/Runner.Worker/JobExtension.cs | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Runner.Common/Util/NodeUtil.cs b/src/Runner.Common/Util/NodeUtil.cs index 59e27a598a4..d87224f9820 100644 --- a/src/Runner.Common/Util/NodeUtil.cs +++ b/src/Runner.Common/Util/NodeUtil.cs @@ -137,9 +137,10 @@ public static (string nodeVersion, string warningMessage) CheckNodeVersionForLin // ARM32 deprecation warning: continue using node20 but warn about upcoming end of support if (deprecateArm32) { + string effectiveDate = string.IsNullOrEmpty(node20RemovalDate) ? Constants.Runner.NodeMigration.Node20RemovalDate : node20RemovalDate; string deprecationWarning = string.Format( Constants.Runner.NodeMigration.LinuxArm32DeprecationMessage, - node20RemovalDate ?? Constants.Runner.NodeMigration.Node20RemovalDate); + effectiveDate); if (string.Equals(preferredVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase)) { diff --git a/src/Runner.Worker/JobExtension.cs b/src/Runner.Worker/JobExtension.cs index 594ade59d36..f3757642947 100644 --- a/src/Runner.Worker/JobExtension.cs +++ b/src/Runner.Worker/JobExtension.cs @@ -737,8 +737,10 @@ public async Task FinalizeJob(IExecutionContext jobContext, Pipelines.AgentJobRe } // Read dates from server variables with hardcoded fallbacks - var node24DefaultDate = context.Global.Variables?.Get(Constants.Runner.NodeMigration.Node24DefaultDateVariable) ?? Constants.Runner.NodeMigration.Node24DefaultDate; - var node20RemovalDate = context.Global.Variables?.Get(Constants.Runner.NodeMigration.Node20RemovalDateVariable) ?? Constants.Runner.NodeMigration.Node20RemovalDate; + var node24DefaultDateRaw = context.Global.Variables?.Get(Constants.Runner.NodeMigration.Node24DefaultDateVariable); + var node24DefaultDate = string.IsNullOrEmpty(node24DefaultDateRaw) ? Constants.Runner.NodeMigration.Node24DefaultDate : node24DefaultDateRaw; + var node20RemovalDateRaw = context.Global.Variables?.Get(Constants.Runner.NodeMigration.Node20RemovalDateVariable); + var node20RemovalDate = string.IsNullOrEmpty(node20RemovalDateRaw) ? Constants.Runner.NodeMigration.Node20RemovalDate : node20RemovalDateRaw; // Add deprecation warning annotation for Node.js 20 actions (Phase 1 - actions still running on node20) if (context.Global.DeprecatedNode20Actions?.Count > 0)