Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/compile/agentic_pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/compile/ir/tasks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
172 changes: 172 additions & 0 deletions src/compile/ir/tasks/nuget_authenticate.rs
Original file line number Diff line number Diff line change
@@ -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:
/// <https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/nuget-authenticate-v1>
#[derive(Debug, Clone)]
pub struct NuGetAuthenticate {
nuget_service_connections: Option<String>,
force_reinstall_credential_provider: Option<bool>,
workload_identity_service_connection: Option<String>,
feed_url: Option<String>,
display_name: Option<String>,
}

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<String>) -> 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<String>) -> 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<String>) -> Self {
self.feed_url = Some(value.into());
self
}

/// Override the default `displayName` (`"NuGet Authenticate"`).
pub fn with_display_name(mut self, value: impl Into<String>) -> 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");
}
}
8 changes: 4 additions & 4 deletions src/runtimes/dotnet/extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down