Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
17 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
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"),
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 ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

One knob is used as a feature flag to enable/disable the feature all together. Another is used to read the value of toggle from UI injected from server side. The same feature flag can be used to enable/disable Ui toggle & agent side functionality to maintain consistency on both ends.

Regarding E2E testing: Planning to use devfabric for updated server-side logic along with updated agent logic to test integrated functionality.

new BuiltInDefaultKnobSource("false"));

public static readonly Knob DisableTeePluginRemoval = new Knob(
nameof(DisableTeePluginRemoval),
"Disables removing TEE plugin after using it during checkout.",
Expand Down
148 changes: 148 additions & 0 deletions src/Agent.Worker/NodeVersionStrategies/GlibcCompatibilityChecker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using Agent.Sdk.Knob;
using Microsoft.VisualStudio.Services.Agent.Util;

namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies
{
/// <summary>
/// Utility class for checking glibc compatibility with Node.js versions on Linux systems.
/// </summary>
public sealed class GlibcCompatibilityChecker
{
private readonly IExecutionContext _executionContext;
private readonly IHostContext _hostContext;
private static bool? _supportsNode20;
private static bool? _supportsNode24;

public GlibcCompatibilityChecker(IExecutionContext executionContext, IHostContext hostContext)
{
ArgUtil.NotNull(executionContext, nameof(executionContext));
ArgUtil.NotNull(hostContext, nameof(hostContext));
_executionContext = executionContext;
_hostContext = hostContext;
}

/// <summary>
/// Checks glibc compatibility for both Node20 and Node24.
/// This method combines the behavior from NodeHandler for both Node versions.
/// </summary>
/// <returns>GlibcCompatibilityInfo containing compatibility results for both Node versions</returns>
public async Task<GlibcCompatibilityInfo> CheckGlibcCompatibilityAsync()
{
bool useNode20InUnsupportedSystem = AgentKnobs.UseNode20InUnsupportedSystem.GetValue(_executionContext).AsBoolean();
bool useNode24InUnsupportedSystem = AgentKnobs.UseNode24InUnsupportedSystem.GetValue(_executionContext).AsBoolean();

bool node20HasGlibcError = false;
bool node24HasGlibcError = false;

if (!useNode20InUnsupportedSystem)
{
if (_supportsNode20.HasValue)
{
node20HasGlibcError = !_supportsNode20.Value;
}
else
{
node20HasGlibcError = await CheckIfNodeResultsInGlibCErrorAsync("node20_1");
_executionContext.EmitHostNode20FallbackTelemetry(node20HasGlibcError);
_supportsNode20 = !node20HasGlibcError;
}
}

if (!useNode24InUnsupportedSystem)
{
if (_supportsNode24.HasValue)
{
node24HasGlibcError = !_supportsNode24.Value;
}
else
{
node24HasGlibcError = await CheckIfNodeResultsInGlibCErrorAsync("node24");
_executionContext.EmitHostNode24FallbackTelemetry(node24HasGlibcError);
_supportsNode24 = !node24HasGlibcError;
}
}

return GlibcCompatibilityInfo.Create(node24HasGlibcError, node20HasGlibcError);
}

/// <summary>
/// Checks if the specified Node.js version results in glibc compatibility errors.
/// </summary>
/// <param name="nodeFolder">The node folder name (e.g., "node20_1", "node24")</param>
/// <returns>True if glibc error is detected, false otherwise</returns>
public async Task<bool> CheckIfNodeResultsInGlibCErrorAsync(string nodeFolder)
{
var nodePath = Path.Combine(_hostContext.GetDirectory(WellKnownDirectory.Externals), nodeFolder, "bin", $"node{IOUtil.ExeExtension}");
List<string> nodeVersionOutput = await ExecuteCommandAsync(_executionContext, nodePath, "-v", requireZeroExitCode: false, showOutputOnFailureOnly: true);
var nodeResultsInGlibCError = WorkerUtilities.IsCommandResultGlibcError(_executionContext, nodeVersionOutput, out string nodeInfoLine);

return nodeResultsInGlibCError;
}

private async Task<List<string>> ExecuteCommandAsync(IExecutionContext context, string command, string arg, bool requireZeroExitCode, bool showOutputOnFailureOnly)
{
string commandLog = $"{command} {arg}";
if (!showOutputOnFailureOnly)
{
context.Command(commandLog);
}

List<string> outputs = new List<string>();
object outputLock = new object();
var processInvoker = _hostContext.CreateService<IProcessInvoker>();
processInvoker.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs message)
{
if (!string.IsNullOrEmpty(message.Data))
{
lock (outputLock)
{
outputs.Add(message.Data);
}
}
};

processInvoker.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs message)
{
if (!string.IsNullOrEmpty(message.Data))
{
lock (outputLock)
{
outputs.Add(message.Data);
}
}
};

var exitCode = await processInvoker.ExecuteAsync(
workingDirectory: _hostContext.GetDirectory(WellKnownDirectory.Work),
fileName: command,
arguments: arg,
environment: null,
requireExitCodeZero: requireZeroExitCode,
outputEncoding: null,
cancellationToken: System.Threading.CancellationToken.None);

if ((showOutputOnFailureOnly && exitCode != 0) || !showOutputOnFailureOnly)
{
if (showOutputOnFailureOnly)
{
context.Command(commandLog);
}

foreach (var outputLine in outputs)
{
context.Debug(outputLine);
}
}

return outputs;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies
{
/// <summary>
/// Contains glibc compatibility information for different Node.js versions.
/// </summary>
public sealed class GlibcCompatibilityInfo
{
/// <summary>
/// True if Node24 has glibc compatibility errors (requires glibc 2.28+).
/// </summary>
public bool Node24HasGlibcError { get; set; }

/// <summary>
/// True if Node20 has glibc compatibility errors (requires glibc 2.17+).
/// </summary>
public bool Node20HasGlibcError { get; set; }

/// <summary>
/// Creates a new instance with no glibc errors (compatible system).
/// </summary>
public static GlibcCompatibilityInfo Compatible => new GlibcCompatibilityInfo
{
Node24HasGlibcError = false,
Node20HasGlibcError = false
};

/// <summary>
/// Creates a new instance with specific compatibility information.
/// </summary>
public static GlibcCompatibilityInfo Create(bool node24HasGlibcError, bool node20HasGlibcError) =>
new GlibcCompatibilityInfo
{
Node24HasGlibcError = node24HasGlibcError,
Node20HasGlibcError = node20HasGlibcError
};
}
}
25 changes: 25 additions & 0 deletions src/Agent.Worker/NodeVersionStrategies/INodeVersionStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// 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 INodeVersionStrategy
{

/// <summary>
/// Evaluates if this strategy can handle the given context and determines the node version to use.
/// Includes handler type checks, knob evaluation, EOL policy enforcement, and glibc compatibility.
/// </summary>
/// <param name="context">Context with environment, task, and glibc information</param>
/// <param name="executionContext">Execution context for knob evaluation</param>
/// <param name="glibcInfo">Glibc compatibility information for Node versions</param>
/// <returns>NodeRunnerInfo with selected version and metadata if this strategy can handle the context, null if it cannot handle</returns>
/// <exception cref="NotSupportedException">Thrown when EOL policy prevents using any compatible version</exception>
NodeRunnerInfo CanHandle(TaskContext context, IExecutionContext executionContext, GlibcCompatibilityInfo glibcInfo);
}
}
56 changes: 56 additions & 0 deletions src/Agent.Worker/NodeVersionStrategies/Node10Strategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.IO;
using Agent.Sdk;
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 Node10Strategy : INodeVersionStrategy
{
public NodeRunnerInfo CanHandle(TaskContext context, IExecutionContext executionContext, GlibcCompatibilityInfo glibcInfo)
{
bool hasNode10Handler = context.HandlerData is Node10HandlerData;
bool eolPolicyEnabled = AgentKnobs.EnableEOLNodeVersionPolicy.GetValue(executionContext).AsBoolean();

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

bool isAlpine = PlatformUtil.RunningOnAlpine;
if (isAlpine)
{
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 DetermineNodeVersionSelection(context, eolPolicyEnabled, "Selected for Alpine Linux compatibility (Node6 incompatible)");
}

return null;
}

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

return new NodeRunnerInfo
{
NodePath = null,
NodeVersion = "node10",
Reason = baseReason,
Warning = StringUtil.Loc("NodeEOLWarning", "Node10")
};
}

}
}
44 changes: 44 additions & 0 deletions src/Agent.Worker/NodeVersionStrategies/Node16Strategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// 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 Node16Strategy : INodeVersionStrategy
{
public NodeRunnerInfo CanHandle(TaskContext context, IExecutionContext executionContext, GlibcCompatibilityInfo glibcInfo)
{
bool hasNode16Handler = context.HandlerData is Node16HandlerData;
bool eolPolicyEnabled = AgentKnobs.EnableEOLNodeVersionPolicy.GetValue(executionContext).AsBoolean();

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

return null;
}

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

return new NodeRunnerInfo
{
NodePath = null,
NodeVersion = "node16",
Reason = baseReason,
Warning = StringUtil.Loc("NodeEOLWarning", "Node16")
};
}
}
}
Loading