src/PowerShell/ExecSelection.ps1
|
# cdp PowerShell domain: ExecSelection.ps1 # Loaded by src/cdp.psm1; do not dot-source peer files. function New-CdpExecPlanItem { param( [object]$Project, [string]$Name, [AllowEmptyString()][string]$RawPath, [AllowEmptyString()][string]$ResolvedPath, [string]$Status, [AllowEmptyString()][string]$Error = '' ) [PSCustomObject]@{ Name = $Name RawPath = $RawPath ResolvedPath = $ResolvedPath Project = $Project Status = $Status ExitCode = $null ElapsedMs = 0 Stdout = '' Stderr = '' Error = $Error } } function ConvertTo-CdpExecProjectPlanItem { param([Parameter(Mandatory = $true)][object]$Project) $resolution = Resolve-CdpProjectPath -Project $Project $status = 'planned' $error = '' if ($Project.enabled -ne $true) { $status = 'disabled_project' $error = 'Project is disabled.' } elseif ($resolution.ErrorCode) { $status = 'path_profile_invalid' $error = $resolution.ErrorMessage } elseif (-not (Test-Path -LiteralPath $resolution.ResolvedPath -PathType Container)) { $status = 'path_missing' $error = 'Resolved project path does not exist.' } New-CdpExecPlanItem -Project $Project -Name ([string]$Project.name) ` -RawPath ([string]$resolution.RawPath) -ResolvedPath ([string]$resolution.ResolvedPath) ` -Status $status -Error $error } function Get-CdpExecExplicitItems { param([string[]]$Names, [object[]]$Projects) $items = @() foreach ($name in $Names) { $matches = @($Projects | Where-Object { [string]::Equals([string]$_.name, $name, [StringComparison]::Ordinal) }) if ($matches.Count -eq 0) { throw "Exec project '$name' not found." } if ($matches.Count -gt 1) { throw "Exec project '$name' is ambiguous." } $items += ConvertTo-CdpExecProjectPlanItem -Project $matches[0] } $items } function Get-CdpExecTagItems { param([string]$Tag, [object[]]$Projects) $comparison = [StringComparison]::OrdinalIgnoreCase $matches = @($Projects | Where-Object { $_.enabled -eq $true -and @(Get-CdpProjectStringList -Project $_ -PropertyName 'tags' | Where-Object { [string]::Equals([string]$_, $Tag, $comparison) }).Count -gt 0 }) if ($matches.Count -eq 0) { throw "Exec tag '@$Tag' matched no enabled projects." } @($matches | ForEach-Object { ConvertTo-CdpExecProjectPlanItem -Project $_ }) } function Get-CdpExecAllItems { param([object[]]$Projects) $matches = @($Projects | Where-Object { $_.enabled -eq $true }) if ($matches.Count -eq 0) { throw 'Exec --all matched no enabled projects.' } @($matches | ForEach-Object { ConvertTo-CdpExecProjectPlanItem -Project $_ }) } function ConvertTo-CdpExecWorkspaceReference { param([AllowNull()][object]$Reference) if ($Reference -is [string] -or -not (Test-CdpWorkspaceObject $Reference)) { return $Reference } [PSCustomObject]@{ name = Get-CdpWorkspaceReferenceValue -Reference $Reference -Name 'name' rootPath = Get-CdpWorkspaceReferenceValue -Reference $Reference -Name 'rootPath' } } function ConvertTo-CdpExecWorkspaceItem { param([Parameter(Mandatory = $true)][object]$Resolution) $statusMap = @{ 'ok'='planned'; 'legacy'='planned'; 'renamed'='planned' 'missing-project'='missing_project'; 'ambiguous-project'='ambiguous_project' 'disabled-project'='disabled_project'; 'invalid-path-profile'='path_profile_invalid' 'missing-path'='path_missing'; 'invalid-reference'='missing_project' } $status = $statusMap[[string]$Resolution.Status] if (-not $status) { $status = 'missing_project' } $error = if ($status -eq 'planned') { '' } else { "Workspace reference status: $($Resolution.Status)." } New-CdpExecPlanItem -Project $Resolution.Project -Name ([string]$Resolution.Name) ` -RawPath ([string]$Resolution.RawPath) -ResolvedPath ([string]$Resolution.ResolvedPath) ` -Status $status -Error $error } function Get-CdpExecWorkspaceItems { param([string]$Name, [string]$ConfigPath, [object[]]$Projects) $workspacePath = Get-CdpWorkspacesPath -ConfigPath $ConfigPath $workspaces = @((Read-CdpWorkspaceDocument -Path $workspacePath).Value) $matches = @($workspaces | Where-Object { [string]::Equals([string]$_.name, $Name, [StringComparison]::Ordinal) }) if ($matches.Count -eq 0) { throw "Workspace '$Name' not found." } if ($matches.Count -gt 1) { throw "Workspace '$Name' is ambiguous." } $referenceValue = $matches[0].projects if (-not ($referenceValue -is [System.Array])) { throw "Workspace '$Name' projects must be a JSON array." } $references = @($referenceValue) if ($references.Count -eq 0) { throw "Workspace '$Name' does not contain projects." } foreach ($reference in $references) { $stableReference = ConvertTo-CdpExecWorkspaceReference -Reference $reference $resolution = Resolve-CdpWorkspaceReference -Reference $stableReference -Projects $Projects -WorkspaceOpen '' ConvertTo-CdpExecWorkspaceItem -Resolution $resolution } } function Assert-CdpExecProjectConfigArray { param([Parameter(Mandatory = $true)][string]$Path) $content = Get-Content -LiteralPath $Path -Raw -Encoding UTF8 -ErrorAction Stop if (-not $content.TrimStart().StartsWith('[')) { throw 'Project configuration must be a JSON array.' } } function Select-CdpUniqueExecItems { param([object[]]$Items) $identities = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::Ordinal) foreach ($item in $Items) { if ([string]::IsNullOrEmpty([string]$item.RawPath) -or $identities.Add([string]$item.RawPath)) { $item } } } function Resolve-CdpExecExecutable { param([Parameter(Mandatory = $true)][string]$Command) $resolved = @(Get-Command -Name $Command -CommandType Application -ErrorAction SilentlyContinue) if ($resolved.Count -eq 0) { throw "Exec command '$Command' was not found as a native executable." } $path = [string]$resolved[0].Path if ([string]::IsNullOrWhiteSpace($path)) { $path = [string]$resolved[0].Source } if ([string]::IsNullOrWhiteSpace($path)) { throw "Exec command '$Command' has no executable path." } $path } function Get-CdpExecSelectorDocument { param([Parameter(Mandatory = $true)][object]$Invocation) $value = switch ($Invocation.ExecSelectorKind) { 'projects' { @($Invocation.ExecProjectNames) } 'tag' { [string]$Invocation.ExecTag } 'workspace' { [string]$Invocation.ExecWorkspace } 'all' { $null } } [PSCustomObject]@{ kind = [string]$Invocation.ExecSelectorKind; value = $value } } function New-CdpExecPlan { param([Parameter(Mandatory = $true)][object]$Invocation) $configPath = $Invocation.ConfigPath if ([string]::IsNullOrWhiteSpace($configPath)) { $configPath = Get-DefaultConfigPath } $config = Get-CdpProjectConfig -ConfigPath $configPath Assert-CdpExecProjectConfigArray -Path $config.Path $projects = @($config.Projects) $items = switch ($Invocation.ExecSelectorKind) { 'projects' { @(Get-CdpExecExplicitItems -Names @($Invocation.ExecProjectNames) -Projects $projects) } 'tag' { @(Get-CdpExecTagItems -Tag $Invocation.ExecTag -Projects $projects) } 'workspace' { @(Get-CdpExecWorkspaceItems -Name $Invocation.ExecWorkspace -ConfigPath $config.Path -Projects $projects) } 'all' { @(Get-CdpExecAllItems -Projects $projects) } default { throw 'Exec selector is invalid.' } } $jobsDefault = [Math]::Max(1, [Math]::Min(4, [Environment]::ProcessorCount)) $jobs = if ($Invocation.ThrottleLimit -gt 0) { $Invocation.ThrottleLimit } else { Get-CdpStatusIntegerSetting -Name 'CDP_EXEC_CONCURRENCY' -Default $jobsDefault -Minimum 1 -Maximum 16 } $timeout = if ($Invocation.TimeoutSeconds -gt 0) { $Invocation.TimeoutSeconds } else { Get-CdpStatusIntegerSetting -Name 'CDP_EXEC_TIMEOUT_SECONDS' -Default 300 -Minimum 1 -Maximum 3600 } [PSCustomObject]@{ ConfigPath = $config.Path Selector = Get-CdpExecSelectorDocument -Invocation $Invocation Command = [string]$Invocation.ExecCommand Executable = Resolve-CdpExecExecutable -Command $Invocation.ExecCommand Arguments = @($Invocation.ExecArguments) Jobs = [int]$jobs TimeoutSeconds = [int]$timeout FailFast = [bool]$Invocation.FailFast DryRun = [bool]$Invocation.DryRun Items = @(Select-CdpUniqueExecItems -Items @($items)) } } |