From 8260642aa221c6be554c54bec3d585a078e123e1 Mon Sep 17 00:00:00 2001 From: Kamil Nowinski Date: Sat, 17 Oct 2026 00:05:14 +0100 Subject: [PATCH] Call npm.cmd on Windows in Export-AdfToArmTemplate #462 On Windows, 'npm' runs npm.ps1, which in some npm versions reads $MyInvocation.Statement under strict mode and fails on Windows PowerShell 5.1 (windows-2025 agents). npm.cmd skips npm.ps1. A failed npm command now stops the export instead of being ignored. Co-Authored-By: Claude Opus 5.5 --- changelog.md | 1 + private/Invoke-AdfNpm.ps1 | 21 +++++ public/Export-AdfToArmTemplate.ps1 | 7 +- test/Export-AdfToArmTemplate-Npm.Tests.ps1 | 94 ++++++++++++++++++++++ 4 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 private/Invoke-AdfNpm.ps1 create mode 100644 test/Export-AdfToArmTemplate-Npm.Tests.ps1 diff --git a/changelog.md b/changelog.md index a245759..d769218 100644 --- a/changelog.md +++ b/changelog.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed * A failed object deployment now stops `Publish-AdfV2FromJson` whatever `$ErrorActionPreference` is, instead of being skipped and the object marked as deployed #295 * Starting and stopping triggers now catches Az errors whatever `$ErrorActionPreference` is, so retries and warnings work; a failed start is a warning and the deployment carries on #508 +* `Export-AdfToArmTemplate` calls `npm.cmd` on Windows (avoids npm.ps1 failing on Windows PowerShell 5.1) and stops when an npm command fails #462 ## [1.19.0] - 2026-10-16 ### Added diff --git a/private/Invoke-AdfNpm.ps1 b/private/Invoke-AdfNpm.ps1 new file mode 100644 index 0000000..df94bfa --- /dev/null +++ b/private/Invoke-AdfNpm.ps1 @@ -0,0 +1,21 @@ +function Get-AdfNpmCommand { + # On Windows, 'npm' resolves to npm.ps1, which fails on Windows PowerShell 5.1 in some npm versions + # ($MyInvocation.Statement under strict mode) #462. npm.cmd does the same without PowerShell. + if ($env:OS -eq 'Windows_NT') { 'npm.cmd' } else { 'npm' } +} + +function Invoke-AdfNpm { + [CmdletBinding()] + param + ( + [parameter(Mandatory = $true)] + [String[]] $Arguments + ) + + $npm = Get-AdfNpmCommand + Write-Verbose "$npm $($Arguments -join ' ')" + & $npm @Arguments + if ($global:LASTEXITCODE -ne 0) { + throw "Command 'npm $($Arguments -join ' ')' failed with exit code $($global:LASTEXITCODE)." + } +} diff --git a/public/Export-AdfToArmTemplate.ps1 b/public/Export-AdfToArmTemplate.ps1 index 824d356..8bcc2af 100644 --- a/public/Export-AdfToArmTemplate.ps1 +++ b/public/Export-AdfToArmTemplate.ps1 @@ -30,19 +30,18 @@ function Export-AdfToArmTemplate { # Validate and export ARM Template using @microsoft/azure-data-factory-utilities module Write-Host "=== Check NPM Version..." - npm version + Invoke-AdfNpm -Arguments 'version' Write-Host "=== Check finished." Write-Host "=== Installing NPM azure-data-factory-utilities..." - npm i @microsoft/azure-data-factory-utilities + Invoke-AdfNpm -Arguments 'i', '@microsoft/azure-data-factory-utilities' Write-Host "=== Installation finished." $AdfName = Split-Path -Path $RootFolder -Leaf $adfAzurePath = "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroup/providers/Microsoft.DataFactory/factories/$AdfName" Write-Host "=== Validating & exporting ARM Template..." - Write-Verbose "npm run build export $RootFolder $adfAzurePath ""$OutputFolder""" - npm run build export $RootFolder $adfAzurePath "$OutputFolder" + Invoke-AdfNpm -Arguments 'run', 'build', 'export', $RootFolder, $adfAzurePath, $OutputFolder Write-Host "=== Export finished." } diff --git a/test/Export-AdfToArmTemplate-Npm.Tests.ps1 b/test/Export-AdfToArmTemplate-Npm.Tests.ps1 new file mode 100644 index 0000000..819db2b --- /dev/null +++ b/test/Export-AdfToArmTemplate-Npm.Tests.ps1 @@ -0,0 +1,94 @@ +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 { + + # Issue #462: on Windows, 'npm' runs npm.ps1, which fails on Windows PowerShell 5.1 + Describe 'Get-AdfNpmCommand' -Tag 'Unit' { + + BeforeAll { + $script:origOS = $env:OS + } + + AfterEach { + $env:OS = $script:origOS + } + + It 'Should return npm.cmd on Windows' { + $env:OS = 'Windows_NT' + Get-AdfNpmCommand | Should -Be 'npm.cmd' + } + + It 'Should return npm on other systems' { + $env:OS = $null + Get-AdfNpmCommand | Should -Be 'npm' + } + } + + Describe 'Invoke-AdfNpm' -Tag 'Unit' { + + BeforeAll { + function Invoke-FakeNpm { $global:LASTEXITCODE = $script:fakeExitCode; $script:fakeArgs = $args } + } + + BeforeEach { + $ErrorActionPreference = 'Continue' + $script:fakeExitCode = 0 + $script:fakeArgs = $null + Mock Get-AdfNpmCommand { 'Invoke-FakeNpm' } + } + + AfterEach { + $global:LASTEXITCODE = 0 + } + + It 'Should pass the arguments to npm' { + Invoke-AdfNpm -Arguments 'i', '@microsoft/azure-data-factory-utilities' + $script:fakeArgs | Should -Be @('i', '@microsoft/azure-data-factory-utilities') + } + + It 'Should throw when npm fails' { + $script:fakeExitCode = 1 + { Invoke-AdfNpm -Arguments 'i', 'pkg' } | Should -Throw "*'npm i pkg' failed with exit code 1*" + } + } + + Describe 'Export-AdfToArmTemplate calls npm' -Tag 'Unit' { + + BeforeAll { + $script:origLocation = Get-Location + } + + BeforeEach { + $ErrorActionPreference = 'Continue' + $script:RootFolder = Join-Path $TestDrive 'adf1' + New-Item -Path $script:RootFolder -ItemType Directory -Force | Out-Null + Mock Invoke-AdfNpm { } + } + + AfterEach { + Set-Location $script:origLocation + } + + It 'Should check the version, install the utilities and export' { + Export-AdfToArmTemplate -RootFolder $script:RootFolder -SubscriptionId 'sub1' -ResourceGroup 'rg1' -OutputFolder 'out' + Should -Invoke Invoke-AdfNpm -Times 3 -Exactly + Should -Invoke Invoke-AdfNpm -Times 1 -Exactly -ParameterFilter { ($Arguments -join ' ') -eq 'version' } + Should -Invoke Invoke-AdfNpm -Times 1 -Exactly -ParameterFilter { ($Arguments -join ' ') -eq 'i @microsoft/azure-data-factory-utilities' } + Should -Invoke Invoke-AdfNpm -Times 1 -Exactly -ParameterFilter { + ($Arguments -join ' ') -eq "run build export $($script:RootFolder) /subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.DataFactory/factories/adf1 out" + } + } + + It 'Should stop when npm install fails' { + Mock Invoke-AdfNpm { throw 'npm failed' } -ParameterFilter { $Arguments[0] -eq 'i' } + { Export-AdfToArmTemplate -RootFolder $script:RootFolder } | Should -Throw 'npm failed' + Should -Invoke Invoke-AdfNpm -Times 0 -Exactly -ParameterFilter { $Arguments[0] -eq 'run' } + } + } +}