From 864e86728aaaab5113ebfa37fb2558dbfaf98424 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 21:14:31 +0000 Subject: [PATCH] feat(ir): add typed builder for DownloadPackage@1 Adds a typed builder struct for `DownloadPackage@1` to the ado-aw IR. Introduces `PackageType` enum (NuGet, Npm, PyPi, Maven, UPack, Cargo) and a `DownloadPackage` builder with a `nuget()` convenience constructor. Migrates `download_package_step` in `agentic_pipeline.rs` from a raw `TaskStep::new("DownloadPackage@1", ...)` call to the new typed builder, eliminating stringly-typed input keys at the call site. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/compile/agentic_pipeline.rs | 10 +- src/compile/ir/tasks/download_package.rs | 205 +++++++++++++++++++++++ src/compile/ir/tasks/mod.rs | 1 + 3 files changed, 210 insertions(+), 6 deletions(-) create mode 100644 src/compile/ir/tasks/download_package.rs diff --git a/src/compile/agentic_pipeline.rs b/src/compile/agentic_pipeline.rs index 95037ccb..4353fb1b 100644 --- a/src/compile/agentic_pipeline.rs +++ b/src/compile/agentic_pipeline.rs @@ -65,6 +65,7 @@ use super::ir::step::{ BashStep, CheckoutRepo, CheckoutStep, DownloadStep, PublishStep, Step, SubmodulesOpt, TaskStep, }; use super::ir::tasks::docker_installer::DockerInstaller; +use super::ir::tasks::download_package::DownloadPackage; use super::ir::{ CiTrigger, Parameter, ParameterDefault, ParameterKind, PipelineResource, PipelineVar, PrTrigger, RepositoryResource, Resources, Schedule, Triggers, @@ -1222,12 +1223,9 @@ pub(crate) fn download_package_step( version: &str, download_path: &str, ) -> TaskStep { - TaskStep::new("DownloadPackage@1", display) - .with_input("packageType", "nuget") - .with_input("feed", feed) - .with_input("definition", package) - .with_input("version", version) - .with_input("downloadPath", download_path) + DownloadPackage::nuget(feed, package, version, download_path) + .with_display_name(display) + .into_step() } /// Bash body that locates a payload file inside a `DownloadPackage@1` staging diff --git a/src/compile/ir/tasks/download_package.rs b/src/compile/ir/tasks/download_package.rs new file mode 100644 index 00000000..4b1297be --- /dev/null +++ b/src/compile/ir/tasks/download_package.rs @@ -0,0 +1,205 @@ +//! Typed builder for `DownloadPackage@1`. + +use super::common::{bool_input, push_opt}; +use crate::compile::ir::step::TaskStep; + +/// Package ecosystem for [`DownloadPackage`] (`packageType` input). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PackageType { + NuGet, + Npm, + PyPi, + Maven, + UPack, + Cargo, +} + +impl PackageType { + /// The exact token the ADO task expects. + pub fn as_ado_str(self) -> &'static str { + match self { + PackageType::NuGet => "nuget", + PackageType::Npm => "npm", + PackageType::PyPi => "pypi", + PackageType::Maven => "maven", + PackageType::UPack => "upack", + PackageType::Cargo => "cargo", + } + } +} + +/// Builder for a [`TaskStep`] invoking `DownloadPackage@1`. +/// +/// Downloads a package from an Azure Artifacts feed into `download_path`. +/// The required inputs are `package_type`, `feed`, `definition` (package +/// name), `version`, and `download_path`; optional inputs are applied through +/// typed setters and only emitted when set. +/// +/// Use [`DownloadPackage::nuget`] as a convenience constructor when downloading +/// NuGet packages (the most common case in ado-aw supply-chain steps). +/// +/// ADO task reference: +/// +#[derive(Debug, Clone)] +pub struct DownloadPackage { + package_type: PackageType, + feed: String, + /// The package name (`definition` input). + definition: String, + version: String, + download_path: String, + view: Option, + files: Option, + extract: Option, + display_name: Option, +} + +impl DownloadPackage { + /// Required inputs: `packageType`, `feed`, package `definition`, `version`, + /// and `downloadPath`. + pub fn new( + package_type: PackageType, + feed: impl Into, + definition: impl Into, + version: impl Into, + download_path: impl Into, + ) -> Self { + Self { + package_type, + feed: feed.into(), + definition: definition.into(), + version: version.into(), + download_path: download_path.into(), + view: None, + files: None, + extract: None, + display_name: None, + } + } + + /// Convenience constructor for NuGet packages. + pub fn nuget( + feed: impl Into, + definition: impl Into, + version: impl Into, + download_path: impl Into, + ) -> Self { + Self::new(PackageType::NuGet, feed, definition, version, download_path) + } + + /// `view` — view within the feed to resolve the package from (e.g. + /// `"Release"`, `"Prerelease"`). Omit to resolve from the feed directly. + pub fn view(mut self, value: impl Into) -> Self { + self.view = Some(value.into()); + self + } + + /// `files` — glob patterns selecting files to download from the package + /// (default: `"**"` — all files). + pub fn files(mut self, value: impl Into) -> Self { + self.files = Some(value.into()); + self + } + + /// `extract` — whether to extract the package contents after download + /// (default: `true`). + pub fn extract(mut self, value: bool) -> Self { + self.extract = Some(value); + self + } + + /// Override the default `displayName` (`"Download Package"`). + 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 mut t = TaskStep::new( + "DownloadPackage@1", + self.display_name.unwrap_or_else(|| "Download Package".into()), + ) + .with_input("packageType", self.package_type.as_ado_str()) + .with_input("feed", self.feed) + .with_input("definition", self.definition) + .with_input("version", self.version) + .with_input("downloadPath", self.download_path); + push_opt(&mut t, "view", self.view); + push_opt(&mut t, "files", self.files); + if let Some(v) = self.extract { + t = t.with_input("extract", bool_input(v)); + } + t + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn nuget_convenience_sets_required_inputs() { + let t = DownloadPackage::nuget( + "my-feed", + "my-package", + "1.2.3", + "$(System.ArtifactsDirectory)", + ) + .into_step(); + assert_eq!(t.task, "DownloadPackage@1"); + assert_eq!(t.display_name, "Download Package"); + assert_eq!(t.inputs.get("packageType").map(String::as_str), Some("nuget")); + assert_eq!(t.inputs.get("feed").map(String::as_str), Some("my-feed")); + assert_eq!(t.inputs.get("definition").map(String::as_str), Some("my-package")); + assert_eq!(t.inputs.get("version").map(String::as_str), Some("1.2.3")); + assert_eq!( + t.inputs.get("downloadPath").map(String::as_str), + Some("$(System.ArtifactsDirectory)") + ); + } + + #[test] + fn optional_inputs_emit_only_when_set() { + let t = DownloadPackage::nuget("feed", "pkg", "2.0.0", "$(Pipeline.Workspace)/out") + .view("Release") + .files("**/*.dll") + .extract(false) + .into_step(); + assert_eq!(t.inputs.get("view").map(String::as_str), Some("Release")); + assert_eq!(t.inputs.get("files").map(String::as_str), Some("**/*.dll")); + assert_eq!(t.inputs.get("extract").map(String::as_str), Some("false")); + } + + #[test] + fn optional_inputs_absent_when_not_set() { + let t = DownloadPackage::nuget("feed", "pkg", "1.0.0", "/tmp/out").into_step(); + assert!(t.inputs.get("view").is_none()); + assert!(t.inputs.get("files").is_none()); + assert!(t.inputs.get("extract").is_none()); + } + + #[test] + fn display_name_override() { + let t = DownloadPackage::nuget("feed", "pkg", "1.0.0", "/tmp/out") + .with_display_name("Download my-package v1.0.0") + .into_step(); + assert_eq!(t.display_name, "Download my-package v1.0.0"); + } + + #[test] + fn all_package_types_round_trip() { + let cases = [ + (PackageType::NuGet, "nuget"), + (PackageType::Npm, "npm"), + (PackageType::PyPi, "pypi"), + (PackageType::Maven, "maven"), + (PackageType::UPack, "upack"), + (PackageType::Cargo, "cargo"), + ]; + for (pt, expected) in cases { + let t = DownloadPackage::new(pt, "feed", "pkg", "1.0.0", "/out").into_step(); + assert_eq!(t.inputs.get("packageType").map(String::as_str), Some(expected)); + } + } +} diff --git a/src/compile/ir/tasks/mod.rs b/src/compile/ir/tasks/mod.rs index a19fdec6..71404c16 100644 --- a/src/compile/ir/tasks/mod.rs +++ b/src/compile/ir/tasks/mod.rs @@ -26,6 +26,7 @@ pub mod delete_files; pub mod docker; pub mod docker_installer; pub mod dotnet_core_cli; +pub mod download_package; pub mod download_pipeline_artifact; pub mod extract_files; pub mod npm;