Public/Invoke-DotNetTestHarvesting.ps1
|
function Invoke-DotNetTestHarvesting { <# .SYNOPSIS Harvests .NET test maintainer metadata for a DataMiner Test Package. .DESCRIPTION Thin PowerShell wrapper around qaops-qaframework harvest-dotnet. It installs the orchestrator into a per-user cache outside the repository, passes any GitHub token only through the child-process QAOPS_GITHUB_TOKEN environment variable, and returns a skipped report instead of failing the build when optional maintainer harvesting is unavailable. .PARAMETER TestPackageContentPath TestPackageContent directory that contains TestHarvesting and TestPackagePipeline. .PARAMETER TestAssemblyPath Optional repeatable test assembly path, relative to TestPackageContent or absolute. .PARAMETER RepositoryRoot Optional repository root used by the harvester for source and Git fallback mapping. .PARAMETER ReviewedManifestPath Optional reviewed maintainer manifest path. .PARAMETER GitHubLookup Auto to allow eligible build-time GitHub lookup; Off to disable it. .PARAMETER GitHubOrganizations Optional GitHub owner allow-list for lookup eligibility. .PARAMETER AllowedEmailDomains Optional exact email-domain allow-list. .PARAMETER GitHubToken Optional token passed only as QAOPS_GITHUB_TOKEN to the child process. .PARAMETER TimeBudgetSeconds Harvester time budget; the wrapper adds 60 seconds of process grace time. .OUTPUTS The parsed harvest-dotnet JSON report, or a synthesized skipped report. #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory=$true)][string]$TestPackageContentPath, [Parameter()][string[]]$TestAssemblyPath, [Parameter()][string]$RepositoryRoot, [Parameter()][string]$ReviewedManifestPath, [Parameter()][ValidateSet('Auto','Off')][string]$GitHubLookup = 'Auto', [Parameter()][string[]]$GitHubOrganizations, [Parameter()][string[]]$AllowedEmailDomains, [Parameter()][System.Security.SecureString]$GitHubToken, [Parameter()][ValidateRange(1,[int]::MaxValue)][int]$TimeBudgetSeconds = 120 ) function New-SkippedHarvestReport { param([string]$Code,[string]$Message) Write-Warning $Message [pscustomobject]@{ schemaVersion = 1 command = 'harvest-dotnet' status = 'skipped' sidecarPath = $null assemblies = @() counts = [pscustomobject]@{ tests = 0; declared = 0; reviewedManifest = 0; gitFallback = 0; unresolved = 0; conflicts = 0 } diagnostics = @([pscustomobject]@{ code = $Code; severity = 'Warning'; message = $Message }) } } $paths = Resolve-QAFrameworkContentPath -Path $TestPackageContentPath $resolvedRepositoryRoot = $null if ($PSBoundParameters.ContainsKey('RepositoryRoot') -and -not [string]::IsNullOrWhiteSpace($RepositoryRoot)) { try { $resolvedRepositoryRoot = (Resolve-Path -LiteralPath $RepositoryRoot -ErrorAction Stop).ProviderPath } catch { return New-SkippedHarvestReport -Code 'invalidRepositoryRoot' -Message 'Skipped .NET maintainer harvesting because the repository root path could not be resolved.' } } $resolvedReviewedManifestPath = $null if ($PSBoundParameters.ContainsKey('ReviewedManifestPath') -and -not [string]::IsNullOrWhiteSpace($ReviewedManifestPath)) { try { $resolvedReviewedManifestPath = (Resolve-Path -LiteralPath $ReviewedManifestPath -ErrorAction Stop).ProviderPath } catch { return New-SkippedHarvestReport -Code 'invalidReviewedManifest' -Message 'Skipped .NET maintainer harvesting because the reviewed maintainer manifest path could not be resolved.' } } if ($null -eq (Get-Command -Name dotnet -ErrorAction SilentlyContinue)) { return New-SkippedHarvestReport -Code 'dotnetMissing' -Message 'Skipped .NET maintainer harvesting because dotnet is not available.' } $cacheBase = if (-not [string]::IsNullOrWhiteSpace($env:LOCALAPPDATA)) { $env:LOCALAPPDATA } else { [System.IO.Path]::GetTempPath() } $toolDir = Join-Path (Join-Path (Join-Path $cacheBase 'Skyline') 'QAOps') 'qaops-qaframework-tool' try { if (-not (Test-Path -LiteralPath $toolDir -PathType Container)) { New-Item -Path $toolDir -ItemType Directory -Force -ErrorAction Stop | Out-Null } } catch { return New-SkippedHarvestReport -Code 'toolCacheUnavailable' -Message 'Skipped .NET maintainer harvesting because the per-user tool cache is unavailable.' } $manifestPath = Join-Path (Join-Path $toolDir '.config') 'dotnet-tools.json' $mutex = $null $hasMutex = $false try { try { $mutex = New-Object System.Threading.Mutex($false, 'Global\Skyline.QAOps.QAFrameworkOrchestrator.ToolCache') } catch { return New-SkippedHarvestReport -Code 'toolLockUnavailable' -Message 'Skipped .NET maintainer harvesting because the tool-cache lock is unavailable.' } $lockTimeoutSeconds = 60 if (-not [string]::IsNullOrWhiteSpace($env:QAOPS_QAFRAMEWORK_TOOL_LOCK_TIMEOUT_SECONDS)) { [int]::TryParse($env:QAOPS_QAFRAMEWORK_TOOL_LOCK_TIMEOUT_SECONDS, [ref]$lockTimeoutSeconds) | Out-Null; if ($lockTimeoutSeconds -lt 0) { $lockTimeoutSeconds = 0 } } if ($lockTimeoutSeconds -eq 0) { $hasMutex = $false } else { try { $hasMutex = $mutex.WaitOne([TimeSpan]::FromSeconds($lockTimeoutSeconds)) } catch [System.Threading.AbandonedMutexException] { $hasMutex = $true } catch { $hasMutex = $false } } if ($hasMutex) { try { $null = Install-QAFrameworkTool -PipelineDirectory $toolDir -TimeoutSeconds ([Math]::Max(120, $TimeBudgetSeconds + 60)) } catch { if (Test-Path -LiteralPath $manifestPath -PathType Leaf) { Write-Information "Using cached QAFramework orchestrator after install/update failed." } else { return New-SkippedHarvestReport -Code 'toolInstallFailed' -Message 'Skipped .NET maintainer harvesting because the QAFramework orchestrator tool could not be installed or restored.' } } } elseif (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { return New-SkippedHarvestReport -Code 'toolInstallContention' -Message 'Skipped .NET maintainer harvesting because another process held the tool-cache lock and no cached tool exists yet.' } } catch { return New-SkippedHarvestReport -Code 'toolInstallFailed' -Message 'Skipped .NET maintainer harvesting because the QAFramework orchestrator tool cache could not be prepared.' } finally { if ($hasMutex -and $mutex) { try { $mutex.ReleaseMutex() | Out-Null } catch {} } if ($mutex) { $mutex.Dispose() } } $arguments = @('tool','run','qaops-qaframework','--','harvest-dotnet','--content',$paths.ContentPath,'--github-lookup',$GitHubLookup.ToLowerInvariant(),'--time-budget-seconds',[string]$TimeBudgetSeconds,'--json') foreach ($assembly in @($TestAssemblyPath)) { if (-not [string]::IsNullOrWhiteSpace($assembly)) { if ([System.IO.Path]::IsPathRooted($assembly)) { $resolvedAssembly = [System.IO.Path]::GetFullPath($assembly) } else { $resolvedAssembly = [System.IO.Path]::GetFullPath((Join-Path $paths.ContentPath $assembly)) } $arguments += @('--assembly',$resolvedAssembly) } } if ($resolvedRepositoryRoot) { $arguments += @('--repository-root',$resolvedRepositoryRoot) } if ($resolvedReviewedManifestPath) { $arguments += @('--reviewed-manifest',$resolvedReviewedManifestPath) } if ($GitHubOrganizations -and @($GitHubOrganizations).Count -gt 0) { $arguments += @('--github-orgs',($GitHubOrganizations -join ',')) } if ($AllowedEmailDomains -and @($AllowedEmailDomains).Count -gt 0) { $arguments += @('--allowed-email-domains',($AllowedEmailDomains -join ',')) } $tokenPlain = $null if ($PSBoundParameters.ContainsKey('GitHubToken') -and $null -ne $GitHubToken) { $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($GitHubToken) try { $tokenPlain = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) } finally { if ($bstr -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } } } elseif (-not [string]::IsNullOrWhiteSpace($env:GITHUB_TOKEN)) { $tokenPlain = $env:GITHUB_TOKEN } $envOverrides = @{} if (-not [string]::IsNullOrWhiteSpace($tokenPlain)) { $envOverrides['QAOPS_GITHUB_TOKEN'] = $tokenPlain } try { $result = Invoke-QAFrameworkDotNet -Arguments $arguments -WorkingDirectory $toolDir -Environment $envOverrides -TimeoutSeconds ($TimeBudgetSeconds + 60) } catch { return New-SkippedHarvestReport -Code 'toolLaunchFailed' -Message 'Skipped .NET maintainer harvesting because the QAFramework orchestrator tool could not be started.' } finally { $tokenPlain = $null } if ($result.TimedOut) { return New-SkippedHarvestReport -Code 'toolTimeout' -Message 'Skipped .NET maintainer harvesting because harvest-dotnet timed out.' } if ($result.ExitCode -ne 0) { $combined = (($result.StdErr + "`n" + $result.StdOut) | Out-String) if ($combined -match 'unrecognized command|No command.*harvest-dotnet|Required command was not provided') { return New-SkippedHarvestReport -Code 'toolTooOld' -Message 'Skipped .NET maintainer harvesting because the installed QAFramework orchestrator does not support harvest-dotnet yet.' } return New-SkippedHarvestReport -Code 'toolRunFailed' -Message 'Skipped .NET maintainer harvesting because harvest-dotnet failed.' } try { return ($result.StdOut.Trim() | ConvertFrom-Json -ErrorAction Stop) } catch { return New-SkippedHarvestReport -Code 'nonJsonOutput' -Message 'Skipped .NET maintainer harvesting because harvest-dotnet did not return a valid JSON report.' } } |