functions/export-adousercommentmapping.ps1


<#
    .SYNOPSIS
        Exports a JSON template mapping of distinct source comment authors to target users for later migration.
     
    .DESCRIPTION
        Scans all work item comments in a source Azure DevOps project, collects distinct author email addresses (unique case-insensitive),
        and produces a JSON file with entries:
            - sourceEmail : the detected author (from comment createdBy uniqueName or email)
            - targetEmail : initially empty (to be manually filled before running full migration)
            - targetPat : initially empty (optional per-user PAT if needed during migration)
        A final node named '@default_user' is appended to serve as fallback mapping when a source user is not explicitly mapped.
         
        The resulting JSON array can be edited and then supplied to future migration logic to impersonate or attribute comments
        according to the mapping (functionality to consume this map is implemented separately).
    .PARAMETER Organization
        Source Azure DevOps organization name.
    .PARAMETER ProjectName
        Source Azure DevOps project name.
    .PARAMETER Token
        Personal Access Token with read access to Work Items & Comments.
    .PARAMETER ApiVersion
        API version to use (defaults to module default if not provided).
    .PARAMETER OutputPath
        Destination path for JSON file. If only a directory is provided, default filename 'ado.commentUserMapping.json' is used.
    .PARAMETER Force
        Overwrite existing output file if present.
    .EXAMPLE
        Export-ADOUserCommentMapping -Organization org -ProjectName Sample -Token $pat -OutputPath C:\temp\comment-map.json
         
        Exports mapping JSON template to C:\temp\comment-map.json
    .EXAMPLE
        Export-ADOUserCommentMapping -Organization org -ProjectName Sample -Token $pat -Verbose
         
        Writes mapping into current directory as ado.commentUserMapping.json
    .NOTES
        Author: Oleksandr Nikolaiev (@onikolaiev)
#>

function Export-ADOUserCommentMapping {
    [CmdletBinding()] param(
        [Parameter(Mandatory)][string]$Organization,
        [Parameter(Mandatory)][string]$ProjectName,
        [Parameter(Mandatory)][string]$Token,
        [Parameter()][string]$ApiVersion = $Script:ADOApiVersion,
        [Parameter()][string]$OutputPath = (Join-Path (Get-Location) 'ado.commentUserMapping.json'),
        [Parameter()][switch]$Force
    )

    begin {
        $msg = "Building comment author mapping for project '{0}' in org '{1}'..." -f $ProjectName, $Organization
        Write-PSFMessage -Level Host -Message $msg
    }

    process {
        try {
            $allIds = @()
            # Enumerate work item ids
            try {
                $sourceItems = Get-ADOSourceWorkItemsList -SourceOrganization $Organization -SourceProjectName $ProjectName -SourceToken $Token -ApiVersion $ApiVersion
                if ($sourceItems) { $allIds = $sourceItems.'System.Id' | Sort-Object -Unique }
            } catch {
                $warn = "Failed to enumerate work items. Error: {0}" -f $_.Exception.Message
                Write-PSFMessage -Level Warning -Message $warn
                return
            }
            if (-not $allIds -or $allIds.Count -eq 0) {
                Write-PSFMessage -Level Warning -Message 'No work items found; mapping will contain only the default node.'
            }
            $authors = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
            foreach ($wid in $allIds) {
                try {
                    $comments = Get-ADOWorkItemCommentList -Organization $Organization -Project $ProjectName -Token $Token -WorkItemId $wid -All -Order asc -ApiVersion $ApiVersion
                    if ($comments) {
                        foreach ($c in $comments) {
                            $createdBy = $null
                            if ($c.createdBy) {
                                if ($c.createdBy.PSObject.Properties['uniqueName']) { $createdBy = $c.createdBy.uniqueName }
                                elseif ($c.createdBy.PSObject.Properties['mailAddress']) { $createdBy = $c.createdBy.mailAddress }
                                elseif ($c.createdBy.PSObject.Properties['displayName']) { $createdBy = $c.createdBy.displayName }
                            }
                            $isEmptyAuthor = [string]::IsNullOrWhiteSpace($createdBy)
                            if (-not $isEmptyAuthor) { $null = $authors.Add($createdBy.Trim()) }
                        }
                    }
                } catch {
                    Write-PSFMessage -Level Verbose -Message ("Failed to load comments for work item {0}" -f $wid)
                }
            }
            $list = foreach ($a in ($authors | Sort-Object)) {
                [pscustomobject]@{ sourceEmail = $a; targetEmail = ''; targetPat = '' }
            }
            $list += [pscustomobject]@{ sourceEmail = '@default_user'; targetEmail = ''; targetPat = '' }
            $json = $list | ConvertTo-Json -Depth 4
            $outFile = $OutputPath
            if (Test-Path -LiteralPath $outFile -PathType Container) { $outFile = Join-Path $outFile 'ado.commentUserMapping.json' }
            if ((Test-Path -LiteralPath $outFile) -and -not $Force) {
                throw ("Output file '{0}' already exists. Use -Force to overwrite." -f $outFile)
            }
            $json | Out-File -LiteralPath $outFile -Encoding UTF8 -Force
            Write-PSFMessage -Level Host -Message ("Comment author mapping exported to '{0}'. Entries: {1}." -f $outFile, $list.Count)
        } catch {
            Write-PSFMessage -Level Error -Message ("Export failed. Error: {0}" -f $_.Exception.Message)
        }
    }
}