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

public static readonly string InternalTelemetryIssueDataKey = "_internal_telemetry";
Expand Down
44 changes: 39 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,48 @@ 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)
{
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.Node20RemovalDate);

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; }
}
}
57 changes: 43 additions & 14 deletions src/Runner.Worker/Handlers/HandlerFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,29 +65,29 @@ 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;
string actionName = GetActionName(action);

// Check if node20 was explicitly specified in the action
// We don't modify if node24 was explicitly specified
if (string.Equals(nodeData.NodeVersion, Constants.Runner.NodeMigration.Node20, StringComparison.InvariantCultureIgnoreCase))
{
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))
Expand All @@ -100,6 +100,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 (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 (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 +129,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;
}
Expand Down
39 changes: 33 additions & 6 deletions src/Runner.Worker/Handlers/StepHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,17 @@ 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;

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);
Expand Down Expand Up @@ -142,8 +151,17 @@ 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;

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);
Expand Down Expand Up @@ -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);
Expand Down
22 changes: 20 additions & 2 deletions src/Runner.Worker/JobExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -736,14 +736,32 @@ 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);
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);
}

// 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);
}

// 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.Node20RemovalDate}. Please migrate to a supported platform. For more information see: {Constants.Runner.NodeMigration.Node20DeprecationUrl}";
context.Warning(arm32Message);
}
}
catch (Exception ex)
{
Expand Down
Loading
Loading