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
17 changes: 17 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# CLAUDE.md

## Changelog

- Every functional change must be documented in [changelog.md](changelog.md) **before committing**.
- Each functional change gets **one line at most** in the changelog (one bullet, single line), ending with the issue/PR number, e.g. `#404`.

## Tests (Pester)

- Tests target **Pester 6 and newer**. Use Pester 5/6 syntax only.
- Do **not** use `Assert-MockCalled` or `Assert-VerifiableMock` (removed in Pester 6). Use `Should -Invoke <command> -Times N -Exactly` instead; add `-Scope Describe` when the mocked command was called in `BeforeAll`.
- Reason: calling `Assert-MockCalled` makes PowerShell autoload the old built-in Pester 3.4.0 (`C:\Program Files\WindowsPowerShell\Modules`), which takes over `Mock`/`Should` and breaks the whole test run.
- Run tests in a clean process with Pester 6 loaded first, e.g.:

```powershell
pwsh -NoProfile -Command "Import-Module Pester -MinimumVersion 6.0; Import-Module ./azure.datafactory.tools.psd1; Invoke-Pester -Path ./test/Stop-Trigger.Tests.ps1 -Output Detailed"
```
2 changes: 1 addition & 1 deletion azure.datafactory.tools.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
RootModule = 'azure.datafactory.tools.psm1'

# Version number of this module.
ModuleVersion = '1.18.0'
ModuleVersion = '1.19.0'

# Supported PSEditions
# CompatiblePSEditions = @()
Expand Down
4 changes: 4 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/).

## [1.19.0] - 2026-10-16
### Added
* Stopping a trigger now retries up to 5 times (and waits while the trigger is provisioning), like starting a trigger #404

## [1.18.0] - 2026-07-01
### Added
* `Publish-AdfV2FromJson` now supports terraform-like planning via `-DryRun`/`-Plan`, including structured `DryRunPlan` output (Create/Update/Delete/Unchanged) #360
Expand Down
60 changes: 53 additions & 7 deletions private/Stop-Trigger.ps1
Original file line number Diff line number Diff line change
@@ -1,17 +1,63 @@
function Stop-Trigger {
[CmdletBinding()]
[OutputType([bool])]
param (
[parameter(Mandatory = $true)] [string] $ResourceGroupName,
[parameter(Mandatory = $true)] [string] $DataFactoryName,
[parameter(Mandatory = $true)] [string] $Name
)
Write-Debug "BEGIN: Stop-Trigger()"

Write-host "- Disabling trigger: $Name"

Stop-AzDataFactoryV2Trigger `
-ResourceGroupName $ResourceGroupName `
-DataFactoryName $DataFactoryName `
-Name $Name `
-Force | Out-Null
Write-Host "- Disabling trigger: $Name"
$attempts = 5;
$i = 0
$stopped = $false
while ($i -lt $attempts)
{
$i++
try {
Stop-AzDataFactoryV2Trigger `
-ResourceGroupName $ResourceGroupName `
-DataFactoryName $DataFactoryName `
-Name $Name `
-Force | Out-Null
# If successful, break out of the loop
$stopped = $true
break
}
catch {
$errMsg = $_.Exception.Message
if ($i -lt $attempts)
{
if ($errMsg -like "*cannot be updated during provisioning*") {
Write-Verbose "Attempt #${i}: Trigger '$Name' is still provisioning. Waiting for provisioning to complete..."
$provisioningTimeout = 300
$pollInterval = 10
$elapsed = 0
while ($elapsed -lt $provisioningTimeout) {
Start-Sleep -Seconds $pollInterval
$elapsed += $pollInterval
$trigger = Get-AzDataFactoryV2Trigger `
-ResourceGroupName $ResourceGroupName `
-DataFactoryName $DataFactoryName `
-Name $Name
$provState = $trigger.Properties.ProvisioningState
Write-Verbose "Trigger provisioning state: $provState ($elapsed sec elapsed)"
if ($provState -ne 'Provisioning') { break }
}
} else {
Write-Verbose "Attempt #$i of stopping trigger failed. Retry in 2 seconds."
Start-Sleep -Seconds 2
}
}
else
{
Write-Host "Failed stopping trigger after $attempts attempts."
Write-Warning -Message $errMsg
}
}
}

Write-Debug "END: Stop-Trigger()"
return $stopped
}
23 changes: 16 additions & 7 deletions private/Stop-Triggers.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,24 @@ function Stop-Triggers {
if ($toBeStopped.Count -gt 0)
{
Write-Host "Stopping deployed triggers:"
$toBeStopped | ForEach-Object {
Stop-Trigger `
[System.Collections.ArrayList] $failedToStop = @{}
$toBeStopped | ForEach-Object {
$TrName = $_
$isStopped = Stop-Trigger `
-ResourceGroupName $adf.ResourceGroupName `
-DataFactoryName $adf.Name `
-Name $_ `
| Out-Null

$TrName = $_
$adf.TargetTriggerNames | Where-Object { $_.Name -eq $TrName } | ForEach-Object { $_.RuntimeState = "Stopped" }
-Name $TrName

if ($isStopped) {
$adf.TargetTriggerNames | Where-Object { $_.Name -eq $TrName } | ForEach-Object { $_.RuntimeState = "Stopped" }
} else {
$failedToStop.Add($TrName) | Out-Null
}
}
# Keep only triggers that have actually been stopped
$failedToStop | ForEach-Object { $toBeStopped.Remove($_) }
if ($failedToStop.Count -gt 0) {
Write-Warning "The number of triggers that failed to stop: $($failedToStop.Count) ($($failedToStop -join ', '))"
}
Write-Host "Complete stopping deployed triggers."
}
Expand Down
103 changes: 103 additions & 0 deletions test/Stop-Trigger.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
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

Describe 'Command ' -Tag 'Unit' {
It 'Stop-Trigger Should exist' {
{ Get-Command -Name Stop-Trigger -ErrorAction Stop } | Should -Not -Throw
}
}

Describe 'Stop-Trigger' -Tag 'Unit' {

BeforeEach {
Mock Start-Sleep {}
}

It 'Should return true when stopped at first attempt' {
Mock Stop-AzDataFactoryV2Trigger { }
$r = Stop-Trigger -ResourceGroupName 'rg' -DataFactoryName 'adf' -Name 'tr1'
$r | Should -BeTrue
Should -Invoke Stop-AzDataFactoryV2Trigger -Times 1 -Exactly
}

It 'Should retry after the first failure' {
$script:stopAttempt = 0
Mock Stop-AzDataFactoryV2Trigger { $script:stopAttempt++; if ($script:stopAttempt -eq 1) { throw 'Conflict: concurrent operations' } }
$r = Stop-Trigger -ResourceGroupName 'rg' -DataFactoryName 'adf' -Name 'tr1'
$r | Should -BeTrue
Should -Invoke Stop-AzDataFactoryV2Trigger -Times 2 -Exactly
}

It 'Should retry max 5 times when failure, warn and return false' {
Mock Stop-AzDataFactoryV2Trigger { throw 'BadRequest' }
Mock Write-Warning {}
$r = Stop-Trigger -ResourceGroupName 'rg' -DataFactoryName 'adf' -Name 'tr1'
$r | Should -BeFalse
Should -Invoke Stop-AzDataFactoryV2Trigger -Times 5 -Exactly
Should -Invoke Write-Warning -Times 1 -Exactly
}

It 'Should wait for provisioning and retry when trigger cannot be updated during provisioning' {
$script:stopAttempt = 0
Mock Stop-AzDataFactoryV2Trigger {
$script:stopAttempt++
if ($script:stopAttempt -eq 1) { throw 'BadRequest: Resource cannot be updated during provisioning' }
}
$script:getAttempt = 0
Mock Get-AzDataFactoryV2Trigger {
$script:getAttempt++
$state = if ($script:getAttempt -eq 1) { 'Provisioning' } else { 'Succeeded' }
return [PSCustomObject]@{ Properties = [PSCustomObject]@{ ProvisioningState = $state } }
}

$r = Stop-Trigger -ResourceGroupName 'rg' -DataFactoryName 'adf' -Name 'tr1'

$r | Should -BeTrue
Should -Invoke Stop-AzDataFactoryV2Trigger -Times 2 -Exactly
Should -Invoke Get-AzDataFactoryV2Trigger -Times 2 -Exactly
}
}

Describe 'Stop-Triggers when a trigger fails to stop' -Tag 'Unit' {

BeforeAll {
Mock Get-SortedTriggers {
@(
[PSCustomObject]@{ Name = 'tr1'; RuntimeState = 'Started' }
[PSCustomObject]@{ Name = 'tr2'; RuntimeState = 'Started' }
)
}
Mock Stop-Trigger { param ($ResourceGroupName, $DataFactoryName, $Name) return ($Name -ne 'tr2') }
Mock Write-Warning {}

$script:adf = [Adf]::new()
$script:adf.Name = 'adf'
$script:adf.ResourceGroupName = 'rg'
$script:adf.PublishOptions = New-AdfPublishOption
Stop-Triggers -adf $script:adf
}

It 'Should try to stop all active triggers' {
Should -Invoke Stop-Trigger -Times 2 -Exactly -Scope Describe
}
It 'Should register only successfully stopped triggers' {
$script:adf.StoppedTriggerNames | Should -Be @('tr1')
}
It 'Should keep the RuntimeState of trigger which failed to stop' {
($script:adf.TargetTriggerNames | Where-Object Name -eq 'tr1').RuntimeState | Should -Be 'Stopped'
($script:adf.TargetTriggerNames | Where-Object Name -eq 'tr2').RuntimeState | Should -Be 'Started'
}
It 'Should warn about triggers which failed to stop' {
Should -Invoke Write-Warning -Times 1 -Exactly -Scope Describe -ParameterFilter { $Message -like '*tr2*' }
}
}
}
1 change: 1 addition & 0 deletions test/Triggers.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ InModuleScope azure.datafactory.tools {
$o = $script:TargetAdf.GetObjectByFullName("*triggers.$Name")
$o.RuntimeState = 'Stopped'
$o.Body.properties.RuntimeState = $o.RuntimeState
return $true
}
Mock Start-Trigger {
param ($ResourceGroupName, $DataFactoryName, $Name)
Expand Down
Loading