Skip to content

Commit 18d0789

Browse files
authored
Node 24 enforcement + Linux ARM32 deprecation support (#4303)
1 parent c985a9f commit 18d0789

File tree

9 files changed

+848
-28
lines changed

9 files changed

+848
-28
lines changed

src/Runner.Common/Constants.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,22 @@ public static class NodeMigration
195195
public static readonly string RequireNode24Flag = "actions.runner.requirenode24";
196196
public static readonly string WarnOnNode20Flag = "actions.runner.warnonnode20";
197197

198+
// Feature flags for Linux ARM32 deprecation
199+
public static readonly string DeprecateLinuxArm32Flag = "actions_runner_deprecate_linux_arm32";
200+
public static readonly string KillLinuxArm32Flag = "actions_runner_kill_linux_arm32";
201+
198202
// Blog post URL for Node 20 deprecation
199203
public static readonly string Node20DeprecationUrl = "https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/";
204+
205+
// Node 20 migration dates (hardcoded fallbacks, can be overridden via job variables)
206+
public static readonly string Node24DefaultDate = "June 2nd, 2026";
207+
public static readonly string Node20RemovalDate = "September 16th, 2026";
208+
209+
// Variable keys for server-overridable dates
210+
public static readonly string Node24DefaultDateVariable = "actions_runner_node24_default_date";
211+
public static readonly string Node20RemovalDateVariable = "actions_runner_node20_removal_date";
212+
213+
public static readonly string LinuxArm32DeprecationMessage = "Linux ARM32 runners are deprecated and will no longer be supported after {0}. Please migrate to a supported platform.";
200214
}
201215

202216
public static readonly string InternalTelemetryIssueDataKey = "_internal_telemetry";

src/Runner.Common/Util/NodeUtil.cs

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public static (string nodeVersion, string warningMessage) DetermineActionsNodeVe
5858
{
5959
return (Constants.Runner.NodeMigration.Node24, null);
6060
}
61-
61+
6262
// Get environment variable details with source information
6363
var forceNode24Details = GetEnvironmentVariableDetails(
6464
Constants.Runner.NodeMigration.ForceNode24Variable, workflowEnvironment);
@@ -108,14 +108,50 @@ public static (string nodeVersion, string warningMessage) DetermineActionsNodeVe
108108

109109
/// <summary>
110110
/// Checks if Node24 is requested but running on ARM32 Linux, and determines if fallback is needed.
111+
/// Also handles ARM32 deprecation and kill switch phases.
111112
/// </summary>
112113
/// <param name="preferredVersion">The preferred Node version</param>
114+
/// <param name="deprecateArm32">Feature flag indicating ARM32 Linux is deprecated</param>
115+
/// <param name="killArm32">Feature flag indicating ARM32 Linux should no longer work</param>
113116
/// <returns>A tuple containing the adjusted node version and an optional warning message</returns>
114-
public static (string nodeVersion, string warningMessage) CheckNodeVersionForLinuxArm32(string preferredVersion)
117+
public static (string nodeVersion, string warningMessage) CheckNodeVersionForLinuxArm32(
118+
string preferredVersion,
119+
bool deprecateArm32 = false,
120+
bool killArm32 = false,
121+
string node20RemovalDate = null)
115122
{
116-
if (string.Equals(preferredVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase) &&
117-
Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.Arm) &&
118-
Constants.Runner.Platform.Equals(Constants.OSPlatform.Linux))
123+
bool isArm32Linux = Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.Arm) &&
124+
Constants.Runner.Platform.Equals(Constants.OSPlatform.Linux);
125+
126+
if (!isArm32Linux)
127+
{
128+
return (preferredVersion, null);
129+
}
130+
131+
// ARM32 kill switch: runner should no longer work on this platform
132+
if (killArm32)
133+
{
134+
return (null, "Linux ARM32 runners are no longer supported. Please migrate to a supported platform.");
135+
}
136+
137+
// ARM32 deprecation warning: continue using node20 but warn about upcoming end of support
138+
if (deprecateArm32)
139+
{
140+
string effectiveDate = string.IsNullOrEmpty(node20RemovalDate) ? Constants.Runner.NodeMigration.Node20RemovalDate : node20RemovalDate;
141+
string deprecationWarning = string.Format(
142+
Constants.Runner.NodeMigration.LinuxArm32DeprecationMessage,
143+
effectiveDate);
144+
145+
if (string.Equals(preferredVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase))
146+
{
147+
return (Constants.Runner.NodeMigration.Node20, deprecationWarning);
148+
}
149+
150+
return (preferredVersion, deprecationWarning);
151+
}
152+
153+
// Legacy behavior: fall back to node20 if node24 was requested on ARM32
154+
if (string.Equals(preferredVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase))
119155
{
120156
return (Constants.Runner.NodeMigration.Node20, "Node 24 is not supported on Linux ARM32 platforms. Falling back to Node 20.");
121157
}

src/Runner.Worker/ExecutionContext.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,12 @@ public void InitializeJob(Pipelines.AgentJobRequestMessage message, Cancellation
854854
// Track Node.js 20 actions for deprecation warning
855855
Global.DeprecatedNode20Actions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
856856

857+
// Track actions upgraded from Node.js 20 to Node.js 24
858+
Global.UpgradedToNode24Actions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
859+
860+
// Track actions stuck on Node.js 20 due to ARM32 (separate from general deprecation)
861+
Global.Arm32Node20Actions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
862+
857863
// Job Outputs
858864
JobOutputs = new Dictionary<string, VariableValue>(StringComparer.OrdinalIgnoreCase);
859865

src/Runner.Worker/GlobalContext.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,7 @@ public sealed class GlobalContext
3434
public bool HasDeprecatedSetOutput { get; set; }
3535
public bool HasDeprecatedSaveState { get; set; }
3636
public HashSet<string> DeprecatedNode20Actions { get; set; }
37+
public HashSet<string> UpgradedToNode24Actions { get; set; }
38+
public HashSet<string> Arm32Node20Actions { get; set; }
3739
}
3840
}

src/Runner.Worker/Handlers/HandlerFactory.cs

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ IHandler Create(
2525

2626
public sealed class HandlerFactory : RunnerService, IHandlerFactory
2727
{
28+
internal static bool ShouldTrackAsArm32Node20(bool deprecateArm32, string preferredNodeVersion, string finalNodeVersion, string platformWarningMessage)
29+
{
30+
return deprecateArm32 &&
31+
!string.IsNullOrEmpty(platformWarningMessage) &&
32+
string.Equals(preferredNodeVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase) &&
33+
string.Equals(finalNodeVersion, Constants.Runner.NodeMigration.Node20, StringComparison.OrdinalIgnoreCase);
34+
}
35+
2836
public IHandler Create(
2937
IExecutionContext executionContext,
3038
Pipelines.ActionStepDefinitionReference action,
@@ -65,19 +73,12 @@ public IHandler Create(
6573
nodeData.NodeVersion = Common.Constants.Runner.NodeMigration.Node20;
6674
}
6775

68-
// Track Node.js 20 actions for deprecation annotation
69-
if (string.Equals(nodeData.NodeVersion, Constants.Runner.NodeMigration.Node20, StringComparison.InvariantCultureIgnoreCase))
70-
{
71-
bool warnOnNode20 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.WarnOnNode20Flag) ?? false;
72-
if (warnOnNode20)
73-
{
74-
string actionName = GetActionName(action);
75-
if (!string.IsNullOrEmpty(actionName))
76-
{
77-
executionContext.Global.DeprecatedNode20Actions?.Add(actionName);
78-
}
79-
}
80-
}
76+
// Read flags early; actionName is also resolved up front for tracking after version is determined
77+
bool warnOnNode20 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.WarnOnNode20Flag) ?? false;
78+
bool deprecateArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.DeprecateLinuxArm32Flag) ?? false;
79+
bool killArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.KillLinuxArm32Flag) ?? false;
80+
string node20RemovalDate = executionContext.Global.Variables?.Get(Constants.Runner.NodeMigration.Node20RemovalDateVariable);
81+
string actionName = GetActionName(action);
8182

8283
// Check if node20 was explicitly specified in the action
8384
// We don't modify if node24 was explicitly specified
@@ -87,7 +88,15 @@ public IHandler Create(
8788
bool requireNode24 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.RequireNode24Flag) ?? false;
8889

8990
var (nodeVersion, configWarningMessage) = NodeUtil.DetermineActionsNodeVersion(environment, useNode24ByDefault, requireNode24);
90-
var (finalNodeVersion, platformWarningMessage) = NodeUtil.CheckNodeVersionForLinuxArm32(nodeVersion);
91+
var (finalNodeVersion, platformWarningMessage) = NodeUtil.CheckNodeVersionForLinuxArm32(nodeVersion, deprecateArm32, killArm32, node20RemovalDate);
92+
93+
// ARM32 kill switch: fail the step
94+
if (finalNodeVersion == null)
95+
{
96+
executionContext.Error(platformWarningMessage);
97+
throw new InvalidOperationException(platformWarningMessage);
98+
}
99+
91100
nodeData.NodeVersion = finalNodeVersion;
92101

93102
if (!string.IsNullOrEmpty(configWarningMessage))
@@ -100,6 +109,26 @@ public IHandler Create(
100109
executionContext.Warning(platformWarningMessage);
101110
}
102111

112+
// Track actions based on their final node version
113+
if (!string.IsNullOrEmpty(actionName))
114+
{
115+
if (string.Equals(finalNodeVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase))
116+
{
117+
// Action was upgraded from node20 to node24
118+
executionContext.Global.UpgradedToNode24Actions?.Add(actionName);
119+
}
120+
else if (ShouldTrackAsArm32Node20(deprecateArm32, nodeVersion, finalNodeVersion, platformWarningMessage))
121+
{
122+
// Action is on node20 because ARM32 can't run node24
123+
executionContext.Global.Arm32Node20Actions?.Add(actionName);
124+
}
125+
else if (warnOnNode20)
126+
{
127+
// Action is still running on node20 (general case)
128+
executionContext.Global.DeprecatedNode20Actions?.Add(actionName);
129+
}
130+
}
131+
103132
// Show information about Node 24 migration in Phase 2
104133
if (useNode24ByDefault && !requireNode24 && string.Equals(finalNodeVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase))
105134
{
@@ -109,6 +138,30 @@ public IHandler Create(
109138
executionContext.Output(infoMessage);
110139
}
111140
}
141+
else if (string.Equals(nodeData.NodeVersion, Constants.Runner.NodeMigration.Node24, StringComparison.InvariantCultureIgnoreCase))
142+
{
143+
var (finalNodeVersion, platformWarningMessage) = NodeUtil.CheckNodeVersionForLinuxArm32(nodeData.NodeVersion, deprecateArm32, killArm32, node20RemovalDate);
144+
145+
// ARM32 kill switch: fail the step
146+
if (finalNodeVersion == null)
147+
{
148+
executionContext.Error(platformWarningMessage);
149+
throw new InvalidOperationException(platformWarningMessage);
150+
}
151+
152+
var preferredVersion = nodeData.NodeVersion;
153+
nodeData.NodeVersion = finalNodeVersion;
154+
155+
if (!string.IsNullOrEmpty(platformWarningMessage))
156+
{
157+
executionContext.Warning(platformWarningMessage);
158+
}
159+
160+
if (!string.IsNullOrEmpty(actionName) && ShouldTrackAsArm32Node20(deprecateArm32, preferredVersion, finalNodeVersion, platformWarningMessage))
161+
{
162+
executionContext.Global.Arm32Node20Actions?.Add(actionName);
163+
}
164+
}
112165

113166
(handler as INodeScriptActionHandler).Data = nodeData;
114167
}

src/Runner.Worker/Handlers/StepHost.cs

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,23 @@ public string ResolvePathForStepHost(IExecutionContext executionContext, string
5858

5959
public Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionContext, string preferredVersion)
6060
{
61-
// Use NodeUtil to check if Node24 is requested but we're on ARM32 Linux
62-
var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion);
61+
bool deprecateArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.DeprecateLinuxArm32Flag) ?? false;
62+
bool killArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.KillLinuxArm32Flag) ?? false;
63+
string node20RemovalDate = executionContext.Global.Variables?.Get(Constants.Runner.NodeMigration.Node20RemovalDateVariable);
64+
65+
var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion, deprecateArm32, killArm32, node20RemovalDate);
66+
67+
if (nodeVersion == null)
68+
{
69+
executionContext.Error(warningMessage);
70+
throw new InvalidOperationException(warningMessage);
71+
}
72+
6373
if (!string.IsNullOrEmpty(warningMessage))
6474
{
6575
executionContext.Warning(warningMessage);
6676
}
67-
77+
6878
return Task.FromResult(nodeVersion);
6979
}
7080

@@ -142,8 +152,18 @@ public string ResolvePathForStepHost(IExecutionContext executionContext, string
142152

143153
public async Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionContext, string preferredVersion)
144154
{
145-
// Use NodeUtil to check if Node24 is requested but we're on ARM32 Linux
146-
var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion);
155+
bool deprecateArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.DeprecateLinuxArm32Flag) ?? false;
156+
bool killArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.KillLinuxArm32Flag) ?? false;
157+
string node20RemovalDate = executionContext.Global.Variables?.Get(Constants.Runner.NodeMigration.Node20RemovalDateVariable);
158+
159+
var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion, deprecateArm32, killArm32, node20RemovalDate);
160+
161+
if (nodeExternal == null)
162+
{
163+
executionContext.Error(warningMessage);
164+
throw new InvalidOperationException(warningMessage);
165+
}
166+
147167
if (!string.IsNullOrEmpty(warningMessage))
148168
{
149169
executionContext.Warning(warningMessage);
@@ -273,8 +293,18 @@ await containerHookManager.RunScriptStepAsync(context,
273293

274294
private string CheckPlatformForAlpineContainer(IExecutionContext executionContext, string preferredVersion)
275295
{
276-
// Use NodeUtil to check if Node24 is requested but we're on ARM32 Linux
277-
var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion);
296+
bool deprecateArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.DeprecateLinuxArm32Flag) ?? false;
297+
bool killArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.KillLinuxArm32Flag) ?? false;
298+
string node20RemovalDate = executionContext.Global.Variables?.Get(Constants.Runner.NodeMigration.Node20RemovalDateVariable);
299+
300+
var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion, deprecateArm32, killArm32, node20RemovalDate);
301+
302+
if (nodeExternal == null)
303+
{
304+
executionContext.Error(warningMessage);
305+
throw new InvalidOperationException(warningMessage);
306+
}
307+
278308
if (!string.IsNullOrEmpty(warningMessage))
279309
{
280310
executionContext.Warning(warningMessage);

0 commit comments

Comments
 (0)