diff --git a/src/compile/ir/tasks/mod.rs b/src/compile/ir/tasks/mod.rs index a19fdec6..7fb00daf 100644 --- a/src/compile/ir/tasks/mod.rs +++ b/src/compile/ir/tasks/mod.rs @@ -31,5 +31,6 @@ pub mod extract_files; pub mod npm; pub mod nuget_command; pub mod powershell; +pub mod publish_build_artifacts; pub mod publish_pipeline_artifact; pub mod publish_test_results; diff --git a/src/compile/ir/tasks/publish_build_artifacts.rs b/src/compile/ir/tasks/publish_build_artifacts.rs new file mode 100644 index 00000000..becd3a43 --- /dev/null +++ b/src/compile/ir/tasks/publish_build_artifacts.rs @@ -0,0 +1,251 @@ +//! Typed builder for `PublishBuildArtifacts@1`. +//! +//! This is a location-dispatch task: the `publishLocation` input (`Container` +//! or `FilePath`) determines which optional inputs are meaningful. Because +//! file-share-only inputs (`TargetPath`, `Parallel`, `ParallelCount`, +//! `FileCopyOptions`) live inside [`FilePathLocation`], applying them when +//! `publishLocation` is `Container` is unrepresentable. +//! +//! ADO task reference: +//! + +use super::common::{push_bool, push_opt}; +use crate::compile::ir::step::TaskStep; + +/// `PublishBuildArtifacts@1` `publishLocation` selector, carrying +/// per-location optional inputs. +#[derive(Debug, Clone)] +pub enum PublishLocation { + /// Publish to Azure Pipelines artifact storage (default). + Container, + /// Publish to a file share; carries file-share-specific optionals. + FilePath(FilePathLocation), +} + +/// Per-location optionals for `publishLocation: FilePath`. +#[derive(Debug, Clone)] +pub struct FilePathLocation { + /// `TargetPath` — UNC file share path. Required for the `FilePath` location. + target_path: String, + /// `Parallel` — copy files in parallel using multiple threads. + parallel: Option, + /// `ParallelCount` — number of threads for parallel copy (1–128, default 8). + parallel_count: Option, + /// `FileCopyOptions` — additional robocopy arguments. + file_copy_options: Option, +} + +impl FilePathLocation { + /// Required: `TargetPath` — UNC path to the file share. + pub fn new(target_path: impl Into) -> Self { + Self { + target_path: target_path.into(), + parallel: None, + parallel_count: None, + file_copy_options: None, + } + } + + /// `Parallel` — enable parallel copy. + pub fn parallel(mut self, value: bool) -> Self { + self.parallel = Some(value); + self + } + + /// `ParallelCount` — thread count for parallel copy (`"1"` – `"128"`, default `"8"`). + pub fn parallel_count(mut self, value: impl Into) -> Self { + self.parallel_count = Some(value.into()); + self + } + + /// `FileCopyOptions` — additional robocopy-style options. + pub fn file_copy_options(mut self, value: impl Into) -> Self { + self.file_copy_options = Some(value.into()); + self + } +} + +/// Builder for a [`TaskStep`] invoking `PublishBuildArtifacts@1`. +/// +/// Use [`PublishBuildArtifacts::container`] to publish to Azure Pipelines +/// artifact storage and [`PublishBuildArtifacts::file_path`] to publish to a +/// file share. Shared optionals (`max_artifact_size`, `store_as_tar`) are +/// available on both. +/// +/// ADO task reference: +/// +#[derive(Debug, Clone)] +pub struct PublishBuildArtifacts { + path_to_publish: String, + artifact_name: String, + location: PublishLocation, + /// `MaxArtifactSize` — artifact size limit in bytes; `"0"` disables the limit. + max_artifact_size: Option, + /// `StoreAsTar` — tar the artifact directory before uploading. + store_as_tar: Option, + display_name: Option, +} + +impl PublishBuildArtifacts { + /// Construct from an explicit [`PublishLocation`]. + pub fn new( + path_to_publish: impl Into, + artifact_name: impl Into, + location: PublishLocation, + ) -> Self { + Self { + path_to_publish: path_to_publish.into(), + artifact_name: artifact_name.into(), + location, + max_artifact_size: None, + store_as_tar: None, + display_name: None, + } + } + + /// `publishLocation: Container` — publish to Azure Pipelines artifact storage. + pub fn container( + path_to_publish: impl Into, + artifact_name: impl Into, + ) -> Self { + Self::new(path_to_publish, artifact_name, PublishLocation::Container) + } + + /// `publishLocation: FilePath` — publish to a file share. + pub fn file_path( + path_to_publish: impl Into, + artifact_name: impl Into, + location: FilePathLocation, + ) -> Self { + Self::new(path_to_publish, artifact_name, PublishLocation::FilePath(location)) + } + + /// `MaxArtifactSize` — maximum artifact size in bytes; `"0"` means no limit. + pub fn max_artifact_size(mut self, value: impl Into) -> Self { + self.max_artifact_size = Some(value.into()); + self + } + + /// `StoreAsTar` — tar the artifact directory before uploading. + pub fn store_as_tar(mut self, value: bool) -> Self { + self.store_as_tar = Some(value); + self + } + + /// Override the default `displayName` (`"Publish Artifact"`). + 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( + "PublishBuildArtifacts@1", + self.display_name.unwrap_or_else(|| "Publish Artifact".into()), + ) + .with_input("PathtoPublish", self.path_to_publish) + .with_input("ArtifactName", self.artifact_name); + + match self.location { + PublishLocation::Container => { + t = t.with_input("publishLocation", "Container"); + } + PublishLocation::FilePath(loc) => { + t = t + .with_input("publishLocation", "FilePath") + .with_input("TargetPath", loc.target_path); + push_bool(&mut t, "Parallel", loc.parallel); + push_opt(&mut t, "ParallelCount", loc.parallel_count); + push_opt(&mut t, "FileCopyOptions", loc.file_copy_options); + } + } + + push_opt(&mut t, "MaxArtifactSize", self.max_artifact_size); + push_bool(&mut t, "StoreAsTar", self.store_as_tar); + t + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn container_defaults() { + let t = PublishBuildArtifacts::container( + "$(Build.ArtifactStagingDirectory)", + "drop", + ) + .into_step(); + assert_eq!(t.task, "PublishBuildArtifacts@1"); + assert_eq!(t.display_name, "Publish Artifact"); + assert_eq!( + t.inputs.get("PathtoPublish").map(String::as_str), + Some("$(Build.ArtifactStagingDirectory)") + ); + assert_eq!(t.inputs.get("ArtifactName").map(String::as_str), Some("drop")); + assert_eq!(t.inputs.get("publishLocation").map(String::as_str), Some("Container")); + assert!(t.inputs.get("TargetPath").is_none()); + } + + #[test] + fn container_with_shared_optionals() { + let t = PublishBuildArtifacts::container("$(Build.ArtifactStagingDirectory)", "binaries") + .max_artifact_size("1073741824") + .store_as_tar(true) + .with_display_name("Publish build binaries") + .into_step(); + assert_eq!(t.display_name, "Publish build binaries"); + assert_eq!( + t.inputs.get("MaxArtifactSize").map(String::as_str), + Some("1073741824") + ); + assert_eq!(t.inputs.get("StoreAsTar").map(String::as_str), Some("true")); + } + + #[test] + fn file_path_with_location_optionals() { + let t = PublishBuildArtifacts::file_path( + "$(Build.ArtifactStagingDirectory)", + "drop", + FilePathLocation::new(r"\\myshare\builds") + .parallel(true) + .parallel_count("16") + .file_copy_options("/MIR"), + ) + .into_step(); + assert_eq!(t.inputs.get("publishLocation").map(String::as_str), Some("FilePath")); + assert_eq!( + t.inputs.get("TargetPath").map(String::as_str), + Some(r"\\myshare\builds") + ); + assert_eq!(t.inputs.get("Parallel").map(String::as_str), Some("true")); + assert_eq!(t.inputs.get("ParallelCount").map(String::as_str), Some("16")); + assert_eq!(t.inputs.get("FileCopyOptions").map(String::as_str), Some("/MIR")); + } + + #[test] + fn file_path_omits_absent_optionals() { + let t = PublishBuildArtifacts::file_path( + "$(Build.ArtifactStagingDirectory)", + "drop", + FilePathLocation::new(r"\\myshare\builds"), + ) + .into_step(); + assert_eq!(t.inputs.get("publishLocation").map(String::as_str), Some("FilePath")); + assert!(t.inputs.get("Parallel").is_none()); + assert!(t.inputs.get("ParallelCount").is_none()); + assert!(t.inputs.get("FileCopyOptions").is_none()); + assert!(t.inputs.get("MaxArtifactSize").is_none()); + assert!(t.inputs.get("StoreAsTar").is_none()); + } + + #[test] + fn display_name_override() { + let t = PublishBuildArtifacts::container("out/", "my-artifact") + .with_display_name("Stage and publish") + .into_step(); + assert_eq!(t.display_name, "Stage and publish"); + } +}