Skip to content
Open
7 changes: 7 additions & 0 deletions src/Agent.Sdk/Knob/AgentKnobs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,13 @@ public class AgentKnobs
new EnvironmentKnobSource("AGENT_DISABLE_NODE6_TASKS"),
new BuiltInDefaultKnobSource("false"));

public static readonly Knob EnableEOLNodeVersionPolicy = new Knob(
nameof(EnableEOLNodeVersionPolicy),
"When enabled, automatically upgrades tasks using end-of-life Node.js versions (6, 10, 16) to supported versions (Node 20.1 or Node 24). Throws error if no supported versions are available on the agent.",
new PipelineFeatureSource("AGENT_RESTRICT_EOL_NODE_VERSIONS"),
new EnvironmentKnobSource("AGENT_RESTRICT_EOL_NODE_VERSIONS"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DO we need both sources for toggle to work ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How are we planning to do E2E test for this ?

new BuiltInDefaultKnobSource("false"));

public static readonly Knob DisableTeePluginRemoval = new Knob(
nameof(DisableTeePluginRemoval),
"Disables removing TEE plugin after using it during checkout.",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;

namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies
{
/// <summary>
/// Strategy interface for both host and container node selection.
/// </summary>
public interface IUnifiedNodeVersionStrategy
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Unified" seems extra, can we remove it from everywhere, as I am not clear what unified represent here.

{
/// <summary>
/// Human-readable name of this strategy for logging and debugging.
/// Examples: "Node24", "Node20", "Node16", "CustomNode"
/// </summary>
string Name { get; }

/// <summary>
/// Checks if this strategy can handle the given context.
/// Includes handler type, knob checks, EOL policy, and glibc compatibility.
/// </summary>
/// <param name="context">Context with environment, task, and glibc information</param>
/// <returns>True if this strategy can handle the context, false otherwise</returns>
bool CanHandle(UnifiedNodeContext context);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Input should be taskcontext and output should NodePathResult.


/// <summary>
/// Gets the Node path for the given context.
/// Works for both host and container (path translation handled internally).
/// May throw NotSupportedException if EOL policy is violated.
/// </summary>
/// <param name="context">Context with environment, task, and glibc information</param>
/// <returns>NodePathResult with path, version, reason, and optional warning</returns>
/// <exception cref="NotSupportedException">If EOL policy prevents using this version</exception>
NodePathResult GetNodePath(UnifiedNodeContext context);
}
}
36 changes: 36 additions & 0 deletions src/Agent.Worker/NodeVersionStrategies/NodePathResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;

namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies
{
/// <summary>
/// Result containing the selected Node path and metadata.
/// Used by unified strategy pattern for both host and container node selection.
/// </summary>
public sealed class NodePathResult
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming wise it can be called NodeRunnerInfo

{
/// <summary>
/// Full path to the node executable.
/// </summary>
public string NodePath { get; set; }

/// <summary>
/// The node version folder name (e.g., "node24", "node20_1", "node16").
/// </summary>
public string NodeVersion { get; set; }

/// <summary>
/// Explanation of why this version was selected.
/// Used for debugging and telemetry.
/// </summary>
public string Reason { get; set; }

/// <summary>
/// Optional warning message to display to user.
/// Example: "Container OS doesn't support Node24, using Node20 instead."
/// </summary>
public string Warning { get; set; }
}
}
69 changes: 69 additions & 0 deletions src/Agent.Worker/NodeVersionStrategies/UnifiedNode10Strategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.IO;
using Agent.Sdk.Knob;
using Microsoft.TeamFoundation.DistributedTask.WebApi;
using Microsoft.VisualStudio.Services.Agent.Util;
using Microsoft.VisualStudio.Services.Agent.Worker;

namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies
{
public sealed class UnifiedNode10Strategy : IUnifiedNodeVersionStrategy
{
public string Name => "Node10";

public bool CanHandle(UnifiedNodeContext context)
{
bool hasNode10Handler = context.HandlerData is Node10HandlerData;
bool eolPolicyEnabled = AgentKnobs.EnableEOLNodeVersionPolicy.GetValue(context.ExecutionContext).AsBoolean();

if (hasNode10Handler)
{
return DetermineNodeVersionAndSetContext(context, eolPolicyEnabled, "Selected for Node10 task handler");
}

bool isAlpine = context.IsAlpine;
if (isAlpine)
{
context.ExecutionContext.Warning(
"Using Node10 on Alpine Linux because Node6 is not compatible. " +
"Node10 has reached End-of-Life. Please upgrade to Node20 or Node24 for continued support.");

return DetermineNodeVersionAndSetContext(context, eolPolicyEnabled, "Selected for Alpine Linux compatibility (Node6 incompatible)");
}

return false;
}

private bool DetermineNodeVersionAndSetContext(UnifiedNodeContext context, bool eolPolicyEnabled, string baseReason)
{
if (eolPolicyEnabled)
{
throw new NotSupportedException(StringUtil.Loc("NodeEOLPolicyBlocked", "Node10"));
}

context.SelectedNodeVersion = "node10";
context.SelectionReason = baseReason;
context.SelectionWarning = StringUtil.Loc("NodeEOLWarning", "Node10");
return true;
}

public NodePathResult GetNodePath(UnifiedNodeContext context)
{
string externalsPath = context.HostContext.GetDirectory(WellKnownDirectory.Externals);
string hostPath = Path.Combine(externalsPath, context.SelectedNodeVersion, "bin", $"node{IOUtil.ExeExtension}");
string finalPath = context.IsContainer && context.Container != null ?
context.Container.TranslateToContainerPath(hostPath) : hostPath;

return new NodePathResult
{
NodePath = finalPath,
NodeVersion = context.SelectedNodeVersion,
Reason = context.SelectionReason,
Warning = context.SelectionWarning
};
}
}
}
59 changes: 59 additions & 0 deletions src/Agent.Worker/NodeVersionStrategies/UnifiedNode16Strategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.IO;
using Agent.Sdk.Knob;
using Microsoft.TeamFoundation.DistributedTask.WebApi;
using Microsoft.VisualStudio.Services.Agent.Util;
using Microsoft.VisualStudio.Services.Agent.Worker;

namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies
{
public sealed class UnifiedNode16Strategy : IUnifiedNodeVersionStrategy
{
public string Name => "Node16";

public bool CanHandle(UnifiedNodeContext context)
{
bool hasNode16Handler = context.HandlerData is Node16HandlerData;
bool eolPolicyEnabled = AgentKnobs.EnableEOLNodeVersionPolicy.GetValue(context.ExecutionContext).AsBoolean();

if (hasNode16Handler)
{
return DetermineNodeVersionAndSetContext(context, eolPolicyEnabled, "Selected for Node16 task handler");
}

return false;
}

private bool DetermineNodeVersionAndSetContext(UnifiedNodeContext context, bool eolPolicyEnabled, string baseReason)
{
if (eolPolicyEnabled)
{
throw new NotSupportedException(StringUtil.Loc("NodeEOLPolicyBlocked", "Node16"));
}

context.SelectedNodeVersion = "node16";
context.SelectionReason = baseReason;
context.SelectionWarning = StringUtil.Loc("NodeEOLWarning", "Node16");
return true;
}

public NodePathResult GetNodePath(UnifiedNodeContext context)
{
string externalsPath = context.HostContext.GetDirectory(WellKnownDirectory.Externals);
string hostPath = Path.Combine(externalsPath, context.SelectedNodeVersion, "bin", $"node{IOUtil.ExeExtension}");
string finalPath = context.IsContainer && context.Container != null ?
context.Container.TranslateToContainerPath(hostPath) : hostPath;

return new NodePathResult
{
NodePath = finalPath,
NodeVersion = context.SelectedNodeVersion,
Reason = context.SelectionReason,
Warning = context.SelectionWarning
};
}
}
}
79 changes: 79 additions & 0 deletions src/Agent.Worker/NodeVersionStrategies/UnifiedNode20Strategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.IO;
using Agent.Sdk.Knob;
using Microsoft.TeamFoundation.DistributedTask.WebApi;
using Microsoft.VisualStudio.Services.Agent.Util;
using Microsoft.VisualStudio.Services.Agent.Worker;

namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies
{
public sealed class UnifiedNode20Strategy : IUnifiedNodeVersionStrategy
{
public string Name => "Node20";

public bool CanHandle(UnifiedNodeContext context)
{
bool useNode20Globally = AgentKnobs.UseNode20_1.GetValue(context.ExecutionContext).AsBoolean();
bool hasNode20Handler = context.HandlerData is Node20_1HandlerData;
bool eolPolicyEnabled = AgentKnobs.EnableEOLNodeVersionPolicy.GetValue(context.ExecutionContext).AsBoolean();

if (useNode20Globally)
{
return DetermineNodeVersionAndSetContext(context, eolPolicyEnabled, "Selected via global AGENT_USE_NODE20_1 override");
}

if (hasNode20Handler)
{
return DetermineNodeVersionAndSetContext(context, eolPolicyEnabled, "Selected for Node20 task handler");
}

if (eolPolicyEnabled)
{
return DetermineNodeVersionAndSetContext(context, eolPolicyEnabled, "Upgraded from end-of-life Node version due to EOL policy");
}

return false;
}

private bool DetermineNodeVersionAndSetContext(UnifiedNodeContext context, bool eolPolicyEnabled, string baseReason)
{
if (!context.Node20HasGlibcError)
{
context.SelectedNodeVersion = "node20_1";
context.SelectionReason = baseReason;
context.SelectionWarning = null;
return true;
}

if (eolPolicyEnabled)
{
throw new NotSupportedException(StringUtil.Loc("NodeEOLFallbackBlocked", "Node20", "Node16"));
}

string systemType = context.IsContainer ? "container" : "agent";
context.SelectedNodeVersion = "node16";
context.SelectionReason = $"{baseReason}, fallback to Node16 due to Node20 glibc compatibility issue";
context.SelectionWarning = StringUtil.Loc("NodeGlibcFallbackWarning", systemType, "Node20", "Node16");
return true;
}

public NodePathResult GetNodePath(UnifiedNodeContext context)
{
string externalsPath = context.HostContext.GetDirectory(WellKnownDirectory.Externals);
string hostPath = Path.Combine(externalsPath, context.SelectedNodeVersion, "bin", $"node{IOUtil.ExeExtension}");
string finalPath = context.IsContainer && context.Container != null ?
context.Container.TranslateToContainerPath(hostPath) : hostPath;

return new NodePathResult
{
NodePath = finalPath,
NodeVersion = context.SelectedNodeVersion,
Reason = context.SelectionReason,
Warning = context.SelectionWarning
};
}
}
}
Loading
Loading