From a15378721313ff4a68f9607015fdedf1547865c0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 17:04:18 +0000 Subject: [PATCH] feat(ir): add typed builder for NodeTool@0 Adds NodeTool (the legacy Node.js tool installer) to the typed IR task catalog. The builder supports both version-spec mode (NodeTool::new) and version-file mode (NodeTool::from_file) matching the two versionSource values. All optional inputs are typed setters; bool inputs use bool_input. NodeTool@0 appears 7 times in the safe-outputs test lock files and is therefore a high-signal addition to the typed task coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/compile/ir/tasks/mod.rs | 1 + src/compile/ir/tasks/node_tool.rs | 243 ++++++++++++++++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 src/compile/ir/tasks/node_tool.rs diff --git a/src/compile/ir/tasks/mod.rs b/src/compile/ir/tasks/mod.rs index 60be370c..50241224 100644 --- a/src/compile/ir/tasks/mod.rs +++ b/src/compile/ir/tasks/mod.rs @@ -46,6 +46,7 @@ pub mod java_tool_installer; pub mod manual_validation; pub mod maven; pub mod maven_authenticate; +pub mod node_tool; pub mod npm; pub mod npm_authenticate; pub mod nuget_authenticate; diff --git a/src/compile/ir/tasks/node_tool.rs b/src/compile/ir/tasks/node_tool.rs new file mode 100644 index 00000000..277a4a61 --- /dev/null +++ b/src/compile/ir/tasks/node_tool.rs @@ -0,0 +1,243 @@ +//! Typed builder for `NodeTool@0`. +//! +//! ADO task reference: +//! + +use super::common::bool_input; +use crate::compile::ir::step::TaskStep; + +/// Version source for [`NodeTool`]. +/// +/// Controls whether the Node.js version is taken from an explicit version +/// spec string or read from an `.nvmrc` / `.node-version` file. +#[derive(Debug, Clone)] +enum VersionSource { + /// Use an explicit version spec (e.g. `"20.x"`). This is the default. + Spec(String), + /// Read the version from the file at the given path (e.g. `".nvmrc"`). + File(String), +} + +/// Builder for a [`TaskStep`] invoking `NodeTool@0`. +/// +/// Installs a specific version of Node.js and adds it to the PATH. +/// `NodeTool@0` is the legacy Node.js tool installer; new pipelines should +/// prefer [`super::use_node::UseNode`] (`UseNode@1`). +/// +/// ADO task reference: +/// +#[derive(Debug, Clone)] +pub struct NodeTool { + source: VersionSource, + check_latest: Option, + force32bit: Option, + nodejs_mirror: Option, + retry_count_on_download_fails: Option, + delay_between_retries: Option, + display_name: Option, +} + +impl NodeTool { + /// Construct with an explicit version spec (e.g. `"20.x"`, `"18.x"`). + /// + /// The `versionSpec` input is set to `spec`; `versionSource` defaults to + /// `spec` and is omitted from the emitted YAML (no unnecessary noise). + pub fn new(version_spec: impl Into) -> Self { + Self { + source: VersionSource::Spec(version_spec.into()), + check_latest: None, + force32bit: None, + nodejs_mirror: None, + retry_count_on_download_fails: None, + delay_between_retries: None, + display_name: None, + } + } + + /// Construct from a version file (e.g. `".nvmrc"` or `".node-version"`). + /// + /// Sets `versionSource` to `fromFile` and `versionFilePath` to `path`. + pub fn from_file(path: impl Into) -> Self { + Self { + source: VersionSource::File(path.into()), + check_latest: None, + force32bit: None, + nodejs_mirror: None, + retry_count_on_download_fails: None, + delay_between_retries: None, + display_name: None, + } + } + + /// `checkLatest` — check online for the latest available version that + /// satisfies the version spec. Default: `false`. + pub fn check_latest(mut self, value: bool) -> Self { + self.check_latest = Some(value); + self + } + + /// `force32bit` — install the x86 version of Node.js on a 64-bit Windows + /// agent. Default: `false`. + pub fn force32bit(mut self, value: bool) -> Self { + self.force32bit = Some(value); + self + } + + /// `nodejsMirror` — base URL for Node.js binaries. + /// Default: `"https://nodejs.org/dist"`. + pub fn nodejs_mirror(mut self, value: impl Into) -> Self { + self.nodejs_mirror = Some(value.into()); + self + } + + /// `retryCountOnDownloadFails` — how many times to retry if the Node.js + /// binary download fails. Default: `"5"`. + pub fn retry_count_on_download_fails(mut self, value: impl Into) -> Self { + self.retry_count_on_download_fails = Some(value.into()); + self + } + + /// `delayBetweenRetries` — delay in milliseconds between download retries. + /// Default: `"1000"`. + pub fn delay_between_retries(mut self, value: impl Into) -> Self { + self.delay_between_retries = Some(value.into()); + self + } + + /// Override the default `displayName`. + pub fn with_display_name(mut self, value: impl Into) -> Self { + self.display_name = Some(value.into()); + self + } + + /// Lower into a [`TaskStep`]. + pub fn into_step(self) -> TaskStep { + let default_display = match &self.source { + VersionSource::Spec(spec) => format!("Install Node.js {spec}"), + VersionSource::File(path) => format!("Install Node.js (from {path})"), + }; + let mut t = TaskStep::new("NodeTool@0", self.display_name.unwrap_or(default_display)); + match self.source { + VersionSource::Spec(spec) => { + t = t.with_input("versionSpec", spec); + } + VersionSource::File(path) => { + t = t + .with_input("versionSource", "fromFile") + .with_input("versionFilePath", path); + } + } + if let Some(v) = self.check_latest { + t = t.with_input("checkLatest", bool_input(v)); + } + if let Some(v) = self.force32bit { + t = t.with_input("force32bit", bool_input(v)); + } + if let Some(v) = self.nodejs_mirror { + t = t.with_input("nodejsMirror", v); + } + if let Some(v) = self.retry_count_on_download_fails { + t = t.with_input("retryCountOnDownloadFails", v); + } + if let Some(v) = self.delay_between_retries { + t = t.with_input("delayBetweenRetries", v); + } + t + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sets_task_and_version_spec() { + let t = NodeTool::new("20.x").into_step(); + assert_eq!(t.task, "NodeTool@0"); + assert_eq!(t.display_name, "Install Node.js 20.x"); + assert_eq!( + t.inputs.get("versionSpec").map(String::as_str), + Some("20.x") + ); + assert!(t.inputs.get("versionSource").is_none()); + assert!(t.inputs.get("checkLatest").is_none()); + assert!(t.inputs.get("force32bit").is_none()); + assert!(t.inputs.get("nodejsMirror").is_none()); + } + + #[test] + fn from_file_sets_version_source_and_path() { + let t = NodeTool::from_file(".nvmrc").into_step(); + assert_eq!(t.task, "NodeTool@0"); + assert_eq!(t.display_name, "Install Node.js (from .nvmrc)"); + assert_eq!( + t.inputs.get("versionSource").map(String::as_str), + Some("fromFile") + ); + assert_eq!( + t.inputs.get("versionFilePath").map(String::as_str), + Some(".nvmrc") + ); + assert!(t.inputs.get("versionSpec").is_none()); + } + + #[test] + fn optional_inputs_are_emitted_when_set() { + let t = NodeTool::new("18.x") + .check_latest(true) + .force32bit(false) + .nodejs_mirror("https://my.mirror/nodejs/dist") + .retry_count_on_download_fails("3") + .delay_between_retries("500") + .into_step(); + assert_eq!( + t.inputs.get("versionSpec").map(String::as_str), + Some("18.x") + ); + assert_eq!( + t.inputs.get("checkLatest").map(String::as_str), + Some("true") + ); + assert_eq!( + t.inputs.get("force32bit").map(String::as_str), + Some("false") + ); + assert_eq!( + t.inputs.get("nodejsMirror").map(String::as_str), + Some("https://my.mirror/nodejs/dist") + ); + assert_eq!( + t.inputs.get("retryCountOnDownloadFails").map(String::as_str), + Some("3") + ); + assert_eq!( + t.inputs.get("delayBetweenRetries").map(String::as_str), + Some("500") + ); + } + + #[test] + fn display_name_override() { + let t = NodeTool::new("20.x") + .with_display_name("Install Node.js for ado-script") + .into_step(); + assert_eq!(t.display_name, "Install Node.js for ado-script"); + assert_eq!( + t.inputs.get("versionSpec").map(String::as_str), + Some("20.x") + ); + } + + #[test] + fn different_versions() { + for version in &["16.x", "18.x", "20.x", "22.x"] { + let t = NodeTool::new(*version).into_step(); + assert_eq!(t.task, "NodeTool@0"); + assert_eq!( + t.inputs.get("versionSpec").map(String::as_str), + Some(*version) + ); + assert_eq!(t.display_name, format!("Install Node.js {version}")); + } + } +}