Public/TestCaseManagement/Sync-TcmTestCase.ps1
function Sync-TcmTestCase { <# .SYNOPSIS Synchronizes test cases between local YAML files and Azure DevOps. .DESCRIPTION Synchronizes test case data between local YAML files and Azure DevOps work items. Supports bidirectional synchronization, push-only, and pull-only operations. Automatically detects sync status and handles conflicts based on the specified resolution strategy. The function compares content hashes to determine if local and remote versions differ, and performs the appropriate sync operation based on the direction and conflict resolution settings. .PARAMETER InputObject The local test case to synchronize. Accepts: - Test case ID (string) - e.g., "TC001" - File path (string) - relative or absolute path to YAML file - Test case object (hashtable) - from Get-TcmTestCase Accepts pipeline input by value or property name. If not specified, synchronizes all test cases found by Get-TcmTestCase. .PARAMETER Direction Direction of synchronization: - Bidirectional: Push local changes and pull remote changes (default) - ToRemote: Only push local changes to Azure DevOps - FromRemote: Only pull changes from Azure DevOps .PARAMETER TestCasesRoot Root directory containing test case YAML files. If not specified, uses the current directory or searches parent directories for .tcm-config.yaml. .PARAMETER ConflictResolution How to handle conflicts when both local and remote versions have changes: - Manual: Stop and require manual resolution (default) - LocalWins: Use local version, overwrite remote - RemoteWins: Use remote version, overwrite local - LatestWins: Use the version with the most recent modification date .PARAMETER WhatIf Shows what would happen if the cmdlet runs without actually performing the sync operations. .EXAMPLE PS C:\> Sync-TcmTestCase -InputObject "TC001" Synchronizes test case TC001 bidirectionally using default settings. .EXAMPLE PS C:\> Get-TcmTestCase -Id "TC*" | Sync-TcmTestCase -Direction ToRemote Gets all test cases matching "TC*" and pushes them to Azure DevOps. .EXAMPLE PS C:\> Sync-TcmTestCase -InputObject "authentication/TC001-login.yaml" -Direction FromRemote -ConflictResolution RemoteWins Pulls the latest version from Azure DevOps for the specified file, using remote version in case of conflicts. .EXAMPLE PS C:\> Sync-TcmTestCase -WhatIf Shows what sync operations would be performed without making any changes. .EXAMPLE PS C:\> Sync-TcmTestCase Synchronizes all test cases bidirectionally using default settings. .INPUTS System.String System.Collections.Hashtable Accepts test case IDs, file paths, or test case objects from the pipeline. .OUTPUTS None. The function displays progress and results to the console. .NOTES - Requires a valid .tcm-config.yaml configuration file. - Azure DevOps credentials must be configured for the target collection and project. - Sync operations are atomic per test case to prevent partial updates. - Use -WhatIf to preview changes before executing. - Conflict resolution strategies only apply when both versions have changes. .LINK Get-TcmTestCase .LINK Resolve-TcmTestCaseConflict .LINK New-TcmConfig #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias("Path", "Id", "TestCaseId", "WorkItemId")] $InputObject, [ValidateSet('Bidirectional', 'ToRemote', 'FromRemote')] [string] $Direction = 'Bidirectional', [string] $TestCasesRoot, [ValidateSet('Manual', 'LocalWins', 'RemoteWins', 'LatestWins')] [string] $ConflictResolution = 'Manual' ) begin { # Get configuration $config = Get-TcmTestCaseConfig -TestCasesRoot $TestCasesRoot # Override conflict resolution from config if not specified if ($PSBoundParameters.ContainsKey('ConflictResolution') -eq $false) { $ConflictResolution = $config.sync.conflictResolution } # Override direction from config if not specified if ($PSBoundParameters.ContainsKey('Direction') -eq $false) { $Direction = $config.sync.direction } $stats = @{ Processed = 0 Synced = 0 Conflicts = 0 Errors = 0 Skipped = 0 } Write-Verbose "Starting sync with direction: $Direction, conflict resolution: $ConflictResolution" } process { foreach ($resolved in ($InputObject | Resolve-TcmTestCaseFilePathInput -TestCasesRoot $config.TestCasesRoot)) { $testCaseId = $resolved.Id $stats.Processed++ try { Write-Verbose "Syncing test case '$testCaseId'..." # Determine sync status $syncStatus = Get-TcmTestCaseSyncStatus -Id $testCaseId -Config $config Write-Verbose "Test case '$testCaseId' status: $syncStatus" switch ($syncStatus) { 'synced' { Write-Host "✓ Test case '$testCaseId' is already synced" -ForegroundColor Green $stats.Synced++ } 'new-local' { if ($Direction -in @('Bidirectional', 'ToRemote')) { if ($PSCmdlet.ShouldProcess("Test case '$testCaseId'", "Push to Azure DevOps")) { Write-Host "→ Pushing new test case '$testCaseId' to Azure DevOps..." -ForegroundColor Cyan Sync-TcmTestCaseToRemote -InputObject $testCaseId -TestCasesRoot $config.TestCasesRoot $stats.Synced++ } } else { Write-Host "○ Skipping test case '$testCaseId' (new local, direction: $Direction)" -ForegroundColor Yellow $stats.Skipped++ } } 'local-changes' { if ($Direction -in @('Bidirectional', 'ToRemote')) { if ($PSCmdlet.ShouldProcess("Test case '$testCaseId'", "Push changes to Azure DevOps")) { Write-Host "→ Pushing changes for test case '$testCaseId' to Azure DevOps..." -ForegroundColor Cyan Sync-TcmTestCaseToRemote -InputObject $testCaseId -TestCasesRoot $config.TestCasesRoot $stats.Synced++ } } else { Write-Host "○ Skipping test case '$testCaseId' (local changes, direction: $Direction)" -ForegroundColor Yellow $stats.Skipped++ } } 'remote-changes' { if ($Direction -in @('Bidirectional', 'FromRemote')) { if ($PSCmdlet.ShouldProcess("Test case '$testCaseId'", "Pull changes from Azure DevOps")) { Write-Host "← Pulling changes for test case '$testCaseId' from Azure DevOps..." -ForegroundColor Cyan Sync-TcmTestCaseFromRemote -Id $testCaseId -TestCasesRoot $config.TestCasesRoot $stats.Synced++ } } else { Write-Host "○ Skipping test case '$testCaseId' (remote changes, direction: $Direction)" -ForegroundColor Yellow $stats.Skipped++ } } 'new-remote' { if ($Direction -in @('Bidirectional', 'FromRemote')) { if ($PSCmdlet.ShouldProcess("Test case '$testCaseId'", "Pull from Azure DevOps")) { Write-Host "← Pulling test case '$testCaseId' from Azure DevOps..." -ForegroundColor Cyan Sync-TcmTestCaseFromRemote -Id $testCaseId -TestCasesRoot $config.TestCasesRoot $stats.Synced++ } } else { Write-Host "○ Skipping test case '$testCaseId' (new remote, direction: $Direction)" -ForegroundColor Yellow $stats.Skipped++ } } 'conflict' { $stats.Conflicts++ if ($ConflictResolution -eq 'Manual') { Write-Warning "⚠ Conflict detected for test case '$testCaseId'. Local and remote versions have diverged. Run 'Resolve-TcmTestCaseConflict -Id $testCaseId' to resolve manually, or specify a different ConflictResolution strategy." } else { if ($PSCmdlet.ShouldProcess("Test case '$testCaseId'", "Resolve conflict using $ConflictResolution")) { Write-Host "⚠ Resolving conflict for test case '$testCaseId' using strategy: $ConflictResolution" -ForegroundColor Yellow $resolveParams = @{ Id = $testCaseId Strategy = $ConflictResolution TestCasesRoot = $config.TestCasesRoot } Resolve-TcmTestCaseConflict @resolveParams $stats.Synced++ } } } default { Write-Warning "Unknown sync status '$syncStatus' for test case '$testCaseId'" $stats.Errors++ } } } catch { Write-Error "Failed to sync test case '$testCaseId': $($_.Exception.Message)" $stats.Errors++ } } } end { # Display summary Write-Host "`nSync Summary:" -ForegroundColor Cyan Write-Host " Processed: $($stats.Processed)" -ForegroundColor White Write-Host " Synced: $($stats.Synced)" -ForegroundColor Green Write-Host " Conflicts: $($stats.Conflicts)" -ForegroundColor Yellow Write-Host " Skipped: $($stats.Skipped)" -ForegroundColor Gray Write-Host " Errors: $($stats.Errors)" -ForegroundColor Red } } |