diff --git a/src/Agent.Sdk/ContainerInfo.cs b/src/Agent.Sdk/ContainerInfo.cs index 8e7433211d..89f1500283 100644 --- a/src/Agent.Sdk/ContainerInfo.cs +++ b/src/Agent.Sdk/ContainerInfo.cs @@ -94,6 +94,7 @@ public ContainerInfo(Pipelines.ContainerResource container, Boolean isJobContain public bool IsJobContainer { get; set; } public bool MapDockerSocket { get; set; } public bool NeedsNode16Redirect { get; set; } + public bool NeedsNode20Redirect { get; set; } public PlatformUtil.OS ImageOS { get diff --git a/src/Agent.Sdk/Knob/AgentKnobs.cs b/src/Agent.Sdk/Knob/AgentKnobs.cs index 8338d07780..806ade56dd 100644 --- a/src/Agent.Sdk/Knob/AgentKnobs.cs +++ b/src/Agent.Sdk/Knob/AgentKnobs.cs @@ -200,6 +200,22 @@ public class AgentKnobs new EnvironmentKnobSource("AGENT_USE_NODE20_IN_UNSUPPORTED_SYSTEM"), new BuiltInDefaultKnobSource("false")); + public static readonly Knob UseNode24 = new Knob( + nameof(UseNode24), + "Forces the agent to use Node 24 handler for all Node-based tasks", + new PipelineFeatureSource("UseNode24"), + new RuntimeKnobSource("AGENT_USE_NODE24"), + new EnvironmentKnobSource("AGENT_USE_NODE24"), + new BuiltInDefaultKnobSource("false")); + + public static readonly Knob UseNode24InUnsupportedSystem = new Knob( + nameof(UseNode24InUnsupportedSystem), + "Forces the agent to use Node 24 handler for all Node-based tasks, even if it's in an unsupported system", + new PipelineFeatureSource("UseNode24InUnsupportedSystem"), + new RuntimeKnobSource("AGENT_USE_NODE24_IN_UNSUPPORTED_SYSTEM"), + new EnvironmentKnobSource("AGENT_USE_NODE24_IN_UNSUPPORTED_SYSTEM"), + new BuiltInDefaultKnobSource("false")); + public static readonly Knob FetchByCommitForFullClone = new Knob( nameof(FetchByCommitForFullClone), "If true, allow fetch by commit when doing a full clone (depth=0).", @@ -707,8 +723,15 @@ public class AgentKnobs public static readonly Knob UseNode20ToStartContainer = new Knob( nameof(UseNode20ToStartContainer), "If true, the agent will use Node 20 to start docker container when executing container job and the container platform is the same as the host platform.", - new RuntimeKnobSource("AZP_AGENT_USE_NODE20_TO_START_CONTAINER"), new PipelineFeatureSource("UseNode20ToStartContainer"), + new RuntimeKnobSource("AZP_AGENT_USE_NODE20_TO_START_CONTAINER"), + new BuiltInDefaultKnobSource("false")); + + public static readonly Knob UseNode24ToStartContainer = new Knob( + nameof(UseNode24ToStartContainer), + "If true, try to start container job using Node24, then fallback to Node20, then Node16.", + new RuntimeKnobSource("AZP_AGENT_USE_NODE24_TO_START_CONTAINER"), + new PipelineFeatureSource("UseNode24ToStartContainer"), new BuiltInDefaultKnobSource("false")); public static readonly Knob EnableNewMaskerAndRegexes = new Knob( diff --git a/src/Agent.Worker/ContainerOperationProvider.cs b/src/Agent.Worker/ContainerOperationProvider.cs index 2f9aae5e8d..ac712d2c95 100644 --- a/src/Agent.Worker/ContainerOperationProvider.cs +++ b/src/Agent.Worker/ContainerOperationProvider.cs @@ -460,6 +460,7 @@ private async Task PullContainerAsync(IExecutionContext executionContext, Contai } } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "CA1505:Avoid unmaintainable code", Justification = "Complex container startup logic with multiple fallback paths")] private async Task StartContainerAsync(IExecutionContext executionContext, ContainerInfo container) { Trace.Entering(); @@ -526,8 +527,10 @@ private async Task StartContainerAsync(IExecutionContext executionContext, Conta } bool useNode20ToStartContainer = AgentKnobs.UseNode20ToStartContainer.GetValue(executionContext).AsBoolean(); + bool useNode24ToStartContainer = AgentKnobs.UseNode24ToStartContainer.GetValue(executionContext).AsBoolean(); bool useAgentNode = false; + string labelContainerStartupUsingNode24 = "container-startup-using-node-24"; string labelContainerStartupUsingNode20 = "container-startup-using-node-20"; string labelContainerStartupUsingNode16 = "container-startup-using-node-16"; string labelContainerStartupFailed = "container-startup-failed"; @@ -540,6 +543,7 @@ string containerNodePath(string nodeFolder) string nodeContainerPath = containerNodePath(NodeHandler.NodeFolder); string node16ContainerPath = containerNodePath(NodeHandler.Node16Folder); string node20ContainerPath = containerNodePath(NodeHandler.Node20_1Folder); + string node24ContainerPath = containerNodePath(NodeHandler.Node24Folder); if (container.IsJobContainer) { @@ -573,7 +577,20 @@ string useDoubleQuotes(string value) else { useAgentNode = true; - string sleepCommand = useNode20ToStartContainer ? $"'{node20ContainerPath}' --version && echo '{labelContainerStartupUsingNode20}' && {nodeSetInterval(node20ContainerPath)} || '{node16ContainerPath}' --version && echo '{labelContainerStartupUsingNode16}' && {nodeSetInterval(node16ContainerPath)} || echo '{labelContainerStartupFailed}'" : nodeSetInterval(nodeContainerPath); + string sleepCommand; + + if (useNode24ToStartContainer) + { + sleepCommand = $"'{node24ContainerPath}' --version && echo '{labelContainerStartupUsingNode24}' && {nodeSetInterval(node24ContainerPath)} || '{node20ContainerPath}' --version && echo '{labelContainerStartupUsingNode20}' && {nodeSetInterval(node20ContainerPath)} || '{node16ContainerPath}' --version && echo '{labelContainerStartupUsingNode16}' && {nodeSetInterval(node16ContainerPath)} || echo '{labelContainerStartupFailed}'"; + } + else if (useNode20ToStartContainer) + { + sleepCommand = $"'{node20ContainerPath}' --version && echo '{labelContainerStartupUsingNode20}' && {nodeSetInterval(node20ContainerPath)} || '{node16ContainerPath}' --version && echo '{labelContainerStartupUsingNode16}' && {nodeSetInterval(node16ContainerPath)} || echo '{labelContainerStartupFailed}'"; + } + else + { + sleepCommand = nodeSetInterval(nodeContainerPath); + } container.ContainerCommand = PlatformUtil.RunningOnWindows ? $"cmd.exe /c call {useDoubleQuotes(sleepCommand)}" : $"bash -c \"{sleepCommand}\""; container.ResultNodePath = nodeContainerPath; } @@ -609,7 +626,7 @@ string useDoubleQuotes(string value) executionContext.Warning($"Docker container {container.ContainerId} is not in running state."); } - else if (useAgentNode && useNode20ToStartContainer) + else if (useAgentNode && (useNode20ToStartContainer || useNode24ToStartContainer)) { bool containerStartupCompleted = false; int containerStartupTimeoutInMilliseconds = 10000; @@ -622,7 +639,14 @@ string useDoubleQuotes(string value) foreach (string logLine in containerLogs) { - if (logLine.Contains(labelContainerStartupUsingNode20)) + if (logLine.Contains(labelContainerStartupUsingNode24)) + { + executionContext.Debug("Using Node 24 for container startup."); + containerStartupCompleted = true; + container.ResultNodePath = node24ContainerPath; + break; + } + else if (logLine.Contains(labelContainerStartupUsingNode20)) { executionContext.Debug("Using Node 20 for container startup."); containerStartupCompleted = true; @@ -931,8 +955,29 @@ string useDoubleQuotes(string value) if (PlatformUtil.RunningOnLinux) { bool useNode20InUnsupportedSystem = AgentKnobs.UseNode20InUnsupportedSystem.GetValue(executionContext).AsBoolean(); + bool useNode24InUnsupportedSystem = AgentKnobs.UseNode24InUnsupportedSystem.GetValue(executionContext).AsBoolean(); - if (!useNode20InUnsupportedSystem) + if (!useNode24InUnsupportedSystem) + { + var node24 = container.TranslateToContainerPath(Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), NodeHandler.Node24Folder, "bin", $"node{IOUtil.ExeExtension}")); + + string node24TestCmd = $"bash -c \"{node24} -v\""; + List node24VersionOutput = await DockerExec(executionContext, container.ContainerId, node24TestCmd, noExceptionOnError: true); + + container.NeedsNode20Redirect = WorkerUtilities.IsCommandResultGlibcError(executionContext, node24VersionOutput, out string node24InfoLine); + + if (container.NeedsNode20Redirect) + { + PublishTelemetry( + executionContext, + new Dictionary + { + { "ContainerNode24to20Fallback", container.NeedsNode20Redirect.ToString() } + } + ); + } + } + else if (!useNode20InUnsupportedSystem) { var node20 = container.TranslateToContainerPath(Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), NodeHandler.Node20_1Folder, "bin", $"node{IOUtil.ExeExtension}")); @@ -953,6 +998,19 @@ string useDoubleQuotes(string value) } } + if (!container.NeedsNode20Redirect) + { + container.ResultNodePath = container.TranslateToContainerPath(Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), NodeHandler.Node24Folder, "bin", $"node{IOUtil.ExeExtension}")); + } + else if (!container.NeedsNode16Redirect) + { + container.ResultNodePath = container.TranslateToContainerPath(Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), NodeHandler.Node20_1Folder, "bin", $"node{IOUtil.ExeExtension}")); + } + else + { + container.ResultNodePath = container.TranslateToContainerPath(Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), NodeHandler.Node16Folder, "bin", $"node{IOUtil.ExeExtension}")); + } + } if (!string.IsNullOrEmpty(containerUserName)) diff --git a/src/Agent.Worker/ExecutionContext.cs b/src/Agent.Worker/ExecutionContext.cs index 2aeae764db..19554591c7 100644 --- a/src/Agent.Worker/ExecutionContext.cs +++ b/src/Agent.Worker/ExecutionContext.cs @@ -96,6 +96,7 @@ public interface IExecutionContext : IAgentService, IKnobValueContext /// void CancelForceTaskCompletion(); void EmitHostNode20FallbackTelemetry(bool node20ResultsInGlibCErrorHost); + void EmitHostNode24FallbackTelemetry(bool node24ResultsInGlibCErrorHost); void PublishTaskRunnerTelemetry(Dictionary taskRunnerData); } @@ -132,6 +133,7 @@ public sealed class ExecutionContext : AgentService, IExecutionContext, ICorrela private FileStream _buildLogsData; private StreamWriter _buildLogsWriter; private bool emittedHostNode20FallbackTelemetry = false; + private bool emittedHostNode24FallbackTelemetry = false; // only job level ExecutionContext will track throttling delay. private long _totalThrottlingDelayInMilliseconds = 0; @@ -973,6 +975,18 @@ public void EmitHostNode20FallbackTelemetry(bool node20ResultsInGlibCErrorHost) emittedHostNode20FallbackTelemetry = true; } } + public void EmitHostNode24FallbackTelemetry(bool node24ResultsInGlibCErrorHost) + { + if (!emittedHostNode24FallbackTelemetry) + { + PublishTelemetry(new Dictionary + { + { "HostNode24to20Fallback", node24ResultsInGlibCErrorHost.ToString() } + }); + + emittedHostNode24FallbackTelemetry = true; + } + } // This overload is to handle specific types some other way. private void PublishTelemetry( diff --git a/src/Agent.Worker/Handlers/NodeHandler.cs b/src/Agent.Worker/Handlers/NodeHandler.cs index adabc5b51d..c176cab8fa 100644 --- a/src/Agent.Worker/Handlers/NodeHandler.cs +++ b/src/Agent.Worker/Handlers/NodeHandler.cs @@ -20,7 +20,7 @@ namespace Microsoft.VisualStudio.Services.Agent.Worker.Handlers [ServiceLocator(Default = typeof(NodeHandler))] public interface INodeHandler : IHandler { - // Data can be of these four types: NodeHandlerData, Node10HandlerData, Node16HandlerData and Node20_1HandlerData + // Data can be of these five types: NodeHandlerData, Node10HandlerData, Node16HandlerData, Node20_1HandlerData and Node24HandlerData BaseNodeHandlerData Data { get; set; } } @@ -59,10 +59,11 @@ public sealed class NodeHandler : Handler, INodeHandler internal const string NodeFolder = "node"; internal static readonly string Node16Folder = "node16"; internal static readonly string Node20_1Folder = "node20_1"; + internal static readonly string Node24Folder = "node24"; private static readonly string nodeLTS = Node16Folder; private const string useNodeKnobLtsKey = "LTS"; private const string useNodeKnobUpgradeKey = "UPGRADE"; - private string[] possibleNodeFolders = { NodeFolder, node10Folder, Node16Folder, Node20_1Folder }; + private string[] possibleNodeFolders = { NodeFolder, node10Folder, Node16Folder, Node20_1Folder, Node24Folder }; private static Regex _vstsTaskLibVersionNeedsFix = new Regex("^[0-2]\\.[0-9]+", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static string[] _extensionsNode6 ={ "if (process.versions.node && process.versions.node.match(/^5\\./)) {", @@ -83,6 +84,7 @@ public sealed class NodeHandler : Handler, INodeHandler "};" }; private bool? supportsNode20; + private bool? supportsNode24; public NodeHandler() { @@ -189,32 +191,48 @@ public async Task RunAsync() else { bool useNode20InUnsupportedSystem = AgentKnobs.UseNode20InUnsupportedSystem.GetValue(ExecutionContext).AsBoolean(); + bool useNode24InUnsupportedSystem = AgentKnobs.UseNode24InUnsupportedSystem.GetValue(ExecutionContext).AsBoolean(); bool node20ResultsInGlibCErrorHost = false; + bool node24ResultsInGlibCErrorHost = false; - if (PlatformUtil.HostOS == PlatformUtil.OS.Linux && !useNode20InUnsupportedSystem) + if (PlatformUtil.HostOS == PlatformUtil.OS.Linux) { - if (supportsNode20.HasValue) + if (!useNode20InUnsupportedSystem) { - node20ResultsInGlibCErrorHost = supportsNode20.Value; + if (supportsNode20.HasValue) + { + node20ResultsInGlibCErrorHost = !supportsNode20.Value; + } + else + { + node20ResultsInGlibCErrorHost = await CheckIfNodeResultsInGlibCError(NodeHandler.Node20_1Folder); + ExecutionContext.EmitHostNode20FallbackTelemetry(node20ResultsInGlibCErrorHost); + supportsNode20 = !node20ResultsInGlibCErrorHost; + } } - else + if (!useNode24InUnsupportedSystem) { - node20ResultsInGlibCErrorHost = await CheckIfNode20ResultsInGlibCError(); - - ExecutionContext.EmitHostNode20FallbackTelemetry(node20ResultsInGlibCErrorHost); - - supportsNode20 = node20ResultsInGlibCErrorHost; + if (supportsNode24.HasValue) + { + node24ResultsInGlibCErrorHost = !supportsNode24.Value; + } + else + { + node24ResultsInGlibCErrorHost = await CheckIfNodeResultsInGlibCError(NodeHandler.Node24Folder); + ExecutionContext.EmitHostNode24FallbackTelemetry(node24ResultsInGlibCErrorHost); + supportsNode24 = !node24ResultsInGlibCErrorHost; + } } } ContainerInfo container = (ExecutionContext.StepTarget() as ContainerInfo); if (container == null) { - file = GetNodeLocation(node20ResultsInGlibCErrorHost, inContainer: false); + file = GetNodeLocation(node20ResultsInGlibCErrorHost, node24ResultsInGlibCErrorHost, inContainer: false); } else { - file = GetNodeLocation(container.NeedsNode16Redirect, inContainer: true); + file = GetNodeLocation(container.NeedsNode16Redirect, container.NeedsNode20Redirect, inContainer: true); } ExecutionContext.Debug("Using node path: " + file); @@ -251,7 +269,6 @@ public async Task RunAsync() } } } - try { @@ -300,40 +317,72 @@ public async Task RunAsync() } } - private async Task CheckIfNode20ResultsInGlibCError() + private async Task CheckIfNodeResultsInGlibCError(string nodeFolder) { - var node20 = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), NodeHandler.Node20_1Folder, "bin", $"node{IOUtil.ExeExtension}"); - List nodeVersionOutput = await ExecuteCommandAsync(ExecutionContext, node20, "-v", requireZeroExitCode: false, showOutputOnFailureOnly: true); - var node20ResultsInGlibCError = WorkerUtilities.IsCommandResultGlibcError(ExecutionContext, nodeVersionOutput, out string nodeInfoLine); + var nodePath = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), nodeFolder, "bin", $"node{IOUtil.ExeExtension}"); + List nodeVersionOutput = await ExecuteCommandAsync(ExecutionContext, nodePath, "-v", requireZeroExitCode: false, showOutputOnFailureOnly: true); + var nodeResultsInGlibCError = WorkerUtilities.IsCommandResultGlibcError(ExecutionContext, nodeVersionOutput, out string nodeInfoLine); - return node20ResultsInGlibCError; + return nodeResultsInGlibCError; } - public string GetNodeLocation(bool node20ResultsInGlibCError, bool inContainer) + private string GetNodeFolderWithFallback(string preferredNodeFolder, bool node20ResultsInGlibCError, bool node24ResultsInGlibCError, bool inContainer) + { + switch (preferredNodeFolder) + { + case var folder when folder == NodeHandler.Node24Folder: + if (node24ResultsInGlibCError) + { + // Fallback to Node20, then Node16 if Node20 also fails + if (node20ResultsInGlibCError) + { + NodeFallbackWarning("20", "16", inContainer); + return NodeHandler.Node16Folder; + } + else + { + NodeFallbackWarning("24", "20", inContainer); + return NodeHandler.Node20_1Folder; + } + } + return NodeHandler.Node24Folder; + + case var folder when folder == NodeHandler.Node20_1Folder: + if (node20ResultsInGlibCError) + { + NodeFallbackWarning("20", "16", inContainer); + return NodeHandler.Node16Folder; + } + return NodeHandler.Node20_1Folder; + + default: + return preferredNodeFolder; + } + } + + + public string GetNodeLocation(bool node20ResultsInGlibCError, bool node24ResultsInGlibCError, bool inContainer) { bool useNode10 = AgentKnobs.UseNode10.GetValue(ExecutionContext).AsBoolean(); bool useNode20_1 = AgentKnobs.UseNode20_1.GetValue(ExecutionContext).AsBoolean(); bool UseNode20InUnsupportedSystem = AgentKnobs.UseNode20InUnsupportedSystem.GetValue(ExecutionContext).AsBoolean(); + bool useNode24 = AgentKnobs.UseNode24.GetValue(ExecutionContext).AsBoolean(); bool taskHasNode10Data = Data is Node10HandlerData; bool taskHasNode16Data = Data is Node16HandlerData; bool taskHasNode20_1Data = Data is Node20_1HandlerData; + bool taskHasNode24Data = Data is Node24HandlerData; string useNodeKnob = AgentKnobs.UseNode.GetValue(ExecutionContext).AsString(); string nodeFolder = NodeHandler.NodeFolder; - - if (taskHasNode20_1Data) + if (taskHasNode24Data) + { + Trace.Info($"Task.json has node24 handler data: {taskHasNode24Data}"); + nodeFolder = GetNodeFolderWithFallback(NodeHandler.Node24Folder, node20ResultsInGlibCError, node24ResultsInGlibCError, inContainer); + } + else if (taskHasNode20_1Data) { Trace.Info($"Task.json has node20_1 handler data: {taskHasNode20_1Data} node20ResultsInGlibCError = {node20ResultsInGlibCError}"); - - if (node20ResultsInGlibCError) - { - nodeFolder = NodeHandler.Node16Folder; - Node16FallbackWarning(inContainer); - } - else - { - nodeFolder = NodeHandler.Node20_1Folder; - } + nodeFolder = GetNodeFolderWithFallback(NodeHandler.Node20_1Folder, node20ResultsInGlibCError, node24ResultsInGlibCError, inContainer); } else if (taskHasNode16Data) { @@ -351,22 +400,18 @@ public string GetNodeLocation(bool node20ResultsInGlibCError, bool inContainer) nodeFolder = NodeHandler.node10Folder; } - if (useNode20_1) + if (useNode24) + { + Trace.Info($"Found UseNode24 knob, using node24 for node tasks: {useNode24}"); + nodeFolder = GetNodeFolderWithFallback(NodeHandler.Node24Folder, node20ResultsInGlibCError, node24ResultsInGlibCError, inContainer); + } + else if (useNode20_1) { Trace.Info($"Found UseNode20_1 knob, using node20_1 for node tasks {useNode20_1} node20ResultsInGlibCError = {node20ResultsInGlibCError}"); - - if (node20ResultsInGlibCError) - { - nodeFolder = NodeHandler.Node16Folder; - Node16FallbackWarning(inContainer); - } - else - { - nodeFolder = NodeHandler.Node20_1Folder; - } + nodeFolder = GetNodeFolderWithFallback(NodeHandler.Node20_1Folder, node20ResultsInGlibCError, node24ResultsInGlibCError, inContainer); } - if (useNode10) + else if (useNode10) { Trace.Info($"Found UseNode10 knob, use node10 for node tasks: {useNode10}"); nodeFolder = NodeHandler.node10Folder; @@ -429,22 +474,15 @@ public string GetNodeLocation(bool node20ResultsInGlibCError, bool inContainer) return nodeHandlerHelper.GetNodeFolderPath(nodeFolder, HostContext); } - private void Node16FallbackWarning(bool inContainer) + private void NodeFallbackWarning(string fromVersion, string toVersion, bool inContainer) { - if (inContainer) - { - ExecutionContext.Warning($"The container operating system doesn't support Node20. Using Node16 instead. " + - "Please upgrade the operating system of the container to remain compatible with future updates of tasks: " + - "https://github.com/nodesource/distributions"); - } - else - { - ExecutionContext.Warning($"The agent operating system doesn't support Node20. Using Node16 instead. " + - "Please upgrade the operating system of the agent to remain compatible with future updates of tasks: " + + string systemType = inContainer ? "container" : "agent"; + ExecutionContext.Warning($"The {systemType} operating system doesn't support Node{fromVersion}. Using Node{toVersion} instead. " + + $"Please upgrade the operating system of the {systemType} to remain compatible with future updates of tasks: " + "https://github.com/nodesource/distributions"); - } } + private void OnDataReceived(object sender, ProcessDataReceivedEventArgs e) { // drop any outputs after the task get force completed. @@ -575,6 +613,7 @@ private void PublishHandlerTelemetry(string realHandler) string expectedHandler = ""; expectedHandler = Data switch { + Node24HandlerData => "Node24", Node20_1HandlerData => "Node20", Node16HandlerData => "Node16", Node10HandlerData => "Node10", diff --git a/src/Agent.Worker/TaskManager.cs b/src/Agent.Worker/TaskManager.cs index 1bd5659583..b5c33e7134 100644 --- a/src/Agent.Worker/TaskManager.cs +++ b/src/Agent.Worker/TaskManager.cs @@ -376,7 +376,7 @@ private void CheckForTaskDeprecation(IExecutionContext executionContext, Pipelin private void CheckIfTaskNodeRunnerIsDeprecated(IExecutionContext executionContext, Pipelines.TaskStepDefinitionReference task) { string[] deprecatedNodeRunners = { "Node", "Node10", "Node16" }; - string[] approvedNodeRunners = { "Node20_1" }; // Node runners which are not considered as deprecated + string[] approvedNodeRunners = { "Node20_1", "Node24" }; // Node runners which are not considered as deprecated string[] executionSteps = { "prejobexecution", "execution", "postjobexecution" }; JObject taskJson = GetTaskJson(task); @@ -615,6 +615,7 @@ public sealed class ExecutionData private Node10HandlerData _node10; private Node16HandlerData _node16; private Node20_1HandlerData _node20_1; + private Node24HandlerData _node24; private PowerShellHandlerData _powerShell; private PowerShell3HandlerData _powerShell3; private PowerShellExeHandlerData _powerShellExe; @@ -698,6 +699,20 @@ public Node20_1HandlerData Node20_1 } } + public Node24HandlerData Node24 + { + get + { + return _node24; + } + + set + { + _node24 = value; + Add(value); + } + } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public PowerShellHandlerData PowerShell { @@ -872,25 +887,29 @@ public string WorkingDirectory public sealed class NodeHandlerData : BaseNodeHandlerData { - public override int Priority => 4; + public override int Priority => 105; } public sealed class Node10HandlerData : BaseNodeHandlerData { - public override int Priority => 3; + public override int Priority => 104; } public sealed class Node16HandlerData : BaseNodeHandlerData { - public override int Priority => 2; + public override int Priority => 103; } public sealed class Node20_1HandlerData : BaseNodeHandlerData { - public override int Priority => 1; + public override int Priority => 102; + } + public sealed class Node24HandlerData : BaseNodeHandlerData + { + public override int Priority => 101; } public sealed class PowerShell3HandlerData : HandlerData { - public override int Priority => 5; + public override int Priority => 106; } public sealed class PowerShellHandlerData : HandlerData @@ -908,7 +927,7 @@ public string ArgumentFormat } } - public override int Priority => 6; + public override int Priority => 107; public string WorkingDirectory { @@ -939,7 +958,7 @@ public string ArgumentFormat } } - public override int Priority => 7; + public override int Priority => 108; public string WorkingDirectory { @@ -996,7 +1015,7 @@ public string InlineScript } } - public override int Priority => 7; + public override int Priority => 108; public string ScriptType { @@ -1053,7 +1072,7 @@ public string ModifyEnvironment } } - public override int Priority => 8; + public override int Priority => 109; public string WorkingDirectory { diff --git a/src/Misc/externals.sh b/src/Misc/externals.sh index 7b682caf4c..12b1906bb9 100644 --- a/src/Misc/externals.sh +++ b/src/Misc/externals.sh @@ -6,6 +6,7 @@ L1_MODE=$4 INCLUDE_NODE6=${INCLUDE_NODE6:-true} INCLUDE_NODE10=${INCLUDE_NODE10:-true} +INCLUDE_NODE24=${INCLUDE_NODE24:-true} CONTAINER_URL=https://vstsagenttools.blob.core.windows.net/tools @@ -27,6 +28,7 @@ NODE10_VERSION="10.24.1" NODE16_VERSION="16.20.2" NODE16_WIN_ARM64_VERSION="16.9.1" NODE20_VERSION="20.19.4" +NODE24_VERSION="24.10.0" MINGIT_VERSION="2.50.1" LFS_VERSION="3.4.0" @@ -179,6 +181,9 @@ if [[ "$PACKAGERUNTIME" == "win-x"* ]]; then # Copy vstsom to vstshost for default PowerShell handler behavior cp -r "$LAYOUT_DIR/externals/vstsom/"* "$LAYOUT_DIR/externals/vstshost/" fi + if [[ "$PACKAGERUNTIME" == "win-x86" ]]; then + INCLUDE_NODE24=false + fi acquireExternalTool "$CONTAINER_URL/mingit/${MINGIT_VERSION}/MinGit-${MINGIT_VERSION}-${BIT}-bit.zip" git acquireExternalTool "$CONTAINER_URL/git-lfs/${LFS_VERSION}/x${BIT}/git-lfs.exe" "git/mingw${BIT}/bin" @@ -201,6 +206,10 @@ if [[ "$PACKAGERUNTIME" == "win-x"* ]]; then acquireExternalTool "${NODE_URL}/v${NODE16_VERSION}/${PACKAGERUNTIME}/node.lib" node16/bin acquireExternalTool "${NODE_URL}/v${NODE20_VERSION}/${PACKAGERUNTIME}/node.exe" node20_1/bin acquireExternalTool "${NODE_URL}/v${NODE20_VERSION}/${PACKAGERUNTIME}/node.lib" node20_1/bin + if [[ "$INCLUDE_NODE24" == "true" ]]; then + acquireExternalTool "${NODE_URL}/v${NODE24_VERSION}/${PACKAGERUNTIME}/node.exe" node24/bin + acquireExternalTool "${NODE_URL}/v${NODE24_VERSION}/${PACKAGERUNTIME}/node.lib" node24/bin + fi elif [[ "$PACKAGERUNTIME" == "win-arm64" || "$PACKAGERUNTIME" == "win-arm32" ]]; then # Download external tools for Windows ARM @@ -242,6 +251,10 @@ elif [[ "$PACKAGERUNTIME" == "win-arm64" || "$PACKAGERUNTIME" == "win-arm32" ]]; # Official distribution of Node contains Node 20 for Windows ARM acquireExternalTool "${NODE_URL}/v${NODE20_VERSION}/${PACKAGERUNTIME}/node.exe" node20_1/bin acquireExternalTool "${NODE_URL}/v${NODE20_VERSION}/${PACKAGERUNTIME}/node.lib" node20_1/bin + + # Official distribution of Node contains Node 24 for Windows ARM + acquireExternalTool "${NODE_URL}/v${NODE24_VERSION}/${PACKAGERUNTIME}/node.exe" node24/bin + acquireExternalTool "${NODE_URL}/v${NODE24_VERSION}/${PACKAGERUNTIME}/node.lib" node24/bin else # Download external tools for Linux and OSX. @@ -258,6 +271,7 @@ else ARCH="darwin-arm64" acquireExternalTool "${NODE_URL}/v${NODE16_VERSION}/node-v${NODE16_VERSION}-${ARCH}.tar.gz" node16 fix_nested_dir acquireExternalTool "${NODE_URL}/v${NODE20_VERSION}/node-v${NODE20_VERSION}-${ARCH}.tar.gz" node20_1 fix_nested_dir + acquireExternalTool "${NODE_URL}/v${NODE24_VERSION}/node-v${NODE24_VERSION}-${ARCH}.tar.gz" node24 fix_nested_dir elif [[ "$PACKAGERUNTIME" == "linux-musl-arm64" ]]; then ARCH="linux-arm64-musl" @@ -267,6 +281,7 @@ else acquireExternalTool "${CONTAINER_URL}/nodejs/${ARCH}/node-v${NODE16_VERSION}-${ARCH}.tar.gz" node16/bin fix_nested_dir false node_alpine_arm64 acquireExternalTool "${CONTAINER_URL}/nodejs/${ARCH}/node-v${NODE20_VERSION}-${ARCH}.tar.gz" node20_1/bin fix_nested_dir false node_alpine_arm64 + acquireExternalTool "${CONTAINER_URL}/nodejs/${ARCH}/node-v${NODE24_VERSION}-${ARCH}.tar.gz" node24/bin fix_nested_dir false node_alpine_arm64 else case $PACKAGERUNTIME in "linux-musl-x64") ARCH="linux-x64-musl";; @@ -285,6 +300,7 @@ else fi acquireExternalTool "${NODE_URL}/v${NODE16_VERSION}/node-v${NODE16_VERSION}-${ARCH}.tar.gz" node16 fix_nested_dir acquireExternalTool "${NODE_URL}/v${NODE20_VERSION}/node-v${NODE20_VERSION}-${ARCH}.tar.gz" node20_1 fix_nested_dir + acquireExternalTool "${NODE_URL}/v${NODE24_VERSION}/node-v${NODE24_VERSION}-${ARCH}.tar.gz" node24 fix_nested_dir fi # remove `npm`, `npx`, `corepack`, and related `node_modules` from the `externals/node*` agent directory # they are installed along with node, but agent does not use them @@ -305,6 +321,11 @@ else rm "$LAYOUT_DIR/externals/node20_1/bin/npm" rm "$LAYOUT_DIR/externals/node20_1/bin/npx" rm "$LAYOUT_DIR/externals/node20_1/bin/corepack" + + rm -rf "$LAYOUT_DIR/externals/node24/lib" + rm "$LAYOUT_DIR/externals/node24/bin/npm" + rm "$LAYOUT_DIR/externals/node24/bin/npx" + rm "$LAYOUT_DIR/externals/node24/bin/corepack" fi if [[ "$L1_MODE" != "" || "$PRECACHE" != "" ]]; then diff --git a/src/Test/L0/NodeHandlerL0.cs b/src/Test/L0/NodeHandlerL0.cs index 1116263ccf..0a96ef8ed9 100644 --- a/src/Test/L0/NodeHandlerL0.cs +++ b/src/Test/L0/NodeHandlerL0.cs @@ -50,7 +50,7 @@ public void UseNodeForNodeHandlerEnvVarNotSet() nodeVersion = "node10"; // version 6 does not exist on Alpine } - string actualLocation = nodeHandler.GetNodeLocation(node20ResultsInGlibCError: false, inContainer: false); + string actualLocation = nodeHandler.GetNodeLocation(node20ResultsInGlibCError: false, node24ResultsInGlibCError: false, inContainer: false); string expectedLocation = Path.Combine(thc.GetDirectory(WellKnownDirectory.Externals), nodeVersion, "bin", @@ -64,6 +64,7 @@ public void UseNodeForNodeHandlerEnvVarNotSet() [InlineData("node10")] [InlineData("node16")] [InlineData("node20_1")] + [InlineData("node24")] [Trait("Level", "L0")] [Trait("Category", "Common")] public void UseNewNodeForNewNodeHandler(string nodeVersion) @@ -85,10 +86,11 @@ public void UseNewNodeForNewNodeHandler(string nodeVersion) "node10" => new Node10HandlerData(), "node16" => new Node16HandlerData(), "node20_1" => new Node20_1HandlerData(), + "node24" => new Node24HandlerData(), _ => throw new Exception("Invalid node version"), }; - string actualLocation = nodeHandler.GetNodeLocation(node20ResultsInGlibCError: false, inContainer: false); + string actualLocation = nodeHandler.GetNodeLocation(node20ResultsInGlibCError: false, node24ResultsInGlibCError: false, inContainer: false); string expectedLocation = Path.Combine(thc.GetDirectory(WellKnownDirectory.Externals), nodeVersion, "bin", @@ -117,7 +119,7 @@ public void UseNewNodeForNodeHandlerEnvVarSet() nodeHandler.ExecutionContext = CreateTestExecutionContext(thc); nodeHandler.Data = new Node10HandlerData(); - string actualLocation = nodeHandler.GetNodeLocation(node20ResultsInGlibCError: false, inContainer: false); + string actualLocation = nodeHandler.GetNodeLocation(node20ResultsInGlibCError: false, node24ResultsInGlibCError: false, inContainer: false); string expectedLocation = Path.Combine(thc.GetDirectory(WellKnownDirectory.Externals), "node10", "bin", @@ -151,7 +153,7 @@ public void UseNewNodeForNodeHandlerHostContextVarSet() nodeHandler.ExecutionContext = CreateTestExecutionContext(thc, variables); nodeHandler.Data = new Node10HandlerData(); - string actualLocation = nodeHandler.GetNodeLocation(node20ResultsInGlibCError: false, inContainer: false); + string actualLocation = nodeHandler.GetNodeLocation(node20ResultsInGlibCError: false, node24ResultsInGlibCError: false, inContainer: false); string expectedLocation = Path.Combine(thc.GetDirectory(WellKnownDirectory.Externals), "node10", "bin", @@ -183,7 +185,7 @@ public void UseNewNodeForNewNodeHandlerHostContextVarUnset() nodeHandler.ExecutionContext = CreateTestExecutionContext(thc, variables); nodeHandler.Data = new Node10HandlerData(); - string actualLocation = nodeHandler.GetNodeLocation(node20ResultsInGlibCError: false, inContainer: false); + string actualLocation = nodeHandler.GetNodeLocation(node20ResultsInGlibCError: false, node24ResultsInGlibCError: false, inContainer: false); string expectedLocation = Path.Combine(thc.GetDirectory(WellKnownDirectory.Externals), "node10", "bin", @@ -222,7 +224,7 @@ public void UseLTSNodeIfUseNodeKnobIsLTS() nodeHandler.ExecutionContext = CreateTestExecutionContext(thc, variables); nodeHandler.Data = new Node10HandlerData(); - string actualLocation = nodeHandler.GetNodeLocation(node20ResultsInGlibCError: false, inContainer: false); + string actualLocation = nodeHandler.GetNodeLocation(node20ResultsInGlibCError: false, node24ResultsInGlibCError: false, inContainer: false); string expectedLocation = Path.Combine(thc.GetDirectory(WellKnownDirectory.Externals), "node16", "bin", @@ -261,7 +263,7 @@ public void ThrowExceptionIfUseNodeKnobIsLTSAndLTSNotAvailable() nodeHandler.ExecutionContext = CreateTestExecutionContext(thc, variables); nodeHandler.Data = new Node10HandlerData(); - Assert.Throws(() => nodeHandler.GetNodeLocation(node20ResultsInGlibCError: false, inContainer: false)); + Assert.Throws(() => nodeHandler.GetNodeLocation(node20ResultsInGlibCError: false, node24ResultsInGlibCError: false, inContainer: false)); } } @@ -290,7 +292,7 @@ public void ThrowExceptionIfUseNodeKnobIsLTSAndFilteredPossibleNodeFoldersEmpty( nodeHandler.ExecutionContext = CreateTestExecutionContext(thc, variables); nodeHandler.Data = new Node10HandlerData(); - Assert.Throws(() => nodeHandler.GetNodeLocation(node20ResultsInGlibCError: false, inContainer: false)); + Assert.Throws(() => nodeHandler.GetNodeLocation(node20ResultsInGlibCError: false, node24ResultsInGlibCError: false, inContainer: false)); } } @@ -323,7 +325,7 @@ public void UseFirstAvailableNodeIfUseNodeKnobIsUpgrade() nodeHandler.ExecutionContext = CreateTestExecutionContext(thc, variables); nodeHandler.Data = new Node10HandlerData(); - string actualLocation = nodeHandler.GetNodeLocation(node20ResultsInGlibCError: false, inContainer: false); + string actualLocation = nodeHandler.GetNodeLocation(node20ResultsInGlibCError: false, node24ResultsInGlibCError: false, inContainer: false); string expectedLocation = Path.Combine(thc.GetDirectory(WellKnownDirectory.Externals), "nextAvailableNode1", "bin", @@ -362,7 +364,7 @@ public void UseSecondAvailableNodeIfUseNodeKnobIsUpgradeFilteredNodeFoldersFirst nodeHandler.ExecutionContext = CreateTestExecutionContext(thc, variables); nodeHandler.Data = new Node10HandlerData(); - string actualLocation = nodeHandler.GetNodeLocation(node20ResultsInGlibCError: false, inContainer: false); + string actualLocation = nodeHandler.GetNodeLocation(node20ResultsInGlibCError: false, node24ResultsInGlibCError: false, inContainer: false); string expectedLocation = Path.Combine(thc.GetDirectory(WellKnownDirectory.Externals), "nextAvailableNode2", "bin", @@ -401,7 +403,7 @@ public void ThrowExceptionIfUseNodeKnobIsUpgradeFilteredNodeFoldersAllNotAvailab nodeHandler.ExecutionContext = CreateTestExecutionContext(thc, variables); nodeHandler.Data = new Node10HandlerData(); - Assert.Throws(() => nodeHandler.GetNodeLocation(node20ResultsInGlibCError: false, inContainer: false)); + Assert.Throws(() => nodeHandler.GetNodeLocation(node20ResultsInGlibCError: false, node24ResultsInGlibCError: false, inContainer: false)); } } @@ -471,6 +473,8 @@ private void ResetNodeKnobs() Environment.SetEnvironmentVariable("AGENT_USE_NODE10", null); Environment.SetEnvironmentVariable("AGENT_USE_NODE20_1", null); Environment.SetEnvironmentVariable("AGENT_USE_NODE20_IN_UNSUPPORTED_SYSTEM", null); + Environment.SetEnvironmentVariable("AGENT_USE_NODE24", null); + Environment.SetEnvironmentVariable("AGENT_USE_NODE24_IN_UNSUPPORTED_SYSTEM", null); } } } \ No newline at end of file diff --git a/src/Test/L1/Worker/L1TestBase.cs b/src/Test/L1/Worker/L1TestBase.cs index 16138739f6..16886fd7d4 100644 --- a/src/Test/L1/Worker/L1TestBase.cs +++ b/src/Test/L1/Worker/L1TestBase.cs @@ -370,6 +370,8 @@ private void ResetNodeKnobs() Environment.SetEnvironmentVariable("AGENT_USE_NODE10", null); Environment.SetEnvironmentVariable("AGENT_USE_NODE20_1", null); Environment.SetEnvironmentVariable("AGENT_USE_NODE20_IN_UNSUPPORTED_SYSTEM", null); + Environment.SetEnvironmentVariable("AGENT_USE_NODE24", null); + Environment.SetEnvironmentVariable("AGENT_USE_NODE24_IN_UNSUPPORTED_SYSTEM", null); } protected virtual void Dispose(bool disposing)