Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
68d2621
Adding Strategies for node 24 to node 6 | Adding Interfaces created f…
rishabhmalikMS Dec 8, 2025
0854386
Merge branch 'master' into users/rishabhmalikMS/NodehandlerStrategies
rishabhmalikMS Dec 8, 2025
e3b655a
adding EnableEOLNodeVersionPolicy knob
rishabhmalikMS Dec 8, 2025
32e38b9
Merge branch 'users/rishabhmalikMS/NodehandlerStrategies' of https://…
rishabhmalikMS Dec 8, 2025
11d1b64
adding agent knob AGENT_RESTRICT_EOL_NODE_VERSIONS
rishabhmalikMS Dec 8, 2025
b5a899b
Minor fix
rishabhmalikMS Dec 9, 2025
a52fbdf
Adding localized strings in string.json.
rishabhmalikMS Dec 9, 2025
3e73fe2
Added NodeVersionNotAvailable string
rishabhmalikMS Dec 9, 2025
baec4a9
Added IUnifiedNodeVersionStrategy in ServiceInterfaceL0 test
rishabhmalikMS Dec 9, 2025
79110c8
Removed NodeVersionNotAvailable string temporarily.
rishabhmalikMS Dec 9, 2025
7ae51f0
Updating nomenclature
rishabhmalikMS Dec 10, 2025
e785a37
- Add NodeVersionOrchestrator to centralize Node.js version selection…
rishabhmalikMS Dec 11, 2025
90b4c7a
_ Added integration for node strategy orchestrator in nodehandler to …
rishabhmalikMS Dec 15, 2025
61fb138
Merge branch 'master' into users/rishabhmalikMS/NodehandlerStrategies
rishabhmalikMS Dec 15, 2025
04ee161
- Consolidate glibc functionality into single provider class
rishabhmalikMS Dec 15, 2025
99ab6bd
Added comment in orchestrator for priorities of strategies which is u…
rishabhmalikMS Dec 15, 2025
92584a2
- Added check for only performing glibc compatibility checks on Linux…
rishabhmalikMS Dec 16, 2025
0cc45e3
- Replace hardcoded string literals with NodeVersion enum across all …
rishabhmalikMS Dec 17, 2025
8f937b1
Minor update to log stack trace
rishabhmalikMS Dec 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"),
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
{
/// <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);

/// <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
{
/// <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