From d2ac9edef805bec9a3863171141c8e03788119b3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 21 Jun 2026 09:39:46 +0000 Subject: [PATCH] feat(ir): add typed builder for NuGetAuthenticate@1 Add `nuget_authenticate::NuGetAuthenticate` builder struct to `src/compile/ir/tasks/` and migrate both call sites that were hand-crafting raw `TaskStep::new("NuGetAuthenticate@1", ...)` to use the typed builder instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/compile/agentic_pipeline.rs | 7 +- src/compile/ir/tasks/mod.rs | 1 + src/compile/ir/tasks/nuget_authenticate.rs | 172 +++++++++++++++++++++ src/runtimes/dotnet/extension.rs | 8 +- 4 files changed, 181 insertions(+), 7 deletions(-) create mode 100644 src/compile/ir/tasks/nuget_authenticate.rs diff --git a/src/compile/agentic_pipeline.rs b/src/compile/agentic_pipeline.rs index 4353fb1b..05ad73fd 100644 --- a/src/compile/agentic_pipeline.rs +++ b/src/compile/agentic_pipeline.rs @@ -66,6 +66,7 @@ use super::ir::step::{ }; use super::ir::tasks::docker_installer::DockerInstaller; use super::ir::tasks::download_package::DownloadPackage; +use super::ir::tasks::nuget_authenticate::NuGetAuthenticate; use super::ir::{ CiTrigger, Parameter, ParameterDefault, ParameterKind, PipelineResource, PipelineVar, PrTrigger, RepositoryResource, Resources, Schedule, Triggers, @@ -1207,11 +1208,11 @@ fn acr_login_step(registry_base: &str, connection: &str) -> TaskStep { /// passed via `nuGetServiceConnections` (cross-org/external feeds); otherwise /// the task authenticates the build identity with `$(System.AccessToken)`. pub(crate) fn nuget_authenticate_step(connection: Option<&str>) -> TaskStep { - let mut step = TaskStep::new("NuGetAuthenticate@1", "Authenticate to internal feed"); + let mut auth = NuGetAuthenticate::new().with_display_name("Authenticate to internal feed"); if let Some(conn) = connection { - step = step.with_input("nuGetServiceConnections", conn); + auth = auth.nuget_service_connections(conn); } - step + auth.into_step() } /// `DownloadPackage@1` step pulling a single NuGet package by name+version diff --git a/src/compile/ir/tasks/mod.rs b/src/compile/ir/tasks/mod.rs index 46d08807..e337b6f0 100644 --- a/src/compile/ir/tasks/mod.rs +++ b/src/compile/ir/tasks/mod.rs @@ -32,6 +32,7 @@ pub mod download_pipeline_artifact; pub mod extract_files; pub mod java_tool_installer; pub mod npm; +pub mod nuget_authenticate; pub mod nuget_command; pub mod powershell; pub mod python_script; diff --git a/src/compile/ir/tasks/nuget_authenticate.rs b/src/compile/ir/tasks/nuget_authenticate.rs new file mode 100644 index 00000000..94b1782c --- /dev/null +++ b/src/compile/ir/tasks/nuget_authenticate.rs @@ -0,0 +1,172 @@ +//! Typed builder for `NuGetAuthenticate@1`. + +use super::common::bool_input; +use crate::compile::ir::step::TaskStep; + +/// Builder for a [`TaskStep`] invoking `NuGetAuthenticate@1`. +/// +/// Configures NuGet tooling (nuget.exe, dotnet, MSBuild) to authenticate with +/// Azure Artifacts feeds and other NuGet repositories. All inputs are optional; +/// calling `into_step()` with no inputs set authenticates the ADO build service +/// identity against any feeds discovered in `nuget.config` files in the +/// workspace. +/// +/// ADO task reference: +/// +#[derive(Debug, Clone)] +pub struct NuGetAuthenticate { + nuget_service_connections: Option, + force_reinstall_credential_provider: Option, + workload_identity_service_connection: Option, + feed_url: Option, + display_name: Option, +} + +impl NuGetAuthenticate { + /// Create a new builder; all inputs are optional. + pub fn new() -> Self { + Self { + nuget_service_connections: None, + force_reinstall_credential_provider: None, + workload_identity_service_connection: None, + feed_url: None, + display_name: None, + } + } + + /// `nuGetServiceConnections` — comma-separated service connections for + /// feeds outside this organization. + pub fn nuget_service_connections(mut self, value: impl Into) -> Self { + self.nuget_service_connections = Some(value.into()); + self + } + + /// `forceReinstallCredentialProvider` — reinstall the credential provider + /// even if already installed. Default: `false`. + pub fn force_reinstall_credential_provider(mut self, value: bool) -> Self { + self.force_reinstall_credential_provider = Some(value); + self + } + + /// `workloadIdentityServiceConnection` (alias `azureDevOpsServiceConnection`) — + /// service connection for authenticating against this Azure DevOps + /// organization's feeds via workload-identity federation. + pub fn workload_identity_service_connection(mut self, value: impl Into) -> Self { + self.workload_identity_service_connection = Some(value.into()); + self + } + + /// `feedUrl` — Azure Artifacts feed URL to authenticate against. + /// Used together with [`workload_identity_service_connection`]. + pub fn feed_url(mut self, value: impl Into) -> Self { + self.feed_url = Some(value.into()); + self + } + + /// Override the default `displayName` (`"NuGet Authenticate"`). + 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( + "NuGetAuthenticate@1", + self.display_name + .unwrap_or_else(|| "NuGet Authenticate".into()), + ); + if let Some(v) = self.nuget_service_connections { + t = t.with_input("nuGetServiceConnections", v); + } + if let Some(v) = self.force_reinstall_credential_provider { + t = t.with_input("forceReinstallCredentialProvider", bool_input(v)); + } + if let Some(v) = self.workload_identity_service_connection { + t = t.with_input("workloadIdentityServiceConnection", v); + } + if let Some(v) = self.feed_url { + t = t.with_input("feedUrl", v); + } + t + } +} + +impl Default for NuGetAuthenticate { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sets_task_identifier() { + let t = NuGetAuthenticate::new().into_step(); + assert_eq!(t.task, "NuGetAuthenticate@1"); + } + + #[test] + fn no_inputs_by_default() { + let t = NuGetAuthenticate::new().into_step(); + assert!(t.inputs.is_empty(), "expected no inputs when none are set"); + } + + #[test] + fn default_display_name() { + let t = NuGetAuthenticate::new().into_step(); + assert_eq!(t.display_name, "NuGet Authenticate"); + } + + #[test] + fn nuget_service_connections() { + let t = NuGetAuthenticate::new() + .nuget_service_connections("my-service-connection") + .into_step(); + assert_eq!( + t.inputs.get("nuGetServiceConnections").map(String::as_str), + Some("my-service-connection") + ); + } + + #[test] + fn force_reinstall_credential_provider() { + let t = NuGetAuthenticate::new() + .force_reinstall_credential_provider(true) + .into_step(); + assert_eq!( + t.inputs + .get("forceReinstallCredentialProvider") + .map(String::as_str), + Some("true") + ); + } + + #[test] + fn workload_identity_and_feed_url() { + let t = NuGetAuthenticate::new() + .workload_identity_service_connection("my-wif-conn") + .feed_url("https://pkgs.dev.azure.com/myorg/_packaging/myfeed/nuget/v3/index.json") + .into_step(); + assert_eq!( + t.inputs + .get("workloadIdentityServiceConnection") + .map(String::as_str), + Some("my-wif-conn") + ); + assert_eq!( + t.inputs.get("feedUrl").map(String::as_str), + Some("https://pkgs.dev.azure.com/myorg/_packaging/myfeed/nuget/v3/index.json") + ); + } + + #[test] + fn with_display_name_override() { + let t = NuGetAuthenticate::new() + .with_display_name("Authenticate to internal feed") + .into_step(); + assert_eq!(t.display_name, "Authenticate to internal feed"); + } +} diff --git a/src/runtimes/dotnet/extension.rs b/src/runtimes/dotnet/extension.rs index 3cd09876..c0b29fb1 100644 --- a/src/runtimes/dotnet/extension.rs +++ b/src/runtimes/dotnet/extension.rs @@ -3,6 +3,7 @@ use super::{DOTNET_BASH_COMMANDS, DotnetRuntimeConfig, GLOBAL_JSON_SENTINEL}; use crate::compile::extensions::{CompileContext, CompilerExtension, Declarations, ExtensionPhase}; use crate::compile::ir::step::{BashStep, Step, TaskStep}; +use crate::compile::ir::tasks::nuget_authenticate::NuGetAuthenticate; use crate::validate; use anyhow::Result; @@ -164,10 +165,9 @@ fn dotnet_install_task_step(config: &DotnetRuntimeConfig) -> TaskStep { /// Build the typed [`TaskStep`] for NuGet authentication. fn nuget_authenticate_task_step() -> TaskStep { - TaskStep::new( - "NuGetAuthenticate@1", - "Authenticate NuGet (build service identity)", - ) + NuGetAuthenticate::new() + .with_display_name("Authenticate NuGet (build service identity)") + .into_step() } /// Build the typed [`BashStep`] that ensures `nuget.config`. Same