Skip to content
Merged
14 changes: 14 additions & 0 deletions src/Runner.Common/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,22 @@ 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/";

// 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.";
}

public static readonly string InternalTelemetryIssueDataKey = "_internal_telemetry";
Expand Down
46 changes: 41 additions & 5 deletions src/Runner.Common/Util/NodeUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -108,14 +108,50 @@ public static (string nodeVersion, string warningMessage) DetermineActionsNodeVe

/// <summary>
/// Checks if Node24 is requested but running on ARM32 Linux, and determines if fallback is needed.
/// Also handles ARM32 deprecation and kill switch phases.
/// </summary>
/// <param name="preferredVersion">The preferred Node version</param>
/// <param name="deprecateArm32">Feature flag indicating ARM32 Linux is deprecated</param>
/// <param name="killArm32">Feature flag indicating ARM32 Linux should no longer work</param>
/// <returns>A tuple containing the adjusted node version and an optional warning message</returns>
public static (string nodeVersion, string warningMessage) CheckNodeVersionForLinuxArm32(string preferredVersion)
public static (string nodeVersion, string warningMessage) CheckNodeVersionForLinuxArm32(
string preferredVersion,
bool deprecateArm32 = false,
bool killArm32 = false,
string node20RemovalDate = null)
{
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 effectiveDate = string.IsNullOrEmpty(node20RemovalDate) ? Constants.Runner.NodeMigration.Node20RemovalDate : node20RemovalDate;
string deprecationWarning = string.Format(
Constants.Runner.NodeMigration.LinuxArm32DeprecationMessage,
effectiveDate);

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.");
}
Expand Down
6 changes: 6 additions & 0 deletions src/Runner.Worker/ExecutionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,12 @@ public void InitializeJob(Pipelines.AgentJobRequestMessage message, Cancellation
// Track Node.js 20 actions for deprecation warning
Global.DeprecatedNode20Actions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

// Track actions upgraded from Node.js 20 to Node.js 24
Global.UpgradedToNode24Actions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

// Track actions stuck on Node.js 20 due to ARM32 (separate from general deprecation)
Global.Arm32Node20Actions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

// Job Outputs
JobOutputs = new Dictionary<string, VariableValue>(StringComparer.OrdinalIgnoreCase);

Expand Down
2 changes: 2 additions & 0 deletions src/Runner.Worker/GlobalContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,7 @@ public sealed class GlobalContext
public bool HasDeprecatedSetOutput { get; set; }
public bool HasDeprecatedSaveState { get; set; }
public HashSet<string> DeprecatedNode20Actions { get; set; }
public HashSet<string> UpgradedToNode24Actions { get; set; }
public HashSet<string> Arm32Node20Actions { get; set; }
}
}
81 changes: 67 additions & 14 deletions src/Runner.Worker/Handlers/HandlerFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -65,19 +73,12 @@ public IHandler Create(
nodeData.NodeVersion = Common.Constants.Runner.NodeMigration.Node20;
}

// 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);
}
}
}
// 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
// We don't modify if node24 was explicitly specified
Expand All @@ -87,7 +88,15 @@ public IHandler Create(
bool requireNode24 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.RequireNode24Flag) ?? false;

var (nodeVersion, configWarningMessage) = NodeUtil.DetermineActionsNodeVersion(environment, useNode24ByDefault, requireNode24);
var (finalNodeVersion, platformWarningMessage) = NodeUtil.CheckNodeVersionForLinuxArm32(nodeVersion);
var (finalNodeVersion, platformWarningMessage) = NodeUtil.CheckNodeVersionForLinuxArm32(nodeVersion, deprecateArm32, killArm32, node20RemovalDate);

// ARM32 kill switch: fail the step
if (finalNodeVersion == null)
{
executionContext.Error(platformWarningMessage);
throw new InvalidOperationException(platformWarningMessage);
}

nodeData.NodeVersion = finalNodeVersion;

if (!string.IsNullOrEmpty(configWarningMessage))
Expand All @@ -100,6 +109,26 @@ 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 (ShouldTrackAsArm32Node20(deprecateArm32, nodeVersion, finalNodeVersion, platformWarningMessage))
{
// Action is on node20 because ARM32 can't run node24
executionContext.Global.Arm32Node20Actions?.Add(actionName);
}
else if (warnOnNode20)
{
// Action is still running on node20 (general case)
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))
{
Expand All @@ -109,6 +138,30 @@ public IHandler Create(
executionContext.Output(infoMessage);
}
}
else if (string.Equals(nodeData.NodeVersion, Constants.Runner.NodeMigration.Node24, StringComparison.InvariantCultureIgnoreCase))
{
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.Arm32Node20Actions?.Add(actionName);
}
}

(handler as INodeScriptActionHandler).Data = nodeData;
}
Expand Down
44 changes: 37 additions & 7 deletions src/Runner.Worker/Handlers/StepHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,23 @@ public string ResolvePathForStepHost(IExecutionContext executionContext, string

public Task<string> 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;
string node20RemovalDate = executionContext.Global.Variables?.Get(Constants.Runner.NodeMigration.Node20RemovalDateVariable);

var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion, deprecateArm32, killArm32, node20RemovalDate);

if (nodeVersion == null)
{
executionContext.Error(warningMessage);
throw new InvalidOperationException(warningMessage);
}

if (!string.IsNullOrEmpty(warningMessage))
{
executionContext.Warning(warningMessage);
}

return Task.FromResult(nodeVersion);
}

Expand Down Expand Up @@ -142,8 +152,18 @@ public string ResolvePathForStepHost(IExecutionContext executionContext, string

public async Task<string> 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;
string node20RemovalDate = executionContext.Global.Variables?.Get(Constants.Runner.NodeMigration.Node20RemovalDateVariable);

var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion, deprecateArm32, killArm32, node20RemovalDate);

if (nodeExternal == null)
{
executionContext.Error(warningMessage);
throw new InvalidOperationException(warningMessage);
}

if (!string.IsNullOrEmpty(warningMessage))
{
executionContext.Warning(warningMessage);
Expand Down Expand Up @@ -273,8 +293,18 @@ 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;
string node20RemovalDate = executionContext.Global.Variables?.Get(Constants.Runner.NodeMigration.Node20RemovalDateVariable);

var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion, deprecateArm32, killArm32, node20RemovalDate);

if (nodeExternal == null)
{
executionContext.Error(warningMessage);
throw new InvalidOperationException(warningMessage);
}

if (!string.IsNullOrEmpty(warningMessage))
{
executionContext.Warning(warningMessage);
Expand Down
Loading
Loading