From 55df207b8a6a41b1711d7ae5e1451b9de6be9a42 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 17:27:28 +0000 Subject: [PATCH] feat(ir): add typed helper for Npm@1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds npm_step() factory function to src/compile/ir/tasks.rs for the Npm@1 ADO built-in task. Required input (command) is positional; optional inputs (workingDir, verbose, customCommand, customRegistry, customFeed, publishRegistry, publishFeed, etc.) are applied via .with_input(…) on the returned TaskStep. Six unit tests cover install, ci, publish, custom with workingDir, publish with Azure Artifacts feed, and install with a custom feed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/compile/ir/tasks.rs | 120 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/src/compile/ir/tasks.rs b/src/compile/ir/tasks.rs index 389be749..2ddc5f27 100644 --- a/src/compile/ir/tasks.rs +++ b/src/compile/ir/tasks.rs @@ -381,6 +381,36 @@ pub fn delete_files_step(contents: impl Into) -> TaskStep { .with_input("Contents", contents) } +/// Returns a [`TaskStep`] for `Npm@1`. +/// +/// Runs an npm command against the package in the working directory. +/// Supports `npmjs.com` and authenticated registries such as Azure Artifacts. +/// +/// - `command` — the npm operation: `"install"`, `"ci"`, `"publish"`, or +/// `"custom"`. The ADO task default is `"install"`. +/// +/// Optional inputs (applied with `.with_input(…)` on the returned value): +/// +/// | Input key | Type | Default | Description | +/// |---|---|---|---| +/// | `workingDir` | string | — | Working folder containing `package.json`. | +/// | `verbose` | bool string | — | Enable verbose logging (for `install`, `ci`, `publish`). | +/// | `customCommand` | string | — | Required when `command = "custom"`. The npm arguments to forward. | +/// | `customRegistry` | string | `"useNpmrc"` | Registry for `install`/`ci`/`custom`: `"useNpmrc"` or `"useFeed"`. | +/// | `customFeed` | string | — | Required when `customRegistry = "useFeed"`. Azure Artifacts feed ID or URL. | +/// | `customEndpoint` | string | — | Service connection for registries outside the organisation. | +/// | `publishRegistry` | string | `"useExternalRegistry"` | Registry for `publish`: `"useExternalRegistry"` or `"useFeed"`. | +/// | `publishFeed` | string | — | Required when `publishRegistry = "useFeed"`. Target Azure Artifacts feed. | +/// | `publishEndpoint` | string | — | Required when `publishRegistry = "useExternalRegistry"`. External registry service connection. | +/// | `publishPackageMetadata` | bool string | `"true"` | Attach pipeline metadata to packages published via `useFeed`. | +/// +/// ADO task reference: +/// +pub fn npm_step(command: impl Into) -> TaskStep { + let cmd: String = command.into(); + TaskStep::new("Npm@1", format!("npm {cmd}")).with_input("command", cmd) +} + #[cfg(test)] mod tests { use super::*; @@ -1122,4 +1152,94 @@ mod tests { ); assert_eq!(t.inputs.len(), 2); } + + // ── Npm@1 ───────────────────────────────────────────────────────────── + + #[test] + fn npm_step_install_sets_task_and_command() { + let t = npm_step("install"); + assert_eq!(t.task, "Npm@1"); + assert_eq!(t.display_name, "npm install"); + assert_eq!(t.inputs.get("command").map(|s| s.as_str()), Some("install")); + // only the required input is set by default + assert_eq!(t.inputs.len(), 1); + } + + #[test] + fn npm_step_ci_command() { + let t = npm_step("ci"); + assert_eq!(t.task, "Npm@1"); + assert_eq!(t.display_name, "npm ci"); + assert_eq!(t.inputs.get("command").map(|s| s.as_str()), Some("ci")); + assert_eq!(t.inputs.len(), 1); + } + + #[test] + fn npm_step_publish_command() { + let t = npm_step("publish"); + assert_eq!(t.task, "Npm@1"); + assert_eq!(t.display_name, "npm publish"); + assert_eq!( + t.inputs.get("command").map(|s| s.as_str()), + Some("publish") + ); + assert_eq!(t.inputs.len(), 1); + } + + #[test] + fn npm_step_custom_with_working_dir_and_command() { + let t = npm_step("custom") + .with_input("customCommand", "run build -- --production") + .with_input("workingDir", "$(Build.SourcesDirectory)/frontend"); + assert_eq!(t.task, "Npm@1"); + assert_eq!(t.display_name, "npm custom"); + assert_eq!(t.inputs.get("command").map(|s| s.as_str()), Some("custom")); + assert_eq!( + t.inputs.get("customCommand").map(|s| s.as_str()), + Some("run build -- --production") + ); + assert_eq!( + t.inputs.get("workingDir").map(|s| s.as_str()), + Some("$(Build.SourcesDirectory)/frontend") + ); + assert_eq!(t.inputs.len(), 3); + } + + #[test] + fn npm_step_publish_with_azure_artifacts_feed() { + let t = npm_step("publish") + .with_input("publishRegistry", "useFeed") + .with_input("publishFeed", "my-org/my-feed"); + assert_eq!(t.task, "Npm@1"); + assert_eq!( + t.inputs.get("command").map(|s| s.as_str()), + Some("publish") + ); + assert_eq!( + t.inputs.get("publishRegistry").map(|s| s.as_str()), + Some("useFeed") + ); + assert_eq!( + t.inputs.get("publishFeed").map(|s| s.as_str()), + Some("my-org/my-feed") + ); + assert_eq!(t.inputs.len(), 3); + } + + #[test] + fn npm_step_install_with_custom_feed() { + let t = npm_step("install") + .with_input("customRegistry", "useFeed") + .with_input("customFeed", "my-org/npm-feed"); + assert_eq!(t.task, "Npm@1"); + assert_eq!( + t.inputs.get("customRegistry").map(|s| s.as_str()), + Some("useFeed") + ); + assert_eq!( + t.inputs.get("customFeed").map(|s| s.as_str()), + Some("my-org/npm-feed") + ); + assert_eq!(t.inputs.len(), 3); + } }