From 4f6d8a736fdbc8e508608154452e0d86f6849134 Mon Sep 17 00:00:00 2001 From: Kamil Nowinski Date: Wed, 27 May 2026 03:05:21 +0100 Subject: [PATCH] Initial fix #472 --- changelog.md | 4 + private/Deploy-AdfObject.ps1 | 2 +- private/Deploy-AdfObjectOnly.ps1 | 4 +- private/Update-PropertiesFromFile.ps1 | 4 +- public/Publish-AdfV2FromJson.ps1 | 8 +- ...lish-AdfV2FromJson-ErrorHandling.Tests.ps1 | 141 ++++++++++++++++++ 6 files changed, 154 insertions(+), 9 deletions(-) create mode 100644 test/Publish-AdfV2FromJson-ErrorHandling.Tests.ps1 diff --git a/changelog.md b/changelog.md index 91a3b61..13cbd5b 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## Unreleased +### Fixed +* `Publish-AdfV2FromJson` no longer silently continues after errors: config path errors (ADFT0010) and deployment failures (e.g. Azure Policy blocks) now propagate as terminating errors to the caller #472 + ## [1.15.0] - 2026-05-26 ### Added * Credential type of ADF objects is now supported for deployment via REST API diff --git a/private/Deploy-AdfObject.ps1 b/private/Deploy-AdfObject.ps1 index 9b67c54..d6b0ce4 100644 --- a/private/Deploy-AdfObject.ps1 +++ b/private/Deploy-AdfObject.ps1 @@ -31,7 +31,7 @@ function Deploy-AdfObject { if ($adf.PublishOptions.IgnoreLackOfReferencedObject -eq $true) { Write-Warning "ADFT0006: Referenced object [$type].[$name] was not found. No error raised as user wanted to carry on." } else { - Write-Error "ADFT0005: Referenced object [$type].[$name] was not found." + throw "ADFT0005: Referenced object [$type].[$name] was not found." } } elseif ($type -notin [AdfObject]::IgnoreTypes) { Deploy-AdfObject -obj $depobj diff --git a/private/Deploy-AdfObjectOnly.ps1 b/private/Deploy-AdfObjectOnly.ps1 index 1398035..0d9af2e 100644 --- a/private/Deploy-AdfObjectOnly.ps1 +++ b/private/Deploy-AdfObjectOnly.ps1 @@ -73,7 +73,7 @@ function Deploy-AdfObjectOnly { -Force | Out-Null } else { - Write-Error "ADFT0012: Deployment for this kind of Integration Runtime is not supported yet." + throw "ADFT0012: Deployment for this kind of Integration Runtime is not supported yet." } } 'linkedService' @@ -155,7 +155,7 @@ function Deploy-AdfObjectOnly { } default { - Write-Error "ADFT0013: Type $($obj.Type) is not supported." + throw "ADFT0013: Type $($obj.Type) is not supported." } } diff --git a/private/Update-PropertiesFromFile.ps1 b/private/Update-PropertiesFromFile.ps1 index 542675a..f62f900 100644 --- a/private/Update-PropertiesFromFile.ps1 +++ b/private/Update-PropertiesFromFile.ps1 @@ -72,7 +72,7 @@ function Update-PropertiesFromFile { if ($option.FailsWhenConfigItemNotFound -eq $false) { Write-Warning "Could not find object: $type.$name, skipping..." } else { - Write-Error -Message "ADFT0007: Could not find object: $type.$name" + throw "ADFT0007: Could not find object: $type.$name" } } else { Write-Verbose "- Performing: $action for object(path): $type.$name(properties.$path)" @@ -131,7 +131,7 @@ function Update-PropertiesForObject { Write-Warning "Wrong path defined in config for object(path): $type.$name(properties.$path), skipping..." } else { $exc = ([System.Data.DataException]::new("ADFT0010: Wrong path defined in config for object(path): $type.$name(properties.$path)")) - Write-Error -Exception $exc + throw $exc } } diff --git a/public/Publish-AdfV2FromJson.ps1 b/public/Publish-AdfV2FromJson.ps1 index 0f079e0..3d687e7 100644 --- a/public/Publish-AdfV2FromJson.ps1 +++ b/public/Publish-AdfV2FromJson.ps1 @@ -272,16 +272,16 @@ function Publish-AdfV2FromJson { $adf.Factories[0].ToBeDeployed = $false } } - $adf.AllObjects() | ForEach-Object { - Deploy-AdfObject -obj $_ + foreach ($obj in $adf.AllObjects()) { + Deploy-AdfObject -obj $obj } Write-Host "==================================================================================="; Write-Host "STEP: Deleting objects not in source ..." if ($opt.DeleteNotInSource -eq $true) { $adfIns = Get-AdfFromService -FactoryName "$DataFactoryName" -ResourceGroupName "$ResourceGroupName" - $adfIns.AllObjects() | ForEach-Object { - Remove-AdfObjectIfNotInSource -adfSource $adf -adfTargetObj $_ -adfInstance $adfIns + foreach ($targetObj in $adfIns.AllObjects()) { + Remove-AdfObjectIfNotInSource -adfSource $adf -adfTargetObj $targetObj -adfInstance $adfIns } Write-Host "Deleted $($adf.DeletedObjectNames.Count) objects from ADF service." } else { diff --git a/test/Publish-AdfV2FromJson-ErrorHandling.Tests.ps1 b/test/Publish-AdfV2FromJson-ErrorHandling.Tests.ps1 new file mode 100644 index 0000000..9a4a3a2 --- /dev/null +++ b/test/Publish-AdfV2FromJson-ErrorHandling.Tests.ps1 @@ -0,0 +1,141 @@ +BeforeDiscovery { + $ModuleRootPath = $PSScriptRoot | Split-Path -Parent + $moduleManifestName = 'azure.datafactory.tools.psd1' + $moduleManifestPath = Join-Path -Path $ModuleRootPath -ChildPath $moduleManifestName + Import-Module -Name $moduleManifestPath -Force -Verbose:$false +} + +InModuleScope azure.datafactory.tools { + $testHelperPath = $PSScriptRoot | Join-Path -ChildPath 'TestHelper' + Import-Module -Name $testHelperPath -Force + + $script:SrcFolder = "$PSScriptRoot\BigFactorySample2" + $script:TmpFolder = (New-TemporaryDirectory).FullName + $script:RootFolder = Join-Path -Path $script:TmpFolder -ChildPath (Split-Path -Path $script:SrcFolder -Leaf) + Copy-Item -Path "$SrcFolder" -Destination "$TmpFolder" -Filter "*.*" -Recurse:$true -Force + + AfterAll { + Remove-Item -Path $script:TmpFolder -Recurse -Force -ErrorAction SilentlyContinue + } + + # ------------------------------------------------------------------------- + # Issue #472 – Scenario 1: wrong property path in config (ADFT0010) + # + # config-c004-wrongpath.csv contains "typeProperties:baseUrl" (colon + # separator instead of dot), which causes Write-Error "ADFT0010" inside + # Update-PropertiesForObject. The error is non-terminating, so the + # ForEach-Object loop in Update-PropertiesFromFile swallows it and + # Publish-AdfV2FromJson continues to print "deployed successfully". + # + # Expected fix: the error must propagate as a terminating error so the + # caller observes a failure regardless of $ErrorActionPreference scope. + # ------------------------------------------------------------------------- + Describe 'Publish-AdfV2FromJson - Error propagation for wrong config path (Issue #472)' -Tag 'Unit' { + + Context 'When stage config has a wrong property path and FailsWhenPathNotFound is true (default)' { + + It 'Should throw when Publish-AdfV2FromJson is called with -DryRun' { + # BUG REPRODUCTION: currently does NOT throw (Write-Error is swallowed). + # After fix this test must pass. + $o = New-AdfPublishOption + # FailsWhenPathNotFound defaults to $true + { + Publish-AdfV2FromJson ` + -RootFolder $script:RootFolder ` + -ResourceGroupName 'rg-test' ` + -DataFactoryName 'adf-test' ` + -Stage 'c004-wrongpath' ` + -Option $o ` + -DryRun + } | Should -Throw + } + } + + Context 'When stage config has a wrong property path and FailsWhenPathNotFound is false' { + + It 'Should not throw (wrong path is only a warning)' { + $o = New-AdfPublishOption + $o.FailsWhenPathNotFound = $false + { + Publish-AdfV2FromJson ` + -RootFolder $script:RootFolder ` + -ResourceGroupName 'rg-test' ` + -DataFactoryName 'adf-test' ` + -Stage 'c004-wrongpath' ` + -Option $o ` + -DryRun + } | Should -Not -Throw + } + } + } + + # ------------------------------------------------------------------------- + # Issue #472 – Scenario 2: ADF object deployment fails at runtime + # + # When Deploy-AdfObjectOnly emits a non-terminating Write-Error (e.g. an + # Azure Policy block: "RequestDisallowedByPolicy"), the ForEach-Object + # pipeline in Publish-AdfV2FromJson absorbs the error and the function + # continues to report "deployed successfully". + # + # Expected fix: the deployment loop must propagate errors so the function + # terminates and the caller sees a failure. + # ------------------------------------------------------------------------- + Describe 'Publish-AdfV2FromJson - Error propagation for deployment failure (Issue #472)' -Tag 'Unit' { + + Context 'When an ADF object deployment produces a terminating error' { + + BeforeAll { + Mock Get-AzDataFactoryV2 { + [PSCustomObject]@{ Name = 'adf-test'; ResourceGroupName = 'rg-test' } + } + # Azure cmdlets (e.g. New-AzResource) throw terminating errors for + # serious failures like Azure Policy blocks. In PowerShell 5.1 the + # ForEach-Object pipeline was absorbing these terminating errors and + # allowing the loop to continue, so the function reported success. + # The fix replaces ForEach-Object with a foreach statement, which + # propagates terminating errors correctly in all PS versions. + Mock Deploy-AdfObjectOnly { + throw 'Simulated: RequestDisallowedByPolicy - resource was disallowed by policy' + } + } + + It 'Should throw when any object deployment fails' { + # BUG REPRODUCTION: previously did NOT throw because ForEach-Object + # absorbed the terminating error in PS 5.1. + # After fix (foreach loop) this test must pass. + $o = New-AdfPublishOption + $o.StopStartTriggers = $false + $o.DeleteNotInSource = $false + { + Publish-AdfV2FromJson ` + -RootFolder $script:RootFolder ` + -ResourceGroupName 'rg-test' ` + -DataFactoryName 'adf-test' ` + -Option $o + } | Should -Throw + } + + It 'Should throw even when the caller has not set $ErrorActionPreference to Stop' { + # BUG REPRODUCTION: the module scope isolates $ErrorActionPreference + # from the caller's script scope, so setting it externally has no effect. + # The fix must make the function fail independently of the caller's EAP. + $o = New-AdfPublishOption + $o.StopStartTriggers = $false + $o.DeleteNotInSource = $false + $threw = $false + # Deliberately NOT using Should -Throw so Pester does not alter EAP + try { + Publish-AdfV2FromJson ` + -RootFolder $script:RootFolder ` + -ResourceGroupName 'rg-test' ` + -DataFactoryName 'adf-test' ` + -Option $o + } + catch { + $threw = $true + } + $threw | Should -BeTrue -Because 'deployment failures must propagate to the caller' + } + } + } +}