.github/templates/scripts/Find-ModuleManifest.ps1
|
<#
.SYNOPSIS Finds and validates a PowerShell module manifest file. .DESCRIPTION Locates the module manifest (.psd1) file for a given module with robust validation. Implements a hybrid approach: 1. Preferred: Searches for exact match "<ModuleName>.psd1" 2. Fallback: Searches for any "*.psd1" file with warning 3. Validates the manifest contains expected module metadata .PARAMETER ModuleName The name of the module to find the manifest for. .PARAMETER SearchPath The directory path to search in. Defaults to current directory. .PARAMETER Strict If specified, only accepts exact match "<ModuleName>.psd1" and fails on multiple .psd1 files. .OUTPUTS PSCustomObject with properties: - ManifestPath: Full path to the manifest file (or $null if not found) - IsValid: Boolean indicating if manifest passed validation - ValidationMethod: String indicating how manifest was found (Exact, Fallback, or NotFound) - Warnings: Array of warning messages - Errors: Array of error messages .EXAMPLE $result = .\Find-ModuleManifest.ps1 -ModuleName "MyModule" if ($result.IsValid) { Write-Host "Found manifest: $($result.ManifestPath)" } .EXAMPLE $result = .\Find-ModuleManifest.ps1 -ModuleName "MyModule" -Strict # Will fail if multiple .psd1 files exist or name doesn't match exactly #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$ModuleName, [Parameter(Mandatory = $false)] [string]$SearchPath = '.', [Parameter(Mandatory = $false)] [switch]$Strict ) $ErrorActionPreference = 'Stop' # Initialize result object $result = [PSCustomObject]@{ ManifestPath = $null IsValid = $false ValidationMethod = 'NotFound' Warnings = @() Errors = @() } Write-Verbose "Searching for module manifest in: $SearchPath" Write-Verbose "Module name: $ModuleName" # Step 1: Try exact match "<ModuleName>.psd1" $exactManifestPath = Join-Path $SearchPath "$ModuleName.psd1" $exactManifestExists = Test-Path $exactManifestPath if ($exactManifestExists) { Write-Verbose "Found exact match: $exactManifestPath" $result.ManifestPath = (Get-Item $exactManifestPath).FullName $result.ValidationMethod = 'Exact' } else { Write-Verbose "Exact match not found. Searching for any *.psd1 files..." # Step 2: Fallback to any *.psd1 file $allManifests = Get-ChildItem -Path $SearchPath -Filter '*.psd1' -File -ErrorAction SilentlyContinue if ($allManifests.Count -eq 0) { $result.Errors += "No .psd1 files found in: $SearchPath" Write-Error "No module manifest files found in $SearchPath" return $result } if ($allManifests.Count -gt 1) { $manifestList = ($allManifests.Name | ForEach-Object { " - $_" }) -join "`n" $warningMsg = "Multiple .psd1 files found in $SearchPath`:`n$manifestList" $result.Warnings += $warningMsg if ($Strict) { $result.Errors += "Strict mode: Multiple .psd1 files found but expected exactly one named '$ModuleName.psd1'" Write-Error $result.Errors[-1] return $result } Write-Warning $warningMsg Write-Warning "Using first file: $($allManifests[0].Name)" } $result.ManifestPath = $allManifests[0].FullName $result.ValidationMethod = 'Fallback' if (-not $Strict) { $result.Warnings += "Using fallback manifest discovery (not exact match): $($allManifests[0].Name)" } } # Step 3: Validate the manifest file Write-Verbose "Validating manifest: $($result.ManifestPath)" try { # First: Test if it's valid PowerShell syntax $null = Test-ModuleManifest -Path $result.ManifestPath -ErrorAction Stop -WarningAction SilentlyContinue # Second: Load raw manifest data (Import-PowerShellDataFile is more reliable than Test-ModuleManifest for properties) $manifestData = Import-PowerShellDataFile -Path $result.ManifestPath -ErrorAction Stop # Check for essential module properties $hasModuleVersion = -not [string]::IsNullOrWhiteSpace($manifestData.ModuleVersion) $hasRootModule = -not [string]::IsNullOrWhiteSpace($manifestData.RootModule) $hasGuid = -not [string]::IsNullOrWhiteSpace($manifestData.GUID) if (-not $hasModuleVersion) { $result.Errors += "Manifest missing ModuleVersion" } if (-not $hasRootModule) { $result.Warnings += "Manifest missing RootModule (may be a manifest-only module)" } if (-not $hasGuid) { $result.Warnings += "Manifest missing GUID" } # Check if module name matches (if we used fallback discovery) # Note: Raw manifest doesn't have a 'Name' property, so we check filename instead $manifestBaseName = [System.IO.Path]::GetFileNameWithoutExtension($result.ManifestPath) if ($result.ValidationMethod -eq 'Fallback' -and $manifestBaseName -ne $ModuleName) { $warnMsg = "Manifest filename '$manifestBaseName.psd1' does not match expected name '$ModuleName.psd1'" $result.Warnings += $warnMsg if ($Strict) { $result.Errors += "Strict mode: $warnMsg" $result.IsValid = $false Write-Error $result.Errors[-1] return $result } Write-Warning $warnMsg } # If no errors, mark as valid $result.IsValid = $result.Errors.Count -eq 0 Write-Verbose "Manifest validation complete. Valid: $($result.IsValid)" } catch { $result.Errors += "Manifest validation failed: $($_.Exception.Message)" $result.IsValid = $false Write-Error "Failed to validate manifest: $($_.Exception.Message)" } # Output warnings if any foreach ($warning in $result.Warnings) { Write-Warning $warning } return $result |