Skip to content

Commit 4c5eac9

Browse files
committed
Node 24 enforcement + Linux ARM32 deprecation support
1 parent 45ed15d commit 4c5eac9

8 files changed

Lines changed: 321 additions & 19 deletions

File tree

src/Runner.Common/Constants.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,16 @@ 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+
// Linux ARM32 deprecation date (TBD - placeholder for October 2026)
206+
public static readonly string LinuxArm32DeprecationDate = "October 2026";
207+
public static readonly string LinuxArm32DeprecationMessage = "Linux ARM32 runners are deprecated and will no longer be supported after {0}. Please migrate to a supported platform.";
200208
}
201209

202210
public static readonly string InternalTelemetryIssueDataKey = "_internal_telemetry";

src/Runner.Common/Util/NodeUtil.cs

Lines changed: 39 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,48 @@ 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)
115121
{
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))
122+
bool isArm32Linux = Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.Arm) &&
123+
Constants.Runner.Platform.Equals(Constants.OSPlatform.Linux);
124+
125+
if (!isArm32Linux)
126+
{
127+
return (preferredVersion, null);
128+
}
129+
130+
// ARM32 kill switch: runner should no longer work on this platform
131+
if (killArm32)
132+
{
133+
return (null, "Linux ARM32 runners are no longer supported. Please migrate to a supported platform.");
134+
}
135+
136+
// ARM32 deprecation warning: continue using node20 but warn about upcoming end of support
137+
if (deprecateArm32)
138+
{
139+
string deprecationWarning = string.Format(
140+
Constants.Runner.NodeMigration.LinuxArm32DeprecationMessage,
141+
Constants.Runner.NodeMigration.LinuxArm32DeprecationDate);
142+
143+
if (string.Equals(preferredVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase))
144+
{
145+
return (Constants.Runner.NodeMigration.Node20, deprecationWarning);
146+
}
147+
148+
return (preferredVersion, deprecationWarning);
149+
}
150+
151+
// Legacy behavior: fall back to node20 if node24 was requested on ARM32
152+
if (string.Equals(preferredVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase))
119153
{
120154
return (Constants.Runner.NodeMigration.Node20, "Node 24 is not supported on Linux ARM32 platforms. Falling back to Node 20.");
121155
}

src/Runner.Worker/ExecutionContext.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,9 @@ 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+
857860
// Job Outputs
858861
JobOutputs = new Dictionary<string, VariableValue>(StringComparer.OrdinalIgnoreCase);
859862

src/Runner.Worker/GlobalContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,6 @@ 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; }
3738
}
3839
}

src/Runner.Worker/Handlers/HandlerFactory.cs

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -66,28 +66,30 @@ public IHandler Create(
6666
}
6767

6868
// 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-
}
69+
// Note: tracking happens before potential upgrade to node24
70+
// Actions that get upgraded will be moved to UpgradedToNode24Actions below
71+
bool warnOnNode20 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.WarnOnNode20Flag) ?? false;
72+
string actionName = GetActionName(action);
8173

8274
// Check if node20 was explicitly specified in the action
8375
// We don't modify if node24 was explicitly specified
8476
if (string.Equals(nodeData.NodeVersion, Constants.Runner.NodeMigration.Node20, StringComparison.InvariantCultureIgnoreCase))
8577
{
8678
bool useNode24ByDefault = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.UseNode24ByDefaultFlag) ?? false;
8779
bool requireNode24 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.RequireNode24Flag) ?? false;
80+
bool deprecateArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.DeprecateLinuxArm32Flag) ?? false;
81+
bool killArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.KillLinuxArm32Flag) ?? false;
8882

8983
var (nodeVersion, configWarningMessage) = NodeUtil.DetermineActionsNodeVersion(environment, useNode24ByDefault, requireNode24);
90-
var (finalNodeVersion, platformWarningMessage) = NodeUtil.CheckNodeVersionForLinuxArm32(nodeVersion);
84+
var (finalNodeVersion, platformWarningMessage) = NodeUtil.CheckNodeVersionForLinuxArm32(nodeVersion, deprecateArm32, killArm32);
85+
86+
// ARM32 kill switch: fail the step
87+
if (finalNodeVersion == null)
88+
{
89+
executionContext.Error(platformWarningMessage);
90+
throw new InvalidOperationException(platformWarningMessage);
91+
}
92+
9193
nodeData.NodeVersion = finalNodeVersion;
9294

9395
if (!string.IsNullOrEmpty(configWarningMessage))
@@ -100,6 +102,21 @@ public IHandler Create(
100102
executionContext.Warning(platformWarningMessage);
101103
}
102104

105+
// Track actions based on their final node version
106+
if (!string.IsNullOrEmpty(actionName))
107+
{
108+
if (string.Equals(finalNodeVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase))
109+
{
110+
// Action was upgraded from node20 to node24
111+
executionContext.Global.UpgradedToNode24Actions?.Add(actionName);
112+
}
113+
else if (warnOnNode20)
114+
{
115+
// Action is still running on node20 (e.g., ARM32 fallback)
116+
executionContext.Global.DeprecatedNode20Actions?.Add(actionName);
117+
}
118+
}
119+
103120
// Show information about Node 24 migration in Phase 2
104121
if (useNode24ByDefault && !requireNode24 && string.Equals(finalNodeVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase))
105122
{
@@ -109,6 +126,15 @@ public IHandler Create(
109126
executionContext.Output(infoMessage);
110127
}
111128
}
129+
else if (warnOnNode20 && string.Equals(nodeData.NodeVersion, Constants.Runner.NodeMigration.Node20, StringComparison.InvariantCultureIgnoreCase))
130+
{
131+
// This handles the case where nodeData.NodeVersion is still node20 but wasn't caught above
132+
// (shouldn't normally happen, but kept for safety)
133+
if (!string.IsNullOrEmpty(actionName))
134+
{
135+
executionContext.Global.DeprecatedNode20Actions?.Add(actionName);
136+
}
137+
}
112138

113139
(handler as INodeScriptActionHandler).Data = nodeData;
114140
}

src/Runner.Worker/JobExtension.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -736,14 +736,23 @@ public async Task FinalizeJob(IExecutionContext jobContext, Pipelines.AgentJobRe
736736
}
737737
}
738738

739-
// Add deprecation warning annotation for Node.js 20 actions
739+
// Add deprecation warning annotation for Node.js 20 actions (Phase 1 - actions still running on node20)
740740
if (context.Global.DeprecatedNode20Actions?.Count > 0)
741741
{
742742
var sortedActions = context.Global.DeprecatedNode20Actions.OrderBy(a => a, StringComparer.OrdinalIgnoreCase);
743743
var actionsList = string.Join(", ", sortedActions);
744744
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}";
745745
context.Warning(deprecationMessage);
746746
}
747+
748+
// Add annotation for actions upgraded from Node.js 20 to Node.js 24 (Phase 2/3)
749+
if (context.Global.UpgradedToNode24Actions?.Count > 0)
750+
{
751+
var sortedActions = context.Global.UpgradedToNode24Actions.OrderBy(a => a, StringComparer.OrdinalIgnoreCase);
752+
var actionsList = string.Join(", ", sortedActions);
753+
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}";
754+
context.Warning(upgradeMessage);
755+
}
747756
}
748757
catch (Exception ex)
749758
{

src/Test/L0/Worker/HandlerFactoryL0.cs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,5 +370,124 @@ public void LocalNode20Action_TrackedWhenWarnFlagEnabled()
370370
Assert.Contains("./.github/actions/my-action", deprecatedActions);
371371
}
372372
}
373+
374+
[Fact]
375+
[Trait("Level", "L0")]
376+
[Trait("Category", "Worker")]
377+
public void Node20Action_TrackedAsUpgradedWhenUseNode24ByDefaultEnabled()
378+
{
379+
using (TestHostContext hc = CreateTestContext())
380+
{
381+
// Arrange.
382+
var hf = new HandlerFactory();
383+
hf.Initialize(hc);
384+
385+
var variables = new Dictionary<string, VariableValue>
386+
{
387+
{ Constants.Runner.NodeMigration.WarnOnNode20Flag, new VariableValue("true") },
388+
{ Constants.Runner.NodeMigration.UseNode24ByDefaultFlag, new VariableValue("true") }
389+
};
390+
Variables serverVariables = new(hc, variables);
391+
var deprecatedActions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
392+
var upgradedActions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
393+
394+
_ec.Setup(x => x.Global).Returns(new GlobalContext()
395+
{
396+
Variables = serverVariables,
397+
EnvironmentVariables = new Dictionary<string, string>(),
398+
DeprecatedNode20Actions = deprecatedActions,
399+
UpgradedToNode24Actions = upgradedActions
400+
});
401+
402+
var actionRef = new RepositoryPathReference
403+
{
404+
Name = "actions/checkout",
405+
Ref = "v4"
406+
};
407+
408+
// Act.
409+
var data = new NodeJSActionExecutionData();
410+
data.NodeVersion = "node20";
411+
var handler = hf.Create(
412+
_ec.Object,
413+
actionRef,
414+
new Mock<IStepHost>().Object,
415+
data,
416+
new Dictionary<string, string>(),
417+
new Dictionary<string, string>(),
418+
new Variables(hc, new Dictionary<string, VariableValue>()),
419+
"",
420+
new List<JobExtensionRunner>()
421+
) as INodeScriptActionHandler;
422+
423+
// On non-ARM32 platforms, action should be upgraded to node24
424+
// and tracked in UpgradedToNode24Actions, NOT in DeprecatedNode20Actions
425+
bool isArm32Linux = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.Arm &&
426+
System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux);
427+
428+
if (!isArm32Linux)
429+
{
430+
Assert.Equal("node24", handler.Data.NodeVersion);
431+
Assert.Contains("actions/checkout@v4", upgradedActions);
432+
Assert.DoesNotContain("actions/checkout@v4", deprecatedActions);
433+
}
434+
}
435+
}
436+
437+
[Fact]
438+
[Trait("Level", "L0")]
439+
[Trait("Category", "Worker")]
440+
public void Node20Action_NotUpgradedWhenPhase1Only()
441+
{
442+
using (TestHostContext hc = CreateTestContext())
443+
{
444+
// Arrange.
445+
var hf = new HandlerFactory();
446+
hf.Initialize(hc);
447+
448+
var variables = new Dictionary<string, VariableValue>
449+
{
450+
{ Constants.Runner.NodeMigration.WarnOnNode20Flag, new VariableValue("true") }
451+
};
452+
Variables serverVariables = new(hc, variables);
453+
var deprecatedActions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
454+
var upgradedActions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
455+
456+
_ec.Setup(x => x.Global).Returns(new GlobalContext()
457+
{
458+
Variables = serverVariables,
459+
EnvironmentVariables = new Dictionary<string, string>(),
460+
DeprecatedNode20Actions = deprecatedActions,
461+
UpgradedToNode24Actions = upgradedActions
462+
});
463+
464+
var actionRef = new RepositoryPathReference
465+
{
466+
Name = "actions/checkout",
467+
Ref = "v4"
468+
};
469+
470+
// Act.
471+
var data = new NodeJSActionExecutionData();
472+
data.NodeVersion = "node20";
473+
var handler = hf.Create(
474+
_ec.Object,
475+
actionRef,
476+
new Mock<IStepHost>().Object,
477+
data,
478+
new Dictionary<string, string>(),
479+
new Dictionary<string, string>(),
480+
new Variables(hc, new Dictionary<string, VariableValue>()),
481+
"",
482+
new List<JobExtensionRunner>()
483+
) as INodeScriptActionHandler;
484+
485+
// In Phase 1 (no UseNode24ByDefault), action stays on node20
486+
// and should be in DeprecatedNode20Actions
487+
Assert.Equal("node20", handler.Data.NodeVersion);
488+
Assert.Contains("actions/checkout@v4", deprecatedActions);
489+
Assert.Empty(upgradedActions);
490+
}
491+
}
373492
}
374493
}

0 commit comments

Comments
 (0)